← Files VeraARCHIVED FILE
modules/treasury-forecast/scripts/treasury_inputs.py
11 KB · Oct 2, 2026 · 00:29 UTC
"""Read the published treasury tables and optional linked FatturaPA evidence."""
from __future__ import annotations
import csv
import hashlib
import importlib.util
import json
import sys
from datetime import date, datetime
from pathlib import Path
from typing import Any
# Exception type only; parsing delegates to the size-limited, DTD-rejecting parser.
from xml.etree.ElementTree import ParseError # nosec B405
from zipfile import BadZipFile, ZipFile
from treasury_core import TreasuryError, money, validate_record
__all__ = [
"HEADERS",
"load_inputs",
"manifest_paths",
"read_json",
"safe_path",
"write_templates",
]
ROOT = Path(__file__).resolve().parents[1]
HEADERS = {
"accounts": ["account_id", "balance"],
"bank_movements": ["movement_id", "account_id", "date", "amount", "description"],
"open_items": [
"item_id",
"party_id",
"party_name",
"side",
"document_type",
"document_number",
"document_date",
"installment",
"due_date",
"amount",
"replaces_flow_id",
],
"planned_flows": [
"flow_id",
"side",
"amount",
"expected_date",
"description",
"basis",
],
"allocations": [
"allocation_id",
"movement_id",
"target_type",
"target_id",
"amount",
],
"adjustments": [
"adjustment_id",
"item_id",
"date",
"amount",
"kind",
"evidence_ref",
],
}
MONEY_COLUMNS = {"balance", "amount"}
MAX_SOURCE_BYTES = 50 * 1024 * 1024
def safe_path(root: Path, relative: str) -> Path:
"""Permit regular input files within the selected run, with no symlinks."""
path = Path(relative)
if path.is_absolute() or ".." in path.parts or not path.parts:
raise TreasuryError("Source paths must be relative to the run inputs")
cursor = root
for part in path.parts:
cursor = cursor / part
if cursor.is_symlink():
raise TreasuryError("Linked input paths are unsupported")
resolved = cursor.resolve(strict=True)
if not resolved.is_relative_to(root.resolve()) or not resolved.is_file():
raise TreasuryError("Source escapes the input boundary")
if resolved.stat().st_size > MAX_SOURCE_BYTES:
raise TreasuryError("Input file exceeds the supported size")
return resolved
def read_json(path: Path) -> dict[str, Any]:
"""Read a bounded object; duplicate fields cannot silently replace evidence."""
def unique(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
result: dict[str, Any] = {}
for key, value in pairs:
if key in result:
raise TreasuryError(f"Duplicate JSON field: {key}")
result[key] = value
return result
if path.stat().st_size > MAX_SOURCE_BYTES:
raise TreasuryError("JSON input is too large")
value = json.loads(path.read_text(encoding="utf-8"), object_pairs_hook=unique)
if not isinstance(value, dict):
raise TreasuryError("Expected a JSON object")
return value
def manifest_paths(manifest: dict[str, Any], root: Path) -> list[Path]:
"""Resolve every source before a caller validates archive receipts."""
if manifest.get("schema_version") != "vera.treasury_manifest.v1":
raise TreasuryError("Unsupported input manifest")
if set(manifest["tables"]) != set(HEADERS):
raise TreasuryError("All six named tables are required, including empty tables")
paths = [safe_path(root, item["path"]) for item in manifest["tables"].values()]
paths.extend(safe_path(root, path) for path in manifest.get("invoice_files", []))
if manifest.get("previous"):
paths.append(safe_path(root, manifest["previous"]["path"]))
return sorted(set(paths))
def _cell(value: Any, column: str) -> str:
if value is None:
return ""
if isinstance(value, (date, datetime)):
if column not in {"date", "document_date", "due_date", "expected_date"}:
raise TreasuryError("Date cell appears in a non-date column")
return (
value.date().isoformat()
if isinstance(value, datetime)
else value.isoformat()
)
if column in MONEY_COLUMNS:
if isinstance(value, bool):
raise TreasuryError("Boolean is not a monetary value")
return f"{money(str(value)):.2f}"
if not isinstance(value, str):
raise TreasuryError(
f"{column} must be a text cell; preserve stable IDs explicitly"
)
return value.strip()
def _table(path: Path, name: str, sheet: str | None) -> list[dict[str, str]]:
"""Parse exact supported headers; there is no heuristic layout selection."""
header = HEADERS[name]
rows: list[dict[str, str]] = []
def consume(values: list[Any]) -> None:
if len(values) != len(header):
raise TreasuryError(f"Ragged row in {name}")
if len(rows) >= 100000:
raise TreasuryError("Table exceeds the supported row count")
rows.append({key: _cell(value, key) for key, value in zip(header, values)})
if path.suffix.lower() == ".csv":
if sheet:
raise TreasuryError("CSV inputs do not have sheets")
with path.open(newline="", encoding="utf-8-sig") as stream:
reader = csv.reader(stream, strict=True)
if next(reader, None) != header:
raise TreasuryError(
f"Unsupported {name} CSV headers; use the published template"
)
for row in reader:
consume(row)
elif path.suffix.lower() == ".xlsx":
from openpyxl import load_workbook
try:
with ZipFile(path) as archive:
if (
sum(info.file_size for info in archive.infolist())
> 250 * 1024 * 1024
):
raise TreasuryError("Expanded workbook is too large")
except BadZipFile as exc:
raise TreasuryError("Invalid XLSX package") from exc
workbook = load_workbook(
path, read_only=True, data_only=False, keep_links=False
)
try:
if sheet not in workbook.sheetnames:
raise TreasuryError(f"Missing exact sheet for {name}")
iterator = workbook[sheet].iter_rows()
first = next(iterator, ())
if [cell.value for cell in first] != header:
raise TreasuryError(f"Unsupported {name} XLSX headers")
for cells in iterator:
if any(cell.data_type == "f" for cell in cells):
raise TreasuryError(
"Input formulas are unsupported; supply exported values"
)
values = [cell.value for cell in cells]
if any(value is not None for value in values):
consume(values)
finally:
workbook.close()
else:
raise TreasuryError("Only the specified CSV and XLSX tables are supported")
return rows
def _invoices(manifest: dict[str, Any], root: Path) -> list[dict[str, Any]]:
if not manifest.get("invoice_files"):
return []
parser_path = ROOT.parent / "client-file-preparation/scripts/parse_fatturapa_xml.py"
spec = importlib.util.spec_from_file_location("treasury_fatturapa", parser_path)
if spec is None or spec.loader is None:
raise TreasuryError("The bundled FatturaPA parser is unavailable")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
result = []
identities: dict[tuple[str, ...], str] = {}
seen_bytes: set[str] = set()
for relative in manifest["invoice_files"]:
path = safe_path(root, relative)
if path.suffix.lower() != ".xml":
raise TreasuryError(
"Supply extracted FatturaPA XML; ZIP/P7M are not input formats"
)
file_digest = hashlib.sha256(path.read_bytes()).hexdigest()
if file_digest in seen_bytes:
continue
seen_bytes.add(file_digest)
try:
invoices = module.parse_fatturapa_audit_file(path, root)
except (ParseError, ValueError) as exc:
raise TreasuryError(f"Unreadable FatturaPA evidence: {relative}") from exc
for invoice in invoices:
identity = tuple(
str(invoice[key])
for key in (
"supplier_vat",
"customer_tax_id",
"invoice_number",
"invoice_date",
"document_type",
)
)
if not all(identity) or invoice["currency"] != manifest["currency"]:
raise TreasuryError("Invoice identity or currency is unsupported")
if identity in identities:
raise TreasuryError(
"Different XML bytes represent the same invoice identity; review the sources"
)
identities[identity] = file_digest
result.append({**invoice, "source_sha256": file_digest})
return result
def load_inputs(
manifest: dict[str, Any],
root: Path,
*,
client_id: str,
engagement_id: str,
) -> tuple[dict[str, Any], dict[str, Any] | None]:
"""Load qualified tables; XML supplies evidence, never unpaid balances."""
paths = manifest_paths(manifest, root)
data = {
"schema_version": "vera.treasury_inputs.v1",
"client_id": client_id,
"engagement_id": engagement_id,
**{
key: manifest[key]
for key in (
"company_id",
"company_name",
"currency",
"as_of",
"horizon_end",
"coverage",
)
},
"sources": [
{
"path": path.relative_to(root.resolve()).as_posix(),
"sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
}
for path in paths
],
}
for name, source in manifest["tables"].items():
data[name] = _table(safe_path(root, source["path"]), name, source.get("sheet"))
data["invoice_evidence"] = _invoices(manifest, root)
previous = None
if manifest.get("previous"):
previous = read_json(safe_path(root, manifest["previous"]["path"]))
validate_record(previous, accepted=True)
if previous["record_sha256"] != manifest["previous"]["record_sha256"]:
raise TreasuryError("Wrong predecessor forecast")
return data, previous
def write_templates(output: Path) -> None:
"""Create exact empty input templates without replacing existing files."""
output.mkdir(parents=True, exist_ok=True)
paths = [output / f"{name}.csv" for name in HEADERS]
if any(path.exists() for path in paths):
raise TreasuryError("Input templates already exist; no overwrite performed")
for name, header in HEADERS.items():
with (output / f"{name}.csv").open("x", newline="", encoding="utf-8") as stream:
csv.writer(stream).writerow(header)
SHA-256: 420dc8e9943f6ed743b88b9c32ee7f1da892950c3d71fa7708bfb5f10ba03713