Merge branch 'development' of https://git.aduanasoft.com/ADUANASOFT/anexo76 into development

This commit is contained in:
2026-02-26 12:37:29 -06:00
57 changed files with 4024 additions and 807 deletions

View File

@@ -3,7 +3,7 @@ import logging
from core.database import get_core_db
from core.security import get_current_user, validate_access_to_resource
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query, Request
from pydantic import BaseModel
from sqlalchemy.orm import Session
@@ -137,6 +137,7 @@ class TenantCRUDRoutes(
description=f"Get paginated list of {self.resource_name}s with optional filters",
)
async def list_resources(
request: Request,
company_id: int = Query(..., description="Company ID"),
page: int = Query(1, ge=1, description="Page number"),
page_size: int = Query(
@@ -145,13 +146,6 @@ class TenantCRUDRoutes(
le=self.max_page_size,
description="Page size",
),
status: Optional[str] = Query(None, description="Filter by status"),
operation_type: Optional[str] = Query(
None, description="Filter by operation type"
),
invoice_type: Optional[str] = Query(
None, description="Filter by invoice type"
),
db: Session = Depends(self.db_dependency),
current_user: Dict[str, Any] = Depends(self.auth_dependency),
):
@@ -164,13 +158,15 @@ class TenantCRUDRoutes(
)
skip = (page - 1) * page_size
filters = {}
if status:
filters["status"] = status
if operation_type:
filters["operation_type"] = operation_type
if invoice_type:
filters["invoice_type"] = invoice_type
# Extraer todos los parámetros de búsqueda dinámicamente
# Excluimos los parámetros estándar de paginación y control
standard_params = {"company_id", "page", "page_size"}
filters = {
k: v
for k, v in request.query_params.items()
if k not in standard_params and v is not None and v != ""
}
items, total = self.service.get_all(
db, tenant_id, company_id, skip, page_size, filters

View File

@@ -46,7 +46,7 @@ class ClientProviderProgramsDTO(BaseModel):
program_number: Optional[str] = Field(
None, max_length=40, description="Program number"
)
prosec: Optional[int] = Field(None, description="PROSEC")
prosec: Optional[str] = Field(None, max_length=8, description="PROSEC")
prosec_authorization: Optional[str] = Field(
None, max_length=20, description="PROSEC authorization"
)

View File

@@ -136,7 +136,7 @@ class ClientProviderPrograms(Base, TenantScopedMixin, TimestampMixin):
# Program information
program: Mapped[Optional[str]] = mapped_column(String(7))
program_number: Mapped[Optional[str]] = mapped_column(String(40))
prosec: Mapped[Optional[int]] = mapped_column(SmallInteger)
prosec: Mapped[Optional[str]] = mapped_column(String(8))
prosec_authorization: Mapped[Optional[str]] = mapped_column(String(20))
secon_auth_date: Mapped[Optional[int]] = mapped_column(Integer)
manufacturer_id: Mapped[Optional[str]] = mapped_column(String(25))

View File

@@ -43,7 +43,7 @@ class CustomsBrokerResponseDTO(CustomsBrokerBaseDTO):
broker_key: str
tenant_id: int
company_id: int
vu: Optional["CustomsBrokerVUCreateDTO"] = None
vu: Optional["CustomsBrokerVUResponseDTO"] = None
class Config:
from_attributes = True
@@ -95,6 +95,11 @@ class CustomsBrokerVUCreateDTO(BaseModel):
doda_web_service_access_key: Optional[str] = None
doda_fiel_access_key: Optional[str] = None
doda_xml_files_path: Optional[str] = None
tenant_id: Optional[int] = None
company_id: Optional[int] = None
class CustomsBrokerVUResponseDTO(CustomsBrokerVUCreateDTO):
customs_broker_id: int
class Config:
from_attributes = True
@@ -112,6 +117,8 @@ class CustomsBrokerPersonnelDTO(BaseModel):
last_name: Optional[str] = None
middle_name: Optional[str] = None
email: Optional[str] = None
tenant_id: Optional[int] = None
company_id: Optional[int] = None
class Config:
from_attributes = True

View File

@@ -62,7 +62,7 @@ def update_customs_broker(
@router.put(
"/customs-broker-vu/{broker_key}",
response_model=dto.CustomsBrokerVUCreateDTO,
response_model=dto.CustomsBrokerVUResponseDTO,
)
def update_customs_broker_vu(
broker_key: str,
@@ -77,7 +77,7 @@ def update_customs_broker_vu(
if not broker:
raise HTTPException(status_code=404, detail="Customs Broker not found")
updated_vu = services.CustomsBrokerVUService.update_vu(db, broker_key, vu_data)
updated_vu = services.CustomsBrokerVUService.update_vu(db, broker_key, vu_data, tenant_id, company_id)
if not updated_vu:
raise HTTPException(status_code=404, detail="Customs Broker VU not found")
return updated_vu
@@ -102,7 +102,7 @@ def update_customs_broker_personnel(
raise HTTPException(status_code=404, detail="Customs Broker not found")
updated_personnel = services.CustomsBrokerPersonnelService.update_personnel(
db, broker_key, line, personnel_data
db, broker_key, line, personnel_data, tenant_id, company_id
)
if not updated_personnel:
raise HTTPException(

View File

@@ -90,9 +90,17 @@ class CustomsBrokerVUService:
return new_vu
@staticmethod
def update_vu(db: Session, broker_key: str, vu_data: dto.CustomsBrokerVUCreateDTO):
def update_vu(db: Session, broker_key: str, vu_data: dto.CustomsBrokerVUCreateDTO, tenant_id: int, company_id: int):
# We need the custom broker ID to insert a new VU
broker = db.query(models.CustomsBroker).filter(models.CustomsBroker.broker_key == broker_key).first()
broker = (
db.query(models.CustomsBroker)
.filter(
models.CustomsBroker.broker_key == broker_key,
models.CustomsBroker.tenant_id == tenant_id,
models.CustomsBroker.company_id == company_id,
)
.first()
)
if not broker:
return None
@@ -108,6 +116,8 @@ class CustomsBrokerVUService:
else:
# Create new
new_vu_data = vu_data.model_dump()
new_vu_data["tenant_id"] = tenant_id
new_vu_data["company_id"] = company_id
new_vu = models.CustomsBrokerVU(customs_broker_id=broker.id, **new_vu_data)
db.add(new_vu)
db.commit()
@@ -125,24 +135,36 @@ class CustomsBrokerVUService:
class CustomsBrokerPersonnelService:
@staticmethod
def get_by_broker_key_and_line(db: Session, broker_key: str, line: int):
def get_by_broker_key_and_line(db: Session, broker_key: str, line: int, tenant_id: int, company_id: int):
return (
db.query(models.CustomsBrokerPersonnel)
.join(models.CustomsBroker)
.filter(
models.CustomsBroker.broker_key == broker_key,
models.CustomsBrokerPersonnel.line == line,
models.CustomsBroker.tenant_id == tenant_id,
models.CustomsBroker.company_id == company_id,
)
.first()
)
@staticmethod
def create_personnel(db: Session, broker_key: str, personnel_data: dto.CustomsBrokerPersonnelDTO):
broker = db.query(models.CustomsBroker).filter(models.CustomsBroker.broker_key == broker_key).first()
def create_personnel(db: Session, broker_key: str, personnel_data: dto.CustomsBrokerPersonnelDTO, tenant_id: int, company_id: int):
broker = (
db.query(models.CustomsBroker)
.filter(
models.CustomsBroker.broker_key == broker_key,
models.CustomsBroker.tenant_id == tenant_id,
models.CustomsBroker.company_id == company_id,
)
.first()
)
if not broker:
return None
new_personnel_data = personnel_data.model_dump()
new_personnel_data["tenant_id"] = tenant_id
new_personnel_data["company_id"] = company_id
new_personnel = models.CustomsBrokerPersonnel(customs_broker_id=broker.id, **new_personnel_data)
db.add(new_personnel)
db.commit()
@@ -155,16 +177,22 @@ class CustomsBrokerPersonnelService:
broker_key: str,
line: int,
personnel_data: dto.CustomsBrokerPersonnelDTO,
tenant_id: int,
company_id: int,
):
personnel = CustomsBrokerPersonnelService.get_by_broker_key_and_line(
db, broker_key, line
db, broker_key, line, tenant_id, company_id
)
if personnel:
for key, value in personnel_data.model_dump(exclude_unset=True).items():
setattr(personnel, key, value)
db.commit()
db.refresh(personnel)
return personnel
return personnel
else:
return CustomsBrokerPersonnelService.create_personnel(
db, broker_key, personnel_data, tenant_id, company_id
)
@staticmethod
def delete_personnel(db: Session, broker_key: str, line: int):

View File

@@ -6,6 +6,8 @@ from sqlalchemy.orm import Session
from api.v1.modules.public.reference_data.pedimento_codes.models import PedimentoCode
from api.v1.modules.public.reference_data.customs_sections.models import CustomsSection
from api.v1.modules.public.reference_data.code_pedimento_regimens.models import CodePedimentoRegimen
from api.v1.modules.public.reference_data.transport_types.models import TransportType
from api.v1.modules.public.reference_data.transport_modes.models import TransportMode
# Import A76 Services
from api.v1.modules.a76.customs_brokers.services import CustomsBrokerService
@@ -15,6 +17,8 @@ from api.v1.modules.a76.clients_and_providers.service import ClientProviderServi
from api.v1.modules.public.reference_data.pedimento_codes.dto import PedimentoCodeDTO
from api.v1.modules.public.reference_data.customs_sections.dto import CustomsSectionDTO
from api.v1.modules.public.reference_data.code_pedimento_regimens.dto import CodePedimentoRegimenDTO
from api.v1.modules.public.reference_data.transport_types.dto import TransportTypeDTO
from api.v1.modules.public.reference_data.transport_modes.dto import TransportModeDTO
from api.v1.modules.a76.customs_brokers.dto import CustomsBrokerResponseDTO
from api.v1.modules.a76.clients_and_providers.dto import ClientProviderResponseDTO
from .dtos.pedimentos import PedimentosResponse
@@ -53,6 +57,20 @@ class PedimentoCatalogService:
except Exception as e:
print(f"Error fetching code_pedimento_regimens: {e}")
try:
response.transport_types = [
TransportTypeDTO.model_validate(obj) for obj in db.query(TransportType).limit(200).all()
]
except Exception as e:
print(f"Error fetching transport_types: {e}")
try:
response.transport_modes = [
TransportModeDTO.model_validate(obj) for obj in db.query(TransportMode).limit(100).all()
]
except Exception as e:
print(f"Error fetching transport_modes: {e}")
# Helper to fetch tenant/company specific data
def fetch_tenant_data():
# Customs Brokers

View File

@@ -11,6 +11,8 @@ from api.v1.modules.public.reference_data.customs_sections.dto import CustomsSec
from api.v1.modules.public.reference_data.code_pedimento_regimens.dto import CodePedimentoRegimenDTO
from api.v1.modules.a76.customs_brokers.dto import CustomsBrokerResponseDTO
from api.v1.modules.a76.clients_and_providers.dto import ClientProviderResponseDTO
from api.v1.modules.public.reference_data.transport_types.dto import TransportTypeDTO
from api.v1.modules.public.reference_data.transport_modes.dto import TransportModeDTO
from .dtos.pedimentos import PedimentosResponse
@@ -22,6 +24,8 @@ class PedimentoCatalogsResponse(BaseModel):
code_pedimento_regimens: List[CodePedimentoRegimenDTO] = []
customs_brokers: List[CustomsBrokerResponseDTO] = []
clients: List[ClientProviderResponseDTO] = []
transport_types: List[TransportTypeDTO] = []
transport_modes: List[TransportModeDTO] = []
class PedimentoCreationResponse(PedimentoCatalogsResponse):

View File

@@ -25,6 +25,10 @@ class TrailerService:
# Apply filters if provided
if filters:
if filters.get("trailer_number"):
query = query.filter(
models.Trailer.trailer_number.ilike(f"%{filters['trailer_number']}%")
)
if filters.get("plate_number"):
query = query.filter(
models.Trailer.plate_number.ilike(f"%{filters['plate_number']}%")
@@ -75,8 +79,8 @@ class TrailerService:
db: Session,
trailer_number: str,
tenant_id: int,
company_id: int,
trailer_data: dto.TrailerUpdateDTO,
company_id: int,
) -> Optional[models.Trailer]:
"""Update a trailer"""
trailer = TrailerService.get_by_id(db, trailer_number, tenant_id, company_id)

View File

@@ -9,7 +9,7 @@ class Transporter(Base, TenantScopedMixin, TimestampMixin):
{"schema": "a76"},
)
transporter_key = Column(String(5), primary_key=True, nullable=False)
transporter_key = Column(String(23), primary_key=True, nullable=False)
name = Column(String(256), nullable=True)
short_name = Column(String(10), nullable=True)
responsible = Column(String(100), nullable=True)
@@ -27,4 +27,4 @@ class Transporter(Base, TenantScopedMixin, TimestampMixin):
ftp_user = Column(String(200), nullable=True)
ftp_password = Column(String(100), nullable=True)
ftp_directory = Column(String(1000), nullable=True)
filler_code = Column(String(4), nullable=True)
filler_code = Column(String(20), nullable=True)

View File

@@ -25,6 +25,10 @@ class TransporterService:
# Apply filters if provided
if filters:
if filters.get("transporter_key"):
query = query.filter(
models.Transporter.transporter_key.ilike(f"%{filters['transporter_key']}%")
)
if filters.get("name"):
query = query.filter(
models.Transporter.name.ilike(f"%{filters['name']}%")
@@ -75,8 +79,8 @@ class TransporterService:
db: Session,
transporter_key: str,
tenant_id: int,
company_id: int,
transporter_data: dto.TransporterUpdateDTO,
company_id: int,
) -> Optional[models.Transporter]:
"""Update a transporter"""
transporter = TransporterService.get_by_id(

View File

@@ -25,6 +25,10 @@ class VehicleService:
# Apply filters if provided
if filters:
if filters.get("vehicle_key"):
query = query.filter(
models.Vehicle.vehicle_key.ilike(f"%{filters['vehicle_key']}%")
)
if filters.get("plate_number"):
query = query.filter(
models.Vehicle.plate_number.ilike(f"%{filters['plate_number']}%")
@@ -75,8 +79,8 @@ class VehicleService:
db: Session,
vehicle_key: str,
tenant_id: int,
company_id: int,
vehicle_data: dto.VehicleUpdateDTO,
company_id: int,
) -> Optional[models.Vehicle]:
"""Update a vehicle"""
vehicle = VehicleService.get_by_id(db, vehicle_key, tenant_id, company_id)

View File

@@ -58,6 +58,11 @@ from api.v1.modules.a76.clients_and_providers.models import ClientProvider
from api.v1.modules.a76.customs_brokers.models import CustomsBroker
from api.v1.modules.a76.general_catalogs.company.models import Company
# Transportation Modules
from api.v1.modules.a76.transportation.trailers.models import Trailer
from api.v1.modules.a76.transportation.transporters.models import Transporter
from api.v1.modules.a76.transportation.vehicles.models import Vehicle
# Core Modules & Transactional Models
from api.v1.modules.a76.items.models import LineItem
from api.v1.modules.a76.items.series.models import Serie
@@ -146,6 +151,137 @@ def run_migrations():
subprocess.run(["alembic", "upgrade", "head"], check=True)
def create_transportation_tables():
"""Crea las tablas de transporte directamente si no existen.
Se usa en lugar de una migración Alembic para evitar gestionar versiones.
"""
from sqlalchemy import text
from core.database import core_engine
ddl_statements = [
"""
CREATE TABLE IF NOT EXISTS a76.transporter (
transporter_key VARCHAR(23) PRIMARY KEY,
name VARCHAR(256),
short_name VARCHAR(10),
responsible VARCHAR(100),
rfc VARCHAR(30),
streets VARCHAR(100),
postal_code VARCHAR(15),
city VARCHAR(30),
state VARCHAR(30),
country VARCHAR(3),
loader_code VARCHAR(9),
caat_code VARCHAR(49),
transport_code VARCHAR(8),
transport_interface_type VARCHAR(20),
ftp_server VARCHAR(200),
ftp_user VARCHAR(200),
ftp_password VARCHAR(100),
ftp_directory VARCHAR(1000),
filler_code VARCHAR(20),
tenant_id INTEGER NOT NULL REFERENCES core.tenants(id),
company_id INTEGER NOT NULL REFERENCES a76.company(id),
created_at TIMESTAMP NOT NULL DEFAULT now(),
updated_at TIMESTAMP NOT NULL DEFAULT now(),
deleted_at TIMESTAMP
);
""",
"""
CREATE TABLE IF NOT EXISTS a76.trailer (
trailer_number VARCHAR(20) PRIMARY KEY,
ace_trailer_number VARCHAR(10),
trailer_type_key VARCHAR(2),
seal VARCHAR(15),
entity_code VARCHAR(1),
plate_number VARCHAR(17),
state VARCHAR(30),
country VARCHAR(3),
container_key VARCHAR(3),
tenant_id INTEGER NOT NULL REFERENCES core.tenants(id),
company_id INTEGER NOT NULL REFERENCES a76.company(id),
created_at TIMESTAMP NOT NULL DEFAULT now(),
updated_at TIMESTAMP NOT NULL DEFAULT now(),
deleted_at TIMESTAMP
);
""",
"""
CREATE TABLE IF NOT EXISTS a76.vehicle (
vehicle_key VARCHAR(14) PRIMARY KEY,
ace_vehicle_key VARCHAR(10),
transporter_key VARCHAR(23),
transport_identifier VARCHAR(30),
transport_type VARCHAR(2),
entity_code VARCHAR(1),
transponder_number VARCHAR(16),
dot_number VARCHAR(8),
plate_number VARCHAR(17),
city VARCHAR(30),
state VARCHAR(30),
country VARCHAR(3),
seal VARCHAR(49),
insurance_company_name VARCHAR(30),
insurance_number VARCHAR(20),
insurance_amount NUMERIC(13, 2),
insurance_date INTEGER,
box_number VARCHAR(300),
brand VARCHAR(20),
year VARCHAR(4),
series VARCHAR(30),
description VARCHAR(100),
engine_number VARCHAR(50),
sct_permission VARCHAR(40),
color VARCHAR(20),
container_key VARCHAR(3),
tenant_id INTEGER NOT NULL REFERENCES core.tenants(id),
company_id INTEGER NOT NULL REFERENCES a76.company(id),
created_at TIMESTAMP NOT NULL DEFAULT now(),
updated_at TIMESTAMP NOT NULL DEFAULT now(),
deleted_at TIMESTAMP
);
""",
]
with core_engine.connect() as conn:
for stmt in ddl_statements:
conn.execute(text(stmt))
# Ampliar columnas que pudieron haberse creado con tamaño incorrecto
conn.execute(text("""
DO $$
BEGIN
-- Fix transporter_key si fue creada como VARCHAR(5)
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema='a76' AND table_name='transporter'
AND column_name='transporter_key'
AND character_maximum_length < 23
) THEN
ALTER TABLE a76.transporter ALTER COLUMN transporter_key TYPE VARCHAR(23);
END IF;
-- Fix filler_code si fue creada como VARCHAR(4)
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema='a76' AND table_name='transporter'
AND column_name='filler_code'
AND character_maximum_length < 20
) THEN
ALTER TABLE a76.transporter ALTER COLUMN filler_code TYPE VARCHAR(20);
END IF;
-- Eliminar constraint de trailer_type_key en trailer si existe
IF EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name='trailer_trailer_type_key_fkey'
AND table_schema='a76' AND table_name='trailer'
) THEN
ALTER TABLE a76.trailer DROP CONSTRAINT trailer_trailer_type_key_fkey;
END IF;
END$$;
"""))
conn.commit()
logger.info("Tablas de transporte verificadas/creadas correctamente.")
# Inicializar la base de datos
@app.on_event("startup")
async def on_startup():
@@ -153,6 +289,7 @@ async def on_startup():
logger.info("Iniciando la aplicación Anexo76...")
init_db()
run_migrations()
create_transportation_tables()
logger.info("Base de datos inicializada correctamente.")
@@ -266,6 +403,10 @@ def register_audit():
CustomsBroker,
Part,
Company,
# Transportation Modules
Trailer,
Transporter,
Vehicle,
# Reference Data
Country,
CurrencyType,