[DTP-Worker] Sprint 8 · buffer · Fiscal/e-CF DGII : gate d'IDENTITÉ des ÉVÈNEMENTS D'ÉMISSION (« le cœur du livrable ») — ENSEMBLE émetteurs {réservation,contrat} ⨯ DISJONCTION des brouillons (jamais de facture sur brouillon) ⨯ base_field ⨯ rôle unique compta-fiscaliste-ecf double-ancré RBAC portail compta ⨯ FormaPago défaut 3/Tarjeta/Cardnet #10, recomputés d'ecf_plan.json

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Code DTP Worker
2026-08-01 14:14:18 +00:00
parent 915be3cb77
commit 8667cdf4de
3 changed files with 250 additions and 1 deletions
+160
View File
@@ -5557,6 +5557,166 @@ if _rp_src is not None:
bad(f"RunPod endpoint · AUCUNE fiche/master prompt ne mentionne RunPod — "
f"la surface d'identité de l'endpoint GPU s'est évaporée (régression #6)")
# ============================================================================
# Fiscal · e-CF DGII — IDENTITÉ des ÉVÈNEMENTS D'ÉMISSION (le « cœur du livrable »)
# ----------------------------------------------------------------------------
# La section §Cross-cohérence du README (05_deliverables_mvp/fiscal/ecf_dgii/
# README.md:40-59) — présentée comme « le cœur du livrable » — transcrit À LA MAIN
# l'IDENTITÉ des emission_events d'out/ecf_plan.json (byte-gaté par check_artifacts
# ⇒ artefact faisant autorité) :
# • L48 l'ENSEMBLE des évènements qui émettent un e-CF (« seulement réservation
# et contrat ») + l'ENSEMBLE exclu « on ne facture pas un brouillon
# (lead/visite/devis) » — la RAISON D'ÊTRE du module : jamais de facture
# sur un brouillon (états SOUMIS uniquement · doc_status=1)
# • L50 l'ENSEMBLE des base_field (« montant_reservation, montant_contrat »)
# • L53 le rôle UNIQUE (« compta-fiscaliste-ecf » · « OTO Compta Fiscaliste
# eCF » · portail compta)
# • L59 la FormaPago défaut (« = 3 (Tarjeta) » · Cardnet #10)
# Le SEUL gate fiscal existant couvre le CONTRAT DE FORMAT e-NCF + le compte de
# tests ; le bloc racine, le seul agrégat « e-CF 39 tests ». RIEN ne confronte
# cette identité à l'artefact. Dérive la plus grave, #6/#5 : AJOUTER un
# emission_event sur un état BROUILLON (ex. `devis`) — l'anti-pattern EXACT que le
# module interdit (« on ne facture pas un brouillon ») — pendant que L48 dit encore
# « seulement réservation et contrat » → l'agent ERPNext Backend câblerait une
# facturation sur brouillon (e-CF émis avant soumission/encaissement). Aucune suite
# tests/ (FONCTIONS de résolution/composition, jamais la prose vs l'artefact) ne
# l'attrape — classe « vert trompeur » notée en mémoire.
#
# On RECOMPUTE les ENSEMBLES depuis ecf_plan.json (jamais une liste à la main · #6)
# et EXIGE que le README les reflète EXACTEMENT (set-diff : ni fantôme ni manquant ;
# accents/casse normalisés). Le rôle est en outre DOUBLE-ANCRÉ sur rbac_50_roles
# (id → portail compta → nom ERPNext concordant). Cohérences croisées en bonus
# (mordent un plan INTERNEMENT incohérent) : rôle UNIQUE sur tous les évènements ·
# défaut ∈ catalogue FormaPago · ensembles NON VIDES. Un claim absent échoue AUSSI
# (traçabilité #6).
# ============================================================================
ECF_RD2 = f"{D}/fiscal/ecf_dgii/README.md"
try:
_ecfp = load("fiscal/ecf_dgii/out/ecf_plan.json")
_ecf_ev = _ecfp["emission_events"]
if not _ecf_ev:
raise ValueError("emission_events vide")
ecf_updvals = {e["update_value"] for e in _ecf_ev}
ecf_bases = {e["base_field"] for e in _ecf_ev}
ecf_roleids = {e["role_id"] for e in _ecf_ev}
ecf_rolenms = {e["erpnext_role_name"] for e in _ecf_ev}
_ecf_fpd = _ecfp["forma_pago_defaut"]
_ecf_fpcat = {str(f["code"]): f["label"] for f in _ecfp["formas_pago"]}
except (OSError, KeyError, TypeError, ValueError, json.JSONDecodeError) as e:
bad(f"Fiscal e-CF évènements · ecf_plan.json illisible/inattendu : {e}")
_ecf_ev = None
if _ecf_ev is not None:
def _ecf_ai(s): # accent-insensible · minuscule · sans espaces
return "".join(c for c in unicodedata.normalize("NFKD", str(s))
if unicodedata.category(c) != "Mn").lower().replace(" ", "")
try:
ecf_doc = re.sub(r"\s+", " ", open(ECF_RD2, encoding="utf-8").read())
except OSError as e:
bad(f"Fiscal e-CF évènements · {ECF_RD2} illisible : {e}"); ecf_doc = None
# (0) cohérence interne du plan : rôle UNIQUE + défaut ∈ catalogue FormaPago
if len(ecf_roleids) != 1 or len(ecf_rolenms) != 1:
bad(f"Fiscal e-CF évènements · plan INCOHÉRENT : rôle non unique sur les "
f"évènements (ids={sorted(ecf_roleids)} noms={sorted(ecf_rolenms)}) — "
f"le README promet un « rôle dédié » unique")
_fp_code = str(_ecf_fpd.get("code"))
if _fp_code not in _ecf_fpcat:
bad(f"Fiscal e-CF évènements · forma_pago_defaut.code={_fp_code!r} ABSENT "
f"du catalogue formas_pago {sorted(_ecf_fpcat)} (plan incohérent)")
if ecf_doc is not None:
# (1) ENSEMBLE des évènements émetteurs — L48 « seulement X et Y »
m_bill = re.search(r"seulement\s+([^.]+?)\.", ecf_doc)
want = {_ecf_ai(x) for x in ecf_updvals}
if m_bill is None:
bad(f"Fiscal e-CF évènements · {ECF_RD2} — énumération « seulement … » "
f"INTROUVABLE (attendu {sorted(ecf_updvals)})")
else:
bill = {_ecf_ai(x) for x in re.split(r"\s+et\s+|,\s*", m_bill.group(1)) if x.strip()}
if bill == want:
good(f"Fiscal e-CF évènements · émetteurs README == ecf_plan "
f"({sorted(ecf_updvals)})")
else:
bad(f"Fiscal e-CF évènements · émetteurs README {sorted(bill)} ≠ "
f"ecf_plan {sorted(want)} (fantôme={sorted(bill-want)} "
f"manquant={sorted(want-bill)}) → régénérer le README")
# (2) ENSEMBLE exclu (brouillons) DISJOINT des émetteurs — anti-facture-brouillon
m_excl = re.search(r"on ne facture pas un brouillon\s*\(([^)]+)\)", ecf_doc)
if m_excl is None:
bad(f"Fiscal e-CF évènements · {ECF_RD2} — clause « on ne facture pas un "
f"brouillon (…) » INTROUVABLE (raison d'être #6 · traçabilité)")
else:
excl = {_ecf_ai(x) for x in re.split(r"[/,]", m_excl.group(1)) if x.strip()}
clash = excl & want
if clash:
bad(f"Fiscal e-CF évènements · un état déclaré BROUILLON {sorted(clash)} "
f"est ÉMETTEUR dans ecf_plan → FACTURE SUR BROUILLON (l'anti-pattern "
f"que le module interdit · #6) OU README auto-contradictoire")
else:
good(f"Fiscal e-CF évènements · brouillons {sorted(excl)} ∩ émetteurs = ∅ "
f"(jamais de facture sur brouillon)")
# (3) ENSEMBLE des base_field — L50 « (montant_reservation, montant_contrat) »
m_base = re.search(r"champ \*\*Currency réel\*\*[^(]*\(([^)]+)\)", ecf_doc)
if m_base is None:
bad(f"Fiscal e-CF évènements · {ECF_RD2} — énumération des base_field "
f"INTROUVABLE (attendu {sorted(ecf_bases)})")
else:
docbases = set(re.findall(r"`([^`]+)`", m_base.group(1)))
if docbases == ecf_bases:
good(f"Fiscal e-CF évènements · base_field README == ecf_plan "
f"({sorted(ecf_bases)})")
else:
bad(f"Fiscal e-CF évènements · base_field README {sorted(docbases)} ≠ "
f"ecf_plan {sorted(ecf_bases)} (fantôme={sorted(docbases-ecf_bases)} "
f"manquant={sorted(ecf_bases-docbases)})")
# (4) rôle UNIQUE : backtické + nom ERPNext + DOUBLE-ANCRÉ portail compta (RBAC)
_rid = next(iter(ecf_roleids)); _rnm = next(iter(ecf_rolenms))
if f"`{_rid}`" not in ecf_doc:
bad(f"Fiscal e-CF évènements · rôle dédié `{_rid}` (== ecf_plan) NON cité "
f"backtické dans le README")
elif _rnm not in ecf_doc:
bad(f"Fiscal e-CF évènements · nom ERPNext « {_rnm} » (== ecf_plan) ABSENT "
f"du README")
else:
try:
_rbac = load("rbac/rbac_50_roles.json")["roles"]
_rrec = next((r for r in _rbac if r.get("id") == _rid), None)
except (OSError, KeyError, TypeError, json.JSONDecodeError) as e:
bad(f"Fiscal e-CF évènements · rbac_50_roles illisible : {e}"); _rrec = "ERR"
if _rrec is None:
bad(f"Fiscal e-CF évènements · rôle `{_rid}` (ecf_plan) INTROUVABLE dans "
f"rbac_50_roles → rôle Compta fantôme")
elif _rrec == "ERR":
pass
elif _rrec.get("portail") != "compta":
bad(f"Fiscal e-CF évènements · rôle `{_rid}` portail={_rrec.get('portail')!r} "
f"≠ « compta » (le README exige le portail compta)")
elif _rrec.get("erpnext_role_name") != _rnm:
bad(f"Fiscal e-CF évènements · nom ERPNext discordant ecf_plan « {_rnm} » "
f"vs RBAC « {_rrec.get('erpnext_role_name')} »")
else:
good(f"Fiscal e-CF évènements · rôle unique `{_rid}` (« {_rnm} ») == "
f"ecf_plan == RBAC (portail compta)")
# (5) FormaPago défaut — L59 « = 3 (Tarjeta) » · Cardnet #10
m_fp = re.search(r"FormaPago défaut\s*=\s*(\d+)\s*\(([^)]+)\)", ecf_doc)
_lbl = _ecf_fpcat.get(_fp_code, "")
if m_fp is None:
bad(f"Fiscal e-CF évènements · {ECF_RD2} — « FormaPago défaut = N (…) » "
f"INTROUVABLE (attendu {_fp_code} / {_lbl})")
elif m_fp.group(1) != _fp_code:
bad(f"Fiscal e-CF évènements · FormaPago défaut README ={m_fp.group(1)} "
f"MAIS ecf_plan dit ={_fp_code} ({_lbl}) → défaut périmé")
elif _ecf_ai(m_fp.group(2)) not in _ecf_ai(_lbl):
bad(f"Fiscal e-CF évènements · FormaPago défaut label README "
f"« {m_fp.group(2)} » incohérent avec le catalogue « {_lbl} » (code {_fp_code})")
elif "cardnet" not in _ecf_ai(_ecf_fpd.get("motif", "")):
bad(f"Fiscal e-CF évènements · forma_pago_defaut.motif ne cite plus "
f"« Cardnet » (#10) → justification du défaut évaporée")
else:
good(f"Fiscal e-CF évènements · FormaPago défaut = {_fp_code} "
f"({_lbl}) · Cardnet #10 == ecf_plan")
sys.exit(1 if FAIL else 0)
PY
rc=$?