312c119c06
Entité WeighingTicket - Entité métier complète (#[Auditable], TimestampableBlamableTrait, relations ORM Client/Supplier/Site) + contrat de sérialisation à 3 maillons (weighing_ticket:read / :item:read + contextes par opération). - Getters calculés displayDate et plateFreeFormat (#[SerializedName]), sécurité view/manage, pas de Delete/archive. - Validation #[Assert\*] messages FR + #[Assert\Callback] RG-5.03 (->atPath()), libellé i18n audit.entity.logistique_weighingticket. - Repository : interface Domain + DoctrineWeighingTicketRepository (recherche + tri number DESC, deletedAt IS NULL). Dette site.code - Site.code mappé VARCHAR(8) (groupes read/write), dérivation auto au PrePersist (2 premiers chiffres du CP), UniqueConstraint uq_site_code. - Migration Version20260617160000 : ALTER COLUMN code SET NOT NULL + COMMENT. - Fixtures (codes 86/17/82) et SiteApiTest ajustés. Câblage - doctrine.yaml : mapping ORM du module Logistique (absent du scaffold ERP-181). - ColumnCommentsCatalog : site.code + table weighing_ticket. Specs M5 versionnées (spec-back / spec-front / prompts).
247 lines
18 KiB
Markdown
247 lines
18 KiB
Markdown
---
|
||
# === IDENTITÉ ===
|
||
module: M5
|
||
nom: "Tickets de pesée"
|
||
ecran: tickets-pesee
|
||
owner_spec: Matthieu
|
||
backup_spec: Tristan
|
||
version: V0.1
|
||
date_redaction: 2026-06-17
|
||
# Historique :
|
||
# V0.1 (2026-06-17) — Restitution Markdown du docx « M5-ticket-de-pesee-V02 » (V0.2, 15/06/2026,
|
||
# validation client en attente) + maquette Figma (node 1322-16774). Précisions techniques (back)
|
||
# dans spec-back.md. Réutilise le pattern et les composants M1/M2/M3/M4.
|
||
# Maquette : les 2 blocs (vide + plein) portent « Pesée bascule » + « Pesée manuelle » ;
|
||
# contrepartie portée par le bloc « Poids à vide » ; net = plein − vide (confirmé Matthieu).
|
||
|
||
# === LIENS ===
|
||
maquette_figma: "https://www.figma.com/design/jRYgT0T9c03VsEbjGhCwwS/Composants---Design-System?node-id=1322-16774&p=f&m=dev"
|
||
regles_metier: [RG-5.01, RG-5.02, RG-5.03, RG-5.04, RG-5.05, RG-5.06, RG-5.07, RG-5.08, RG-5.09, RG-5.10]
|
||
roles: [Admin, Bureau, Compta, Commerciale, Usine]
|
||
lien_spec_back: ./spec-back.md
|
||
|
||
# === VALIDATION CLIENT ===
|
||
client_validation_1:
|
||
statut: validee
|
||
version: V0.2
|
||
date_doc: 2026-06-15
|
||
date_validation: 2026-06-17
|
||
valide_par: "Matthieu (CP MALIO)"
|
||
|
||
# === LIEN LESSTIME ===
|
||
lesstime_project_id: 6
|
||
lesstime_taskgroup_id: 33 # M5 — Tickets de pesée (ERP-181 → ERP-192)
|
||
statut_global: pret_a_dev
|
||
---
|
||
|
||
# Module 5 — Tickets de pesée (V0.1 front)
|
||
|
||
> **Origine** : spec fonctionnelle `M5-ticket-de-pesee-V02` (V0.2, 15/06/2026, **validation client en attente**) + maquette Figma (node 1322-16774). Restitution Markdown pour intégration au workflow MALIO. Toute décision technique (back) vit dans [`spec-back.md`](./spec-back.md). Le M5 réutilise le pattern et les composants posés aux [M1 clients](../M1-clients/spec-front.md) → [M4 transporteurs](../M4-transporteurs/spec-front.md).
|
||
|
||
> **Nouveau module `Logistique`** (DÉCISION Matthieu 17/06). La maquette montre une section sidebar **Logistique** plus large (Réception, Expédition, Validations, Triage, **Ticket de pesée**, Bons…) ; **le M5 ne livre que l'écran « Ticket de pesée »**. Les autres items sont hors périmètre (modules/écrans ultérieurs).
|
||
|
||
> **Décisions (17/06)** : (1) **pont bascule = stub** renvoyant un poids aléatoire ∈ [10000, 50000] kg (pas de liaison matérielle — [`spec-back.md § 2.6`](./spec-back.md)) ; (2) **DSD = compteur de pesée** par site, +1 par pesée ([`§ 2.7`](./spec-back.md)) ; (3) **net = plein − vide** ([`§ 2.8`](./spec-back.md)) ; (4) numéro **`{siteCode}-TP-{NNNN}` par site** ([`§ 2.5`](./spec-back.md)).
|
||
|
||
## But
|
||
|
||
Lister les tickets de pesée et accéder à leur fiche : consultation, création (pesée à vide + pesée à plein au pont bascule), modification, impression. Chaque ticket porte un **numéro unique par site** (ex. `86-TP-0001`) et une **contrepartie** Client / Fournisseur / Autre.
|
||
|
||
## Accès
|
||
|
||
- **Depuis** : menu principal → section **Logistique** → item **« Ticket de pesée »** (route `/weighing-tickets`).
|
||
- **Site** : l'écran dépend du **site courant** (sélecteur de site en haut de l'app — onglets `CHÂTELLERAULT` / `SAINT-JEAN` / `POMMEVIC`). Le site pilote la numérotation et (par défaut) le cloisonnement de la liste ([`spec-back.md § 2.3 / § 2.5`](./spec-back.md)).
|
||
- **Rôles autorisés** (tableau « Rôles & permissions » du docx p.3, V0.2) :
|
||
|
||
| Rôle | Consultation | Ajout / Modification |
|
||
|---|---|---|
|
||
| **Admin** | ✅ Tout | ✅ Tout |
|
||
| **Bureau** | ✅ Tout | ✅ Tout |
|
||
| **Usine** | ✅ Tout | ✅ Tout |
|
||
| **Compta** | ❌ | ❌ |
|
||
| **Commerciale** | ❌ | ❌ |
|
||
|
||
> **Notes** :
|
||
> - RBAC transposée sur `logistique.weighing_tickets.view` / `.manage` ([`spec-back.md § 5`](./spec-back.md)).
|
||
> - ⚠ **Changement vs M5 V0.1** : en **V0.2, Usine = Tout / Tout**. **Compta** et **Commerciale** n'ont **aucun** accès (item sidebar masqué).
|
||
|
||
## Navigation
|
||
|
||
Page d'entrée de l'écran : **datatable** « Tickets de pesées ».
|
||
|
||
- **Clic sur une ligne** → écran **Modification d'un ticket de pesée** (le docx ne prévoit pas d'écran de consultation séparé — clic = édition).
|
||
- **Bouton « + Ajouter »** (haut droite, si `manage`) → écran **Ajouter un ticket de pesée**.
|
||
- **Bouton « Exporter »** (bas de liste, maquette) → télécharge un **XLSX** de **toute la liste** (filtres + site courant appliqués). Format dans [`spec-back.md § 4.5`](./spec-back.md).
|
||
|
||
## Datatable des tickets
|
||
|
||
Composant : `<MalioDataTable>` branché sur `usePaginatedList<WeighingTicket>({ url: '/weighing_tickets' })` *(URL API en `snake_case` ; la route Nuxt reste `/weighing-tickets`)* (règle frontend obligatoire — pagination Hydra, état 100 % local). Colonnes (docx p.3 + maquette) :
|
||
|
||
| Colonne | Source | Tri |
|
||
|---|---|---|
|
||
| **Numéro** | `ticket.number` (`{siteCode}-TP-{NNNN}`) | DESC par défaut (plus récents en tête) |
|
||
| **Client** | `ticket.client.companyName` (vide si contrepartie ≠ Client) | Non |
|
||
| **Fournisseur** | `ticket.supplier.companyName` (vide si ≠ Fournisseur) | Non |
|
||
| **Autre** | `ticket.otherLabel` (vide si ≠ Autre) | Non |
|
||
| **Date** | `ticket.displayDate` (`fullDate ?? emptyDate`, format `JJ-MM-AAAA`) | Oui |
|
||
| **Poids** | `ticket.netWeight` (kg, = plein − vide — RG-5.05) | Oui |
|
||
|
||
> **Clic ligne** → écran Modification. **Pagination** : standard Starseed 10 / 25 / 50 (défaut 10). Liste **cloisonnée par site courant** par défaut ([`spec-back.md § 2.3`](./spec-back.md)).
|
||
|
||
## Écran « Ajouter un ticket de pesée »
|
||
|
||
**Accès** : bouton « + Ajouter ». **Rôles** : Admin, Bureau, Usine.
|
||
**Titre** : « ← Ticket de pesée » (flèche retour vers la liste).
|
||
|
||
L'écran (maquette) est composé de **deux blocs empilés** — **« Poids à vide »** puis **« Poids à plein »** — et d'un bouton **« Valider »** en bas.
|
||
|
||
### Bloc « Poids à vide »
|
||
|
||
Boutons en haut à droite du bloc : **« Pesée bascule »** (`<MalioButton>` secondaire) + **« Pesée manuelle »** (`<MalioButton>` primaire).
|
||
|
||
**Champs** :
|
||
|
||
| Champ | Type composant | Obligatoire | Règle |
|
||
|---|---|---|---|
|
||
| **Fournisseur / Client / Autre** | `<MalioSelect>` (3 valeurs) | Oui | RG-5.03 — pilote le champ suivant |
|
||
| **Nom du fournisseur** | `<MalioSelect>` (liste fournisseurs M2) | Conditionnel | RG-5.03 — visible + obligatoire si « Fournisseur » |
|
||
| **Nom du client** | `<MalioSelect>` (liste clients M1) | Conditionnel | RG-5.03 — visible + obligatoire si « Client » |
|
||
| **Autre** | `<MalioInputText>` | Conditionnel | RG-5.03 — visible + obligatoire si « Autre » |
|
||
| **Date** | `<MalioInputText>` type `date` *(cf. note)* | Oui | RG-5.07 — **date du jour par défaut** |
|
||
| **Poids** | `<MalioInputNumber>` (suffixe « Kg ») | Oui | RG-5.07 — **readonly**, rempli par la pesée |
|
||
| **DSD** | `<MalioInputNumber>` | Oui | RG-5.04 / RG-5.07 — **readonly**, rempli par la pesée |
|
||
| **Immatriculation** | `<MalioInputText>` (masque `XX-000-XX`) | Oui | RG-5.01 |
|
||
| **Tout format** | `<MalioCheckbox>` | Non | RG-5.01 — désactive le masque |
|
||
|
||
> **La contrepartie (Fournisseur/Client/Autre) + son champ associé est portée par le bloc « Poids à vide » uniquement** (maquette) — c'est une donnée du ticket, pas répétée sur le bloc plein. Côté back : champs `counterpartyType` / `client` / `supplier` / `otherLabel` du ticket ([`spec-back.md § 2.9`](./spec-back.md)).
|
||
|
||
**Action « Enregistrer »** (sous le bloc, maquette) : POST `/api/weighing_tickets` (création initiale du ticket avec la pesée à vide) — [`spec-back.md § 4.3`](./spec-back.md). Le numéro `{siteCode}-TP-{NNNN}` est attribué serveur.
|
||
|
||
### Bloc « Poids à plein »
|
||
|
||
Mêmes boutons **« Pesée bascule »** + **« Pesée manuelle »**. **Champs** : Date (date du jour par défaut), Poids (readonly, Kg), DSD (readonly), Immatriculation (`XX-000-XX`), « Tout format ».
|
||
|
||
> **Immatriculation + « Tout format » connectés entre les 2 blocs** (RG-5.01) : une seule valeur partagée — modifier l'un met à jour l'autre (même véhicule). Géré dans `useWeighingTicketForm()` (état partagé).
|
||
|
||
### Boutons de pesée — comportement
|
||
|
||
| Bouton | Déclencheur | Comportement |
|
||
|---|---|---|
|
||
| **Pesée bascule** | clic | Ouvre une **modal de confirmation** « Êtes-vous sûr de vouloir déclencher une pesée ? » (`<MalioButton>` « Valider »). Si confirmé → `POST /api/weighbridge_readings { mode: 'AUTO' }` ([`spec-back.md § 4.2`](./spec-back.md)) → remplit **Poids** et **DSD** du bloc, ferme la modal. **En cas d'erreur** (RG-5.06) : le message d'erreur s'affiche **dans la modal** et invite à passer en **pesée manuelle**. *(Au M5, le stub renvoie toujours un poids ∈ [10000,50000] — le chemin d'erreur est néanmoins géré.)* |
|
||
| **Pesée manuelle** | clic | Ouvre une **modal « Pesée manuelle »** avec **Poids** et **Numéro de pesée** à saisir (`<MalioInputNumber>` + `<MalioInputText>`), bouton « Enregistrer ». Une fois validé → le **Poids** du bloc est rempli ; le **DSD** est **calculé automatiquement** = dernier dsd du site + 1 (`POST /api/weighbridge_readings { mode: 'MANUAL', weight, manualNumber }` — RG-5.04). |
|
||
|
||
### Action « Valider » (bas d'écran)
|
||
|
||
`<MalioButton>` « Valider » → finalise le ticket (PATCH `/api/weighing_tickets/{id}` avec la pesée à plein + recalcul du net — [`spec-back.md § 4.4`](./spec-back.md)) puis **ouvre la modal d'impression** du ticket (RG-5.08 — **bon d'impression réalisé par Tristan**, cf. § Modales).
|
||
|
||
## Écran « Modification d'un ticket de pesée »
|
||
|
||
**But** : modifier un ticket existant et/ou **imprimer** le ticket.
|
||
**Accès** : clic sur une ligne de la liste. **Rôles** : Admin, Bureau, Usine.
|
||
|
||
**Identique à l'écran d'ajout** — mêmes 2 blocs, mêmes règles (RG-5.01 → RG-5.10) — **sauf** (docx + maquette) :
|
||
- Les champs sont **pré-remplis** avec les valeurs actuelles.
|
||
- Le **bouton « Enregistrer » du bloc « Poids à vide » disparaît** (RG-5.08) — on enregistre via le bas d'écran.
|
||
- En bas : **« Enregistrer »** (remplace « Valider ») + **« Imprimer »** (bouton d'impression **absent à l'ajout**, RG-5.08).
|
||
- Le numéro et le site sont **immuables** (lecture seule).
|
||
|
||
## Modales
|
||
|
||
| Modale | Contenu | Source |
|
||
|---|---|---|
|
||
| **Confirmation pesée bascule** | « Êtes-vous sûr de vouloir déclencher une pesée ? » + bouton « Valider ». Erreur affichée inline → invite pesée manuelle (RG-5.06). | docx p.5 + maquette |
|
||
| **Pesée manuelle** | Champs « Poids » + « Numéro de pesée » + bouton « Enregistrer ». DSD auto = dernier +1 (RG-5.04). | docx p.5 + maquette |
|
||
| **Impression du ticket / bon de pesée** | Aperçu imprimable du ticket (numéro, contrepartie, immat, pesée vide/plein, net, DSD, date). **Réalisé par Tristan** (voir encadré ci-dessous). | docx p.5 / RG-5.08 ; [`spec-back.md § 2.12`](./spec-back.md) |
|
||
|
||
> **⚠ Bon d'impression = Tristan.** La conception et la réalisation du **bon d'impression** (gabarit du ticket de pesée, mise en page, déclenchement) sont **prises en charge par Tristan lui-même**, hors de la découpe front standard du M5. Le reste de l'écran (modale de confirmation, modale pesée manuelle, formulaires) reste dans la découpe M5.
|
||
> - **Déclencheur attendu** : modale d'impression à la **validation** (création) ; bouton **« Imprimer »** en **modification** (absent à l'ajout — RG-5.08).
|
||
> - **Données disponibles** : toute la réponse `GET /api/weighing_tickets/{id}` (numéro, site, contrepartie, immat, pesées vide/plein, net, DSD, dates) — [`spec-back.md § 2.12 / § 4.0`](./spec-back.md).
|
||
> - **Modales** : réutiliser le wrapper de modal partagé `frontend/shared/` (comme M1→M4).
|
||
|
||
## Composants UI à utiliser (`@malio/layer-ui`)
|
||
|
||
- **Datatable** : `<MalioDataTable>` (+ `usePaginatedList`)
|
||
- **Select** : `<MalioSelect>` (contrepartie, nom client, nom fournisseur)
|
||
- **Input texte** : `<MalioInputText>` (Autre, Immatriculation, Numéro de pesée)
|
||
- **Input nombre** : `<MalioInputNumber>` (Poids, DSD)
|
||
- **Checkbox** : `<MalioCheckbox>` (« Tout format »)
|
||
- **Bouton** : `<MalioButton>`, `<MalioButtonIcon>` (Pesée bascule, Pesée manuelle, Valider, Enregistrer, Imprimer, + Ajouter, Exporter)
|
||
- **Validation par champ** : `useFormErrors` (mapping 422 inline — règle frontend obligatoire)
|
||
- **Toasts** : standards via `useApi()`
|
||
|
||
**Exceptions autorisées** (commenter `// TODO migrer quand Malio couvre`) :
|
||
- **Date** : `<MalioInput>` ne couvrant pas `date` nativement, utiliser un `<input type="date">` encapsulé OU `MalioDate` si dispo (cf. exceptions @.claude/rules/frontend.md — type `date` explicitement listé comme exception tolérée).
|
||
- **Masque immatriculation `XX-000-XX`** : si non couvert par `<MalioInputText>`, masque local (directive) + `// TODO`. La validation de format reste **autoritaire côté serveur** (RG-5.01 / RG-5.10).
|
||
- **Modales** : wrapper partagé `frontend/shared/`.
|
||
|
||
## Composables & appels API
|
||
|
||
- `usePaginatedList<WeighingTicket>({ url: '/weighing_tickets' })` — liste paginée (obligatoire). Consomme `number`, `client`/`supplier`/`otherLabel`, `displayDate`, `netWeight` ([`spec-back.md § 4.0`](./spec-back.md)).
|
||
- `useWeighingTicket(id)` — charge le détail via `GET /api/weighing_tickets/{id}` (pesées vide + plein embarquées, client/supplier/site imbriqués). **DoD avant intégration** : vérifier le JSON réel ([`spec-back.md § 4.0.bis`](./spec-back.md)).
|
||
- `useWeighingTicketForm()` — workflow 2 blocs (POST à l'« Enregistrer » du bloc vide, PATCH au « Valider ») + **état partagé** immatriculation/« Tout format » entre les 2 blocs (RG-5.01) + gestion des champs conditionnels de contrepartie (RG-5.03).
|
||
- `useWeighbridge()` — déclenche la pesée : `POST /api/weighbridge_readings` (AUTO ou MANUAL), gère la modal de confirmation et le chemin d'erreur → pesée manuelle (RG-5.06).
|
||
- `useClientOptions()` / `useSupplierOptions()` — alimentent les selects (référentiels M1/M2 via `?pagination=false` — échappatoire selects).
|
||
- `useCurrentSite()` — site courant (sélecteur) — déjà exposé côté front (Sites). Le back lit le site courant pour la numérotation ; le front n'a pas à l'envoyer.
|
||
- `usePermissions()` — masque l'item sidebar et les boutons selon `logistique.weighing_tickets.view/manage`.
|
||
- Tous les appels passent par `useApi()` (jamais `$fetch` direct — règle ABSOLUE n°4).
|
||
|
||
## Règles de formatage et normalisation
|
||
|
||
Le serveur normalise systématiquement ([`spec-back.md § 6`](./spec-back.md)) :
|
||
|
||
| Champ | Normalisation serveur | Affichage front |
|
||
|---|---|---|
|
||
| Immatriculation | trim + UPPER ; format `XX-000-XX` sauf « Tout format » (RG-5.01) | UPPER, masqué |
|
||
| Autre (`otherLabel`) | trim | identique |
|
||
| Poids / DSD | entiers (kg) | « 7 150 Kg », DSD brut |
|
||
| Numéro de ticket | `{siteCode}-TP-{NNNN}` (serveur) | affiché tel quel |
|
||
|
||
> Le front **ne normalise pas** : il envoie la valeur saisie, le serveur normalise et renvoie la valeur que l'UI affiche.
|
||
|
||
## Différences notables avec les modules précédents
|
||
|
||
| Zone | M1→M4 | M5 tickets de pesée |
|
||
|---|---|---|
|
||
| Module | Commercial / Transport… | **Logistique** (nouveau, ERP à venir) |
|
||
| Saisie poids | — | **Pesée au pont bascule** (stub random) + pesée manuelle |
|
||
| Cloisonnement par site | M3 oui / M4 non | **Oui** (site courant) + numéro par site |
|
||
| Numérotation métier | id technique | **`{siteCode}-TP-{NNNN}`** par site (RG-5.02) |
|
||
| Onglets | présents | **Aucun onglet** : 2 blocs empilés (vide + plein) |
|
||
| Impression | aucune | **Modal d'impression** du ticket (RG-5.08) |
|
||
| Contrepartie | — | **Client / Fournisseur / Autre** (conditionnel, RG-5.03) |
|
||
|
||
## Points résolus côté back
|
||
|
||
| # | Zone d'ombre | Résolution (cf. `spec-back.md`) |
|
||
|---|---|---|
|
||
| 1 | Module | **Nouveau module `Logistique`** (§ 2.1) |
|
||
| 2 | Pont bascule | **Stub** poids aléatoire ∈ [10000,50000], interface réutilisable, driver réel HP (§ 2.6) |
|
||
| 3 | DSD | **Compteur de pesée par site**, +1 par pesée ; manuel = dernier +1 (§ 2.7) |
|
||
| 4 | Poids net | **plein − vide**, calculé serveur (§ 2.8) |
|
||
| 5 | Numérotation | **`{siteCode}-TP-{NNNN}`** par site, séquence verrouillée (§ 2.5) ; ajout `site.code` |
|
||
| 6 | Contrepartie | `counterpartyType` + FK Client/Supplier ou `otherLabel` (RG-5.03, § 2.9) |
|
||
| 7 | Deux pesées | Colonnes plates `empty_*` / `full_*` ; les 2 blocs supportent bascule + manuelle (§ 2.4) |
|
||
| 8 | Impression | Modal d'impression front ; bouton dispo en modif seulement (RG-5.08, § 2.12) |
|
||
| 9 | Masque immat | `XX-000-XX` + « Tout format », connectés entre blocs (RG-5.01, § 2.10) |
|
||
| 10 | RBAC | `logistique.weighing_tickets.view/manage` ; Usine = Tout ; Compta + Commerciale sans accès (§ 5.2) |
|
||
|
||
---
|
||
|
||
## 📦 Tickets Lesstime générés
|
||
|
||
**TaskGroup Lesstime** : **#33 — M5 — Tickets de pesée** (projet `ERP / Starseed`, projectId=6) — créé le 17/06/2026, 12 tickets au statut « Prêt à dev ».
|
||
|
||
| # | ERP | Ticket | Effort | Tag |
|
||
|---|---|---|---|---|
|
||
| 1.1 | ERP-181 | Scaffolder le module Logistique + RBAC | M | Backend |
|
||
| 1.2 | ERP-182 | Migrer le schéma M5 (site.code, compteurs, weighing_ticket) | M | Backend |
|
||
| 1.3 | ERP-183 | Créer l'entité WeighingTicket + repository + contrat sérialisation | M | Backend |
|
||
| 1.4 | ERP-184 | Implémenter la pesée pont bascule (stub + DSD + endpoint) | M | Backend |
|
||
| 1.5 | ERP-185 | Créer Provider + Processor (numérotation, RG, normalisation) | L | Backend |
|
||
| 1.6 | ERP-186 | Implémenter l'export XLSX | S | Backend |
|
||
| 1.7 | ERP-187 | Tests PHPUnit RG-5.01→5.10 + capture contrat JSON | M | Backend |
|
||
| 1.8 | ERP-188 | Créer la page liste `/weighing-tickets` + export | M | Frontend |
|
||
| 1.9 | ERP-189 | Implémenter l'écran Ajouter (blocs vide+plein, pesée, masque immat) | L | Frontend |
|
||
| 1.10 | ERP-190 | Implémenter l'écran Modification + déclenchement impression | M | Frontend |
|
||
| 1.11 | ERP-191 | i18n + libellés + branchement site courant | S | Frontend |
|
||
| 1.12 | ERP-192 | **Bon d'impression du ticket de pesée — OWNER Tristan** | — | Frontend |
|