fix(commercial) : rendre obligatoires les 6 champs comptables — 422 back par champ + bouton « Valider » grisé tant que l'onglet est incomplet (ERP-110)

spec-front marquait SIREN / N° compte / Mode TVA / N° TVA / Délai / Type de règlement obligatoires, mais ni le back (colonnes nullable, aucun NotBlank) ni le front (canValidateAccounting ne gardait que bank/RIB) ne l'imposaient : on validait un onglet vide.

- Back : ClientAccountingCompletenessValidator (calqué sur RG-1.04) invoqué par le ClientProcessor quand les 6 champs sont présents dans le payload (validation d'onglet) → 422 par propertyPath. PATCH partiel ciblé non impacté.
- Front : helper hasAllRequiredAccountingFields + canValidateAccounting étendu dans new.vue / edit.vue (cohérent avec les onglets Contact/Adresse).
- Spec-back : RG-1.30 documente la règle et résout l'incohérence spec-front/spec-back.
This commit is contained in:
2026-06-04 14:45:14 +02:00
parent ad32d8147d
commit c2282fdac5
8 changed files with 263 additions and 0 deletions
@@ -411,6 +411,7 @@ import {
} from '~/modules/commercial/utils/clientEdit'
import {
buildClientFormTabKeys,
hasAllRequiredAccountingFields,
hasAtLeastOneValidContact,
isBankRequiredForPaymentType,
isBillingEmailRequired,
@@ -878,6 +879,7 @@ function ribIsComplete(rib: { label: string | null, bic: string | null, iban: st
}
const canValidateAccounting = computed(() => {
if (!hasAllRequiredAccountingFields(accounting)) return false
if (isBankRequired.value && accounting.bankIri === null) return false
if (isRibRequired.value && !ribs.value.some(ribIsComplete)) return false
return true
@@ -382,6 +382,7 @@ import { useClientFormErrors } from '~/modules/commercial/composables/useClientF
import {
buildClientFormTabKeys,
CLIENT_FORM_PLACEHOLDER_TABS,
hasAllRequiredAccountingFields,
hasAtLeastOneValidContact,
isBankRequiredForPaymentType,
isBillingEmailRequired,
@@ -860,8 +861,11 @@ function ribIsComplete(rib: RibFormDraft): boolean {
return filled(rib.label) && filled(rib.bic) && filled(rib.iban)
}
// RG-1.30 : les 6 champs scalaires obligatoires (comme les onglets Contact /
// Adresse, le bouton reste desactive tant que l'onglet n'est pas complet).
// RG-1.12 : banque requise si VIREMENT. RG-1.13 : >= 1 RIB complet si LCR.
const canValidateAccounting = computed(() => {
if (!hasAllRequiredAccountingFields(accounting)) return false
if (isBankRequired.value && (accounting.bankIri === null)) return false
if (isRibRequired.value && !ribs.value.some(ribIsComplete)) return false
return true
@@ -4,6 +4,7 @@ import {
buildClientFormTabKeys,
canSelectDeliveryOrBilling,
canSelectProspect,
hasAllRequiredAccountingFields,
hasAtLeastOneValidContact,
isBankRequiredForPaymentType,
isBillingEmailRequired,
@@ -209,3 +210,36 @@ describe('regles type de reglement (RG-1.12 / RG-1.13)', () => {
expect(isRibRequiredForPaymentType(null)).toBe(false)
})
})
describe('hasAllRequiredAccountingFields (RG-1.30)', () => {
const complete = {
siren: '123456789',
accountNumber: '00012345678',
nTva: 'FR12345678901',
tvaModeIri: '/api/tva_modes/1',
paymentDelayIri: '/api/payment_delays/1',
paymentTypeIri: '/api/payment_types/1',
}
it('vrai quand les six champs obligatoires sont remplis', () => {
expect(hasAllRequiredAccountingFields(complete)).toBe(true)
})
it('faux si un champ est manquant (null ou vide apres trim)', () => {
expect(hasAllRequiredAccountingFields({ ...complete, siren: null })).toBe(false)
expect(hasAllRequiredAccountingFields({ ...complete, accountNumber: ' ' })).toBe(false)
expect(hasAllRequiredAccountingFields({ ...complete, tvaModeIri: null })).toBe(false)
expect(hasAllRequiredAccountingFields({ ...complete, paymentTypeIri: null })).toBe(false)
})
it('faux quand tout est vide (onglet non rempli)', () => {
expect(hasAllRequiredAccountingFields({
siren: null,
accountNumber: null,
nTva: null,
tvaModeIri: null,
paymentDelayIri: null,
paymentTypeIri: null,
})).toBe(false)
})
})
@@ -208,3 +208,32 @@ export function isBankRequiredForPaymentType(code: string | null | undefined): b
export function isRibRequiredForPaymentType(code: string | null | undefined): boolean {
return code === PAYMENT_TYPE_LCR
}
/** Sous-ensemble du brouillon comptable portant les six champs obligatoires. */
export interface AccountingRequiredDraft {
siren: string | null
accountNumber: string | null
nTva: string | null
tvaModeIri: string | null
paymentDelayIri: string | null
paymentTypeIri: string | null
}
/**
* RG-1.30 : les six champs scalaires de l'onglet Comptabilite sont obligatoires
* pour valider l'onglet (SIREN, N de compte, Mode de TVA, N de TVA, Delai de
* reglement, Type de reglement). bank / RIB restent conditionnels (RG-1.12 /
* RG-1.13) et sont evalues a part. Miroir front du
* ClientAccountingCompletenessValidator : meme gate que les onglets Contact /
* Adresse (bouton « Valider » desactive tant que l'onglet n'est pas complet).
*/
export function hasAllRequiredAccountingFields(accounting: AccountingRequiredDraft): boolean {
const filled = (v: string | null): boolean => v !== null && v.trim() !== ''
return filled(accounting.siren)
&& filled(accounting.accountNumber)
&& filled(accounting.nTva)
&& filled(accounting.tvaModeIri)
&& filled(accounting.paymentDelayIri)
&& filled(accounting.paymentTypeIri)
}