---
suivi: 1758
date: 2026-09-30
sujet: Diagnostic — chèque marqué payé alors que Sage est Approved et à 0 €
chantier: factures / paiement / sage
type: diagnostic
statut: poussé
hash:
fichiers:
  - docs/suivi/SUIVI_1758_diag_facture_cheque_ecart_sage.md
---

# SUIVI #1758 — diagnostic facture chèque / écart Sage

Lecture seule. Aucun fichier applicatif modifié. Aucune écriture ERP, aucune écriture Sage.
Base MySQL locale refusée (`SQLSTATE[HY000] [2002] 127.0.0.1:3306`, base `erp_immo`).
SSH OVH refusé (`Permission denied (publickey,password)`).
Les lignes `facture_historique` et `storage/logs/intacct.log` de cette facture n’ont donc pas été lues.
Rien n’est inventé à leur place. Le code cité est celui de `4f57974e` (#1754), présent sur `origin/main` au moment de l’écriture, au-dessus de `e2dd69d0` (#1753).
#1754 renomme la première étape (« Facture », « Déposée »). Pour une facture qui n’est pas « déjà réglée », la branche `payee` et les étapes de paiement restent celles décrites ici.

## PROMPT ENVOYÉ

PHASE 1. Facture 2604901-973983, ELIS LANGUEDOC ROUSSILLON, HECTARE HOLDING, 2 366,92 € TTC, mode CHÈQUE, statut « Paiement confirmé », sélecteur « À régler » (override admin). Sage : APBILL 2389236 STATE Selected, TOTALPAID 0, TOTALDUE 2366.92 ; APPYMT 2389306 STATE A, STATUS T, DISPLAYSTATE Approved, TOTALPAID 0. Verdict relecture : silence. Pourquoi « Déposé sur Suite » par Sage, pourquoi « Virement exécuté » sans date, quelle action a posé « Paiement confirmé » par Rudy BENOR, comment l’ERP traite un chèque, requête lecture seule. Proposer une frise chèque et un badge d’écart, sans coder.

## FICHES LUES

- **#1671** — la frise peint toutes les étapes de paiement en passé dès que le statut est payé ; « par Sage (automatique) » sur « Déposé sur Suite » est un libellé fixe, pas une lecture Delivered ; « Virement exécuté » sans date = `virement_execute_at` vide. Même mécanisme ici, déclenché par le chèque et non par un forçage.
- **#1570** — `Approved` n’est pas un signal de paiement ; `Confirmed` / `Delivered` le sont. Un payé ne recule pas.
- **#1691 / #1693** — un silence Sage n’annule rien. « Constater le non-paiement » refuse une facture déjà payée.
- **#1754** (`4f57974e`, déjà sur `origin/main`) — renomme la première étape. La branche `payee` standard, celle de cette facture, reste : acteur « Sage » seulement si `intacct_when_paid` est rempli.

## 1. « Déposé sur Suite » par Sage, alors que Sage est Approved

Ce n’est pas une lecture `Delivered`.

`IntacctPaiementStatusService::mapperDepuisDisplayState` (`app/Services/IntacctPaiementStatusService.php`) :

- `delivered` → `paiement_disponible` (ou `virement_execute` si un virement a été marqué à la main)
- `confirmed` → `paye`
- `approved` → `null`

`Approved` ne fait pas avancer `etat_paiement_sage`. Le repli `STATE` ne mappe que `P` et `C`. `STATE A` (Approved, doc API ci-dessous) tombe aussi sur `null`. Le verdict de relecture est alors `silence` : « Sage n'a rien dit de concluant. Un silence n'est pas une annulation : le statut de paiement n'a pas été reculé. » (`FacturePaiementStatutSyncService::relectureDemandee`). La lecture brute du panneau super-admin ne écrit rien (`FactureLectureSageBruteService`, `applique: false`).

L’étape devient verte pour une autre raison. Dès que `factures.statut` normalisé vaut `payee`, `FactureFriseEtapePresenter::buildWorkflowComputation` (`:182`) peint **chaque** étape en `past`, avant même le calcul des étapes de paiement. Le libellé passé de la clé `paiement_disponible` est figé : « Déposé sur Suite » (`EtatPaiementSage::libelleFrise`).

La mention « par Sage (automatique) » est aussi figée, sur `4f57974e` :

```
FactureController.php:1781-1784
'paiement_disponible' => [
    'date_label' => format(intacct_appymt_when_created ?? intacct_paiement_synced_at),
    'actor_label' => 'Sage',
    'actor_automatique' => true,
]
```

`Show.vue:1032-1037` assemble « {date} par {acteur} (automatique) » dès que la date et l’acteur sont tous les deux présents. L’acteur ne dépend pas du `DISPLAYSTATE` lu.

La date `30/09` ne peut venir que de l’une de ces deux colonnes.

- `intacct_appymt_when_created` n’est écrit que si l’état calculé est `paiement_disponible` (`FacturePaiementStatutSyncService::appliquerEtat`, vers `:746-749`). `Approved` ne produit pas cet état. Cette colonne n’est donc pas alimentée par la lecture du 30/09 10:38.
- `intacct_paiement_synced_at` est écrit même quand Sage ne dit rien (`appliquerEtat` si l’état calculé est `null` : seule cette date bouge, décision journalière `conserve_etat_synced_at_seulement`). L’ouverture de la fiche le fait si la facture est encore éligible (`FactureController.php:699`). Le chemin chèque met `statut = payee` **sans** mettre `etat_paiement_sage = paye` : l’exclusion « déjà payé et état payé » ne s’applique pas, la fiche reste éligible.

Lignes que le code écrit dans le canal `intacct` (`storage/logs/intacct.log`), non lues ici :

- au passage payé du chèque : `Workflow synchronisé après compta (payée)` avec `facture_id` (`FactureWorkflowService.php:84`)
- à la lecture Approved : `Diag #213b lecture statut bill Sage` avec `appymt_displaystate_lu` et `etat_erp_mappe` null (`IntacctPaiementStatusService.php:431`)
- puis `Diag #213b appliquerEtat — conserve (Sage sans signal P/Paid)` / `decision = conserve_etat_synced_at_seulement`
- si le bouton « Actualiser statut Sage » a été utilisé : `Évaluation preuve de non-paiement Sage` avec `verdict = silence`

`facture_historique` n’a **pas** d’action `sync_statut_paiement_sage` dans ce cas : cette ligne n’est insérée que quand l’état change (`:798-809`). Un silence n’en crée pas.

La valeur lue, celle fournie dans le prompt (30/09 10:38, entité E-05) : APBILL `2389236` `STATE Selected`, `TOTALPAID 0`, `TOTALDUE 2366.92` ; APPYMT `2389306` `STATE A`, `STATUS T`, `DISPLAYSTATE Approved`, `TOTALPAID 0`, `WHENMODIFIED 2026-09-30T10:30:09Z`. `Approved` → état ERP `null`. `STATUS T` n’est pas défini dans la page développeur AP Payments lue pour ce diagnostic.

## 2. « Virement exécuté » sans date, pour un chèque

Même peinture. La clé `virement_execute` est dans la chaîne unique de toute facture qui n’est pas « déjà réglée » (`FactureFriseEtapePresenter::etapesPaiement` : `preparation_paiement`, `paiement_disponible`, `virement_execute`). Le mode chèque ne retire pas cette étape.

Le libellé passé est « Virement exécuté ». La date affichée est **uniquement** `factures.virement_execute_at` (`FactureController.php:1787-1788`, bloc `virement_execute`). Le chemin chèque n’écrit pas cette colonne, ni `virement_execute_par_id`. L’étape est verte à cause du statut payé, sans date et sans auteur.

Le bouton « Virement exécuté » n’est proposé que si `etat_paiement_sage = paiement_disponible` (`FactureController.php:823`). `Approved` ne pose pas cet état. Rien n’indique que ce bouton a été utilisé.

## 3. « Paiement confirmé » par Rudy BENOR le 30/09, Sage à 0 €

L’auteur « Rudy BENOR » exclut le bouton fiche `POST /factures/{facture}/confirmer-paiement-effectif`.

Ce bouton est de toute façon coupé : `can_confirmer_paiement_effectif` est écrit `false` en dur (`FactureController.php:822`). S’il avait tourné, il poserait `intacct_when_paid`, et la frise afficherait alors « Sage (automatique) », pas le nom du valideur (`:1805-1806`). L’affichage « par Rudy BENOR » sans « (automatique) » implique `intacct_when_paid` vide, et `workflow_validations` de niveau `paiement_effectif` en `valide`, `validateur_id` = cet utilisateur, `date_validation` = le 30/09.

C’est le chemin chèque, pas une confirmation Sage.

`PaiementModeReglementMapper` envoie un chèque en `PAYMENTMETHOD` `Printed Check` (`IntacctService::APPYMT_PAYMENT_METHOD_PRINTED_CHECK`). Ce n’est pas `ACH`. Après un `create` APPYMT réussi et une ligne `paiements_intacct`, `PaiementAppymtExecutionService::mettreAJourStatutFactureApresPaiement` (`:144-160`) :

- ne lit ni `DISPLAYSTATE`, ni `TOTALPAID`, ni l’état du bill ;
- met `factures.statut = payee` ;
- **ne** met **pas** `etat_paiement_sage` ni `intacct_when_paid` ;
- appelle `synchroniserWorkflowFacturePayeeApresCompta`, qui clôt `paiement_effectif` avec l’id de l’utilisateur qui a cliqué (`cloturerPaiementEffectif`, `date_validation = now()`).

Deux boutons arrivent là, dès que l’envoi Sage a répondu :

- « Payer la sélection » — `POST /factures/mise-en-paiement/traiter`
- étape 2 du popup « Comptabiliser et payer » — `POST /factures/comptabilisation/{uuid}/confirmer-paiement`

Aucun des deux n’écrit `facture_historique`. Le journal `intacct` reçoit seulement `Workflow synchronisé après compta (payée)`.

Le sélecteur « À régler » n’est pas cette action. C’est `depot_factures.deja_reglee = false` (`Show.vue` `REGLEMENT_STATUT_OPTIONS`, valeur `false`). Il reste visible en override admin parce que la facture est déjà `payee` : hors de la fenêtre « après comptabilisation et avant paiement », `GS_ADMIN_ERP` peut encore le modifier (`DepotReglementStatutService::evaluerModification`). La valeur « À régler » confirme que le paiement n’a pas été saisi comme déjà fait hors Sage (ce chemin-là enverrait un virement `EFT`, pas un chèque).

Conditions qui ont laissé l’ERP « payé » avec Sage à 0 € : mode non ACH + APPYMT créé + clôture immédiate du workflow. La relecture `Approved` / `TOTALPAID 0` / bill `Selected` calcule `null` et ne recule pas (`appliquerEtat`, garde « déjà payé » et garde « pas de signal »). « Constater le non-paiement » refuse aussi : `Une facture payée ne peut pas revenir en arrière.` (`FactureConstatNonPaiementService.php:35-36`, et `FactureController.php:648`).

« Comptabilisée 30/09 (R. BENOR) » est un autre événement : la comptabilisation pose `statut = comptabilisee`, valide le niveau `comptabilisation` avec l’utilisateur, et **ouvre** `paiement_effectif` en attente (`synchroniserWorkflowFactureComptabiliseeApresCompta`). L’historique dépôt correspondant est `intacct_tentative`, `intacct_envoyee`, `envoye_intacct_comptabilisee`, `intacct_comptabilisee` (« Comptabilisée dans Sage Intacct (RECORDNO …) »). « Paiement préparé 30/09 (ERP automatique) » est la `date_assignation` de ce `paiement_effectif`, acteur « ERP » en dur (`FactureController.php` bloc `preparation_paiement`).

## 4. Circuit chèque aujourd’hui, et ce que dit Sage

Côté ERP, un chèque suit la même frise qu’un virement. Seul le moment du « payé » change.

| Étape ERP | Quand, pour un chèque |
|---|---|
| Paiement à préparer / préparé | comptabilisation : `paiement_effectif` en attente |
| Déposé sur Suite | peint en passé dès que le statut passe payé ; pas à la lecture `Delivered` |
| Virement à exécuter / exécuté | même peinture ; la date n’existe que si quelqu’un a marqué le virement |
| Paiement confirmé | dès le `create` APPYMT, parce que `Printed Check` n’est pas `ACH` |

Mapping réellement codé (`mapperDepuisDisplayState` + repli `STATE`) :

| Valeur lue | État ERP |
|---|---|
| `DISPLAYSTATE` `delivered` | `paiement_disponible` |
| `DISPLAYSTATE` `confirmed` | `paye` |
| `DISPLAYSTATE` `approved` | rien (`null`) |
| `STATE` `P` | `paiement_disponible` |
| `STATE` `C` | `paye` |
| `STATE` `A` | rien |

`Printed` et `Complete` ne sont pas des valeurs de ce `match`.

Documentation Sage, citée, pas résumée de mémoire :

- Aide « Payment status descriptions » (modifiée le 25/09/2026), https://www.intacct.com/ia/docs/en_US/help_action/Accounts_Payable/Payments/Payment_Requests/payment-request-status-descriptions.htm — statuts : Draft, Submitted, Unapproved, Partially approved, Approved, Delivered, Complete, Voided. Pour un chèque, **Approved** : « Check payments: Print the check from the Print checks page. » **Delivered** : « Check payments: Confirm the check on the Print checks page. Printing the check reserves the check number […] but the check will not appear in the register until it is posted (confirmed). » **Complete** : « Check payment: You confirm the associated check on the Print Checks page. » Le statut posté documenté s’appelle **Complete**, pas Confirmed.
- Aide « Payment request workflow », https://www.intacct.com/ia/docs/en_US/help_action/Accounts_Payable/Payments/Payment_Requests/about-payment-status.htm — tableau : Approved → Delivered = imprimer le chèque ; Delivered → Complete = confirmer le chèque sur Print checks. « Bills included in a payment request have a state of Selected. The bill state remains Selected until the payment is posted. » « For all payment methods, as soon as the payment status is Complete, […] The bill state […] changes to Paid. »
- Aide « Track a bill payment request », https://www.intacct.com/ia/docs/en_US/help_action/Accounts_Payable/Payments/Payment_Requests/track-a-bill-payment-request.htm — la ligne de workflow nommée **Printed** a le statut de page « Payment requests » = **Delivered** (le PDF est généré, le numéro est réservé, le bill reste Selected). La ligne « Payment is confirmed (Posted to GL) » passe le bill à **Paid**. « Printed » n’est pas un statut de page.
- API « AP Payments », https://developer.intacct.com/api/accounts-payable/ap-payments/ — `PAYMENTMETHOD` `Printed Check` est l’exemple de création. `STATE` : `C` Posted, `D` Draft, `A` Approved, `V` voided, `S` Submitted. Même phrase pour `APPYMTDETAIL`. `confirm_appaymentrequest` (legacy) : « Use this to confirm checks have been successfully printed. » La page lue ne documente pas `DISPLAYSTATE` ni `STATUS = T`.

La lecture du 30/09 (bill Selected, TOTALPAID 0, DISPLAYSTATE Approved, STATE A) est l’état **Approved** de cette doc : le chèque n’est pas imprimé, le bill n’est pas Paid. Les deux lignes `APPYMTDETAIL` `STATE D` sont, selon la même page API, Draft.

## 5. Requête lecture seule

Colonnes réelles. À lancer en prod, `SELECT` seulement. Les valeurs n’ont pas été lues depuis ici.

```sql
SELECT
  f.id,
  f.reference,
  f.numero_facture_fournisseur,
  f.statut,
  f.etat_paiement_sage,
  f.intacct_bill_id,
  f.intacct_record_no,
  f.intacct_when_paid,
  f.intacct_appymt_when_created,
  f.intacct_paiement_synced_at,
  f.virement_execute_at,
  f.virement_execute_par_id,
  f.paiement_manuel_at,
  f.updated_at
FROM factures f
WHERE f.numero_facture_fournisseur = '2604901-973983'
   OR f.intacct_bill_id = '2389236'
   OR f.intacct_record_no = '2389236';

SELECT
  d.id,
  d.facture_id,
  d.statut,
  d.deja_reglee,
  d.date_reglement,
  d.date_paiement,
  d.date_paiement_previsionnel,
  d.mode_reglement_direct,
  d.intacct_bill_id
FROM depot_factures d
WHERE d.facture_id = :id;

SELECT
  w.niveau,
  w.statut,
  w.date_assignation,
  w.date_validation,
  w.validateur_id,
  u.name AS validateur,
  w.commentaire
FROM workflow_validations w
LEFT JOIN users u ON u.id = w.validateur_id
WHERE w.facture_id = :id
ORDER BY w.id;

SELECT
  h.id,
  h.created_at,
  h.action,
  h.user_id,
  u.name AS utilisateur,
  h.commentaire,
  h.champs_modifies
FROM facture_historique h
LEFT JOIN users u ON u.id = h.user_id
WHERE h.facture_id = :id
ORDER BY h.id;

SELECT
  p.id,
  p.created_at,
  p.intacct_payment_id,
  p.intacct_bill_id,
  p.mode_paiement,
  p.payment_method_sage,
  p.intacct_appymt_state,
  p.intacct_apbill_state,
  p.montant,
  p.date_paiement,
  p.intacct_paiement_synced_at
FROM paiements_intacct p
WHERE p.intacct_bill_id = '2389236'
   OR p.intacct_payment_id = '2389306';
```

Attendu par le code, à confirmer par ce SELECT : `statut = payee`, `etat_paiement_sage` vide ou antérieur à `paye`, `intacct_when_paid` NULL, `virement_execute_at` NULL, `deja_reglee = 0`, `paiement_effectif` valide au nom de Rudy BENOR, `payment_method_sage = Printed Check`. Si `intacct_appymt_when_created` est NULL, la date « 30/09 » de « Déposé sur Suite » est `intacct_paiement_synced_at`.

## PROPOSITIONS — non implémentées

Frise propre au chèque (`Printed Check`, `deja_reglee` faux) : Paiement préparé → Chèque émis → Paiement confirmé. Pas de « Déposé sur Suite », pas de « Virement exécuté ». « Chèque émis » au statut Sage Delivered (le chèque est imprimé, le bill reste Selected). « Paiement confirmé » seulement au statut Complete / bill Paid (confirmation sur Print checks), pas au `create` APPYMT. Le `create` laisserait la facture en paiement, comme le fait déjà l’ACH.

Écart ERP payé / Sage non payé : badge visible « Écart Sage » quand le statut ERP est payé et que la lecture montre un bill Selected, `TOTALPAID` 0, ou un paiement Approved / Delivered qui n’est pas Complete. Pas de recul automatique. La synchro garde « un payé ne recule jamais ». Le badge signale ; une remise en paiement resterait un geste explicite, aujourd’hui refusé par « Constater le non-paiement ».

## DÉPLOIEMENT-TEST

Fiche seule. `git pull origin main`. Pas de migration, pas de `view:clear`, pas de `journal:sync`.

## LEÇON

Un chèque `Printed Check` est marqué payé dans l’ERP au moment où Sage crée l’APPYMT, c’est-à-dire à l’état Approved, avant l’impression et avant la confirmation. La frise de virement repeint alors « Déposé sur Suite » et « Virement exécuté » avec un auteur Sage en dur, sans avoir lu Delivered.
