← Files VeraARCHIVED FILE

modules/open-item-reconciliation/scripts/build_missing_evidence_requests.py

62 KB · Oct 4, 2026 · 12:28 UTC

↓ Download file

"""Build targeted evidence requests from a reconciliation workbook."""

from __future__ import annotations

import sys as _bootstrap_sys

_bootstrap_sys.dont_write_bytecode = True
_bootstrap_sys.pycache_prefix = (
    r"Z:\__open_item_reconciliation_no_bytecode__"
    if _bootstrap_sys.platform == "win32"
    else "/dev/null/open-item-reconciliation"
)

import os as _bootstrap_os

_BOOTSTRAP_PATH = _bootstrap_os.path.join(
    _bootstrap_os.path.dirname(_bootstrap_os.path.abspath(__file__)),
    "implementation_bootstrap.py",
)
_BOOTSTRAP_NAMESPACE = {
    "__file__": _BOOTSTRAP_PATH,
    "__name__": "_open_item_reconciliation_implementation_bootstrap",
}
_bootstrap_stat = _bootstrap_os.lstat(_BOOTSTRAP_PATH)
if _bootstrap_stat.st_mode & 0o170000 != 0o100000 or _bootstrap_stat.st_nlink != 1:
    raise RuntimeError(
        "implementation bootstrap must be an ordinary single-link regular file"
    )
_bootstrap_descriptor = _bootstrap_os.open(
    _BOOTSTRAP_PATH,
    _bootstrap_os.O_RDONLY | getattr(_bootstrap_os, "O_NOFOLLOW", 0),
)
try:
    _bootstrap_open_stat = _bootstrap_os.fstat(_bootstrap_descriptor)
    _bootstrap_identity = (
        _bootstrap_stat.st_dev,
        _bootstrap_stat.st_ino,
        _bootstrap_stat.st_size,
        _bootstrap_stat.st_mtime_ns,
        _bootstrap_stat.st_nlink,
    )
    if _bootstrap_identity != (
        _bootstrap_open_stat.st_dev,
        _bootstrap_open_stat.st_ino,
        _bootstrap_open_stat.st_size,
        _bootstrap_open_stat.st_mtime_ns,
        _bootstrap_open_stat.st_nlink,
    ):
        raise RuntimeError("implementation bootstrap changed before it was read")
    with _bootstrap_os.fdopen(
        _bootstrap_descriptor,
        "rb",
        closefd=False,
    ) as _bootstrap_handle:
        _bootstrap_source = _bootstrap_handle.read()
    _bootstrap_after_stat = _bootstrap_os.fstat(_bootstrap_descriptor)
    if (
        _bootstrap_identity
        != (
            _bootstrap_after_stat.st_dev,
            _bootstrap_after_stat.st_ino,
            _bootstrap_after_stat.st_size,
            _bootstrap_after_stat.st_mtime_ns,
            _bootstrap_after_stat.st_nlink,
        )
        or len(_bootstrap_source) != _bootstrap_after_stat.st_size
    ):
        raise RuntimeError("implementation bootstrap changed while it was read")
finally:
    _bootstrap_os.close(_bootstrap_descriptor)
# Execute only the pre-opened single-link bootstrap source.
exec(  # nosec B102
    compile(_bootstrap_source, _BOOTSTRAP_PATH, "exec"),
    _BOOTSTRAP_NAMESPACE,
)
_BOOTSTRAP_ROOTS = _BOOTSTRAP_NAMESPACE["activate_implementation_boundary"](
    ("locale_support",)
)
_SCRIPTS_DIR = _bootstrap_os.path.dirname(_bootstrap_os.path.abspath(__file__))
if _SCRIPTS_DIR not in _bootstrap_sys.path:
    _bootstrap_sys.path.insert(0, _SCRIPTS_DIR)

import argparse
import logging
import sys
from dataclasses import dataclass
from decimal import Decimal, InvalidOperation
from pathlib import Path
from typing import Any

from openpyxl import Workbook, load_workbook
from openpyxl.styles import Alignment, Font, PatternFill
from openpyxl.utils import get_column_letter

_VERA_ASSURANCE_MODULE_ROOT = Path(str(_BOOTSTRAP_ROOTS["shared_assurance"])).parent
if str(_VERA_ASSURANCE_MODULE_ROOT) not in sys.path:
    sys.path.insert(0, str(_VERA_ASSURANCE_MODULE_ROOT))

from vera_assurance import (  # noqa: E402
    AssuranceContractError,
    load_client_engagement_context_file,
)

try:
    from .locale_support import normalize_language
except ImportError:  # pragma: no cover - direct import support
    scripts_dir = Path(__file__).resolve().parent
    if str(scripts_dir) not in sys.path:
        sys.path.insert(0, str(scripts_dir))
    from locale_support import normalize_language  # type: ignore


__all__ = [
    "MissingEvidenceRequestPack",
    "build_missing_evidence_request_pack",
    "load_reconciliation_context",
    "write_missing_evidence_workbook",
]


LOGGER = logging.getLogger(__name__)
DEFAULT_EXCEL_NAME = "richieste_mirate_evidenze.xlsx"
HEADER_FILL = "17365D"

SECTION_ORDER = (
    "reconciled_strong",
    "probable_payment",
    "accounting_support_needed",
    "missing_evidence_needed",
    "open_balance_confirmation",
    "unresolved",
)
REQUEST_FIELDS = (
    "row_id",
    "side",
    "document",
    "document_date",
    "amount",
    "operational_category",
    "operational_criterion",
    "operational_owner",
    "available_evidence",
    "targeted_missing_item",
    "existing_reference",
    "requested_action",
    "evidence_description",
)
INSTRUCTION_FIELDS = ("principle", "guidance")
SUMMARY_FIELDS = ("section", "rows", "amount_total")
INVENTORY_FIELDS = (
    "evidence_category",
    "normalized_rows",
    "source_files",
    "operational_use",
)


TEXT: dict[str, dict[str, Any]] = {
    "it": {
        "sheet_names": {
            "instructions": "istruzioni",
            "summary": "sintesi",
            "inventory": "evidenze_disponibili",
            "reconciled_strong": "riconciliate_forti",
            "probable_payment": "pagamenti_probabili",
            "accounting_support_needed": "scritture_da_supportare",
            "missing_evidence_needed": "evidenze_da_integrare",
            "open_balance_confirmation": "saldi_aperti",
            "unresolved": "non_risolte",
        },
        "headers": {
            "principle": "principio",
            "guidance": "indicazione",
            "section": "sezione",
            "rows": "righe",
            "amount_total": "importo_totale",
            "evidence_category": "categoria_evidenza",
            "normalized_rows": "righe_normalizzate",
            "source_files": "file_sorgente",
            "operational_use": "uso_operativo",
            "row_id": "id_riga_workpaper",
            "side": "lato",
            "document": "documento",
            "document_date": "data_documento",
            "amount": "importo",
            "operational_category": "categoria_operativa",
            "operational_criterion": "criterio_operativo",
            "operational_owner": "destinatario_operativo",
            "available_evidence": "evidenza_gia_disponibile",
            "targeted_missing_item": "dato_mancante_mirato",
            "existing_reference": "riferimento_esistente",
            "requested_action": "azione_richiesta",
            "evidence_description": "descrizione_evidenza",
        },
        "sections": {
            "reconciled_strong": "Riconciliate con evidenza forte",
            "probable_payment": "Pagamenti probabili da allocare",
            "accounting_support_needed": "Scritture contabili da supportare",
            "missing_evidence_needed": "Evidenze da integrare",
            "open_balance_confirmation": "Saldi aperti da confermare",
            "unresolved": "Non risolte",
        },
        "criteria": {
            "reconciled_strong": "Evidenza esterna o catena contabile-bancaria sufficiente.",
            "probable_payment": "Movimento bancario o distinta probabile, allocazione non ancora confermata.",
            "accounting_support_needed": "Scrittura contabile di chiusura priva di supporto esterno sufficiente.",
            "missing_evidence_needed": "Indizio presente ma prova conclusiva mancante.",
            "open_balance_confirmation": "Saldo aperto supportato dalle evidenze interne.",
            "unresolved": "Collegamento insufficiente nelle evidenze acquisite.",
        },
        "side": {
            "customer": "cliente",
            "receivable": "cliente",
            "client": "cliente",
            "supplier": "fornitore",
            "payable": "fornitore",
            "vendor": "fornitore",
            "unknown": "non indicato",
        },
        "owners": {
            "default_entity": "soggetto revisionato",
            "default_counterparty": "controparte",
            "counterparty_confirmation": "{entity}; eventuale conferma saldo {counterparty}",
        },
        "inventory_usage": {
            "open_items": "Popolazione delle partite aperte o da riconciliare.",
            "ledger": "Supporto di saldo, partitario, mastro e registrazioni interne.",
            "journal": "Scritture contabili, chiusure interne, giroconti e movimenti di periodo.",
            "bank_statement": "Incassi e pagamenti effettivi da estratto conto.",
            "payment_order": "Distinte o batch utili per collegare banca e fatture.",
            "factoring_statement": "Evidenza factor, anticipo o operatore esterno.",
            "compensation_support": "Supporto di compensazione o netting documentato.",
            "unknown": "Fonte acquisita ma non classificata automaticamente.",
            "default": "Evidenza acquisita nel fascicolo.",
        },
        "available": {
            "reconciled_strong": "La riga e gia riconciliata nel workpaper con evidenza forte.",
            "probable_payment": "Estratto banca e candidato di match gia presenti nel workpaper.",
            "accounting_support_needed": "Partita aperta e scrittura contabile gia rilevate.",
            "missing_evidence_needed": "Indizio o documento ponte gia rilevato, ma non conclusivo.",
            "open_balance_confirmation": "Partita aperta e supporto interno di saldo gia presenti.",
            "unresolved": "Partita aperta presente, senza collegamento sufficiente nelle evidenze acquisite.",
            "reference": " Riferimento: {reference}.",
        },
        "missing": {
            "reconciled_strong": "Nessun dato mancante per la richiesta mirata.",
            "probable_payment": "Conferma allocazione fattura-per-fattura del movimento bancario o della distinta gia identificata.",
            "accounting_support_needed": "Supporto esterno o spiegazione documentata della scrittura contabile: banca, factor, compensazione, giroconto o chiusura non-cash.",
            "missing_evidence_needed": "Documento necessario per trasformare l'indizio in evidenza conclusiva.",
            "open_balance_confirmation": "Conferma che la partita era effettivamente aperta al {cutoff}, oppure prova puntuale della chiusura non rilevata.",
            "unresolved": "Riferimento sorgente o prova puntuale di chiusura non rilevata.",
        },
        "actions": {
            "reconciled_strong": "Nessuna richiesta operativa; conservare il riferimento nel workpaper.",
            "probable_payment": "Confermare o correggere la mappatura proposta, indicando per ogni fattura importo allocato, distinta/SEPA e data pagamento.",
            "accounting_support_needed": "Inviare solo il supporto della contropartita o spiegare la natura della chiusura non-cash; non rimandare mastro/giornale se invariati.",
            "missing_evidence_needed": "Indicare quale documento gia acquisito chiude la riga, oppure fornire il solo documento mancante indicato.",
            "open_balance_confirmation": "Se aperta: confermare il saldo. Se chiusa: indicare documento di pagamento/incasso/factor/compensazione e data.",
            "unresolved": "Fornire riferimento fattura/partitario o indicare se la riga e errata; se chiusa, allegare il documento di chiusura.",
        },
        "instructions": {
            "no_reask": "Non richiedere documenti gia acquisiti",
            "no_reask_body": "Il workpaper principale contiene gia la popolazione delle partite aperte, mastri, giornale, estratti banca/factor, distinte e normalizzazioni disponibili.",
            "targeted": "Richiesta mirata",
            "targeted_body": "Per ogni riga chiedere solo il tassello mancante indicato: allocazione, supporto esterno, conferma aperto o riferimento sorgente.",
            "owner": "Destinatario",
            "owner_body": "{entity} deve prima confermare/correggere usando il fascicolo. {counterparty} serve solo per eventuale conferma saldo o riconciliazione esterna al {cutoff}.",
        },
        "log_path": "Workbook richieste mirate: {path}",
    },
    "fr": {
        "sheet_names": {
            "instructions": "instructions",
            "summary": "synthese",
            "inventory": "preuves_disponibles",
            "reconciled_strong": "rapprochees_preuve_forte",
            "probable_payment": "paiements_probables",
            "accounting_support_needed": "ecritures_a_justifier",
            "missing_evidence_needed": "preuves_a_completer",
            "open_balance_confirmation": "soldes_ouverts",
            "unresolved": "non_resolues",
        },
        "headers": {
            "principle": "principe",
            "guidance": "indication",
            "section": "section",
            "rows": "lignes",
            "amount_total": "montant_total",
            "evidence_category": "categorie_preuve",
            "normalized_rows": "lignes_normalisees",
            "source_files": "fichier_source",
            "operational_use": "usage_operationnel",
            "row_id": "id_ligne_workpaper",
            "side": "sens",
            "document": "document",
            "document_date": "date_document",
            "amount": "montant",
            "operational_category": "categorie_operationnelle",
            "operational_criterion": "critere_operationnel",
            "operational_owner": "destinataire_operationnel",
            "available_evidence": "preuve_deja_disponible",
            "targeted_missing_item": "element_manquant_cible",
            "existing_reference": "reference_existante",
            "requested_action": "action_demandee",
            "evidence_description": "description_preuve",
        },
        "sections": {
            "reconciled_strong": "Rapprochees avec preuve forte",
            "probable_payment": "Paiements probables a affecter",
            "accounting_support_needed": "Ecritures comptables a justifier",
            "missing_evidence_needed": "Preuves a completer",
            "open_balance_confirmation": "Soldes ouverts a confirmer",
            "unresolved": "Non resolues",
        },
        "criteria": {
            "reconciled_strong": "Preuve externe ou chaine comptable-bancaire suffisante.",
            "probable_payment": "Mouvement bancaire ou remise probable, affectation non encore confirmee.",
            "accounting_support_needed": "Ecriture comptable de cloture sans support externe suffisant.",
            "missing_evidence_needed": "Indice present mais preuve concluante manquante.",
            "open_balance_confirmation": "Solde ouvert supporte par les preuves internes.",
            "unresolved": "Lien insuffisant dans les preuves disponibles.",
        },
        "side": {
            "customer": "client",
            "receivable": "client",
            "client": "client",
            "supplier": "fournisseur",
            "payable": "fournisseur",
            "vendor": "fournisseur",
            "unknown": "non indique",
        },
        "owners": {
            "default_entity": "entite auditee",
            "default_counterparty": "contrepartie",
            "counterparty_confirmation": "{entity}; confirmation eventuelle du solde {counterparty}",
        },
        "inventory_usage": {
            "open_items": "Population des postes ouverts ou a rapprocher.",
            "ledger": "Support de solde, grand livre, auxiliaire et ecritures internes.",
            "journal": "Ecritures comptables, clotures internes, virements internes et mouvements de periode.",
            "bank_statement": "Encaissements et paiements effectifs selon releve bancaire.",
            "payment_order": "Remises ou lots utiles pour relier banque et factures.",
            "factoring_statement": "Preuve factor, avance ou operateur externe.",
            "compensation_support": "Support de compensation ou netting documente.",
            "unknown": "Source recue mais non classee automatiquement.",
            "default": "Preuve disponible dans le dossier.",
        },
        "available": {
            "reconciled_strong": "La ligne est deja rapprochee dans le workpaper avec une preuve forte.",
            "probable_payment": "Releve bancaire et candidat de rapprochement deja presents dans le workpaper.",
            "accounting_support_needed": "Poste ouvert et ecriture comptable deja identifies.",
            "missing_evidence_needed": "Indice ou document de liaison deja identifie, mais non concluant.",
            "open_balance_confirmation": "Poste ouvert et support interne de solde deja presents.",
            "unresolved": "Poste ouvert present, sans lien suffisant dans les preuves disponibles.",
            "reference": " Reference: {reference}.",
        },
        "missing": {
            "reconciled_strong": "Aucun element manquant pour la demande ciblee.",
            "probable_payment": "Confirmation de l'affectation facture par facture du mouvement bancaire ou de la remise deja identifiee.",
            "accounting_support_needed": "Support externe ou explication documentee de l'ecriture comptable: banque, factor, compensation, virement interne ou cloture non cash.",
            "missing_evidence_needed": "Document necessaire pour transformer l'indice en preuve concluante.",
            "open_balance_confirmation": "Confirmation que le poste etait effectivement ouvert au {cutoff}, ou preuve ponctuelle d'une cloture non detectee.",
            "unresolved": "Reference source ou preuve ponctuelle de cloture non detectee.",
        },
        "actions": {
            "reconciled_strong": "Aucune demande operationnelle; conserver la reference dans le workpaper.",
            "probable_payment": "Confirmer ou corriger l'affectation proposee, avec montant affecte, remise/SEPA et date de paiement par facture.",
            "accounting_support_needed": "Envoyer seulement le support de contrepartie ou expliquer la nature de la cloture non cash; ne pas renvoyer grand livre/journal inchanges.",
            "missing_evidence_needed": "Indiquer quel document deja acquis cloture la ligne, ou fournir uniquement le document manquant indique.",
            "open_balance_confirmation": "Si ouvert: confirmer le solde. Si cloture: indiquer document de paiement/encaissement/factor/compensation et date.",
            "unresolved": "Fournir la reference facture/auxiliaire ou indiquer si la ligne est erronee; si cloturee, joindre le document de cloture.",
        },
        "instructions": {
            "no_reask": "Ne pas redemander les documents deja obtenus",
            "no_reask_body": "Le workpaper principal contient deja postes ouverts, grands livres, journal, releves bancaires/factor, remises et normalisations disponibles.",
            "targeted": "Demande ciblee",
            "targeted_body": "Pour chaque ligne, demander uniquement l'element manquant indique: affectation, support externe, confirmation du solde ouvert ou reference source.",
            "owner": "Destinataire",
            "owner_body": "{entity} doit d'abord confirmer/corriger avec le dossier. {counterparty} sert seulement pour une eventuelle confirmation de solde ou reconciliation externe au {cutoff}.",
        },
        "log_path": "Workbook demandes ciblees: {path}",
    },
    "de": {
        "sheet_names": {
            "instructions": "anweisungen",
            "summary": "zusammenfassung",
            "inventory": "verfuegbare_evidenzen",
            "reconciled_strong": "abgestimmt_stark",
            "probable_payment": "wahrscheinliche_zahlungen",
            "accounting_support_needed": "buchungen_zu_belegen",
            "missing_evidence_needed": "evidenzen_ergaenzen",
            "open_balance_confirmation": "offene_salden",
            "unresolved": "nicht_geloest",
        },
        "headers": {
            "principle": "prinzip",
            "guidance": "hinweis",
            "section": "abschnitt",
            "rows": "zeilen",
            "amount_total": "betrag_summe",
            "evidence_category": "evidenz_kategorie",
            "normalized_rows": "normalisierte_zeilen",
            "source_files": "quelldateien",
            "operational_use": "operative_verwendung",
            "row_id": "workpaper_zeilen_id",
            "side": "seite",
            "document": "dokument",
            "document_date": "dokumentdatum",
            "amount": "betrag",
            "operational_category": "operative_kategorie",
            "operational_criterion": "operatives_kriterium",
            "operational_owner": "operativer_empfaenger",
            "available_evidence": "bereits_verfuegbare_evidenz",
            "targeted_missing_item": "gezielt_fehlendes_element",
            "existing_reference": "bestehende_referenz",
            "requested_action": "angeforderte_aktion",
            "evidence_description": "evidenz_beschreibung",
        },
        "sections": {
            "reconciled_strong": "Abgestimmt mit starker Evidenz",
            "probable_payment": "Wahrscheinliche Zahlungen zuzuordnen",
            "accounting_support_needed": "Buchungen zu belegen",
            "missing_evidence_needed": "Evidenzen zu ergaenzen",
            "open_balance_confirmation": "Offene Salden zu bestaetigen",
            "unresolved": "Nicht geloest",
        },
        "criteria": {
            "reconciled_strong": "Externe Evidenz oder ausreichende Buchungs-Bank-Kette.",
            "probable_payment": "Wahrscheinliche Bankbewegung oder Zahlungsliste, Zuordnung noch nicht bestaetigt.",
            "accounting_support_needed": "Buchhalterische Ausbuchung ohne ausreichenden externen Beleg.",
            "missing_evidence_needed": "Hinweis vorhanden, aber abschliessende Evidenz fehlt.",
            "open_balance_confirmation": "Offener Saldo durch interne Evidenz gestuetzt.",
            "unresolved": "Unzureichende Verbindung in den verfuegbaren Evidenzen.",
        },
        "side": {
            "customer": "kunde",
            "receivable": "kunde",
            "client": "kunde",
            "supplier": "lieferant",
            "payable": "lieferant",
            "vendor": "lieferant",
            "unknown": "nicht angegeben",
        },
        "owners": {
            "default_entity": "gepruefte einheit",
            "default_counterparty": "gegenpartei",
            "counterparty_confirmation": "{entity}; ggf. Saldobestaetigung {counterparty}",
        },
        "inventory_usage": {
            "open_items": "Grundgesamtheit der offenen oder abzustimmenden Posten.",
            "ledger": "Saldo-, Hauptbuch-, Nebenbuch- und interne Buchungsnachweise.",
            "journal": "Buchungen, interne Ausbuchungen, Umbuchungen und Periodenbewegungen.",
            "bank_statement": "Tatsaechliche Ein- und Auszahlungen laut Bankauszug.",
            "payment_order": "Zahlungslisten oder Zahlungslaeufe zur Verbindung von Bank und Rechnungen.",
            "factoring_statement": "Evidenz des Factors, Vorschusses oder externen Operators.",
            "compensation_support": "Dokumentierter Aufrechnungs- oder Netting-Nachweis.",
            "unknown": "Quelle erfasst, aber nicht automatisch klassifiziert.",
            "default": "Im Dossier verfuegbare Evidenz.",
        },
        "available": {
            "reconciled_strong": "Die Zeile ist im Workpaper bereits mit starker Evidenz abgestimmt.",
            "probable_payment": "Bankauszug und wahrscheinlicher Match-Kandidat sind im Workpaper bereits vorhanden.",
            "accounting_support_needed": "Offener Posten und Buchung sind bereits identifiziert.",
            "missing_evidence_needed": "Hinweis oder Brueckenbeleg ist bereits identifiziert, aber nicht abschliessend.",
            "open_balance_confirmation": "Offener Posten und interne Saldounterstuetzung sind bereits vorhanden.",
            "unresolved": "Offener Posten vorhanden, ohne ausreichende Verbindung in den verfuegbaren Evidenzen.",
            "reference": " Referenz: {reference}.",
        },
        "missing": {
            "reconciled_strong": "Kein fehlendes Element fuer die gezielte Anfrage.",
            "probable_payment": "Bestaetigung der rechnungsweisen Zuordnung der bereits identifizierten Bankbewegung oder Zahlungsliste.",
            "accounting_support_needed": "Externer Beleg oder dokumentierte Erklaerung der Buchung: Bank, Factor, Aufrechnung, Umbuchung oder nicht zahlungswirksame Schliessung.",
            "missing_evidence_needed": "Dokument, das den Hinweis in abschliessende Evidenz ueberfuehrt.",
            "open_balance_confirmation": "Bestaetigung, dass der Posten am {cutoff} tatsaechlich offen war, oder konkreter Nachweis einer nicht erkannten Schliessung.",
            "unresolved": "Quellreferenz oder konkreter Nachweis einer nicht erkannten Schliessung.",
        },
        "actions": {
            "reconciled_strong": "Keine operative Anfrage; Referenz im Workpaper beibehalten.",
            "probable_payment": "Vorgeschlagene Zuordnung bestaetigen oder korrigieren, je Rechnung mit zugeordnetem Betrag, Zahlungsliste/SEPA und Zahlungsdatum.",
            "accounting_support_needed": "Nur den Gegenbeleg senden oder die nicht zahlungswirksame Schliessung erklaeren; unveraendertes Hauptbuch/Journal nicht erneut senden.",
            "missing_evidence_needed": "Angeben, welches bereits vorliegende Dokument die Zeile schliesst, oder nur das angeforderte fehlende Dokument liefern.",
            "open_balance_confirmation": "Wenn offen: Saldo bestaetigen. Wenn geschlossen: Zahlungs-/Einzugs-/Factor-/Aufrechnungsdokument und Datum angeben.",
            "unresolved": "Rechnungs-/Nebenbuchreferenz liefern oder angeben, ob die Zeile falsch ist; falls geschlossen, Schliessungsbeleg beifuegen.",
        },
        "instructions": {
            "no_reask": "Bereits erhaltene Dokumente nicht erneut anfordern",
            "no_reask_body": "Das Haupt-Workpaper enthaelt bereits offene Posten, Hauptbuecher, Journal, Bank-/Factor-Ausweise, Zahlungslisten und verfuegbare Normalisierungen.",
            "targeted": "Gezielte Anfrage",
            "targeted_body": "Je Zeile nur das angegebene fehlende Element anfordern: Zuordnung, externer Beleg, Bestaetigung offener Saldo oder Quellreferenz.",
            "owner": "Empfaenger",
            "owner_body": "{entity} soll zuerst anhand des Dossiers bestaetigen/korrigieren. {counterparty} wird nur fuer eine moegliche Saldobestaetigung oder externe Abstimmung zum {cutoff} benoetigt.",
        },
        "log_path": "Workbook gezielte Anfragen: {path}",
    },
    "es": {
        "sheet_names": {
            "instructions": "instrucciones",
            "summary": "resumen",
            "inventory": "evidencias_disponibles",
            "reconciled_strong": "conciliadas_prueba_fuerte",
            "probable_payment": "pagos_probables",
            "accounting_support_needed": "asientos_por_justificar",
            "missing_evidence_needed": "evidencias_por_completar",
            "open_balance_confirmation": "saldos_abiertos",
            "unresolved": "sin_resolver",
        },
        "headers": {
            "principle": "principio",
            "guidance": "indicación",
            "section": "sección",
            "rows": "líneas",
            "amount_total": "importe_total",
            "evidence_category": "categoría_evidencia",
            "normalized_rows": "líneas_normalizadas",
            "source_files": "archivos_fuente",
            "operational_use": "uso_operativo",
            "row_id": "id_línea_papel_trabajo",
            "side": "lado",
            "document": "documento",
            "document_date": "fecha_documento",
            "amount": "importe",
            "operational_category": "categoría_operativa",
            "operational_criterion": "criterio_operativo",
            "operational_owner": "destinatario_operativo",
            "available_evidence": "evidencia_disponible",
            "targeted_missing_item": "elemento_pendiente_dirigido",
            "existing_reference": "referencia_existente",
            "requested_action": "acción_solicitada",
            "evidence_description": "descripción_evidencia",
        },
        "sections": {
            "reconciled_strong": "Conciliadas con evidencia sólida",
            "probable_payment": "Pagos probables por asignar",
            "accounting_support_needed": "Asientos contables por justificar",
            "missing_evidence_needed": "Evidencias por completar",
            "open_balance_confirmation": "Saldos abiertos por confirmar",
            "unresolved": "Sin resolver",
        },
        "criteria": {
            "reconciled_strong": "Evidencia externa o cadena contable-bancaria suficiente.",
            "probable_payment": "Movimiento bancario o remesa probable cuya asignación todavía no está confirmada.",
            "accounting_support_needed": "Asiento contable de cierre sin respaldo externo suficiente.",
            "missing_evidence_needed": "Existe un indicio, pero falta evidencia concluyente.",
            "open_balance_confirmation": "Saldo abierto respaldado por evidencia interna.",
            "unresolved": "Vínculo insuficiente en las evidencias disponibles.",
        },
        "side": {
            "customer": "cliente",
            "receivable": "cliente",
            "client": "cliente",
            "supplier": "proveedor",
            "payable": "proveedor",
            "vendor": "proveedor",
            "unknown": "no indicado",
        },
        "owners": {
            "default_entity": "entidad auditada",
            "default_counterparty": "contraparte",
            "counterparty_confirmation": "{entity}; posible confirmación del saldo por {counterparty}",
        },
        "inventory_usage": {
            "open_items": "Población de partidas abiertas o por conciliar.",
            "ledger": "Respaldo de saldos, mayores, auxiliares y registros internos.",
            "journal": "Asientos contables, cierres internos, traspasos y movimientos del periodo.",
            "bank_statement": "Cobros y pagos efectivos según el extracto bancario.",
            "payment_order": "Órdenes o lotes de pago útiles para vincular bancos y facturas.",
            "factoring_statement": "Evidencia del factor, anticipo u operador externo.",
            "compensation_support": "Respaldo documentado de compensación o netting.",
            "unknown": "Fuente adquirida, pero no clasificada automáticamente.",
            "default": "Evidencia disponible en el expediente.",
        },
        "available": {
            "reconciled_strong": "La línea ya está conciliada en el papel de trabajo con evidencia sólida.",
            "probable_payment": "El extracto bancario y el candidato de coincidencia ya constan en el papel de trabajo.",
            "accounting_support_needed": "La partida abierta y el asiento contable ya están identificados.",
            "missing_evidence_needed": "Ya se identificó un indicio o documento puente, pero no es concluyente.",
            "open_balance_confirmation": "La partida abierta y el respaldo interno del saldo ya están disponibles.",
            "unresolved": "La partida abierta está disponible, pero no existe un vínculo suficiente en las evidencias adquiridas.",
            "reference": " Referencia: {reference}.",
        },
        "missing": {
            "reconciled_strong": "No falta ningún elemento para la solicitud dirigida.",
            "probable_payment": "Confirmación de la asignación factura por factura del movimiento bancario o del lote de pagos ya identificado.",
            "accounting_support_needed": "Respaldo externo o explicación documentada del asiento contable: banco, factor, compensación, traspaso o cierre sin efectivo.",
            "missing_evidence_needed": "Documento necesario para convertir el indicio en evidencia concluyente.",
            "open_balance_confirmation": "Confirmación de que la partida seguía abierta a {cutoff}, o evidencia concreta de un cierre no detectado.",
            "unresolved": "Referencia fuente o evidencia concreta de un cierre no detectado.",
        },
        "actions": {
            "reconciled_strong": "No se requiere ninguna solicitud operativa; conserve la referencia en el papel de trabajo.",
            "probable_payment": "Confirme o corrija la asignación propuesta e indique el importe asignado, el lote o SEPA y la fecha de pago de cada factura.",
            "accounting_support_needed": "Envíe únicamente el justificante de contrapartida o explique el cierre sin efectivo; no vuelva a enviar el mayor o el diario si no han cambiado.",
            "missing_evidence_needed": "Indique qué documento ya adquirido cierra la línea o proporcione únicamente el documento pendiente indicado.",
            "open_balance_confirmation": "Si está abierta, confirme el saldo. Si está cerrada, indique el documento de pago, cobro, factor o compensación y la fecha.",
            "unresolved": "Proporcione la referencia de la factura o del auxiliar, o indique si la línea es errónea; si está cerrada, adjunte el documento de cierre.",
        },
        "instructions": {
            "no_reask": "No vuelva a solicitar documentos ya adquiridos",
            "no_reask_body": "El papel de trabajo principal ya contiene las partidas abiertas, los mayores, el diario, los extractos bancarios o del factor, los lotes de pagos y las normalizaciones disponibles.",
            "targeted": "Solicitud dirigida",
            "targeted_body": "Para cada línea, solicite solo el elemento pendiente indicado: asignación, respaldo externo, confirmación de saldo abierto o referencia fuente.",
            "owner": "Destinatario",
            "owner_body": "{entity} debe confirmar o corregir primero con el expediente. {counterparty} solo se necesita para una posible confirmación de saldo o conciliación externa a {cutoff}.",
        },
        "log_path": "Libro de solicitudes dirigidas: {path}",
    },
    "en": {
        "sheet_names": {
            "instructions": "instructions",
            "summary": "summary",
            "inventory": "available_evidence",
            "reconciled_strong": "reconciled_strong",
            "probable_payment": "probable_payments",
            "accounting_support_needed": "accounting_support_needed",
            "missing_evidence_needed": "evidence_to_complete",
            "open_balance_confirmation": "open_balances",
            "unresolved": "unresolved",
        },
        "headers": {
            "principle": "principle",
            "guidance": "guidance",
            "section": "section",
            "rows": "rows",
            "amount_total": "amount_total",
            "evidence_category": "evidence_category",
            "normalized_rows": "normalized_rows",
            "source_files": "source_files",
            "operational_use": "operational_use",
            "row_id": "workpaper_row_id",
            "side": "side",
            "document": "document",
            "document_date": "document_date",
            "amount": "amount",
            "operational_category": "operational_category",
            "operational_criterion": "operational_criterion",
            "operational_owner": "operational_owner",
            "available_evidence": "available_evidence",
            "targeted_missing_item": "targeted_missing_item",
            "existing_reference": "existing_reference",
            "requested_action": "requested_action",
            "evidence_description": "evidence_description",
        },
        "sections": {
            "reconciled_strong": "Reconciled with strong evidence",
            "probable_payment": "Probable payments to allocate",
            "accounting_support_needed": "Accounting entries to support",
            "missing_evidence_needed": "Evidence to complete",
            "open_balance_confirmation": "Open balances to confirm",
            "unresolved": "Unresolved",
        },
        "criteria": {
            "reconciled_strong": "External evidence or sufficient accounting-bank chain.",
            "probable_payment": "Probable bank movement or batch, allocation not yet confirmed.",
            "accounting_support_needed": "Accounting closing entry without sufficient external support.",
            "missing_evidence_needed": "An indicator exists but conclusive evidence is missing.",
            "open_balance_confirmation": "Open balance supported by internal evidence.",
            "unresolved": "Insufficient link in available evidence.",
        },
        "side": {
            "customer": "customer",
            "receivable": "customer",
            "client": "customer",
            "supplier": "supplier",
            "payable": "supplier",
            "vendor": "supplier",
            "unknown": "not specified",
        },
        "owners": {
            "default_entity": "audited entity",
            "default_counterparty": "counterparty",
            "counterparty_confirmation": "{entity}; possible balance confirmation from {counterparty}",
        },
        "inventory_usage": {
            "open_items": "Open-item or disputed population.",
            "ledger": "Balance, subledger, ledger and internal posting support.",
            "journal": "Accounting entries, internal closings, transfers and period movements.",
            "bank_statement": "Actual receipts and payments from bank statement.",
            "payment_order": "Payment orders or batches used to connect bank and invoices.",
            "factoring_statement": "Factoring, advance or external operator evidence.",
            "compensation_support": "Documented set-off or netting support.",
            "unknown": "Source acquired but not automatically classified.",
            "default": "Evidence acquired in the file.",
        },
        "available": {
            "reconciled_strong": "The row is already reconciled in the workpaper with strong evidence.",
            "probable_payment": "Bank statement and match candidate already exist in the workpaper.",
            "accounting_support_needed": "Open item and accounting entry already identified.",
            "missing_evidence_needed": "Bridge document or indicator already identified, but not conclusive.",
            "open_balance_confirmation": "Open item and internal balance support already exist.",
            "unresolved": "Open item exists, with no sufficient link in the acquired evidence.",
            "reference": " Reference: {reference}.",
        },
        "missing": {
            "reconciled_strong": "No missing item for the targeted request.",
            "probable_payment": "Invoice-by-invoice allocation confirmation for the bank movement or payment batch already identified.",
            "accounting_support_needed": "External support or documented explanation for the accounting entry: bank, factor, set-off, transfer or non-cash closing.",
            "missing_evidence_needed": "Document required to turn the indicator into conclusive evidence.",
            "open_balance_confirmation": "Confirmation that the item was still open at {cutoff}, or specific evidence of an undetected closing.",
            "unresolved": "Source reference or specific evidence of an undetected closing.",
        },
        "actions": {
            "reconciled_strong": "No operational request; retain the reference in the workpaper.",
            "probable_payment": "Confirm or correct the proposed allocation, with allocated amount, batch/SEPA and payment date for each invoice.",
            "accounting_support_needed": "Send only the counterparty support or explain the non-cash closing; do not resend unchanged ledger/journal.",
            "missing_evidence_needed": "Indicate which already-acquired document closes the row, or provide only the missing document requested.",
            "open_balance_confirmation": "If open: confirm the balance. If closed: indicate payment/receipt/factor/set-off document and date.",
            "unresolved": "Provide invoice/subledger reference or indicate if the row is wrong; if closed, attach the closing document.",
        },
        "instructions": {
            "no_reask": "Do not request documents already acquired",
            "no_reask_body": "The main workpaper already contains available open items, ledgers, journal, bank/factor statements, payment batches and normalizations.",
            "targeted": "Targeted request",
            "targeted_body": "For each row, request only the indicated missing item: allocation, external support, open-balance confirmation or source reference.",
            "owner": "Recipient",
            "owner_body": "{entity} should first confirm/correct using the file. {counterparty} is only for possible balance confirmation or external reconciliation at {cutoff}.",
        },
        "log_path": "Targeted request workbook: {path}",
    },
}


@dataclass(frozen=True)
class MissingEvidenceRequestPack:
    """Targeted evidence requests and the source inventory behind them."""

    language: str
    instructions: list[dict[str, Any]]
    summary: list[dict[str, Any]]
    evidence_inventory: list[dict[str, Any]]
    request_sections: dict[str, list[dict[str, Any]]]


def clean_text(value: object) -> str:
    return " ".join(str(value or "").strip().split())


def text_for(language: object) -> dict[str, Any]:
    code = normalize_language(language)
    return TEXT.get(code, TEXT["en"])


def parse_decimal(value: object) -> Decimal:
    text = clean_text(value).replace(" ", "")
    if not text:
        return Decimal("0.00")
    if "," in text and "." in text:
        text = text.replace(".", "").replace(",", ".")
    else:
        text = text.replace(",", ".")
    try:
        return Decimal(text)
    except InvalidOperation:
        return Decimal("0.00")


def format_decimal(value: object) -> str:
    return f"{parse_decimal(value):.2f}"


def side_label(value: object, language: object) -> str:
    side = clean_text(value).lower()
    labels = text_for(language)["side"]
    return labels.get(side, labels["unknown"])


def load_sheet_rows(
    workbook_path: str | Path,
    sheet_name: str,
    *,
    retain_workbook_row: bool = False,
) -> list[dict[str, Any]]:
    workbook = load_workbook(workbook_path, read_only=True, data_only=True)
    if sheet_name not in workbook.sheetnames:
        return []
    sheet = workbook[sheet_name]
    header_row = next(sheet.iter_rows(min_row=1, max_row=1), None)
    if header_row is None:
        return []
    headers = [clean_text(cell.value) for cell in header_row]
    rows: list[dict[str, Any]] = []
    for workbook_row, values in enumerate(
        sheet.iter_rows(min_row=2, values_only=True), start=2
    ):
        if not any(value not in (None, "") for value in values):
            continue
        row = dict(zip(headers, values))
        if retain_workbook_row:
            row["workbook_row"] = workbook_row
        rows.append(row)
    return rows


def load_reconciliation_context(
    workbook_path: str | Path,
) -> dict[str, list[dict[str, Any]]]:
    """Load the standard sheets needed for targeted evidence requests."""

    return {
        "reconciliation_rows": load_sheet_rows(
            workbook_path, "Reconciliation detail", retain_workbook_row=True
        ),
        "source_inventory": load_sheet_rows(workbook_path, "Source inventory"),
        "normalized_records": load_sheet_rows(workbook_path, "Normalized records"),
        "post_cutoff_candidates": load_sheet_rows(
            workbook_path, "Post-cutoff candidates"
        ),
    }


def source_files_by_role(
    source_inventory: list[dict[str, Any]],
    normalized_records: list[dict[str, Any]],
) -> dict[str, set[str]]:
    files_by_role: dict[str, set[str]] = {}
    for row in [*source_inventory, *normalized_records]:
        role = clean_text(row.get("source_role"))
        source_file = clean_text(row.get("source_file"))
        if role and source_file:
            files_by_role.setdefault(role, set()).add(source_file)
    return files_by_role


def record_counts_by_role(normalized_records: list[dict[str, Any]]) -> dict[str, int]:
    counts: dict[str, int] = {}
    for row in normalized_records:
        role = clean_text(row.get("source_role")) or "unknown"
        counts[role] = counts.get(role, 0) + 1
    return counts


def evidence_usage(role: str, language: object) -> str:
    inventory_usage = text_for(language)["inventory_usage"]
    return inventory_usage.get(role, inventory_usage["default"])


def evidence_inventory_rows(
    source_inventory: list[dict[str, Any]],
    normalized_records: list[dict[str, Any]],
    *,
    language: object,
) -> list[dict[str, Any]]:
    files_by_role = source_files_by_role(source_inventory, normalized_records)
    counts_by_role = record_counts_by_role(normalized_records)
    roles = sorted(set(files_by_role) | set(counts_by_role))
    return [
        {
            "evidence_category": evidence_usage(role, language).split(".", 1)[0],
            "normalized_rows": counts_by_role.get(role, 0),
            "source_files": "; ".join(sorted(files_by_role.get(role, set()))),
            "operational_use": evidence_usage(role, language),
        }
        for role in roles
    ]


def operational_owner(
    section: str,
    *,
    entity_name: str,
    counterparty_name: str,
    language: object,
) -> str:
    texts = text_for(language)
    owners = texts["owners"]
    entity = clean_text(entity_name) or owners["default_entity"]
    counterparty = clean_text(counterparty_name) or owners["default_counterparty"]
    if section == "open_balance_confirmation":
        return owners["counterparty_confirmation"].format(
            entity=entity, counterparty=counterparty
        )
    return entity


def section_for_row(row: dict[str, Any]) -> str:
    status = clean_text(row.get("reconciliation_status"))
    rule = clean_text(row.get("rule_applied"))
    if status == "closed":
        return "reconciled_strong"
    if status == "probable_payment":
        return "probable_payment"
    if status == "needs_evidence" and rule == "internal_closure_without_external":
        return "accounting_support_needed"
    if status in {"needs_evidence", "partially_paid"}:
        return "missing_evidence_needed"
    if status == "open_supported":
        return "open_balance_confirmation"
    if status == "unresolved":
        return "unresolved"
    return ""


def existing_reference(row: dict[str, Any]) -> str:
    for field in (
        "probable_bank_reference",
        "matched_evidence_reference",
        "supporting_bank_reference",
        "evidence_reference",
        "source_reference",
    ):
        value = clean_text(row.get(field))
        if value:
            return value
    source_file = clean_text(row.get("source_file"))
    workbook_row = clean_text(row.get("workbook_row"))
    if source_file and workbook_row:
        return f"{source_file}; workpaper row {workbook_row}"
    return source_file


def existing_description(row: dict[str, Any]) -> str:
    for field in (
        "probable_bank_description",
        "matched_evidence_description",
        "supporting_bank_description",
        "evidence_description",
        "description",
    ):
        value = clean_text(row.get(field))
        if value:
            return value
    return ""


def available_text(
    section: str,
    row: dict[str, Any],
    *,
    language: object,
) -> str:
    texts = text_for(language)
    available = texts["available"][section]
    reference = existing_reference(row)
    if reference:
        available += texts["available"]["reference"].format(reference=reference)
    return available


def missing_text(
    section: str,
    *,
    cutoff_date: object,
    language: object,
) -> str:
    cutoff = clean_text(cutoff_date) or "cut-off"
    return text_for(language)["missing"][section].format(cutoff=cutoff)


def requested_action(section: str, *, language: object) -> str:
    return text_for(language)["actions"][section]


def request_row(
    row: dict[str, Any],
    *,
    section: str,
    entity_name: str,
    counterparty_name: str,
    cutoff_date: object,
    language: object,
) -> dict[str, Any]:
    texts = text_for(language)
    result = {
        "row_id": clean_text(row.get("record_id")),
        "side": side_label(row.get("expected_side"), language),
        "document": clean_text(row.get("document_no")),
        "document_date": clean_text(row.get("document_date")),
        "amount": format_decimal(row.get("amount")),
        "operational_category": texts["sections"][section],
        "operational_criterion": texts["criteria"][section],
        "operational_owner": operational_owner(
            section,
            entity_name=entity_name,
            counterparty_name=counterparty_name,
            language=language,
        ),
        "available_evidence": available_text(section, row, language=language),
        "targeted_missing_item": missing_text(
            section, cutoff_date=cutoff_date, language=language
        ),
        "existing_reference": existing_reference(row),
        "requested_action": requested_action(section, language=language),
        "evidence_description": existing_description(row),
    }
    if clean_text(row.get("reconciliation_status")) == "partially_paid":
        paid = format_decimal(row.get("allocated_amount"))
        residual = format_decimal(row.get("residual_amount"))
        partial_texts = {
            "it": (
                "Pagamento parziale documentato: {paid}.",
                "Per il solo residuo di {residual}, acquisire evidenza del saldo oppure conferma che resta aperto alla data di riferimento.",
            ),
            "en": (
                "Documented partial payment: {paid}.",
                "For the remaining {residual} only, obtain settlement evidence or confirmation that it remains open at the cut-off.",
            ),
            "fr": (
                "Paiement partiel documenté : {paid}.",
                "Pour le seul solde de {residual}, obtenir une preuve de règlement ou la confirmation qu’il reste ouvert à la date de référence.",
            ),
            "de": (
                "Belegte Teilzahlung: {paid}.",
                "Nur für den Restbetrag von {residual} einen Zahlungsnachweis oder die Bestätigung einholen, dass er am Stichtag noch offen ist.",
            ),
            "es": (
                "Pago parcial documentado: {paid}.",
                "Solo para el saldo de {residual}, obtener evidencia de liquidación o confirmación de que sigue pendiente a la fecha de corte.",
            ),
        }
        available, action = partial_texts[normalize_language(language)]
        result["amount"] = residual
        result["available_evidence"] = available.format(paid=paid)
        if reference := existing_reference(row):
            result["available_evidence"] += texts["available"]["reference"].format(
                reference=reference
            )
        result["targeted_missing_item"] = action.format(residual=residual)
        result["requested_action"] = result["targeted_missing_item"]
    if clean_text(row.get("rule_applied")) == "accounting_perimeter_mismatch":
        actions = {
            "it": "Il candidato non rispetta il perimetro verificato (soggetto, controparte, valuta o unità). Confermare i dati delle due fonti e fornire il riscontro documentale necessario; importi numericamente uguali non dimostrano la chiusura.",
            "en": "The candidate does not share the reviewed entity, party, currency or unit. Confirm both source records and provide the necessary documentary support; numerically equal amounts do not establish settlement.",
            "fr": "Le candidat ne respecte pas le périmètre vérifié (entité, contrepartie, devise ou unité). Confirmer les deux sources et fournir le justificatif nécessaire ; des montants numériquement égaux ne prouvent pas le règlement.",
            "de": "Der Kandidat entspricht nicht dem geprüften Umfang (Unternehmen, Gegenpartei, Währung oder Einheit). Beide Quellen bestätigen und erforderliche Belege vorlegen; numerisch gleiche Beträge beweisen keine Zahlung.",
            "es": "El candidato no comparte el perímetro revisado (entidad, contraparte, moneda o unidad). Confirmar ambas fuentes y aportar el soporte necesario; importes numéricamente iguales no demuestran la liquidación.",
        }
        result["targeted_missing_item"] = actions[normalize_language(language)]
        result["requested_action"] = result["targeted_missing_item"]
    return result


def build_instructions(
    *,
    entity_name: str,
    counterparty_name: str,
    cutoff_date: object,
    language: object,
) -> list[dict[str, Any]]:
    texts = text_for(language)
    instructions = texts["instructions"]
    owners = texts["owners"]
    entity = clean_text(entity_name) or owners["default_entity"]
    counterparty = clean_text(counterparty_name) or owners["default_counterparty"]
    cutoff = clean_text(cutoff_date) or "cut-off"
    return [
        {
            "principle": instructions["no_reask"],
            "guidance": instructions["no_reask_body"],
        },
        {
            "principle": instructions["targeted"],
            "guidance": instructions["targeted_body"],
        },
        {
            "principle": instructions["owner"],
            "guidance": instructions["owner_body"].format(
                entity=entity, counterparty=counterparty, cutoff=cutoff
            ),
        },
    ]


def build_summary(
    request_sections: dict[str, list[dict[str, Any]]],
    *,
    language: object,
) -> list[dict[str, Any]]:
    texts = text_for(language)
    rows: list[dict[str, Any]] = []
    for section in SECTION_ORDER:
        section_rows = request_sections.get(section, [])
        amount_total = sum(parse_decimal(row.get("amount")) for row in section_rows)
        rows.append(
            {
                "section": texts["sections"][section],
                "rows": len(section_rows),
                "amount_total": f"{amount_total:.2f}",
            }
        )
    return rows


def build_missing_evidence_request_pack(
    reconciliation_rows: list[dict[str, Any]],
    *,
    source_inventory: list[dict[str, Any]] | None = None,
    normalized_records: list[dict[str, Any]] | None = None,
    post_cutoff_candidates: list[dict[str, Any]] | None = None,
    entity_name: str = "",
    counterparty_name: str = "",
    cutoff_date: object = "",
    language: object = "it",
) -> MissingEvidenceRequestPack:
    """Build a localized operational request pack from technical workpaper rows."""

    output_language = normalize_language(language)
    request_sections: dict[str, list[dict[str, Any]]] = {
        section: [] for section in SECTION_ORDER
    }
    for row in reconciliation_rows:
        section = section_for_row(row)
        if not section:
            continue
        request = request_row(
            row,
            section=section,
            entity_name=entity_name,
            counterparty_name=counterparty_name,
            cutoff_date=cutoff_date,
            language=output_language,
        )
        later = [
            candidate
            for candidate in post_cutoff_candidates or []
            if candidate.get("open_record_id") == row.get("record_id")
        ]
        if later and section != "reconciled_strong":
            templates = {
                "it": (
                    "Evidenza candidata già acquisita successiva al cut-off: {details}.",
                    "Confermare il saldo al {cutoff}; l'evidenza successiva non dimostra la chiusura a quella data. Se si dichiara una chiusura precedente, indicare solo il relativo documento mancante.",
                ),
                "en": (
                    "Later candidate evidence already held: {details}.",
                    "Confirm the balance at {cutoff}; later evidence does not establish settlement at that date. If earlier settlement is claimed, identify only its missing supporting document.",
                ),
                "fr": (
                    "Élément candidat postérieur déjà disponible : {details}.",
                    "Confirmer le solde au {cutoff} ; cet élément postérieur ne prouve pas le règlement à cette date. Si un règlement antérieur est déclaré, indiquer uniquement son justificatif manquant.",
                ),
                "de": (
                    "Bereits vorliegender späterer Belegkandidat: {details}.",
                    "Saldo zum {cutoff} bestätigen; der spätere Beleg beweist keine Zahlung zum Stichtag. Wird eine frühere Zahlung erklärt, nur den fehlenden Beleg dafür angeben.",
                ),
                "es": (
                    "Evidencia candidata posterior ya disponible: {details}.",
                    "Confirmar el saldo al {cutoff}; la evidencia posterior no demuestra la liquidación a esa fecha. Si se declara una liquidación anterior, indicar solo su justificante pendiente.",
                ),
            }
            available, action = templates[output_language]
            references = [
                f"{clean_text(item.get('evidence_date'))}; {clean_text(item.get('evidence_source_file'))}; "
                f"page={clean_text(item.get('evidence_source_page'))}; row={clean_text(item.get('evidence_source_row'))}; "
                f"id={clean_text(item.get('evidence_record_id'))}"
                for item in later
            ]
            request["available_evidence"] += " " + available.format(
                details=" | ".join(references)
            )
            request["existing_reference"] += " | " + " | ".join(references)
            request["targeted_missing_item"] = action.format(
                cutoff=clean_text(cutoff_date) or "cut-off"
            )
            request["requested_action"] = request["targeted_missing_item"]
        request_sections[section].append(request)
    return MissingEvidenceRequestPack(
        language=output_language,
        instructions=build_instructions(
            entity_name=entity_name,
            counterparty_name=counterparty_name,
            cutoff_date=cutoff_date,
            language=output_language,
        ),
        summary=build_summary(request_sections, language=output_language),
        evidence_inventory=evidence_inventory_rows(
            source_inventory or [],
            normalized_records or [],
            language=output_language,
        ),
        request_sections=request_sections,
    )


def sheet_headers(
    rows: list[dict[str, Any]],
    fields: tuple[str, ...],
) -> list[str]:
    headers = list(fields)
    extras = sorted({key for row in rows for key in row if key not in fields})
    headers.extend(extras)
    return headers


def write_sheet(
    workbook: Workbook,
    name: str,
    rows: list[dict[str, Any]],
    fields: tuple[str, ...],
    *,
    language: object,
) -> None:
    texts = text_for(language)
    sheet = workbook.create_sheet(title=name[:31])
    keys = sheet_headers(rows, fields)
    display_headers = [texts["headers"].get(key, key) for key in keys]
    sheet.append(display_headers)
    for row in rows:
        sheet.append([row.get(key, "") for key in keys])
    fill = PatternFill(fill_type="solid", fgColor=HEADER_FILL)
    for cell in sheet[1]:
        cell.fill = fill
        cell.font = Font(color="FFFFFF", bold=True)
        cell.alignment = Alignment(wrap_text=True, vertical="top")
    for column_cells in sheet.columns:
        max_length = (
            max(
                len(clean_text(cell.value))
                for cell in column_cells
                if cell.value is not None
            )
            if any(cell.value is not None for cell in column_cells)
            else 8
        )
        adjusted_width = min(max(max_length + 2, 10), 60)
        sheet.column_dimensions[get_column_letter(column_cells[0].column)].width = (
            adjusted_width
        )
        for cell in column_cells:
            cell.alignment = Alignment(wrap_text=True, vertical="top")
    sheet.freeze_panes = "A2"


def write_missing_evidence_workbook(
    path: str | Path,
    pack: MissingEvidenceRequestPack,
) -> Path:
    """Write the localized targeted request workbook."""

    output_path = Path(path)
    output_path.parent.mkdir(parents=True, exist_ok=True)
    workbook = Workbook()
    workbook.remove(workbook.active)
    texts = text_for(pack.language)
    sheet_names = texts["sheet_names"]
    write_sheet(
        workbook,
        sheet_names["instructions"],
        pack.instructions,
        INSTRUCTION_FIELDS,
        language=pack.language,
    )
    write_sheet(
        workbook,
        sheet_names["summary"],
        pack.summary,
        SUMMARY_FIELDS,
        language=pack.language,
    )
    write_sheet(
        workbook,
        sheet_names["inventory"],
        pack.evidence_inventory,
        INVENTORY_FIELDS,
        language=pack.language,
    )
    for section in SECTION_ORDER:
        write_sheet(
            workbook,
            sheet_names[section],
            pack.request_sections.get(section, []),
            REQUEST_FIELDS,
            language=pack.language,
        )
    workbook.save(output_path)
    return output_path


def default_output_dir(workbook_path: str | Path) -> Path:
    return Path(workbook_path).resolve().parent


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(
        description="Build localized targeted evidence requests from a reconciliation workbook."
    )
    parser.add_argument("workbook", type=Path)
    parser.add_argument("--output-dir", type=Path)
    parser.add_argument("--output-name", default=DEFAULT_EXCEL_NAME)
    parser.add_argument("--entity-name", default="")
    parser.add_argument("--counterparty-name", default="")
    parser.add_argument("--cutoff-date", default="")
    parser.add_argument("--language", default="it")
    parser.add_argument("--client-engagement", type=Path, required=True)
    args = parser.parse_args(argv)

    logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
    language = normalize_language(args.language)
    output_dir = args.output_dir or default_output_dir(args.workbook)
    output_path = output_dir / args.output_name
    try:
        load_client_engagement_context_file(
            args.client_engagement,
            expected_workflow_id="open-item-reconciliation",
            input_paths=[args.workbook],
            output_dir=output_path,
        )
    except AssuranceContractError as exc:
        parser.error(str(exc))
    context = load_reconciliation_context(args.workbook)
    pack = build_missing_evidence_request_pack(
        context["reconciliation_rows"],
        source_inventory=context["source_inventory"],
        normalized_records=context["normalized_records"],
        post_cutoff_candidates=context["post_cutoff_candidates"],
        entity_name=args.entity_name,
        counterparty_name=args.counterparty_name,
        cutoff_date=args.cutoff_date,
        language=language,
    )
    output_path = write_missing_evidence_workbook(output_path, pack)
    LOGGER.info(text_for(language)["log_path"].format(path=output_path))
    for row in pack.summary:
        LOGGER.info(
            "%s: %s / %s",
            row["section"],
            row["rows"],
            row["amount_total"],
        )
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

SHA-256: a7a8bb44aca8d25c6d4ad4d1b447db9d205fc2c6873e990a7f4b8d1e65aa49ec