fcx-toolkit — Manuel d'utilisation
Version 1.0.0 — 2026-07-20
Table des matières
- Introduction
- Installation
- Format de réponse JSON
- Appel depuis PHP
- Configuration
- Commande
execute - Commande
extract - Commande
flexcube-batch - Ajouter un nouveau workflow
- Codes de sortie
- Dépannage
1. Introduction
fcx-toolkit est un outil en ligne de commande autonome qui permet de :
- exécuter du SQL ou du PL/SQL sur une base Oracle Flexcube ;
- exporter le résultat d'une requête en CSV, Excel ou JSON ;
- intégrer un batch d'écritures comptables depuis un fichier CSV ou Excel, en appliquant les contrôles métier Flexcube avant tout envoi ;
- ajouter de nouveaux traitements en déposant un fichier
.py, sans modifier une ligne du code existant.
À qui s'adresse cet outil ? À un développeur ou un exploitant qui doit intervenir sur Flexcube sans connaître l'application Laravel dont les scripts historiques dépendaient. Aucune installation d'Oracle Instant Client, aucun identifiant en dur, aucun couplage à une API tierce.
L'outil est conçu pour être appelé par un programme. Chaque exécution
produit un seul document JSON sur la sortie standard — y compris en cas
d'erreur. Un appelant PHP peut donc faire un json_decode sans précaution
particulière : voir Format de réponse JSON et
Appel depuis PHP.
Principe de sécurité central : les contrôles métier accumulent toutes les anomalies et les présentent ensemble. Une seule exécution suffit pour connaître la liste complète des corrections. Et rien n'est envoyé tant qu'une seule anomalie subsiste.
2. Installation
2.1 Prérequis
| Élément | Nécessité | Remarque |
|---|---|---|
| Python ≥ 3.9 | obligatoire | Testé sous 3.12 |
oracledb |
installé par requirements.txt |
Mode thin : aucun Instant Client |
| LibreOffice | facultatif | Uniquement pour la conversion PDF |
| Accès réseau Oracle | à l'exécution seulement | La suite de tests n'en a pas besoin |
2.2 Étapes
git clone <url-du-depot> fcx-toolkit
cd fcx-toolkit
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
Vérifiez ensuite l'installation. La commande commands liste, en JSON, les
commandes découvertes :
.venv/bin/python fcx.py commands
{
"status": "success",
"command": "commands",
"exit_code": 0,
"messages": ["3 commande(s) disponible(s)."],
"data": {
"count": 3,
"commands": [
{"name": "execute", "help": "Exécute du SQL ou du PL/SQL ...", "source": ".../fcx/commands/execute.py"},
{"name": "extract", "help": "Exécute un SELECT et exporte ...", "source": ".../fcx/commands/extract.py"},
{"name": "flexcube-batch", "help": "Intègre un batch ...", "source": ".../workflows/flexcube_batch.py"}
]
},
"error": null
}
Si execute, extract et flexcube-batch apparaissent, la découverte
automatique des commandes fonctionne.
--helpreste la seule sortie en texte, car elle est destinée à un humain. Un appelant automatisé utilisecommands, qui renvoie la même information en JSON.
Lancez enfin la suite de tests — elle ne requiert ni base ni réseau :
.venv/bin/pytest
Sortie attendue : 124 passed.
2.3 Lancement sans installation
Le paquet n'a pas besoin d'être installé. Depuis la racine du projet,
python3 fcx.py <commande> fonctionne tel quel. pyproject.toml est fourni
pour ceux qui préfèrent un pip install -e ., mais reste facultatif.
3. Format de réponse JSON
3.1 L'enveloppe
Toute exécution produit exactement un document JSON sur la sortie standard, succès comme échec. Sa structure ne change jamais :
{
"status": "success",
"command": "extract",
"exit_code": 0,
"messages": ["1523 ligne(s) écrite(s) dans /tmp/comptes.xlsx"],
"data": { },
"error": null
}
| Champ | Type | Toujours présent | Description |
|---|---|---|---|
status |
"success" | "error" |
oui | Vaut "success" si et seulement si exit_code est 0 |
command |
chaîne | null |
oui | Commande exécutée ; null si aucune n'a été fournie |
exit_code |
entier | oui | Identique au code de sortie du processus |
messages |
tableau de chaînes | oui | Informations lisibles par un humain ; peut être vide |
data |
objet | null |
oui | Charge utile, propre à chaque commande |
error |
objet | null |
oui | null en cas de succès |
En cas d'échec, error est renseigné :
{
"type": "ValidationError",
"message": "Le batch B0012 n'a pas été intégré : 2 anomalie(s) détectée(s).",
"problems": [
"Ligne numero 1 : Solde insuffisant sur le compte 0012345678 (solde : 5000.0, débit demandé : 150000.0).",
"Déséquilibre débit/crédit — total débit : 150000.0, total crédit : 100000.0"
]
}
Champ de error |
Description |
|---|---|
type |
Nom de l'exception : ConfigError, OracleError, ValidationError, IntegrationError, UsageError, UnexpectedError |
message |
Message principal, en français |
problems |
Anomalies accumulées ; tableau vide si sans objet |
traceback |
Présent uniquement si --traceback a été passé |
data peut rester renseigné malgré une erreur : flexcube-batch refusé par
l'API expose quand même le fichier produit et la réponse de l'API.
3.2 Quatre garanties pour l'appelant
- La sortie standard ne contient que du JSON. Les commandes livrées n'écrivent jamais directement à l'écran, et c'est vérifié par la suite de tests.
- Aucun journal n'est affiché par défaut — ni sur la sortie standard, ni
sur la sortie d'erreur. Tout est écrit dans le fichier de log. Le décodage
tient donc même si l'appelant fusionne les deux flux avec
2>&1, ce que faitshell_execen PHP.--verboserétablit l'affichage pour le débogage interactif. - Les erreurs d'usage aussi sont en JSON. Commande inconnue, argument
obligatoire manquant, valeur invalide :
argparsea été adapté pour ne jamais produire de texte brut. exit_codeet le code de sortie du processus sont identiques. Vous pouvez vous fier à l'un ou à l'autre indifféremment.
Rien n'est perdu quand la console est muette : les erreurs figurent dans la réponse JSON (
error), les informations de déroulement dansmessages, et le détail complet dans le fichier de log.
3.3 Des réponses compactes
Les noms de colonnes ne sont donnés qu'une seule fois, dans columns.
Chaque ligne est ensuite un simple tableau de valeurs, dans le même ordre :
"columns": ["NO_COMPTE", "SOLDE"],
"rows": [
["0012345678", 150000.0],
["0098765432", 250000.0]
]
Répéter les noms sur chaque ligne alourdirait la réponse sans rien apporter : sur une table de 135 colonnes, cette seule décision réduit le volume de 64 %. Combinée à la sortie compacte par défaut, elle rend les gros extraits raisonnables à transporter.
3.4 Indentation
La sortie est compacte par défaut : une seule ligne, sans indentation ni espaces superflus. C'est la forme attendue par un programme, et la plus légère à transporter.
python3 fcx.py envs
{"status":"success","command":"envs","exit_code":0,"messages":["1 environnement(s) disponible(s)."],"data":{"envs_dir":"/opt/fcx-toolkit/envs","envs":["cofina"],"count":1},"error":null}
--pretty indente la même réponse, pour la lecture pendant une mise au point :
python3 fcx.py envs --pretty
{
"status": "success",
"command": "envs",
"exit_code": 0,
"messages": ["1 environnement(s) disponible(s)."],
"data": {"envs_dir": "/opt/fcx-toolkit/envs", "envs": ["cofina"], "count": 1},
"error": null
}
Les réponses de ce manuel sont montrées indentées pour rester lisibles. Sans
--pretty, elles tiennent chacune sur une seule ligne.
4. Appel depuis PHP
4.1 Recette de base
Utilisez proc_open (ou Symfony\Component\Process) afin de séparer la sortie
standard de la sortie d'erreur : ne mélangez jamais les deux, sous peine de
recevoir des journaux au milieu du JSON.
<?php
/**
* Exécute une commande fcx-toolkit et retourne la réponse décodée.
*
* @throws RuntimeException si la sortie n'est pas du JSON exploitable.
*/
function fcx(array $arguments, array $secrets = []): array
{
$racine = '/opt/fcx-toolkit';
$commande = array_merge([$racine . '/.venv/bin/python', $racine . '/fcx.py'], $arguments);
$descripteurs = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'], // JSON
2 => ['pipe', 'w'], // journaux
];
// Les secrets transitent par l'environnement, jamais par la ligne de commande
// (celle-ci est visible de tous les utilisateurs de la machine via `ps`).
$environnement = array_merge(getenv(), $secrets);
$processus = proc_open($commande, $descripteurs, $tuyaux, $racine, $environnement);
if (!is_resource($processus)) {
throw new RuntimeException("Impossible de lancer fcx-toolkit.");
}
fclose($tuyaux[0]);
$sortie = stream_get_contents($tuyaux[1]);
$journal = stream_get_contents($tuyaux[2]);
fclose($tuyaux[1]);
fclose($tuyaux[2]);
$code = proc_close($processus);
$reponse = json_decode($sortie, true);
if (!is_array($reponse)) {
throw new RuntimeException(
"Réponse illisible de fcx-toolkit (code {$code}) : "
. substr($sortie, 0, 500) . ' | journal : ' . substr($journal, 0, 500)
);
}
return $reponse;
}
4.2 Exploiter la réponse
$reponse = fcx([
'flexcube-batch',
'--env', 'cofina',
'--file', $cheminDuFichier,
'--no-batch', $noBatch,
'--value-date', $dateDeValeur, // JJ/MM/AAAA
'--user-id', $utilisateur,
], [
'COFINA_ORACLE_PASSWORD' => $motDePasse,
'COFINA_APIKEY' => $cleApi,
]);
if ($reponse['status'] === 'success') {
$fichier = $reponse['data']['output_file'];
$piece = $reponse['data']['receipt']['pdf'] ?? null;
return "Batch {$noBatch} intégré.";
}
// Les anomalies métier sont déjà une liste prête à afficher.
switch ($reponse['error']['type']) {
case 'ValidationError':
return view('batch.erreurs', ['anomalies' => $reponse['error']['problems']]);
case 'IntegrationError':
case 'OracleError':
Log::error('fcx-toolkit', $reponse['error']);
return "Service indisponible, réessayez plus tard.";
default:
return $reponse['error']['message'];
}
4.3 Les cinq pièges à éviter
| Piège | Conséquence | Bon réflexe |
|---|---|---|
shell_exec / exec avec 2>&1 |
Sans risque depuis que la console est muette, mais vous perdez les journaux en cas d'incident | Séparer les flux avec proc_open pour pouvoir les lire |
| Concaténer les arguments dans une chaîne | Injection de commande via un nom de fichier | Passer un tableau à proc_open |
| Mettre le mot de passe en argument | Visible dans ps par tous les utilisateurs |
Le passer par l'environnement |
| Ne pas fixer de délai maximal | Une requête lente bloque la requête PHP | Utiliser Symfony\Process avec setTimeout() |
| Se fier au seul code de sortie | On perd le détail des anomalies | Lire error.problems |
4.4 Toujours simuler avant d'intégrer
Ajoutez --dry-run pour exécuter tous les contrôles sans rien envoyer. C'est le
filet de sécurité principal, à proposer dans votre interface avant la
validation définitive :
$controle = fcx([...$argumentsCommuns, '--dry-run']);
if ($controle['status'] !== 'success') {
return view('batch.erreurs', ['anomalies' => $controle['error']['problems']]);
}
// L'utilisateur confirme, puis seulement :
$integration = fcx($argumentsCommuns);
5. Configuration
5.1 Créer un environnement
Un environnement est un fichier INI envs/<nom>.conf. L'option --env <nom>
le sélectionne.
cp envs/exemple.conf.dist envs/cofina.conf
$EDITOR envs/cofina.conf
.venv/bin/python fcx.py envs
{
"status": "success",
"command": "envs",
"exit_code": 0,
"messages": ["1 environnement(s) disponible(s)."],
"data": {"envs_dir": "/chemin/vers/fcx-toolkit/envs", "envs": ["cofina"], "count": 1},
"error": null
}
5.2 Les secrets ne sont jamais écrits dans le fichier
Toute valeur accepte deux formes de substitution, résolues au chargement depuis les variables d'environnement :
| Forme | Comportement |
|---|---|
${VARIABLE} |
Remplacée par la variable. Absente → ConfigError (code 2) |
${VARIABLE:-valeur} |
Remplacée par la variable, ou par valeur si absente |
password = ${COFINA_ORACLE_PASSWORD}
timeout = ${COFINA_TIMEOUT:-60}
export COFINA_ORACLE_PASSWORD='mot-de-passe'
Ne committez jamais un fichier
envs/*.conf. Ils sont déjà exclus par.gitignoreetenvs/.gitignore. Seulenvs/exemple.conf.dist, qui ne contient aucun secret, est versionné.
5.3 Toutes les clés
Section [oracle] — obligatoire
| Clé | Obligatoire | Défaut | Description |
|---|---|---|---|
host |
oui | — | Adresse du serveur Oracle |
port |
oui | — | Port d'écoute (entier), typiquement 1521 |
service_name |
oui | — | Nom du service Oracle |
user |
oui | — | Compte de connexion |
password |
oui | — | Mot de passe — utilisez ${VAR} |
schema |
non | (aucun) | ALTER SESSION SET CURRENT_SCHEMA si renseigné |
thick |
non | false |
true active le mode thick (nécessite l'Instant Client) |
lib_dir |
non | (aucun) | Répertoire de l'Instant Client, si thick = true |
nls_date_format |
non | DD/MM/YYYY |
Format de date de la session |
Section [integration] — obligatoire pour flexcube-batch
| Clé | Obligatoire | Défaut | Description |
|---|---|---|---|
api_url |
oui | — | URL de l'API d'intégration |
apikey |
non | "" |
Clé d'API, envoyée en en-tête apikey |
referer |
non | "" |
En-tête Referer si l'API l'exige |
format |
non | csv |
Format d'envoi par défaut : csv ou json |
timeout |
non | 60 |
Délai d'attente en secondes (entier) |
Section [logging]
| Clé | Obligatoire | Défaut | Description |
|---|---|---|---|
level |
non | INFO |
DEBUG, INFO, WARNING ou ERROR |
dir |
non | ./logs |
Répertoire des fichiers de log horodatés |
Section [queries] — libre
Chaque clé y remplace une requête codée en dur dans un workflow. Utile lorsque les noms de tables ou de colonnes diffèrent sur votre environnement.
| Clé | Utilisée par | Rôle |
|---|---|---|
client_request |
flexcube-batch |
Lit un compte : NO_COMPTE, COMPTE, ACCOUNT_CODE, SOLDE |
batch_check_upload_master |
flexcube-batch |
Compte les occurrences du batch dans detb_upload_master |
batch_check_ac_entries |
flexcube-batch |
Compte les occurrences du batch dans acvw_all_ac_entries |
Les paramètres sont nommés : :customer_account_number pour client_request,
:no_batch pour les deux contrôles de batch.
5.4 Options communes à toutes les commandes
| Option | Description |
|---|---|
--env <nom> |
Obligatoire. Sélectionne envs/<nom>.conf |
--log-level |
Surcharge [logging] level pour cette exécution |
--verbose |
Affiche les journaux sur la sortie d'erreur (débogage interactif) |
--pretty |
Indente le JSON pour la lecture humaine (compact par défaut) |
--traceback |
Affiche la trace Python complète (débogage) |
6. Commande execute
Exécute une ou plusieurs instructions : SELECT, appel de procédure, bloc
PL/SQL anonyme.
6.1 Arguments
| Argument | Obligatoire | Défaut | Description |
|---|---|---|---|
--query |
l'un des deux | — | Instructions à exécuter, séparées par ; |
--file |
l'un des deux | — | Fichier .sql contenant les instructions |
--query et --file s'excluent mutuellement.
Contenu de data :
| Champ | Description |
|---|---|
statement_count |
Nombre d'instructions exécutées |
succeeded |
Nombre d'instructions réussies |
results[] |
Une entrée par instruction |
results[].statement |
L'instruction telle que fournie |
results[].status |
"success" ou "error" |
results[].columns |
Colonnes retournées ; vide hors SELECT |
results[].rows |
Lignes, en tableaux de valeurs alignés sur columns |
results[].row_count |
Nombre de lignes |
results[].error |
Message Oracle, ou null |
6.2 Exemple 1 — un SELECT
python3 fcx.py execute --env cofina --query "select user_id, user_name from sttm_user where rownum <= 2"
{
"status": "success",
"command": "execute",
"exit_code": 0,
"messages": ["1/1 instruction(s) exécutée(s) avec succès."],
"data": {
"statement_count": 1,
"succeeded": 1,
"results": [
{
"statement": "select user_id, user_name from sttm_user where rownum <= 2",
"status": "success",
"columns": ["USER_ID", "USER_NAME"],
"rows": [
["OPS01", "Diallo"],
["OPS02", "Traoré"]
],
"row_count": 2,
"error": null
}
]
},
"error": null
}
Les noms de colonnes ne sont donnés qu'une fois, dans columns ; chaque
ligne est un tableau de valeurs dans le même ordre. Sur une table large, cela
divise le volume de la réponse par près de trois. Pour retrouver un tableau
associatif en PHP, une ligne suffit :
$bloc = $reponse['data']['results'][0];
$lignes = array_map(
fn ($valeurs) => array_combine($bloc['columns'], $valeurs),
$bloc['rows']
);
echo $lignes[0]['USER_NAME']; // Diallo
6.3 Exemple 2 — un appel de procédure
exec et execute sont automatiquement normalisés en bloc PL/SQL
(BEGIN ...; END;), forme attendue par le driver.
python3 fcx.py execute --env cofina --query "exec MON_PKG.MA_PROCEDURE"
Une procédure ne retournant rien, columns et rows sont vides :
{
"status": "success",
"command": "execute",
"exit_code": 0,
"messages": ["1/1 instruction(s) exécutée(s) avec succès."],
"data": {
"statement_count": 1,
"succeeded": 1,
"results": [
{
"statement": "exec MON_PKG.MA_PROCEDURE",
"status": "success",
"columns": [],
"rows": [],
"row_count": 0,
"error": null
}
]
},
"error": null
}
6.4 Exemple 3 — un fichier .sql en échec
python3 fcx.py execute --env cofina --file ./procedures.sql
L'exécution s'arrête à la première instruction fautive. Les instructions déjà
passées restent visibles dans results, et le code de sortie est 3 :
{
"status": "error",
"command": "execute",
"exit_code": 3,
"messages": [
"1/2 instruction(s) exécutée(s) avec succès.",
"Arrêt sur l'instruction : exec MON_PKG.ETAPE_2"
],
"data": {
"statement_count": 2,
"succeeded": 1,
"results": [
{"statement": "exec MON_PKG.ETAPE_1", "status": "success", "columns": [], "rows": [], "row_count": 0, "error": null},
{"statement": "exec MON_PKG.ETAPE_2", "status": "error", "columns": [], "rows": [], "row_count": 0, "error": "ORA-00942: table or view does not exist"}
]
},
"error": {"type": "CommandError", "message": "La commande 'execute' a échoué.", "problems": []}
}
6.5 Règles de découpage
- Les instructions sont séparées par
;. - Un texte contenant
BEGINouDECLAREest traité comme un bloc unique : ses points-virgules internes ne le découpent pas. - L'exécution s'arrête à la première instruction en échec, rapportée avec son
message Oracle. Le code de sortie passe alors à
3.
7. Commande extract
Exécute un SELECT et écrit le résultat dans un fichier.
7.1 Arguments
| Argument | Obligatoire | Défaut | Description |
|---|---|---|---|
--query |
l'un des deux | — | Requête SELECT |
--file |
l'un des deux | — | Fichier .sql contenant la requête |
--out |
non | (dans la réponse) | Fichier de sortie ; l'extension choisit le format |
--format |
non | (extension) | Force csv, excel ou json |
--delimiter |
non | ; |
Séparateur du CSV produit |
Contenu de data :
| Champ | Sans --out |
Avec --out |
|---|---|---|
columns |
Les colonnes | Les colonnes |
rows |
Les lignes, en tableaux de valeurs alignés sur columns |
null — elles sont dans le fichier |
row_count |
Nombre de lignes | Nombre de lignes écrites |
file |
null |
Chemin absolu du fichier produit |
format |
absent | csv, excel ou json |
Sans --out, les lignes reviennent dans la réponse JSON : pratique pour un
appelant PHP qui n'a pas besoin d'un fichier intermédiaire. Avec --out, seul
le chemin est renvoyé — le bon choix pour de gros volumes.
7.2 Exemples
# CSV
python3 fcx.py extract --env cofina --query "select * from sttm_cust_account" --out ./comptes.csv
# Excel
python3 fcx.py extract --env cofina --query "select * from sttm_cust_account" --out ./comptes.xlsx
# JSON
python3 fcx.py extract --env cofina --query "select * from sttm_cust_account" --out ./comptes.json
# Extension inhabituelle : forcer le format
python3 fcx.py extract --env cofina --query "select 1 from dual" --out ./res.dat --format json
# Sans fichier : les lignes reviennent dans la réponse
python3 fcx.py extract --env cofina --query "select 1 from dual"
Réponse avec --out :
{
"status": "success",
"command": "extract",
"exit_code": 0,
"messages": ["1523 ligne(s) écrite(s) dans /chemin/comptes.xlsx"],
"data": {
"columns": ["CUST_AC_NO", "AC_DESC"],
"rows": null,
"row_count": 1523,
"file": "/chemin/comptes.xlsx",
"format": "excel"
},
"error": null
}
Réponse sans --out :
{
"status": "success",
"command": "extract",
"exit_code": 0,
"messages": ["2 ligne(s) extraite(s)."],
"data": {
"columns": ["NO_COMPTE", "SOLDE"],
"rows": [
["0012345678", 150000.0],
["0098765432", 250000.0]
],
"row_count": 2,
"file": null
},
"error": null
}
7.3 Correspondance extension / format
| Extension | Format |
|---|---|
.csv, .txt |
CSV |
.xlsx, .xls |
Excel |
.json |
JSON |
Toute autre extension exige --format, faute de quoi une ValidationError
(code 4) est levée.
8. Commande flexcube-batch
Intègre un batch d'écritures comptables, après avoir passé tous les contrôles métier.
8.1 Arguments
| Argument | Obligatoire | Défaut | Description |
|---|---|---|---|
--file |
oui | — | Fichier d'entrée CSV ou Excel |
--no-batch |
oui | — | Numéro de batch ; doit être inédit |
--value-date |
oui | — | Date de valeur au format JJ/MM/AAAA |
--user-id |
oui | — | Identifiant utilisateur Flexcube |
--format |
non | (config) | csv ou json |
--delimiter |
non | ; |
Séparateur du CSV d'entrée |
--sheet |
non | 1ʳᵉ feuille | Nom ou index de la feuille Excel |
--encoding |
non | (détecté) | Force l'encodage du CSV d'entrée |
--out-dir |
non | ./out |
Répertoire des fichiers produits |
--dry-run |
non | non | Exécute tous les contrôles sans rien envoyer |
--no-receipt |
non | non | N'engendre pas la pièce comptable |
8.2 Colonnes du fichier d'entrée
| Colonne | Obligatoire | Description |
|---|---|---|
numero |
oui | Identifiant de la ligne, repris dans les rapports |
no_compte |
oui | Numéro de compte Flexcube |
montant |
oui | Montant strictement positif (, ou . acceptés) |
code_agence |
oui | Code de l'agence |
sens |
oui | D (débit) ou C (crédit) |
code_operation |
oui | Code opération Flexcube |
libelle_ecriture |
oui | Libellé de l'écriture |
related_account |
non | Compte lié ; à défaut, no_compte est utilisé |
Exemple fourni : examples/exemple-batch.csv.
numero;no_compte;montant;code_agence;sens;code_operation;libelle_ecriture;related_account
1;0012345678;150000;001;D;OP001;Frais de dossier;0012345678
2;0098765432;150000;001;C;OP001;Frais de dossier;0012345678
Le libellé transmis conserve le format historique attendu par Flexcube :
RELATED_ACCOUNT_<related_account>__<libelle_ecriture>.
8.3 Enchaînement et contrôles
| # | Contrôle | Message en cas d'échec |
|---|---|---|
| 1 | Format de --value-date |
Date de valeur invalide : ... Format attendu : JJ/MM/AAAA. |
| 2 | Colonnes obligatoires présentes | Colonnes obligatoires absentes du fichier ... : montant, sens |
| 3 | montant lisible et positif |
Ligne 4 (numero 3) : montant illisible : abc |
| 4 | sens vaut D ou C |
Ligne 4 (numero 3) : sens invalide : X (attendu D ou C) |
| 5 | no_compte renseigné |
Ligne 4 (numero 3) : no_compte est vide |
| 6 | Batch inédit dans detb_upload_master et acvw_all_ac_entries |
Le batch B0012 est déjà utilisé (3 occurrence(s) dans ...). Intégration annulée. |
| 7 | Compte existant | Ligne numero 2 : Le compte 0098765432 n'existe pas. |
| 8 | Solde suffisant au débit (classes 251–254) |
Ligne numero 1 : Solde insuffisant sur le compte ... (solde : 500.0, débit demandé : 150000.0). |
| 9 | Équilibre débit / crédit | Déséquilibre débit/crédit — total débit : 150000.0, total crédit : 100000.0 |
Deux principes gouvernent ces contrôles :
- Les anomalies des étapes 3 à 9 sont accumulées, pas levées une à une. Le rapport final liste toutes les lignes fautives en une seule fois.
- Un contrôle de batch qui échoue techniquement annule l'intégration. Si la
requête sur
detb_upload_mastertombe en erreur, on n'intègre pas un batch dont l'état n'a pas pu être confirmé.
8.4 Exemple avec --dry-run
Commencez toujours par une simulation :
python3 fcx.py flexcube-batch --env cofina --file ./examples/exemple-batch.csv \
--no-batch B0012 --value-date 15/07/2026 --user-id OPS01 --dry-run
{
"status": "success",
"command": "flexcube-batch",
"exit_code": 0,
"messages": [
"Contrôles réussis — 2 écriture(s), total débit 150000.0, total crédit 150000.0.",
"Mode simulation (--dry-run) : aucun envoi effectué."
],
"data": {
"no_batch": "B0012",
"value_date": "15/07/2026",
"user_id": "OPS01",
"entry_count": 2,
"total_debit": 150000.0,
"total_credit": 150000.0,
"dry_run": true,
"integrated": false,
"output_file": null,
"integration": null,
"receipt": null
},
"error": null
}
Les lignes INFO de progression partent sur la sortie d'erreur : elles ne
polluent pas le JSON.
En cas d'anomalies, toutes sont listées d'un coup dans error.problems :
{
"status": "error",
"command": "flexcube-batch",
"exit_code": 4,
"messages": [],
"data": null,
"error": {
"type": "ValidationError",
"message": "Le batch B0012 n'a pas été intégré : 2 anomalie(s) détectée(s).",
"problems": [
"Ligne numero 1 : Solde insuffisant sur le compte 0012345678 (solde : 5000.0, débit demandé : 150000.0).",
"Déséquilibre débit/crédit — total débit : 150000.0, total crédit : 100000.0"
]
}
}
Le code de sortie est alors 4, et rien n'a été envoyé. error.problems
est directement affichable dans votre interface.
8.5 Intégration réelle
Retirez --dry-run une fois la simulation propre :
python3 fcx.py flexcube-batch --env cofina --file ./examples/exemple-batch.csv \
--no-batch B0012 --value-date 15/07/2026 --user-id OPS01
{
"status": "success",
"command": "flexcube-batch",
"exit_code": 0,
"messages": [
"Contrôles réussis — 2 écriture(s), total débit 150000.0, total crédit 150000.0.",
"Fichier généré : /chemin/out/batch-B0012/integration-B0012.csv",
"Batch B0012 intégré avec succès.",
"Pièce comptable : /chemin/out/batch-B0012/piece-comptable-B0012.docx",
"Version PDF : /chemin/out/batch-B0012/piece-comptable-B0012.pdf"
],
"data": {
"no_batch": "B0012",
"value_date": "15/07/2026",
"user_id": "OPS01",
"entry_count": 2,
"total_debit": 150000.0,
"total_credit": 150000.0,
"dry_run": false,
"integrated": true,
"output_file": "/chemin/out/batch-B0012/integration-B0012.csv",
"integration": {"ok": true, "status_code": 201, "payload": {"message": "ok"}},
"receipt": {
"docx": "/chemin/out/batch-B0012/piece-comptable-B0012.docx",
"pdf": "/chemin/out/batch-B0012/piece-comptable-B0012.pdf"
}
},
"error": null
}
Contenu de data :
| Champ | Description |
|---|---|
entry_count |
Nombre d'écritures du lot |
total_debit / total_credit |
Totaux contrôlés, égaux par construction |
dry_run |
true si aucun envoi n'a été tenté |
integrated |
true seulement si l'API a accepté le lot |
output_file |
Chemin du CSV transmis, ou null en simulation |
integration |
Verdict de l'API : ok, status_code, payload |
receipt |
Chemins docx et pdf, chacun pouvant être null |
Si l'API refuse le lot, le code de sortie est 5 et data reste
renseigné : vous conservez le fichier produit et la réponse exacte de l'API
pour diagnostic.
{
"status": "error",
"command": "flexcube-batch",
"exit_code": 5,
"messages": [
"Contrôles réussis — 2 écriture(s), total débit 150000.0, total crédit 150000.0.",
"Fichier généré : /chemin/out/batch-B0012/integration-B0012.csv",
"Intégration refusée par l'API (HTTP 422)."
],
"data": {
"integrated": false,
"output_file": "/chemin/out/batch-B0012/integration-B0012.csv",
"integration": {"ok": false, "status_code": 422, "payload": {"erreur": "batch invalide"}},
"receipt": null
},
"error": {"type": "CommandError", "message": "La commande 'flexcube-batch' a échoué.", "problems": []}
}
Si LibreOffice est absent, receipt.pdf vaut null : l'intégration reste un
succès, seule la conversion est ignorée.
9. Ajouter un nouveau workflow
9.1 Le contrat
Déposez un fichier .py dans workflows/. Il est découvert automatiquement
s'il définit :
| Élément | Obligatoire | Rôle |
|---|---|---|
NAME |
oui | Nom de la sous-commande (chaîne non vide) |
HELP |
recommandé | Aide courte affichée dans --help |
arguments(parser) |
non | Ajoute les options propres à la commande |
run(ctx, args) -> int |
oui | Traitement ; retourne le code de sortie |
Règles de découverte :
- les fichiers commençant par
_sont ignorés ; - un module invalide (attribut manquant, import cassé) produit un avertissement et est ignoré — il n'empêche jamais les autres commandes de fonctionner ;
--env,--log-level,--verbose,--prettyet--tracebacksont ajoutés automatiquement : ne les redéclarez pas.
9.2 Ce qu'offre ctx
| Attribut | Type | Remarque |
|---|---|---|
ctx.env |
EnvConfig |
ctx.env.query(cle, defaut), ctx.env.section(nom) |
ctx.logger |
logging.Logger |
Écrit sur la console et dans le fichier de log |
ctx.oracle |
OracleClient |
Connexion ouverte à la première utilisation |
ctx.integration |
IntegrationClient |
Créé à la demande ; exige [integration] |
Méthodes utiles de ctx.oracle :
| Méthode | Retour |
|---|---|
select(sql, params) |
(colonnes, lignes) — valeurs déjà sérialisées |
select_df(sql, params) |
pandas.DataFrame |
scalar(sql, params) |
Première colonne de la première ligne, ou None |
execute_script(texte) |
list[StatementResult] |
Une commande qui n'utilise pas Oracle n'ouvre aucune connexion.
9.3 Ce que run() doit retourner
N'écrivez jamais sur la sortie standard : elle est réservée à l'enveloppe
JSON. Un print() dans un workflow casserait le json_decode de l'appelant.
Retournez un CommandResult — le CLI se charge de la sérialisation.
from fcx.output import CommandResult
return CommandResult(
data={"total": 42}, # charge utile exploitable
messages=["42 ligne(s) trouvée(s)"], # informations lisibles
exit_code=0, # 0 = succès
)
Trois autres formes restent acceptées, par commodité :
Retour de run() |
Interprétation |
|---|---|
CommandResult(...) |
Forme recommandée |
dict |
Devient data, code de sortie 0 |
int |
Code de sortie seul, data vide |
None |
Succès, data vide |
Pour les informations de progression, utilisez ctx.logger : elles partent sur
la sortie d'erreur, où elles ne gênent personne.
9.4 Exemple complet et fonctionnel
Enregistrez ceci sous workflows/compter.py — il fonctionne tel quel :
"""Exemple : compte les lignes d'une table."""
import sys
from pathlib import Path
RACINE = Path(__file__).resolve().parent.parent
if str(RACINE) not in sys.path:
sys.path.insert(0, str(RACINE))
from fcx.output import CommandResult # noqa: E402
NAME = "compter"
HELP = "Compte les lignes d'une table"
def arguments(parser):
parser.add_argument("--table", required=True, help="Nom de la table")
def run(ctx, args):
ctx.logger.info("Comptage de %s", args.table) # → sortie d'erreur
total = ctx.oracle.scalar(f"SELECT COUNT(*) FROM {args.table}")
return CommandResult(
data={"table": args.table, "count": total},
messages=[f"{args.table} : {total} ligne(s)"],
)
Les trois lignes de sys.path permettent d'importer fcx.* depuis
workflows/, quel que soit le répertoire de lancement.
Utilisation immédiate, sans rien recompiler ni réinstaller :
python3 fcx.py commands # « compter » y figure
python3 fcx.py compter --env cofina --table sttm_user
{
"status": "success",
"command": "compter",
"exit_code": 0,
"messages": ["sttm_user : 128 ligne(s)"],
"data": {"table": "sttm_user", "count": 128},
"error": null
}
9.5 Bonnes pratiques
- N'écrivez jamais sur la sortie standard. Utilisez
messagespour l'humain,datapour la machine,ctx.loggerpour la progression. - Levez les exceptions de
fcx.errorsplutôt que de gérer vous-même l'affichage : le CLI les met en forme et choisit le bon code de sortie. - Accumulez les anomalies dans une liste et levez une seule
ValidationError(message, problems=[...])à la fin : l'appelant les retrouve danserror.problems. - Rendez vos requêtes surchargeables :
ctx.env.query("ma_requete", REQUETE_PAR_DEFAUT). - Gardez les règles métier pures, sans I/O, comme dans
fcx/flexcube.py: elles deviennent testables sans base. - Prévoyez un
--dry-runpour tout traitement qui écrit ou envoie.
9.6 Tester son workflow
Les tests n'ont besoin ni de base ni de réseau : remplacez ctx par un double.
import argparse
import importlib.util
import logging
from pathlib import Path
def charger():
chemin = Path(__file__).resolve().parent.parent / "workflows" / "compter.py"
spec = importlib.util.spec_from_file_location("wf_compter", chemin)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class FauxContexte:
logger = logging.getLogger("test")
class oracle:
@staticmethod
def scalar(sql, params=None):
return 42
def test_compter(capsys):
wf = charger()
resultat = wf.run(FauxContexte(), argparse.Namespace(table="sttm_user"))
assert resultat.exit_code == 0
assert resultat.data == {"table": "sttm_user", "count": 42}
# Garde-fou : rien ne doit fuir sur la sortie standard.
assert capsys.readouterr().out == ""
.venv/bin/pytest tests/test_compter.py -v
10. Codes de sortie
| Exception | Code | Cas typique |
|---|---|---|
| — | 0 |
Succès |
FcxError |
1 |
Autre erreur maîtrisée |
ConfigError |
2 |
Environnement introuvable, clé manquante, ${VAR} non résolue |
OracleError |
3 |
Connexion ou exécution SQL en échec |
ValidationError |
4 |
Fichier d'entrée invalide, contrôles métier en échec |
IntegrationError |
5 |
API d'intégration en erreur ou injoignable |
| — | 130 |
Interruption clavier (Ctrl+C) |
Ces codes permettent à un ordonnanceur (cron, CI) de distinguer une erreur de configuration d'une erreur métier sans même décoder le JSON :
python3 fcx.py flexcube-batch --env cofina --file ./b.csv \
--no-batch B1 --value-date 15/07/2026 --user-id OPS01
case $? in
0) echo "intégré" ;;
4) echo "à corriger dans le fichier" ;;
5) echo "API en erreur — réessayer" ;;
esac
exit_code dans la réponse JSON et le code de sortie du processus sont
toujours identiques : utilisez celui qui vous arrange. La trace Python n'est
jamais incluse par défaut ; --traceback l'ajoute dans error.traceback pour
le débogage.
11. Dépannage
11.1 Environnement introuvable
{
"status": "error",
"command": "execute",
"exit_code": 2,
"messages": [],
"data": null,
"error": {
"type": "ConfigError",
"message": "Environnement 'prod' introuvable (/chemin/envs/prod.conf).\nEnvironnements disponibles : cofina, recette",
"problems": []
}
}
Correction — utilisez un nom listé, ou créez le fichier :
python3 fcx.py envs
cp envs/exemple.conf.dist envs/prod.conf
11.2 Variable d'environnement manquante
{
"error": {
"type": "ConfigError",
"message": "La variable d'environnement 'COFINA_ORACLE_PASSWORD' est requise par la configuration mais n'est pas définie.",
"problems": []
}
}
Correction — exportez-la avant de lancer la commande :
export COFINA_ORACLE_PASSWORD='mot-de-passe'
Pour une valeur non sensible, donnez-lui un défaut dans le .conf :
timeout = ${COFINA_TIMEOUT:-60}.
11.3 DPY-6005 — connexion refusée
{
"exit_code": 3,
"error": {
"type": "OracleError",
"message": "Connexion à Oracle impossible : DPY-6005: cannot connect to database (CONNECTION_ID=...). [Errno 111] Connection refused",
"problems": []
}
}
Vérifications, dans l'ordre :
host,portetservice_namedansenvs/<nom>.conf;- joignabilité du serveur :
nc -zv <host> <port>; - VPN ou pare-feu éventuel ;
service_nameet non SID — un SID exige une autre forme de DSN.
11.4 DPY-3010 / fonctionnalité non gérée en mode thin
Certaines fonctionnalités Oracle ne sont pas disponibles en mode thin. Si vous en avez besoin, installez l'Instant Client et activez le mode thick :
[oracle]
thick = true
lib_dir = /opt/oracle/instantclient_21_13
11.5 ORA-00942 — table ou vue inexistante
{
"exit_code": 3,
"error": {
"type": "OracleError",
"message": "Échec de la requête : ORA-00942: table or view does not exist\nSQL : SELECT COUNT(*) AS NB FROM detb_upload_master WHERE batch_no = :no_batch",
"problems": []
}
}
Causes possibles :
schemaabsent ou erroné dans[oracle]— la table existe mais dans un autre schéma ;- le compte n'a pas les droits de lecture ;
- le nom de table diffère sur cet environnement — surchargez la requête :
[queries]
batch_check_upload_master = SELECT COUNT(*) AS NB FROM MON_SCHEMA.detb_upload_master WHERE batch_no = :no_batch
11.6 Colonnes manquantes dans le fichier d'entrée
{
"exit_code": 4,
"error": {
"type": "ValidationError",
"message": "Colonnes obligatoires absentes du fichier ./batch.csv : montant, sens.\nColonnes trouvées : numero, no_compte, montnat, code_agence, sen",
"problems": []
}
}
Correction — la ligne « Colonnes trouvées » révèle en général une faute de
frappe (montnat) ou un mauvais séparateur. Si votre CSV utilise la virgule :
python3 fcx.py flexcube-batch --env cofina --file ./batch.csv --delimiter , ...
11.7 Problème d'encodage du CSV
Les encodages utf-8-sig, utf-8 puis iso-8859-1 sont essayés dans cet
ordre. Si les accents restent incorrects, forcez-le :
python3 fcx.py flexcube-batch --env cofina --file ./batch.csv --encoding cp1252 ...
11.8 LibreOffice absent — pas de PDF
WARNING Conversion PDF impossible : LibreOffice est introuvable (commande 'soffice').
Ce n'est pas une erreur : le .docx est produit et l'intégration reste un
succès. Pour obtenir le PDF :
sudo apt install libreoffice-writer # Debian / Ubuntu
11.9 Un workflow n'apparaît pas dans --help
Vérifiez, dans l'ordre :
- le fichier est bien dans
workflows/avec l'extension.py; - son nom ne commence pas par
_; - il définit
NAME(chaîne non vide) etrun; - il s'importe sans erreur.
L'avertissement en donne la raison exacte :
python3 fcx.py commands --pretty # la commande y figure-t-elle ?
python3 fcx.py execute --env cofina --log-level DEBUG --query "select 1 from dual"
L'avertissement paraît sur la sortie d'erreur :
WARNING Workflow ignoré (mon_wf.py) : No module named 'pandas_inexistant'
11.10 Le batch est refusé alors que le fichier semble correct
{
"exit_code": 4,
"error": {
"type": "ValidationError",
"message": "Le batch B0012 est déjà utilisé (3 occurrence(s) dans detb_upload_master). Intégration annulée.",
"problems": []
}
}
Le numéro de batch a déjà servi. Utilisez un --no-batch inédit — jamais le
même deux fois, sous peine de doublons comptables.
11.11 Où sont les journaux ?
Dans le répertoire [logging] dir (par défaut ./logs), un fichier horodaté
par exécution : fcx_20260720_093015.log.
Rien ne s'affiche à l'écran par défaut, c'est voulu : un seul message de journal suffirait à rendre la réponse indécodable pour un appelant qui fusionne les flux. Pour suivre l'exécution en direct pendant une mise au point :
python3 fcx.py execute --env cofina --query "..." --verbose --log-level DEBUG
Les journaux partent alors sur la sortie d'erreur. Pour les lire sans perturber le JSON :
python3 fcx.py execute --env cofina --query "..." --verbose 2>debogage.log