""" 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, BigInteger 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) # nome originale # File upload (bind mount /var/uploads dentro container) storage_path = Column(String(1024), nullable=True) # relativo a /var/uploads mime = Column(String(128), nullable=True) size_bytes = Column(BigInteger, nullable=True) sha256 = Column(String(64), nullable=True) uploaded_by = Column(Integer, nullable=True) uploaded_at = Column(DateTime(timezone=True), nullable=True) # 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) # nome originale # File upload storage_path = Column(String(1024), nullable=True) mime = Column(String(128), nullable=True) size_bytes = Column(BigInteger, nullable=True) sha256 = Column(String(64), nullable=True) uploaded_by = Column(Integer, nullable=True) uploaded_at = Column(DateTime(timezone=True), 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) # nome originale uploaded_at = Column(DateTime(timezone=True), nullable=True) expires_at = Column(Date, nullable=True) notes = Column(Text, nullable=True) # File upload storage_path = Column(String(1024), nullable=True) mime = Column(String(128), nullable=True) size_bytes = Column(BigInteger, nullable=True) sha256 = Column(String(64), nullable=True) uploaded_by = Column(Integer, 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")