feat(commercial) : export XLSX fournisseurs (ERP-91) (#70)
Auto Tag Develop / tag (push) Successful in 7s

Export XLSX du répertoire fournisseurs (spec-back M2 § 4.6), jumeau de l'export client M1. **Stack : cible `feature/ERP-90-rbac-fournisseurs`** (ERP-84→91 non encore mergés dans develop).

## Périmètre
- **`SupplierExportController`** avec `#[Route(priority: 1)]` (anti-conflit API Platform `{id}`) + `is_granted('commercial.suppliers.view')`.
- Mêmes filtres que la liste (`includeArchived`/`archivedOnly`/`search`/`categoryCode`/`siteId`) via `createListQueryBuilder()` partagé avec le `SupplierProvider` ; non archivés par défaut.
- Colonnes : Nom fournisseur, **Contact principal** (Nom + Prénom du `SupplierContact` de plus petit `position`, ERP-106), Tél principal, Tél secondaire, Email, Catégories (CSV), Sites (CSV), **SIREN omis sans `accounting.view`**, Date de création.
- Fichier `repertoire-fournisseurs-{YYYYMMDD}.xlsx`.
- **`hydrateContacts()`** ajouté au repository : chargement batché des contacts en une requête `IN` (anti-N+1). Méthode dédiée à l'export — la liste paginée n'embarque pas les contacts, on ne lui impose pas ce coût.

## Correctif hors-périmètre (signalé)
Tables `supplier*` ajoutées à `ColumnCommentsCatalog` : leurs `COMMENT ON COLUMN` (posés par la migration ERP-85) étaient dropés par le `schema:update --force` du `test-db-setup` et non restaurés (catalogue = source rejouée par `app:apply-column-comments`), cassant `ColumnsHaveSqlCommentTest` dès un re-setup de la base de test. Trou laissé par ERP-85/86, vert tant que personne ne re-setup la base.

## Tests
- `SupplierExportControllerTest` (9 cas) : réponse/filename, exclusion archives, filtre search, contact principal, colonnes catégories/sites, gating SIREN avec/sans `accounting.view`, 403, 401.
- `make test` : 508 tests / 2035 assertions, 0 échec. `php-cs-fixer` clean.

---------

Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #70
Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Co-committed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
This commit was merged in pull request #70.
This commit is contained in:
2026-06-08 07:48:59 +00:00
committed by admin malio
parent 58cbfe4437
commit 97459e798f
5 changed files with 729 additions and 0 deletions
@@ -256,6 +256,95 @@ final class ColumnCommentsCatalog
'iban' => 'IBAN du compte (≤ 34 caracteres).',
'position' => 'Ordre d affichage du RIB dans la liste du client (croissant).',
] + self::timestampableBlamableComments(),
// === M2 Commercial (ERP-85) — miroir des COMMENT de la migration
// Version20260605130000 pour le chemin schema:update (dev/test). ===
'supplier' => [
'_table' => 'Repertoire fournisseurs (M2 Commercial) — entites archivables (is_archived) et soft-deletables (deleted_at, HP M3).',
'id' => 'Identifiant interne auto-incremente.',
'company_name' => 'Raison sociale du fournisseur (stockee en MAJUSCULES). Unique case-insensitive parmi les actifs non archives/non supprimes (uq_supplier_company_name_active, § 2.6).',
'description' => 'Onglet Information : description libre. Obligatoire pour le role Commerciale (RG-2.03), optionnel sinon.',
'competitors' => 'Onglet Information : concurrents identifies (texte libre ≤ 255). Obligatoire role Commerciale (RG-2.03).',
'founded_at' => 'Onglet Information : date de creation de l entreprise. Obligatoire role Commerciale (RG-2.03).',
'employees_count' => 'Onglet Information : effectif (entier >= 0). Obligatoire role Commerciale (RG-2.03).',
'revenue_amount' => 'Onglet Information : chiffre d affaires (NUMERIC 15,2). Obligatoire role Commerciale (RG-2.03).',
'director_name' => 'Onglet Information : nom du dirigeant. Obligatoire role Commerciale (RG-2.03).',
'profit_amount' => 'Onglet Information : resultat / benefice (NUMERIC 15,2). Obligatoire role Commerciale (RG-2.03).',
'volume_forecast' => 'Onglet Information : volume previsionnel (entier >= 0) — specifique fournisseur. Obligatoire role Commerciale (RG-2.03).',
'siren' => 'Onglet Comptabilite : SIREN (9 chiffres attendus). NON unique — peut etre partage entre etablissements (§ 2.6).',
'account_number' => 'Onglet Comptabilite : numero de compte comptable du fournisseur.',
'tva_mode_id' => 'Onglet Comptabilite : mode de TVA applique — FK -> tva_mode.id (referentiel partage M1), ON DELETE RESTRICT.',
'n_tva' => 'Onglet Comptabilite : numero de TVA intracommunautaire.',
'payment_delay_id' => 'Onglet Comptabilite : delai de reglement — FK -> payment_delay.id (M1), ON DELETE RESTRICT.',
'payment_type_id' => 'Onglet Comptabilite : type de reglement — FK -> payment_type.id (M1), ON DELETE RESTRICT. Pilote RG-2.07 (Banque si VIREMENT) et RG-2.08 (RIB).',
'bank_id' => 'Onglet Comptabilite : banque — FK -> bank.id (M1), ON DELETE RESTRICT. Obligatoire ssi payment_type = VIREMENT (RG-2.07), null sinon.',
'is_archived' => 'Drapeau fonctionnel d archivage — masque par defaut dans la liste. Bascule via permission commercial.suppliers.archive.',
'archived_at' => 'Horodatage de l archivage — pose quand is_archived passe a vrai, remis a null a la restauration.',
'deleted_at' => 'Horodatage du soft-delete technique (HP M3) — non expose par l API au M2. Null = ligne active.',
] + self::timestampableBlamableComments(),
'supplier_category' => [
'_table' => 'Jointure M2M supplier <-> category (Catalog) — categories de type FOURNISSEUR du fournisseur, au moins une obligatoire (RG-2.10).',
'supplier_id' => 'FK -> supplier.id, ON DELETE CASCADE — fournisseur porteur de la categorie.',
'category_id' => 'FK -> category.id, ON DELETE RESTRICT — categorie de type FOURNISSEUR rattachee au fournisseur (RG-2.10).',
],
'supplier_contact' => [
'_table' => 'Contacts d un fournisseur (1:n) — au moins firstName OU lastName par contact (RG-2.04).',
'id' => 'Identifiant interne auto-incremente.',
'supplier_id' => 'FK -> supplier.id, ON DELETE CASCADE — fournisseur proprietaire du contact.',
'first_name' => 'Prenom du contact (capitalise serveur). first_name OU last_name obligatoire (RG-2.04, chk_supplier_contact_name).',
'last_name' => 'Nom du contact (capitalise serveur). first_name OU last_name obligatoire (RG-2.04, chk_supplier_contact_name).',
'job_title' => 'Fonction / intitule de poste du contact (≤ 120 caracteres).',
'phone_primary' => 'Telephone principal du contact — chiffres uniquement (normalisation serveur).',
'phone_secondary' => 'Telephone secondaire du contact — chiffres uniquement (normalisation serveur).',
'email' => 'Email du contact (lowercase serveur).',
'position' => 'Ordre d affichage du contact dans la liste du fournisseur (croissant).',
] + self::timestampableBlamableComments(),
'supplier_address' => [
'_table' => 'Adresses d un fournisseur (1:n) — type PROSPECT/DEPART/RENDU exclusif (RG-2.09), >= 1 site rattache (RG-2.06).',
'id' => 'Identifiant interne auto-incremente.',
'supplier_id' => 'FK -> supplier.id, ON DELETE CASCADE — fournisseur proprietaire de l adresse.',
'address_type' => 'Type d adresse : PROSPECT | DEPART | RENDU (radio exclusif par construction — RG-2.09, chk_supplier_address_type).',
'country' => 'Pays de l adresse — defaut France.',
'postal_code' => 'Code postal (4-5 chiffres attendus).',
'city' => 'Ville — preremplie depuis le code postal via API BAN cote front.',
'street' => 'Numero et voie de l adresse.',
'street_complement' => 'Complement d adresse (etage, batiment...) — optionnel.',
'bennes' => 'Nombre de bennes sur le site fournisseur (entier nullable) — specifique fournisseur.',
'triage_provider' => 'Le fournisseur est prestataire de triage sur cette adresse. Faux par defaut.',
'position' => 'Ordre d affichage de l adresse dans la liste du fournisseur (croissant).',
] + self::timestampableBlamableComments(),
'supplier_address_site' => [
'_table' => 'Jointure M2M supplier_address <-> site (Sites) — sites rattaches a l adresse (>= 1 obligatoire, RG-2.06).',
'supplier_address_id' => 'FK -> supplier_address.id, ON DELETE CASCADE — adresse concernee.',
'site_id' => 'FK -> site.id, ON DELETE RESTRICT — site rattache a l adresse.',
],
'supplier_address_contact' => [
'_table' => 'Jointure M2M supplier_address <-> supplier_contact — contacts associes a une adresse.',
'supplier_address_id' => 'FK -> supplier_address.id, ON DELETE CASCADE — adresse concernee.',
'supplier_contact_id' => 'FK -> supplier_contact.id, ON DELETE CASCADE — contact associe a l adresse.',
],
'supplier_address_category' => [
'_table' => 'Jointure M2M supplier_address <-> category — categories d adresse de type FOURNISSEUR (RG-2.10).',
'supplier_address_id' => 'FK -> supplier_address.id, ON DELETE CASCADE — adresse concernee.',
'category_id' => 'FK -> category.id, ON DELETE RESTRICT — categorie d adresse de type FOURNISSEUR (RG-2.10).',
],
'supplier_rib' => [
'_table' => 'Coordonnees bancaires d un fournisseur (1:n) — >= 1 RIB attendu selon le type de reglement (RG-2.08). Tous les champs audites (pas d AuditIgnore).',
'id' => 'Identifiant interne auto-incremente.',
'supplier_id' => 'FK -> supplier.id, ON DELETE CASCADE — fournisseur proprietaire du RIB.',
'label' => 'Libelle du RIB (ex: compte principal).',
'bic' => 'Code BIC/SWIFT de la banque (8 ou 11 caracteres).',
'iban' => 'IBAN du compte (≤ 34 caracteres).',
'position' => 'Ordre d affichage du RIB dans la liste du fournisseur (croissant).',
] + self::timestampableBlamableComments(),
];
}