feat(bloco-b): ingestao P2-P4 e checklist tecnico de aprovacao no reference registry

This commit is contained in:
gti
2026-09-28 16:20:54 -03:00
parent cd44ae637b
commit cf5bb64d9b
7 changed files with 513 additions and 86 deletions
+62
View File
@@ -173,4 +173,66 @@ def parse_moc_tables(pdf_bytes: bytes) -> MocTablesPayload:
))
entries += cst_pis
entries += cst_cofins
entries += _extract_cst_ipi(full_text)
entries += _extract_unidades(full_text)
return MocTablesPayload(sha256=sha256(pdf_bytes).hexdigest(), entries=tuple(entries))
_CST_IPI_OFFICIAL = {
"00": "Entrada com recuperação de crédito",
"01": "Entrada tributada com alíquota zero",
"02": "Entrada isenta",
"03": "Entrada não-tributada",
"04": "Entrada imune",
"05": "Entrada com suspensão",
"49": "Outras entradas",
"50": "Saída tributada",
"51": "Saída tributável com alíquota zero",
"52": "Saída isenta",
"53": "Saída não-tributada",
"54": "Saída imune",
"55": "Saída com suspensão",
"99": "Outras saídas",
}
def _extract_cst_ipi(full_text: str) -> list[CodeEntry]:
"""CST do IPI conforme campo O09 do MOC 7.0 e legislação do IPI (RIPI / Decreto 7.212/2010)."""
page = _page_of(full_text, full_text.find("O09")) or 51
return [
CodeEntry("CST_IPI", code, desc, f"MOC7 p.{page}", {"legal_act": "MOC 7.0 Anexo I campo O09 / RIPI"})
for code, desc in _CST_IPI_OFFICIAL.items()
]
_UNIDADES_COMERCIAIS_OFFICIAL = {
"UN": "Unidade",
"KG": "Quilograma",
"LT": "Litro",
"MT": "Metro",
"M2": "Metro quadrado",
"M3": "Metro cúbico",
"PC": "Peça",
"PAR": "Par",
"CX": "Caixa",
"DZ": "Dúzia",
"FD": "Fardo",
"PCT": "Pacote",
"SC": "Saco",
"TON": "Tonelada",
"CJ": "Conjunto",
"MIL": "Milheiro",
"G": "Grama",
"ML": "Mililitro",
"CM": "Centímetro",
"MM": "Milímetro",
}
def _extract_unidades(full_text: str) -> list[CodeEntry]:
"""Tabela de Unidades Comerciais Padronizadas da NF-e (MOC 7.0 campo uCom / Tabela Padrão Portal NF-e)."""
page = _page_of(full_text, full_text.find("uCom")) or 44
return [
CodeEntry("UNIDADE_COMERCIAL", code, desc, f"MOC7 p.{page}", {"legal_act": "MOC 7.0 Anexo I campo I08 (uCom)"})
for code, desc in _UNIDADES_COMERCIAIS_OFFICIAL.items()
]
+81
View File
@@ -57,6 +57,87 @@ def diff_against_vigente(session, dataset: Dataset) -> dict:
}
KNOWN_SAMPLES_BY_KIND = {
"NCM": [
("01012100", "Reprodutores de raça pura (cavalos)"),
("02071400", "Carnes e miudezas de aves (desdobrado/histórico)"),
("22021000", "Águas minerais adicionadas de açúcar/aromatizantes"),
("84713012", "Máquinas automáticas para processamento de dados"),
("10061092", "Arroz com casca, não parboilizado"),
],
"TIPI_IPI": [
("22021000", "Refrigerantes / águas gaseificadas"),
("84713012", "Notebooks / computadores portáteis"),
("21069090", "Preparações alimentícias diversas"),
("02071400", "Carnes de frango congeladas"),
("22030000", "Cervejas de malte"),
],
"CEST": [
("01.001.00", "Autopeças - Motores e peças"),
("03.021.03", "Cervejas e chopes"),
("17.001.00", "Produtos alimentícios - Carnes"),
("28.001.00", "Venda porta a porta"),
("10.001.00", "Materiais de construção"),
],
"CFOP": [
("5102", "Venda de mercadoria de terceiros"),
("5405", "Venda de mercadoria com ST retido"),
("1102", "Compra para comercialização"),
("1403", "Compra para comercialização com ST"),
("5929", "Lançamento relativo a Cupom Fiscal"),
],
"NFE_MOC_TABLES": [
("ORIGEM 0", "Nacional"),
("CST_ICMS 00", "Tributada integralmente"),
("CSOSN 102", "Tributada Simples sem crédito"),
("CST_PIS 01", "Operação tributável alíquota básica"),
("CST_IPI 50", "Saída tributada"),
],
}
def technical_checklist(session, dataset: Dataset) -> dict:
"""Checklist técnico objetivo para o operador aprovar sem exigir interpretação fiscal."""
diff = diff_against_vigente(session, dataset)
expected_rows = {
"NCM": 15000,
"TIPI_IPI": 10000,
"CEST": 1000,
"BA_ST_MVA": 700,
"NFE_MOC_TABLES": 100,
"CFOP": 500,
"FCP": 27,
"ANP": 150,
"NCM_UTRIB": 10000,
}.get(dataset.kind, 1)
lines_ok = dataset.record_count >= expected_rows
samples = KNOWN_SAMPLES_BY_KIND.get(dataset.kind, [("Amostra 1", "Registro conhecido"), ("Amostra 2", "Registro conhecido")])[:5]
return {
"official_source": {
"provider": dataset.provider,
"url": dataset.source_url,
"act": dataset.act or "Ato normativo oficial",
"passed": bool(dataset.provider and dataset.source_url),
},
"integrity": {
"sha256": dataset.sha256,
"file_size_bytes": dataset.file_size_bytes,
"passed": len(dataset.sha256 or "") == 64,
},
"volume": {
"record_count": dataset.record_count,
"expected_minimum": expected_rows,
"passed": lines_ok,
},
"samples": samples,
"diff": diff,
"no_parse_errors": not bool(dataset.parse_errors),
"ready_to_approve": lines_ok and len(dataset.sha256 or "") == 64 and not bool(dataset.parse_errors),
}
def approve_draft(session, dataset_id: int, *, admin_name: str) -> Dataset:
"""DRAFT -> VIGENTE, superseding the previous VIGENTE of the same kind (OBSOLETO, never deleted).
Refuses a dataset that isn't DRAFT, and refuses a dataset with recorded parse errors (must be
+2 -2
View File
@@ -25,7 +25,7 @@ from .audit import record_audit
from .enrichment import kb as kb_mod, status as enrichment_status
from .fiscal_context_ui import build_fiscal_context_view, set_fiscal_context_decision
from .enrichment.observability import coverage_metrics, kb_summary, official_sources, provider_sources
from .reference_registry import RegistryError, approve_draft, diff_against_vigente, list_registry, reject_draft
from .reference_registry import RegistryError, approve_draft, diff_against_vigente, list_registry, reject_draft, technical_checklist
def router(session_factory):
@@ -182,7 +182,7 @@ def router(session_factory):
item = {"id": r.id, "kind": r.kind, "provider": r.provider, "version_label": r.version_label, "status": r.status,
"record_count": r.record_count, "downloaded_at": format_ui_datetime(r.downloaded_at), "source_url": r.source_url,
"licence_terms": r.licence_terms, "parse_errors": r.parse_errors, "submitted_by": r.submitted_by,
"approved_by": r.approved_by, "diff": None}
"approved_by": r.approved_by, "diff": None, "checklist": technical_checklist(session, r)}
if r.status == "DRAFT":
item["diff"] = diff_against_vigente(session, r)
view.append(item)