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
Userinterne ou unTierContactexterne (entrepreneur, visiteur) - TravelWiz — passager d'un manifeste =
UserouTierContact, société émettrice cargo =Tier - Projets — sous-traitants assignés à un projet =
Tier - Conformité — habilitations contrôlées sur un PAX peuvent
remonter au
Tieremployeur (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é pourext.opsflux.iosans 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,fax,address_*surtierssont conservées pour rétro-compat mais le code moderne doit utiliser les tables polymorphes. À terme ces colonnes seront retirées.
Champs TierContact importants#
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 viaPOST /tiers/{tid}/contacts/{cid}/promote-user(tiers.py:479) - L'
UserTierLinkmaté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#
/tiers(apps/main/src/pages/tiers/TiersPage.tsx)+ Nouveau tier→ panneau de création- Renseigner :
- Code unique (convention conseillée :
<type>-<nom>ex.SUB-IRATA-FR) - Nom légal + alias / trade name
- Type :
client/supplier/subcontractor/partner - Pays, devise (par défaut XAF), timezone, langue
- Ajouter onglets latéraux :
- Adresses (legal, postal, livraison) via
<AddressManager> - Téléphones (work, mobile, fax) via
<PhoneManager> - Emails via
<EmailManager> - Identifiants légaux (RCCM, NIU, TVA, …) via composant dédié
- Notes, fichiers, tags — composants polymorphes core
- 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)#
- Ouvrir le tier → bouton
Bloquer - Modale : raison obligatoire, date début (default = today),
date fin (optionnelle — null = blocage permanent jusqu'à
unblockexplicite) - Le blocage est immédiatement effectif :
- Les modules consommateurs vérifient via
_check_tier_block_status - Tentatives de soumettre une ADS / un cargo avec ce tier retournent 400 + raison du block
- Pour débloquer : bouton
Débloquersur la fiche du tier (permissiontier.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.
- Onglet Contacts du tier → ouvrir le contact
- Bouton
Promouvoir en utilisateur - Le système :
- Crée un
Useravec email du contact (récupéré depuiscontact_emailspolymorphe) - Génère un mot de passe initial + envoie email d'invitation
- Crée un
UserTierLink(user_id, tier_id) - Le user hérite des permissions du rôle assigné (typiquement
EXT_REQUESTERavecpaxlog.ads.create, etc.) - Endpoint :
POST /api/v1/tiers/{tid}/contacts/{cid}/promote-user(tiers.py:479) - Permission requise :
tier.portal.manage
5.4 — Transférer un contact (changement d'employeur)#
Quand un employé change de société :
- Sur la fiche contact → bouton
Transférer vers autre société - Picker du nouveau tier
- Le système :
- Crée un
TierContactTransfer(contact_id, from_tier, to_tier, transfer_date, reason) - Met à jour
contact.tier_idvers le nouveau tier - Préserve toutes les relations existantes (ADS passées, manifestes, phones polymorphes restent attachés au contact)
- 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_BLOCKEDdè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 :
- Même entité juridique, juste rebranding → modifier
nameetalias/trade_name, garder code et historique. - 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.
Identifiants légaux (RCCM, TVA) — dans tiers ou legal_identifiers ?#
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#
app/modules/tiers/__init__.py— manifest (8 perms, 1 rôle)app/api/routes/modules/tiers.py— 18 endpoints (~955 lignes)app/models/common.py:637-816— Tier, TierContact, TierBlockapp/models/common.py:1592— TierContactTransferapp/models/common.py:2286— ExternalReferenceapp/models/common.py:468— UserTierLinkapps/main/src/pages/tiers/— UI
Voir aussi#
- PaxLog — consomme TierContact pour les PAX externes
- TravelWiz — consomme TierContact pour les passagers externes
- Spec architecturale : Spec Tiers (auth requise)