feat : M5 — Tickets de pesée (ERP-188 → ERP-193) #144
Reference in New Issue
Block a user
Delete Branch "feat/erp-191-i18n-site-courant"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
MR unique regroupant tout le module M5 « Tickets de pesée » (remplace les MR empilées #140/#141/#142/#143).
Périmètre
GET /api/weighing_tickets/{id}/print.pdf.Vérifications
Base :
develop— contient les 16 commits du M5 (rien d'autre).Endpoint API Platform GET /api/weighing_tickets/{id}/print.pdf (provider renvoyant un binaire, pas de controller) sécurisé par logistique.weighing_tickets.view. Rendu d'un template Twig hydraté avec le ticket converti en PDF via Dompdf. Reproduit le modèle fourni : en-tête fixe (logo + identité société, indépendant du site), pesées à vide/plein avec le numéro de pesée affiché comme un DSD, poids net = plein − vide.Une pesée (bascule ou manuelle) s'enregistre désormais dès la validation de sa modale, sans exiger la contrepartie ni l'immatriculation : le ticket naît « brouillon » (status DRAFT, sans numéro). Le bouton « Valider » finalise quand les 3 champs du haut (contrepartie + champ associé + immatriculation) ET les 2 pesées sont renseignés : attribution du numéro {siteCode}-TP-{NNNN} et passage en VALIDATED, puis ouverture du bon de pesée PDF. Back : counterparty_type/immatriculation/number nullables + colonne status (migration racine), contraintes strictes déplacées en groupe de validation finalize, opération PATCH /weighing_tickets/{id}/validate, numéro attribué à la validation. Front : 4 champs en haut hors blocs, persistance immédiate des pesées, écrans Ajouter/Modifier refondus, colonne Statut dans la liste, form à plat pleine largeur. Tests back (lifecycle brouillon/validate) + front à jour.Revue automatisée de la MR (recall élevé). PR globalement très propre côté conventions (aucune violation CLAUDE.md relevée : strict_types, #[Auditable], i18n d'audit, COMMENT ON COLUMN, priority:1 sur l'export, useApi/useFormErrors/usePaginatedList/Malio*, pagination, tests fournis).
8 points ci-dessous en commentaires inline, du plus grave au moins grave. Le #1 est le seul bloquant (500 atteignable en parcours nominal). Points volontaires non signalés : DSD non unique (ERP-193) et brouillons dans l'export avec colonnes vides + statut « En attente » (miroir de la liste).
@@ -0,0 +48,4 @@* faire échouer l'autre).*/async function load(): Promise<void> {await Promise.allSettled([🟢 Robustesse — selects référentiels : 403 silencieux → cul-de-sac
Les erreurs de
/clientset/supplierssont avalées parPromise.allSettled. Si un rôle porteur delogistique.weighing_tickets.manage(ex. Usine) ne dispose pas decommercial.clients.view/suppliers, le select est vide sans message et la validation échoue ensuite en 422 surclient/suppliersans moyen de corriger. À confirmer vs la matrice RBAC ; sinon afficher un message explicite quand le référentiel est inaccessible.@@ -0,0 +371,4 @@* pesée PDF (RG-5.08) — aussi bien à la validation d'un brouillon qu'à* l'enregistrement d'un ticket déjà validé. Retour à la liste au succès.*/async function submitPrimary(): Promise<void> {🟡 UX — « Enregistrer » sur un ticket déjà validé rejoue
/validateet ré-ouvre le PDFPour un ticket
VALIDATEDle bouton s'intitule « Enregistrer » maissubmitPrimaryappelle quand mêmesaveDraft()puisPATCH …/validate, puiswindow.open(…/print.pdf). Une simple modif (ex. immatriculation) repasse donc par la validation strictefinalizeet rouvre le bon de pesée dans un nouvel onglet — surprenant pour un « Enregistrer ». Côté serveur rien n'interdit non plus de re-déclencher/validatesur un ticket déjà validé. Prévoir un chemin de sauvegarde « draft-only » (PATCH standard) pour un ticket validé.@@ -0,0 +250,4 @@}/** Déclenche la pesée bascule puis enregistre le brouillon (ERP-193). */async function confirmAuto(): Promise<void> {🟡 Duplication —
new.vueet[id]/edit.vuepartagent ~250 lignes quasi identiquesCorps du formulaire (grille contrepartie + champ conditionnel + immat + « Tout format »), les deux modales (auto + manuelle),
counterpartyOptions,confirmAuto/confirmManual,saveDraftet le cycle de vie sont copiés-collés entre les deux pages, avec des divergences cosmétiques déjà présentes (catch (error)vscatch (e)). Toute évolution devra être faite deux fois. À extraire dans unWeighingTicketFormBody.vue+ la persistance/modales dans le composable (qui ne fait aujourd'hui que le GET) — réduirait mécaniquement le risque des deux points UX ci-dessus.@@ -0,0 +309,4 @@await saveDraft()}catch (error) {manualModal.errors = { weight: weighbridge.extractWeighbridgeError(error) }🟡 Bug UX — un 422 sur le DSD s'affiche sous le champ Poids
dsdporte#[Assert\Positive]côtéWeighbridgeReadingResource. Si l'opérateur saisit0(ou ≤ 0), lePOST /weighbridge_readingsrenvoie une 422 depropertyPathdsd, mais lecatchmappe toujours le message sousweight({ weight: extractWeighbridgeError(error) }). L'erreur DSD apparaît sous Poids, le champ DSD reste vierge. Router le message vers le bon champ (réutilisermapViolationsToRecord). Idem dans[id]/edit.vue.@@ -0,0 +10,4 @@* absente ou invalide. Lit les composantes locales (cohérent avec l'affichage* des autres répertoires M1→M4).*/export function formatDateFr(value: string | null | undefined): string {🟢 Mineur —
formatDateFrdupliqué + lecture en fuseau localFonction identique à celle de
transport/pages/carriers/index.vue(3ᵉ variante de rendu date FR dans le repo). De plus elle litgetDate()/getMonth()en fuseau navigateur : sur une date proche de minuit avec offset+02:00, un runtime UTC (CI/Vitest, poste hors Europe/Paris) décale d'un jour, alors que l'écran d'édition (slice de la chaîne brute) et l'export XLSX (format('d/m/Y H:i')serveur) gardent le bon jour → divergence liste vs édition vs export. À hoister dansshared/utils/date.tsà côté denowIsoDateTime/todayIso.@@ -260,3 +310,3 @@* champs hors-branche — ERP-185).*/#[Assert\Callback]#[Assert\Callback(groups: ['finalize'])]🔴 Bug — brouillon avec contrepartie partielle → 500 (au lieu d'une 422 mappable)
validateCounterpartyConsistencyest passé engroups: ['finalize']: il ne joue plus sur les POST/PATCH brouillon.Parcours réel : l'opérateur choisit « Client » dans le menu contrepartie avant d'avoir sélectionné le client, puis effectue une pesée →
confirmAuto/confirmManualappellesaveDraft().buildDraftPayload()(viacompact()) retireclient: nullmais conservecounterpartyType: 'CLIENT'. En groupe Default,Assert\Choicepasse,NotBlank/callback sontfinalize-only → la validation passe. Au persist,chk_wt_client_branch(counterparty_type <> 'CLIENT' OR client_id IS NOT NULL …) est violée → HTTP 500, toast générique, aucune erreur inline, retry impossible (contraire à ERP-101).applyCounterpartyExclusivityne peut pas inventer la FK manquante. Piste : faire jouer une variante « présence cohérente » du callback aussi en Default (message inline sur le champ), ou nullifiercounterpartyTypecôté brouillon quand le champ de branche est absent.@@ -88,3 +87,4 @@$this->allocateAutoDsd($data, $site);}$this->computeNetWeight($data);🟠 Bug — poids net jamais borné, un net négatif est persisté/affiché/imprimé
computeNetWeight()(l. 204-209) faitfull - emptysans garantirfull >= empty. Si l'opérateur intervertit les deux pesées (saisit la pesée chargée dans le bloc « vide »),net_weightdevient négatif, est stocké, affiché en liste (-7 150 Kg) et imprimé sur le bon de pesée — rien ne le signale. Ajouter une garde (422 sifull < empty, ou au moins un avertissement).@@ -0,0 +92,4 @@* (user `sites.bypass_scope`, ou pas de site courant). Miroir de* WeighingTicketProvider::currentScopeSite().*/private function currentScopeSite(): ?Site🟢 Altitude — règle de visibilité/cloisonnement recopiée 3 fois
findVisibleTicket()/currentScopeSite()(soft-delete + scope site + anti-énumération) sont des « miroirs » manuels deWeighingTicketProvider, etapplySiteScopeest mirroré une 3ᵉ fois dansWeighingTicketExportController. C'est une frontière de sécurité (§2.3) : un changement de scoping oublié dans l'un des trois fuiterait des tickets cross-site. De même,DsdAllocatoretWeighingTicketNumberAllocatorréimplémentent le même compteur par site (ON CONFLICT DO NOTHING+SELECT … FOR UPDATE+UPDATE). Un service de visibilité partagé + unSiteCounterAllocator(table)paramétré seraient préférables.