Comptabilite (Super Admin) : - ComptabiliteController avec 7 exports CSV/JSON compatibles SAGE (journal ventes, grand livre, FEC, balance agee, reglements, commissions Stripe 1.5%+0.25E, couts services) - Export PDF via ComptaPdf (FPDF) avec bloc legal pre-rempli, tableau pagine, champ signature DocuSeal - Signature electronique DocuSeal + callback + envoi email signe avec template dedie (compta_export_signed.html.twig) - Rapport financier public (RapportFinancierPdf) : recettes par service, depenses (Stripe, infra, prestataires), bilan excedent/deficit - Codes comptables clients EC-XXXX (plus de 411xxx) Prestataires (Super Admin) : - Entite Prestataire (raisonSociale, siret, email, phone, adresse) - Entite FacturePrestataire (numFacture, montantHt, montantTtc, year, month, isPaid, PDF via Vich) - CRUD complet avec recherche SIRET via proxy API data.gouv.fr - Commande cron app:reminder:factures-prestataire (5 du mois) - Factures prestataires integrees dans export couts services - Sidebar Super Admin : entree Prestataires + Comptabilite Stats (/admin/stats) : - Cout prestataire dynamique depuis FacturePrestataire - Fusion Infra + Prestataire en "Cout de fonctionnement" - Commission Stripe corrigee (1.5% + 0.25E par transaction) Divers : - DocuSealService::sendComptaForSignature() + getApi() - Customer::generateCodeComptable() format EC-XXXX-XXXXX - Protection double prefixe EC- a la creation client - Bouton regenerer PDF cache quand advert state=accepted - Modals sans script inline (data-modal-open/close dans app.js) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
294 lines
7.3 KiB
PHP
294 lines
7.3 KiB
PHP
<?php
|
|
|
|
namespace App\Entity;
|
|
|
|
use App\Repository\AdvertRepository;
|
|
use Doctrine\Common\Collections\ArrayCollection;
|
|
use Doctrine\Common\Collections\Collection;
|
|
use Doctrine\ORM\Mapping as ORM;
|
|
use Symfony\Component\HttpFoundation\File\File;
|
|
use Vich\UploaderBundle\Mapping\Attribute as Vich;
|
|
|
|
#[ORM\Entity(repositoryClass: AdvertRepository::class)]
|
|
#[Vich\Uploadable]
|
|
class Advert
|
|
{
|
|
public const STATE_CREATED = 'created';
|
|
public const STATE_SEND = 'send';
|
|
public const STATE_ACCEPTED = 'accepted';
|
|
public const STATE_REFUSED = 'refused';
|
|
public const STATE_CANCEL = 'cancel';
|
|
|
|
#[ORM\Id]
|
|
#[ORM\GeneratedValue]
|
|
#[ORM\Column]
|
|
private ?int $id = null;
|
|
|
|
#[ORM\ManyToOne(targetEntity: OrderNumber::class)]
|
|
#[ORM\JoinColumn(nullable: false)]
|
|
private OrderNumber $orderNumber;
|
|
|
|
#[ORM\OneToOne(targetEntity: Devis::class, inversedBy: 'advert')]
|
|
#[ORM\JoinColumn(nullable: true)]
|
|
private ?Devis $devis = null;
|
|
|
|
#[ORM\ManyToOne(targetEntity: Customer::class)]
|
|
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
|
|
private ?Customer $customer = null;
|
|
|
|
#[ORM\Column(length: 128)]
|
|
private string $hmac;
|
|
|
|
#[ORM\Column(length: 20, options: ['default' => 'created'])]
|
|
private string $state = self::STATE_CREATED;
|
|
|
|
#[ORM\Column(type: 'text', nullable: true)]
|
|
private ?string $raisonMessage = null;
|
|
|
|
#[ORM\Column(type: 'decimal', precision: 10, scale: 2, options: ['default' => '0.00'])]
|
|
private string $totalHt = '0.00';
|
|
|
|
#[ORM\Column(type: 'decimal', precision: 10, scale: 2, options: ['default' => '0.00'])]
|
|
private string $totalTva = '0.00';
|
|
|
|
#[ORM\Column(type: 'decimal', precision: 10, scale: 2, options: ['default' => '0.00'])]
|
|
private string $totalTtc = '0.00';
|
|
|
|
#[ORM\Column(length: 255, nullable: true)]
|
|
private ?string $submissionId = null;
|
|
|
|
#[ORM\Column(length: 255, nullable: true)]
|
|
private ?string $stripePaymentId = null;
|
|
|
|
#[ORM\Column(length: 255, nullable: true)]
|
|
private ?string $advertFile = null;
|
|
|
|
#[Vich\UploadableField(mapping: 'advert_pdf', fileNameProperty: 'advertFile')]
|
|
private ?File $advertFileUpload = null;
|
|
|
|
#[ORM\Column]
|
|
private \DateTimeImmutable $createdAt;
|
|
|
|
#[ORM\Column(nullable: true)]
|
|
private ?\DateTimeImmutable $updatedAt = null;
|
|
|
|
/** @var Collection<int, Facture> */
|
|
#[ORM\OneToMany(targetEntity: Facture::class, mappedBy: 'advert')]
|
|
private Collection $factures;
|
|
|
|
/** @var Collection<int, AdvertLine> */
|
|
#[ORM\OneToMany(targetEntity: AdvertLine::class, mappedBy: 'advert', cascade: ['persist', 'remove'], orphanRemoval: true)]
|
|
#[ORM\OrderBy(['pos' => 'ASC'])]
|
|
private Collection $lines;
|
|
|
|
/** @var Collection<int, AdvertPayment> */
|
|
#[ORM\OneToMany(targetEntity: AdvertPayment::class, mappedBy: 'advert', cascade: ['persist', 'remove'])]
|
|
#[ORM\OrderBy(['createdAt' => 'DESC'])]
|
|
private Collection $payments;
|
|
|
|
public function __construct(OrderNumber $orderNumber, string $hmacSecret)
|
|
{
|
|
$this->orderNumber = $orderNumber;
|
|
$this->createdAt = new \DateTimeImmutable();
|
|
$this->factures = new ArrayCollection();
|
|
$this->lines = new ArrayCollection();
|
|
$this->payments = new ArrayCollection();
|
|
$this->hmac = $this->generateHmac($hmacSecret);
|
|
}
|
|
|
|
public function getId(): ?int
|
|
{
|
|
return $this->id;
|
|
}
|
|
|
|
public function getOrderNumber(): OrderNumber
|
|
{
|
|
return $this->orderNumber;
|
|
}
|
|
|
|
public function getDevis(): ?Devis
|
|
{
|
|
return $this->devis;
|
|
}
|
|
|
|
public function setDevis(?Devis $devis): void
|
|
{
|
|
$this->devis = $devis;
|
|
}
|
|
|
|
public function getCustomer(): ?Customer
|
|
{
|
|
return $this->customer;
|
|
}
|
|
|
|
public function setCustomer(?Customer $customer): void
|
|
{
|
|
$this->customer = $customer;
|
|
}
|
|
|
|
public function getHmac(): string
|
|
{
|
|
return $this->hmac;
|
|
}
|
|
|
|
public function getState(): string
|
|
{
|
|
return $this->state;
|
|
}
|
|
|
|
public function setState(string $state): void
|
|
{
|
|
$this->state = $state;
|
|
}
|
|
|
|
public function getRaisonMessage(): ?string
|
|
{
|
|
return $this->raisonMessage;
|
|
}
|
|
|
|
public function setRaisonMessage(?string $raisonMessage): void
|
|
{
|
|
$this->raisonMessage = $raisonMessage;
|
|
}
|
|
|
|
public function getTotalHt(): string
|
|
{
|
|
return $this->totalHt;
|
|
}
|
|
|
|
public function setTotalHt(string $totalHt): void
|
|
{
|
|
$this->totalHt = $totalHt;
|
|
}
|
|
|
|
public function getTotalTva(): string
|
|
{
|
|
return $this->totalTva;
|
|
}
|
|
|
|
public function setTotalTva(string $totalTva): void
|
|
{
|
|
$this->totalTva = $totalTva;
|
|
}
|
|
|
|
public function getTotalTtc(): string
|
|
{
|
|
return $this->totalTtc;
|
|
}
|
|
|
|
public function setTotalTtc(string $totalTtc): void
|
|
{
|
|
$this->totalTtc = $totalTtc;
|
|
}
|
|
|
|
public function getSubmissionId(): ?string
|
|
{
|
|
return $this->submissionId;
|
|
}
|
|
|
|
public function setSubmissionId(?string $submissionId): void
|
|
{
|
|
$this->submissionId = $submissionId;
|
|
}
|
|
|
|
public function getStripePaymentId(): ?string
|
|
{
|
|
return $this->stripePaymentId;
|
|
}
|
|
|
|
public function setStripePaymentId(?string $stripePaymentId): void
|
|
{
|
|
$this->stripePaymentId = $stripePaymentId;
|
|
}
|
|
|
|
public function getAdvertFile(): ?string
|
|
{
|
|
return $this->advertFile;
|
|
}
|
|
|
|
public function setAdvertFile(?string $advertFile): void
|
|
{
|
|
$this->advertFile = $advertFile;
|
|
}
|
|
|
|
public function getAdvertFileUpload(): ?File
|
|
{
|
|
return $this->advertFileUpload;
|
|
}
|
|
|
|
public function setAdvertFileUpload(?File $advertFileUpload): void
|
|
{
|
|
$this->advertFileUpload = $advertFileUpload;
|
|
if (null !== $advertFileUpload) {
|
|
$this->updatedAt = new \DateTimeImmutable();
|
|
}
|
|
}
|
|
|
|
public function getCreatedAt(): \DateTimeImmutable
|
|
{
|
|
return $this->createdAt;
|
|
}
|
|
|
|
public function getUpdatedAt(): ?\DateTimeImmutable
|
|
{
|
|
return $this->updatedAt;
|
|
}
|
|
|
|
public function setUpdatedAt(?\DateTimeImmutable $updatedAt): static
|
|
{
|
|
$this->updatedAt = $updatedAt;
|
|
|
|
return $this;
|
|
}
|
|
|
|
/** @return Collection<int, Facture> */
|
|
public function getFactures(): Collection
|
|
{
|
|
return $this->factures;
|
|
}
|
|
|
|
/** @return Collection<int, AdvertLine> */
|
|
public function getLines(): Collection
|
|
{
|
|
return $this->lines;
|
|
}
|
|
|
|
public function addLine(AdvertLine $line): static
|
|
{
|
|
if (!$this->lines->contains($line)) {
|
|
$this->lines->add($line);
|
|
}
|
|
|
|
return $this;
|
|
}
|
|
|
|
public function removeLine(AdvertLine $line): static
|
|
{
|
|
$this->lines->removeElement($line);
|
|
|
|
return $this;
|
|
}
|
|
|
|
/** @return Collection<int, AdvertPayment> */
|
|
public function getPayments(): Collection
|
|
{
|
|
return $this->payments;
|
|
}
|
|
|
|
public function verifyHmac(string $hmacSecret): bool
|
|
{
|
|
return hash_equals($this->hmac, $this->generateHmac($hmacSecret));
|
|
}
|
|
|
|
private function generateHmac(string $secret): string
|
|
{
|
|
$payload = implode('|', [
|
|
'advert',
|
|
$this->orderNumber->getNumOrder(),
|
|
$this->createdAt->format('Y-m-d\TH:i:s'),
|
|
]);
|
|
|
|
return hash_hmac('sha256', $payload, $secret);
|
|
}
|
|
}
|