Files
BFLOWS aeab399afa feat(schemas): picker 3-card blank/template/clone per inizializzazione schema
Sostituisce il vecchio flusso 'initialize-restart' (unico template hardcoded)
con un picker unificato che offre 3 sorgenti di inizializzazione:

1. BLANK — schema scheletro v2 con sezioni vuote (categorie, documenti, custom_checks)
   da popolare; ULA disabilitata di default; max_tranches=1.
2. TEMPLATE — parte da template predefinito nel registry TEMPLATES di app/templates.py.
   Oggi: 'blank' + 'restart'. Struttura estendibile per nuovi template bandi.
3. CLONE — copia schema_json di un altro bando esistente (DRAFT o PUBLISHED).
   Useful per bandi 'sorella'. upgrade_schema_to_v2 applicato on-copy per schemi v1 legacy.

app/templates.py: aggiunto BLANK_TEMPLATE, registry TEMPLATES, helper list_templates()
e get_template(id).

app/routers/schemas.py: riscritto con 3 nuovi endpoint:
- GET /templates  -> lista metadati template disponibili
- GET /templates/{id}  -> preview schema completo
- GET /clonable-calls  -> bandi con schema (per dropdown clone)
- POST /{call_id}/initialize body {source, template_id?, source_call_id?}  -> unificato
Endpoint /initialize-restart mantenuto come alias di /initialize con template=restart
per backward compat del vecchio FE.

Testato E2E via curl: blank OK, template restart OK, clone da call 1 OK, errori
(source invalido/template_id inesistente/clone senza source_call_id/schema gia esistente/
bando inesistente) tutti gestiti con HTTP corretto.
2026-04-18 18:51:42 +02:00

305 lines
12 KiB
Python

"""
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": "2.0",
"schema_version": 2,
"template_id": "RESTART_V2",
"template_label": "RE-START (fondo prestiti con remissione del debito)",
"sections": [
{
"type": "static_fields",
"id": "general",
"label": "Dati generali",
"description": "Regime IVA, dati base del beneficiario, periodo di ammissibilità delle spese.",
"fields": [
{
"id": "period_start_date",
"type": "date",
"label": "Periodo ammissibilità — Data inizio",
"description": "Data minima di emissione/pagamento fatture ammissibili. Di norma: data erogazione del finanziamento.",
"editable_by": "superadmin",
"required": True
},
{
"id": "period_end_date",
"type": "date",
"label": "Periodo ammissibilità — Data fine",
"description": "Data massima di emissione/pagamento fatture ammissibili.",
"editable_by": "superadmin",
"required": True
},
{
"id": "iva_regime",
"type": "select",
"label": "Regime IVA",
"required": True,
"options": [
{"value": "ORDINARIO", "label": "Ordinario — IVA non ammissibile"},
{"value": "FORFETTARIO", "label": "Forfettario — IVA ammissibile"},
{"value": "ESENTE", "label": "Esente"},
],
"help": "Il regime IVA determina se l'IVA delle fatture è rendicontabile. In regime ordinario vale solo l'imponibile.",
}
],
},
{
"type": "category_grid",
"id": "expenses",
"label": "Spese ammissibili per categoria",
"description": "Carica le fatture dentro la categoria appropriata. Totali parziali e complessivo calcolati in tempo reale.",
"categories": [
{
"code": "B1",
"label": "Tecnologie innovative (Industry 4.0, digitale)",
"description": "Hardware, software, soluzioni innovative destinate ad attività produttive",
"cap_amount": None,
},
{
"code": "B2",
"label": "Incremento ULA (occupazione)",
"description": "Costi del personale collegati a incremento di occupazione",
"cap_amount": None,
},
{
"code": "B3",
"label": "Formazione",
"description": "Corsi, docenze, materiali didattici per il personale",
"cap_amount": None,
},
],
"invoice_schema": {
"required_fields": [
"invoice_number",
"invoice_date",
"payment_date",
"supplier_name",
"supplier_vat",
"description",
"taxable",
"vat",
"total",
"pdf",
],
"optional_fields": ["vat_rate", "vat_exempt_reason"],
},
},
{
"type": "ula_block",
"id": "ula",
"label": "Calcolo ULA (incremento occupazione)",
"description": "Per ogni dipendente: codice fiscale, tipologia contratto, percentuale di tempo, periodo. Allegato di supporto obbligatorio (LUL, estratto gestionale, dichiarazione del consulente del lavoro).",
"enabled": True,
"threshold": 1.0,
"period_start_rule": "erogato_date",
"period_end": "2021-12-31",
"supporting_doc_required": True,
"supporting_doc_types": [
{"code": "LUL", "label": "Libro Unico del Lavoro"},
{"code": "GESTIONALE_PAGHE", "label": "Estratto gestionale paghe"},
{"code": "DICHIARAZIONE_CDL", "label": "Dichiarazione Consulente del Lavoro"},
{"code": "ALTRO", "label": "Altro documento di supporto"},
],
},
{
"type": "document_checklist",
"id": "docs",
"label": "Documenti richiesti",
"description": "I documenti già in regola nel repository della Company saranno riutilizzati (semaforo verde). Solo quelli scaduti o mancanti richiedono caricamento.",
"required_types": [
{"code": "DURC", "label": "DURC (Documento Unico di Regolarità Contributiva)"},
{"code": "VISURA_CAMERALE", "label": "Visura camerale aggiornata"},
{"code": "BILANCIO", "label": "Bilancio ultimo esercizio"},
{"code": "ANTIRICICLAGGIO", "label": "Dichiarazione antiriciclaggio"},
],
},
],
"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,
"cap_absolute": 12500,
"iva_ordinario_imponibile_only": True,
"period_start_rule": "erogato_date",
"period_end": "2021-12-31",
"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
# =========================================================================
# BLANK_TEMPLATE — scheletro minimo v2, solo sezioni vuote da popolare
# =========================================================================
BLANK_TEMPLATE = {
"version": "2.0",
"schema_version": 2,
"template_id": "BLANK_V2",
"template_label": "Nuovo schema vuoto",
"sections": [
{
"type": "static_fields",
"id": "general",
"label": "Dati generali",
"description": "Regime IVA, dati base del beneficiario, periodo di ammissibilita delle spese.",
"fields": [
{
"id": "period_start_date",
"type": "date",
"label": "Periodo ammissibilita — Data inizio",
"description": "Data minima di emissione/pagamento fatture ammissibili.",
"editable_by": "superadmin",
"required": True,
},
{
"id": "period_end_date",
"type": "date",
"label": "Periodo ammissibilita — Data fine",
"description": "Data massima di emissione/pagamento fatture ammissibili.",
"editable_by": "superadmin",
"required": True,
},
{
"id": "iva_regime",
"type": "select",
"label": "Regime IVA",
"required": True,
"options": [
{"value": "ORDINARIO", "label": "Ordinario — IVA non ammissibile"},
{"value": "FORFETTARIO", "label": "Forfettario — IVA ammissibile"},
{"value": "ESENTE", "label": "Esente"},
],
"help": "Il regime IVA determina se l'IVA delle fatture e rendicontabile.",
},
],
},
{
"type": "invoice_table",
"id": "invoices",
"label": "Fatture ammissibili",
"description": "Categorie di spesa da configurare. Aggiungi almeno una categoria prima di pubblicare.",
"categories": [],
},
{
"type": "ula_block",
"id": "ula",
"label": "Incremento occupazione (ULA)",
"description": "Dipendenti su cui calcolare l'incremento ULA. Disattiva la sezione se il bando non lo richiede.",
"enabled": False,
"threshold": 1.0,
"fields": [],
},
{
"type": "documents_required",
"id": "docs",
"label": "Documenti richiesti",
"description": "Documenti che il beneficiario deve allegare alla rendicontazione. Aggiungi almeno i documenti obbligatori.",
"items": [],
},
],
"custom_checks": [],
"gate_rules": {
"invoices_min_count": 1,
"amount_range": {"min": 0, "max": 100000},
"cap_pct_erogato": 0.5,
"cap_absolute": 100000,
"amount_basis": "imponibile_only_ordinario",
"period_start_rule": "erogato_date",
"period_end": None,
"require_at_least_one_invoice_per_nonzero_category": True,
"require_ula_above_threshold": False,
"require_all_documents_resolved": True,
"max_tranches": 1,
},
}
# =========================================================================
# TEMPLATES registry — esteso in futuro con nuovi bandi
# =========================================================================
TEMPLATES = {
"blank": {
"template_id": "blank",
"label": "Nuovo schema (da zero)",
"description": "Scheletro minimo: sezioni vuote (categorie, documenti, controlli) da popolare. Usa questo quando il bando e nuovo e non somiglia a bandi precedenti.",
"schema": BLANK_TEMPLATE,
},
"restart": {
"template_id": "restart",
"label": "RE-START (fondo prestiti con remissione del debito)",
"description": "Template del bando RE-START: 3 categorie B1/B2/B3 (tecnologie, ULA, formazione), sezione ULA attiva con soglia 1.0, 4 documenti standard, max 2 tranches.",
"schema": RESTART_TEMPLATE,
},
}
def list_templates():
"""Restituisce i template disponibili (senza lo schema completo, solo metadati)."""
return [
{
"template_id": t["template_id"],
"label": t["label"],
"description": t["description"],
}
for t in TEMPLATES.values()
]
def get_template(template_id: str):
"""Restituisce uno schema template pronto per l'uso (deep copy)."""
import copy
t = TEMPLATES.get(template_id)
if not t:
return None
return copy.deepcopy(t["schema"])