feat(commercial) : referentiel pays (country) en base + branchement front (ERP-116) (#79)
Auto Tag Develop / tag (push) Successful in 9s
Auto Tag Develop / tag (push) Successful in 9s
## Objectif (ERP-116, 1re iteration minimale) Sortir la liste des pays du **code en dur** cote front et la poser en base comme **referentiel `country`**, source unique du select pays. **Perimetre volontairement minimal** : code ISO + libelle + ordre uniquement — **aucune longueur bancaire/fiscale** (numero de compte, IBAN, TVA, BIC, SIREN) a ce stade. ## Backend - Entite `Country` (`code` ISO 3166-1 alpha-2 unique, `name`, `position`), calquee sur `Bank` : referentiel statique **lecture seule** (`GetCollection` + `Get`), gating `commercial.clients.view OR commercial.suppliers.view`. - Migration `Version20260609100000` : table `country` + `COMMENT ON COLUMN` + seed des **6 pays** (France, Allemagne, Belgique, Espagne, Italie, Royaume-Uni), `ON CONFLICT DO NOTHING`. - `CommercialReferentialFixtures` : re-seed des pays en dev/test. - Garde-fous : ajout au `ColumnCommentsCatalog` + whitelist `EntitiesAreTimestampableBlamableTest`. ## Frontend - `useClientReferentials` charge `/countries` (value = **nom** du pays : l'adresse stocke `country` en chaine libre, **pas de FK ni migration de donnees**). - Les 3 listes `countryOptions` en dur (clients new / edit / consultation) sont supprimees ; la consultation derive ses options de l'embed. ## Hors-scope (iterations suivantes du ticket) - Longueurs bancaires/fiscales par pays + validation associee. - FK `country_id` sur les adresses + migration de donnees. ## Tests - Back : suite complete verte (583), tests API dedies countries (200/seed/405/403/401). - Front : Vitest vert (256), spec `useClientReferentials` mise a jour. - Migration appliquee en dev + test. --------- Co-authored-by: Matthieu <contact@malio.fr> Reviewed-on: #79
This commit was merged in pull request #79.
This commit is contained in:
@@ -30,6 +30,10 @@ describe('useClientReferentials.loadCommon (resilience ERP-102)', () => {
|
||||
if (url === '/sites') {
|
||||
return Promise.resolve({ member: [{ '@id': '/api/sites/1', name: 'Chatellerault', postalCode: '86100' }] })
|
||||
}
|
||||
if (url === '/countries') {
|
||||
// Pays : value === label === name (l'adresse stocke le nom).
|
||||
return Promise.resolve({ member: [{ '@id': '/api/countries/1', code: 'FR', name: 'France' }] })
|
||||
}
|
||||
return Promise.resolve({
|
||||
member: [{ '@id': '/api/x/1', code: 'X', label: 'Libelle X' }],
|
||||
})
|
||||
@@ -44,6 +48,8 @@ describe('useClientReferentials.loadCommon (resilience ERP-102)', () => {
|
||||
expect(refs.sites.value).toEqual([{ value: '/api/sites/1', label: '86' }])
|
||||
expect(refs.tvaModes.value).toEqual([{ value: '/api/x/1', label: 'Libelle X' }])
|
||||
expect(refs.banks.value).toEqual([{ value: '/api/x/1', label: 'Libelle X' }])
|
||||
// Pays : value = nom du pays (et non l'IRI).
|
||||
expect(refs.countries.value).toEqual([{ value: 'France', label: 'France' }])
|
||||
|
||||
// Seul le select en echec reste vide.
|
||||
expect(refs.categories.value).toEqual([])
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ref } from 'vue'
|
||||
/**
|
||||
* Charge les referentiels (listes courtes) alimentant les selects de l'ecran
|
||||
* « Ajouter un client » : categories, sites, modes de TVA, delais et types de
|
||||
* reglement, banques, et les listes distributeurs / courtiers.
|
||||
* reglement, banques, pays, et les listes distributeurs / courtiers.
|
||||
*
|
||||
* Toutes les collections sont recuperees en entier via l'echappatoire prevue
|
||||
* `?pagination=false` (referentiels de quelques dizaines d'entrees max), avec
|
||||
@@ -57,6 +57,11 @@ interface ClientMember extends HydraMember {
|
||||
companyName: string
|
||||
}
|
||||
|
||||
interface CountryMember extends HydraMember {
|
||||
code: string
|
||||
name: string
|
||||
}
|
||||
|
||||
const LD_JSON_HEADERS = { Accept: 'application/ld+json' }
|
||||
|
||||
export function useClientReferentials() {
|
||||
@@ -68,6 +73,7 @@ export function useClientReferentials() {
|
||||
const paymentDelays = ref<RefOption[]>([])
|
||||
const paymentTypes = ref<PaymentTypeOption[]>([])
|
||||
const banks = ref<RefOption[]>([])
|
||||
const countries = ref<RefOption[]>([])
|
||||
const distributors = ref<ClientOption[]>([])
|
||||
const brokers = ref<ClientOption[]>([])
|
||||
|
||||
@@ -116,6 +122,12 @@ export function useClientReferentials() {
|
||||
.then((types) => { paymentTypes.value = types.map(t => ({ value: t['@id'], label: t.label, code: t.code })) }),
|
||||
fetchAll<ReferentialMember>('/banks')
|
||||
.then((banksList) => { banks.value = banksList.map(b => ({ value: b['@id'], label: b.label })) }),
|
||||
// Pays (ERP-116) : la valeur d'option est le NOM du pays (et non l'IRI),
|
||||
// car l'adresse stocke `country` en chaine libre (« France »...). On
|
||||
// conserve ainsi la compatibilite avec les adresses existantes sans FK
|
||||
// ni migration de donnees a ce stade. value === label.
|
||||
fetchAll<CountryMember>('/countries')
|
||||
.then((list) => { countries.value = list.map(c => ({ value: c.name, label: c.name })) }),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -144,6 +156,7 @@ export function useClientReferentials() {
|
||||
paymentDelays,
|
||||
paymentTypes,
|
||||
banks,
|
||||
countries,
|
||||
distributors,
|
||||
brokers,
|
||||
loadCommon,
|
||||
|
||||
@@ -51,6 +51,11 @@ interface ReferentialMember extends HydraMember {
|
||||
label: string
|
||||
}
|
||||
|
||||
interface CountryMember extends HydraMember {
|
||||
code: string
|
||||
name: string
|
||||
}
|
||||
|
||||
const LD_JSON_HEADERS = { Accept: 'application/ld+json' }
|
||||
|
||||
export function useSupplierReferentials() {
|
||||
@@ -62,6 +67,7 @@ export function useSupplierReferentials() {
|
||||
const paymentDelays = ref<RefOption[]>([])
|
||||
const paymentTypes = ref<PaymentTypeOption[]>([])
|
||||
const banks = ref<RefOption[]>([])
|
||||
const countries = ref<RefOption[]>([])
|
||||
|
||||
/** Recupere une collection complete (pagination desactivee) en Hydra. */
|
||||
async function fetchAll<T extends HydraMember>(
|
||||
@@ -103,6 +109,13 @@ export function useSupplierReferentials() {
|
||||
.then((types) => { paymentTypes.value = types.map(t => ({ value: t['@id'], label: t.label, code: t.code })) }),
|
||||
fetchAll<ReferentialMember>('/banks')
|
||||
.then((banksList) => { banks.value = banksList.map(b => ({ value: b['@id'], label: b.label })) }),
|
||||
// Pays (ERP-116) : la valeur d'option est le NOM du pays (et non l'IRI),
|
||||
// car l'adresse stocke `country` en chaine libre (« France »...). On
|
||||
// conserve ainsi la compatibilite avec les adresses existantes sans FK
|
||||
// ni migration de donnees a ce stade. value === label. Aligne sur les
|
||||
// clients (`useClientReferentials`) pour une liste de pays identique.
|
||||
fetchAll<CountryMember>('/countries')
|
||||
.then((list) => { countries.value = list.map(c => ({ value: c.name, label: c.name })) }),
|
||||
])
|
||||
}
|
||||
|
||||
@@ -113,6 +126,7 @@ export function useSupplierReferentials() {
|
||||
paymentDelays,
|
||||
paymentTypes,
|
||||
banks,
|
||||
countries,
|
||||
loadCommon,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -553,10 +553,21 @@ const contactOptions = computed<RefOption[]>(() =>
|
||||
})),
|
||||
)
|
||||
|
||||
const countryOptions: RefOption[] = [
|
||||
{ value: 'France', label: 'France' },
|
||||
{ value: 'Espagne', label: 'Espagne' },
|
||||
]
|
||||
// Pays : referentiel `country` charge via l'API (ERP-116), en remplacement de
|
||||
// l'ancienne liste codee en dur. Valeur = nom du pays (l'adresse stocke
|
||||
// `country` en chaine libre, donc value === label). On merge la valeur deja
|
||||
// stockee sur chaque adresse (embed) — comme les autres selects de cet ecran —
|
||||
// pour ne pas vider le select si `/countries` echoue (resilience ERP-102) ou si
|
||||
// un pays historique n'appartient pas au referentiel.
|
||||
const embedCountryOptions = computed<RefOption[]>(() =>
|
||||
mergeOptions([], (client.value?.addresses ?? [])
|
||||
.map(a => a.country)
|
||||
.filter((c): c is string => !!c)
|
||||
.map(c => ({ value: c, label: c }))),
|
||||
)
|
||||
const countryOptions = computed<RefOption[]>(() =>
|
||||
mergeOptions(referentials.countries.value, embedCountryOptions.value),
|
||||
)
|
||||
|
||||
const relationOptions = computed<RefOption[]>(() => [
|
||||
{ value: 'distributeur', label: t('commercial.clients.form.main.relationDistributor') },
|
||||
|
||||
@@ -384,10 +384,18 @@ const relationOptions = computed<SelectOption[]>(() => [
|
||||
{ value: 'courtier', label: t('commercial.clients.form.main.relationBroker') },
|
||||
])
|
||||
|
||||
const countryOptions: SelectOption[] = [
|
||||
{ value: 'France', label: 'France' },
|
||||
{ value: 'Espagne', label: 'Espagne' },
|
||||
]
|
||||
// Pays (ERP-116) : options construites depuis l'EMBED des adresses (jamais via
|
||||
// GET /countries, sur le meme principe que les autres selects de consultation
|
||||
// — en 403 pour les roles metier non-admin). Valeur = nom du pays stocke tel
|
||||
// quel dans l'adresse, donc value === label ; suffit a afficher le libelle en
|
||||
// lecture seule.
|
||||
const countryOptions = computed<SelectOption[]>(() =>
|
||||
[...new Set(
|
||||
(client.value?.addresses ?? [])
|
||||
.map(a => a.country)
|
||||
.filter((c): c is string => !!c),
|
||||
)].map(c => ({ value: c, label: c })),
|
||||
)
|
||||
|
||||
// Selects comptables : libelle issu de l'embed (option unique ou vide).
|
||||
const tvaModeOptions = computed(() => referentialOptionOf(client.value?.tvaMode))
|
||||
|
||||
@@ -778,11 +778,17 @@ const contactOptions = computed<RefOption[]>(() =>
|
||||
})),
|
||||
)
|
||||
|
||||
// Pays disponibles (France preselectionnee par defaut sur chaque adresse).
|
||||
const countryOptions: RefOption[] = [
|
||||
{ value: 'France', label: 'France' },
|
||||
{ value: 'Espagne', label: 'Espagne' },
|
||||
]
|
||||
// Pays disponibles : referentiel `country` charge via l'API (ERP-116), en
|
||||
// remplacement de l'ancienne liste codee en dur. France reste preselectionnee
|
||||
// par defaut sur chaque adresse (cf. valeur initiale du draft d'adresse) : on
|
||||
// garantit donc sa presence en fallback si `/countries` echoue (resilience
|
||||
// ERP-102), pour ne pas afficher un select vide sur une valeur deja soumise.
|
||||
const countryOptions = computed<RefOption[]>(() => {
|
||||
const list = referentials.countries.value
|
||||
return list.some(c => c.value === 'France')
|
||||
? list
|
||||
: [{ value: 'France', label: 'France' }, ...list]
|
||||
})
|
||||
|
||||
// « + Adresse » desactive tant que la derniere adresse n'est pas valide.
|
||||
const canAddAddress = computed(() => {
|
||||
|
||||
@@ -515,10 +515,19 @@ const contactOptions = computed<RefOption[]>(() =>
|
||||
})),
|
||||
)
|
||||
|
||||
const countryOptions: RefOption[] = [
|
||||
{ value: 'France', label: 'France' },
|
||||
{ value: 'Espagne', label: 'Espagne' },
|
||||
]
|
||||
// Pays : referentiel `country` charge via l'API (ERP-116), aligne sur l'ecran
|
||||
// client. On merge la valeur deja stockee sur chaque adresse (embed) — comme les
|
||||
// autres selects de cet ecran — pour ne pas vider le select si `/countries`
|
||||
// echoue (resilience ERP-102) ou si un pays historique n'est plus au referentiel.
|
||||
const embedCountryOptions = computed<RefOption[]>(() =>
|
||||
mergeOptions([], (supplier.value?.addresses ?? [])
|
||||
.map(a => a.country)
|
||||
.filter((c): c is string => !!c)
|
||||
.map(c => ({ value: c, label: c }))),
|
||||
)
|
||||
const countryOptions = computed<RefOption[]>(() =>
|
||||
mergeOptions(referentials.countries.value, embedCountryOptions.value),
|
||||
)
|
||||
|
||||
// Selects comptables : referentiel UNION valeur courante de l'embed (libelle).
|
||||
const tvaModeOptions = computed(() => mergeOptions(referentials.tvaModes.value, referentialOptionOf(supplier.value?.tvaMode)))
|
||||
|
||||
@@ -361,10 +361,16 @@ const allSiteOptions = computed<SelectOption[]>(() =>
|
||||
})),
|
||||
)
|
||||
|
||||
const countryOptions: SelectOption[] = [
|
||||
{ value: 'France', label: 'France' },
|
||||
{ value: 'Espagne', label: 'Espagne' },
|
||||
]
|
||||
// Pays (consultation, lecture seule) : derive des adresses du fournisseur, comme
|
||||
// l'ecran client. Le referentiel `country` (ERP-116) n'est pas charge ici, l'ecran
|
||||
// n'affiche que les valeurs deja stockees.
|
||||
const countryOptions = computed<SelectOption[]>(() =>
|
||||
[...new Set(
|
||||
(supplier.value?.addresses ?? [])
|
||||
.map(a => a.country)
|
||||
.filter((c): c is string => !!c),
|
||||
)].map(c => ({ value: c, label: c })),
|
||||
)
|
||||
|
||||
// Selects comptables : libelle issu de l'embed (option unique ou vide).
|
||||
const tvaModeOptions = computed(() => referentialOptionOf(supplier.value?.tvaMode))
|
||||
|
||||
@@ -646,11 +646,15 @@ const contactOptions = computed<RefOption[]>(() =>
|
||||
})),
|
||||
)
|
||||
|
||||
// Pays disponibles (France preselectionnee par defaut sur chaque adresse).
|
||||
const countryOptions: RefOption[] = [
|
||||
{ value: 'France', label: 'France' },
|
||||
{ value: 'Espagne', label: 'Espagne' },
|
||||
]
|
||||
// Pays : referentiel `country` charge via l'API (ERP-116), aligne sur l'ecran
|
||||
// client. France garantie en tete pour rester preselectionnable par defaut sur
|
||||
// chaque adresse meme si `/countries` echoue (resilience ERP-102).
|
||||
const countryOptions = computed<RefOption[]>(() => {
|
||||
const list = referentials.countries.value
|
||||
return list.some(c => c.value === 'France')
|
||||
? list
|
||||
: [{ value: 'France', label: 'France' }, ...list]
|
||||
})
|
||||
|
||||
// « + Adresse » desactive tant que la derniere adresse n'est pas valide.
|
||||
const canAddAddress = computed(() => {
|
||||
|
||||
Reference in New Issue
Block a user