feat(commercial) : catégories de type Adresse pour les blocs adresse (client + fournisseur)
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 51s
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 2m24s

Introduit un CategoryType dédié ADRESSE (module Catalog) consommé par le champ
« Catégorie » des blocs adresse, en remplacement de la réutilisation détournée des
types CLIENT/FOURNISSEUR.

- Migration de seed du type ADRESSE + 6 catégories (Siège, Contact issues,
  Facturation, Livraison, Approvisionnement, Méthaniseur) ; fixtures alignées.
- ClientAddress : validation blacklist (DISTRIBUTEUR/COURTIER) remplacée par une
  whitelist « catégories de type ADRESSE uniquement ».
- SupplierAddress : type requis FOURNISSEUR -> ADRESSE (le bloc principal
  fournisseur reste en FOURNISSEUR).
- Front : ref dédiée addressCategories (typeCode=ADRESSE) dans les composables
  référentiels client et fournisseur ; pages new/edit câblées sur les blocs adresse.
- Tests : CategoryAdresseSeedTest + adaptation des tests d'adresse client/fournisseur.
This commit is contained in:
2026-06-25 09:22:24 +02:00
parent 2e50a760c6
commit 55c136df66
22 changed files with 444 additions and 168 deletions
@@ -479,9 +479,6 @@ import { readHistoryTab } from '~/shared/utils/historyTab'
const SIREN_MASK = '#########'
const EMPLOYEES_MASK = '#######'
// Codes de categorie interdits sur une adresse (RG-1.29, ERP-78).
const FORBIDDEN_ADDRESS_CATEGORY_CODES = ['DISTRIBUTEUR', 'COURTIER']
const { t } = useI18n()
const api = useApi()
const toast = useToast()
@@ -573,15 +570,17 @@ function mergeOptions<T extends { value: string }>(primary: T[], extra: T[]): T[
return [...primary, ...extra.filter(o => !seen.has(o.value))]
}
const embedCategoryOptions = computed<CategoryOption[]>(() => {
const fromClient = categoryOptionsOf(client.value?.categories)
const fromAddresses = (client.value?.addresses ?? []).flatMap(a => categoryOptionsOf(a.categories))
return mergeOptions(fromClient, fromAddresses)
})
const mainCategoryOptions = computed(() => mergeOptions(referentials.categories.value, embedCategoryOptions.value))
// Categories autorisees sur une adresse : toutes SAUF DISTRIBUTEUR/COURTIER (RG-1.29).
// Categories du formulaire principal (type CLIENT) : referentiel UNION categories
// embarquees du client (fallback si le referentiel n'est pas chargeable).
const embedClientCategoryOptions = computed<CategoryOption[]>(() => categoryOptionsOf(client.value?.categories))
const mainCategoryOptions = computed(() => mergeOptions(referentials.categories.value, embedClientCategoryOptions.value))
// Categories des blocs adresse (type ADRESSE) : referentiel dedie UNION categories
// embarquees des adresses (fallback meme fonction qu'au-dessus).
const embedAddressCategoryOptions = computed<CategoryOption[]>(() =>
mergeOptions([], (client.value?.addresses ?? []).flatMap(a => categoryOptionsOf(a.categories))),
)
const addressCategoryOptions = computed(() =>
mainCategoryOptions.value.filter(c => !FORBIDDEN_ADDRESS_CATEGORY_CODES.includes(c.code)),
mergeOptions(referentials.addressCategories.value, embedAddressCategoryOptions.value),
)
const embedSiteOptions = computed<RefOption[]>(() =>
@@ -456,9 +456,6 @@ const SIREN_MASK = '#########'
// Masque « nombre » du champ Nombre de salaries : chiffres uniquement (max 7).
const EMPLOYEES_MASK = '#######'
// Codes de categorie interdits sur une adresse (RG-1.29, ERP-78).
const FORBIDDEN_ADDRESS_CATEGORY_CODES = ['DISTRIBUTEUR', 'COURTIER']
const { t } = useI18n()
const api = useApi()
const toast = useToast()
@@ -816,10 +813,8 @@ async function submitContacts(): Promise<void> {
const addresses = ref<AddressFormDraft[]>([emptyAddress()])
const addressDegradedNotified = ref(false)
// Categories autorisees sur une adresse : toutes SAUF DISTRIBUTEUR/COURTIER (RG-1.29).
const addressCategoryOptions = computed(() =>
referentials.categories.value.filter(c => !FORBIDDEN_ADDRESS_CATEGORY_CODES.includes(c.code)),
)
// Categories autorisees sur une adresse : taxonomie dediee type ADRESSE.
const addressCategoryOptions = computed(() => referentials.addressCategories.value)
// Contacts deja crees, rattachables a une adresse (M2M, via leur IRI).
const contactOptions = computed<RefOption[]>(() =>
@@ -181,7 +181,7 @@
:model-value="address"
:title="t('commercial.suppliers.form.address.title', { n: index + 1 })"
:last="index === addresses.length - 1"
:category-options="mainCategoryOptions"
:category-options="addressCategoryOptions"
:site-options="siteOptions"
:contact-options="contactOptions"
:country-options="countryOptions"
@@ -536,15 +536,18 @@ function mergeOptions<T extends { value: string }>(primary: T[], extra: T[]): T[
return [...primary, ...extra.filter(o => !seen.has(o.value))]
}
// Categories issues de l'embed (fournisseur + adresses), role-independantes.
const embedCategoryOptions = computed<CategoryOption[]>(() => {
const fromSupplier = categoryOptionsOf(supplier.value?.categories)
const fromAddresses = (supplier.value?.addresses ?? []).flatMap(a => categoryOptionsOf(a.categories))
return mergeOptions(fromSupplier, fromAddresses)
})
// Toutes les categories de type FOURNISSEUR sont autorisees, sur le bloc principal
// comme sur une adresse (pas de restriction Distributeur/Courtier comme au M1 — RG-2.10).
const mainCategoryOptions = computed(() => mergeOptions(referentials.categories.value, embedCategoryOptions.value))
// Categories du formulaire principal (type FOURNISSEUR) : referentiel UNION
// categories embarquees du fournisseur (fallback si referentiel non chargeable).
const embedSupplierCategoryOptions = computed<CategoryOption[]>(() => categoryOptionsOf(supplier.value?.categories))
const mainCategoryOptions = computed(() => mergeOptions(referentials.categories.value, embedSupplierCategoryOptions.value))
// Categories des blocs adresse (type ADRESSE) : referentiel dedie UNION categories
// embarquees des adresses (meme logique de fallback).
const embedAddressCategoryOptions = computed<CategoryOption[]>(() =>
mergeOptions([], (supplier.value?.addresses ?? []).flatMap(a => categoryOptionsOf(a.categories))),
)
const addressCategoryOptions = computed(() =>
mergeOptions(referentials.addressCategories.value, embedAddressCategoryOptions.value),
)
const embedSiteOptions = computed<RefOption[]>(() =>
mergeOptions([], (supplier.value?.addresses ?? []).flatMap(a => siteOptionsOf(a.sites))),
@@ -179,7 +179,7 @@
:model-value="address"
:title="t('commercial.suppliers.form.address.title', { n: index + 1 })"
:last="index === addresses.length - 1"
:category-options="referentials.categories.value"
:category-options="referentials.addressCategories.value"
:site-options="referentials.sites.value"
:contact-options="contactOptions"
:country-options="countryOptions"