feat(istruttoria): verifica riga-per-riga con dual declared/verified
Replica il workflow del foglio Excel originale (REMISSIONE_DEL_DEBITO_5888.xlsm).
Istruttore ora verifica ogni fattura, ogni dipendente ULA, ogni documento singolarmente
invece di accettare/respingere la pratica intera.
Modello dati - nuove colonne su 3 tabelle:
- remission_invoice: taxable_verified, vat_verified, total_verified,
verification_status (PENDING/AMMESSA/PARZIALE/RESPINTA), verification_notes,
date_checks (JSONB con invoice_in_period/payment_in_period), verified_by, verified_at
- remission_ula_employee: fte_pct_verified, verification_status, verification_notes,
verified_by, verified_at
- remission_document: verification_status (PENDING/VALIDO/NON_VALIDO/SCADUTO),
verification_notes, verified_by, verified_at
- remission_practice: instructor_final_notes, instructor_checklist (JSONB 3 gate SI/NO),
verbale_date
Nuovi endpoint:
- PUT /instructor/{id}/invoices/{inv_id}/verify (status + rettifica importi + note)
- PUT /instructor/{id}/ula-employees/{emp_id}/verify (rettifica FTE + note)
- PUT /instructor/{id}/documents/{doc_code}/verify (VALIDO/NON_VALIDO/SCADUTO + note)
- PUT /instructor/{id}/final-notes (note sintetiche + checklist)
Ricalcolo gate_check dual track:
- grand_total_declared: sempre (importo richiesto dal beneficiario)
- grand_total_verified: somma solo fatture AMMESSA/PARZIALE (se PARZIALE usa verified)
- remission_due: usa verified se any_verified=True, altrimenti declared (backward compat)
- residuo_da_restituire: amount_erogato - remission_due
- flag any_verified e all_verified per gating decisione finale
_auto_check_dates: fattura in periodo? pagamento in periodo?
Legge period_start e period_end da schema.gate_rules (superadmin editor).
Template: aggiunto period_start/period_end_date come campi 'editable_by superadmin'
nella sezione general static_fields.
Schema editor FE (BandoRendicontazioneSchemaEdit): aggiunto Calendar period_start
accanto a period_end in section gate rules. period_start_rule dropdown per logica
(erogato_date|fixed) resta; period_start data fissa usata dal check.
This commit is contained in:
@@ -52,20 +52,49 @@ def _ensure_editable(practice: RemissionPractice):
|
||||
|
||||
|
||||
def _compute_gate_check(practice: RemissionPractice) -> GateCheckResult:
|
||||
"""Valuta le gate_rules dello schema snapshot contro il contenuto della pratica."""
|
||||
"""Valuta le gate_rules dello schema snapshot contro il contenuto della pratica.
|
||||
Calcola:
|
||||
- per_category_declared: totali dichiarati dal beneficiario (sempre)
|
||||
- per_category_verified: totali verificati dall istruttore (solo AMMESSA/PARZIALE)
|
||||
- grand_total: totale dichiarato (rif. beneficiario)
|
||||
- grand_total_verified: totale verificato (rif. remissione finale)
|
||||
"""
|
||||
rules = practice.schema_snapshot.get("gate_rules", {}) or {}
|
||||
sections = practice.schema_snapshot.get("sections", []) or []
|
||||
|
||||
# totali per categoria
|
||||
per_category = {}
|
||||
grand_total = Decimal("0")
|
||||
iva_ordinario_only_taxable = rules.get("iva_ordinario_imponibile_only", True)
|
||||
use_taxable_only = (practice.iva_regime == "ORDINARIO" and iva_ordinario_only_taxable)
|
||||
|
||||
per_category_declared = {}
|
||||
per_category_verified = {}
|
||||
grand_total = Decimal("0")
|
||||
grand_total_verified = Decimal("0")
|
||||
|
||||
any_verified = False
|
||||
all_verified = len(practice.invoices) > 0
|
||||
|
||||
for inv in practice.invoices:
|
||||
amt = inv.taxable if use_taxable_only else inv.total
|
||||
per_category[inv.category_code] = per_category.get(inv.category_code, Decimal("0")) + amt
|
||||
grand_total += amt
|
||||
# Dichiarato (sempre)
|
||||
amt_decl = inv.taxable if use_taxable_only else inv.total
|
||||
per_category_declared[inv.category_code] = per_category_declared.get(inv.category_code, Decimal("0")) + amt_decl
|
||||
grand_total += amt_decl
|
||||
|
||||
# Verificato (solo se stato AMMESSA o PARZIALE)
|
||||
if inv.verification_status in ("AMMESSA", "PARZIALE"):
|
||||
any_verified = True
|
||||
# se PARZIALE usa i valori verified; se AMMESSA ma verified sono null, usa dichiarato
|
||||
if inv.verification_status == "PARZIALE":
|
||||
tax_v = inv.taxable_verified if inv.taxable_verified is not None else inv.taxable
|
||||
tot_v = inv.total_verified if inv.total_verified is not None else inv.total
|
||||
else: # AMMESSA
|
||||
tax_v = inv.taxable_verified if inv.taxable_verified is not None else inv.taxable
|
||||
tot_v = inv.total_verified if inv.total_verified is not None else inv.total
|
||||
amt_ver = tax_v if use_taxable_only else tot_v
|
||||
per_category_verified[inv.category_code] = per_category_verified.get(inv.category_code, Decimal("0")) + amt_ver
|
||||
grand_total_verified += amt_ver
|
||||
elif inv.verification_status == "PENDING":
|
||||
all_verified = False
|
||||
# RESPINTA: non contribuisce ai verified
|
||||
|
||||
# cap remissione
|
||||
amt_erogato = practice.amount_erogato
|
||||
@@ -73,6 +102,14 @@ def _compute_gate_check(practice: RemissionPractice) -> GateCheckResult:
|
||||
cap_abs = Decimal(str(rules.get("cap_absolute", 12500)))
|
||||
max_remission = min(cap_pct * amt_erogato, cap_abs)
|
||||
|
||||
# Se almeno 1 verifica fatta -> uso grand_total_verified per remission_due
|
||||
# altrimenti uso grand_total (dichiarato) per preview pre-istruttoria
|
||||
effective_total = grand_total_verified if any_verified else grand_total
|
||||
remission_due = min(effective_total, max_remission)
|
||||
|
||||
# Per compatibilità: per_category e grand_total restano "dichiarato"
|
||||
per_category = per_category_declared
|
||||
|
||||
checks = []
|
||||
|
||||
# Check 1: regime IVA scelto
|
||||
@@ -140,17 +177,22 @@ def _compute_gate_check(practice: RemissionPractice) -> GateCheckResult:
|
||||
"detail": f"Erogato: {amt_erogato} EUR"
|
||||
})
|
||||
|
||||
remission_due = min(grand_total, max_remission)
|
||||
|
||||
return GateCheckResult(
|
||||
passed=all(c["passed"] for c in checks),
|
||||
checks=checks,
|
||||
totals={
|
||||
"per_category": {k: float(v) for k, v in per_category.items()},
|
||||
"per_category_declared": {k: float(v) for k, v in per_category_declared.items()},
|
||||
"per_category_verified": {k: float(v) for k, v in per_category_verified.items()},
|
||||
"grand_total": float(grand_total),
|
||||
"grand_total_declared": float(grand_total),
|
||||
"grand_total_verified": float(grand_total_verified),
|
||||
"max_remission": float(max_remission),
|
||||
"remission_due": float(remission_due),
|
||||
"amount_erogato": float(amt_erogato)
|
||||
"amount_erogato": float(amt_erogato),
|
||||
"any_verified": any_verified,
|
||||
"all_verified": all_verified,
|
||||
"residuo_da_restituire": float(max(amt_erogato - Decimal(str(remission_due)), Decimal("0")))
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user