---
suivi: 1769
date: 2026-10-01
sujet: Diagnostic HT à régler et HT brut
chantier: factures / comptabilisation
type: diagnostic
statut: poussé
hash:
fichiers:
  - docs/suivi/SUIVI_1769_diag_montant_brut_net_retenues.md
---

# SUIVI #1769 — diagnostic : la saisie stocke le HT à régler, Sage et le décompte parlent du brut

Phase 1. Aucun correctif. La modification de Robin PORTALES du 01/10/2026 sur la facture 1691 est postérieure à la comptabilisation : elle ne fait pas partie du circuit. Elle confirme que 19 988,88 € validé en NV1 / NV2 était le HT à régler, et que 21 609,60 € est le brut déduit des retenues (RG 5 % + CP 2,5 %).

## PROMPT ENVOYÉ

Savoir si `depot_factures.montant_ht` est le brut ou le HT à régler, ce que l’écran de comptabilisation attend, ce que l’APBILL envoie, et ce que voient NV1, les listes, le marché et le bilan. Classer les 24 factures corrigées : écart = facteur de retenue, ou écart quelconque.

## FICHES LUES

- **#1537 LEÇON** — l’ERP reconstitue le brut depuis le net saisi. Impact : l’hypothèse de Robin est le modèle déjà codé, pas un cas isolé.
- **#1293** — même couple de chiffres (base HT à régler, HT brut affiché à part). Impact : le brut est un affichage, pas la valeur saisie.
- **#1261** — la reconstitution est un seul calcul (TTC à régler + finitions + CIE) / (1 − RG% − CP% − OPC%). Impact : ne pas chercher un second stockage du brut.
- **#1768** — photos à partir du déploiement. Impact : le script ci-dessous s’appuie sur `facture_historique`, pas sur les photos.

## 1. Ce qui est stocké à la saisie

Le champ saisi s’appelle « Base HT à régler » (`DepotFactureSaisieForm.vue` 9760 et 9780). Il est lié à `ligne.base_ht`. Le total de ces lignes est recopié dans `form.montant_ht`, `montant_tva`, `montant_ttc` (5166-5174). Le payload envoie ces trois totaux et `lignes_tva[].base_ht` (7141-7147). Le serveur les écrit dans `depot_factures.montant_ht`, `montant_tva`, `montant_ttc` (`DepotFactureController.php` 1956-1959) et dans `ocr_raw.saisie` (1848-1850). À l’engagement, `factures.montant_ht` et `factures.montant_ttc` reçoivent les mêmes valeurs (2389-2396), et `net_a_payer_ttc` est posé égal au TTC saisi. `FactureMontantsDepotSynchronizer.php` 61-73 refait cette copie à chaque enregistrement, y compris `net_a_payer_ttc = montant_ttc`.

Donc, quand des retenues existent, les trois colonnes stockent le **à régler** (HT, TVA et TTC du net saisi), pas le brut.

Le brut n’a pas de colonne. Il est recalculé à l’affichage.

Formule, saisie (`DepotFactureSaisieForm.vue` 3530-3544) et décompte fiche (`FactureDecompteSaisiePresenter.php` 80-110) :

base TTC = (TTC à régler + finitions signées + CIE − libération RG) / (1 − RG% − CP% − OPC%)

HT brut = HT à régler × (base TTC / TTC à régler)

TVA brute = TVA à régler × le même facteur

Sans retenue, HT brut = HT à régler (4391-4396).

Facture 1691, valeurs validées : HT 19 988,88, TVA 3 997,78, TTC 23 986,66, RG 5 %, CP 2,5 %, dénominateur 0,925.

19 988,88 / 0,925 = 21 609,60. C’est le HT brut du décompte. 21 609,60 × 1,2 = 25 931,52, le TTC brut. TVA brute 4 321,92. RG affichée 1 080,48 HT et CP 540,24 HT sont ces montants TTC ramenés en HT (présentateur 115-137).

## 2. Écran de comptabilisation

C’est le même formulaire (`Factures/Comptabilisation/Index.vue` 709). Le champ éditable reste « Base HT à régler ». Le décompte à côté affiche le HT brut, non saisissable.

La compta modifie `montant_ht` parce que ce champ, et la colonne qui en dépend, portent le net, alors que le brut est ce que le décompte, la ligne de charge et plusieurs cumuls présentent comme « le montant » de la facture. Recopier 21 609,60 dans « Base HT à régler » aligne la colonne sur le brut. Ce n’est pas une ressaisie exigée par le calcul Sage : les écritures, si elles ont été générées à la saisie, ont déjà la charge au brut (point 3). L’enregistrement ne régénère pas les écritures tout seul (`tryAutoPrefillComptaLignes` 6931-6936 s’arrête s’il y a déjà des lignes ; la régénération est le bouton « générer », 6956).

## 3. Payload Sage (APBILL)

Les écritures, quand il y a des retenues (`EcrituresComptablesCalculator.php` 400-402, même formule dans `depotFactureEcrituresCalculator.js` 313-316) :

- débit du compte de charge = HT brut = base TTC / 1,2 (taux forcé à 20 %, pas le taux de la ligne) ;
- débit TVA 445 = ce HT × 20 % ;
- crédit 401000 = TTC à régler (le net saisi) ;
- crédits 401700 / 401800 / etc. = retenues en **TTC** (pourcentage de la base TTC), pas en HT.

L’APBILL (`ComptabilisationController.php` 1564-1626 et 1856-1906) :

- part de `depot.montant_ht` ;
- le remplace, en facture simple, par la somme des lignes candidates (débits de charge ; les crédits 401 sont exclus ; les 445 aussi) dès que cette somme dépasse 0,0001 (1595-1597) ;
- ajoute des lignes négatives pour chaque crédit de retenue, au montant du crédit donc en TTC (2946-2972).

Contrôle avant envoi (3163-3185) : somme algébrique des lignes = HT candidat − crédits de retenue. Il ne compare pas au HT à régler. `net_a_payer_ht` n’est pas envoyé par le formulaire ; le contrôle qui l’utilise ne part que s’il est renseigné.

Facture 1691, **sans** la modification de Rudy, écritures générées depuis 19 988,88 :

- charge 21 609,60 HT + TVA 4 321,92 ;
- retenue de garantie −1 296,58 TTC (5 % de 25 931,52) ;
- compte de prorata −648,29 TTC (2,5 %) ;
- net pièces = 25 931,52 − 1 296,58 − 648,29 = 23 986,65 (un centime sous le TTC saisi 23 986,66).

**Avec** la modification, si seules les colonnes ont bougé (c’est ce que l’historique cité décrit : `_depot.montant_ht` 19 988,88 → 21 609,60, TTC → 25 931,52, TVA → 4 321,92) et que les écritures n’ont pas été régénérées : **le XML est le même**. La somme des candidates remplace le HT de la colonne (1595-1597).

**Avec** régénération des écritures après avoir mis 21 609,60 dans « Base HT à régler » : ce brut est relu comme un net. Nouveau brut = 21 609,60 / 0,925 = 23 361,73 HT, TVA 4 672,35, TTC brut 28 034,08. Ce serait un double grossissement. Le script dit si `lignes_comptables` a changé dans la même ligne d’historique.

La modification du 01/10 qui remet 19 988,88 est après RECORDNO 2406087. Elle ne change pas ce qui est parti.

## 4. Autres écrans

| Écran | Montant montré | Cohérence avec la charge Sage (HT brut) |
|---|---|---|
| Fiche, en-tête `Show.vue` 1805-1810 | `factures.montant_ht` / `montant_ttc` = à régler, puis la valeur enregistrée par la compta | Non, tant que la colonne n’a pas été écrasée avec le brut |
| Fiche, décompte `FactureDecompteBloc.vue` 27-37 | HT brut, TVA brute, TTC brut recalculés, puis HT à régler et net TTC | Le brut affiché = la charge ; le HT à régler = la colonne d’origine |
| NV1 / NV2 | Même fiche. Liste validations : `net_a_payer_ttc` (`Validations/Index.vue` 288), égal au TTC saisi (`FactureMontantsDepotSynchronizer.php` 72) | Le net TTC validé = crédit 401, pas la charge |
| Liste factures | `depot_factures.montant_ht` (`FactureGlobaleController.php` 775) | À régler, sauf après correction compta |
| Marché, historique `MarcheSyntheseFinanciereService.php` 603-654 | `montant_ht` affiché = colonne, sauf libération de RG : alors HT de la base reconstituée (`LiberationRgFactureService.php` 61-70). `netHt` (623) = colonne − RG − CP − CIE − finitions − OPC | Sans libération, le « HT » du marché est le à régler, puis le net retranche les retenues une seconde fois. Après correction compta, la colonne est le brut et le net redevient juste. La charge Sage est le brut dans les deux cas |
| Bilan `BilanFinancierService.php` 1278 et 1329-1330 | `SUM(depot_factures.montant_ht)` après NV1 | À régler, ou brut si la compta a écrasé la colonne. Pas le HT de la ligne de charge |
| Plafond marché `MontantFactureSousTraitantService.php` 294-317 | même colonne, même exception libération RG | Le plafond « hors retenues » est comparé au à régler |

Sage encaisse un HT brut en charge et des retenues négatives en TTC. L’ERP, partout où il lit la colonne, encaisse le à régler, sauf le décompte et les écritures.

## 5. Script lecture seule

À coller à la racine du projet, lancer `php diag_1769_ecarts_retenues.php`, puis supprimer le fichier. Aucun INSERT, UPDATE, DELETE. Les 300 dernières factures sont celles qui ont une ligne d’historique `intacct_comptabilisee`, `intacct_acompte_envoye` ou `intacct_avoir_envoyee`. Pour chacune, le script prend le HT avant la première modification de `montant_ht` située **avant** cette comptabilisation, et le HT après la dernière. La modification du 01/10 sur la 1691, postérieure au RECORDNO, est ignorée. `report_retenue` = l’écart vaut le brut reconstitué à 5 centimes près. `ecart_autre` = le reste.

```php
<?php

require __DIR__.'/vendor/autoload.php';
$app = require __DIR__.'/bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();

use Illuminate\Support\Facades\DB;

$actionsCompta = ['intacct_comptabilisee', 'intacct_acompte_envoye', 'intacct_avoir_envoyee'];

$comptas = DB::table('facture_historique')
    ->whereIn('action', $actionsCompta)
    ->orderByDesc('id')
    ->get(['id', 'depot_facture_id', 'facture_id', 'action', 'created_at']);

$vus = [];
$lot = [];
foreach ($comptas as $row) {
    $id = (int) $row->depot_facture_id;
    if ($id <= 0 || isset($vus[$id])) {
        continue;
    }
    $vus[$id] = true;
    $lot[] = $row;
    if (count($lot) >= 300) {
        break;
    }
}

$ids = array_map(static fn ($r) => (int) $r->depot_facture_id, $lot);
$depots = DB::table('depot_factures')
    ->whereIn('id', $ids)
    ->get(['id', 'facture_id', 'numero_facture', 'montant_ht', 'ocr_raw'])
    ->keyBy('id');

$mods = DB::table('facture_historique')
    ->whereIn('depot_facture_id', $ids)
    ->where('action', 'modifie')
    ->orderBy('id')
    ->get(['id', 'depot_facture_id', 'created_at', 'champs_modifies', 'commentaire']);

$parDepot = [];
foreach ($mods as $m) {
    $parDepot[(int) $m->depot_facture_id][] = $m;
}

function nombre(?array $bloc, string $cle): ?float
{
    if (! is_array($bloc) || ! array_key_exists($cle, $bloc) || ! is_numeric($bloc[$cle])) {
        return null;
    }

    return round((float) $bloc[$cle], 2);
}

function brutAttendu(float $ht, float $tva, array $saisie): ?float
{
    $ttc = round($ht + $tva, 2);
    if (abs($ttc) < 0.009) {
        return null;
    }
    $rg = (float) ($saisie['retenue_garantie_pct'] ?? 0);
    $cp = (float) ($saisie['compte_prorata_pct'] ?? 0);
    $opc = (float) ($saisie['retenue_opc_pct'] ?? 0);
    $fin = (float) ($saisie['retenue_finitions_montant'] ?? 0);
    $cie = (float) ($saisie['montant_cie'] ?? 0);
    $lib = (float) ($saisie['liberation_rg_montant_ttc'] ?? 0);
    $den = 1 - ($rg + $cp + $opc) / 100;
    if ($den < 0.0001) {
        return null;
    }
    $base = round(($ttc + $fin + $cie - $lib) / $den, 2);
    $facteur = $base / $ttc;

    return round($ht * $facteur, 2);
}

$report = 0;
$autre = 0;
echo "depot_id;facture_id;numero;ht_avant;ht_apres;ht_brut_attendu;classe;lignes_comptables_touchees;rg;cp;opc\n";

foreach ($lot as $compta) {
    $depotId = (int) $compta->depot_facture_id;
    $quand = (string) $compta->created_at;
    $avant = null;
    $apres = null;
    $tvaAvant = null;
    $lignesTouchees = 'non';
    foreach ($parDepot[$depotId] ?? [] as $m) {
        if ((string) $m->created_at > $quand) {
            continue;
        }
        $json = json_decode((string) $m->champs_modifies, true);
        if (! is_array($json)) {
            continue;
        }
        $a = $json['avant']['_depot'] ?? $json['avant'] ?? [];
        $b = $json['apres']['_depot'] ?? $json['apres'] ?? [];
        $ha = nombre(is_array($a) ? $a : [], 'montant_ht');
        $hb = nombre(is_array($b) ? $b : [], 'montant_ht');
        if ($ha === null || $hb === null || abs($ha - $hb) < 0.009) {
            continue;
        }
        if ($avant === null) {
            $avant = $ha;
            $tvaAvant = nombre(is_array($a) ? $a : [], 'montant_tva');
        }
        $apres = $hb;
        if (isset($json['avant']['lignes_comptables']) || isset($json['apres']['lignes_comptables'])) {
            $lignesTouchees = 'oui';
        }
    }
    if ($avant === null || $apres === null) {
        continue;
    }
    $depot = $depots->get($depotId);
    $ocr = json_decode((string) ($depot->ocr_raw ?? ''), true);
    $saisie = is_array($ocr['saisie'] ?? null) ? $ocr['saisie'] : [];
    $tva = $tvaAvant ?? 0.0;
    $attendu = brutAttendu($avant, $tva, $saisie);
    $classe = ($attendu !== null && abs($apres - $attendu) <= 0.05) ? 'report_retenue' : 'ecart_autre';
    if ($classe === 'report_retenue') {
        $report++;
    } else {
        $autre++;
    }
    echo implode(';', [
        $depotId,
        (int) ($compta->facture_id ?? 0),
        (string) ($depot->numero_facture ?? ''),
        number_format($avant, 2, '.', ''),
        number_format($apres, 2, '.', ''),
        $attendu === null ? '' : number_format($attendu, 2, '.', ''),
        $classe,
        $lignesTouchees,
        (string) ($saisie['retenue_garantie_pct'] ?? ''),
        (string) ($saisie['compte_prorata_pct'] ?? ''),
        (string) ($saisie['retenue_opc_pct'] ?? ''),
    ])."\n";
}

echo "report_retenue={$report} ecart_autre={$autre}\n";
```

## PROPOSITIONS (non implémentées)

Règle unique :

- saisi et stocké dans `depot_factures.montant_ht` / `montant_tva` / `montant_ttc` et `factures` : le **à régler** ;
- libellé d’écran : « HT à régler », jamais « Montant HT » seul ;
- brut : calculé, affiché comme « HT brut », et, si on le persiste un jour, dans un champ distinct ;
- envoyé à Sage : ligne de charge = HT brut, TVA = TVA brute, lignes de retenue négatives = montants TTC des écritures (déjà le cas), crédit fournisseur = TTC à régler ;
- bilan, liste et marché « facturé » : dire lequel des deux ils cumulent. Aujourd’hui ils cumulent la colonne, donc le à régler, puis le marché retranche encore les retenues.

Correctif pour que la compta n’ait plus à recopier le brut : ne pas faire de la colonne le montant de la charge. Afficher le HT brut en lecture seule sur l’écran de comptabilisation, laisser le à régler intact, et faire lire au bilan et au marché le brut reconstitué (ou le débit de charge) quand ils parlent de facturé. L’APBILL n’a pas besoin de cette recopie tant que les écritures n’ont pas été régénérées après coup.

Factures déjà comptabilisées : aucune écriture Sage, aucune réécriture des colonnes dans ce lot. Le script constate. La 1691 a le brut en Sage (charge 21 609,60) ; la remise à 19 988,88 du 01/10 ne concerne que l’ERP.

## DÉPLOIEMENT-TEST

Aucun déploiement applicatif. Fiche seule.

## LEÇON

Le net saisi est la colonne. Le brut est un affichage et le débit de charge. Les écraser l’un sur l’autre pour « faire coller Sage » change le bilan et le marché, pas l’APBILL, sauf si les écritures sont régénérées — et là le brut est grossi une seconde fois.
