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.
This commit is contained in:
126
app/templates.py
126
app/templates.py
@@ -176,3 +176,129 @@ def upgrade_schema_to_v2(schema_json: dict) -> dict:
|
||||
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"])
|
||||
|
||||
Reference in New Issue
Block a user