Files
crm_ecosplay/src/Entity/Advert.php
Serreau Jovann 8b35e2b6d2 feat: comptabilite + prestataires + rapport financier + stats dynamiques
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>
2026-04-07 23:39:31 +02:00

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);
}
}