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.
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -182,7 +182,8 @@
|
||||
<div class="hdr__right">
|
||||
<div><strong>Verbale di istruttoria</strong></div>
|
||||
<div>Rendicontazione bando</div>
|
||||
<div>Pratica n. {{ practice.application_id }}</div>
|
||||
<div>Pratica n. {{ practice.application_id }}{% if max_tranches_snapshot > 1 or practice.sequence_number > 1 %} — Tranche {{ practice.sequence_number }}/{{ max_tranches_snapshot }}{% endif %}</div>
|
||||
{% if practice.period_label %}<div><small>{{ practice.period_label }}</small></div>{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<span class="hdr__logo">GEPAFIN</span>
|
||||
@@ -237,6 +238,15 @@
|
||||
{% else %}—{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if max_tranches_snapshot > 1 %}
|
||||
<div class="row">
|
||||
<div class="cell label">Tranche / fase</div>
|
||||
<div class="cell val">
|
||||
<strong>Tranche {{ practice.sequence_number }} di {{ max_tranches_snapshot }}</strong>
|
||||
{% if practice.period_label %} — {{ practice.period_label }}{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<div class="cell label">Data presentazione</div>
|
||||
<div class="cell val">{{ practice.submitted_at|datetimefmt if practice.submitted_at else '—' }}</div>
|
||||
@@ -387,6 +397,45 @@
|
||||
<p class="text-secondary">Nessun documento richiesto dallo schema del bando.</p>
|
||||
{% endif %}
|
||||
|
||||
{# ============ CONTROLLI AGGIUNTIVI ============ #}
|
||||
{% if custom_checks_merged %}
|
||||
<h2>Controlli aggiuntivi (dichiarazioni beneficiario)</h2>
|
||||
<table class="data">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:32%">Controllo</th>
|
||||
<th style="width:12%">Obbligatorio</th>
|
||||
<th style="width:10%">Dichiarato</th>
|
||||
<th style="width:11%">Doc. allegato</th>
|
||||
<th style="width:11%">Validazione</th>
|
||||
<th style="width:24%">Note istruttore</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% 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 %}
|
||||
<tr class="{{ 'rejected' if missing else cls }}">
|
||||
<td>
|
||||
<strong>{{ cc.label or cc.code }}</strong>
|
||||
{% if cc.description %}<br><small>{{ cc.description|truncate(180) }}</small>{% endif %}
|
||||
</td>
|
||||
<td>{% if cc.required %}<span class="ko">SÌ</span>{% else %}<small class="text-secondary">opzionale</small>{% endif %}</td>
|
||||
<td>{% if cc.beneficiary_declared %}<span class="ok">SÌ</span>{% else %}<span class="ko">NO</span>{% endif %}</td>
|
||||
<td>
|
||||
{% if cc.requires_document %}
|
||||
{% if cc.has_document %}<span class="ok">SÌ</span>{% else %}<span class="ko">NO</span>{% endif %}
|
||||
{% else %}<small class="text-secondary">non richiesto</small>{% endif %}
|
||||
</td>
|
||||
<td><span class="status-inline status-{{ stat }}">{{ stat }}</span></td>
|
||||
<td>{{ cc.verification_notes or '—' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{# ============ SOCCORSI ============ #}
|
||||
{% if amendments %}
|
||||
<h2>Soccorso istruttorio</h2>
|
||||
@@ -406,43 +455,94 @@
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{# ============ TOTALI ============ #}
|
||||
<h2>Riepilogo finanziario</h2>
|
||||
{# ============ STORICO TRANCHES PRECEDENTI ============ #}
|
||||
{% if previous_tranches %}
|
||||
<h2>Storico tranches precedenti</h2>
|
||||
<table class="data">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:8%">#</th>
|
||||
<th style="width:35%">Periodo / fase</th>
|
||||
<th style="width:17%">Data approvazione</th>
|
||||
<th style="width:20%" class="num">Importo ammesso</th>
|
||||
<th style="width:20%" class="num">Cumulativo</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for t in previous_tranches %}
|
||||
<tr>
|
||||
<td><strong>T{{ t.sequence_number }}</strong></td>
|
||||
<td>{{ t.period_label or '—' }}</td>
|
||||
<td>{{ t.reviewed_at|datefmt }}</td>
|
||||
<td class="num">{{ t.approved_remission|euro }}</td>
|
||||
<td class="num"><strong>{{ t.cumulative|euro }}</strong></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
|
||||
{# ============ 5 VOCI UFFICIALI CECILIA ============ #}
|
||||
<h2>Riepilogo finanziario (cap tranche {{ practice.sequence_number }})</h2>
|
||||
|
||||
<div class="totals-summary">
|
||||
<div class="row">
|
||||
<div class="cell">
|
||||
<div class="lbl">Totale dichiarato</div>
|
||||
<div class="val">{{ totals.grand_total_declared|euro }}</div>
|
||||
<div class="cell" style="width:50%">
|
||||
<div class="lbl">(1) Importo massimo ammissibile (cap globale)</div>
|
||||
<div class="val">{{ totals.max_remission_global|euro }}</div>
|
||||
{% if totals.already_approved_previous_tranches > 0 %}
|
||||
<div class="lbl" style="margin-top:4pt">già approvato nelle tranche precedenti</div>
|
||||
<div style="font-size:10pt; font-weight:700; color:#744210">− {{ totals.already_approved_previous_tranches|euro }}</div>
|
||||
<div class="lbl" style="margin-top:4pt">max. disponibile per questa tranche</div>
|
||||
<div style="font-size:11pt; font-weight:700; color:#2b6cb0">= {{ totals.max_remission_this_tranche|euro }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div class="lbl">Totale ammesso</div>
|
||||
<div class="val">{{ totals.grand_total_verified|euro }}</div>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div class="lbl">Cap remissione</div>
|
||||
<div class="val">{{ totals.max_remission|euro }}</div>
|
||||
</div>
|
||||
<div class="cell">
|
||||
<div class="lbl">Remissione spettante</div>
|
||||
<div class="val final">{{ totals.remission_due|euro }}</div>
|
||||
<div class="cell" style="width:50%">
|
||||
<div class="lbl">(4) Importo finanziamento erogato</div>
|
||||
<div class="val">{{ totals.amount_erogato|euro }}</div>
|
||||
<div class="lbl" style="margin-top:6pt">tranches complessive</div>
|
||||
<div style="font-size:10pt">{{ totals.tranches_count }} / {{ totals.tranches_max }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if practice.status == 'APPROVED' %}
|
||||
<div class="row">
|
||||
<div class="cell" style="background: #f0fff4;">
|
||||
<div class="lbl">Remissione approvata</div>
|
||||
<div class="val" style="color: #22543d;">{{ practice.approved_remission|euro }}</div>
|
||||
</div>
|
||||
<div class="cell" style="background: #fff5f5;">
|
||||
<div class="lbl">Residuo da restituire</div>
|
||||
<div class="val residuo">{{ (practice.amount_erogato - (practice.approved_remission or 0))|euro }}</div>
|
||||
</div>
|
||||
<div class="cell" colspan="2"></div>
|
||||
<div class="cell"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="totals-summary" style="margin-top:8pt">
|
||||
<div class="row">
|
||||
<div class="cell" style="width:33%">
|
||||
<div class="lbl">(2) Richiesto pre-controllo (ammissibile)</div>
|
||||
<div class="val">{{ totals.pre_check_admissible|euro }}</div>
|
||||
<div class="lbl" style="margin-top:4pt">dichiarato tranche</div>
|
||||
<div style="font-size:9pt">{{ totals.grand_total_declared|euro }}</div>
|
||||
</div>
|
||||
<div class="cell" style="width:33%">
|
||||
<div class="lbl">(3) Ammesso post-controllo istruttore</div>
|
||||
<div class="val final">{{ totals.remission_due|euro }}</div>
|
||||
{% if totals.any_verified %}
|
||||
<div class="lbl" style="margin-top:4pt">verificato tranche</div>
|
||||
<div style="font-size:9pt">{{ totals.grand_total_verified|euro }}</div>
|
||||
{% else %}
|
||||
<div class="lbl" style="margin-top:4pt"><em>in attesa di verifica istruttore</em></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="cell" style="width:34%; background:#fff5f5">
|
||||
<div class="lbl">(5) Residuo da restituire</div>
|
||||
<div class="val residuo">{{ totals.residuo_da_restituire|euro }}</div>
|
||||
<div class="lbl" style="margin-top:4pt">= erogato − approvato precedente − ammesso tranche</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if practice.status == 'APPROVED' %}
|
||||
<div class="totals-summary" style="margin-top:8pt; background:#f0fff4; border-color:#68d391">
|
||||
<div class="row">
|
||||
<div class="cell" style="width:100%">
|
||||
<div class="lbl">REMISSIONE APPROVATA PER QUESTA TRANCHE</div>
|
||||
<div class="val" style="color:#22543d; font-size:18pt">{{ practice.approved_remission|euro }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# ============ CHECKLIST + NOTE ============ #}
|
||||
{% set checklist = practice.instructor_checklist or {} %}
|
||||
{% if checklist %}
|
||||
|
||||
Reference in New Issue
Block a user