feat(v2): multi-tranche DB schema + gate cumulativo 5 voci Cecilia

A1 migrations.py:
- remission_practice DROP uq_application + ADD sequence_number/period_label/suggested_instructor_id
- UNIQUE composita (application_id, sequence_number)
- partial index idx_remission_practice_unassigned su assigned_instructor_id NULL
- nuova tabella remission_custom_check_value (storage_path/mime/size/sha256 allineata adapter)

A2 models.py + templates.py:
- RemissionPractice: UniqueConstraint composita, campi multi-tranche, relationship custom_checks
- classe RemissionCustomCheckValue
- RESTART_TEMPLATE schema_version=2, max_tranches=2, custom_checks esempio
  (antiriciclaggio required no-doc, polizza_fidejussoria optional con-doc)
- upgrade_schema_to_v2 idempotente per snapshot v1 esistenti

A3 _compute_gate_check(db, practice) CUMULATIVO:
- max_remission_global = min(cap_pct * erogato, cap_abs)
- already_approved = func.sum(approved_remission) su tranche APPROVED precedenti
  dello stesso application_id con sequence_number < corrente
- max_remission_this_tranche = max(0, global - already_approved)
- pre_check_admissible = min(grand_total_declared, this_tranche)  [voce 2 Cecilia]
- remission_due = min(effective_total, this_tranche)
- residuo_da_restituire = erogato - already_approved - remission_due (cumulativo)
- output totals esteso: sequence_number, tranches_count, tranches_max
- signature (db, practice) - aggiornati 6 call site in practices/instructor/verbale

Test su NAPOLI SAS: erogato 17K, cap 8500, tranche 1 approvata 467.14EUR,
tranche 2 vuota -> residuo disponibile 8032.86EUR, residuo_da_restituire 16532.86EUR.
This commit is contained in:
BFLOWS
2026-04-18 17:35:56 +02:00
parent 6c089fb7b2
commit 25215f388b
7 changed files with 520 additions and 57 deletions

View File

@@ -1,11 +1,14 @@
"""
Template schemi precompilati per bandi noti.
RE-START: il bando del xlsx di Cecilia, base per la prima iterazione.
v2 (2026-04-18): schema_version=2, max_tranches, custom_checks[]
"""
RESTART_TEMPLATE = {
"version": "1.0",
"template_id": "RESTART_V1",
"version": "2.0",
"schema_version": 2,
"template_id": "RESTART_V2",
"template_label": "RE-START (fondo prestiti con remissione del debito)",
"sections": [
{
@@ -115,6 +118,22 @@ RESTART_TEMPLATE = {
],
},
],
"custom_checks": [
{
"code": "antiriciclaggio",
"label": "Dichiarazione antiriciclaggio",
"description": "Dichiaro che il beneficiario rispetta la normativa antiriciclaggio (D.Lgs. 231/2007 e s.m.i.) e che i soggetti coinvolti non sono iscritti in liste sanzionatorie.",
"requires_document": False,
"required": True,
},
{
"code": "polizza_fidejussoria",
"label": "Polizza fidejussoria",
"description": "Allegare copia della polizza fidejussoria a garanzia dell'importo erogato (se richiesta da bando).",
"requires_document": True,
"required": False,
},
],
"gate_rules": {
"amount_range": {"min": 5000, "max": 25000},
"cap_pct_erogato": 0.5,
@@ -125,5 +144,35 @@ RESTART_TEMPLATE = {
"require_at_least_one_invoice_per_nonzero_category": True,
"require_ula_above_threshold": True,
"require_all_documents_resolved": True,
"max_tranches": 2, # v2: superadmin configurabile, default 1
},
}
def upgrade_schema_to_v2(schema_json: dict) -> dict:
"""Upgrade in-place di schema v1 a v2.
- Aggiunge schema_version=2 se mancante
- Aggiunge gate_rules.max_tranches=1 se mancante
- Aggiunge custom_checks=[] se mancante
- Assicura ula_section.enabled presente (default True se ula_block esiste)
Idempotente: se lo schema e gia v2, no-op.
"""
if not isinstance(schema_json, dict):
return schema_json
changed = False
if schema_json.get("schema_version", 1) < 2:
schema_json["schema_version"] = 2
changed = True
gate = schema_json.setdefault("gate_rules", {})
if "max_tranches" not in gate:
gate["max_tranches"] = 1
changed = True
if "custom_checks" not in schema_json:
schema_json["custom_checks"] = []
changed = True
# ula_section.enabled esplicito
for sec in schema_json.get("sections", []):
if sec.get("type") == "ula_block" and "enabled" not in sec:
sec["enabled"] = True
changed = True
return schema_json