Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 19ac8833eb | |||
| c25c33116d |
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
parameters:
|
parameters:
|
||||||
app.version: '0.1.118'
|
app.version: '0.1.119'
|
||||||
|
|||||||
@@ -432,12 +432,27 @@
|
|||||||
"add": "Nouvelle adresse",
|
"add": "Nouvelle adresse",
|
||||||
"degraded": "Service d'adresse indisponible : saisie de la ville et de l'adresse en mode libre."
|
"degraded": "Service d'adresse indisponible : saisie de la ville et de l'adresse en mode libre."
|
||||||
},
|
},
|
||||||
|
"accounting": {
|
||||||
|
"siren": "SIREN",
|
||||||
|
"accountNumber": "Numéro de compte",
|
||||||
|
"tvaMode": "Mode de TVA",
|
||||||
|
"nTva": "N° de TVA",
|
||||||
|
"paymentDelay": "Délai de règlement",
|
||||||
|
"paymentType": "Type de règlement",
|
||||||
|
"bank": "Banque",
|
||||||
|
"ribLabel": "Libellé",
|
||||||
|
"ribBic": "BIC",
|
||||||
|
"ribIban": "IBAN",
|
||||||
|
"addRib": "Ajouter un RIB",
|
||||||
|
"removeRib": "Supprimer le RIB"
|
||||||
|
},
|
||||||
"confirmDelete": {
|
"confirmDelete": {
|
||||||
"title": "Confirmer la suppression",
|
"title": "Confirmer la suppression",
|
||||||
"cancel": "Annuler",
|
"cancel": "Annuler",
|
||||||
"confirm": "Supprimer",
|
"confirm": "Supprimer",
|
||||||
"contact": "Supprimer ce contact ?",
|
"contact": "Supprimer ce contact ?",
|
||||||
"address": "Supprimer cette adresse ?"
|
"address": "Supprimer cette adresse ?",
|
||||||
|
"rib": "Supprimer ce RIB ?"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"toast": {
|
"toast": {
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|||||||
|
|
||||||
const mockPost = vi.hoisted(() => vi.fn())
|
const mockPost = vi.hoisted(() => vi.fn())
|
||||||
const mockPatch = vi.hoisted(() => vi.fn())
|
const mockPatch = vi.hoisted(() => vi.fn())
|
||||||
// Permission accounting.view pilotable par test (presence de l'onglet Comptabilite).
|
// Permissions comptables pilotables par test (presence/edition de l'onglet Comptabilite).
|
||||||
const permState = vi.hoisted(() => ({ accountingView: false }))
|
const permState = vi.hoisted(() => ({ accountingView: false, accountingManage: false }))
|
||||||
|
|
||||||
vi.stubGlobal('useApi', () => ({
|
vi.stubGlobal('useApi', () => ({
|
||||||
get: vi.fn(),
|
get: vi.fn(),
|
||||||
@@ -37,7 +37,11 @@ vi.stubGlobal('useToast', () => ({
|
|||||||
info: vi.fn(),
|
info: vi.fn(),
|
||||||
}))
|
}))
|
||||||
vi.stubGlobal('usePermissions', () => ({
|
vi.stubGlobal('usePermissions', () => ({
|
||||||
can: (perm: string) => perm === 'technique.providers.accounting.view' ? permState.accountingView : true,
|
can: (perm: string) => {
|
||||||
|
if (perm === 'technique.providers.accounting.view') return permState.accountingView
|
||||||
|
if (perm === 'technique.providers.accounting.manage') return permState.accountingManage
|
||||||
|
return true
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const { useProviderForm, buildProviderCreateTabKeys } = await import('../useProviderForm')
|
const { useProviderForm, buildProviderCreateTabKeys } = await import('../useProviderForm')
|
||||||
@@ -62,6 +66,7 @@ describe('useProviderForm', () => {
|
|||||||
mockPost.mockReset()
|
mockPost.mockReset()
|
||||||
mockPatch.mockReset()
|
mockPatch.mockReset()
|
||||||
permState.accountingView = false
|
permState.accountingView = false
|
||||||
|
permState.accountingManage = false
|
||||||
})
|
})
|
||||||
|
|
||||||
it('RG-3.03/RG-3.09 (front) : bloque le POST si aucun site / aucune categorie', async () => {
|
it('RG-3.03/RG-3.09 (front) : bloque le POST si aucun site / aucune categorie', async () => {
|
||||||
@@ -208,6 +213,7 @@ describe('useProviderForm — onglet Contact (ERP-142)', () => {
|
|||||||
mockPost.mockReset()
|
mockPost.mockReset()
|
||||||
mockPatch.mockReset()
|
mockPatch.mockReset()
|
||||||
permState.accountingView = false
|
permState.accountingView = false
|
||||||
|
permState.accountingManage = false
|
||||||
})
|
})
|
||||||
|
|
||||||
/** Place le formulaire en etat « prestataire cree » (onglet Contact accessible). */
|
/** Place le formulaire en etat « prestataire cree » (onglet Contact accessible). */
|
||||||
@@ -315,6 +321,7 @@ describe('useProviderForm — onglet Adresse (ERP-143)', () => {
|
|||||||
mockPost.mockReset()
|
mockPost.mockReset()
|
||||||
mockPatch.mockReset()
|
mockPatch.mockReset()
|
||||||
permState.accountingView = false
|
permState.accountingView = false
|
||||||
|
permState.accountingManage = false
|
||||||
})
|
})
|
||||||
|
|
||||||
/** Place le formulaire en etat « prestataire cree » (onglet Adresse accessible). */
|
/** Place le formulaire en etat « prestataire cree » (onglet Adresse accessible). */
|
||||||
@@ -406,3 +413,175 @@ describe('useProviderForm — onglet Adresse (ERP-143)', () => {
|
|||||||
expect(form.isValidated('address')).toBe(false)
|
expect(form.isValidated('address')).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('useProviderForm — onglet Comptabilite (ERP-144)', () => {
|
||||||
|
const TVA = '/api/tva_modes/1'
|
||||||
|
const DELAY = '/api/payment_delays/1'
|
||||||
|
const TYPE = '/api/payment_types/3'
|
||||||
|
const BANK = '/api/banks/2'
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockPost.mockReset()
|
||||||
|
mockPatch.mockReset()
|
||||||
|
permState.accountingView = true
|
||||||
|
permState.accountingManage = true
|
||||||
|
})
|
||||||
|
|
||||||
|
/** Prestataire cree, onglet Comptabilite editable (view + manage). */
|
||||||
|
function createdForm() {
|
||||||
|
const form = useProviderForm()
|
||||||
|
form.providerId.value = 7
|
||||||
|
return form
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remplit les scalaires comptables communs. */
|
||||||
|
function fillScalars(form: ProviderForm): void {
|
||||||
|
form.accounting.siren = '123456789'
|
||||||
|
form.accounting.accountNumber = '4010'
|
||||||
|
form.accounting.tvaModeIri = TVA
|
||||||
|
form.accounting.nTva = 'FR123'
|
||||||
|
form.accounting.paymentDelayIri = DELAY
|
||||||
|
form.accounting.paymentTypeIri = TYPE
|
||||||
|
}
|
||||||
|
|
||||||
|
it('lecture seule sans accounting.manage (Compta consultation / autres roles)', () => {
|
||||||
|
permState.accountingManage = false
|
||||||
|
const form = createdForm()
|
||||||
|
expect(form.accountingReadonly.value).toBe(true)
|
||||||
|
|
||||||
|
permState.accountingManage = true
|
||||||
|
const form2 = createdForm()
|
||||||
|
expect(form2.accountingReadonly.value).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('RG-3.07 : setPaymentType(VIREMENT) garde la banque ; un autre type la vide', () => {
|
||||||
|
const form = createdForm()
|
||||||
|
form.accounting.bankIri = BANK
|
||||||
|
|
||||||
|
// Type VIREMENT -> banque requise, conservee.
|
||||||
|
form.setPaymentType(TYPE, true, false)
|
||||||
|
expect(form.accounting.bankIri).toBe(BANK)
|
||||||
|
|
||||||
|
// Type non-VIREMENT -> banque videe (sans objet).
|
||||||
|
form.setPaymentType(TYPE, false, false)
|
||||||
|
expect(form.accounting.bankIri).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('RG-3.08 : setPaymentType(LCR) garantit au moins un bloc RIB', () => {
|
||||||
|
const form = createdForm()
|
||||||
|
expect(form.ribs.value).toHaveLength(0)
|
||||||
|
|
||||||
|
form.setPaymentType(TYPE, false, true)
|
||||||
|
expect(form.ribs.value).toHaveLength(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('« + RIB » desactive tant que le dernier RIB est incomplet (RG-3.08)', () => {
|
||||||
|
const form = createdForm()
|
||||||
|
form.setPaymentType(TYPE, false, true)
|
||||||
|
expect(form.canAddRib.value).toBe(false)
|
||||||
|
|
||||||
|
const rib = form.ribs.value[0]
|
||||||
|
if (rib) {
|
||||||
|
rib.label = 'Compte'
|
||||||
|
rib.bic = 'BNPAFRPP'
|
||||||
|
rib.iban = 'FR76...'
|
||||||
|
}
|
||||||
|
expect(form.canAddRib.value).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('VIREMENT : PATCH des scalaires avec banque, aucun appel RIB', async () => {
|
||||||
|
mockPatch.mockResolvedValueOnce({})
|
||||||
|
const form = createdForm()
|
||||||
|
fillScalars(form)
|
||||||
|
form.accounting.bankIri = BANK
|
||||||
|
|
||||||
|
const ok = await form.submitAccounting(true, false, vi.fn())
|
||||||
|
|
||||||
|
expect(ok).toBe(true)
|
||||||
|
expect(mockPost).not.toHaveBeenCalled()
|
||||||
|
expect(mockPatch).toHaveBeenCalledWith(
|
||||||
|
'/providers/7',
|
||||||
|
expect.objectContaining({ paymentType: TYPE, bank: BANK, siren: '123456789' }),
|
||||||
|
{ toast: false },
|
||||||
|
)
|
||||||
|
expect(form.isValidated('accounting')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('hors VIREMENT : la banque part a null dans le payload (RG-3.07)', async () => {
|
||||||
|
mockPatch.mockResolvedValueOnce({})
|
||||||
|
const form = createdForm()
|
||||||
|
fillScalars(form)
|
||||||
|
form.accounting.bankIri = BANK // residu : doit etre ignore (isBankRequired=false)
|
||||||
|
|
||||||
|
await form.submitAccounting(false, false, vi.fn())
|
||||||
|
|
||||||
|
const body = mockPatch.mock.calls[0]?.[1] as Record<string, unknown>
|
||||||
|
expect(body.bank).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('LCR : POST des RIB AVANT le PATCH des scalaires (ordre RG-3.08)', async () => {
|
||||||
|
mockPost.mockResolvedValueOnce({ id: 50 })
|
||||||
|
mockPatch.mockResolvedValueOnce({})
|
||||||
|
const form = createdForm()
|
||||||
|
fillScalars(form)
|
||||||
|
form.setPaymentType(TYPE, false, true)
|
||||||
|
const rib = form.ribs.value[0]
|
||||||
|
if (rib) {
|
||||||
|
rib.label = 'Compte'
|
||||||
|
rib.bic = 'BNPAFRPP'
|
||||||
|
rib.iban = 'FR76...'
|
||||||
|
}
|
||||||
|
|
||||||
|
const ok = await form.submitAccounting(false, true, vi.fn())
|
||||||
|
|
||||||
|
expect(ok).toBe(true)
|
||||||
|
expect(mockPost).toHaveBeenCalledWith(
|
||||||
|
'/providers/7/ribs',
|
||||||
|
expect.objectContaining({ label: 'Compte', bic: 'BNPAFRPP', iban: 'FR76...' }),
|
||||||
|
{ headers: { Accept: 'application/ld+json' }, toast: false },
|
||||||
|
)
|
||||||
|
expect(form.ribs.value[0]?.id).toBe(50)
|
||||||
|
// Le PATCH des scalaires intervient APRES la creation du RIB.
|
||||||
|
expect(mockPatch).toHaveBeenCalledWith('/providers/7', expect.any(Object), { toast: false })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('422 sur les scalaires (bank) : mapping inline, onglet non finalise', async () => {
|
||||||
|
mockPatch.mockRejectedValueOnce({
|
||||||
|
response: {
|
||||||
|
status: 422,
|
||||||
|
_data: { violations: [{ propertyPath: 'bank', message: 'La banque est obligatoire pour un virement.' }] },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const form = createdForm()
|
||||||
|
fillScalars(form)
|
||||||
|
|
||||||
|
const ok = await form.submitAccounting(true, false, vi.fn())
|
||||||
|
|
||||||
|
expect(ok).toBe(false)
|
||||||
|
expect(form.accountingErrors.errors.bank).toBe('La banque est obligatoire pour un virement.')
|
||||||
|
expect(form.isValidated('accounting')).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('LCR : 422 RIB par ligne -> pas de PATCH des scalaires', async () => {
|
||||||
|
mockPost.mockRejectedValueOnce({
|
||||||
|
response: {
|
||||||
|
status: 422,
|
||||||
|
_data: { violations: [{ propertyPath: 'iban', message: 'L\'IBAN est obligatoire.' }] },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const form = createdForm()
|
||||||
|
fillScalars(form)
|
||||||
|
form.setPaymentType(TYPE, false, true)
|
||||||
|
const rib = form.ribs.value[0]
|
||||||
|
if (rib) {
|
||||||
|
rib.label = 'Compte'
|
||||||
|
rib.bic = 'BNPAFRPP'
|
||||||
|
}
|
||||||
|
|
||||||
|
const ok = await form.submitAccounting(false, true, vi.fn())
|
||||||
|
|
||||||
|
expect(ok).toBe(false)
|
||||||
|
expect(form.ribErrors.value[0]?.iban).toBe('L\'IBAN est obligatoire.')
|
||||||
|
expect(mockPatch).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@@ -2,15 +2,20 @@ import { computed, reactive, ref, type Ref } from 'vue'
|
|||||||
import { useFormErrors } from '~/shared/composables/useFormErrors'
|
import { useFormErrors } from '~/shared/composables/useFormErrors'
|
||||||
import { mapViolationsToRecord } from '~/shared/utils/api'
|
import { mapViolationsToRecord } from '~/shared/utils/api'
|
||||||
import {
|
import {
|
||||||
|
emptyProviderAccounting,
|
||||||
emptyProviderAddress,
|
emptyProviderAddress,
|
||||||
emptyProviderContact,
|
emptyProviderContact,
|
||||||
emptyProviderMain,
|
emptyProviderMain,
|
||||||
|
emptyProviderRib,
|
||||||
|
type ProviderAccountingDraft,
|
||||||
type ProviderAddressFormDraft,
|
type ProviderAddressFormDraft,
|
||||||
type ProviderAddressResponse,
|
type ProviderAddressResponse,
|
||||||
type ProviderContactFormDraft,
|
type ProviderContactFormDraft,
|
||||||
type ProviderContactResponse,
|
type ProviderContactResponse,
|
||||||
type ProviderMainDraft,
|
type ProviderMainDraft,
|
||||||
type ProviderMainResponse,
|
type ProviderMainResponse,
|
||||||
|
type ProviderRibFormDraft,
|
||||||
|
type ProviderRibResponse,
|
||||||
} from '~/modules/technique/types/providerForm'
|
} from '~/modules/technique/types/providerForm'
|
||||||
import {
|
import {
|
||||||
buildProviderContactPayload,
|
buildProviderContactPayload,
|
||||||
@@ -20,6 +25,12 @@ import {
|
|||||||
buildProviderAddressPayload,
|
buildProviderAddressPayload,
|
||||||
isProviderAddressValid,
|
isProviderAddressValid,
|
||||||
} from '~/modules/technique/utils/forms/providerAddress'
|
} from '~/modules/technique/utils/forms/providerAddress'
|
||||||
|
import {
|
||||||
|
buildProviderAccountingPayload,
|
||||||
|
buildProviderRibPayload,
|
||||||
|
isRibBlank,
|
||||||
|
isRibComplete,
|
||||||
|
} from '~/modules/technique/utils/forms/providerAccounting'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Workflow de l'ecran « Ajouter un prestataire » (M3 Technique, ERP-141) —
|
* Workflow de l'ecran « Ajouter un prestataire » (M3 Technique, ERP-141) —
|
||||||
@@ -72,6 +83,7 @@ export function useProviderForm() {
|
|||||||
|
|
||||||
// ── Onglets : ordre + gating progressif ───────────────────────────────────
|
// ── Onglets : ordre + gating progressif ───────────────────────────────────
|
||||||
const canAccountingView = computed(() => can('technique.providers.accounting.view'))
|
const canAccountingView = computed(() => can('technique.providers.accounting.view'))
|
||||||
|
const canAccountingManage = computed(() => can('technique.providers.accounting.manage'))
|
||||||
const tabKeys = computed(() => buildProviderCreateTabKeys(canAccountingView.value))
|
const tabKeys = computed(() => buildProviderCreateTabKeys(canAccountingView.value))
|
||||||
|
|
||||||
// Index du dernier onglet deverrouille (-1 tant que le prestataire n'est pas cree).
|
// Index du dernier onglet deverrouille (-1 tant que le prestataire n'est pas cree).
|
||||||
@@ -370,6 +382,130 @@ export function useProviderForm() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Onglet Comptabilite (ERP-144) ─────────────────────────────────────────
|
||||||
|
const accounting = reactive<ProviderAccountingDraft>(emptyProviderAccounting())
|
||||||
|
const ribs = ref<ProviderRibFormDraft[]>([])
|
||||||
|
const accountingErrors = useFormErrors()
|
||||||
|
// Erreurs 422 par ligne de RIB (alignees sur l'index du v-for).
|
||||||
|
const ribErrors = ref<Record<string, string>[]>([])
|
||||||
|
|
||||||
|
// L'onglet est editable seulement avec accounting.manage (sinon lecture seule).
|
||||||
|
const accountingReadonly = computed(() => isValidated('accounting') || !canAccountingManage.value)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met a jour le type de reglement (IRI) en propageant ses RG inter-champs :
|
||||||
|
* - hors VIREMENT (RG-3.07) : on vide la banque (sans objet) ;
|
||||||
|
* - LCR (RG-3.08) : on garantit au moins un bloc RIB visible ; hors LCR, on
|
||||||
|
* purge les erreurs de RIB (les blocs sont conserves mais non persistes).
|
||||||
|
* `isBankRequired` / `isRibRequired` sont calcules par l'appelant (page) a
|
||||||
|
* partir du code resolu via les referentiels.
|
||||||
|
*/
|
||||||
|
function setPaymentType(iri: string | null, isBankRequired: boolean, isRibRequired: boolean): void {
|
||||||
|
accounting.paymentTypeIri = iri
|
||||||
|
if (!isBankRequired) {
|
||||||
|
accounting.bankIri = null
|
||||||
|
}
|
||||||
|
if (isRibRequired) {
|
||||||
|
if (ribs.value.length === 0) {
|
||||||
|
ribs.value.push(emptyProviderRib())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ribErrors.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// « + RIB » desactive tant que le dernier bloc RIB n'est pas complet (RG-3.08).
|
||||||
|
const canAddRib = computed(() => {
|
||||||
|
const last = ribs.value[ribs.value.length - 1]
|
||||||
|
return last !== undefined && isRibComplete(last)
|
||||||
|
})
|
||||||
|
|
||||||
|
function addRib(): void {
|
||||||
|
if (canAddRib.value) {
|
||||||
|
ribs.value.push(emptyProviderRib())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeRib(index: number): void {
|
||||||
|
ribs.value.splice(index, 1)
|
||||||
|
ribErrors.value.splice(index, 1)
|
||||||
|
// Garde au moins un bloc RIB visible (sous LCR).
|
||||||
|
if (ribs.value.length === 0) {
|
||||||
|
ribs.value.push(emptyProviderRib())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valide l'onglet Comptabilite : (1) sous LCR, POST/PATCH des RIB d'abord
|
||||||
|
* (le back valide RG-3.08 sur le PATCH scalaires, les RIB doivent donc exister
|
||||||
|
* AVANT) ; (2) PATCH des scalaires comptables (groupe provider:write:accounting,
|
||||||
|
* banque envoyee seulement si VIREMENT — RG-3.07). Erreurs RIB par ligne ;
|
||||||
|
* erreurs scalaires inline (bank/paymentType). Retourne true si l'onglet a ete
|
||||||
|
* valide.
|
||||||
|
*/
|
||||||
|
async function submitAccounting(
|
||||||
|
isBankRequired: boolean,
|
||||||
|
isRibRequired: boolean,
|
||||||
|
onRibError: (error: unknown) => void,
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (providerId.value === null || tabSubmitting.value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
tabSubmitting.value = true
|
||||||
|
accountingErrors.clearErrors()
|
||||||
|
try {
|
||||||
|
// 1) RIB d'abord, uniquement sous LCR. Une amorce vide neuve est sautee
|
||||||
|
// s'il reste un autre RIB soumettable ; sinon (LCR sans aucun RIB rempli)
|
||||||
|
// on la soumet pour declencher la 422 NotBlank inline.
|
||||||
|
if (isRibRequired) {
|
||||||
|
const hasSubmittableRib = ribs.value.some(r => r.id !== null || !isRibBlank(r))
|
||||||
|
const ribHasError = await submitRows(
|
||||||
|
ribs.value,
|
||||||
|
ribErrors,
|
||||||
|
async (rib) => {
|
||||||
|
const body = buildProviderRibPayload(rib)
|
||||||
|
if (rib.id === null) {
|
||||||
|
const created = await api.post<ProviderRibResponse>(
|
||||||
|
`/providers/${providerId.value}/ribs`,
|
||||||
|
body,
|
||||||
|
{ headers: { Accept: 'application/ld+json' }, toast: false },
|
||||||
|
)
|
||||||
|
rib.id = created.id
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
await api.patch(`/provider_ribs/${rib.id}`, body, { toast: false })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRibError,
|
||||||
|
rib => hasSubmittableRib && rib.id === null && isRibBlank(rib),
|
||||||
|
)
|
||||||
|
if (ribHasError) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) PATCH des scalaires comptables (erreurs inline sur leurs champs).
|
||||||
|
try {
|
||||||
|
await api.patch(
|
||||||
|
`/providers/${providerId.value}`,
|
||||||
|
buildProviderAccountingPayload(accounting, isBankRequired),
|
||||||
|
{ toast: false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
accountingErrors.handleApiError(error, { fallbackMessage: t('technique.providers.toast.error') })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
completeTab('accounting')
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
tabSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// etat
|
// etat
|
||||||
main,
|
main,
|
||||||
@@ -380,6 +516,7 @@ export function useProviderForm() {
|
|||||||
mainErrors,
|
mainErrors,
|
||||||
// onglets
|
// onglets
|
||||||
canAccountingView,
|
canAccountingView,
|
||||||
|
canAccountingManage,
|
||||||
tabKeys,
|
tabKeys,
|
||||||
activeTab,
|
activeTab,
|
||||||
unlockedIndex,
|
unlockedIndex,
|
||||||
@@ -399,6 +536,17 @@ export function useProviderForm() {
|
|||||||
addAddress,
|
addAddress,
|
||||||
removeAddress,
|
removeAddress,
|
||||||
submitAddresses,
|
submitAddresses,
|
||||||
|
// comptabilite
|
||||||
|
accounting,
|
||||||
|
ribs,
|
||||||
|
accountingErrors,
|
||||||
|
ribErrors,
|
||||||
|
accountingReadonly,
|
||||||
|
setPaymentType,
|
||||||
|
canAddRib,
|
||||||
|
addRib,
|
||||||
|
removeRib,
|
||||||
|
submitAccounting,
|
||||||
// actions
|
// actions
|
||||||
validateMainFront,
|
validateMainFront,
|
||||||
buildMainPayload,
|
buildMainPayload,
|
||||||
|
|||||||
@@ -28,10 +28,20 @@ export interface RefOption {
|
|||||||
label: string
|
label: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Option de type de reglement enrichie de son code stable (RG-3.07 / RG-3.08). */
|
||||||
|
export interface PaymentTypeOption extends RefOption {
|
||||||
|
code: string
|
||||||
|
}
|
||||||
|
|
||||||
interface HydraMember {
|
interface HydraMember {
|
||||||
'@id': string
|
'@id': string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ReferentialMember extends HydraMember {
|
||||||
|
code: string
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
interface CategoryMember extends HydraMember {
|
interface CategoryMember extends HydraMember {
|
||||||
code: string
|
code: string
|
||||||
name: string
|
name: string
|
||||||
@@ -55,6 +65,11 @@ export function useProviderReferentials() {
|
|||||||
const categories = ref<RefOption[]>([])
|
const categories = ref<RefOption[]>([])
|
||||||
const sites = ref<RefOption[]>([])
|
const sites = ref<RefOption[]>([])
|
||||||
const countries = ref<RefOption[]>([])
|
const countries = ref<RefOption[]>([])
|
||||||
|
// Referentiels comptables (charges a la demande via loadAccounting).
|
||||||
|
const tvaModes = ref<RefOption[]>([])
|
||||||
|
const paymentDelays = ref<RefOption[]>([])
|
||||||
|
const paymentTypes = ref<PaymentTypeOption[]>([])
|
||||||
|
const banks = ref<RefOption[]>([])
|
||||||
|
|
||||||
/** Recupere une collection complete (pagination desactivee) en Hydra. */
|
/** Recupere une collection complete (pagination desactivee) en Hydra. */
|
||||||
async function fetchAll<T extends HydraMember>(
|
async function fetchAll<T extends HydraMember>(
|
||||||
@@ -88,10 +103,34 @@ export function useProviderReferentials() {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Charge les referentiels comptables (onglet Comptabilite, ERP-144). Appele
|
||||||
|
* uniquement quand l'utilisateur peut voir l'onglet (accounting.view). Resilient
|
||||||
|
* (allSettled) : un referentiel en echec reste vide.
|
||||||
|
*/
|
||||||
|
async function loadAccounting(): Promise<void> {
|
||||||
|
await Promise.allSettled([
|
||||||
|
fetchAll<ReferentialMember>('/tva_modes')
|
||||||
|
.then((list) => { tvaModes.value = list.map(t => ({ value: t['@id'], label: t.label })) }),
|
||||||
|
fetchAll<ReferentialMember>('/payment_delays')
|
||||||
|
.then((list) => { paymentDelays.value = list.map(d => ({ value: d['@id'], label: d.label })) }),
|
||||||
|
// Le code stable du type sert les RG-3.07 (VIREMENT) / RG-3.08 (LCR).
|
||||||
|
fetchAll<ReferentialMember>('/payment_types')
|
||||||
|
.then((list) => { paymentTypes.value = list.map(p => ({ value: p['@id'], label: p.label, code: p.code })) }),
|
||||||
|
fetchAll<ReferentialMember>('/banks')
|
||||||
|
.then((list) => { banks.value = list.map(b => ({ value: b['@id'], label: b.label })) }),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
categories,
|
categories,
|
||||||
sites,
|
sites,
|
||||||
countries,
|
countries,
|
||||||
|
tvaModes,
|
||||||
|
paymentDelays,
|
||||||
|
paymentTypes,
|
||||||
|
banks,
|
||||||
loadMain,
|
loadMain,
|
||||||
|
loadAccounting,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,7 +127,136 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="canAccountingView" #accounting><ComingSoonPlaceholder /></template>
|
<!-- Onglet Comptabilite (present uniquement si accounting.view ; editable si manage). -->
|
||||||
|
<template v-if="canAccountingView" #accounting>
|
||||||
|
<div class="mt-12 flex flex-col gap-6">
|
||||||
|
<div class="bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]">
|
||||||
|
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
|
<MalioInputText
|
||||||
|
v-model="accounting.siren"
|
||||||
|
:label="t('technique.providers.form.accounting.siren')"
|
||||||
|
:mask="SIREN_MASK"
|
||||||
|
:readonly="accountingReadonly"
|
||||||
|
:required="true"
|
||||||
|
:error="accountingErrors.errors.siren"
|
||||||
|
/>
|
||||||
|
<MalioInputText
|
||||||
|
v-model="accounting.accountNumber"
|
||||||
|
:label="t('technique.providers.form.accounting.accountNumber')"
|
||||||
|
:readonly="accountingReadonly"
|
||||||
|
:required="true"
|
||||||
|
:error="accountingErrors.errors.accountNumber"
|
||||||
|
/>
|
||||||
|
<MalioSelect
|
||||||
|
:model-value="accounting.tvaModeIri"
|
||||||
|
:options="referentials.tvaModes.value"
|
||||||
|
:label="t('technique.providers.form.accounting.tvaMode')"
|
||||||
|
:readonly="accountingReadonly"
|
||||||
|
empty-option-label=""
|
||||||
|
:required="true"
|
||||||
|
:error="accountingErrors.errors.tvaMode"
|
||||||
|
@update:model-value="(v: string | number | null) => accounting.tvaModeIri = v === null ? null : String(v)"
|
||||||
|
/>
|
||||||
|
<MalioInputText
|
||||||
|
v-model="accounting.nTva"
|
||||||
|
:label="t('technique.providers.form.accounting.nTva')"
|
||||||
|
:readonly="accountingReadonly"
|
||||||
|
:required="true"
|
||||||
|
:error="accountingErrors.errors.nTva"
|
||||||
|
/>
|
||||||
|
<MalioSelect
|
||||||
|
:model-value="accounting.paymentDelayIri"
|
||||||
|
:options="referentials.paymentDelays.value"
|
||||||
|
:label="t('technique.providers.form.accounting.paymentDelay')"
|
||||||
|
:readonly="accountingReadonly"
|
||||||
|
empty-option-label=""
|
||||||
|
:required="true"
|
||||||
|
:error="accountingErrors.errors.paymentDelay"
|
||||||
|
@update:model-value="(v: string | number | null) => accounting.paymentDelayIri = v === null ? null : String(v)"
|
||||||
|
/>
|
||||||
|
<MalioSelect
|
||||||
|
:model-value="accounting.paymentTypeIri"
|
||||||
|
:options="referentials.paymentTypes.value"
|
||||||
|
:label="t('technique.providers.form.accounting.paymentType')"
|
||||||
|
:readonly="accountingReadonly"
|
||||||
|
empty-option-label=""
|
||||||
|
:required="true"
|
||||||
|
:error="accountingErrors.errors.paymentType"
|
||||||
|
@update:model-value="onPaymentTypeChange"
|
||||||
|
/>
|
||||||
|
<!-- Banque : visible et obligatoire seulement si VIREMENT (RG-3.07). -->
|
||||||
|
<MalioSelect
|
||||||
|
v-if="isBankRequired"
|
||||||
|
:model-value="accounting.bankIri"
|
||||||
|
:options="referentials.banks.value"
|
||||||
|
:label="t('technique.providers.form.accounting.bank')"
|
||||||
|
:readonly="accountingReadonly"
|
||||||
|
empty-option-label=""
|
||||||
|
:required="true"
|
||||||
|
:error="accountingErrors.errors.bank"
|
||||||
|
@update:model-value="(v: string | number | null) => accounting.bankIri = v === null ? null : String(v)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Blocs RIB — affiches uniquement si type de reglement = LCR (RG-3.08). -->
|
||||||
|
<div
|
||||||
|
v-for="(rib, index) in visibleRibs"
|
||||||
|
:key="index"
|
||||||
|
class="relative bg-white py-4 pl-[28px] pr-[60px] shadow-[0_4px_4px_0_rgba(0,0,0,0.25)]"
|
||||||
|
>
|
||||||
|
<MalioButtonIcon
|
||||||
|
v-if="!accountingReadonly && visibleRibs.length > 1"
|
||||||
|
icon="mdi:delete-outline"
|
||||||
|
variant="ghost"
|
||||||
|
button-class="absolute top-3 right-3"
|
||||||
|
v-bind="{ ariaLabel: t('technique.providers.form.accounting.removeRib') }"
|
||||||
|
@click="askRemoveRib(index)"
|
||||||
|
/>
|
||||||
|
<div class="grid grid-cols-4 gap-x-[44px] gap-y-4">
|
||||||
|
<MalioInputText
|
||||||
|
v-model="rib.label"
|
||||||
|
:label="t('technique.providers.form.accounting.ribLabel')"
|
||||||
|
:readonly="accountingReadonly"
|
||||||
|
:required="true"
|
||||||
|
:error="ribErrors[index]?.label"
|
||||||
|
/>
|
||||||
|
<MalioInputText
|
||||||
|
v-model="rib.bic"
|
||||||
|
:label="t('technique.providers.form.accounting.ribBic')"
|
||||||
|
:readonly="accountingReadonly"
|
||||||
|
:required="true"
|
||||||
|
:error="ribErrors[index]?.bic"
|
||||||
|
/>
|
||||||
|
<MalioInputText
|
||||||
|
v-model="rib.iban"
|
||||||
|
:label="t('technique.providers.form.accounting.ribIban')"
|
||||||
|
:readonly="accountingReadonly"
|
||||||
|
:required="true"
|
||||||
|
:error="ribErrors[index]?.iban"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!accountingReadonly" class="flex justify-center gap-6">
|
||||||
|
<MalioButton
|
||||||
|
v-if="isRibRequired"
|
||||||
|
variant="secondary"
|
||||||
|
icon-name="mdi:add-bold"
|
||||||
|
icon-position="left"
|
||||||
|
:label="t('technique.providers.form.accounting.addRib')"
|
||||||
|
:disabled="!canAddRib"
|
||||||
|
@click="addRib"
|
||||||
|
/>
|
||||||
|
<MalioButton
|
||||||
|
variant="primary"
|
||||||
|
:label="t('technique.providers.form.submit')"
|
||||||
|
:disabled="tabSubmitting"
|
||||||
|
@click="onSubmitAccounting"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</MalioTabList>
|
</MalioTabList>
|
||||||
|
|
||||||
<!-- Modal de confirmation generique (suppression d'un bloc contact). -->
|
<!-- Modal de confirmation generique (suppression d'un bloc contact). -->
|
||||||
@@ -158,8 +287,15 @@
|
|||||||
import { computed, onMounted, reactive, ref } from 'vue'
|
import { computed, onMounted, reactive, ref } from 'vue'
|
||||||
import { useProviderReferentials, type RefOption } from '~/modules/technique/composables/useProviderReferentials'
|
import { useProviderReferentials, type RefOption } from '~/modules/technique/composables/useProviderReferentials'
|
||||||
import { useProviderForm } from '~/modules/technique/composables/useProviderForm'
|
import { useProviderForm } from '~/modules/technique/composables/useProviderForm'
|
||||||
|
import {
|
||||||
|
isBankRequiredForPaymentType,
|
||||||
|
isRibRequiredForPaymentType,
|
||||||
|
} from '~/modules/technique/utils/forms/providerAccounting'
|
||||||
import { extractApiErrorMessage } from '~/shared/utils/api'
|
import { extractApiErrorMessage } from '~/shared/utils/api'
|
||||||
|
|
||||||
|
// Masque SIREN : 9 chiffres (la normalisation finale reste serveur).
|
||||||
|
const SIREN_MASK = '#########'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -200,6 +336,16 @@ const {
|
|||||||
addAddress,
|
addAddress,
|
||||||
removeAddress,
|
removeAddress,
|
||||||
submitAddresses,
|
submitAddresses,
|
||||||
|
accounting,
|
||||||
|
ribs,
|
||||||
|
accountingErrors,
|
||||||
|
ribErrors,
|
||||||
|
accountingReadonly,
|
||||||
|
setPaymentType,
|
||||||
|
canAddRib,
|
||||||
|
addRib,
|
||||||
|
removeRib,
|
||||||
|
submitAccounting,
|
||||||
} = useProviderForm()
|
} = useProviderForm()
|
||||||
|
|
||||||
/** Retour vers le repertoire prestataires (fleche d'en-tete). */
|
/** Retour vers le repertoire prestataires (fleche d'en-tete). */
|
||||||
@@ -282,6 +428,43 @@ function askRemoveAddress(index: number): void {
|
|||||||
askConfirm(t('technique.providers.form.confirmDelete.address'), () => removeAddress(index))
|
askConfirm(t('technique.providers.form.confirmDelete.address'), () => removeAddress(index))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Onglet Comptabilite ───────────────────────────────────────────────────────
|
||||||
|
// Code stable du type de reglement selectionne (pour RG-3.07 / RG-3.08).
|
||||||
|
const selectedPaymentTypeCode = computed(() =>
|
||||||
|
referentials.paymentTypes.value.find(p => p.value === accounting.paymentTypeIri)?.code ?? null,
|
||||||
|
)
|
||||||
|
const isBankRequired = computed(() => isBankRequiredForPaymentType(selectedPaymentTypeCode.value))
|
||||||
|
const isRibRequired = computed(() => isRibRequiredForPaymentType(selectedPaymentTypeCode.value))
|
||||||
|
|
||||||
|
// Les blocs RIB ne sont affiches que pour une LCR (RG-3.08).
|
||||||
|
const visibleRibs = computed(() => isRibRequired.value ? ribs.value : [])
|
||||||
|
|
||||||
|
/** Changement de type de reglement : propage les RG inter-champs (banque / RIB). */
|
||||||
|
function onPaymentTypeChange(value: string | number | null): void {
|
||||||
|
const iri = value === null ? null : String(value)
|
||||||
|
const code = referentials.paymentTypes.value.find(p => p.value === iri)?.code ?? null
|
||||||
|
setPaymentType(iri, isBankRequiredForPaymentType(code), isRibRequiredForPaymentType(code))
|
||||||
|
}
|
||||||
|
|
||||||
|
function askRemoveRib(index: number): void {
|
||||||
|
askConfirm(t('technique.providers.form.confirmDelete.rib'), () => removeRib(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Valide l'onglet Comptabilite ; toast de succes si l'onglet a ete finalise. */
|
||||||
|
async function onSubmitAccounting(): Promise<void> {
|
||||||
|
const ok = await submitAccounting(
|
||||||
|
isBankRequired.value,
|
||||||
|
isRibRequired.value,
|
||||||
|
error => toast.error({
|
||||||
|
title: t('technique.providers.toast.error'),
|
||||||
|
message: apiErrorMessage(error),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
if (ok) {
|
||||||
|
toast.success({ title: t('technique.providers.toast.updateSuccess') })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Modal de confirmation generique ─────────────────────────────────────────
|
// ── Modal de confirmation generique ─────────────────────────────────────────
|
||||||
const confirmModal = reactive({
|
const confirmModal = reactive({
|
||||||
open: false,
|
open: false,
|
||||||
@@ -320,5 +503,9 @@ const tabs = computed(() => tabKeys.value.map((key, index) => ({
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// Echec du chargement des referentiels non bloquant : les selects restent vides.
|
// Echec du chargement des referentiels non bloquant : les selects restent vides.
|
||||||
referentials.loadMain().catch(() => {})
|
referentials.loadMain().catch(() => {})
|
||||||
|
// Referentiels comptables charges uniquement si l'onglet est accessible.
|
||||||
|
if (canAccountingView.value) {
|
||||||
|
referentials.loadAccounting().catch(() => {})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -124,3 +124,54 @@ export function emptyProviderAddress(): ProviderAddressFormDraft {
|
|||||||
export interface ProviderAddressResponse {
|
export interface ProviderAddressResponse {
|
||||||
id: number
|
id: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Etat « plat » de l'onglet Comptabilite (groupe `provider:write:accounting`).
|
||||||
|
* Relations (TVA / delai / type de reglement / banque) portees par leur IRI.
|
||||||
|
*/
|
||||||
|
export interface ProviderAccountingDraft {
|
||||||
|
siren: string | null
|
||||||
|
accountNumber: string | null
|
||||||
|
tvaModeIri: string | null
|
||||||
|
nTva: string | null
|
||||||
|
paymentDelayIri: string | null
|
||||||
|
paymentTypeIri: string | null
|
||||||
|
/** Banque : requise et envoyee uniquement si Type de reglement = VIREMENT (RG-3.07). */
|
||||||
|
bankIri: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fabrique un onglet Comptabilite vierge. */
|
||||||
|
export function emptyProviderAccounting(): ProviderAccountingDraft {
|
||||||
|
return {
|
||||||
|
siren: null,
|
||||||
|
accountNumber: null,
|
||||||
|
tvaModeIri: null,
|
||||||
|
nTva: null,
|
||||||
|
paymentDelayIri: null,
|
||||||
|
paymentTypeIri: null,
|
||||||
|
bankIri: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Un RIB du prestataire (sous-collection comptable, obligatoire si Type = LCR — RG-3.08). */
|
||||||
|
export interface ProviderRibFormDraft {
|
||||||
|
id: number | null
|
||||||
|
label: string | null
|
||||||
|
bic: string | null
|
||||||
|
iban: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fabrique un RIB vierge. */
|
||||||
|
export function emptyProviderRib(): ProviderRibFormDraft {
|
||||||
|
return {
|
||||||
|
id: null,
|
||||||
|
label: null,
|
||||||
|
bic: null,
|
||||||
|
iban: null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reponse du POST /providers/{id}/ribs (id suffisant pour le suivi cote front). */
|
||||||
|
export interface ProviderRibResponse {
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import {
|
||||||
|
buildProviderAccountingPayload,
|
||||||
|
buildProviderRibPayload,
|
||||||
|
isBankRequiredForPaymentType,
|
||||||
|
isRibBlank,
|
||||||
|
isRibComplete,
|
||||||
|
isRibRequiredForPaymentType,
|
||||||
|
} from '../providerAccounting'
|
||||||
|
import { emptyProviderAccounting, emptyProviderRib } from '~/modules/technique/types/providerForm'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helpers purs de l'onglet Comptabilite prestataire (ERP-144) : RG inter-champs
|
||||||
|
* RG-3.07 (banque si VIREMENT) / RG-3.08 (RIB si LCR) + construction des payloads.
|
||||||
|
*/
|
||||||
|
describe('providerAccounting helpers', () => {
|
||||||
|
describe('RG-3.07 / RG-3.08 — type de reglement', () => {
|
||||||
|
it('banque requise uniquement pour VIREMENT', () => {
|
||||||
|
expect(isBankRequiredForPaymentType('VIREMENT')).toBe(true)
|
||||||
|
expect(isBankRequiredForPaymentType('LCR')).toBe(false)
|
||||||
|
expect(isBankRequiredForPaymentType('CHEQUE')).toBe(false)
|
||||||
|
expect(isBankRequiredForPaymentType(null)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('RIB requis uniquement pour LCR', () => {
|
||||||
|
expect(isRibRequiredForPaymentType('LCR')).toBe(true)
|
||||||
|
expect(isRibRequiredForPaymentType('VIREMENT')).toBe(false)
|
||||||
|
expect(isRibRequiredForPaymentType(null)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('isRibBlank / isRibComplete', () => {
|
||||||
|
it('un RIB vierge est vide et incomplet', () => {
|
||||||
|
expect(isRibBlank(emptyProviderRib())).toBe(true)
|
||||||
|
expect(isRibComplete(emptyProviderRib())).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('un RIB partiel n\'est ni vide ni complet', () => {
|
||||||
|
const rib = { ...emptyProviderRib(), iban: 'FR76...' }
|
||||||
|
expect(isRibBlank(rib)).toBe(false)
|
||||||
|
expect(isRibComplete(rib)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('un RIB avec libelle + BIC + IBAN est complet', () => {
|
||||||
|
const rib = { ...emptyProviderRib(), label: 'Compte', bic: 'BNPAFRPP', iban: 'FR76...' }
|
||||||
|
expect(isRibComplete(rib)).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('buildProviderAccountingPayload (RG-3.07)', () => {
|
||||||
|
it('envoie la banque si requise (VIREMENT)', () => {
|
||||||
|
const payload = buildProviderAccountingPayload({
|
||||||
|
...emptyProviderAccounting(),
|
||||||
|
paymentTypeIri: '/api/payment_types/3',
|
||||||
|
bankIri: '/api/banks/2',
|
||||||
|
}, true)
|
||||||
|
expect(payload.bank).toBe('/api/banks/2')
|
||||||
|
expect(payload.paymentType).toBe('/api/payment_types/3')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('force la banque a null si non requise (hors VIREMENT)', () => {
|
||||||
|
const payload = buildProviderAccountingPayload({
|
||||||
|
...emptyProviderAccounting(),
|
||||||
|
bankIri: '/api/banks/2',
|
||||||
|
}, false)
|
||||||
|
expect(payload.bank).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('buildProviderRibPayload', () => {
|
||||||
|
it('omet les champs requis vides (NotBlank back joue sur le champ)', () => {
|
||||||
|
const payload = buildProviderRibPayload(emptyProviderRib())
|
||||||
|
expect(payload).not.toHaveProperty('label')
|
||||||
|
expect(payload).not.toHaveProperty('bic')
|
||||||
|
expect(payload).not.toHaveProperty('iban')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('conserve les champs remplis', () => {
|
||||||
|
const payload = buildProviderRibPayload({ ...emptyProviderRib(), label: 'Compte', bic: 'BNPAFRPP', iban: 'FR76...' })
|
||||||
|
expect(payload).toEqual({ label: 'Compte', bic: 'BNPAFRPP', iban: 'FR76...' })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* Helpers purs de l'onglet Comptabilite prestataire (M3 Technique, ERP-144) —
|
||||||
|
* miroir SIMPLIFIE des regles M2, reimplemente cote module Technique (regle
|
||||||
|
* ABSOLUE n°1 : pas d'import inter-module). Portent les RG inter-champs RG-3.07
|
||||||
|
* (banque si VIREMENT) et RG-3.08 (RIB si LCR), testables sans Vue ni API.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ProviderAccountingDraft,
|
||||||
|
ProviderRibFormDraft,
|
||||||
|
} from '~/modules/technique/types/providerForm'
|
||||||
|
|
||||||
|
/** Code pivot du type de reglement imposant une banque (RG-3.07). */
|
||||||
|
const PAYMENT_TYPE_VIREMENT = 'VIREMENT'
|
||||||
|
/** Code pivot du type de reglement imposant au moins un RIB (RG-3.08). */
|
||||||
|
const PAYMENT_TYPE_LCR = 'LCR'
|
||||||
|
|
||||||
|
/** Champs RIB obligatoires non nullable cote back (NotBlank) — omis si vides au POST. */
|
||||||
|
const RIB_REQUIRED_NON_NULLABLE_KEYS = ['label', 'bic', 'iban'] as const
|
||||||
|
|
||||||
|
/** Vrai si une chaine porte au moins un caractere non-espace. */
|
||||||
|
function isFilled(value: string | null | undefined): boolean {
|
||||||
|
return value !== null && value !== undefined && value.trim() !== ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/** RG-3.07 : la banque n'est requise/visible que pour un reglement par VIREMENT. */
|
||||||
|
export function isBankRequiredForPaymentType(code: string | null | undefined): boolean {
|
||||||
|
return code === PAYMENT_TYPE_VIREMENT
|
||||||
|
}
|
||||||
|
|
||||||
|
/** RG-3.08 : au moins un RIB n'est requis que pour un reglement par LCR. */
|
||||||
|
export function isRibRequiredForPaymentType(code: string | null | undefined): boolean {
|
||||||
|
return code === PAYMENT_TYPE_LCR
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Vrai si AUCUN champ du bloc RIB n'est rempli (amorce vide a ignorer au submit). */
|
||||||
|
export function isRibBlank(rib: ProviderRibFormDraft): boolean {
|
||||||
|
return ![rib.label, rib.bic, rib.iban].some(isFilled)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Vrai si les 3 champs du RIB sont remplis (gating « + RIB »). */
|
||||||
|
export function isRibComplete(rib: ProviderRibFormDraft): boolean {
|
||||||
|
return isFilled(rib.label) && isFilled(rib.bic) && isFilled(rib.iban)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payload du PATCH comptable (groupe `provider:write:accounting`). Les relations
|
||||||
|
* sont en IRI ; la banque n'est envoyee que si elle est requise (RG-3.07), sinon
|
||||||
|
* `null` (le back vide la relation hors VIREMENT).
|
||||||
|
*/
|
||||||
|
export function buildProviderAccountingPayload(
|
||||||
|
accounting: ProviderAccountingDraft,
|
||||||
|
isBankRequired: boolean,
|
||||||
|
): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
siren: accounting.siren || null,
|
||||||
|
accountNumber: accounting.accountNumber || null,
|
||||||
|
tvaMode: accounting.tvaModeIri,
|
||||||
|
nTva: accounting.nTva || null,
|
||||||
|
paymentDelay: accounting.paymentDelayIri,
|
||||||
|
paymentType: accounting.paymentTypeIri,
|
||||||
|
bank: isBankRequired ? accounting.bankIri : null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payload d'un RIB (sous-ressource, groupe `provider:write:accounting`). Les
|
||||||
|
* champs requis vides sont omis a la creation pour que la 422 NotBlank porte sur
|
||||||
|
* le champ.
|
||||||
|
*/
|
||||||
|
export function buildProviderRibPayload(rib: ProviderRibFormDraft): Record<string, unknown> {
|
||||||
|
const payload: Record<string, unknown> = {
|
||||||
|
label: rib.label,
|
||||||
|
bic: rib.bic,
|
||||||
|
iban: rib.iban,
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of RIB_REQUIRED_NON_NULLABLE_KEYS) {
|
||||||
|
const value = payload[key]
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
delete payload[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user