fix(logistique) : bon de pesée — cartouche tiers + filtrage des listes contrepartie par site (ERP-208) (#155)
Auto Tag Develop / tag (push) Successful in 14s

## ERP-208 — Fix ticket de pesée

### Bon de pesée (PDF)
Ajout d'un **cartouche bordé en haut à droite** du bon de pesée, contenant le **type de contrepartie** (Client / Fournisseur / Autre, en gras au-dessus) et le **nom du tiers**.
- `WeighingTicket::getCounterpartyName()` + `getCounterpartyTypeLabel()` (testés).
- En-tête du template passé en table 2 colonnes (contrainte Dompdf CSS 2.1).

### Écran de saisie (Ajouter / Modifier)
Les listes **Client / Fournisseur** sont **filtrées sur le site courant** (un tiers est rattaché à un site via les sites de ses adresses) et **rechargées au changement de site**.
- Réutilise le filtre back existant `?siteId[]=` de /clients et /suppliers (aucun changement back sur le filtre).
- Au switch de site : le tiers sélectionné est réinitialisé **uniquement** s'il sort du périmètre du nouveau site.
- Portée limitée au ticket de pesée : les répertoires M1/M2 ne changent pas.

### Tests
- Back : test unitaire `WeighingTicketCounterpartyNameTest` (nom + libellé) ; test PDF existant inchangé.
- Front : specs référentiels + écrans Ajouter/Modifier (673/673).
- Pas de migration, pas de RBAC, pas d'E2E.

### À vérifier en recette
En **modification**, si le tiers d'un ticket n'a pas d'adresse sur le site courant, le select peut s'afficher vide (valeur conservée mais option filtrée).

Reviewed-on: #155
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #155.
This commit is contained in:
2026-06-25 13:02:31 +00:00
committed by Autin
parent f6c556ca1b
commit 086be7b4f0
11 changed files with 718 additions and 23 deletions
@@ -175,6 +175,15 @@ class WeighingTicket implements TimestampableInterface, BlamableInterface
/** Valide : contrepartie + immatriculation + 2 pesees OK, numero attribue (« Terminée »). */
public const string STATUS_VALIDATED = 'VALIDATED';
/** Contrepartie « Client » (M1) — RG-5.03. */
public const string COUNTERPARTY_CLIENT = 'CLIENT';
/** Contrepartie « Fournisseur » (M2) — RG-5.03. */
public const string COUNTERPARTY_FOURNISSEUR = 'FOURNISSEUR';
/** Contrepartie « Autre » (libelle libre) — RG-5.03. */
public const string COUNTERPARTY_AUTRE = 'AUTRE';
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
@@ -195,7 +204,7 @@ class WeighingTicket implements TimestampableInterface, BlamableInterface
/** CLIENT | FOURNISSEUR | AUTRE (RG-5.03) — null tant que brouillon, requis a la validation. Pilote le champ associe obligatoire. */
#[ORM\Column(name: 'counterparty_type', length: 12, nullable: true)]
#[Assert\NotBlank(message: 'La contrepartie (Client / Fournisseur / Autre) est obligatoire.', groups: ['finalize'])]
#[Assert\Choice(choices: ['CLIENT', 'FOURNISSEUR', 'AUTRE'], message: 'Type de contrepartie invalide.')]
#[Assert\Choice(choices: [self::COUNTERPARTY_CLIENT, self::COUNTERPARTY_FOURNISSEUR, self::COUNTERPARTY_AUTRE], message: 'Type de contrepartie invalide.')]
#[Groups(['weighing_ticket:read', 'weighing_ticket:write'])]
private ?string $counterpartyType = null;
@@ -313,7 +322,7 @@ class WeighingTicket implements TimestampableInterface, BlamableInterface
public function validateCounterpartyConsistency(ExecutionContextInterface $context): void
{
switch ($this->counterpartyType) {
case 'CLIENT':
case self::COUNTERPARTY_CLIENT:
if (null === $this->client) {
$context->buildViolation('Le client est obligatoire pour une contrepartie « Client ».')
->atPath('client')
@@ -323,7 +332,7 @@ class WeighingTicket implements TimestampableInterface, BlamableInterface
break;
case 'FOURNISSEUR':
case self::COUNTERPARTY_FOURNISSEUR:
if (null === $this->supplier) {
$context->buildViolation('Le fournisseur est obligatoire pour une contrepartie « Fournisseur ».')
->atPath('supplier')
@@ -333,7 +342,7 @@ class WeighingTicket implements TimestampableInterface, BlamableInterface
break;
case 'AUTRE':
case self::COUNTERPARTY_AUTRE:
if (null === $this->otherLabel || '' === trim($this->otherLabel)) {
$context->buildViolation('Le libellé est obligatoire pour une contrepartie « Autre ».')
->atPath('otherLabel')
@@ -458,6 +467,21 @@ class WeighingTicket implements TimestampableInterface, BlamableInterface
return $this;
}
/**
* Nom du tiers à afficher (cartouche du bon de pesée PDF, ERP-208) : raison
* sociale du client/fournisseur ou libellé libre selon le type de contrepartie
* (RG-5.03). Null si aucune contrepartie cohérente (brouillon).
*/
public function getCounterpartyName(): ?string
{
return match ($this->counterpartyType) {
self::COUNTERPARTY_CLIENT => $this->client?->getCompanyName(),
self::COUNTERPARTY_FOURNISSEUR => $this->supplier?->getCompanyName(),
self::COUNTERPARTY_AUTRE => $this->otherLabel,
default => null,
};
}
public function getImmatriculation(): ?string
{
return $this->immatriculation;