4d20583e1b
Le passage d'un tiers de LCR vers virement (ou autre) supprimait ses RIB en base via le front (DELETE differe). Le RIB est une coordonnee bancaire du tiers, decouplee du mode de reglement : on le conserve desormais pour un eventuel retour en LCR. Clients ET fournisseurs (new.vue / [id]/edit.vue) : - onPaymentTypeChange ne marque plus les RIB existants pour suppression et ne vide plus la saisie ; les RIB sont seulement masques (visibleRibs) et reapparaissent tels quels au retour LCR. - submitAccounting ne (re)soumet les RIB que sous LCR ; seules les suppressions EXPLICITES (corbeille d'un bloc) restent en DELETE. Consultation ([id]/index.vue) : RIB dormants totalement masques hors-LCR, via le helper pur type-safe paymentTypeCodeOf (clientConsultation / supplierConsultation) + tests Vitest. Aucune modification back (RG LCR -> >=1 RIB deja correcte, rien n'interdit un RIB sur un tiers non-LCR) ni migration.
317 lines
12 KiB
TypeScript
317 lines
12 KiB
TypeScript
/**
|
|
* Helpers purs de l'ecran « Consultation fournisseur » (M2 Commercial, lecture
|
|
* seule). Miroir de `clientConsultation.ts` (M1), adapte aux differences M2.
|
|
*
|
|
* Mappent le payload `GET /api/suppliers/{id}` (relations embarquees, cf. groupe
|
|
* `supplier:item:read` + `supplier:read:accounting`) vers les brouillons « plats »
|
|
* partages avec les blocs reutilisables `SupplierContactBlock` / `SupplierAddressBlock`
|
|
* et l'onglet Comptabilite. Ne touchent ni a l'API ni a l'etat reactif : testables
|
|
* unitairement (cf. supplierConsultation.spec.ts).
|
|
*
|
|
* Rappels de contrat back (verifies sur le JSON reel fige — ERP-92, spec-back § 4.0.bis) :
|
|
* - les relations ManyToOne (tvaMode/paymentDelay/paymentType/bank) sont
|
|
* serialisees en OBJETS embarques (`{id, code, label}`), pas en IRI nu ;
|
|
* - les champs nuls sont OMIS du JSON (skip_null_values) → toujours lire avec `?? null` ;
|
|
* - les champs comptables et `ribs` sont TOTALEMENT ABSENTS (cle omise, pas `null`)
|
|
* sans permission accounting.view (gate serveur via SupplierReadGroupContextBuilder).
|
|
*
|
|
* Differences M2 vs M1 :
|
|
* - Adresse via enum `addressType` (PROSPECT/DEPART/RENDU, RG-2.09) — pas de
|
|
* drapeaux isProspect/isDelivery/isBilling.
|
|
* - Adresse : champs specifiques fournisseur `bennes` (nombre) et `triageProvider`.
|
|
* Pas d'email de facturation.
|
|
* - Information : champ specifique fournisseur `volumeForecast`.
|
|
* - Pas de relation Distributeur/Courtier ni de triage sur le bloc principal.
|
|
*/
|
|
|
|
import { formatPhoneFR } from '~/shared/utils/phone'
|
|
import {
|
|
emptyAddress,
|
|
type SupplierAddressFormDraft,
|
|
type SupplierAddressType,
|
|
type SupplierContactFormDraft,
|
|
type SupplierRibFormDraft,
|
|
} from '~/modules/commercial/types/supplierForm'
|
|
|
|
/** Reference Hydra embarquee minimale (@id toujours present). */
|
|
export interface HydraRef {
|
|
'@id': string
|
|
[key: string]: unknown
|
|
}
|
|
|
|
/** Une relation peut etre embarquee (objet), un IRI nu (chaine) ou absente. */
|
|
export type Relation = HydraRef | string | null | undefined
|
|
|
|
/** Site embarque dans une adresse (groupe site:read). */
|
|
export interface SiteRead extends HydraRef {
|
|
name?: string
|
|
color?: string
|
|
}
|
|
|
|
/** Categorie embarquee (groupe category:read). */
|
|
export interface CategoryRead extends HydraRef {
|
|
code?: string
|
|
name?: string
|
|
}
|
|
|
|
/** Contact embarque (groupe supplier_contact:read). */
|
|
export interface ContactRead extends HydraRef {
|
|
id: number
|
|
firstName?: string | null
|
|
lastName?: string | null
|
|
jobTitle?: string | null
|
|
phonePrimary?: string | null
|
|
phoneSecondary?: string | null
|
|
email?: string | null
|
|
}
|
|
|
|
/** Adresse embarquee (groupe supplier_address:read). */
|
|
export interface AddressRead extends HydraRef {
|
|
id: number
|
|
addressType?: SupplierAddressType | null
|
|
country?: string | null
|
|
postalCode?: string | null
|
|
city?: string | null
|
|
street?: string | null
|
|
streetComplement?: string | null
|
|
bennes?: number | null
|
|
triageProvider?: boolean
|
|
sites?: SiteRead[]
|
|
categories?: CategoryRead[]
|
|
// L'embed M2M des contacts d'adresse peut etre un objet (partiel) ou un IRI nu.
|
|
contacts?: Array<HydraRef | string>
|
|
}
|
|
|
|
/** RIB embarque (groupe supplier:read:accounting, present ssi accounting.view). */
|
|
export interface RibRead extends HydraRef {
|
|
id: number
|
|
label?: string | null
|
|
bic?: string | null
|
|
iban?: string | null
|
|
}
|
|
|
|
/**
|
|
* Detail d'un fournisseur tel que renvoye par `GET /api/suppliers/{id}`. Tous les
|
|
* champs sont optionnels : skip_null_values cote serveur et gating accounting
|
|
* peuvent omettre n'importe quelle cle.
|
|
*/
|
|
export interface SupplierDetail extends HydraRef {
|
|
id: number
|
|
companyName?: string | null
|
|
isArchived?: boolean
|
|
categories?: CategoryRead[]
|
|
contacts?: ContactRead[]
|
|
addresses?: AddressRead[]
|
|
ribs?: RibRead[]
|
|
// Onglet Information
|
|
description?: string | null
|
|
competitors?: string | null
|
|
foundedAt?: string | null
|
|
employeesCount?: number | null
|
|
revenueAmount?: string | null
|
|
profitAmount?: string | null
|
|
directorName?: string | null
|
|
/** Volume previsionnel (entier, specifique fournisseur). */
|
|
volumeForecast?: number | null
|
|
// Onglet Comptabilite (present ssi accounting.view)
|
|
siren?: string | null
|
|
accountNumber?: string | null
|
|
nTva?: string | null
|
|
tvaMode?: Relation
|
|
paymentDelay?: Relation
|
|
paymentType?: Relation
|
|
bank?: Relation
|
|
}
|
|
|
|
/** Etat « plat » de l'onglet Comptabilite (miroir lecture du formulaire). */
|
|
export interface AccountingDraft {
|
|
siren: string | null
|
|
accountNumber: string | null
|
|
nTva: string | null
|
|
tvaModeIri: string | null
|
|
paymentDelayIri: string | null
|
|
paymentTypeIri: string | null
|
|
bankIri: string | null
|
|
}
|
|
|
|
/** Option de select ({ value, label }) construite a partir de l'embed. */
|
|
export interface SelectOption {
|
|
value: string
|
|
label: string
|
|
}
|
|
|
|
/** Option de categorie enrichie de son code (compatible CategoryOption des blocs). */
|
|
export interface CategorySelectOption extends SelectOption {
|
|
code: string
|
|
}
|
|
|
|
/**
|
|
* Vue d'une adresse pour la consultation : le brouillon + ses options de select
|
|
* construites a partir de l'embed (sites/categories propres a CETTE adresse).
|
|
*/
|
|
export interface AddressView {
|
|
draft: SupplierAddressFormDraft
|
|
siteOptions: SelectOption[]
|
|
categoryOptions: CategorySelectOption[]
|
|
}
|
|
|
|
/** Extrait l'IRI d'une relation (objet embarque, IRI nu, ou null si absente). */
|
|
export function iriOf(relation: Relation): string | null {
|
|
if (relation === null || relation === undefined) {
|
|
return null
|
|
}
|
|
if (typeof relation === 'string') {
|
|
return relation
|
|
}
|
|
return relation['@id'] ?? null
|
|
}
|
|
|
|
/** Mappe un contact embarque vers un brouillon (telephones formates XX XX XX XX XX). */
|
|
export function mapContactToDraft(contact: ContactRead): SupplierContactFormDraft {
|
|
const phoneSecondary = contact.phoneSecondary ?? null
|
|
return {
|
|
id: contact.id,
|
|
iri: contact['@id'] ?? null,
|
|
firstName: contact.firstName ?? null,
|
|
lastName: contact.lastName ?? null,
|
|
jobTitle: contact.jobTitle ?? null,
|
|
phonePrimary: contact.phonePrimary ? formatPhoneFR(contact.phonePrimary) : null,
|
|
phoneSecondary: phoneSecondary ? formatPhoneFR(phoneSecondary) : null,
|
|
email: contact.email ?? null,
|
|
hasSecondaryPhone: phoneSecondary !== null && phoneSecondary !== '',
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mappe une adresse embarquee vers un brouillon (IRI extraits des sous-collections).
|
|
* `bennes` (entier) est converti en chaine pour MalioInputNumber (defaut « 0 »).
|
|
*/
|
|
export function mapAddressToDraft(address: AddressRead): SupplierAddressFormDraft {
|
|
return {
|
|
id: address.id,
|
|
addressType: address.addressType ?? null,
|
|
country: address.country ?? 'France',
|
|
postalCode: address.postalCode ?? null,
|
|
city: address.city ?? null,
|
|
street: address.street ?? null,
|
|
streetComplement: address.streetComplement ?? null,
|
|
categoryIris: (address.categories ?? []).map(c => c['@id']),
|
|
siteIris: (address.sites ?? []).map(s => s['@id']),
|
|
contactIris: (address.contacts ?? []).map(c => (typeof c === 'string' ? c : c['@id'])),
|
|
bennes: address.bennes != null ? String(address.bennes) : '0',
|
|
triageProvider: address.triageProvider ?? false,
|
|
}
|
|
}
|
|
|
|
/** Mappe un RIB embarque vers un brouillon. */
|
|
export function mapRibToDraft(rib: RibRead): SupplierRibFormDraft {
|
|
return {
|
|
id: rib.id,
|
|
label: rib.label ?? null,
|
|
bic: rib.bic ?? null,
|
|
iban: rib.iban ?? null,
|
|
}
|
|
}
|
|
|
|
/** Mappe les champs comptables du fournisseur (scalaires + IRI des referentiels). */
|
|
export function mapAccountingDraft(supplier: SupplierDetail): AccountingDraft {
|
|
return {
|
|
siren: supplier.siren ?? null,
|
|
accountNumber: supplier.accountNumber ?? null,
|
|
nTva: supplier.nTva ?? null,
|
|
tvaModeIri: iriOf(supplier.tvaMode),
|
|
paymentDelayIri: iriOf(supplier.paymentDelay),
|
|
paymentTypeIri: iriOf(supplier.paymentType),
|
|
bankIri: iriOf(supplier.bank),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Options de categories (value=IRI, label=nom, code) construites depuis l'embed.
|
|
* Source role-independante : evite de dependre de `GET /categories` (403 pour les
|
|
* roles metier non-admin), qui laisserait les libelles vides.
|
|
*/
|
|
export function categoryOptionsOf(categories: CategoryRead[] | undefined): CategorySelectOption[] {
|
|
return (categories ?? []).map(c => ({
|
|
value: c['@id'],
|
|
label: c.name ?? c.code ?? c['@id'],
|
|
code: c.code ?? '',
|
|
}))
|
|
}
|
|
|
|
/** Options de sites (value=IRI, label=nom) construites depuis l'embed d'une adresse. */
|
|
export function siteOptionsOf(sites: SiteRead[] | undefined): SelectOption[] {
|
|
return (sites ?? []).map(s => ({ value: s['@id'], label: s.name ?? s['@id'] }))
|
|
}
|
|
|
|
/** Options de contacts (value=IRI, label=nom complet ou email) depuis l'embed fournisseur. */
|
|
export function contactOptionsOf(contacts: ContactRead[] | undefined): SelectOption[] {
|
|
return (contacts ?? []).map(c => ({
|
|
value: c['@id'],
|
|
label: [c.firstName, c.lastName].filter(Boolean).join(' ') || (c.email ?? c['@id']),
|
|
}))
|
|
}
|
|
|
|
/**
|
|
* Liste a une seule option (ou vide) construite depuis un referentiel embarque
|
|
* (TvaMode / PaymentDelay / PaymentType / Bank) pour alimenter un MalioSelect en
|
|
* lecture seule. Le libelle vient de l'embed (`label` ou `name`), jamais d'un
|
|
* `GET` de referentiel — l'affichage reste correct quel que soit le role.
|
|
*/
|
|
export function referentialOptionOf(relation: Relation): SelectOption[] {
|
|
if (!relation || typeof relation === 'string') {
|
|
return []
|
|
}
|
|
const label = (relation.label as string | undefined)
|
|
?? (relation.name as string | undefined)
|
|
?? relation['@id']
|
|
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 {
|
|
draft: mapAddressToDraft(address),
|
|
siteOptions: siteOptionsOf(address.sites),
|
|
categoryOptions: categoryOptionsOf(address.categories),
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Bouton « Modifier » : visible si l'utilisateur peut editer au moins un onglet
|
|
* — `manage` (formulaire/onglets metier) OU `accounting.manage` (le role Compta
|
|
* doit pouvoir ouvrir l'edition pour son onglet Comptabilite). Le readonly fin
|
|
* par onglet est gere sur l'ecran d'edition (96).
|
|
*/
|
|
export function canEditSupplier(canAny: (codes: string[]) => boolean): boolean {
|
|
return canAny(['commercial.suppliers.manage', 'commercial.suppliers.accounting.manage'])
|
|
}
|
|
|
|
/** Bouton « Archiver » : permission archive ET fournisseur encore actif. */
|
|
export function showArchiveAction(can: (code: string) => boolean, isArchived: boolean): boolean {
|
|
return can('commercial.suppliers.archive') && !isArchived
|
|
}
|
|
|
|
/** Bouton « Restaurer » : permission archive ET fournisseur deja archive. */
|
|
export function showRestoreAction(can: (code: string) => boolean, isArchived: boolean): boolean {
|
|
return can('commercial.suppliers.archive') && isArchived
|
|
}
|
|
|
|
/** Brouillon d'adresse vierge (reexport pour la page : 1 bloc vide si aucune adresse). */
|
|
export { emptyAddress }
|