---
suivi: 1467
date: 2026-09-07
sujet: Diagnostic ouverture destination PROJETS — restitution #1443 + cadrage
chantier: facturation dossiers fonciers / dépôt / PROJETS
type: diagnostic
statut: poussé
hash: 14cf1d22
fichiers:
  - docs/suivi/SUIVI_1467_diag_ouverture_destination_projets.md
---

# SUIVI #1467 — PHASE 1 DIAGNOSTIC — ouverture destination PROJETS

## BASE GIT

```
git log --oneline -1 origin/main
ade0e501 chore(suivi): hash fiche #1466
```

(attendu prompt : 51eb6e30 #1465 — main a avancé avec #1466.)

MySQL agent : refusé. Aucun chiffre inventé. Aucune modification de code
applicatif.

## FICHES / EMPLACEMENTS CHERCHÉS POUR #1443

| Emplacement | Résultat |
|-------------|----------|
| `docs/suivi/SUIVI_1443*` | **0 fichier** |
| `ls docs/suivi/` autour de 144x | 1440, 1441, 1442, **1444**, 1445… — **trou 1443** |
| `git log --all --full-history -- '**/SUIVI_1443*'` | vide |
| `git log --all --grep=1443` | vide |
| `gh api search/commits?q=repo:HECTAREG/erp-immo+1443` | `total_count: 0` |
| `docs/foncier/`, `docs/*.md`, `AUDIT_PRESTATION.md` | rien sur #1443 |
| `PROJECT.md` du dépôt | pas de section #1443 ; pas de claim STORED sur `postes_budgetaires` |
| Références croisées | #1445 et #1448 citent `#1443` comme diagnostic acquis |

## CLAUSE D'ARRÊT — FICHE #1443 INTROUVABLE

**ARRÊT partiel sur A1** : la restitution littérale intégrale est **impossible**.
Le numéro #1443 a été **consommé** (référencé par #1445 / #1448) mais **jamais
commité** (ou perdu hors dépôt).

**Ne pas confondre** avec une absence de chantier : les lots #1445 et #1448
existent et citent explicitement « premier / deuxième lot du chantier
facturation des dossiers fonciers (#1443) ».

---

# EN TÊTE — RESTITUTION #1443

## A1. Restitution littérale

```
INTROUVABLE.

Chemin attendu : docs/suivi/SUIVI_1443_*.md
État dans origin/main (ade0e501) : aucun fichier correspondant.
Aucun historique git, aucun commit GitHub indexé contenant « 1443 ».

Impossible de coller le contenu littéral.
```

## A2–A4. Reconstruction à partir des lots aval (≠ restitution)

Sources uniquement : `docs/suivi/SUIVI_1445_…`, `SUIVI_1448_…`, code actuel.
**Ce n’est pas l’étude #1443** — décisions **inférées** des lots qui s’en
réclament.

### A2. Décisions relatives à PROJETS (inférées)

| Décision | Statut | Source |
|----------|--------|--------|
| Ne pas ouvrir PROJETS dans #1445 | TRANCHÉ (lot) | SUIVI_1445 L28–30 |
| Ne pas toucher dépôt ni comptabilisation dans #1445 | TRANCHÉ | idem |
| Classe Sage = `intacct_class_analytics_id` (pas legacy seul) | TRANCHÉ (#1445) | SUIVI_1445 ; #1443 cité « diagnostic CLASSID » L41 |
| Postes budgétaires sur dossier, même table | TRANCHÉ (#1448) | SUIVI_1448 option a |
| Destination PROJETS fermée pendant #1448 | TRANCHÉ | SUIVI_1448 L34–35 |
| Pas de flux comptable dans #1448 | TRANCHÉ | idem |
| Types 119/120 sans compte : avertir sans bloquer | TRANCHÉ (#1448) | SUIVI_1448 L105–106 |
| Colonne `poste_budgetaire_id` / `dossier_foncier_id` sur dépôts = **futures** | EN ATTENTE | SUIVI_1448 L106–108 |
| Ouverture PROJETS (dégrisage + flux) | EN ATTENTE | UI « À venir » + Rule::in sans PROJETS |

### A3. Séquence planifiée (inférée)

1. **#1443** — diagnostic (fiche absente)
2. **#1445** — classe Sage dossier
3. **#1448** — postes budgétaires + onglet Facturation
4. **Lot(s) suivant(s)** — ouverture PROJETS / rattachement dépôt (annoncé
   mais non numéroté dans les fiches disponibles)

### A4. Règle compte comptable dossier (mot pour mot dans les fiches dispo)

**Aucune** règle mot-à-mot « compte = … pour facture dossier » dans #1445 /
#1448. #1448 dit seulement (L105–106) :

> Avertissement si `compte_comptable_id` NULL (119/120 inclus, pas de
> blocage).

La décision Robin #1467 (« s’il n’y a pas de code comptable, les comptables le
modifient à la main ») **n’est pas contredite** par ces fiches (elles ne
tranchent pas le contraire).

---

# VERDICT SCHÉMA — STORED vs deux UNIQUE

**Code = foi. #1448 a raison. Pas de colonne générée STORED.**

Migration réelle
`database/migrations/2026_09_04_144800_add_dossier_foncier_id_to_postes_budgetaires.php` :

- L31–35 (commentaire) : « Unique historique … CONSERVÉ … Pas de colonne
  générée STORED »
- L55 : `$table->unique(['dossier_foncier_id', 'poste_type_id'], 'pb_dossier_type_uq');`
- L63 : `MODIFY programme_id … NULL`
- L72–77 : CHECK `pb_parent_xor`
- **Aucun** `GENERATED … STORED` dans le fichier

`PROJECT.md` du dépôt **ne contient pas** le claim « colonne générée STORED »
sur `postes_budgetaires` (recherche `STORED` / `1443` : rien). Si PROJECT_v72
externe l’affirme, il est **périmé / faux** pour cette table.

### Requête confirmation prod

```sql
SHOW CREATE TABLE postes_budgetaires\G
```

Attendu (relecture statique migration) : index unique historique
`(programme_id, poste_type_id)` + `pb_dossier_type_uq (dossier_foncier_id,
poste_type_id)` + CHECK `pb_parent_xor` ; **pas** de colonne STORED dédiée
à l’unicité dossier.

---

# B. VERROU DU COMPTE 627000

## B1. Classement dépôt sans programme → FRAIS GÉNÉRAUX

Pas un seul « if sans programme alors FG » serveur à l’enregistrement (#1450
retire le défaut PROGRAMMES).

Nature métier **après** facture :

`app/Support/FactureEstFraisGeneraux.php` L13–16 :

```
return $facture instanceof Facture && $facture->programme_id === null;
```

Liste à comptabiliser — filtre FG :

`app/Support/ComptabilisationListeFilter.php` L60 :

```
self::NATURE_FRAIS_GENERAUX => $query->whereNull('depot_factures.programme_id'),
```

Destination **explicite** à l’envoi (sinon erreur) :

`DepotFactureController.php` L994 + L5062–5069.

## B2. Où 627000 est appliqué

Constante PHP : `CompteChargePropositionResolver::COMPTE_FG_DEFAUT = '627000'`
(`app/Support/CompteChargePropositionResolver.php` L15) — **non appelée**
dans un parcours serveur de proposition FG aujourd’hui (`numeroPourProgrammes`
seulement).

Application réelle = **front** :

`DepotFactureSaisieForm.vue` L5734–5753 `numeroCompteChargeSelonDestination()` :
si destination ≠ PROGRAMMES, patterns libellé catégorie, sinon **`return '627000'`**.

## B3. Si on dégrise PROJETS sans autre changement — parcours de rupture

1. **UI** : bouton encore `disabled` L8450–8457 — il faudrait lever le disabled.
2. **Enregistrement / envoi** : `Rule::in(['PROGRAMMES','FRAIS_GENERAUX'])`
   (L869, L994, etc.) → requête avec `destination=PROJETS` **refusée**
   (preuve : `tests/Unit/DepotDestinationProjetsRefuseTest.php`).
3. **assertRattachementBudgetCompletPourEnvoi** L5066–5069 : si destination ≠
   PROGRAMMES et ≠ FG → ValidationException.
4. **Colonnes** `dossier_foncier_id` / `poste_budgetaire_id` absentes sur
   `depot_factures` / `factures` → aucun rattachement dossier persistable.
5. **NV1/NV2** : si un dépôt passait sans programme_id → traité comme FG
   (`FactureEstFraisGeneraux`) → NV1 seul, listes FG.
6. **Comptabilisation CLASSID** : `resolveProgrammeClassIdForApbill` (L75–88)
   exige un `Programme` — **aucune** lecture de
   `DossierFoncier.intacct_class_analytics_id`.
7. **Écriture Sage** : compte charge proposé via front (627000 si non
   PROGRAMMES) sauf édition manuelle ; CLASSID programme/categorie seulement.

**Conclusion** : dégriser seul = soit impossible (validation), soit pollution FG
sans CLASSID dossier.

## B4. Apparition dans listes FG

Oui **si** `programme_id` NULL : filtre nature FG = `whereNull(programme_id)`.
Écrans : liste « À comptabiliser » (`ComptabilisationController` +
`ComptabilisationListeFilter`), et tout écran s’appuyant sur
`FactureEstFraisGeneraux`.

### Requête volume FG (Robin)

```sql
SELECT
  COUNT(*) AS n_depots_sans_programme,
  SUM(CASE WHEN statut = 'a_comptabiliser' THEN 1 ELSE 0 END) AS n_a_comptabiliser,
  SUM(CASE WHEN statut IN ('en_validation_nv1','en_validation_nv2') THEN 1 ELSE 0 END) AS n_en_validation
FROM depot_factures
WHERE deleted_at IS NULL
  AND programme_id IS NULL;
```

(Ne pas inventer le résultat.)

## B5. CLASSID dossier — stocké ou consommé ?

**Stocké + affiché + audité** (#1445) sur `dossiers_fonciers.intacct_class_analytics_id`.

**Non consommé** dans le flux dépôt / comptabilisation / APBILL :

- `ComptabilisationController::resolveProgrammeClassIdForApbill` L75–88 :
  `Programme` analytics → legacy → saisie — **pas** `DossierFoncier`.
- Aucun usage `DossierFoncier` / `dossier_foncier` dans
  `ComptabilisationController` pour CLASSID.

**Déterminant** : ouvrir PROJETS **exige** brancher le CLASSID dossier dans
la comptabilisation (sinon écritures sans classe ou classe catégorie FG).

---

# C. CE QUE #1448 A PRÉPARÉ

## C1. Colonnes futures sur factures / dépôts

**N’existent pas** aujourd’hui sur `depot_factures` ni `factures`
(`rg` modèles + migrations : pas de `dossier_foncier_id` /
`poste_budgetaire_id` sur ces tables).

`depot_factures` a `poste_budgetaire_type_id` + `programme_budget_id` (types /
enveloppes programme), pas l’id de ligne `postes_budgetaires`.

Garde-fou anticipatif : `DossierFoncierPosteBudgetaireService::posteUtiliseParFacture`
L256–270 — `Schema::hasColumn(...)` ; retourne `false` tant que colonnes
absentes.

## C2. Scope `PosteBudgetaireProgrammeScope`

`app/Models/Scopes/PosteBudgetaireProgrammeScope.php` L20–23 :
`whereNotNull(...programme_id)`.

Suffisant pour **isoler** les lecteurs Eloquent historiques (dépôt build postes
programme). **Insuffisant** dès que le dépôt doit lister les postes d’un
dossier : il faudra `withoutGlobalScope` / `queryPourDossier()` (déjà prévu
côté service dossier) **et** un chemin UI dépôt dédié PROJETS.

## C3. Bascule dossier → programme

SQL #1448 :

```
UPDATE postes_budgetaires
SET programme_id = :id_programme, dossier_foncier_id = NULL
WHERE dossier_foncier_id = :id_dossier;
```

**Aujourd’hui** (pas de FK facture → poste ligne) : bascule **sûre** pour les
ids de postes (mêmes ids → bilan).

**Dès** que `depot_factures.poste_budgetaire_id` (ou équivalent) existe :
bascule **conserve** l’id → factures restent pointées ; mais `programme_id`
du dépôt / nature FG / CLASSID devront être **réalignés** séparément. Sans
ces colonnes, la bascule ne « casse » pas de factures (il n’y en a pas
rattachées).

---

# D. TYPES SANS COMPTE — DÉCISION ROBIN

## D1 / D2. Compte modifiable à la main ?

**Oui**, dans l’écran de comptabilisation :

- Accès : `ComptabilisationController::ensureComptabilisationUser` L96–100 —
  groupes `GS_COMPTABILITE`, `GS_ADMIN_ERP`, `GS_DSI`
  (⚠️ utilise `hasAnyM365Group` — faux positif ADMIN_ERP ; pour un test de
  refus réel, utiliser un compte COMPTABILITE non admin).
- UI : `Factures/Comptabilisation/Index.vue` embarque
  `DepotFactureSaisieForm` en mode compta ; champ compte = AutoComplete sur
  lignes d’écritures (`compte_selection` / comptes_comptables).
- La proposition 627000 / poste / 471000 est un **préremplissage** éditable
  avant envoi Sage.

Donc la décision Robin (« comptables modifient à la main ») est
**applicable** en l’état pour le compte de charge. **Pas un bloquant** pour
l’ouverture, sous réserve que la proposition initiale puisse être vide ou
627000 et corrigée.

### Requête types actifs sans compte (toutes enseignes)

```sql
SELECT
  pbt.id,
  pbt.code_interne,
  pbt.libelle,
  e.code AS enseigne_code,
  pbt.compte_comptable_id,
  pbt.actif
FROM postes_budgetaires_types pbt
LEFT JOIN enseignes e ON e.id = pbt.enseigne_id
WHERE pbt.actif = 1
  AND pbt.compte_comptable_id IS NULL
ORDER BY e.code, pbt.code_interne, pbt.id;
```

(Vérifier noms de colonnes réels via `SHOW COLUMNS FROM postes_budgetaires_types`
si divergence — ne pas inventer.)

---

# E. PÉRIMÈTRE D’OUVERTURE — CADRAGE

## E1. Écrans / services touchés (exhaustif minimal)

| Zone | Chemin |
|------|--------|
| Validation destination | `DepotFactureController` (Rule::in × plusieurs) |
| Assert envoi | `assertRattachementBudgetCompletPourEnvoi` |
| UI dépôt | `DepotFactureSaisieForm.vue` (bouton PROJETS + étapes) |
| Schéma dépôt/facture | migrations `dossier_foncier_id`, éventuellement `poste_budgetaire_id` |
| Modèles | `DepotFacture`, `Facture` |
| Postes dossier | `DossierFoncierPosteBudgetaireService`, scope |
| Panel dossier | `DossierFacturationPanel.vue` |
| Comptabilisation CLASSID | `ComptabilisationController::resolveProgrammeClassIdForApbill` (+ branche dossier) |
| Proposition compte | front `numeroCompteChargeSelonDestination` (+ éventuellement resolver PHP) |
| Nature FG | `FactureEstFraisGeneraux`, `ComptabilisationListeFilter` |
| OCR | `DepotFactureClaudeAnalysisService` (normalizeDestination) |
| Paiement | déjà tolère PROJETS (`PaiementFondsPropres`) — vérifier cohérence |
| Tests | `DepotDestinationProjetsRefuseTest`, #1450 source tests |

## E2. Découpage proposé (étapes indépendantes)

| Étape | Permet | Laisse fermé | Risque |
|-------|--------|--------------|--------|
| **E2.1 Schéma** : colonnes `depot_factures.dossier_foncier_id` (+ `poste_budgetaire_id` si retenu) | Persistance rattachement | UI PROJETS | Moyen (migration) |
| **E2.2 Règles dépôt** : Rule::in + assert + UI dégrisée + sélection dossier/poste | Saisir et envoyer un dépôt PROJETS | CLASSID Sage dossier | **Haut** (pollution FG si programme_id mal géré) |
| **E2.3 Comptabilisation** : CLASSID depuis `DossierFoncier.intacct_class_analytics_id` ; exclure des listes FG | Écritures Sage correctes | — | **Haut** (ComptabilisationController) |
| **E2.4 Liste factures dossier** | Panel #1448 vivant | — | Bas |

**Clause d’arrêt future** : l’étape E2.3 **touche** `ComptabilisationController`
/ flux Sage — maîtrisable **si** branchée comme PROGRAMMES (même helper,
autre source). Sinon lot non maîtrisable → ARRÊT.

## E3. Arbitrages restants

### Pour Robin

1. Retrouver / republier la fiche #1443 (ou confirmer qu’elle n’a jamais été
   poussée) — **bloquant documentaire**.
2. Ordre des étapes E2.1 → E2.3 : un seul lot vs plusieurs.
3. Faut-il `poste_budgetaire_id` (ligne) en plus de `poste_budgetaire_type_id`
   + `dossier_foncier_id` ?
4. Comportement si dossier **sans** classe Sage à la comptabilisation
   (bloquer / permettre saisie / défaut) — analytics absente aujourd’hui non
   consommée.
5. Homonymie / libellé UI « PROJETS » vs « dossier foncier ».

### Pour la compta (Sabrina / Rudy)

1. Confirmer le workflow : dépôt PROJETS → NV1(/NV2 ?) → compta → Sage.
2. Compte par défaut acceptable quand type sans compte (627000 vs 471000 vs
   vide obligatoire).
3. Mesurer volume FG actuel (requête B4) pour estimer le risque de pollution.
4. Valider CLASSID dossier : qui le renseigne (#1445) avant première facture.

**Ne pas rouvrir** : structure postes même table (#1448) ; analytics vs legacy
CLASSID (#1445) ; types sans compte = non bloquant (Robin #1467).

## E4. Risques (étude inférée + code)

| Sujet | Niveau | Motif |
|-------|--------|-------|
| Pollution listes FG | **Haut** | `programme_id` NULL = FG |
| CLASSID manquant APBILL | **Haut** | dossier non branché |
| Rule::in / assert oubliés | Moyen | PROJETS déjà refusé partout |
| Scope postes | Moyen | fantôme programme_id=0 si scope levé mal |
| Compte 627000 | Moyen / accepté Robin | éditable en compta |
| Bascule bilan | Bas aujourd’hui | pas de FK facture→poste |

---

## DÉPLOIEMENT

Aucun. Diagnostic seul. Fiche `docs/suivi/SUIVI_1467_…` uniquement.

## LEÇON

Un numéro SUIVI référencé par des lots aval peut être **absent du dépôt** :
toujours vérifier `ls` + historique git avant de promettre une restitution
littérale. Le code migration bat PROJECT.md. Dégriser PROJETS sans schéma ni
CLASSID = pollution FG garantie.
