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

@@ -40,8 +40,10 @@ class RemissionSchemaOut(BaseModel):
# ====================== Pratica di rendicontazione (beneficiario) ======================
class PracticeStartRequest(BaseModel):
"""Input minimo per avviare una pratica: solo application_id. Il resto viene dal DB."""
"""Input per avviare una (nuova) pratica o tranche."""
application_id: int
period_label: Optional[str] = None # es "I trimestre 2021" — libero
copy_ula_from_previous: bool = True # ignorato se e la prima tranche
class PracticeUpdate(BaseModel):
@@ -160,6 +162,11 @@ class PracticeOut(BaseModel):
instructor_checklist: Optional[dict] = None
verbale_date: Optional[date] = None
# v2 multi-tranche
sequence_number: int = 1
period_label: Optional[str] = None
suggested_instructor_id: Optional[int] = None
invoices: List[InvoiceOut] = []
ula_employees: List[UlaEmployeeOut] = []
documents: List[DocumentOut] = []
@@ -178,6 +185,11 @@ class PracticeListItem(BaseModel):
created_at: datetime
submitted_at: Optional[datetime] = None
# v2 multi-tranche
sequence_number: int = 1
period_label: Optional[str] = None
suggested_instructor_id: Optional[int] = None
# campi denormalizzati aggiunti a runtime
call_name: Optional[str] = None
company_name: Optional[str] = None
@@ -237,6 +249,9 @@ class InstructorQueueItem(BaseModel):
id: UUID
call_id: int
application_id: int
sequence_number: int = 1
period_label: Optional[str] = None
suggested_instructor_id: Optional[int] = None
company_id: int
status: str
amount_erogato: Decimal
@@ -288,3 +303,76 @@ class ApiResponse(BaseModel):
status: str = "SUCCESS"
message: Optional[str] = None
data: Optional[Any] = None
# ====================== v2 Custom checks ======================
class CustomCheckDeclareBody(BaseModel):
beneficiary_declared: bool
class CustomCheckVerifyBody(BaseModel):
verification_status: str # PENDING | VALIDO | NON_VALIDO
verification_notes: Optional[str] = None
class CustomCheckOut(BaseModel):
"""Vista merged di definition (da schema) + value (dal DB)."""
code: str
label: str
description: Optional[str] = None
requires_document: bool = False
required: bool = False
# valori
beneficiary_declared: bool = False
declared_at: Optional[datetime] = None
filename_original: Optional[str] = None
storage_path: Optional[str] = None
size_bytes: Optional[int] = None
document_uploaded_at: Optional[datetime] = None
verification_status: str = "PENDING"
verification_notes: Optional[str] = None
verified_by: Optional[int] = None
verified_at: Optional[datetime] = None
# ====================== v2 Reassign istruttore ======================
class PracticeReassignBody(BaseModel):
new_instructor_id: Optional[int] = None # None = unassign ritorno in coda
reassignment_reason: Optional[str] = None
# ====================== v2 Tranches ======================
class ApplicationTranchesSummary(BaseModel):
"""Riepilogo pratiche/tranche per una application."""
application_id: int
call_id: int
call_name: Optional[str] = None
company_id: int
company_name: Optional[str] = None
amount_erogato: float
max_tranches: int = 1
# summary tranche esistenti
tranches: List[PracticeListItem] = []
# stato apertura nuova tranche
can_start_new: bool = False
start_blocked_reason: Optional[str] = None
# importi cumulativi
already_approved_sum: float = 0
max_remission_global: float = 0
max_remission_next_tranche: float = 0
class CopyUlaOption(BaseModel):
"""Dipendente copiabile da tranche precedente."""
codice_fiscale: str
full_name: str
contract_type: str
role_description: Optional[str] = None
fte_pct: float
period_start_date: date
period_end_date: date
supporting_doc_type: Optional[str] = None