diff --git a/frontend/modules/commercial/pages/clients/[id]/edit.vue b/frontend/modules/commercial/pages/clients/[id]/edit.vue index 0a2802c..be7ef59 100644 --- a/frontend/modules/commercial/pages/clients/[id]/edit.vue +++ b/frontend/modules/commercial/pages/clients/[id]/edit.vue @@ -956,35 +956,21 @@ function askRemoveRib(index: number): void { } /** - * Valide l'onglet Comptabilite : PATCH des scalaires (groupe client:write:accounting, - * exige accounting.manage cote back) PUIS DELETE/POST/PATCH des RIB sur la - * sous-ressource. Aucun champ main/information dans le payload (mode strict - * RG-1.28 : sinon 403 sur tout le payload). + * Valide l'onglet Comptabilite : POST/PATCH des RIB sur la sous-ressource PUIS + * PATCH des scalaires (groupe client:write:accounting, exige accounting.manage cote + * back) PUIS DELETE des RIB retires. Les RIB crees d'abord : le back valide RG-1.13 + * (LCR => au moins un RIB persiste) sur le PATCH scalaires ; les suppressions en + * dernier (le guard back n'autorise la suppression du dernier RIB qu'une fois quitte + * LCR). Aucun champ main/information dans le payload (mode strict RG-1.28 : sinon + * 403 sur tout le payload). */ async function submitAccounting(): Promise { if (accountingReadonly.value || !canValidateAccounting.value || tabSubmitting.value) return tabSubmitting.value = true accountingErrors.clearErrors() - // Reset des erreurs RIB des le debut : l'etape 1 (PATCH scalaires) peut - // echouer et `return` avant submitRows (qui porte sinon le reset), laissant - // des erreurs de RIB obsoletes affichees sous les blocs. - ribErrors.value = [] try { - // 1) PATCH des scalaires comptables (erreurs inline sur leurs champs). - try { - await api.patch(`/clients/${clientId}`, buildAccountingPayload(accounting, isBankRequired.value), { toast: false }) - } - catch (error) { - accountingErrors.handleApiError(error, { fallbackMessage: t('commercial.clients.toast.error') }) - return - } - - for (const id of removedRibIds.value) { - await api.delete(`/client_ribs/${id}`, {}, { toast: false }) - } - removedRibIds.value = [] - - // 2) POST/PATCH des RIB (erreurs inline par ligne, tous les blocs tentes). + // 1) POST/PATCH des RIB d'abord (erreurs inline par ligne, tous les blocs + // tentes). Le back exige >=1 RIB persiste pour valider une LCR a l'etape 2. // Seuls les blocs RIB TOTALEMENT vides sont ignores : un RIB partiel (ex. // IBAN seul) est soumis -> 422 NotBlank (label / bic / iban) inline. const ribHasError = await submitRows( @@ -1011,6 +997,23 @@ async function submitAccounting(): Promise { rib => rib.id === null && isRibBlank(rib), ) if (ribHasError) return + + // 2) PATCH des scalaires comptables (erreurs inline sur leurs champs). + try { + await api.patch(`/clients/${clientId}`, buildAccountingPayload(accounting, isBankRequired.value), { toast: false }) + } + catch (error) { + accountingErrors.handleApiError(error, { fallbackMessage: t('commercial.clients.toast.error') }) + return + } + + // 3) DELETE des RIB retires : APRES le PATCH scalaires (si on quitte LCR, le + // guard back n'autorise la suppression du dernier RIB qu'une fois le type change). + for (const id of removedRibIds.value) { + await api.delete(`/client_ribs/${id}`, {}, { toast: false }) + } + removedRibIds.value = [] + toast.success({ title: t('commercial.clients.toast.updateSuccess') }) } catch (e) { diff --git a/frontend/modules/commercial/pages/clients/new.vue b/frontend/modules/commercial/pages/clients/new.vue index feff5c6..73b5734 100644 --- a/frontend/modules/commercial/pages/clients/new.vue +++ b/frontend/modules/commercial/pages/clients/new.vue @@ -939,37 +939,20 @@ function askRemoveRib(index: number): void { } /** - * Valide l'onglet Comptabilite : PATCH des scalaires (groupe client:write:accounting) - * PUIS POST des RIB sur /clients/{id}/ribs. Deux appels distincts (mode strict - * RG-1.28 : il n'existe pas d'endpoint /accounting, cf. recon back). + * Valide l'onglet Comptabilite : POST/PATCH des RIB sur /clients/{id}/ribs PUIS + * PATCH des scalaires (groupe client:write:accounting). Les RIB d'abord : le back + * valide RG-1.13 (LCR => au moins un RIB persiste) sur le PATCH scalaires, les RIB + * doivent donc exister en base AVANT (sinon 422 « Au moins un RIB est obligatoire + * pour le type de reglement LCR »). Deux appels distincts (mode strict RG-1.28 : + * il n'existe pas d'endpoint /accounting, cf. recon back). */ async function submitAccounting(): Promise { if (clientId.value === null || !canValidateAccounting.value || tabSubmitting.value) return tabSubmitting.value = true accountingErrors.clearErrors() - // Reset des erreurs RIB des le debut : l'etape 1 (PATCH scalaires) peut - // echouer et `return` avant submitRows (qui porte sinon le reset), laissant - // des erreurs de RIB obsoletes affichees sous les blocs. - ribErrors.value = [] try { - // 1) PATCH des scalaires comptables (erreurs inline sur leurs champs). - try { - await api.patch(`/clients/${clientId.value}`, { - siren: accounting.siren || null, - accountNumber: accounting.accountNumber || null, - tvaMode: accounting.tvaModeIri, - nTva: accounting.nTva || null, - paymentDelay: accounting.paymentDelayIri, - paymentType: accounting.paymentTypeIri, - bank: isBankRequired.value ? accounting.bankIri : null, - }, { toast: false }) - } - catch (error) { - accountingErrors.handleApiError(error, { fallbackMessage: t('commercial.clients.toast.error') }) - return - } - - // 2) POST/PATCH des RIB (erreurs inline par ligne, tous les blocs tentes). + // 1) POST/PATCH des RIB d'abord (erreurs inline par ligne, tous les blocs + // tentes). Le back exige >=1 RIB persiste pour valider une LCR a l'etape 2. // Seuls les blocs RIB TOTALEMENT vides sont ignores : un RIB partiel (ex. // IBAN seul) est soumis -> 422 NotBlank (label / bic / iban) inline. const ribHasError = await submitRows( @@ -997,6 +980,23 @@ async function submitAccounting(): Promise { ) if (ribHasError) return + // 2) PATCH des scalaires comptables (erreurs inline sur leurs champs). + try { + await api.patch(`/clients/${clientId.value}`, { + siren: accounting.siren || null, + accountNumber: accounting.accountNumber || null, + tvaMode: accounting.tvaModeIri, + nTva: accounting.nTva || null, + paymentDelay: accounting.paymentDelayIri, + paymentType: accounting.paymentTypeIri, + bank: isBankRequired.value ? accounting.bankIri : null, + }, { toast: false }) + } + catch (error) { + accountingErrors.handleApiError(error, { fallbackMessage: t('commercial.clients.toast.error') }) + return + } + completeTab('accounting') toast.success({ title: t('commercial.clients.toast.updateSuccess') }) }