Aller au contenu

Tiers#

Source de cette page

Chaque affirmation est sourcée du code (chemin de fichier indiqué). Le module Tiers est défini dans app/modules/tiers/__init__.py, les modèles dans app/models/common.py (factor commun avec d'autres modules), les routes dans app/api/routes/modules/tiers.py.

Résumé en 30 secondes#

Tiers gère les sociétés tierces (clients, fournisseurs, sous-traitants, partenaires) et leurs contacts internes. C'est la base de référence pour tous les modules qui consomment des entités externes :

  • PaxLog — un PAX peut être un User interne ou un TierContact externe (entrepreneur, visiteur)
  • TravelWiz — passager d'un manifeste = User ou TierContact, société émettrice cargo = Tier
  • Projets — sous-traitants assignés à un projet = Tier
  • Conformité — habilitations contrôlées sur un PAX peuvent remonter au Tier employeur (ban collectif)

Trois piliers :

  • Tier (la société) — code unique, infos légales (RCCM, NIU, TVA), industrie, devise, fuseau, blocage temporaire
  • TierContact (la personne) — civilité, nom, fonction, contact info via composants polymorphes (phones, emails, addresses, notes)
  • Portail externe — un TierContact peut être promu User pour accéder à app.opsflux.io, OU recevoir un lien magique signé pour ext.opsflux.io sans création de compte

Stack : 6 modèles SQLAlchemy (app/models/common.py), 18 endpoints API (app/api/routes/modules/tiers.py), 8 permissions, intégrations cross-modules.


1. À quoi ça sert#

Problème métier : un opérateur industriel travaille avec des dizaines à centaines de sociétés tierces — chaque chantier mobilise des sous-traitants spécialisés (échafaudage, électricité, IRATA), chaque flux logistique implique fournisseurs et transporteurs, chaque projet a ses partenaires. Sans référentiel central :

  • Saisie redondante de la même société dans chaque module métier
  • Pas de cohérence sur le "code" (un sous-traitant a 3 noms différents selon les chantiers)
  • Pas de traçabilité des contacts (qui appeler chez ce sous-traitant ?)
  • Pas de moyen de bloquer une société temporairement (litige paiement, manquement HSE) → des ADS partent malgré tout
  • Pas d'intégration ERP externe possible (pas d'identifiant légal exposé)

OpsFlux Tiers crée le référentiel unique. Tout autre module pointe ici quand il a besoin de « société externe » ou « contact externe ».

Pour qui :

Rôle Permissions clés (app/modules/tiers/__init__.py)
Gestionnaire référentiel (RH, achats, support) tier.read, tier.create, tier.update, tier.contact.manage
Admin Tiers (TIER_ADMIN) toutes les perms tier.* + tier.delete + tier.portal.manage
Importateur (one-shot lors de la mise en place) tier.import (CSV/XLSX bulk), tier.export

Tous les autres modules ont tier.read granted via leur propre rôle quand pertinent.


2. Concepts clés#

Terme Modèle / Table Description
Tier Tier / tiers La société. code unique par instance (ex. ECHAF-001, IRATA-FR-MARSEILLE). Type : client, supplier, subcontractor, partner.
TierContact TierContact / tier_contacts La personne employée par un tier. Civilité, nom, fonction. Phones/emails/addresses gérés via composants polymorphes (pas de champ direct).
TierBlock TierBlock / tier_blocks Blocage actif/expiré d'un tier (litige, manquement HSE, conformité). Dates start/end + raison. Empêche les ADS / cargo / contrats.
TierContactTransfer TierContactTransfer / tier_contact_transfers Audit du déplacement d'un contact d'un tier à un autre (changement d'employeur).
ExternalReference ExternalReference / external_references Identifiants externes (ERP partenaire, code fournisseur SAP, etc.). Permet l'intégration sans changer le code interne.
UserTierLink UserTierLink / user_tier_links Lien user_id ↔ tier_id quand un TierContact est promu User OpsFlux. Permet à un externe de se connecter à app.opsflux.io avec son périmètre limité.

Composants polymorphes utilisés par Tier et TierContact#

OpsFlux n'a pas de colonnes phone/email/address répétées sur chaque modèle. Tier et TierContact utilisent les tables polymorphes core (app/models/common.py) :

Table polymorphe Owner type Description
addresses tier, tier_contact Adresses postales (legal, postal, livraison)
phones tier, tier_contact Téléphones (work, mobile, home, fax)
contact_emails tier, tier_contact Adresses email (work, personal, other)
legal_identifiers tier, tier_contact Identifiants légaux (RCCM, NIU, TVA, SIREN, passport, NIN, etc.)
notes tier, tier_contact Notes internes versionnées
attachments tier, tier_contact Fichiers attachés (contrat, KBIS, photo identité)
tags tier, tier_contact Tags libres pour catégorisation

Avantage : un module a les mêmes APIs pour gérer phones/addresses sur n'importe quel objet (User, Tier, TierContact, …). Pas de duplication de code, pas de champ "phone1/phone2/phone3".

Utilisation dans l'interface OpsFlux#

La fiche d'un tier est organisée pour séparer les responsabilités :

  • Fiche : identité, description, pays, logo, informations légales et blocage.
  • Contacts : employés du tier, emails, téléphones, adresses et rattachements.
  • Conformité : référentiels et preuves nécessaires à la vérification.
  • Projets : projets associés à l'entreprise, avec les informations utiles pour comprendre le contexte opérationnel.
  • Documents : notes et pièces jointes polymorphes.

Les logos peuvent venir d'une URL ou d'une pièce jointe image. Les listes affichent le drapeau du pays pour gagner de l'espace, tandis que la fiche détaillée affiche le drapeau et le nom du pays.

Les documents et preuves sont portés par les composants polymorphes. Quand un référentiel de conformité exige une pièce justificative, la pièce doit être attachée au bon owner (tier ou tier_contact) et typée clairement. Le type de pièce doit être visible sous forme de tag afin d'éviter une saisie ambiguë ou une liste déroulante redondante.

La prévisualisation des pièces jointes permet de contrôler rapidement PDF, images, documents Office et fichiers techniques lorsqu'un viewer est disponible. Le fichier original reste la référence d'audit.

Champs Tier importants#

app/models/common.py:637-696 :

code (unique), name, alias, trade_name, logo_url
type (client | supplier | subcontractor | partner)
website
legal_form (SARL, SA, SAS, GIE, ...)
registration_number, tax_id, vat_number  -- aussi en legal_identifiers
capital, currency (XAF par défaut), fiscal_year_start
industry, founded_date, payment_terms, incoterm + incoterm_city
description, country, language, timezone
is_blocked (bool), scope (local | international)
metadata_ (JSONB libre)
social_networks (JSONB), opening_hours (JSONB)

Note legacy : les colonnes directes phone, email, fax, address_* sur tiers sont conservées pour rétro-compat mais le code moderne doit utiliser les tables polymorphes. À terme ces colonnes seront retirées.

Champs TierContact importants#

app/models/common.py:698+ :

tier_id (FK), civility, first_name, last_name, title (poste)
-- AUCUN champ phone/email direct, tout via polymorphic

3. Architecture data#

graph TD
    TIER[Tier<br/>tiers — société, code unique]
    TIER -->|0..N| CONT[TierContact<br/>tier_contacts — employé]
    TIER -->|0..N| BLK[TierBlock<br/>blocage actif/expiré]
    TIER -->|0..N| EXT[ExternalReference<br/>codes ERP partenaires]

    CONT -->|0..N| TRANS[TierContactTransfer<br/>audit changement employeur]

    TIER -.->|polymorphes<br/>owner_type='tier'| POLY1[addresses, phones,<br/>contact_emails, legal_identifiers,<br/>notes, attachments, tags]
    CONT -.->|polymorphes<br/>owner_type='tier_contact'| POLY2[mêmes tables]

    CONT -->|0..1| LINK[UserTierLink<br/>promotion en User]
    LINK -->|user_id| USER[User OpsFlux<br/>se connecte à app.opsflux.io]

    CONT -.->|consommé par| PAX[PaxLog<br/>AdsPax.contact_id]
    CONT -.->|consommé par| TW[TravelWiz<br/>ManifestPassenger.contact_id]
    TIER -.->|consommé par| PROJ[Projets<br/>sous-traitants]
    TIER -.->|consommé par| CARGO[PackLog<br/>cargo sender/receiver]

Lecture rapide :

  • Une Tier contient 0..N TierContact, 0..N blocages, 0..N références externes
  • Le TierContact est polymorphe XOR avec User : un PAX (paxlog) est user OU contact, jamais les deux
  • Quand un externe doit accéder à app.opsflux.io, on promeut le TierContact en User via POST /tiers/{tid}/contacts/{cid}/promote-user (tiers.py:479)
  • L'UserTierLink matérialise le lien — l'utilisateur garde sa société d'origine, hérite de ses permissions limitées

4. Workflow Tier — états (pas de FSM strict)#

Tier n'a pas de FSM au sens PaxLog/MOC. Les "états" sont combinatoires :

Combinaison Effet
active=true, is_blocked=false Normalement utilisable partout
active=true, is_blocked=true Visible mais rejeté par les modules consommateurs (ADS, cargo, manifestes)
active=false Soft-désactivé — masqué des pickers, pas de nouvelle relation possible
deleted_at IS NOT NULL Soft-deleted (mixin SoftDelete). Invisible. Restaurable.

Cycle de vie typique#

stateDiagram-v2
    [*] --> active : POST /tiers
    active --> blocked : POST /tiers/{id}/block (raison + dates)
    blocked --> active : POST /tiers/{id}/unblock
    active --> inactive : PATCH active=false
    inactive --> active : PATCH active=true
    active --> [*] : DELETE (soft)
    blocked --> [*] : DELETE (soft)

Endpoints blocage#

Action Endpoint Source
Lister blocages d'un tier GET /api/v1/tiers/{id}/blocks 560
Bloquer POST /api/v1/tiers/{id}/block 586
Débloquer POST /api/v1/tiers/{id}/unblock 626

Un blocage actif est détecté par les modules consommateurs (PaxLog _check_pax_ban_status consulte tier_blocks pour déterminer si le tier d'un AdsPax externe est bloqué).


5. Step-by-step utilisateur#

5.1 — Gestionnaire référentiel : créer une société + ses contacts#

  1. /tiers (apps/main/src/pages/tiers/TiersPage.tsx)
  2. + Nouveau tier → panneau de création
  3. Renseigner :
  4. Code unique (convention conseillée : <type>-<nom> ex. SUB-IRATA-FR)
  5. Nom légal + alias / trade name
  6. Type : client / supplier / subcontractor / partner
  7. Pays, devise (par défaut XAF), timezone, langue
  8. Ajouter onglets latéraux :
  9. Adresses (legal, postal, livraison) via <AddressManager>
  10. Téléphones (work, mobile, fax) via <PhoneManager>
  11. Emails via <EmailManager>
  12. Identifiants légaux (RCCM, NIU, TVA, …) via composant dédié
  13. Notes, fichiers, tags — composants polymorphes core
  14. Onglet Contacts (TierContacts.tsx) : créer les TierContact (employés). Chaque contact reçoit ses propres polymorphic phones/emails/addresses.

5.2 — Bloquer un tier (litige, manquement HSE)#

  1. Ouvrir le tier → bouton Bloquer
  2. Modale : raison obligatoire, date début (default = today), date fin (optionnelle — null = blocage permanent jusqu'à unblock explicite)
  3. Le blocage est immédiatement effectif :
  4. Les modules consommateurs vérifient via _check_tier_block_status
  5. Tentatives de soumettre une ADS / un cargo avec ce tier retournent 400 + raison du block
  6. Pour débloquer : bouton Débloquer sur la fiche du tier (permission tier.update). Réactive immédiatement.

5.3 — Promouvoir un TierContact en User OpsFlux#

Cas d'usage : un sous-traitant doit accéder à app.opsflux.io pour soumettre ses propres ADS, voir ses missions, etc.

  1. Onglet Contacts du tier → ouvrir le contact
  2. Bouton Promouvoir en utilisateur
  3. Le système :
  4. Crée un User avec email du contact (récupéré depuis contact_emails polymorphe)
  5. Génère un mot de passe initial + envoie email d'invitation
  6. Crée un UserTierLink(user_id, tier_id)
  7. Le user hérite des permissions du rôle assigné (typiquement EXT_REQUESTER avec paxlog.ads.create, etc.)
  8. Endpoint : POST /api/v1/tiers/{tid}/contacts/{cid}/promote-user (tiers.py:479)
  9. Permission requise : tier.portal.manage

5.4 — Transférer un contact (changement d'employeur)#

Quand un employé change de société :

  1. Sur la fiche contact → bouton Transférer vers autre société
  2. Picker du nouveau tier
  3. Le système :
  4. Crée un TierContactTransfer(contact_id, from_tier, to_tier, transfer_date, reason)
  5. Met à jour contact.tier_id vers le nouveau tier
  6. Préserve toutes les relations existantes (ADS passées, manifestes, phones polymorphes restent attachés au contact)
  7. Audit complet préservé via la table tier_contact_transfers

5.5 — Importer en bulk (mise en place initiale)#

Endpoints tier.import / tier.export permettent l'import CSV/XLSX en lot — utile pour la mise en place initiale d'OpsFlux quand on a plusieurs centaines de sociétés à charger depuis un fichier Excel existant. Format documenté dans Settings → Import/Export.


6. Permissions matrix#

8 permissions définies dans le MANIFEST (app/modules/tiers/__init__.py:9-17) :

Permission Effet
tier.read GET liste + détail tiers + contacts
tier.create POST nouveau tier
tier.update PATCH tier + block/unblock
tier.delete DELETE soft tier
tier.contact.manage CRUD complet sur TierContact
tier.portal.manage Promotion contact → user, gestion accès portail externe
tier.import Import CSV/XLSX bulk
tier.export Export CSV/XLSX

Rôle système#

Le seul rôle système déclaré est TIER_ADMIN. Pour les profils opérationnels (achats, support, RH), composer les permissions par rôle custom selon le périmètre :

Lecteur référentiel     : tier.read
Gestionnaire achats     : tier.read + tier.create + tier.update + tier.contact.manage
Admin tiers complet     : TIER_ADMIN (toutes)

7. Endpoints (résumé)#

18 endpoints dans app/api/routes/modules/tiers.py.

Action Endpoint Source
Lister tiers GET /api/v1/tiers (avant 205)
Créer POST /api/v1/tiers (avant 205)
Détail GET /api/v1/tiers/{id} 205
Update PATCH /api/v1/tiers/{id} 223
Soft delete DELETE /api/v1/tiers/{id} 239
Tous contacts (cross-tier) GET /api/v1/tiers/contacts/all 256
Détail contact (cross-tier) GET /api/v1/tiers/contacts/all/{cid} 298
Contacts d'un tier GET /api/v1/tiers/{id}/contacts 345
Compter contacts GET /api/v1/tiers/{id}/contacts/count 362
Détail contact GET /api/v1/tiers/{id}/contacts/{cid} 377
Créer contact POST /api/v1/tiers/{id}/contacts 389
Update contact PATCH /api/v1/tiers/{id}/contacts/{cid} 444
Supprimer contact DELETE /api/v1/tiers/{id}/contacts/{cid} 464
Promouvoir en user POST /api/v1/tiers/{id}/contacts/{cid}/promote-user 479
Lister blocages GET /api/v1/tiers/{id}/blocks 560
Bloquer POST /api/v1/tiers/{id}/block 586
Débloquer POST /api/v1/tiers/{id}/unblock 626
Lister refs externes GET /api/v1/tiers/{id}/external-refs 680
Créer ref externe POST /api/v1/tiers/{id}/external-refs 698
Supprimer ref externe DELETE /api/v1/tiers/{id}/external-refs/{rid} 725

8. Intégrations cross-modules#

Tous les modules métier consomment Tiers :

graph LR
    TIERS[Tiers] -->|Tier IDs| PAX[PaxLog<br/>AdsAllowedCompany]
    TIERS -->|TierContact IDs| PAX_C[PaxLog<br/>AdsPax.contact_id]
    TIERS -->|TierContact IDs| TW[TravelWiz<br/>ManifestPassenger.contact_id]
    TIERS -->|Tier IDs| PACK[PackLog<br/>cargo sender / receiver]
    TIERS -->|Tier IDs| PROJ[Projets<br/>sous-traitants assignés]
    TIERS -->|Tier IDs| MOC[MOC<br/>tiers impactés]
    TIERS -->|TierBlock| ALL[Tous modules<br/>filtre les blocked]

Effet du TierBlock :

  • PaxLog : ADS soumise avec un PAX externe d'un tier blocked → 400 TIER_BLOCKED dès le compliance check.
  • PackLog : cargo avec sender/receiver blocked → idem.
  • Projets : impossible d'assigner un sous-traitant blocked.

Un block expiré (date_end < today) est automatiquement ignoré sans intervention.


9. Pièges & FAQ#

Le code du tier est-il modifiable ?#

Non, le code est unique au niveau base et utilisé comme clé business dans tous les modules (références ADS, manifestes, etc.). Pour "renommer" un tier sans changer son code, modifier name/alias qui sont libres.

Si vraiment besoin de changer le code : créer un nouveau tier + migrer les FK manuellement + soft-delete l'ancien. Aucun endpoint API ne le fait automatiquement (volontaire).

Pourquoi les phones/emails ne sont pas dans tier_contacts directement ?#

Architecturalement, OpsFlux utilise des composants polymorphes — les tables phones, contact_emails, addresses portent un (owner_type, owner_id) qui peut pointer vers n'importe quel modèle. Avantages :

  • Mêmes APIs/UI pour gérer phones partout (User, Tier, TierContact, …)
  • Aucune duplication de code
  • Multiple phones par contact sans souci (work, mobile, home, fax)
  • Auto-discoverable : ajouter un nouveau type d'owner = 0 migration

L'UI cache cette complexité — le composant <PhoneManager> reçoit juste (ownerType, ownerId) et fait son CRUD.

Un TierContact promu en User peut-il revenir TierContact uniquement ?#

Pas directement. Le User créé existe, et les ADS/sessions associées ne peuvent pas être ré-attribuées au TierContact. Ce qu'on peut faire : désactiver le User (active=false), conserver le TierContact, le TierContact reste utilisable pour ses fonctions externes.

Comment gérer un sous-traitant qui change de raison sociale ?#

Deux cas :

  1. Même entité juridique, juste rebranding → modifier name et alias/trade_name, garder code et historique.
  2. Nouvelle entité juridique (rachat, fusion, redressement) → créer un nouveau tier + transférer les contacts via TierContactTransfer. Les anciennes ADS/manifestes pointent toujours sur l'ancien tier (intégrité historique).

Le tableau Tous contacts est lent#

L'endpoint /contacts/all paginate par défaut. Vérifier qu'on appelle bien avec ?page=1&limit=50. Index dispo : idx_tier_contacts_tier. Si filtre custom hors index → EXPLAIN pour vérifier.

Les colonnes registration_number, tax_id, vat_number sur tiers sont legacy (créées avant la table polymorphe legal_identifiers). Le code moderne utilise legal_identifiers qui supporte plusieurs identifiants par tier (RCCM Cameroun, NIU, SIREN France si filiale, etc.).

À terme les colonnes legacy seront migrées vers legal_identifiers puis supprimées.

ExternalReference vs code du tier#

  • code : identifiant interne OpsFlux, unique, utilisé dans toutes les références (URLs, références ADS, etc.)
  • ExternalReference : identifiant chez un partenaire externe (code SAP du fournisseur, ID dans l'ERP du client). Permet de mapper les flux d'intégration sans changer l'identifiant interne.

Exemple : un même fournisseur peut avoir code=SUB-IRATA-FR chez nous, ExternalReference(provider='SAP', value='V12345') côté SAP du client, et ExternalReference(provider='ARIBA', value='8765432') côté Ariba. Sync ERP → on lookup par ExternalReference.


10. Liens#

Code#

Voir aussi#

  • PaxLog — consomme TierContact pour les PAX externes
  • TravelWiz — consomme TierContact pour les passagers externes
  • Spec architecturale : Spec Tiers (auth requise)