""" 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) # colonne istruttoria assigned_instructor_id = Column(Integer, nullable=True) reviewed_at = Column(DateTime(timezone=True), nullable=True) reviewed_by = Column(Integer, nullable=True) rejection_reason = Column(Text, nullable=True) approved_remission = Column(Numeric(14, 2), nullable=True) instructor_final_notes = Column(Text, nullable=True) instructor_checklist = Column(JSONB, nullable=True, default=dict) verbale_date = Column(Date, 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") amendment_requests = relationship("RemissionAmendmentRequest", 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 # Campi istruttoria (dual declared/verified) taxable_verified = Column(Numeric(14, 2), nullable=True) vat_verified = Column(Numeric(14, 2), nullable=True) total_verified = Column(Numeric(14, 2), nullable=True) verification_status = Column(String(16), nullable=False, default="PENDING") # PENDING | AMMESSA | PARZIALE | RESPINTA verification_notes = Column(Text, nullable=True) date_checks = Column(JSONB, nullable=True, default=dict) verified_by = Column(Integer, nullable=True) verified_at = Column(DateTime(timezone=True), nullable=True) 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) # Campi istruttoria fte_pct_verified = Column(Numeric(5, 4), nullable=True) verification_status = Column(String(16), nullable=False, default="PENDING") verification_notes = Column(Text, nullable=True) verified_by = Column(Integer, nullable=True) verified_at = Column(DateTime(timezone=True), 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) # Campi istruttoria verification_status = Column(String(16), nullable=False, default="PENDING") # PENDING | VALIDO | NON_VALIDO | SCADUTO verification_notes = Column(Text, nullable=True) verified_by = Column(Integer, nullable=True) verified_at = Column(DateTime(timezone=True), nullable=True) practice = relationship("RemissionPractice", back_populates="documents") class RemissionAmendmentRequest(Base): """Richiesta di soccorso istruttorio: istruttore chiede integrazioni al beneficiario.""" __tablename__ = "remission_amendment_request" __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) requested_by = Column(Integer, nullable=False) request_text = Column(Text, nullable=False) scope = Column(JSONB, nullable=True, default=dict) deadline = Column(Date, nullable=False) status = Column(String(32), nullable=False, default="AWAITING") # AWAITING -> RESPONSE_RECEIVED -> CLOSED | EXPIRED | REJECTED response_text = Column(Text, nullable=True) response_at = Column(DateTime(timezone=True), nullable=True) closed_at = Column(DateTime(timezone=True), nullable=True) closed_by = Column(Integer, 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()) practice = relationship("RemissionPractice", back_populates="amendment_requests")