fix(logistique) : bon de pesée — cartouche tiers + filtrage des listes contrepartie par site (ERP-208) #155

Merged
tristan merged 2 commits from fix/erp-208-ticket-pesee into develop 2026-06-25 13:02:32 +00:00
Owner

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).

## 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).
tristan added the type/fixbackfrontM5-Ticket-pesee labels 2026-06-25 12:16:54 +00:00
tristan added 1 commit 2026-06-25 12:16:54 +00:00
fix(logistique) : bon de pesée — cartouche tiers + filtrage des listes contrepartie par site (ERP-208)
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 2m1s
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 3m38s
527e47d822
- PDF : cartouche bordé en haut à droite avec le type (Client/Fournisseur/Autre) et le nom du tiers (getCounterpartyName + getCounterpartyTypeLabel).
- Écran ticket : listes Client/Fournisseur filtrées sur le site courant (param siteId[]) et rechargées au changement de site ; reset du tiers sélectionné s'il sort du périmètre du nouveau site.
Author
Owner

Code review — ERP-208 (cartouche tiers + filtrage par site)

Le périmètre back (cartouche getCounterpartyName/getCounterpartyTypeLabel, template Twig, tests) est propre et bien couvert. Les points à traiter se concentrent côté front, autour de la purge du tiers sélectionné.

🔴 Correction

1. Perte silencieuse du tiers à l'édition d'un ticket existant — edit.vue (reloadReferentials / onMounted)
La purge vide form.clientIri/supplierIri dès que la valeur n'est plus dans la liste rechargée. Or sur l'écran d'édition cette liste peut légitimement ne pas contenir le tiers déjà enregistré :

  • Filtre site : un ticket créé avant ERP-208 référence un client sans adresse rattachée au site courant → exclu de /clients?siteId[]= → purgé.
  • 403 résilient : load() est tolérant (Promise.allSettled → liste vide si 403 selon le rôle). Liste vide ⇒ tout IRI paraît « absent » ⇒ purge. Un rôle qui édite des tickets sans read sur /clients perd la contrepartie.
  • Race : dans onMounted, reloadReferentials(...) est lancé sans await puis await fetchTicket → form.hydrate (qui pose clientIri). Si load() résout après hydrate, la purge nulle la valeur hydratée.

Conséquence : à la sauvegarde, la vraie contrepartie est écrasée par null (ou 422 RG-5.03). Intermittent. Le design (§ « switch de site avec tiers sélectionné ») n'a prévu le reset-si-absent que pour le switch pendant la saisie, pas pour le chargement d'un ticket persisté.

2. L'édition filtre par currentSite alors que le site du ticket est immuable — edit.vue (watch(currentSite.id))
Basculer le site courant en cours d'édition rejoue la purge et vide une contrepartie valide d'un ticket resté sur son site (RG-5.09, site_id immuable). Sur l'écran d'édition le filtre devrait être piloté par le site du ticket, pas par currentSite.

3. Scoping front contourné quand currentSite est null — new.vue + useWeighingTicketReferentials.ts
currentSite n'est hydraté que par SiteSelector (syncFromAuth), rendu uniquement si sites actif et user.sites non vide. Sinon load(null) n'envoie pas siteId[] → les selects montrent tous les tiers, tous sites confondus (cloisonnement ERP-208 perdu côté front).

4. Chargements concurrents last-write-wins — useWeighingTicketReferentials.load
load() assigne clients.value/suppliers.value sans séquencement ni annulation. Sur des switches rapides, une réponse de l'ancien site peut arriver en dernier → listes filtrées sur le mauvais site, puis purge contre une liste périmée.

🟡 Qualité / altitude

5. Littéraux 'CLIENT'/'FOURNISSEUR'/'AUTRE' répétés en 4 endroits — WeighingTicket.php (Assert\Choice, switch de validateCounterpartyConsistency, 2 nouveaux match). La classe utilise déjà public const string STATUS_*. Une faute dans un bras de match (FOURNISEUR) compile et imprime un nom vide sans test rouge → extraire des constantes / un enum CounterpartyType.

6. {% if ticket.counterpartyName %} masque aussi le libellé de type — weighing_ticket_print.html.twig : type défini mais nom vide (brouillon imprimable, ou client legacy à companyName null) ⇒ tout le cartouche disparaît. Garder sur counterpartyTypeLabel et conditionner le nom à l'intérieur.

7. Libellés FR dans la couche Domain — WeighingTicket.php (getCounterpartyTypeLabel) : chaînes UI codées en dur dans l'entité, et tests domaine couplés au copywriting. Leur place est le template de rendu.

8. Robustesse Dompdf — weighing_ticket_print.html.twig : inline-block + min-width est mal supporté (CSS 2.1) et le <td> gauche n'a pas de largeur ; un companyName long peut déborder le cartouche ou comprimer le bloc société. À valider sur un vrai rendu Dompdf.

9. reloadReferentials + watch + onMounted dupliqués à l'identique entre new.vue et edit.vue : la règle de purge en double dérivera dès qu'une page gagnera un champ que l'autre n'a pas → candidat à remonter dans useWeighingTicketReferentials.


Prioritaire : traiter #1 + #2 ensemble — sur l'édition, ne pas purger une contrepartie déjà persistée : await reloadReferentials après hydrate, ne purger que sur un vrai changement de site (pas au chargement initial), et toujours conserver l'option du tiers actuellement sélectionné (l'injecter si absente) plutôt que la retirer. #5→#9 sont non bloquants.

## Code review — ERP-208 (cartouche tiers + filtrage par site) Le périmètre back (cartouche `getCounterpartyName`/`getCounterpartyTypeLabel`, template Twig, tests) est propre et bien couvert. Les points à traiter se concentrent côté front, autour de la **purge du tiers sélectionné**. ### 🔴 Correction **1. Perte silencieuse du tiers à l'édition d'un ticket existant — `edit.vue` (`reloadReferentials` / `onMounted`)** La purge vide `form.clientIri`/`supplierIri` dès que la valeur n'est plus dans la liste rechargée. Or sur l'écran d'édition cette liste peut légitimement ne pas contenir le tiers **déjà enregistré** : - **Filtre site** : un ticket créé avant ERP-208 référence un client sans adresse rattachée au site courant → exclu de `/clients?siteId[]=` → purgé. - **403 résilient** : `load()` est tolérant (`Promise.allSettled` → liste vide si 403 selon le rôle). Liste vide ⇒ *tout* IRI paraît « absent » ⇒ purge. Un rôle qui édite des tickets sans read sur `/clients` perd la contrepartie. - **Race** : dans `onMounted`, `reloadReferentials(...)` est lancé **sans `await`** puis `await fetchTicket → form.hydrate` (qui pose `clientIri`). Si `load()` résout après `hydrate`, la purge nulle la valeur hydratée. Conséquence : à la sauvegarde, la vraie contrepartie est écrasée par `null` (ou 422 RG-5.03). Intermittent. Le design (§ « switch de site avec tiers sélectionné ») n'a prévu le reset-si-absent que pour le **switch pendant la saisie**, pas pour le chargement d'un ticket persisté. **2. L'édition filtre par `currentSite` alors que le site du ticket est immuable — `edit.vue` (`watch(currentSite.id)`)** Basculer le site courant en cours d'édition rejoue la purge et vide une contrepartie valide d'un ticket resté sur son site (RG-5.09, `site_id` immuable). Sur l'écran d'édition le filtre devrait être piloté par **le site du ticket**, pas par `currentSite`. **3. Scoping front contourné quand `currentSite` est null — `new.vue` + `useWeighingTicketReferentials.ts`** `currentSite` n'est hydraté que par `SiteSelector` (`syncFromAuth`), rendu uniquement si `sites` actif **et** `user.sites` non vide. Sinon `load(null)` n'envoie pas `siteId[]` → les selects montrent **tous** les tiers, tous sites confondus (cloisonnement ERP-208 perdu côté front). **4. Chargements concurrents last-write-wins — `useWeighingTicketReferentials.load`** `load()` assigne `clients.value`/`suppliers.value` sans séquencement ni annulation. Sur des switches rapides, une réponse de l'ancien site peut arriver en dernier → listes filtrées sur le mauvais site, puis purge contre une liste périmée. ### 🟡 Qualité / altitude **5. Littéraux `'CLIENT'/'FOURNISSEUR'/'AUTRE'` répétés en 4 endroits — `WeighingTicket.php`** (`Assert\Choice`, `switch` de `validateCounterpartyConsistency`, 2 nouveaux `match`). La classe utilise déjà `public const string STATUS_*`. Une faute dans un bras de `match` (`FOURNISEUR`) compile et imprime un nom vide sans test rouge → extraire des constantes / un enum `CounterpartyType`. **6. `{% if ticket.counterpartyName %}` masque aussi le libellé de type — `weighing_ticket_print.html.twig`** : type défini mais nom vide (brouillon imprimable, ou client legacy à `companyName` null) ⇒ tout le cartouche disparaît. Garder sur `counterpartyTypeLabel` et conditionner le nom à l'intérieur. **7. Libellés FR dans la couche Domain — `WeighingTicket.php` (`getCounterpartyTypeLabel`)** : chaînes UI codées en dur dans l'entité, et tests domaine couplés au copywriting. Leur place est le template de rendu. **8. Robustesse Dompdf — `weighing_ticket_print.html.twig`** : `inline-block` + `min-width` est mal supporté (CSS 2.1) et le `<td>` gauche n'a pas de largeur ; un `companyName` long peut déborder le cartouche ou comprimer le bloc société. À valider sur un vrai rendu Dompdf. **9. `reloadReferentials` + `watch` + `onMounted` dupliqués à l'identique entre `new.vue` et `edit.vue`** : la règle de purge en double dérivera dès qu'une page gagnera un champ que l'autre n'a pas → candidat à remonter dans `useWeighingTicketReferentials`. --- **Prioritaire :** traiter #1 + #2 ensemble — sur l'édition, ne pas purger une contrepartie déjà persistée : `await reloadReferentials` **après** `hydrate`, ne purger que sur un *vrai* changement de site (pas au chargement initial), et toujours conserver l'option du tiers actuellement sélectionné (l'injecter si absente) plutôt que la retirer. #5→#9 sont non bloquants.
tristan added 1 commit 2026-06-25 12:55:38 +00:00
fix(logistique) : corrections review ticket de pesée (ERP-208)
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m59s
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 2m32s
2b03c4ae15
- Édition : listes contrepartie filtrées sur le site DU TICKET (immuable), chargées après hydrate, sans purge de la contrepartie persistée (injection de l'option si absente) → corrige la perte silencieuse / race.
- Entité : constantes COUNTERPARTY_* (Assert\Choice + validation + getCounterpartyName) ; libellé FR du type déplacé du Domain vers le template.
- PDF : cartouche conditionné sur le type (nom à l'intérieur), layout Dompdf-safe (largeurs de cellules, cartouche en bloc, nom long renvoyé à la ligne).
tristan merged commit 086be7b4f0 into develop 2026-06-25 13:02:32 +00:00
tristan deleted branch fix/erp-208-ticket-pesee 2026-06-25 13:02:33 +00:00
Sign in to join this conversation.