[DTP-Worker] Sprint 4 · Générateur configuration e-CF DGII (Compupar) (ERPNext Backend · roadmap L51)
Facturation électronique dominicaine cross-cohérente workflow↔DocType↔RBAC : émission sur état soumis, base Currency réelle, rôle compta-fiscaliste-ecf. Anti-invention (#6) : RNC/ITBIS/TipoCambio/endpoints Compupar null (a_confirmer, jamais sans source) ; seules les données de référence DGII encodées avec source. Composeur e-NCF traçable (E+tipo(2)+seq(10)). 39 tests · 12 invariants · gate CI (job fiscal-ecf-tests) · 241 tests de régression verts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
"""Composition TRACABLE du numero e-NCF (anti-invention · CLAUDE.md #6).
|
||||
|
||||
Meme doctrine que `commlib/finance.py` / `banclib/finance.py` : une composition
|
||||
transparente, reproductible et entierement sourcee n'est PAS une invention —
|
||||
c'est de la modelisation auditable. Le e-NCF (Numero de Comprobante Fiscal
|
||||
electronique) suit le format DGII :
|
||||
|
||||
e-NCF = "E" + tipoeCF (2 chiffres) + secuencia (10 chiffres) → 13 caracteres
|
||||
|
||||
La `secuencia` provient d'un rango autorise par la DGII (jamais fabriquee ici).
|
||||
`compose_encf` reste donc `None` tant qu'un operande manque (placeholder, jamais
|
||||
0-invente), et publie toujours sa FORMULE. Aucun tipo ni sequence n'est jamais
|
||||
fabrique par defaut : le caller fournit des operandes reels (cote VPS/Compupar).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any, Optional
|
||||
|
||||
from . import deps
|
||||
|
||||
is_filled = deps.is_filled
|
||||
|
||||
# Format e-NCF DGII (Ley 32-23) : E + tipo(2) + secuencia(10) = 13 caracteres.
|
||||
ENCF_RE = re.compile(r"^E([0-9]{2})([0-9]{10})$")
|
||||
FORMULE = "E + tipoeCF(2) + secuencia(10)"
|
||||
|
||||
|
||||
def is_valid_encf(value: Any) -> bool:
|
||||
"""True si `value` respecte exactement le format e-NCF DGII (13 car.)."""
|
||||
return bool(is_filled(value) and ENCF_RE.match(str(value).strip()))
|
||||
|
||||
|
||||
def parse_encf(value: Any) -> Optional[dict]:
|
||||
"""Decompose un e-NCF valide → {prefix, tipo, secuencia}. None sinon."""
|
||||
if not is_filled(value):
|
||||
return None
|
||||
m = ENCF_RE.match(str(value).strip())
|
||||
if not m:
|
||||
return None
|
||||
return {"prefix": "E", "tipo": m.group(1), "secuencia": m.group(2)}
|
||||
|
||||
|
||||
def _tipo(value: Any) -> Optional[str]:
|
||||
"""Un code de type sur 2 chiffres, ou None si absent/mal forme (jamais fabrique)."""
|
||||
if not is_filled(value):
|
||||
return None
|
||||
txt = str(value).strip()
|
||||
return txt if re.fullmatch(r"[0-9]{2}", txt) else None
|
||||
|
||||
|
||||
def _secuencia(value: Any) -> Optional[str]:
|
||||
"""Une secuencia numerique → 10 chiffres zero-paddes, ou None si absente/invalide.
|
||||
|
||||
Ne fabrique jamais de sequence : `None` en entree reste `None` en sortie.
|
||||
"""
|
||||
if not is_filled(value):
|
||||
return None
|
||||
txt = str(value).strip()
|
||||
if not re.fullmatch(r"[0-9]{1,10}", txt):
|
||||
return None
|
||||
return txt.zfill(10)
|
||||
|
||||
|
||||
def compose_encf(tipo: Any, secuencia: Any) -> dict:
|
||||
"""Compose un e-NCF tracable a partir d'operandes reels.
|
||||
|
||||
Retourne un dict publiant la formule, la valeur composee (None si un operande
|
||||
manque — jamais fabriquee), et les champs manquants. Aucun defaut n'est
|
||||
invente : `tipo`/`secuencia` absents ⇒ `e_ncf: None`, `incomplete: True`.
|
||||
"""
|
||||
t = _tipo(tipo)
|
||||
s = _secuencia(secuencia)
|
||||
|
||||
manquants: list[str] = []
|
||||
if t is None:
|
||||
manquants.append("tipo")
|
||||
if s is None:
|
||||
manquants.append("secuencia")
|
||||
|
||||
e_ncf = f"E{t}{s}" if (t is not None and s is not None) else None
|
||||
return {
|
||||
"e_ncf": e_ncf,
|
||||
"tipo": t,
|
||||
"secuencia": s,
|
||||
"formule": FORMULE,
|
||||
"incomplete": bool(manquants),
|
||||
"champs_manquants": manquants,
|
||||
}
|
||||
Reference in New Issue
Block a user