Files
gepafin-rendicontazione-api/app/schemas.py
BFLOWS f9f543b008 feat(istruttoria): verifica riga-per-riga con dual declared/verified
Replica il workflow del foglio Excel originale (REMISSIONE_DEL_DEBITO_5888.xlsm).
Istruttore ora verifica ogni fattura, ogni dipendente ULA, ogni documento singolarmente
invece di accettare/respingere la pratica intera.

Modello dati - nuove colonne su 3 tabelle:
- remission_invoice: taxable_verified, vat_verified, total_verified,
  verification_status (PENDING/AMMESSA/PARZIALE/RESPINTA), verification_notes,
  date_checks (JSONB con invoice_in_period/payment_in_period), verified_by, verified_at
- remission_ula_employee: fte_pct_verified, verification_status, verification_notes,
  verified_by, verified_at
- remission_document: verification_status (PENDING/VALIDO/NON_VALIDO/SCADUTO),
  verification_notes, verified_by, verified_at
- remission_practice: instructor_final_notes, instructor_checklist (JSONB 3 gate SI/NO),
  verbale_date

Nuovi endpoint:
- PUT /instructor/{id}/invoices/{inv_id}/verify (status + rettifica importi + note)
- PUT /instructor/{id}/ula-employees/{emp_id}/verify (rettifica FTE + note)
- PUT /instructor/{id}/documents/{doc_code}/verify (VALIDO/NON_VALIDO/SCADUTO + note)
- PUT /instructor/{id}/final-notes (note sintetiche + checklist)

Ricalcolo gate_check dual track:
- grand_total_declared: sempre (importo richiesto dal beneficiario)
- grand_total_verified: somma solo fatture AMMESSA/PARZIALE (se PARZIALE usa verified)
- remission_due: usa verified se any_verified=True, altrimenti declared (backward compat)
- residuo_da_restituire: amount_erogato - remission_due
- flag any_verified e all_verified per gating decisione finale

_auto_check_dates: fattura in periodo? pagamento in periodo?
Legge period_start e period_end da schema.gate_rules (superadmin editor).

Template: aggiunto period_start/period_end_date come campi 'editable_by superadmin'
nella sezione general static_fields.

Schema editor FE (BandoRendicontazioneSchemaEdit): aggiunto Calendar period_start
accanto a period_end in section gate rules. period_start_rule dropdown per logica
(erogato_date|fixed) resta; period_start data fissa usata dal check.
2026-04-18 11:03:15 +02:00

291 lines
7.3 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 minimo per avviare una pratica: solo application_id. Il resto viene dal DB."""
application_id: int
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
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
# 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
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