---
suivi: 1951
date: 2026-10-16
sujet: Diag FE compta — Array to string conversion lignes brouillon
chantier: factures-electroniques / comptabilisation
type: diagnostic
statut: poussé
hash: 3651c739a
fichiers:
  - docs/suivi/SUIVI_1951_diag_fe_array_to_string_lignes_brouillon.md
---

# SUIVI #1951 — DIAGNOSTIC « Array to string conversion » (dépôt 2541)

## Fiches lues

- #1948 LEÇON : lecture APBILLITEM obligatoire avant ExtBillUpdate ; LINE_NO = remplacement ; VENDORID par ligne. Introduit `lignesBrouillon` + `alignerLignesAvecBrouillon` — chemin du crash.
- #1947 : VENDORID ligne manquant ; brouillon reste Draft si rejet Sage.
- #1853 : jamais second bill / paiement FE.

## PROMPT ENVOYÉ

PHASE 1 diagnostic. Après #1948 : toast « Comptabilisation : Array to string conversion » sur dépôt 2541. Localiser cast tableau→chaîne ; avant/après envoi Sage ; tinker lecture seule ; pourquoi tests #1948 ; proposition log + PHASE 2. Aucun envoi Sage.

## SYNTHÈSE

### 1. Cause (fichiers:lignes) — très probable, reproduite hors Sage

Chaîne d’appel :

1. `ComptabilisationController::envoyerIntacctCompta` → `executerComptabilisationApbill`
2. `:949` `envoyerDepotApbillEtPaiementIntacct` → `:2022-2023` court-circuit FE → `completerBrouillonElectronique`
3. `:1933` lecture brouillon ; `:1934` `lireLignesBrouillonElectronique` → `FactureElectroniqueSage::lignesBrouillon:42-70`
4. `:1935-1939` **`FactureElectroniqueComptabilisation::alignerLignesAvecBrouillon:47-98`** ← **crash**
5. (non atteint) `controlerAvantEnvoi` → `envoyer` → `IntacctService::updateApbillBrouillonElectronique:2568` → `ExtBillUpdate::fonction`

Mécanisme :

- `IntacctService::xmlToArray` (`:2043-2045`) = `json_decode(json_encode(SimpleXML), true)`.
- Un élément XML **vide** (`<PARENTENTRY/>`, `<VENDORID/>`, etc.) devient **`[]`** (tableau vide), pas `''`.
- Dans `alignerLignesAvecBrouillon:56` :
  `$parent = trim((string) ($ligne['PARENTENTRY'] ?? $ligne['parententry'] ?? ''));`
  → `(string) []` → **ErrorException « Array to string conversion »** (handler Laravel).

Preuve locale (sans réseau) : XML `<APBILLITEM><LINE_NO>1</LINE_NO><PARENTENTRY/><VENDORID/>…` → `PARENTENTRY` et `VENDORID` de type `array` ; le cast lève exactement le message prod.

Pistes secondaires (même famille, non nécessaires pour expliquer 2541) :

- `ExtBillUpdate::estLigneTaxeGeneree:75` : même cast `(string)` sur `PARENTENTRY` des lineSpecs (ERP, en pratique scalaires).
- `ExtBillUpdate::ligne:110` : cast des dimensions lineSpecs.
- `ComptabilisationController:1788` : filtre PARENTENTRY sur lineSpecs ERP (scalaires).
- Forme « une seule ligne = objet » : déjà gérée par `lignesObjetApbillItem` + `normaliser:171-181` (objet → `[objet]`). **Pas** la cause du toast (sinon liste vide / mauvais alignement, pas « Array to string »).
- Ligne exonérée : pas de TAXENTRIES / PARENTENTRY renseigné côté Sage → champs vides → `[]` → déclenche le cast. Cas « une ligne HT exonérée » = scénario le plus exposé.

### 2. Avant ou après l’envoi à Sage ?

**AVANT** l’écriture Sage (preuve code) :

- L’ordre dans `completerBrouillonElectronique` place `alignerLignesAvecBrouillon` **avant** `envoyer` / `updateApbillBrouillonElectronique`.
- Le log prod `Erreur comptabilisation: Array to string conversion` vient de `:961-962` (`catch (\Exception)`), **pas** du `catch (IntacctException)` (`:952`) qui couvrirait un rejet API après envoi.
- Donc **aucun** `update`/`Submit` n’a été construit/envoyé : brouillon 2541 doit être **encore Draft**, lignes inchangées (pas de doublement LINE_NO).

Attention historique ERP : `:939-946` journalise `intacct_envoyee` **avant** le `try` d’envoi — trace trompeuse même si Sage n’a rien reçu. Vérifier côté Sage l’état Draft du RECORDNO lié à `depot_factures.intacct_brouillon_recordno` (≠ id dépôt 2541).

### 3. Tinker reproduction LECTURE SEULE (affiche la pile)

À lancer sur prod / staging avec accès Intacct lecture. **N’appelle pas** `envoyer` / `updateApbillBrouillonElectronique`.

```php
// php artisan tinker — SUIVI #1951 — lecture seule dépôt 2541
use App\Models\DepotFacture;
use App\Services\FactureElectronique\FactureElectroniqueSage;
use App\Services\FactureElectronique\FactureElectroniqueComptabilisation;
use App\Intacct\Functions\AccountsPayable\ExtBillUpdate;
use App\Services\IntacctService;

$depot = DepotFacture::query()->findOrFail(2541);
echo "FE=".((int)$depot->facture_electronique)
  ." recordno={$depot->intacct_brouillon_recordno}"
  ." entite={$depot->intacct_brouillon_entite}"
  ." bill_id=".($depot->intacct_bill_id ?? 'null')
  ." statut={$depot->statut}\n";

$sage = new FactureElectroniqueSage(app(IntacctService::class));
$svc = new FactureElectroniqueComptabilisation;

try {
    $brouillon = $sage->brouillon(
        trim((string)$depot->intacct_brouillon_entite),
        trim((string)$depot->intacct_brouillon_recordno)
    );
    echo "STATE=".($svc->champ($brouillon ?? [], 'STATE'))."\n";

    $lignes = $sage->lignesBrouillon(
        trim((string)$depot->intacct_brouillon_entite),
        trim((string)$depot->intacct_brouillon_recordno)
    );
    echo "nb_lignes=".count($lignes)."\n";
    foreach ($lignes as $i => $l) {
        foreach (['LINE_NO','PARENTENTRY','VENDORID','LOCATIONID','ACCOUNTNO','TRX_AMOUNT'] as $k) {
            $v = $l[$k] ?? $l[strtolower($k)] ?? null;
            $t = gettype($v);
            $preview = is_scalar($v) ? (string)$v : json_encode($v);
            echo "  [$i].$k type=$t value=$preview\n";
        }
    }

    // Reproduit le crash #1948 sans envoi :
    $align = $svc->alignerLignesAvecBrouillon(
        [['accountno' => '627800', 'trx_amount' => '58.26', 'location_id' => 'E-04']],
        $lignes,
        'DUMMY-VENDOR'
    );
    echo "align OK, n=".count($align)."\n";
    $xml = ExtBillUpdate::fonction([
        'control_id' => 'diag1951',
        'recordno' => trim((string)$depot->intacct_brouillon_recordno),
        'vendor_id' => 'DUMMY-VENDOR',
        'when_due' => '10/31/2026',
        'lines' => $align,
    ]);
    echo "xml_len=".strlen($xml)." (NON ENVOYÉ)\n";
} catch (Throwable $e) {
    echo get_class($e).': '.$e->getMessage()."\n";
    echo $e->getFile().':'.$e->getLine()."\n";
    echo $e->getTraceAsString()."\n";
}
```

Attendu : pile pointant `FactureElectroniqueComptabilisation.php` ligne du `(string)` sur `PARENTENTRY` (≈56), types `array` pour champs vides.

### 4. Pourquoi les tests #1948 ne l’ont pas vu

- `aligner_lignes_assigne_line_no…` passe `PARENTENTRY => ''` / `'1'` (**strings**), jamais `[]`.
- `xml_update_pose_vendorid…` : dimensions scalaires ; 2 lignes dont une avec tax_entries EXO — **pas** de fixture « réponse Sage xmlToArray ».
- Aucun test « **une seule** APBILLITEM avec champs vides → `[]` ».
- Aucun test d’intégration `lignesBrouillon` mockée avec payload json_encode(SimpleXML).
- Cas exonéré prod = champs PARENTENTRY/VENDORID absents côté Sage → path non couvert.

### 5. Journalisation — proposition (PHASE 2)

Dans `ComptabilisationController` `:961-962` (et miroir `:754-755` si pertinent) :

- Remplacer `catch (\Exception)` par `catch (\Throwable)`.
- Logger : `'exception' => $e` (Laravel sérialise classe, message, **file:line**, trace) + contexte `depot_id`, `facture_electronique`, `intacct_brouillon_recordno`, étape (`aligner` / `envoyer` si balisée).
- Ne plus concaténer seulement `$e->getMessage()` sans pile — c’est ce qui a masqué #1951.

## PROPOSITION PHASE 2 (ne pas coder ici)

1. **Correctif** : helper scalaire (réutiliser l’esprit de `probeScalarAsString` / `champ`) pour lire champs Sage : si `is_array` et vide → `''` ; sinon premier scalaire / json. Appliquer dans `alignerLignesAvecBrouillon` (PARENTENTRY, LINE_NO) et `ExtBillUpdate::estLigneTaxeGeneree` / casts dimensions.
2. **Tests** : (a) une ligne Sage avec `PARENTENTRY => []`, `VENDORID => []`, `LINE_NO => '1'` → aligner OK + XML avec LINE_NO+VENDORID ; (b) ligne exonérée sans tax_entries ; (c) plusieurs lignes mixtes (dont une taxe PARENTENTRY string) ; (d) régression #1948 multi-lignes.
3. **Log** : pile complète comme ci-dessus.
4. **Garde-fous** : aucun second bill ; LINE_NO remplacement ; pas APPYMT ; create classique inchangé ; CIE intact ; pas d’envoi dans les tests unitaires.

## DÉPLOIEMENT-TEST

Diagnostic only. Exécuter le tinker lecture seule ; confirmer STATE=Draft et types `array` sur champs vides ; ne pas Comptabiliser avant correctif #1952.

## LEÇON

`json_encode(SimpleXML)` transforme les balises vides Sage en `[]`. Tout `(string)` sur un champ APBILLITEM non normalisé est une ErrorException silencieuse (message seul dans le log). #1948 a ajouté la lecture des lignes sans sanitizer scalaire — régression latente dès la première ligne exonérée / champ vide.
