---
suivi: 1613
date: 2026-09-18
sujet: Diagnostic — acompte, clôture ERP sans Sage + rapprochement APADVANCE
chantier: comptabilite / depot-factures / sage
type: diagnostic
statut: poussé
hash: 3b356ad1
fichiers:
  - docs/suivi/SUIVI_1613_diagnostic_acompte_reconciliation.md
---

# SUIVI #1613 — diagnostic acompte : pas d’envoi Sage, rapprochement manuel

## PROMPT ENVOYÉ

Décision Robin : l’ERP ne crée pas l’acompte dans Sage (API XML
incapable de APADVANCE / REST disproportionné). Flux : dépôt + saisie
+ NV1 + NV2 (#1612) ; à la compta, rien n’est envoyé ; affichage des
champs à reporter + référence ERP à coller ; bouton de sync à la
demande (pas le cycle 4 h). Diagnostic lecture seule, aucun code
applicatif.

## BASE GIT

HEAD local = `origin/main` = `4d38da36` (chore hash fiche #1612).
Feature #1612 déjà sur main : `3cd1295e`.
Aucun rebase. Aucun fichier applicatif modifié.
MySQL local `127.0.0.1:3306` refusé (`SQLSTATE[HY000] [2002]`),
comme #1610. `Schema::getColumnListing('factures')` tenté, échec.
Colonnes = dump `hectareenvol.sql` CREATE TABLE `factures` +
migrations postérieures au dump (`paiement_manuel_*` #1604).

## FICHES LUES

- **#1610 LEÇON** — objet XML `APADVANCE` / `APADVANCEITEM` ; zéro
  occurrence dans `app/` ; création attendue via `<create>` générique
  mais **non documentée** sur le portail developer AP ; application
  via APPYMTDETAIL `ADVANCEKEY`. Impact : ce lot **abandonne** la
  création (décision Robin) et instruit la **lecture** + le
  rapprochement par `factures.reference`.
- **#1612 LEÇON** — garde-fou serveur
  `reponseSiComptaNonPretPourAction` → `FactureEstAcompte::jsonRefuseEnvoiSage()`
  **avant** tout APBILL ; front `acompteSageIndisponible` désactive
  « Comptabiliser ». Impact : le refus actuel **bloque aussi** la
  clôture ERP sans Sage ; le lot d’écran devra le **restreindre**,
  pas l’enlever pour FACTURE/AVOIR.
- **#1604 LEÇON** — `Gate::before` / `hasAnyM365Group` / middleware
  `m365.groups` short-circuitent `GS_ADMIN_ERP` ; lecture brute =
  `m365GroupNames()` + `in_array`. Impact : le bouton sync doit
  recopier ce modèle ; test compte tiers obligatoire.
- **#1607** — sync PROJECT = `readByQuery` générique + POST web, **pas**
  le cycle 4 h paiements. Impact : ne pas brancher APADVANCE sur
  `FacturePaiementStatutBatchService` / lazy 4 h.
- **#1570** — sonde web GS_DSI, pas CLI Sage OVH. Impact : la preuve
  tenant APADVANCE reste une route web (forme ci-dessous).

Aucune fiche n’a changé le plan métier (pas de create Sage). #1612
est **déjà poussé** : plus de collision git en cours, mais les lots
suivants retoucheront les mêmes fichiers disputés.

---

## A. LE CONNECTEUR SAIT-IL LIRE UN APADVANCE ?

### A.1 Méthodes génériques — preuve code, pas intuition

Le connecteur **n’a aucune** méthode nommée APADVANCE (grep `app/`
2026-09-18 : zéro, inchangé depuis #1610). Il a des **fonctions XML
génériques** dont le nom d’objet est un argument / du XML libre.

| Fonction XML Sage | Où dans Hectarion | Objet paramétrable ? | Visibilité |
|---|---|---|---|
| N’importe quel XML (dont `lookup`, `inspect`, `query`, `read`, `readByQuery`, `create`) | `IntacctService::sendRequest(string $xmlBody)` L152-155 | Oui — le body est libre | **public** |
| `<readByQuery><object>…</object>` | `readByQueryPaginated($object, $fields, $query, …)` L804-856 | Oui, 1er argument `$object` | **private** |
| idem + stratégies wildcard | `probeExecuteReadByQuery` L3553-3570 + `probeTryReadStrategies` L3456 | Oui, `$object` | **private** |
| `<read><object>…</object><keys>` | `probeExecuteReadByKeys` L3576-3592 | Oui | **private** |
| `<readByName><object>…` | `probeVendorStructureByIntacctId` L548-554 (VENDOR en dur dans l’appelant, XML générique) | L’élément `<object>` est générique | public via ce probe |
| `<query><object>APBILL` | `buildApBillStructuredQueryXml` L2665-2706 | Générique XML ; aujourd’hui câblé APBILL | private |
| `<inspect detail="1"><object>APPYMT` etc. | `IntacctController::probeFinancialEntities` L138-157 | Générique ; déjà utilisé pour `*`, CHECKINGACCOUNT, CREDITCARD, APPYMT | route web GS_DSI |
| `<create><VENDOR>` / `<APPYMT>` / `<CUSTOMER>` / `<ARINVOICE>` | `IntacctService` L1205, L2424, L4253, L4310 | Wrapper générique ; enfant = nom d’objet | public via les builders |

Doc Sage (https://developer.intacct.com/web-services/queries/) :
`lookup` liste les champs queryables de **n’importe quel** objet
standard ; `readByQuery` / `query` / `read` acceptent le nom
d’objet. `inspect` est le cousin legacy (déjà en prod via
`/admin/intacct/probe-financial-entities`).

**Verdict lecture (connecteur) :** oui, **le canal existe**. Il
suffit d’appeler `sendRequest` avec un XML `<lookup>`,
`<readByQuery>` ou `<read>` dont `<object>APADVANCE</object>`.
Pas besoin d’un SDK métier. Les helpers paginés / probe #212 sont
déjà object-agnostiques mais **privés** : un lot lecture devra soit
passer par `sendRequest` (public, déjà utilisé par les probes
admin), soit extraire un wrapper public.

**Verdict lecture (tenant Sage) : non prouvé.** Aucun appel
APADVANCE n’a jamais été envoyé. Sage peut répondre « object not
supported » / droits module / objet seulement REST. Ça ne se
mesure **pas** en CLI OVH (contrainte structurelle). Voir A.5.

### A.2 Champs de rapprochement — ne pas trancher sans lookup

Clé ERP retenue (mesurée, ne pas redemander) : `factures.reference`
format `FAC-2026-1538`, 13 caractères, unique.

Côté Sage, **trois** identifiants distincts coexistent. Ne pas les
confondre.

| Nom API (candidat) | Rôle documenté | Est-ce le champ où coller FAC-… ? |
|---|---|---|
| `RECORDNO` | Clé interne, PK DDS, `keys` de `<read>` | **Non** — généré Sage. À **stocker** après rapprochement. |
| `RECORDID` | DDS : « User-visible document number ». UI Sage **Payment ID** (séquence AP Configuration, 2025 R4). Read-only. | **Non** — numéro système, pas la saisie comptable. |
| `DOCNUMBER` | Présent dans le DDS APADVANCE (liste officielle developer.intacct.com/data-delivery-service/objects/). Sur ARADVANCE documenté : « Reference number such as a check number… ». Sur APPYMT : document number / n° de chèque. | **Candidat principal XML** du champ collé. |
| `REFERENCENUMBER` / REST `referenceNumber` | DataBlend create : `referencenumber`. REST CData Advance : `referenceNumber` = « The external reference number associated with this advance » (writable). | **Même champ ou jumeau REST** — à confirmer par LABEL du lookup. |

UI Sage (help « Create an AP advance ») :
- *Reference number* si mode **Credit card** ou **Record transfer** ;
- *Document number* si mode **Cash**.
Help AU : libellé unique **« Reference number/ Document number »**
pour coller un n° de reçu externe.
Help GB field descriptions : colonne **Document number** (CB / virement
interne / cash).

Donc l’UI française « Numéro de référence » est **très probablement**
`DOCNUMBER` en XML et `referenceNumber` en REST — **un seul champ
utilisateur**, deux noms d’API. **Pas tranché** : le lookup doit
renvoyer `<ID>` + `<LABEL>` (et la locale tenant). `RECORDID` est
le Payment ID, pas ce champ.

Filtre de rapprochement proposé (après sonde) :

```
<readByQuery>
  <object>APADVANCE</object>
  <fields>RECORDNO,RECORDID,DOCNUMBER,STATE,RAWSTATE,VENDORID,TOTALENTERED,TOTALPAID,TOTALDUE,TOTALSELECTED,WHENPAID,WHENCREATED</fields>
  <query>DOCNUMBER = 'FAC-2026-1538'</query>
  <pagesize>5</pagesize>
</readByQuery>
```

Si le lookup montre un champ `REFERENCENUMBER` (ou autre) dont le
LABEL = « Reference number » / « Numéro de référence », **c’est
celui-là** qu’il faudra filtrer, pas `DOCNUMBER`.

### A.3 Champs d’état et de montants — candidats, LABEL à confirmer

UI liste Sage (help field descriptions AP Advances) :

| Libellé UI | Sens métier UI | Candidats API (XML DDS / REST CData) |
|---|---|---|
| État | Draft / Pending Approval / In process / **Advance paid** / Partially applied / Fully applied / Voided / Void | XML `STATE` (+ `RAWSTATE`). REST `state` (doc CData : « draft or posted » — **plus pauvre** que l’UI). Les états « paid / applied » peuvent être **calculés** (TOTALDUE = 0 ⇒ fully applied) plutôt qu’un enum. **VALIDVALUES du lookup obligatoires.** |
| Amount / Payment amount | Montant de l’acompte | XML `TOTALENTERED` / `TRX_TOTALENTERED`. REST `totalEntered` / `txnTotalEntered`. |
| Acompte total payé | Montant effectivement payé | XML `TOTALPAID` / `TRX_TOTALPAID`. REST `paymentInformationTotalPaid` / `paymentInformationTxnTotalPaid`. REST `paymentInformationPaidDate` ↔ XML `WHENPAID`. |
| Montant appliqué (Total applied) | Crédit déjà imputé sur des factures AP | **Pas de champ `TOTALAPPLIED` dans le DDS APADVANCE.** Candidat : `TOTALSELECTED`, ou **calcul** `TOTALENTERED − TOTALDUE`. Ne pas inventer. |
| Acompte dû (Advance due) | Amount − Total applied | XML `TOTALDUE` / `TRX_TOTALDUE`. REST `totalDue` / `txnTotalDue`. |

Mapping « créé vs payé » côté ERP (proposition, pas code) :

- **Créé dans Sage, pas encore payé** : STATE ∈ {Draft, In process, Pending Approval, …} et TOTALPAID ≈ 0.
- **Payé (crédit disponible)** : UI « Advance paid » — STATE à lire sur le tenant ; TOTALPAID ≈ TOTALENTERED ; TOTALDUE ≈ TOTALENTERED (rien d’appliqué).
- **Appliqué** : TOTALDUE → 0 (fully) ou 0 < TOTALDUE < TOTALENTERED (partially).

Sans lookup, **ne pas** coder un `match` sur des codes (`C`/`D`/`A`
sont ceux d’**APPYMT**, pas d’APADVANCE).

### A.4 🔴 Bonus — le `<create>` générique accepte-t-il APADVANCE ?

**Preuves qu’on a :**

1. Le wrapper `<create>` est générique (doc XML API : « can be used
   to add … a new vendor … or many other objects »). Hectarion s’en
   sert déjà pour VENDOR / APPYMT / CUSTOMER / ARINVOICE.
2. Le portail developer Intacct **n’a pas** de page « AP Advances »
   dans le menu AP (constat #1610, inchangé). Citation doc : « If an
   object or function is not included in the API documentation, it
   is likely not supported. We recommend using the REST API. »
3. DataBlend documente un **create** SDK (champs `advancedate`,
   `vendorid`, `paymentmethod`, `financialentityid`, `referencenumber`,
   lignes `entry_classid` / `entry_locationid`). Ce n’est **pas**
   une preuve que le XML `<create><APADVANCE>` du tenant Hectare
   marche.
4. Le SDK PHP vendor (`vendor/intacct`) **ne contient aucune**
   classe ApAdvance (grep 2026-09-18).
5. `lookup` standard renvoie par **champ** REQUIRED / READONLY, pas
   un flag objet `CREATABLE` documenté. On **ne peut pas** conclure
   « créable » rien qu’en lisant la doc lookup.

**Verdict créabilité : non établi.** Ne pas affirmer que le create
XML marche. Ne pas affirmer qu’il échoue.

Si la sonde (A.5) envoie un `<create><APADVANCE></APADVANCE></create>`
**volontairement incomplet** :

- erreur type « Cannot create the object APADVANCE » / « Invalid
  object » → XML **ne crée pas** (décide le lot manuel) ;
- erreur type « VENDORID is required » / « FINANCIALENTITY required »
  → l’objet **est créable** via XML → **ça change tout** : la saisie
  manuelle Sage devient optionnelle ; à ré-arbitrer par Robin avant
  tout lot d’écran.

Ce probe create incomplet **n’écrit rien** s’il est rejeté en
validation, mais c’est quand même un POST métier vers Sage prod.
Le faire **uniquement** derrière `?confirm_create_probe=1` +
GS_DSI, jamais en CLI, jamais en boucle.

### A.5 Sonde web — forme exacte pour le lot suivant

Patron existant : `IntacctController::probeFinancialEntities`
(`GET /admin/intacct/probe-financial-entities`, middleware
`m365.groups:GS_DSI`, `sendRequest`, JSON pretty). Même famille :
`probe-apbill-states`, `FournisseurSageProbeController`.

Proposition (à coder dans un lot sonde, **pas ici**) :

- Route : `GET /admin/intacct/probe-apadvance`
- Middleware : `m365.groups:GS_DSI` (le middleware short-circuit
  admin ERP — **tester avec un compte GS_DSI sans GS_ADMIN_ERP**).
- Query : `entity` (LOCATIONENTITY, comme probe APBILL),
  `reference` optionnel (`FAC-2026-xxxx`),
  `confirm_create_probe=1` pour le bonus create.
- Séquence, tout via `IntacctService::sendRequest` :

  1. `<inspect detail="1"><object>APADVANCE</object></inspect>`
  2. `<lookup><object>APADVANCE</object></lookup>` — coller ID +
     LABEL + READONLY des champs DOCNUMBER, RECORDID,
     REFERENCENUMBER, STATE, TOTAL*, WHENPAID + VALIDVALUES de STATE
  3. `<readByQuery>` pagesize 1 fields explicites (le wildcard `*`
     est souvent refusé sur APBILL/APPYMT, probe #212)
  4. Si `reference` : trois `readByQuery` séparés
     `DOCNUMBER = '…'`, `RECORDID = '…'`, et si le lookup a révélé
     `REFERENCENUMBER`, ce champ aussi
  5. Un `<query>` structuré `equalto` (jumeau APBILL L2675) — si
     `readByQuery` échoue et `query` passe, le lot sync utilisera
     `query`
  6. Optionnel : `<create><APADVANCE></APADVANCE></create>` si
     confirm. Classer le message d’erreur. **Ne pas** envoyer un
     create complet.

Pas de CLI. Pas d’ajout à `routes/console.php`. Pas d’appel depuis
le cycle 4 h.

---

## B. L’ÉCRAN DE COMPTABILISATION

### B.1 Où se branche le refus d’envoi Sage (#1612)

Fichier : `app/Http/Controllers/ComptabilisationController.php`.

| Point | Méthode | Lignes | Effet actuel |
|---|---|---|---|
| **Cœur** | `reponseSiComptaNonPretPourAction` | 341-345 | Si `FactureEstAcompte::checkDepot` → 422 `FactureEstAcompte::jsonRefuseEnvoiSage()` **en premier**, avant équilibre / société / analytics. |
| Relais public | `reponseSiComptaNonPretPourActionPublic` | 479-481 | Utilisé par `AvoirIntacctApbillService` |
| Helper | `depotEstTypeAcompte` | 469-472 | `FactureEstAcompte::checkDepot` |
| Envoi stub | `envoyerIntacct` | 635-637 | passe par le cœur |
| Envoi avoir | `envoyerAvoirIntacct` | 670-672 | garde-fou **en plus** du cœur |
| Clôture ERP sans Intacct (legacy) | `comptabiliser` | 695-697 | **aussi bloquée** par le cœur |
| Envoi APBILL réel | `executerComptabilisationApbill` | 773-795 | cœur **puis** second `if (depotEstTypeAcompte)` L794-795 |

Message unique : `FactureEstAcompte::MESSAGE_ENVOI_SAGE` =
« L'envoi Sage des acomptes n'est pas encore disponible. »
(`app/Support/FactureEstAcompte.php` L16, `jsonRefuseEnvoiSage` L50-56).

Front #1612 :

- `DepotFactureSaisieForm.vue` L5037-5038 : `showComptabiliserEtPayer`
  forcé false ; `acompteSageIndisponible: true` si type ACOMPTE.
- `DepotFactureSaisieActionBar.vue` L175-178 : bandeau ambre avec
  le même message ; L214-224 : boutons Comptabiliser /
  Comptabiliser et payer **disabled**.

Conséquence pour le lot écran : aujourd’hui on ne peut **ni**
envoyer Sage **ni** clôturer l’ERP. Il faudra :

1. **Garder** le refus sur tous les chemins APBILL / APPYMT /
   envoyer-avoir (FACTURE/AVOIR intacts).
2. **Ouvrir** un chemin « clôturer l’ERP sans Sage » (nouveau, ou
   analogie `executerComptabilisationAvoirErpSansSage` L487-557)
   qui **ne passe pas** par le early-return acompte du cœur — ou
   dont le cœur n’intercepte plus que l’envoi Sage, pas la clôture
   ERP.

Ne pas élargir `comptabiliser()` legacy (L686, « Intacct non
branché ») aux acomptes sans audit : il pose
`DepotFacture::STATUT_VALIDE` (L701), pas `comptabilisee`.

### B.2 Où afficher le bloc « à reporter dans Sage »

Structure réelle de l’écran :

```
Factures/Comptabilisation/Index.vue
  3 colonnes (useThreeColumnLayout, storageKey comptabilisation-col-prefs)
  Col 1 : liste eligibleComptabilisation (filtres société / nature)
  Col 2 : (PDF / pièce — hors sujet)
  Col 3 : bandeau « Saisie » + onglets Comptabilité | Informations | Historique
          → DepotFactureSaisieForm (comptabilisationMode: true)
             → DepotFactureSaisieActionBar (Comptabiliser / Rejeter / …)
             slot #comptabilisation-historique
```

Le formulaire de saisie **est** le composant dépôt, pas un écran
dédié. Les champs fournisseur / montant / analytics / établissement
sont déjà dans l’onglet Informations / Comptabilité du form
(`ocr_raw.saisie`). Ils ne sont pas regroupés comme « à copier
dans Sage ».

Emplacement recommandé du bloc (proposition, pas code) :

1. **Primaire** — dans la col. 3, **au-dessus** des onglets ou en
   tête de l’onglet Comptabilité, **uniquement** si
   `isTypeFactureAcompte`. Composant **nouveau** (ne pas gonfler
   encore `DepotFactureSaisieForm.vue` au-delà du minimum : un
   import + un `v-if`). Contenu :
   - Fournisseur (nom + `fournisseurs.intacct_id` / VENDORID)
   - Montant TTC (= HT acompte)
   - Classe Intacct (CLASSID résolu, pas seulement l’id ERP)
   - Établissement (nom + `location_id`)
   - Référence ERP `factures.reference` + bouton copier
     (clipboard ; toast « copié »)
2. **Secondaire** — `Factures/Show.vue` header (L1439+), à côté du
   badge Acompte déjà posé par #1612 (L1443-1446), une fois la
   pièce clôturée ERP / en attente Sage.

Ne **pas** mettre le bloc uniquement dans l’action bar (trop
étroit, déjà saturée).

### B.3 Quel état entre compta ERP et confirmation Sage ?

#### Statuts `factures.statut` (ENUM MySQL, ne pas inventer)

Source dump `hectareenvol.sql` L1740 +
`Facture::statutsAutorisesColonne()` + migration
`2026_05_29_140000_add_comptabilisee_to_factures_statut_enum.php` :

| Valeur | Usage actuel |
|---|---|
| `recue` | dépôt / OCR |
| `en_validation_nv1` | NV1 |
| `en_validation_nv2` | NV2 |
| `a_comptabiliser` | file compta (`eligibleComptabilisation`) |
| `comptabilisee` | APBILL envoyé **ou** avoir ERP-sans-Sage (#643e) |
| `en_paiement` | file paiement / APPYMT |
| `payee` | payée |
| `rejetee` | rejet |

**Pas d’autre valeur.** Ajouter un 9e enum = migration MySQL ENUM
(ALTER MODIFY, pas de transaction) + `statutsAutorisesColonne` +
frise + listes + tests. Coût élevé, à n’ouvrir que si les 8
existants sont tous faux.

#### Sous-étapes `factures.etat_paiement_sage`

`EtatPaiementSage` : `preparation` → `paiement_disponible` →
`virement_execute` → `paye`. (L’ENUM dump contient encore
`en_attente`, remappé vers `paiement_disponible`.) Chaîne
**APBILL/APPYMT**. Un acompte manuel n’y entre pas : ces valeurs
pilotenent la frise paiement et le batch 4 h.

#### Statuts `depot_factures.statut` (pertinents)

`a_comptabiliser` | `comptabilisee` | `valide` | `paye` | `rejete` |
`archive`. File écran = `scopeEligibleComptabilisation`
(`DepotFacture.php` L375-390) : statut dépôt
`a_comptabiliser`, **ou** `valide` + facture dans
`factureStatutsEligiblesComptabilisation()` (`a_comptabiliser`,
`en_paiement`, `mise_en_paiement`).

#### Analogie déjà en prod : avoir ERP sans Sage

`executerComptabilisationAvoirErpSansSage` :

- dépôt → `STATUT_COMPTABILISEE`
- facture → `STATUT_COMPTABILISEE` avec `intacct_bill_id` **null**
- JSON `ocr_raw.compta_avoir` = `{en_attente_application, sage_envoye: false}`
- **pas** d’enum nouveau

`FacturePaiementStatutSyncService::estEligible` exige des bill ids
(`billIdsPourFacture !== []`) : un `comptabilisee` **sans**
`intacct_bill_id` **n’entre pas** dans le cycle 4 h. C’est le
garde-fou naturel.

#### Risque si on réutilise `comptabilisee` pour l’acompte en attente

`PaiementLignesPayablesService::eligibleFacturesQuery` (L255-266) :
`factures.statut = comptabilisee` + workflow `paiement_effectif`
`en_attente` + dépôt `comptabilisee`. Un acompte clôturé ERP
**apparaîtrait dans la file « à payer »** si le workflow paiement
est ouvert comme pour une FACTURE.

#1612 a **volontairement** laissé la frise acompte sur la chaîne
paiement (pas la frise avoir). Donc le risque file paiement est
**réel** si on copie l’avoir sans gater le type ACOMPTE dans
`eligibleFacturesQuery`.

#### Options (à arbitrer, pas coder)

| Option | File compta | File à payer | Cycle 4 h | ENUM | Verdict |
|---|---|---|---|---|---|
| A. Rester `a_comptabiliser` + JSON `sage_acompte.en_attente` | reste dans l’écran (pratique pour copier + sync) | non | non | aucun | Simple ; la file compta se remplit d’acomptes « déjà traités ERP ». Filtre dédié recommandé. |
| B. `comptabilisee` + `intacct_bill_id` null + JSON (copie avoir) + **exclure ACOMPTE** de `eligibleFacturesQuery` | sort de la file | non **si** exclusion | non (pas de bill id) | aucun | Cohérent avec l’avoir. Touche le paiement : **une ligne, type-gated**, pas le cycle sync. |
| C. Nouvel enum `en_attente_sage` / `acompte_en_attente` | sort si on l’ôte d’eligibleComptabilisation | non | non | **oui** — coûteux, irréversible ENUM | Uniquement si A et B sont rejetés. |

**Recommandation documentaire :** B (miroir avoir) **à condition**
d’exclure ACOMPTE de la file à payer. Alternative conservatrice :
A (reste dans la file compta) le temps que la sync manuelle soit
rare (~100/an). **Ne pas créer d’enum** au premier lot d’écran.

Marqueur JSON proposé (nom indicatif, analogie `compta_avoir`, **pas**
une colonne) : `ocr_raw.compta_acompte = { sage_envoye: false,
comptabilise_erp_at, intacct_advance_recordno: null, state: null }`.
Après sync : remplir recordno / state / montants.

### B.4 Classe Intacct et établissement — services réutilisables

Oui, déjà utilisés à l’envoi APBILL — réutilisables **en lecture**
pour l’affichage.

| Besoin UI | Résolution existante | Fichier |
|---|---|---|
| CLASSID programme | `ComptabilisationController::resolveProgrammeClassIdForApbill` : `programmes.intacct_class_analytics_id` → `intacctClassAnalytics.classid`, sinon `intacct_class_id` legacy, sinon saisie `analytics_id` | `ComptabilisationController.php` L77-91 ; appelé L1268, L1519 |
| Classe saisie | `IntacctClass::query()->find(analytics_id)` → `classid` | L1247-1253 |
| Catalogue classes | `IntacctClass::optionsActivesPourSelect()` | `app/Models/IntacctClass.php` L33 |
| LOCATION établissement | `saisie.etablissement_id` → `etablissements.location_id` ; par ligne : `LigneComptableEtablissementResolver::resolveLocationIdStrPourLigne` | L1277-1281 ; resolver `app/Support/LigneComptableEtablissementResolver.php` L38-57 |
| Entité / société | `entites.intacct_id` = LOCATIONENTITY | L1237-1240 |
| Fournisseur Sage | `fournisseurs.intacct_id` (VENDORID) | L1242-1245 |

Le payload Inertia compta (`comptaFormProps`, Index.vue L328-375)
fournit déjà `etablissements`, `classes`, `fournisseurs`,
`programmes`. Le bloc « à reporter » peut **dériver** CLASSID /
LOCATION côté serveur (même fonctions) pour afficher les codes Sage
et pas seulement les libellés ERP — important pour la saisie
manuelle dans Sage.

---

## C. LE BOUTON DE SYNCHRONISATION

### C.1 Où le placer

**Les deux**, rôles différents :

| Écran | Pourquoi |
|---|---|
| Comptabilisation (col. 3, dans le bloc à reporter) | Le comptable vient de coller FAC-… dans Sage, il est encore sur la pièce. Un clic, 1 appel. |
| Fiche `Factures/Show.vue` (header, à côté du badge Acompte / référence) | Il a quitté la file. C’est l’écran de suivi quotidien. |

Pas dans `Factures/Global.vue` (bouton sync 4 h déjà là — confusion
garantie). Pas dans `routes/console.php`.

### C.2 Un acompte vs balayage — les deux, leur coût

Volume : ~100 acomptes/an (hypothèse #1610). Tenant Sage : nombre
d’APADVANCE inconnus (sonde pagesize 1 + numremaining).

| Mode | Comportement | Coût Sage | Quand |
|---|---|---|---|
| **Unitaire** (recommandé par défaut) | `readByQuery` / `query` filtré sur la référence de **cette** pièce. 1 (ou 3 si on teste DOCNUMBER + RECORDID + REFERENCENUMBER). | 1–3 round-trips, pagesize 5 | Bouton sur la fiche / l’écran compta |
| **Balayage** | Liste ERP des acomptes `sage_envoye=false` (quelques dizaines max). Soit N appels unitaires, soit 1 `readByQuery` sans filtre pagesize 100 + `readMore`, matching PHP sur `factures.reference`. | N petit, ou 1+ pages si le tenant a peu d’avances | Bouton « Rapporter les acomptes en attente » **à la demande**, hors cycle 4 h |

Le balayage sans filtre est **dangereux** si le tenant a des milliers
d’avances historiques (pagesize 100 × readMore). On ne le sait pas
sans sonde. Tant que `numremaining` de la sonde pagesize 1 est
inconnu : **ne livrer que l’unitaire**. Le balayage = lot suivant
si la sonde montre un volume faible.

Ne **pas** ajouter les acomptes à
`FacturePaiementStatutBatchService::collectEligibleFactureIds`
(filtre actuel : statut comptabilisee/en_paiement **et**
`intacct_bill_id` non vide — APBILL). Les y ajouter violerait le
garde-fou « pas le cycle 4 h » et casserait `probeReadApBill*`
(RECORDNO d’avance lu comme bill).

### C.3 Que stocker au rattachement — colonnes

`Schema::getColumnListing('factures')` : **échec** MySQL local.
Preuve schéma : dump `hectareenvol.sql` L1719-1772 + fillable
`Facture.php` (colonnes #1604 absentes du dump).

Colonnes Sage **déjà là** :

| Colonne | Type dump / migration | Usage actuel | Réutilisable pour APADVANCE ? |
|---|---|---|---|
| `reference` | varchar 255 UNIQUE | FAC-2026-xxxx | **Oui** — clé de rapprochement, déjà unique. |
| `intacct_bill_id` | varchar 255 UNIQUE | RECORDNO **APBILL** | **Non.** Le batch 4 h et `PaiementLignesPayablesService` lisent cette valeur comme bill. Y mettre un RECORDNO d’avance = lectures APBILL fantômes / collisions unique. |
| `intacct_record_no` | varchar 50 | recopié du même billKey (`mettreAJourFactureLieeApresComptaIntacct` L1844-1845) | **Non** — même sémantique bill. |
| `etat_paiement_sage` | enum préparation/…/paye | chaîne APPYMT | **Non** — valeurs fausses pour « Advance paid / applied ». |
| `intacct_paiement_synced_at` | timestamp | last sync APBILL | Réemploi = mélange de deux syncs. Préférer un timestamp dédié **ou** JSON. |
| `intacct_when_paid` | date | WHENPAID bill | Ambigu. Possible pour date de paiement avance **si** on n’écrit jamais un bill sur un acompte. Risque de lecture frise paiement. |
| `intacct_appymt_when_created` | date | APPYMT | **Non.** |
| `paiement_manuel_at` (+ par, date, contexte) | #1604 | facture historique marquée payée **sans** Sage ; exclut la sync APBILL (`estEligible` L37-39) | **Non** — autre métier (historique). S’en servir masquerait l’acompte comme « payé manuel » et **bloquerait** une sync ultérieure. |

**Manquant** (si on veut des colonnes plutôt que du JSON) :

- `intacct_advance_recordno` (varchar 50, nullable) — RECORDNO APADVANCE
- éventuellement `intacct_advance_state` (varchar, pas un nouvel enum MySQL figé avant la sonde)
- éventuellement `intacct_advance_synced_at`
- montants / WHENPAID : JSON suffisent (rares)

**Alternative sans migration** (recommandée pour le 1er lot sync) :
JSON `ocr_raw.compta_acompte` comme `compta_avoir`. Pas d’ALTER.
Requête « en attente » = JSON path (MySQL `JSON_EXTRACT`). Si ça
devient un filtre quotidien, alors seulement on ajoute 1 colonne
`intacct_advance_recordno`.

`depot_factures.intacct_bill_id` : même interdiction (précheck
doublon APBILL L1004-1045).

### C.4 Droits

Demande Robin : `GS_COMPTABILITE` + `GS_DIRECTION_GENERALE`.

Modèle de lecture **brut** déjà en compta :

```
$groups = $user->m365GroupNames();
return in_array('GS_COMPTABILITE', $groups, true)
    || in_array('GS_DIRECTION_GENERALE', $groups, true);
```

Preuve : `FactureController::userCanEditPaiementFondsPropres`
L185-201 — commentaire explicite « m365GroupNames brut ».

**Ne pas** utiliser :

- `hasAnyM365Group` — `User.php` L146-152 : `isAdminERP()` ⇒ true ;
- `hasM365Group` — L134-137 : idem ;
- middleware `m365.groups:…` — `EnsureM365GroupMembership` L30-32 :
  short-circuit `isAdminERP()` **avant** la liste ;
- `Gate::before` (`AppServiceProvider.php` L69-75) : admin ERP ⇒
  true pour toute ability.

Le bouton sync paiements 4 h actuel (`FactureGlobaleController`
`SYNC_PAIEMENTS_SAGE_GROUPS` = COMPTA + ADMIN_ERP + DSI + DG,
`hasAnyM365Group`) est **le contre-exemple** : un admin ERP sans
GS_COMPTABILITE le voit. Ne pas le copier.

Test obligatoire : compte **tiers** GS_COMPTABILITE (pas Robin /
GS_ADMIN_ERP) → 200 ; compte hors groupes → 403 ; compte admin ERP
**sans** COMPTA ni DG → 403 si on a bien lu en brut.

GS_DSI : hors demande Robin. La sonde lookup reste GS_DSI ; le
bouton métier non.

---

## D. CAS D’ERREUR (comportements proposés, pas de code)

### D.1 Référence absente dans Sage

Le comptable a oublié de coller FAC-… (ou a collé dans Description /
Payment ID).

**Proposé :** 422 métier, message clair :
« Aucun acompte Sage trouvé avec la référence FAC-2026-xxxx.
Vérifiez le champ Numéro de référence (pas le Payment ID). »
Ne pas créer l’acompte. Ne pas matcher « le plus récent du
fournisseur » (faux positifs). Logger warning + reference +
VENDORID. Garder l’état ERP inchangé.

### D.2 Plusieurs acomptes avec la même référence

Sage **n’impose pas** l’unicité de DOCNUMBER / referenceNumber
(contrairement à `factures.reference`). Un copier-coller répété, un
brouillon + un posté, un void + un nouveau : possibles.

**Proposé :** refuser le rattachement automatique. 422 avec la
liste (RECORDNO, STATE, montant, date) et demander à Robin / la
compta de choisir **plus tard** (lot UI de désambiguïsation) ou de
corriger dans Sage. Ne **pas** prendre le plus récent en silence.
Compter ce cas dans la sonde (pagesize 5, signaler count > 1).

### D.3 Montant Sage ≠ montant ERP

Arrondi, devise, acompte partiel mal saisi.

**Proposé :** **rattacher quand même** (la pièce Sage existe) mais
**signaler** (warning JSON + bandeau UI + historique dépôt), seuil
0,01 €. Ne pas bloquer : le but du bouton est le rapprochement, pas
un contrôle de saisie Sage. Écart ≥ 1 € : warning plus visible
(confirmation « rattacher malgré l’écart ? ») — à arbitrer.

Ne pas « corriger » le montant ERP pour coller Sage.

### D.4 Acompte supprimé / voided après rattachement

UI Sage : Voided / Void ; XML STATE à confirmer (ARADVANCE a
Reversed ; APPYMT a `V`).

**Proposé :** le bouton unitaire **re-lit** Sage. Si RECORDNO
inconnu / STATE voided :

- ne pas effacer le RECORDNO stocké (trace) ;
- marquer `sage_envoye` / state = voided (ou `sage_introuvable`) ;
- bandeau : « Acompte Sage annulé ou introuvable — la pièce ERP
  n’est plus rapprochée. » ;
- **ne pas** repasser la facture en `a_comptabiliser` tout seul
  (perte d’audit). Action manuelle « détacher » plus tard si besoin.

Le cycle 4 h ne doit **toujours pas** s’en mêler (il parlerait
APBILL).

---

## COLLISION #1612 (prévisible, plus en cours)

#1612 est **sur origin/main** (`3cd1295e` / fiche `4d38da36`).
Working tree applicatif **clean**. Pas de conflit git ouvert.

Lots d’implémentation suivants retoucheront les mêmes zones
disputées (`zones-communes-serialisation`) :

- `ComptabilisationController.php` (cœur du refus)
- `DepotFactureSaisieForm.vue` / `DepotFactureSaisieActionBar.vue`
- `Factures/Comptabilisation/Index.vue`
- `Factures/Show.vue`
- `FactureEstAcompte.php`
- `FactureFriseEtapePresenter.php`
- `FactureController.php`

Règle : **un lot front à la fois**, rebase `origin/main` **à jour**
avant `npm run build`, committer `public/build/` dans le même
commit.

Ne rien changer à FACTURE / AVOIR, au cycle 4 h (#1607 PROJECT
inclus), aux agrégats marché (règle net à régler #1612).

---

## DÉCOUPAGE EN LOTS (risque)

| Lot | Contenu | Risque | Dépend de |
|---|---|---|---|
| **1613** | ce diagnostic | — | #1612 poussé |
| **Sonde** | Route web GS_DSI `probe-apadvance` (lookup + readByQuery + query + inspect ; create incomplet opt-in). Retirer après lecture. | Faible (lecture) / **Moyen** si create probe | Arbitrage Robin sur le create incomplet |
| **Écran clôture** | Restreindre le 422 acompte aux envois Sage ; clôturer ERP sans APBILL ; bloc « à reporter » + copier `reference` ; sortir de la file (option B) **ou** rester (A). Exclure ACOMPTE de `eligibleFacturesQuery` si B. **Zéro appel Sage.** | Moyen (formulaire monstre + file paiement) | #1612, arbitrage A/B |
| **Sync unitaire** | Bouton fiche + compta ; `sendRequest` readByQuery sur le champ **prouvé par la sonde** ; JSON `compta_acompte` (pas de migration au 1er jet) ; droits `m365GroupNames` brut COMPTA+DG ; cas D.1–D.3. | Moyen (mapping champ) | Sonde + écran |
| **Sync balayage** | Optionnel, si sonde `numremaining` faible | Faible si unitaire déjà là | Sync unitaire + volume sonde |
| **Create XML** | Seulement si la sonde prouve la créabilité. Sinon **jamais**. | Élevé (prod Sage, irréversible) | Sonde create + **ré-arbitrage Robin** |
| **Phase 2 application** | APPYMT `ADVANCEKEY` sur facture de solde (#1610 D.3) | Élevé, plus tard | RECORDNO stocké en prod |

Ne pas fusionner sonde et écran. Ne pas fusionner écran et sync
(on peut clôturer ERP et coller à la main **avant** que la lecture
Sage soit prouvée).

---

## POINTS RESTANT À ARBITRER PAR ROBIN

1. **Sonde create incomplet** vers Sage prod : oui / non ?
2. Si la sonde dit « APADVANCE créable en XML » : **on garde le
   flux manuel** (décision actuelle) ou on rouvre un lot create ?
3. **Champ collé** : attendre la sonde (LABEL) avant tout code sync.
   En attendant, consigne UI comptable : coller FAC-… dans
   « Numéro de référence » / Document number, **pas** le Payment ID.
4. **Statut d’attente** : option A (reste à comptabiliser) ou B
   (`comptabilisee` sans bill, exclusion file à payer) ? Pas d’enum
   nouveau sauf veto sur A et B.
5. **Écart de montant** : warning simple vs confirmation si ≥ 1 €.
6. **GS_DSI / GS_ADMIN_ERP** sur le bouton sync métier : hors
   demande (COMPTA+DG). Confirmer l’exclusion admin ERP (lecture
   brute).
7. **Balayage** : reporter après la sonde de volume, ou abandonner.

---

## GARDE-FOUS RAPPELÉS

- FACTURE / AVOIR : aucun changement dans ce diagnostic (rien changé).
- Cycle 4 h / `FacturePaiementStatutBatchService` / #1607 : ne pas
  y ajouter APADVANCE.
- Agrégats : inchangés (net à régler #1612).
- Pas de nom de colonne ni de champ API inventé.
- Droits : pas de test avec le compte Robin pour un 403.
- Appels Sage : web PHP-FPM uniquement, jamais CLI OVH.

## DÉPLOIEMENT

Documentation seule. OVH : `git pull origin main`. Pas de migrate,
pas de `journal:sync`, pas de npm.

## LEÇON

Le connecteur **sait déjà parler XML générique** (`sendRequest`,
`readByQueryPaginated($object)`, `<read>`, `<inspect>`). Il ne
« sait pas APADVANCE » seulement parce que personne n’a envoyé cet
objet : la question décisive est **le tenant**, pas un nouveau SDK.
`RECORDID` n’est pas le champ à coller (Payment ID système) ;
`DOCNUMBER` / `referenceNumber` sont les candidats, **tranchés par
lookup LABEL**. Réutiliser `intacct_bill_id` pour un RECORDNO
d’avance casserait le cycle APBILL. Le refus #1612 est volontairement
total (clôture ERP comprise) : le lot écran doit le **découper**,
pas l’effacer. `comptabilisee` sans bill id existe déjà pour
l’avoir — copiable **si** on exclut l’acompte de la file à payer.
`m365GroupNames()` brut, pas `hasAnyM365Group`.
