Merge branch 'develop' into feature/M6-field-sales
This commit is contained in:
+1
-1
@@ -1,2 +1,2 @@
|
||||
parameters:
|
||||
app.version: '0.1.107'
|
||||
app.version: '0.1.109'
|
||||
|
||||
@@ -26,13 +26,18 @@
|
||||
:error="errors?.firstName"
|
||||
@update:model-value="(v: string) => update('firstName', v)"
|
||||
/>
|
||||
<MalioInputText
|
||||
:model-value="model.jobTitle"
|
||||
:label="t('commercial.clients.form.contact.jobTitle')"
|
||||
:readonly="readonly"
|
||||
:error="errors?.jobTitle"
|
||||
@update:model-value="(v: string) => update('jobTitle', v)"
|
||||
/>
|
||||
<!-- Fonction sur 2 colonnes : on wrappe car MalioInputText
|
||||
(inheritAttrs:false) renvoie `class` sur l'input interne, pas sur la
|
||||
cellule de grille. Le wrapper porte le col-span-2, le champ le remplit. -->
|
||||
<div class="col-span-2">
|
||||
<MalioInputText
|
||||
:model-value="model.jobTitle"
|
||||
:label="t('commercial.clients.form.contact.jobTitle')"
|
||||
:readonly="readonly"
|
||||
:error="errors?.jobTitle"
|
||||
@update:model-value="(v: string) => update('jobTitle', v)"
|
||||
/>
|
||||
</div>
|
||||
<MalioInputEmail
|
||||
:model-value="model.email"
|
||||
:label="t('commercial.clients.form.contact.email')"
|
||||
|
||||
@@ -25,13 +25,18 @@
|
||||
:error="errors?.firstName"
|
||||
@update:model-value="(v: string) => update('firstName', v)"
|
||||
/>
|
||||
<MalioInputText
|
||||
:model-value="model.jobTitle"
|
||||
:label="t('commercial.suppliers.form.contact.jobTitle')"
|
||||
:readonly="readonly"
|
||||
:error="errors?.jobTitle"
|
||||
@update:model-value="(v: string) => update('jobTitle', v)"
|
||||
/>
|
||||
<!-- Fonction sur 2 colonnes : on wrappe car MalioInputText
|
||||
(inheritAttrs:false) renvoie `class` sur l'input interne, pas sur la
|
||||
cellule de grille. Le wrapper porte le col-span-2, le champ le remplit. -->
|
||||
<div class="col-span-2">
|
||||
<MalioInputText
|
||||
:model-value="model.jobTitle"
|
||||
:label="t('commercial.suppliers.form.contact.jobTitle')"
|
||||
:readonly="readonly"
|
||||
:error="errors?.jobTitle"
|
||||
@update:model-value="(v: string) => update('jobTitle', v)"
|
||||
/>
|
||||
</div>
|
||||
<MalioInputEmail
|
||||
:model-value="model.email"
|
||||
:label="t('commercial.suppliers.form.contact.email')"
|
||||
|
||||
@@ -912,17 +912,16 @@ const visibleRibs = computed(() => isRibRequired.value ? ribs.value : [])
|
||||
function onPaymentTypeChange(value: string | number | null): void {
|
||||
accounting.paymentTypeIri = value === null ? null : String(value)
|
||||
if (!isBankRequired.value) accounting.bankIri = null
|
||||
// Les RIB n'ont de sens que pour une LCR (RG-1.13) : on amorce un bloc vide
|
||||
// quand LCR est choisi, sinon on vide la liste — les RIB deja persistes sont
|
||||
// marques pour suppression serveur au prochain enregistrement.
|
||||
// ERP-121 : un RIB est une coordonnee bancaire du client, decouplee du mode de
|
||||
// reglement. Au passage hors-LCR on ne SUPPRIME plus les RIB existants : ils
|
||||
// restent en base, simplement masques a l'ecran (visibleRibs = []), et
|
||||
// reapparaissent tels quels si l'on repasse en LCR. Seule la corbeille d'un
|
||||
// bloc (askRemoveRib) retire reellement un RIB.
|
||||
if (isRibRequired.value) {
|
||||
if (ribs.value.length === 0) ribs.value.push(emptyRib())
|
||||
}
|
||||
else {
|
||||
for (const rib of ribs.value) {
|
||||
if (rib.id != null) removedRibIds.value.push(rib.id)
|
||||
}
|
||||
ribs.value = []
|
||||
// Hors-LCR : on nettoie seulement les erreurs inline (plus affichees).
|
||||
ribErrors.value = []
|
||||
}
|
||||
}
|
||||
@@ -951,50 +950,58 @@ function askRemoveRib(index: number): void {
|
||||
/**
|
||||
* 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).
|
||||
* back) PUIS DELETE des RIB explicitement retires. Les RIB crees d'abord : le back
|
||||
* valide RG-1.13 (LCR => au moins un RIB persiste) sur le PATCH scalaires.
|
||||
*
|
||||
* ERP-121 : les RIB ne sont (re)soumis QUE sous LCR — hors-LCR ce sont des
|
||||
* coordonnees dormantes conservees telles quelles, masquees a l'ecran et jamais
|
||||
* re-ecrites. `removedRibIds` ne contient plus que les suppressions EXPLICITES
|
||||
* (corbeille d'un bloc, toujours sous LCR), plus l'auto-suppression au changement
|
||||
* de type de reglement. Aucun champ main/information dans le payload (mode strict
|
||||
* RG-1.28 : sinon 403 sur tout le payload).
|
||||
*/
|
||||
async function submitAccounting(): Promise<void> {
|
||||
if (accountingReadonly.value || tabSubmitting.value) return
|
||||
tabSubmitting.value = true
|
||||
accountingErrors.clearErrors()
|
||||
try {
|
||||
// 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.
|
||||
// 1) POST/PATCH des RIB d'abord — UNIQUEMENT sous LCR (erreurs inline par
|
||||
// ligne, tous les blocs tentes). Le back exige >=1 RIB persiste pour valider
|
||||
// une LCR a l'etape 2. Hors-LCR (ERP-121), les RIB sont des coordonnees
|
||||
// dormantes : rien d'editable n'est affiche, on ne les re-soumet pas.
|
||||
// On ne saute une amorce neuve vide QUE s'il reste un autre RIB soumettable :
|
||||
// sinon (ex. l'unique RIB existant supprime, remplace par un bloc vide), on la
|
||||
// soumet pour declencher la 422 NotBlank inline plutot que de laisser le DELETE
|
||||
// echouer en « dernier RIB d'une LCR » (message plat sans propertyPath).
|
||||
const hasSubmittableRib = ribs.value.some(r => r.id !== null || !isRibBlank(r))
|
||||
const ribHasError = await submitRows(
|
||||
ribs.value,
|
||||
ribErrors,
|
||||
async (rib) => {
|
||||
// Edition d'un RIB existant : champ requis vide envoye en `''` (NotBlank
|
||||
// 422) au lieu d'etre omis (sinon le PATCH garderait l'ancienne valeur).
|
||||
const body = buildRibPayload(rib, { forUpdate: rib.id !== null })
|
||||
if (rib.id === null) {
|
||||
const created = await api.post<{ id: number }>(
|
||||
`/clients/${clientId}/ribs`,
|
||||
body,
|
||||
{ headers: { Accept: 'application/ld+json' }, toast: false },
|
||||
)
|
||||
rib.id = created.id
|
||||
}
|
||||
else {
|
||||
await api.patch(`/client_ribs/${rib.id}`, body, { toast: false })
|
||||
}
|
||||
},
|
||||
error => showError(error),
|
||||
// On ne saute une amorce neuve (id null) totalement vide que si un autre RIB
|
||||
// est soumettable. Un RIB existant vide est toujours soumis -> 422 NotBlank
|
||||
// inline (sinon la modif serait perdue en silence avec un faux toast succes).
|
||||
rib => hasSubmittableRib && rib.id === null && isRibBlank(rib),
|
||||
)
|
||||
if (ribHasError) return
|
||||
if (isRibRequired.value) {
|
||||
const hasSubmittableRib = ribs.value.some(r => r.id !== null || !isRibBlank(r))
|
||||
const ribHasError = await submitRows(
|
||||
ribs.value,
|
||||
ribErrors,
|
||||
async (rib) => {
|
||||
// Edition d'un RIB existant : champ requis vide envoye en `''` (NotBlank
|
||||
// 422) au lieu d'etre omis (sinon le PATCH garderait l'ancienne valeur).
|
||||
const body = buildRibPayload(rib, { forUpdate: rib.id !== null })
|
||||
if (rib.id === null) {
|
||||
const created = await api.post<{ id: number }>(
|
||||
`/clients/${clientId}/ribs`,
|
||||
body,
|
||||
{ headers: { Accept: 'application/ld+json' }, toast: false },
|
||||
)
|
||||
rib.id = created.id
|
||||
}
|
||||
else {
|
||||
await api.patch(`/client_ribs/${rib.id}`, body, { toast: false })
|
||||
}
|
||||
},
|
||||
error => showError(error),
|
||||
// On ne saute une amorce neuve (id null) totalement vide que si un autre RIB
|
||||
// est soumettable. Un RIB existant vide est toujours soumis -> 422 NotBlank
|
||||
// inline (sinon la modif serait perdue en silence avec un faux toast succes).
|
||||
rib => hasSubmittableRib && rib.id === null && isRibBlank(rib),
|
||||
)
|
||||
if (ribHasError) return
|
||||
}
|
||||
|
||||
// 2) PATCH des scalaires comptables (erreurs inline sur leurs champs).
|
||||
try {
|
||||
@@ -1005,8 +1012,9 @@ async function submitAccounting(): Promise<void> {
|
||||
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).
|
||||
// 3) DELETE des RIB explicitement retires (corbeille d'un bloc) : APRES le
|
||||
// PATCH scalaires (le guard back refuse la suppression du dernier RIB d'une
|
||||
// LCR). ERP-121 : plus aucune suppression automatique au passage hors-LCR.
|
||||
for (const id of removedRibIds.value) {
|
||||
await api.delete(`/client_ribs/${id}`, {}, { toast: false })
|
||||
}
|
||||
|
||||
@@ -280,7 +280,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useClient } from '~/modules/commercial/composables/useClient'
|
||||
import { buildClientFormTabKeys } from '~/modules/commercial/utils/clientFormRules'
|
||||
import { buildClientFormTabKeys, isRibRequiredForPaymentType } from '~/modules/commercial/utils/clientFormRules'
|
||||
import { readHistoryTab } from '~/shared/utils/historyTab'
|
||||
import {
|
||||
canEditClient,
|
||||
@@ -290,6 +290,7 @@ import {
|
||||
mapAddressView,
|
||||
mapContactToDraft,
|
||||
mapRibToDraft,
|
||||
paymentTypeCodeOf,
|
||||
referentialOptionOf,
|
||||
relationOf,
|
||||
showArchiveAction,
|
||||
@@ -355,9 +356,16 @@ const addressViews = computed(() => {
|
||||
return views.length ? views : [{ draft: emptyAddress(), siteOptions: [], categoryOptions: [] }]
|
||||
})
|
||||
// Exception au placeholder ci-dessus : on n'affiche AUCUN bloc RIB quand le
|
||||
// client n'en a pas (un RIB n'existe que pour un reglement LCR — RG-1.13). Pas
|
||||
// de bloc vierge fantome en consultation.
|
||||
const ribs = computed(() => (client.value?.ribs ?? []).map(mapRibToDraft))
|
||||
// client n'en a pas. Pas de bloc vierge fantome en consultation.
|
||||
// ERP-121 : un client peut desormais conserver des RIB « dormants » apres etre
|
||||
// repasse hors-LCR (on ne les supprime plus). En consultation, decision metier =
|
||||
// on les masque TOTALEMENT : on n'affiche les RIB que si le type de reglement
|
||||
// courant est LCR (le `code` est embarque sous client:read:accounting).
|
||||
const ribs = computed(() =>
|
||||
isRibRequiredForPaymentType(paymentTypeCodeOf(client.value?.paymentType))
|
||||
? (client.value?.ribs ?? []).map(mapRibToDraft)
|
||||
: [],
|
||||
)
|
||||
// Draft comptable (tout null si l'utilisateur n'a pas accounting.view).
|
||||
const accounting = computed(() => mapAccountingDraft(client.value ?? ({} as ClientDetail)))
|
||||
|
||||
|
||||
@@ -881,13 +881,14 @@ function onPaymentTypeChange(value: string | number | null): void {
|
||||
accounting.paymentTypeIri = value === null ? null : String(value)
|
||||
// La banque n'a de sens que pour un virement : on la vide sinon (RG-1.12).
|
||||
if (!isBankRequired.value) accounting.bankIri = null
|
||||
// Les RIB n'ont de sens que pour une LCR (RG-1.13) : on amorce un bloc vide
|
||||
// quand LCR est choisi, on vide la liste sinon (pas de RIB fantome soumis).
|
||||
// ERP-121 : on ne jette plus la saisie RIB au passage hors-LCR. Les blocs sont
|
||||
// masques (visibleRibs = []) mais conserves, et reapparaissent si l'on repasse
|
||||
// en LCR. Ils ne sont persistes qu'a la validation SOUS LCR (cf. submitAccounting),
|
||||
// donc une saisie abandonnee hors-LCR ne cree aucun RIB orphelin.
|
||||
if (isRibRequired.value) {
|
||||
if (ribs.value.length === 0) ribs.value.push(emptyRib())
|
||||
}
|
||||
else {
|
||||
ribs.value = []
|
||||
ribErrors.value = []
|
||||
}
|
||||
}
|
||||
@@ -924,36 +925,41 @@ async function submitAccounting(): Promise<void> {
|
||||
tabSubmitting.value = true
|
||||
accountingErrors.clearErrors()
|
||||
try {
|
||||
// 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.
|
||||
// 1) POST/PATCH des RIB d'abord — UNIQUEMENT sous LCR (erreurs inline par
|
||||
// ligne, tous les blocs tentes). Le back exige >=1 RIB persiste pour valider
|
||||
// une LCR a l'etape 2. Hors-LCR (ERP-121), une saisie RIB eventuellement
|
||||
// restee dans le brouillon est masquee et n'est PAS persistee (pas de RIB
|
||||
// orphelin sur un client en virement).
|
||||
// On ne saute une amorce neuve vide QUE s'il reste un autre RIB soumettable :
|
||||
// sinon (LCR sans aucun RIB rempli) on la soumet -> 422 NotBlank inline.
|
||||
const hasSubmittableRib = ribs.value.some(r => r.id !== null || !isRibBlank(r))
|
||||
const ribHasError = await submitRows(
|
||||
ribs.value,
|
||||
ribErrors,
|
||||
async (rib) => {
|
||||
// Payload partage avec l'edition (buildRibPayload, ERP-119).
|
||||
const body = buildRibPayload(rib)
|
||||
if (rib.id === null) {
|
||||
const created = await api.post<{ id: number }>(
|
||||
`/clients/${clientId.value}/ribs`,
|
||||
body,
|
||||
{ headers: { Accept: 'application/ld+json' }, toast: false },
|
||||
)
|
||||
rib.id = created.id
|
||||
}
|
||||
else {
|
||||
await api.patch(`/client_ribs/${rib.id}`, body, { toast: false })
|
||||
}
|
||||
},
|
||||
error => toast.error({ title: t('commercial.clients.toast.error'), message: apiErrorMessage(error) }),
|
||||
// On ne saute une amorce neuve (id null) totalement vide que si un autre RIB
|
||||
// est soumettable. Un RIB existant vide est toujours soumis -> 422 NotBlank
|
||||
// inline (sinon la modif serait perdue en silence avec un faux toast succes).
|
||||
rib => hasSubmittableRib && rib.id === null && isRibBlank(rib),
|
||||
)
|
||||
if (ribHasError) return
|
||||
if (isRibRequired.value) {
|
||||
const hasSubmittableRib = ribs.value.some(r => r.id !== null || !isRibBlank(r))
|
||||
const ribHasError = await submitRows(
|
||||
ribs.value,
|
||||
ribErrors,
|
||||
async (rib) => {
|
||||
// Payload partage avec l'edition (buildRibPayload, ERP-119).
|
||||
const body = buildRibPayload(rib)
|
||||
if (rib.id === null) {
|
||||
const created = await api.post<{ id: number }>(
|
||||
`/clients/${clientId.value}/ribs`,
|
||||
body,
|
||||
{ headers: { Accept: 'application/ld+json' }, toast: false },
|
||||
)
|
||||
rib.id = created.id
|
||||
}
|
||||
else {
|
||||
await api.patch(`/client_ribs/${rib.id}`, body, { toast: false })
|
||||
}
|
||||
},
|
||||
error => toast.error({ title: t('commercial.clients.toast.error'), message: apiErrorMessage(error) }),
|
||||
// On ne saute une amorce neuve (id null) totalement vide que si un autre RIB
|
||||
// est soumettable. Un RIB existant vide est toujours soumis -> 422 NotBlank
|
||||
// inline (sinon la modif serait perdue en silence avec un faux toast succes).
|
||||
rib => hasSubmittableRib && rib.id === null && isRibBlank(rib),
|
||||
)
|
||||
if (ribHasError) return
|
||||
}
|
||||
|
||||
// 2) PATCH des scalaires comptables (erreurs inline sur leurs champs).
|
||||
try {
|
||||
|
||||
@@ -801,17 +801,16 @@ const visibleRibs = computed(() => isRibRequired.value ? ribs.value : [])
|
||||
function onPaymentTypeChange(value: string | number | null): void {
|
||||
accounting.paymentTypeIri = value === null ? null : String(value)
|
||||
if (!isBankRequired.value) accounting.bankIri = null
|
||||
// Les RIB n'ont de sens que pour une LCR (RG-2.08) : on amorce un bloc vide
|
||||
// quand LCR est choisi, sinon on vide la liste — les RIB deja persistes sont
|
||||
// marques pour suppression serveur au prochain enregistrement.
|
||||
// ERP-121 : un RIB est une coordonnee bancaire du fournisseur, decouplee du mode
|
||||
// de reglement. Au passage hors-LCR on ne SUPPRIME plus les RIB existants : ils
|
||||
// restent en base, simplement masques a l'ecran (visibleRibs = []), et
|
||||
// reapparaissent tels quels si l'on repasse en LCR. Seule la corbeille d'un
|
||||
// bloc (askRemoveRib) retire reellement un RIB.
|
||||
if (isRibRequired.value) {
|
||||
if (ribs.value.length === 0) ribs.value.push(emptyRib())
|
||||
}
|
||||
else {
|
||||
for (const rib of ribs.value) {
|
||||
if (rib.id != null) removedRibIds.value.push(rib.id)
|
||||
}
|
||||
ribs.value = []
|
||||
// Hors-LCR : on nettoie seulement les erreurs inline (plus affichees).
|
||||
ribErrors.value = []
|
||||
}
|
||||
}
|
||||
@@ -840,44 +839,53 @@ function askRemoveRib(index: number): void {
|
||||
/**
|
||||
* Valide l'onglet Comptabilite : POST/PATCH des RIB sur la sous-ressource PUIS
|
||||
* PATCH des scalaires (groupe supplier:write:accounting, exige accounting.manage
|
||||
* cote back) PUIS DELETE des RIB retires. Les RIB crees d'abord : le back valide
|
||||
* RG-2.08 (LCR => au moins un RIB persiste) sur le PATCH scalaires. Aucun champ
|
||||
* main/information dans le payload (mode strict RG-2.16 : sinon 403 sur tout le payload).
|
||||
* cote back) PUIS DELETE des RIB explicitement retires. Les RIB crees d'abord : le
|
||||
* back valide RG-2.08 (LCR => au moins un RIB persiste) sur le PATCH scalaires.
|
||||
*
|
||||
* ERP-121 : les RIB ne sont (re)soumis QUE sous LCR — hors-LCR ce sont des
|
||||
* coordonnees dormantes conservees telles quelles, masquees a l'ecran et jamais
|
||||
* re-ecrites. `removedRibIds` ne contient plus que les suppressions EXPLICITES
|
||||
* (corbeille d'un bloc, toujours sous LCR). Aucun champ main/information dans le
|
||||
* payload (mode strict RG-2.16 : sinon 403 sur tout le payload).
|
||||
*/
|
||||
async function submitAccounting(): Promise<void> {
|
||||
if (accountingReadonly.value || tabSubmitting.value) return
|
||||
tabSubmitting.value = true
|
||||
accountingErrors.clearErrors()
|
||||
try {
|
||||
// 1) POST/PATCH des RIB d'abord (erreurs inline par ligne, tous les blocs
|
||||
// tentes). On ne saute une amorce neuve vide QUE s'il reste un autre RIB
|
||||
// 1) POST/PATCH des RIB d'abord — UNIQUEMENT sous LCR (erreurs inline par
|
||||
// ligne, tous les blocs tentes). Hors-LCR (ERP-121), les RIB sont des
|
||||
// coordonnees dormantes : rien d'editable n'est affiche, on ne les re-soumet
|
||||
// pas. On ne saute une amorce neuve vide QUE s'il reste un autre RIB
|
||||
// soumettable : sinon (ex. l'unique RIB existant supprime, remplace par un
|
||||
// bloc vide), on la soumet pour declencher la 422 NotBlank inline plutot que
|
||||
// de laisser le DELETE echouer en « dernier RIB d'une LCR » (message plat).
|
||||
const hasSubmittableRib = ribs.value.some(r => r.id !== null || !isRibBlank(r))
|
||||
const ribHasError = await submitRows(
|
||||
ribs.value,
|
||||
ribErrors,
|
||||
async (rib) => {
|
||||
// Edition d'un RIB existant : champ requis vide envoye en `''` (NotBlank
|
||||
// 422) au lieu d'etre omis (sinon le PATCH garderait l'ancienne valeur).
|
||||
const body = buildRibPayload(rib, { forUpdate: rib.id !== null })
|
||||
if (rib.id === null) {
|
||||
const created = await api.post<{ id: number }>(
|
||||
`/suppliers/${supplierId}/ribs`,
|
||||
body,
|
||||
{ headers: { Accept: 'application/ld+json' }, toast: false },
|
||||
)
|
||||
rib.id = created.id
|
||||
}
|
||||
else {
|
||||
await api.patch(`/supplier_ribs/${rib.id}`, body, { toast: false })
|
||||
}
|
||||
},
|
||||
error => showError(error),
|
||||
rib => hasSubmittableRib && rib.id === null && isRibBlank(rib),
|
||||
)
|
||||
if (ribHasError) return
|
||||
if (isRibRequired.value) {
|
||||
const hasSubmittableRib = ribs.value.some(r => r.id !== null || !isRibBlank(r))
|
||||
const ribHasError = await submitRows(
|
||||
ribs.value,
|
||||
ribErrors,
|
||||
async (rib) => {
|
||||
// Edition d'un RIB existant : champ requis vide envoye en `''` (NotBlank
|
||||
// 422) au lieu d'etre omis (sinon le PATCH garderait l'ancienne valeur).
|
||||
const body = buildRibPayload(rib, { forUpdate: rib.id !== null })
|
||||
if (rib.id === null) {
|
||||
const created = await api.post<{ id: number }>(
|
||||
`/suppliers/${supplierId}/ribs`,
|
||||
body,
|
||||
{ headers: { Accept: 'application/ld+json' }, toast: false },
|
||||
)
|
||||
rib.id = created.id
|
||||
}
|
||||
else {
|
||||
await api.patch(`/supplier_ribs/${rib.id}`, body, { toast: false })
|
||||
}
|
||||
},
|
||||
error => showError(error),
|
||||
rib => hasSubmittableRib && rib.id === null && isRibBlank(rib),
|
||||
)
|
||||
if (ribHasError) return
|
||||
}
|
||||
|
||||
// 2) PATCH des scalaires comptables (erreurs inline sur leurs champs).
|
||||
try {
|
||||
@@ -888,8 +896,9 @@ async function submitAccounting(): Promise<void> {
|
||||
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).
|
||||
// 3) DELETE des RIB explicitement retires (corbeille d'un bloc) : APRES le
|
||||
// PATCH scalaires (le guard back refuse la suppression du dernier RIB d'une
|
||||
// LCR). ERP-121 : plus aucune suppression automatique au passage hors-LCR.
|
||||
for (const id of removedRibIds.value) {
|
||||
await api.delete(`/supplier_ribs/${id}`, {}, { toast: false })
|
||||
}
|
||||
|
||||
@@ -263,7 +263,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useSupplier } from '~/modules/commercial/composables/useSupplier'
|
||||
import { buildSupplierFormTabKeys } from '~/modules/commercial/utils/supplierFormRules'
|
||||
import { buildSupplierFormTabKeys, isRibRequiredForPaymentType } from '~/modules/commercial/utils/supplierFormRules'
|
||||
import { readHistoryTab } from '~/shared/utils/historyTab'
|
||||
import {
|
||||
canEditSupplier,
|
||||
@@ -274,6 +274,7 @@ import {
|
||||
mapAddressView,
|
||||
mapContactToDraft,
|
||||
mapRibToDraft,
|
||||
paymentTypeCodeOf,
|
||||
referentialOptionOf,
|
||||
showArchiveAction,
|
||||
showRestoreAction,
|
||||
@@ -338,8 +339,15 @@ const addressViews = computed(() => {
|
||||
return views.length ? views : [{ draft: emptyAddress(), siteOptions: [], categoryOptions: [] }]
|
||||
})
|
||||
// Exception au placeholder ci-dessus : on n'affiche AUCUN bloc RIB quand le
|
||||
// fournisseur n'en a pas (un RIB n'existe que pour un reglement LCR — RG-2.08).
|
||||
const ribs = computed(() => (supplier.value?.ribs ?? []).map(mapRibToDraft))
|
||||
// fournisseur n'en a pas. ERP-121 : un fournisseur peut desormais conserver des RIB
|
||||
// « dormants » apres etre repasse hors-LCR (on ne les supprime plus). En consultation,
|
||||
// decision metier = on les masque TOTALEMENT : on n'affiche les RIB que si le type de
|
||||
// reglement courant est LCR (le `code` est embarque sous supplier:read:accounting).
|
||||
const ribs = computed(() =>
|
||||
isRibRequiredForPaymentType(paymentTypeCodeOf(supplier.value?.paymentType))
|
||||
? (supplier.value?.ribs ?? []).map(mapRibToDraft)
|
||||
: [],
|
||||
)
|
||||
// Draft comptable (tout null si l'utilisateur n'a pas accounting.view).
|
||||
const accounting = computed(() => mapAccountingDraft(supplier.value ?? ({} as SupplierDetail)))
|
||||
|
||||
|
||||
@@ -745,13 +745,14 @@ function onPaymentTypeChange(value: string | number | null): void {
|
||||
accounting.paymentTypeIri = value === null ? null : String(value)
|
||||
// La banque n'a de sens que pour un virement : on la vide sinon (RG-2.07).
|
||||
if (!isBankRequired.value) accounting.bankIri = null
|
||||
// Les RIB n'ont de sens que pour une LCR (RG-2.08) : amorce un bloc vide quand
|
||||
// LCR est choisi, vide la liste sinon (pas de RIB fantome soumis).
|
||||
// ERP-121 : on ne jette plus la saisie RIB au passage hors-LCR. Les blocs sont
|
||||
// masques (visibleRibs = []) mais conserves, et reapparaissent si l'on repasse
|
||||
// en LCR. Ils ne sont persistes qu'a la validation SOUS LCR (cf. submitAccounting),
|
||||
// donc une saisie abandonnee hors-LCR ne cree aucun RIB orphelin.
|
||||
if (isRibRequired.value) {
|
||||
if (ribs.value.length === 0) ribs.value.push(emptyRib())
|
||||
}
|
||||
else {
|
||||
ribs.value = []
|
||||
ribErrors.value = []
|
||||
}
|
||||
}
|
||||
@@ -786,31 +787,36 @@ async function submitAccounting(): Promise<void> {
|
||||
tabSubmitting.value = true
|
||||
accountingErrors.clearErrors()
|
||||
try {
|
||||
// 1) POST/PATCH des RIB d'abord (erreurs inline par ligne). On ne saute une
|
||||
// amorce neuve vide QUE s'il reste un autre RIB soumettable : sinon (LCR sans
|
||||
// aucun RIB rempli) on la soumet pour declencher la 422 NotBlank inline.
|
||||
const hasSubmittableRib = ribs.value.some(r => r.id !== null || !isRibBlank(r))
|
||||
const ribHasError = await submitRows(
|
||||
ribs.value,
|
||||
ribErrors,
|
||||
async (rib) => {
|
||||
const body = buildRibPayload(rib)
|
||||
if (rib.id === null) {
|
||||
const created = await api.post<{ id: number }>(
|
||||
`/suppliers/${supplierId.value}/ribs`,
|
||||
body,
|
||||
{ headers: { Accept: 'application/ld+json' }, toast: false },
|
||||
)
|
||||
rib.id = created.id
|
||||
}
|
||||
else {
|
||||
await api.patch(`/supplier_ribs/${rib.id}`, body, { toast: false })
|
||||
}
|
||||
},
|
||||
error => toast.error({ title: t('commercial.suppliers.toast.error'), message: apiErrorMessage(error) }),
|
||||
rib => hasSubmittableRib && rib.id === null && isRibBlank(rib),
|
||||
)
|
||||
if (ribHasError) return
|
||||
// 1) POST/PATCH des RIB d'abord — UNIQUEMENT sous LCR (erreurs inline par
|
||||
// ligne). Hors-LCR (ERP-121), une saisie RIB eventuellement restee dans le
|
||||
// brouillon est masquee et n'est PAS persistee (pas de RIB orphelin sur un
|
||||
// fournisseur en virement). On ne saute une amorce neuve vide QUE s'il reste
|
||||
// un autre RIB soumettable : sinon (LCR sans aucun RIB rempli) on la soumet
|
||||
// pour declencher la 422 NotBlank inline.
|
||||
if (isRibRequired.value) {
|
||||
const hasSubmittableRib = ribs.value.some(r => r.id !== null || !isRibBlank(r))
|
||||
const ribHasError = await submitRows(
|
||||
ribs.value,
|
||||
ribErrors,
|
||||
async (rib) => {
|
||||
const body = buildRibPayload(rib)
|
||||
if (rib.id === null) {
|
||||
const created = await api.post<{ id: number }>(
|
||||
`/suppliers/${supplierId.value}/ribs`,
|
||||
body,
|
||||
{ headers: { Accept: 'application/ld+json' }, toast: false },
|
||||
)
|
||||
rib.id = created.id
|
||||
}
|
||||
else {
|
||||
await api.patch(`/supplier_ribs/${rib.id}`, body, { toast: false })
|
||||
}
|
||||
},
|
||||
error => toast.error({ title: t('commercial.suppliers.toast.error'), message: apiErrorMessage(error) }),
|
||||
rib => hasSubmittableRib && rib.id === null && isRibBlank(rib),
|
||||
)
|
||||
if (ribHasError) return
|
||||
}
|
||||
|
||||
// 2) PATCH des scalaires comptables (erreurs inline sur leurs champs).
|
||||
try {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
mapAddressView,
|
||||
mapContactToDraft,
|
||||
mapRibToDraft,
|
||||
paymentTypeCodeOf,
|
||||
referentialOptionOf,
|
||||
relationOf,
|
||||
showArchiveAction,
|
||||
@@ -233,3 +234,17 @@ describe('showArchiveAction / showRestoreAction', () => {
|
||||
expect(showRestoreAction(can([]), true)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('paymentTypeCodeOf (ERP-121 : RIB masques hors-LCR en consultation)', () => {
|
||||
it('retourne le code metier quand le type de reglement est embarque', () => {
|
||||
expect(paymentTypeCodeOf({ '@id': '/api/payment_types/1', code: 'LCR' })).toBe('LCR')
|
||||
expect(paymentTypeCodeOf({ '@id': '/api/payment_types/2', code: 'VIREMENT' })).toBe('VIREMENT')
|
||||
})
|
||||
|
||||
it('retourne null pour un IRI nu, un objet sans code, ou une relation absente', () => {
|
||||
expect(paymentTypeCodeOf('/api/payment_types/1')).toBeNull()
|
||||
expect(paymentTypeCodeOf({ '@id': '/api/payment_types/1' })).toBeNull()
|
||||
expect(paymentTypeCodeOf(null)).toBeNull()
|
||||
expect(paymentTypeCodeOf(undefined)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
mapAddressView,
|
||||
mapContactToDraft,
|
||||
mapRibToDraft,
|
||||
paymentTypeCodeOf,
|
||||
referentialOptionOf,
|
||||
showArchiveAction,
|
||||
showRestoreAction,
|
||||
@@ -222,3 +223,17 @@ describe('showArchiveAction / showRestoreAction', () => {
|
||||
expect(showRestoreAction(can([]), true)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('paymentTypeCodeOf (ERP-121 : RIB masques hors-LCR en consultation)', () => {
|
||||
it('retourne le code metier quand le type de reglement est embarque', () => {
|
||||
expect(paymentTypeCodeOf({ '@id': '/api/payment_types/1', code: 'LCR' })).toBe('LCR')
|
||||
expect(paymentTypeCodeOf({ '@id': '/api/payment_types/2', code: 'VIREMENT' })).toBe('VIREMENT')
|
||||
})
|
||||
|
||||
it('retourne null pour un IRI nu, un objet sans code, ou une relation absente', () => {
|
||||
expect(paymentTypeCodeOf('/api/payment_types/1')).toBeNull()
|
||||
expect(paymentTypeCodeOf({ '@id': '/api/payment_types/1' })).toBeNull()
|
||||
expect(paymentTypeCodeOf(null)).toBeNull()
|
||||
expect(paymentTypeCodeOf(undefined)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -300,6 +300,21 @@ export function referentialOptionOf(relation: Relation): SelectOption[] {
|
||||
return [{ value: relation['@id'], label }]
|
||||
}
|
||||
|
||||
/**
|
||||
* Code metier d'un referentiel embarque (ex: PaymentType.code = 'LCR' / 'VIREMENT'),
|
||||
* ou null si la relation est absente / serialisee en IRI nu. Type-safe : la branche
|
||||
* chaine (IRI nu) et l'absence sont court-circuitees avant l'acces au code. Sert a
|
||||
* conditionner l'affichage selon le type de reglement courant (ERP-121 : RIB masques
|
||||
* hors-LCR en consultation).
|
||||
*/
|
||||
export function paymentTypeCodeOf(relation: Relation): string | null {
|
||||
if (!relation || typeof relation === 'string') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (relation.code as string | undefined) ?? null
|
||||
}
|
||||
|
||||
/** Vue d'une adresse (brouillon + options de select propres a l'adresse). */
|
||||
export function mapAddressView(address: AddressRead): AddressView {
|
||||
return {
|
||||
|
||||
@@ -275,6 +275,21 @@ export function referentialOptionOf(relation: Relation): SelectOption[] {
|
||||
return [{ value: relation['@id'], label }]
|
||||
}
|
||||
|
||||
/**
|
||||
* Code metier d'un referentiel embarque (ex: PaymentType.code = 'LCR' / 'VIREMENT'),
|
||||
* ou null si la relation est absente / serialisee en IRI nu. Type-safe : la branche
|
||||
* chaine (IRI nu) et l'absence sont court-circuitees avant l'acces au code. Sert a
|
||||
* conditionner l'affichage selon le type de reglement courant (ERP-121 : RIB masques
|
||||
* hors-LCR en consultation).
|
||||
*/
|
||||
export function paymentTypeCodeOf(relation: Relation): string | null {
|
||||
if (!relation || typeof relation === 'string') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (relation.code as string | undefined) ?? null
|
||||
}
|
||||
|
||||
/** Vue d'une adresse (brouillon + options de select propres a l'adresse). */
|
||||
export function mapAddressView(address: AddressRead): AddressView {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user