fcx-toolkit — Manuel d'utilisation

Version 1.0.0 — 2026-07-20

Table des matières

  1. Introduction
  2. Installation
  3. Format de réponse JSON
  4. Appel depuis PHP
  5. Configuration
  6. Commande execute
  7. Commande extract
  8. Commande flexcube-batch
  9. Ajouter un nouveau workflow
  10. Codes de sortie
  11. Dépannage

1. Introduction

fcx-toolkit est un outil en ligne de commande autonome qui permet de :

  1. exécuter du SQL ou du PL/SQL sur une base Oracle Flexcube ;
  2. exporter le résultat d'une requête en CSV, Excel ou JSON ;
  3. intégrer un batch d'écritures comptables depuis un fichier CSV ou Excel, en appliquant les contrôles métier Flexcube avant tout envoi ;
  4. 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.

--help reste la seule sortie en texte, car elle est destinée à un humain. Un appelant automatisé utilise commands, 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

  1. 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.
  2. 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 fait shell_exec en PHP. --verbose rétablit l'affichage pour le débogage interactif.
  3. Les erreurs d'usage aussi sont en JSON. Commande inconnue, argument obligatoire manquant, valeur invalide : argparse a été adapté pour ne jamais produire de texte brut.
  4. exit_code et 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 dans messages, 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 .gitignore et envs/.gitignore. Seul envs/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


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 251254) 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 :

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 :

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

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 :

  1. host, port et service_name dans envs/<nom>.conf ;
  2. joignabilité du serveur : nc -zv <host> <port> ;
  3. VPN ou pare-feu éventuel ;
  4. service_name et 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 :

  1. schema absent ou erroné dans [oracle] — la table existe mais dans un autre schéma ;
  2. le compte n'a pas les droits de lecture ;
  3. 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 :

  1. le fichier est bien dans workflows/ avec l'extension .py ;
  2. son nom ne commence pas par _ ;
  3. il définit NAME (chaîne non vide) et run ;
  4. 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