From 3fdfd70633e8f22bbfbb50af43396ddad37cdcc5 Mon Sep 17 00:00:00 2001 From: acazares Date: Sat, 6 Dec 2025 23:36:35 -0600 Subject: [PATCH] feat: add DODA module with DTOs, models, and service layer for managing DODA operations --- .../a76/general_catalogs/doda/__init__.py | 3 + .../modules/a76/general_catalogs/doda/dto.py | 423 ++++++++++++++++++ .../a76/general_catalogs/doda/models.py | 263 +++++++++++ .../a76/general_catalogs/doda/service.py | 288 ++++++++++++ 4 files changed, 977 insertions(+) create mode 100644 backend/api/v1/modules/a76/general_catalogs/doda/__init__.py create mode 100644 backend/api/v1/modules/a76/general_catalogs/doda/dto.py create mode 100644 backend/api/v1/modules/a76/general_catalogs/doda/models.py create mode 100644 backend/api/v1/modules/a76/general_catalogs/doda/service.py diff --git a/backend/api/v1/modules/a76/general_catalogs/doda/__init__.py b/backend/api/v1/modules/a76/general_catalogs/doda/__init__.py new file mode 100644 index 00000000..9c5e22f4 --- /dev/null +++ b/backend/api/v1/modules/a76/general_catalogs/doda/__init__.py @@ -0,0 +1,3 @@ +""" +Módulo de DODA (Documentos de Operación de Aduana) +""" diff --git a/backend/api/v1/modules/a76/general_catalogs/doda/dto.py b/backend/api/v1/modules/a76/general_catalogs/doda/dto.py new file mode 100644 index 00000000..ad1b3e07 --- /dev/null +++ b/backend/api/v1/modules/a76/general_catalogs/doda/dto.py @@ -0,0 +1,423 @@ +""" +DTOs (Data Transfer Objects) para módulo de DODA +""" + +from datetime import datetime +from decimal import Decimal +from typing import Optional, List + +from pydantic import BaseModel, Field + + +# ============ DODA CONTAINER SEAL DTOS ============ +class DodaContainerSealCreateDTO(BaseModel): + """DTO para crear un candado de contenedor""" + + seal_value: Optional[str] = Field( + None, max_length=21, description="Seal value") + + class Config: + from_attributes = True + + +class DodaContainerSealResponseDTO(BaseModel): + """DTO para responder con datos de un candado""" + + id: int + doda_sys_id: int + seal_line: int + seal_value: Optional[str] = None + + class Config: + from_attributes = True + + +# ============ DODA CONTAINER DTOS ============ +class DodaContainerCreateDTO(BaseModel): + """DTO para crear un contenedor""" + + container_value: Optional[str] = Field( + None, max_length=20, description="Container value") + seals: Optional[str] = Field(None, max_length=254, description="Seals") + seals_detail: Optional[List[DodaContainerSealCreateDTO]] = Field( + None, description="Container seals" + ) + + class Config: + from_attributes = True + + +class DodaContainerUpdateDTO(BaseModel): + """DTO para actualizar un contenedor""" + + container_value: Optional[str] = Field( + None, max_length=20, description="Container value") + seals: Optional[str] = Field(None, max_length=254, description="Seals") + + class Config: + from_attributes = True + + +class DodaContainerResponseDTO(BaseModel): + """DTO para responder con datos de un contenedor""" + + id: int + doda_sys_id: int + container_line: int + container_value: Optional[str] = None + seals: Optional[str] = None + seals_detail: Optional[List[DodaContainerSealResponseDTO]] = None + + class Config: + from_attributes = True + + +# ============ DODA AMERICAN PEDIMENTO DTOS ============ +class DodaAmericanPedimentoCreateDTO(BaseModel): + """DTO para crear un pedimento americano""" + + american_pedimento_type: Optional[str] = Field( + None, max_length=2, description="American pedimento type" + ) + american_pedimento_value: Optional[str] = Field( + None, max_length=20, description="American pedimento value" + ) + + class Config: + from_attributes = True + + +class DodaAmericanPedimentoUpdateDTO(BaseModel): + """DTO para actualizar un pedimento americano""" + + american_pedimento_type: Optional[str] = Field( + None, max_length=2, description="American pedimento type" + ) + american_pedimento_value: Optional[str] = Field( + None, max_length=20, description="American pedimento value" + ) + + class Config: + from_attributes = True + + +class DodaAmericanPedimentoResponseDTO(BaseModel): + """DTO para responder con datos de un pedimento americano""" + + id: int + doda_sys_id: int + american_pedimento_line: int + american_pedimento_type: Optional[str] = None + american_pedimento_value: Optional[str] = None + + class Config: + from_attributes = True + + +# ============ DODA PEDIMENTO DTOS ============ +class DodaPedimentoCreateDTO(BaseModel): + """DTO para crear un pedimento DODA""" + + authorization_patent: Optional[str] = Field( + None, max_length=10, description="Authorization patent" + ) + document: Optional[str] = Field( + None, max_length=50, description="Document") + shipment: Optional[str] = Field( + None, max_length=11, description="Shipment") + cove: Optional[str] = Field(None, max_length=50, description="COVE") + umc: Optional[str] = Field(None, max_length=20, description="UMC") + effective_amount_usd: Optional[Decimal] = Field( + None, description="Effective amount USD") + difference_amount_usd: Optional[Decimal] = Field( + None, description="Difference amount USD") + dta_niu: Optional[str] = Field(None, max_length=20, description="DTA NIU") + article_7: Optional[bool] = Field(None, description="Article 7") + pedimento_sys_id: Optional[int] = Field( + None, description="Pedimento system ID") + invoice_line: Optional[int] = Field(None, description="Invoice line") + part_ii_line: Optional[int] = Field(None, description="Part II line") + pedimento_type: Optional[str] = Field( + None, max_length=20, description="Pedimento type") + zero_packaging_validation: Optional[bool] = Field( + None, description="Zero packaging validation" + ) + + class Config: + from_attributes = True + + +class DodaPedimentoUpdateDTO(BaseModel): + """DTO para actualizar un pedimento DODA""" + + authorization_patent: Optional[str] = Field( + None, max_length=10, description="Authorization patent" + ) + document: Optional[str] = Field( + None, max_length=50, description="Document") + shipment: Optional[str] = Field( + None, max_length=11, description="Shipment") + cove: Optional[str] = Field(None, max_length=50, description="COVE") + umc: Optional[str] = Field(None, max_length=20, description="UMC") + effective_amount_usd: Optional[Decimal] = Field( + None, description="Effective amount USD") + difference_amount_usd: Optional[Decimal] = Field( + None, description="Difference amount USD") + dta_niu: Optional[str] = Field(None, max_length=20, description="DTA NIU") + article_7: Optional[bool] = Field(None, description="Article 7") + pedimento_sys_id: Optional[int] = Field( + None, description="Pedimento system ID") + invoice_line: Optional[int] = Field(None, description="Invoice line") + part_ii_line: Optional[int] = Field(None, description="Part II line") + pedimento_type: Optional[str] = Field( + None, max_length=20, description="Pedimento type") + zero_packaging_validation: Optional[bool] = Field( + None, description="Zero packaging validation" + ) + + class Config: + from_attributes = True + + +class DodaPedimentoResponseDTO(BaseModel): + """DTO para responder con datos de un pedimento DODA""" + + id: int + doda_sys_id: int + pedimento_line: int + authorization_patent: Optional[str] = None + document: Optional[str] = None + shipment: Optional[str] = None + cove: Optional[str] = None + umc: Optional[str] = None + effective_amount_usd: Optional[Decimal] = None + difference_amount_usd: Optional[Decimal] = None + dta_niu: Optional[str] = None + article_7: Optional[bool] = None + pedimento_sys_id: Optional[int] = None + invoice_line: Optional[int] = None + part_ii_line: Optional[int] = None + pedimento_type: Optional[str] = None + zero_packaging_validation: Optional[bool] = None + + class Config: + from_attributes = True + + +# ============ MAIN DODA DTOS ============ +class DodaCreateDTO(BaseModel): + """DTO para crear un DODA""" + + integration_number: Optional[str] = Field( + None, max_length=30, description="Integration number") + doda_date: Optional[int] = Field(None, description="DODA date") + doda_time: Optional[int] = Field(None, description="DODA time") + dispatch_customs: Optional[str] = Field( + None, max_length=3, description="Dispatch customs") + customs_sections: Optional[str] = Field( + None, max_length=3, description="Customs sections") + patent: Optional[str] = Field(None, max_length=4, description="Patent") + pedimentos: Optional[str] = Field( + None, max_length=80, description="Pedimentos") + caat: Optional[str] = Field(None, max_length=10, description="CAAT") + transport_identification: Optional[str] = Field( + None, max_length=20, description="Transport identification" + ) + fast_id: Optional[str] = Field(None, max_length=20, description="FAST ID") + operation_type: Optional[str] = Field( + None, max_length=1, description="Operation type") + selected: Optional[bool] = Field(None, description="Selected") + user_selected: Optional[str] = Field( + None, max_length=30, description="User selected") + last_user: Optional[str] = Field( + None, max_length=30, description="Last user") + responsible: Optional[str] = Field( + None, max_length=14, description="Responsible") + carrier: Optional[str] = Field(None, max_length=8, description="Carrier") + shipments: Optional[str] = Field( + None, max_length=80, description="Shipments") + pedimento_type: Optional[str] = Field( + None, max_length=30, description="Pedimento type") + original_chain: Optional[str] = Field( + None, max_length=5000, description="Original chain") + serial_number: Optional[str] = Field( + None, max_length=21, description="Serial number") + electronic_signature: Optional[str] = Field( + None, max_length=2000, description="Electronic signature" + ) + transaction_number: Optional[str] = Field( + None, max_length=30, description="Transaction number") + status: Optional[str] = Field(None, max_length=30, description="Status") + linq_sat_qr: Optional[str] = Field( + None, max_length=1000, description="LINQ SAT QR") + sat_certificate: Optional[str] = Field( + None, max_length=2001, description="SAT certificate") + sat_digital_seal: Optional[str] = Field( + None, description="SAT digital seal") + xml_doda_sent_path: Optional[str] = Field( + None, max_length=1000, description="XML DODA sent path") + xml_doda_response_path: Optional[str] = Field( + None, max_length=1000, description="XML DODA response path" + ) + sat_original_chain: Optional[str] = Field( + None, description="SAT original chain") + customs_clearance: Optional[int] = Field( + None, description="Customs clearance") + unique_badge_number: Optional[str] = Field( + None, max_length=250, description="Unique badge number" + ) + + class Config: + from_attributes = True + + +class DodaUpdateDTO(BaseModel): + """DTO para actualizar un DODA""" + + integration_number: Optional[str] = Field( + None, max_length=30, description="Integration number") + doda_date: Optional[int] = Field(None, description="DODA date") + doda_time: Optional[int] = Field(None, description="DODA time") + dispatch_customs: Optional[str] = Field( + None, max_length=3, description="Dispatch customs") + customs_sections: Optional[str] = Field( + None, max_length=3, description="Customs sections") + patent: Optional[str] = Field(None, max_length=4, description="Patent") + pedimentos: Optional[str] = Field( + None, max_length=80, description="Pedimentos") + caat: Optional[str] = Field(None, max_length=10, description="CAAT") + transport_identification: Optional[str] = Field( + None, max_length=20, description="Transport identification" + ) + fast_id: Optional[str] = Field(None, max_length=20, description="FAST ID") + operation_type: Optional[str] = Field( + None, max_length=1, description="Operation type") + selected: Optional[bool] = Field(None, description="Selected") + user_selected: Optional[str] = Field( + None, max_length=30, description="User selected") + last_user: Optional[str] = Field( + None, max_length=30, description="Last user") + responsible: Optional[str] = Field( + None, max_length=14, description="Responsible") + carrier: Optional[str] = Field(None, max_length=8, description="Carrier") + shipments: Optional[str] = Field( + None, max_length=80, description="Shipments") + pedimento_type: Optional[str] = Field( + None, max_length=30, description="Pedimento type") + original_chain: Optional[str] = Field( + None, max_length=5000, description="Original chain") + serial_number: Optional[str] = Field( + None, max_length=21, description="Serial number") + electronic_signature: Optional[str] = Field( + None, max_length=2000, description="Electronic signature" + ) + transaction_number: Optional[str] = Field( + None, max_length=30, description="Transaction number") + status: Optional[str] = Field(None, max_length=30, description="Status") + linq_sat_qr: Optional[str] = Field( + None, max_length=1000, description="LINQ SAT QR") + sat_certificate: Optional[str] = Field( + None, max_length=2001, description="SAT certificate") + sat_digital_seal: Optional[str] = Field( + None, description="SAT digital seal") + xml_doda_sent_path: Optional[str] = Field( + None, max_length=1000, description="XML DODA sent path") + xml_doda_response_path: Optional[str] = Field( + None, max_length=1000, description="XML DODA response path" + ) + sat_original_chain: Optional[str] = Field( + None, description="SAT original chain") + customs_clearance: Optional[int] = Field( + None, description="Customs clearance") + unique_badge_number: Optional[str] = Field( + None, max_length=250, description="Unique badge number" + ) + + class Config: + from_attributes = True + + +class DodaResponseDTO(BaseModel): + """DTO para responder con datos de un DODA""" + + sys_id: int + integration_number: Optional[str] = None + doda_date: Optional[int] = None + doda_time: Optional[int] = None + dispatch_customs: Optional[str] = None + customs_sections: Optional[str] = None + patent: Optional[str] = None + pedimentos: Optional[str] = None + caat: Optional[str] = None + transport_identification: Optional[str] = None + fast_id: Optional[str] = None + operation_type: Optional[str] = None + selected: Optional[bool] = None + user_selected: Optional[str] = None + last_user: Optional[str] = None + responsible: Optional[str] = None + carrier: Optional[str] = None + shipments: Optional[str] = None + pedimento_type: Optional[str] = None + original_chain: Optional[str] = None + serial_number: Optional[str] = None + electronic_signature: Optional[str] = None + transaction_number: Optional[str] = None + status: Optional[str] = None + linq_sat_qr: Optional[str] = None + sat_certificate: Optional[str] = None + sat_digital_seal: Optional[str] = None + xml_doda_sent_path: Optional[str] = None + xml_doda_response_path: Optional[str] = None + sat_original_chain: Optional[str] = None + customs_clearance: Optional[int] = None + unique_badge_number: Optional[str] = None + containers: Optional[List[DodaContainerResponseDTO]] = None + american_pedimentos: Optional[List[DodaAmericanPedimentoResponseDTO]] = None + pedimentos_detail: Optional[List[DodaPedimentoResponseDTO]] = None + + class Config: + from_attributes = True + + +class DodaDetailResponseDTO(BaseModel): + """DTO detallado para responder con todos los datos de un DODA""" + + sys_id: int + integration_number: Optional[str] = None + doda_date: Optional[int] = None + doda_time: Optional[int] = None + dispatch_customs: Optional[str] = None + customs_sections: Optional[str] = None + patent: Optional[str] = None + pedimentos: Optional[str] = None + caat: Optional[str] = None + transport_identification: Optional[str] = None + fast_id: Optional[str] = None + operation_type: Optional[str] = None + selected: Optional[bool] = None + user_selected: Optional[str] = None + last_user: Optional[str] = None + responsible: Optional[str] = None + carrier: Optional[str] = None + shipments: Optional[str] = None + pedimento_type: Optional[str] = None + original_chain: Optional[str] = None + serial_number: Optional[str] = None + electronic_signature: Optional[str] = None + transaction_number: Optional[str] = None + status: Optional[str] = None + linq_sat_qr: Optional[str] = None + sat_certificate: Optional[str] = None + sat_digital_seal: Optional[str] = None + xml_doda_sent_path: Optional[str] = None + xml_doda_response_path: Optional[str] = None + sat_original_chain: Optional[str] = None + customs_clearance: Optional[int] = None + unique_badge_number: Optional[str] = None + containers: List[DodaContainerResponseDTO] = [] + american_pedimentos: List[DodaAmericanPedimentoResponseDTO] = [] + pedimentos_detail: List[DodaPedimentoResponseDTO] = [] + + class Config: + from_attributes = True diff --git a/backend/api/v1/modules/a76/general_catalogs/doda/models.py b/backend/api/v1/modules/a76/general_catalogs/doda/models.py new file mode 100644 index 00000000..3a635d17 --- /dev/null +++ b/backend/api/v1/modules/a76/general_catalogs/doda/models.py @@ -0,0 +1,263 @@ +""" +Modelos ORM para gestión de DODA (Documentos de Operación de Aduana) +""" + +from typing import Optional + +from api.v1.common.base_models import TenantScopedMixin +from core.database import Base +from sqlalchemy import ( + ForeignKeyConstraint, + Integer, + PrimaryKeyConstraint, + String, + Text, + LargeBinary, + Numeric, + Boolean, +) +from sqlalchemy.orm import Mapped, mapped_column, relationship + + +class Doda(Base, TenantScopedMixin): + """ + Modelo para la tabla Doda - Documentos de Operación de Aduana + """ + + __tablename__ = "doda" # gDODA + __table_args__ = ( + PrimaryKeyConstraint("id", name="doda_pkey"), + {"schema": "a76"}, + ) + + # Primary key + id: Mapped[int] = mapped_column( + Integer, primary_key=True, autoincrement=True) + + # Integration and timestamps + integration_number: Mapped[Optional[str]] = mapped_column(String(30)) + doda_date: Mapped[Optional[int]] = mapped_column(Integer) + doda_time: Mapped[Optional[int]] = mapped_column(Integer) + + # Customs information + dispatch_customs: Mapped[Optional[str]] = mapped_column(String(3)) + customs_sections: Mapped[Optional[str]] = mapped_column(String(3)) + patent: Mapped[Optional[str]] = mapped_column(String(4)) + pedimentos: Mapped[Optional[str]] = mapped_column(String(80)) + caat: Mapped[Optional[str]] = mapped_column(String(10)) + + # Transport and identifiers + transport_identification: Mapped[Optional[str]] = mapped_column(String(20)) + fast_id: Mapped[Optional[str]] = mapped_column(String(20)) + operation_type: Mapped[Optional[str]] = mapped_column(String(1)) + + # Selection info + selected: Mapped[Optional[bool]] = mapped_column(Boolean) + user_selected: Mapped[Optional[str]] = mapped_column(String(30)) + last_user: Mapped[Optional[str]] = mapped_column(String(30)) + + # Responsible parties + responsible: Mapped[Optional[str]] = mapped_column(String(14)) + carrier: Mapped[Optional[str]] = mapped_column(String(8)) + shipments: Mapped[Optional[str]] = mapped_column(String(80)) + pedimento_type: Mapped[Optional[str]] = mapped_column(String(30)) + + # Digital signatures and certificates + original_chain: Mapped[Optional[str]] = mapped_column(String(5000)) + serial_number: Mapped[Optional[str]] = mapped_column(String(21)) + electronic_signature: Mapped[Optional[str]] = mapped_column(String(2000)) + + # Transaction info + transaction_number: Mapped[Optional[str]] = mapped_column(String(30)) + status: Mapped[Optional[str]] = mapped_column(String(30)) + + # SAT information + linq_sat_qr: Mapped[Optional[str]] = mapped_column(String(1000)) + sat_certificate: Mapped[Optional[str]] = mapped_column(String(2001)) + sat_digital_seal: Mapped[Optional[Text]] = mapped_column(Text) + + # XML Paths + xml_doda_sent_path: Mapped[Optional[str]] = mapped_column(String(1000)) + xml_doda_response_path: Mapped[Optional[str]] = mapped_column(String(1000)) + + # SAT original chain + sat_original_chain: Mapped[Optional[Text]] = mapped_column(Text) + + # Customs clearance + customs_clearance: Mapped[Optional[int]] = mapped_column(Integer) + unique_badge_number: Mapped[Optional[str]] = mapped_column(String(250)) + + # Relationships + containers: Mapped[list["DodaContainer"]] = relationship( + "DodaContainer", back_populates="doda", cascade="all, delete-orphan" + ) + american_pedimentos: Mapped[list["DodaAmericanPedimento"]] = relationship( + "DodaAmericanPedimento", back_populates="doda", cascade="all, delete-orphan" + ) + pedimentos_detail: Mapped[list["DodaPedimento"]] = relationship( + "DodaPedimento", back_populates="doda", cascade="all, delete-orphan" + ) + + def __repr__(self): + return f"" + + +class DodaContainer(Base, TenantScopedMixin): + """ + Modelo para la tabla DodaContainer - Contenedores en DODA + """ + + __tablename__ = "doda_containers" # gDoda_Contenedores + __table_args__ = ( + PrimaryKeyConstraint("id", name="doda_containers_pkey"), + ForeignKeyConstraint( + ["doda_id"], ["a76.doda.id"], name="fk_doda_containers_doda" + ), + {"schema": "a76"}, + ) + + # Primary key + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + + # Foreign key and line number + doda_id: Mapped[int] = mapped_column(Integer, nullable=False) + container_line: Mapped[int] = mapped_column(Integer, nullable=False) + + # Container information + container_value: Mapped[Optional[str]] = mapped_column(String(20)) + seals: Mapped[Optional[str]] = mapped_column(String(254)) + + # Relationships + doda: Mapped["Doda"] = relationship("Doda", back_populates="containers") + seals_detail: Mapped[list["DodaContainerSeal"]] = relationship( + "DodaContainerSeal", back_populates="container", cascade="all, delete-orphan" + ) + + def __repr__(self): + return f"" + + +class DodaContainerSeal(Base, TenantScopedMixin): + """ + Modelo para la tabla DodaContainerSeal - Candados de Contenedores + """ + + __tablename__ = "doda_container_seals" # gDoda_Contenedores_Candados + __table_args__ = ( + PrimaryKeyConstraint("id", name="doda_container_seals_pkey"), + ForeignKeyConstraint( + ["container_id"], ["a76.doda_containers.id"], name="fk_doda_container_seals_container" + ), + {"schema": "a76"}, + ) + + # Primary key + id: Mapped[int] = mapped_column( + Integer, primary_key=True, autoincrement=True) + + # Foreign key and line info + container_id: Mapped[int] = mapped_column(Integer, nullable=False) + doda_id: Mapped[int] = mapped_column(Integer, nullable=False) + seal_line: Mapped[int] = mapped_column(Integer, nullable=False) + + # Seal information + seal_value: Mapped[Optional[str]] = mapped_column(String(21)) + + # Relationships + container: Mapped["DodaContainer"] = relationship( + "DodaContainer", back_populates="seals_detail" + ) + + def __repr__(self): + return f"" + + +class DodaAmericanPedimento(Base, TenantScopedMixin): + """ + Modelo para la tabla DodaAmericanPedimento - Pedimentos Americanos en DODA + """ + + __tablename__ = "doda_american_pedimentos" # gDoda_PedimentoAmericano + __table_args__ = ( + PrimaryKeyConstraint("id", name="doda_american_pedimentos_pkey"), + ForeignKeyConstraint( + ["doda_id"], ["a76.doda.id"], name="fk_doda_american_pedimentos_doda" + ), + {"schema": "a76"}, + ) + + # Primary key + id: Mapped[int] = mapped_column( + Integer, primary_key=True, autoincrement=True) + + # Foreign key and line number + doda_id: Mapped[int] = mapped_column(Integer, nullable=False) + american_pedimento_line: Mapped[int] = mapped_column( + Integer, nullable=False) + + # American pedimento information + american_pedimento_type: Mapped[Optional[str]] = mapped_column(String(2)) + american_pedimento_value: Mapped[Optional[str]] = mapped_column(String(20)) + + # Relationships + doda: Mapped["Doda"] = relationship( + "Doda", back_populates="american_pedimentos") + + def __repr__(self): + return f"" + + +class DodaPedimento(Base, TenantScopedMixin): + """ + Modelo para la tabla DodaPedimento - Pedimentos en DODA + """ + + __tablename__ = "doda_pedimentos" # gDoda_Pedimentos + __table_args__ = ( + PrimaryKeyConstraint("id", name="doda_pedimentos_pkey"), + ForeignKeyConstraint( + ["doda_id"], ["a76.doda.id"], name="fk_doda_pedimentos_doda" + ), + {"schema": "a76"}, + ) + + # Primary key + id: Mapped[int] = mapped_column( + Integer, primary_key=True, autoincrement=True) + + # Foreign key and line number + doda_id: Mapped[int] = mapped_column(Integer, nullable=False) + pedimento_line: Mapped[int] = mapped_column(Integer, nullable=False) + + # Authorization and document info + authorization_patent: Mapped[Optional[str]] = mapped_column(String(10)) + document: Mapped[Optional[str]] = mapped_column(String(50)) + shipment: Mapped[Optional[str]] = mapped_column(String(11)) + + # Commercial information + cove: Mapped[Optional[str]] = mapped_column(String(50)) + umc: Mapped[Optional[str]] = mapped_column(String(20)) + + # Financial information + effective_amount_usd: Mapped[Optional[Numeric] + ] = mapped_column(Numeric(15, 2)) + difference_amount_usd: Mapped[Optional[Numeric] + ] = mapped_column(Numeric(15, 2)) + + # Additional identifiers + dta_niu: Mapped[Optional[str]] = mapped_column(String(20)) + article_7: Mapped[Optional[bool]] = mapped_column(Boolean) + pedimento_id: Mapped[Optional[int]] = mapped_column(Integer) + invoice_line: Mapped[Optional[int]] = mapped_column(Integer) + part_ii_line: Mapped[Optional[int]] = mapped_column(Integer) + + # Type and validation + pedimento_type: Mapped[Optional[str]] = mapped_column(String(20)) + zero_packaging_validation: Mapped[Optional[bool]] = mapped_column(Boolean) + + # Relationships + doda: Mapped["Doda"] = relationship( + "Doda", back_populates="pedimentos_detail") + + def __repr__(self): + return f"" diff --git a/backend/api/v1/modules/a76/general_catalogs/doda/service.py b/backend/api/v1/modules/a76/general_catalogs/doda/service.py new file mode 100644 index 00000000..a33a096c --- /dev/null +++ b/backend/api/v1/modules/a76/general_catalogs/doda/service.py @@ -0,0 +1,288 @@ +""" +Capa de servicio para lógica de negocio de DODA +""" + +import logging +from typing import Any, Dict, List, Optional, Tuple + +from fastapi import HTTPException +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import Session + +from .dto import ( + DodaCreateDTO, + DodaResponseDTO, + DodaUpdateDTO, + DodaContainerCreateDTO, + DodaContainerUpdateDTO, + DodaAmericanPedimentoCreateDTO, + DodaAmericanPedimentoUpdateDTO, + DodaPedimentoCreateDTO, + DodaPedimentoUpdateDTO, +) +from .models import ( + Doda, + DodaContainer, + DodaContainerSeal, + DodaAmericanPedimento, + DodaPedimento, +) + +logger = logging.getLogger(__name__) + + +class DodaService: + """Servicio para gestión de DODA""" + + def __init__(self, db: Session): + self.db = db + + # ============ DODA MAIN CRUD ============ + @staticmethod + def get_all( + db: Session, + skip: int = 0, + limit: int = 50, + filters: Optional[Dict[str, Any]] = None, + ) -> Tuple[List[Doda], int]: + """Get all DODAs with pagination""" + query = db.query(Doda) + + if filters: + if filters.get("integration_number"): + query = query.filter( + Doda.integration_number.ilike( + f"%{filters['integration_number']}%") + ) + if filters.get("status"): + query = query.filter( + Doda.status.ilike(f"%{filters['status']}%")) + if filters.get("patent"): + query = query.filter( + Doda.patent.ilike(f"%{filters['patent']}%")) + + total = query.count() + dodas = query.offset(skip).limit(limit).all() + return dodas, total + + @staticmethod + def get_by_id(db: Session, sys_id: int) -> Optional[Doda]: + """Get DODA by ID""" + return db.query(Doda).filter(Doda.sys_id == sys_id).first() + + @staticmethod + def create(db: Session, doda_data: DodaCreateDTO) -> Doda: + """Create a new DODA""" + try: + db_doda = Doda(**doda_data.model_dump(exclude_unset=True)) + db.add(db_doda) + db.commit() + db.refresh(db_doda) + return db_doda + except IntegrityError as e: + db.rollback() + logger.error(f"IntegrityError creating DODA: {str(e)}") + raise HTTPException(status_code=400, detail="Error creating DODA") + except Exception as e: + db.rollback() + logger.error(f"Error creating DODA: {str(e)}") + raise HTTPException(status_code=500, detail="Error creating DODA") + + @staticmethod + def update(db: Session, sys_id: int, doda_data: DodaUpdateDTO) -> Optional[Doda]: + """Update a DODA""" + try: + db_doda = db.query(Doda).filter(Doda.sys_id == sys_id).first() + if not db_doda: + return None + + for key, value in doda_data.model_dump(exclude_unset=True).items(): + setattr(db_doda, key, value) + + db.commit() + db.refresh(db_doda) + return db_doda + except IntegrityError as e: + db.rollback() + logger.error(f"IntegrityError updating DODA: {str(e)}") + raise HTTPException(status_code=400, detail="Error updating DODA") + except Exception as e: + db.rollback() + logger.error(f"Error updating DODA: {str(e)}") + raise HTTPException(status_code=500, detail="Error updating DODA") + + @staticmethod + def delete(db: Session, sys_id: int) -> bool: + """Delete a DODA""" + try: + db_doda = db.query(Doda).filter(Doda.sys_id == sys_id).first() + if not db_doda: + return False + + db.delete(db_doda) + db.commit() + return True + except Exception as e: + db.rollback() + logger.error(f"Error deleting DODA: {str(e)}") + raise HTTPException(status_code=500, detail="Error deleting DODA") + + # ============ CONTAINERS ============ + @staticmethod + def add_container( + db: Session, sys_id: int, container_data: DodaContainerCreateDTO + ) -> Optional[DodaContainer]: + """Add a container to a DODA""" + try: + doda = db.query(Doda).filter(Doda.sys_id == sys_id).first() + if not doda: + return None + + # Get max line number + max_line = ( + db.query(DodaContainer) + .filter(DodaContainer.doda_sys_id == sys_id) + .count() + ) + + db_container = DodaContainer( + doda_sys_id=sys_id, + container_line=max_line + 1, + **{ + k: v + for k, v in container_data.model_dump(exclude_unset=True).items() + if k != "seals_detail" + }, + ) + db.add(db_container) + db.commit() + db.refresh(db_container) + return db_container + except Exception as e: + db.rollback() + logger.error(f"Error adding container: {str(e)}") + raise HTTPException( + status_code=500, detail="Error adding container") + + @staticmethod + def update_container( + db: Session, + sys_id: int, + container_line: int, + container_data: DodaContainerUpdateDTO, + ) -> Optional[DodaContainer]: + """Update a container""" + try: + db_container = ( + db.query(DodaContainer) + .filter( + DodaContainer.doda_sys_id == sys_id, + DodaContainer.container_line == container_line, + ) + .first() + ) + if not db_container: + return None + + for key, value in container_data.model_dump(exclude_unset=True).items(): + setattr(db_container, key, value) + + db.commit() + db.refresh(db_container) + return db_container + except Exception as e: + db.rollback() + logger.error(f"Error updating container: {str(e)}") + raise HTTPException( + status_code=500, detail="Error updating container") + + @staticmethod + def get_containers(db: Session, sys_id: int) -> List[DodaContainer]: + """Get all containers for a DODA""" + return ( + db.query(DodaContainer) + .filter(DodaContainer.doda_sys_id == sys_id) + .all() + ) + + # ============ AMERICAN PEDIMENTOS ============ + @staticmethod + def add_american_pedimento( + db: Session, sys_id: int, pedimento_data: DodaAmericanPedimentoCreateDTO + ) -> Optional[DodaAmericanPedimento]: + """Add an American pedimento to a DODA""" + try: + doda = db.query(Doda).filter(Doda.sys_id == sys_id).first() + if not doda: + return None + + max_line = ( + db.query(DodaAmericanPedimento) + .filter(DodaAmericanPedimento.doda_sys_id == sys_id) + .count() + ) + + db_pedimento = DodaAmericanPedimento( + doda_sys_id=sys_id, + american_pedimento_line=max_line + 1, + **pedimento_data.model_dump(exclude_unset=True), + ) + db.add(db_pedimento) + db.commit() + db.refresh(db_pedimento) + return db_pedimento + except Exception as e: + db.rollback() + logger.error(f"Error adding American pedimento: {str(e)}") + raise HTTPException( + status_code=500, detail="Error adding American pedimento" + ) + + @staticmethod + def get_american_pedimentos(db: Session, sys_id: int) -> List[DodaAmericanPedimento]: + """Get all American pedimentos for a DODA""" + return ( + db.query(DodaAmericanPedimento) + .filter(DodaAmericanPedimento.doda_sys_id == sys_id) + .all() + ) + + # ============ PEDIMENTOS ============ + @staticmethod + def add_pedimento( + db: Session, sys_id: int, pedimento_data: DodaPedimentoCreateDTO + ) -> Optional[DodaPedimento]: + """Add a pedimento to a DODA""" + try: + doda = db.query(Doda).filter(Doda.sys_id == sys_id).first() + if not doda: + return None + + max_line = ( + db.query(DodaPedimento) + .filter(DodaPedimento.doda_sys_id == sys_id) + .count() + ) + + db_pedimento = DodaPedimento( + doda_sys_id=sys_id, + pedimento_line=max_line + 1, + **pedimento_data.model_dump(exclude_unset=True), + ) + db.add(db_pedimento) + db.commit() + db.refresh(db_pedimento) + return db_pedimento + except Exception as e: + db.rollback() + logger.error(f"Error adding pedimento: {str(e)}") + raise HTTPException( + status_code=500, detail="Error adding pedimento") + + @staticmethod + def get_pedimentos(db: Session, sys_id: int) -> List[DodaPedimento]: + """Get all pedimentos for a DODA""" + return ( + db.query(DodaPedimento).filter( + DodaPedimento.doda_sys_id == sys_id).all() + )