Files
gepafin-rendicontazione-api/app/models.py
BFLOWS e217f15e5a feat: endpoint pratiche rendicontazione (lato beneficiario)
- 4 nuove tabelle: remission_practice, remission_invoice, remission_ula_employee, remission_document
  con cascade delete e FK
- 13 endpoint /api/remission-practices/*:
  GET /mine (lista pratiche user + applications CONTRACT_SIGNED ready_to_start)
  POST /start (avvia pratica da application_id, richiede schema PUBLISHED)
  GET /{id}, PUT /{id} (regime IVA + note)
  POST/DELETE /{id}/invoices
  POST/DELETE /{id}/ula-employees
  PUT/DELETE /{id}/documents/{doc_code}
  GET /{id}/gate-check (valida gate rules contro pratica, ritorna totali + checks)
  POST /{id}/submit (gate-check obbligatorio, status DRAFT -> SUBMITTED)
- 1 endpoint debug /api/debug/impersonate (sandbox-only, genera JWT per utente
  - necessario perche' /v1/user/login del BE Spring esclude ROLE_BENEFICIARY)
- Gate check calcola: totali per categoria, grand_total, max_remission = min(cap_pct*erogato, cap_abs),
  remission_due = min(grand_total, max_remission), applica iva_ordinario_imponibile_only
2026-04-18 09:51:06 +02:00

144 lines
6.5 KiB
Python

"""
ORM models per rendicontazione-api.
Schema: gepafin_rendic (stesso DB del BE Gepafin sandbox).
"""
import uuid
from datetime import datetime
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, UniqueConstraint, Numeric, Boolean, Date
from sqlalchemy.dialects.postgresql import UUID, JSONB
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
from .db import Base
class CallRemissionSchema(Base):
"""
Schema di rendicontazione per un bando. Uno per call_id.
status: DRAFT (modificabile) -> PUBLISHED (visibile ai beneficiari).
"""
__tablename__ = "call_remission_schema"
__table_args__ = (
UniqueConstraint("call_id", name="uq_call_remission_schema_call_id"),
{"schema": "gepafin_rendic"},
)
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
call_id = Column(Integer, nullable=False, unique=True)
schema_version = Column(Integer, nullable=False, default=1)
status = Column(String(32), nullable=False, default="DRAFT")
schema_json = Column(JSONB, nullable=False)
created_by = Column(Integer, nullable=False)
created_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now())
updated_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now())
published_at = Column(DateTime(timezone=True), nullable=True)
published_by = Column(Integer, nullable=True)
class RemissionPractice(Base):
"""
Pratica di rendicontazione di un beneficiario per una specifica application in CONTRACT_SIGNED.
Uno schema_snapshot congelato alla creazione: se il superadmin modifica lo schema
del bando dopo, la pratica continua a usare la versione snapshot.
"""
__tablename__ = "remission_practice"
__table_args__ = (
UniqueConstraint("application_id", name="uq_remission_practice_application"),
{"schema": "gepafin_rendic"},
)
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
call_id = Column(Integer, nullable=False)
application_id = Column(Integer, nullable=False, unique=True)
company_id = Column(Integer, nullable=False)
user_id = Column(Integer, nullable=False) # beneficiario che compila
status = Column(String(32), nullable=False, default="DRAFT")
# DRAFT -> SUBMITTED -> UNDER_REVIEW -> APPROVED | REJECTED | AWAITING_AMENDMENT
schema_snapshot = Column(JSONB, nullable=False) # copia schema al momento start
iva_regime = Column(String(32), nullable=True) # ORDINARIO | FORFETTARIO | ESENTE
amount_erogato = Column(Numeric(14, 2), nullable=False) # copiato da application.amount_accepted
notes_beneficiario = Column(Text, nullable=True)
created_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now())
updated_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now())
submitted_at = Column(DateTime(timezone=True), nullable=True)
# relazioni
invoices = relationship("RemissionInvoice", back_populates="practice", cascade="all, delete-orphan")
ula_employees = relationship("RemissionUlaEmployee", back_populates="practice", cascade="all, delete-orphan")
documents = relationship("RemissionDocument", back_populates="practice", cascade="all, delete-orphan")
class RemissionInvoice(Base):
"""Fattura rendicontata dentro una pratica, assegnata a una categoria."""
__tablename__ = "remission_invoice"
__table_args__ = ({"schema": "gepafin_rendic"},)
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
practice_id = Column(UUID(as_uuid=True),
ForeignKey("gepafin_rendic.remission_practice.id", ondelete="CASCADE"),
nullable=False)
category_code = Column(String(16), nullable=False) # B1 / B2 / B3 / custom
invoice_number = Column(String(128), nullable=False)
invoice_date = Column(Date, nullable=False)
payment_date = Column(Date, nullable=False)
supplier_name = Column(String(255), nullable=False)
supplier_vat = Column(String(32), nullable=False)
description = Column(Text, nullable=False)
taxable = Column(Numeric(14, 2), nullable=False) # imponibile
vat = Column(Numeric(14, 2), nullable=False, default=0)
total = Column(Numeric(14, 2), nullable=False)
pdf_filename = Column(String(512), nullable=True) # per ora solo nome, upload vero dopo
created_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now())
practice = relationship("RemissionPractice", back_populates="invoices")
class RemissionUlaEmployee(Base):
"""Dipendente conteggiato nel calcolo ULA."""
__tablename__ = "remission_ula_employee"
__table_args__ = ({"schema": "gepafin_rendic"},)
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
practice_id = Column(UUID(as_uuid=True),
ForeignKey("gepafin_rendic.remission_practice.id", ondelete="CASCADE"),
nullable=False)
codice_fiscale = Column(String(16), nullable=False)
full_name = Column(String(255), nullable=False)
contract_type = Column(String(64), nullable=False) # T_IND / T_DET / APPR / ...
role_description = Column(String(255), nullable=True)
fte_pct = Column(Numeric(5, 4), nullable=False, default=1) # 0..1
period_start_date = Column(Date, nullable=False)
period_end_date = Column(Date, nullable=False)
supporting_doc_type = Column(String(64), nullable=True)
supporting_doc_filename = Column(String(512), nullable=True)
created_at = Column(DateTime(timezone=True), nullable=False, server_default=func.now())
practice = relationship("RemissionPractice", back_populates="ula_employees")
class RemissionDocument(Base):
"""Documento associato alla pratica (DURC, visura, ecc.)."""
__tablename__ = "remission_document"
__table_args__ = ({"schema": "gepafin_rendic"},)
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
practice_id = Column(UUID(as_uuid=True),
ForeignKey("gepafin_rendic.remission_practice.id", ondelete="CASCADE"),
nullable=False)
doc_code = Column(String(64), nullable=False) # DURC / VISURA_CAMERALE / ...
filename = Column(String(512), nullable=True)
uploaded_at = Column(DateTime(timezone=True), nullable=True)
expires_at = Column(Date, nullable=True)
notes = Column(Text, nullable=True)
practice = relationship("RemissionPractice", back_populates="documents")