feat: add DODA module with DTOs, models, and service layer for managing DODA operations

This commit is contained in:
2025-12-06 23:36:35 -06:00
parent 6f2b755898
commit 3fdfd70633
4 changed files with 977 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
"""
Módulo de DODA (Documentos de Operación de Aduana)
"""

View File

@@ -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

View File

@@ -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"<Doda(id={self.id}, integration_number={self.integration_number}, status={self.status})>"
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"<DodaContainer(id={self.id}, doda_id={self.doda_id}, container_line={self.container_line})>"
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"<DodaContainerSeal(id={self.id}, doda_id={self.doda_id}, seal_line={self.seal_line})>"
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"<DodaAmericanPedimento(id={self.id}, doda_id={self.doda_id}, american_pedimento_line={self.american_pedimento_line})>"
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"<DodaPedimento(id={self.id}, doda_id={self.doda_id}, pedimento_line={self.pedimento_line})>"

View File

@@ -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()
)