From 3021792c31f171be2ed31f83cd702d293267da4e Mon Sep 17 00:00:00 2001 From: BFLOWS Date: Sat, 18 Apr 2026 17:53:04 +0200 Subject: [PATCH] feat(v2): verbale istruttoria PDF con tranche N/M + custom_checks + 5 voci Cecilia + storico B6 routers/verbale.py: - _build_context arricchito con custom_checks_merged (schema+values da RemissionCustomCheckValue) - previous_tranches: elenco tranche APPROVED precedenti con cumulative progressivo - max_tranches_snapshot letto dallo schema_snapshot.gate_rules.max_tranches - filename include _t{sequence_number}.pdf B6 templates_jinja/verbale_istruttoria.html: - Header: 'Tranche N/M' + period_label dopo numero pratica - Meta-grid: riga 'Tranche / fase' quando max_tranches > 1 - Nuova sezione 'Controlli aggiuntivi' (dopo verifica documenti): tabella label, obbligatorio, dichiarato SI/NO, doc allegato SI/NO, validazione, note - Sezione 'Storico tranches precedenti' (solo se sequence > 1): tabella con cumulativo progressivo - Box totali riscritto con **5 VOCI UFFICIALI CECILIA**: (1) Importo massimo ammissibile (cap globale) + gia approvato tranche precedenti (2) Richiesto pre-controllo = pre_check_admissible (3) Ammesso post-controllo = remission_due (4) Importo finanziamento erogato + tranches count/max (5) Residuo da restituire = erogato - approvato_prec - ammesso - Box 'REMISSIONE APPROVATA PER QUESTA TRANCHE' evidenziato quando APPROVED Test E2E: verbale T1 APPROVED 29.3KB con tutte sezioni presenti. Verbale T2 simulata con storico T1 e cap tranche 2 correttamente calcolato. --- app/routers/verbale.py | 52 +++++- app/templates_jinja/verbale_istruttoria.html | 162 +++++++++++++++---- 2 files changed, 181 insertions(+), 33 deletions(-) diff --git a/app/routers/verbale.py b/app/routers/verbale.py index c28a3ae..08ef456 100644 --- a/app/routers/verbale.py +++ b/app/routers/verbale.py @@ -16,7 +16,7 @@ from sqlalchemy import text from ..db import get_db from ..auth import AuthUser, get_current_user -from ..models import RemissionPractice +from ..models import RemissionPractice, RemissionCustomCheckValue from .practices import _compute_gate_check router = APIRouter(prefix="/api/remission-practices/instructor", tags=["verbale"]) @@ -177,6 +177,50 @@ def _build_context(db: Session, practice: RemissionPractice, user: AuthUser) -> """), {"uid": user.user_id}).scalar() instructor_name = row + # v2: custom_checks merged (schema_snapshot.custom_checks[] + RemissionCustomCheckValue) + check_defs = practice.schema_snapshot.get("custom_checks") or [] + values_by_code = {v.check_code: v for v in practice.custom_checks} + custom_checks_merged = [] + for d in check_defs: + code = d.get("code") + val = values_by_code.get(code) + custom_checks_merged.append({ + "code": code, + "label": d.get("label"), + "description": d.get("description"), + "requires_document": bool(d.get("requires_document")), + "required": bool(d.get("required")), + "beneficiary_declared": bool(val.beneficiary_declared) if val else False, + "declared_at": val.declared_at if val else None, + "has_document": bool(val and val.storage_path), + "verification_status": (val.verification_status if val else "PENDING"), + "verification_notes": (val.verification_notes if val else None), + }) + + # v2: storico tranche precedenti APPROVED (se sequence > 1) + previous_tranches = [] + cumulative_approved = 0.0 + if practice.sequence_number > 1: + prevs = db.query(RemissionPractice).filter( + RemissionPractice.application_id == practice.application_id, + RemissionPractice.sequence_number < practice.sequence_number, + RemissionPractice.status == "APPROVED", + ).order_by(RemissionPractice.sequence_number).all() + for pv in prevs: + amt = float(pv.approved_remission or 0) + cumulative_approved += amt + previous_tranches.append({ + "sequence_number": pv.sequence_number, + "period_label": pv.period_label, + "reviewed_at": pv.reviewed_at, + "approved_remission": amt, + "cumulative": cumulative_approved, + }) + + # v2 max_tranches dallo schema_snapshot (o dal bando corrente, fallback 1) + snap_rules = practice.schema_snapshot.get("gate_rules") or {} + max_tranches_snapshot = int(snap_rules.get("max_tranches") or totals.get("tranches_max") or 1) + return { "practice": practice, "totals": totals, @@ -196,6 +240,10 @@ def _build_context(db: Session, practice: RemissionPractice, user: AuthUser) -> "company": company, "instructor_name": instructor_name, "generated_at": datetime.now().strftime("%d/%m/%Y"), + # v2 + "custom_checks_merged": custom_checks_merged, + "previous_tranches": previous_tranches, + "max_tranches_snapshot": max_tranches_snapshot, } @@ -234,7 +282,7 @@ def verbale_pdf( practice, html = _render_html(db, practice_id, user) pdf_bytes = WeasyHTML(string=html).write_pdf() - filename = f"verbale_istruttoria_pratica_{practice.application_id}.pdf" + filename = f"verbale_istruttoria_pratica_{practice.application_id}_t{practice.sequence_number}.pdf" return Response( content=pdf_bytes, media_type="application/pdf", diff --git a/app/templates_jinja/verbale_istruttoria.html b/app/templates_jinja/verbale_istruttoria.html index 2c54f5c..b5addb5 100644 --- a/app/templates_jinja/verbale_istruttoria.html +++ b/app/templates_jinja/verbale_istruttoria.html @@ -182,7 +182,8 @@
Verbale di istruttoria
Rendicontazione bando
-
Pratica n. {{ practice.application_id }}
+
Pratica n. {{ practice.application_id }}{% if max_tranches_snapshot > 1 or practice.sequence_number > 1 %} — Tranche {{ practice.sequence_number }}/{{ max_tranches_snapshot }}{% endif %}
+ {% if practice.period_label %}
{{ practice.period_label }}
{% endif %}
@@ -237,6 +238,15 @@ {% else %}—{% endif %}
+ {% if max_tranches_snapshot > 1 %} +
+
Tranche / fase
+
+ Tranche {{ practice.sequence_number }} di {{ max_tranches_snapshot }} + {% if practice.period_label %} — {{ practice.period_label }}{% endif %} +
+
+ {% endif %}
Data presentazione
{{ practice.submitted_at|datetimefmt if practice.submitted_at else '—' }}
@@ -387,6 +397,45 @@

Nessun documento richiesto dallo schema del bando.

{% endif %} +{# ============ CONTROLLI AGGIUNTIVI ============ #} +{% if custom_checks_merged %} +

Controlli aggiuntivi (dichiarazioni beneficiario)

+ + + + + + + + + + + + + {% for cc in custom_checks_merged %} + {% set stat = cc.verification_status or 'PENDING' %} + {% set cls = 'rejected' if stat == 'NON_VALIDO' else '' %} + {% set missing = cc.required and not cc.beneficiary_declared %} + + + + + + + + + {% endfor %} + +
ControlloObbligatorioDichiaratoDoc. allegatoValidazioneNote istruttore
+ {{ cc.label or cc.code }} + {% if cc.description %}
{{ cc.description|truncate(180) }}{% endif %} +
{% if cc.required %}{% else %}opzionale{% endif %}{% if cc.beneficiary_declared %}{% else %}NO{% endif %} + {% if cc.requires_document %} + {% if cc.has_document %}{% else %}NO{% endif %} + {% else %}non richiesto{% endif %} + {{ stat }}{{ cc.verification_notes or '—' }}
+{% endif %} + {# ============ SOCCORSI ============ #} {% if amendments %}

Soccorso istruttorio

@@ -406,43 +455,94 @@ {% endfor %} {% endif %} -{# ============ TOTALI ============ #} -

Riepilogo finanziario

+{# ============ STORICO TRANCHES PRECEDENTI ============ #} +{% if previous_tranches %} +

Storico tranches precedenti

+ + + + + + + + + + + + {% for t in previous_tranches %} + + + + + + + + {% endfor %} + +
#Periodo / faseData approvazioneImporto ammessoCumulativo
T{{ t.sequence_number }}{{ t.period_label or '—' }}{{ t.reviewed_at|datefmt }}{{ t.approved_remission|euro }}{{ t.cumulative|euro }}
+{% endif %} + +{# ============ 5 VOCI UFFICIALI CECILIA ============ #} +

Riepilogo finanziario (cap tranche {{ practice.sequence_number }})

+
-
-
Totale dichiarato
-
{{ totals.grand_total_declared|euro }}
+
+
(1) Importo massimo ammissibile (cap globale)
+
{{ totals.max_remission_global|euro }}
+ {% if totals.already_approved_previous_tranches > 0 %} +
già approvato nelle tranche precedenti
+
− {{ totals.already_approved_previous_tranches|euro }}
+
max. disponibile per questa tranche
+
= {{ totals.max_remission_this_tranche|euro }}
+ {% endif %}
-
-
Totale ammesso
-
{{ totals.grand_total_verified|euro }}
-
-
-
Cap remissione
-
{{ totals.max_remission|euro }}
-
-
-
Remissione spettante
-
{{ totals.remission_due|euro }}
+
+
(4) Importo finanziamento erogato
+
{{ totals.amount_erogato|euro }}
+
tranches complessive
+
{{ totals.tranches_count }} / {{ totals.tranches_max }}
- {% if practice.status == 'APPROVED' %} -
-
-
Remissione approvata
-
{{ practice.approved_remission|euro }}
-
-
-
Residuo da restituire
-
{{ (practice.amount_erogato - (practice.approved_remission or 0))|euro }}
-
-
-
-
- {% endif %}
+
+
+
+
(2) Richiesto pre-controllo (ammissibile)
+
{{ totals.pre_check_admissible|euro }}
+
dichiarato tranche
+
{{ totals.grand_total_declared|euro }}
+
+
+
(3) Ammesso post-controllo istruttore
+
{{ totals.remission_due|euro }}
+ {% if totals.any_verified %} +
verificato tranche
+
{{ totals.grand_total_verified|euro }}
+ {% else %} +
in attesa di verifica istruttore
+ {% endif %} +
+
+
(5) Residuo da restituire
+
{{ totals.residuo_da_restituire|euro }}
+
= erogato − approvato precedente − ammesso tranche
+
+
+
+ +{% if practice.status == 'APPROVED' %} +
+
+
+
REMISSIONE APPROVATA PER QUESTA TRANCHE
+
{{ practice.approved_remission|euro }}
+
+
+
+{% endif %} + {# ============ CHECKLIST + NOTE ============ #} {% set checklist = practice.instructor_checklist or {} %} {% if checklist %}