Files
gepafin-rendicontazione-api/app/schemas.py
BFLOWS 25215f388b 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.
2026-04-18 17:35:56 +02:00

379 lines
10 KiB
Python

"""
Pydantic schemas per API.
"""
from typing import Optional, Any, List
from datetime import datetime, date
from decimal import Decimal
from uuid import UUID
from pydantic import BaseModel, Field
# ====================== Schema di rendicontazione (bando) ======================
class RemissionSchemaBase(BaseModel):
schema_json: dict
class RemissionSchemaCreate(RemissionSchemaBase):
pass
class RemissionSchemaUpdate(BaseModel):
schema_json: Optional[dict] = None
class RemissionSchemaOut(BaseModel):
id: UUID
call_id: int
schema_version: int
status: str
schema_json: dict
created_by: int
created_at: datetime
updated_at: datetime
published_at: Optional[datetime] = None
published_by: Optional[int] = None
model_config = {"from_attributes": True}
# ====================== Pratica di rendicontazione (beneficiario) ======================
class PracticeStartRequest(BaseModel):
"""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):
iva_regime: Optional[str] = None
notes_beneficiario: Optional[str] = None
# Fattura
class InvoiceCreate(BaseModel):
category_code: str
invoice_number: str
invoice_date: date
payment_date: date
supplier_name: str
supplier_vat: str
description: str
taxable: Decimal
vat: Decimal = Decimal("0")
total: Decimal
pdf_filename: Optional[str] = None
class InvoiceOut(InvoiceCreate):
id: UUID
practice_id: UUID
created_at: datetime
# istruttoria
taxable_verified: Optional[Decimal] = None
vat_verified: Optional[Decimal] = None
total_verified: Optional[Decimal] = None
verification_status: str = "PENDING"
verification_notes: Optional[str] = None
date_checks: Optional[dict] = None
verified_by: Optional[int] = None
verified_at: Optional[datetime] = None
model_config = {"from_attributes": True}
# ULA Employee
class UlaEmployeeCreate(BaseModel):
codice_fiscale: str
full_name: str
contract_type: str
role_description: Optional[str] = None
fte_pct: Decimal = Decimal("1")
period_start_date: date
period_end_date: date
supporting_doc_type: Optional[str] = None
supporting_doc_filename: Optional[str] = None
class UlaEmployeeOut(UlaEmployeeCreate):
id: UUID
practice_id: UUID
created_at: datetime
# istruttoria
fte_pct_verified: Optional[Decimal] = None
verification_status: str = "PENDING"
verification_notes: Optional[str] = None
verified_by: Optional[int] = None
verified_at: Optional[datetime] = None
model_config = {"from_attributes": True}
# Document
class DocumentUpsert(BaseModel):
doc_code: str
filename: Optional[str] = None
uploaded_at: Optional[datetime] = None
expires_at: Optional[date] = None
notes: Optional[str] = None
class DocumentOut(BaseModel):
id: UUID
practice_id: UUID
doc_code: str
filename: Optional[str] = None
uploaded_at: Optional[datetime] = None
expires_at: Optional[date] = None
notes: Optional[str] = None
# istruttoria
verification_status: str = "PENDING"
verification_notes: Optional[str] = None
verified_by: Optional[int] = None
verified_at: Optional[datetime] = None
model_config = {"from_attributes": True}
# Pratica dettagliata
class PracticeOut(BaseModel):
id: UUID
call_id: int
application_id: int
company_id: int
user_id: int
status: str
schema_snapshot: dict
iva_regime: Optional[str] = None
amount_erogato: Decimal
notes_beneficiario: Optional[str] = None
created_at: datetime
updated_at: datetime
submitted_at: Optional[datetime] = None
# istruttoria
assigned_instructor_id: Optional[int] = None
reviewed_at: Optional[datetime] = None
reviewed_by: Optional[int] = None
rejection_reason: Optional[str] = None
approved_remission: Optional[Decimal] = None
instructor_final_notes: Optional[str] = None
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] = []
model_config = {"from_attributes": True}
class PracticeListItem(BaseModel):
"""Riga leggera per liste."""
id: UUID
call_id: int
application_id: int
company_id: int
status: str
amount_erogato: Decimal
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
invoice_count: int = 0
ula_count: int = 0
document_count: int = 0
model_config = {"from_attributes": True}
# Gate check
class GateCheckResult(BaseModel):
passed: bool
checks: List[dict] # [{id, label, passed, detail}]
totals: dict # {per_category: {B1: 1234.56, ...}, grand_total, max_remission_due, ...}
# ====================== Istruttoria ======================
class AmendmentRequestCreate(BaseModel):
request_text: str
deadline: date
scope: Optional[dict] = None
class AmendmentResponseSubmit(BaseModel):
response_text: str
class AmendmentRequestOut(BaseModel):
id: UUID
practice_id: UUID
requested_by: int
request_text: str
scope: Optional[dict] = None
deadline: date
status: str
response_text: Optional[str] = None
response_at: Optional[datetime] = None
closed_at: Optional[datetime] = None
closed_by: Optional[int] = None
created_at: datetime
model_config = {"from_attributes": True}
class ReviewRejectBody(BaseModel):
rejection_reason: str
class ReviewApproveBody(BaseModel):
approved_remission: Optional[Decimal] = None
notes: Optional[str] = None
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
submitted_at: Optional[datetime] = None
assigned_instructor_id: Optional[int] = None
call_name: Optional[str] = None
company_name: Optional[str] = None
invoice_count: int = 0
ula_count: int = 0
document_count: int = 0
open_amendments: int = 0
remission_due: Optional[float] = None
model_config = {"from_attributes": True}
# Verifica singola fattura
class InvoiceVerifyBody(BaseModel):
verification_status: str # AMMESSA | PARZIALE | RESPINTA
taxable_verified: Optional[Decimal] = None
vat_verified: Optional[Decimal] = None
total_verified: Optional[Decimal] = None
verification_notes: Optional[str] = None
# Verifica singolo dipendente ULA
class UlaVerifyBody(BaseModel):
verification_status: str # AMMESSA | PARZIALE | RESPINTA
fte_pct_verified: Optional[Decimal] = None
verification_notes: Optional[str] = None
# Verifica singolo documento
class DocumentVerifyBody(BaseModel):
verification_status: str # VALIDO | NON_VALIDO | SCADUTO
verification_notes: Optional[str] = None
# Note finali istruttore + checklist
class InstructorFinalNotesBody(BaseModel):
instructor_final_notes: Optional[str] = None
instructor_checklist: Optional[dict] = None
# ====================== Wrapper ======================
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