Files
plantillas-proyectos/backend/api/v1/modules/a76/pedmientos/services/pedimentos.py

644 lines
29 KiB
Python

"""
Service layer for Pedimentos CRUD operations
"""
import logging
import re
from typing import Any, Dict, List, Optional
from sqlalchemy import desc, func
from sqlalchemy.orm import Session, joinedload
from sqlalchemy.orm import selectinload
from sqlalchemy.exc import IntegrityError
from datetime import datetime
from api.v1.modules.a76.audit_log.models import AuditLog
from api.v1.modules.a76.audit_log.services.service import AuditService
from core.context import get_user_context
from ..dtos.pedimentos import PedimentosCreate, PedimentosUpdate
from .pedimento_config_additional import PedimentoConfigAdditionalService
from .pedimento_config_calculations import PedimentoConfigCalculationsService
from .pedimento_config_parameters import PedimentoConfigParametersService
from .pedimento_config_surcharges import PedimentoConfigSurchargesService
from .pedimento_config_update_rectification import PedimentoConfigUpdateRectificationService
from .pedimento_config_updates import PedimentoConfigUpdatesService
from .pedimento_customs_offices import PedimentoCustomsOfficesService
from .pedimento_dates import PedimentoDatesService
from .pedimento_decrementables import PedimentoDecrementablesService
from .pedimento_incrementables import PedimentoIncrementablesService
from .pedimento_indexes import PedimentoIndexesService
from .pedimento_payments import PedimentoPaymentsService
from .pedimento_rectification_destination import PedimentoRectificationDestinationService
from .pedimento_rectification_origin import PedimentoRectificationOriginService
from .pedimento_transport_means import PedimentoTransportMeansService
from .pedimento_validation import PedimentoValidationService
# Crear tablas relacionadas si existen datos
from ..models.pedimentos import Pedimentos
from ..models.pedimento_dates import PedimentoDates
from ..models.pedimento_decrementables import PedimentoDecrementables
from ..models.pedimento_incrementables import PedimentoIncrementables
from ..models.pedimento_indexes import PedimentoIndexes
from ..models.pedimento_validation import PedimentoValidation
from ..models.pedimento_customs_offices import PedimentoCustomsOffices
from ..models.pedimento_payments import PedimentoPayments
from ..models.pedimento_rectification_destination import PedimentoRectificationDestination
from ..models.pedimento_rectification_origin import PedimentoRectificationOrigin
from ..models.pedimento_transport_means import PedimentoTransportMeans
from ..models.pedimento_config_additional import PedimentoConfigAdditional
from ..models.pedimento_config_calculations import PedimentoConfigCalculations
from ..models.pedimento_config_parameters import PedimentoConfigParameters
from ..models.pedimento_config_surcharges import PedimentoConfigSurcharges
from ..models.pedimento_config_update_rectification import PedimentoConfigUpdateRectification
from ..models.pedimento_config_updates import PedimentoConfigUpdates
from ..models.pedimento_packages import PedimentoPackages
from ..models.pedimento_transport_carriers import PedimentoTransportCarriers
from ..models.pedimento_seals import PedimentoSeals
from ..models.pedimento_containers import PedimentoContainers
from ..models.pedimento_guides import PedimentoGuides
from ..models.pedimento_contributions import PedimentoContributions
logger = logging.getLogger(__name__)
def _ensure_create_audit_log(
db: Session, *, table_name: str, record_id: str, record_data: Dict[str, Any], username: str, tenant_id: int, company_id: int
) -> None:
"""
Backup audit writer: inserts CREATE log only when listener did not.
"""
exists = (
db.query(AuditLog.spec_id)
.filter(
AuditLog.table_name == table_name,
AuditLog.operation_type == "CREATE",
AuditLog.record_id == record_id,
AuditLog.tenant_id == tenant_id,
AuditLog.company_id == company_id,
)
.first()
)
if exists:
return
AuditService.log_crud_operation(
db=db,
table_name=table_name,
operation_type="CREATE",
record_data=record_data,
username=username,
record_id=record_id,
company_id=company_id,
tenant_id=tenant_id,
)
db.commit()
def _get_current_username() -> str:
try:
context = get_user_context()
if context:
return (
context.get("preferred_username")
or context.get("email")
or context.get("sub")
or "System"
)
except Exception:
pass
return "System"
class PedimentosService:
"""Service class for Pedimentos business logic"""
@staticmethod
def get_all(
db: Session,
tenant_id: int,
company_id: Optional[int],
skip: int = 0,
limit: int = 100,
filters: Optional[Dict[str, Any]] = None,
sort_by: Optional[str] = None,
sort_order: Optional[str] = "asc",
) -> tuple[List[Pedimentos], int]:
"""
Get all pedimentos for a tenant with pagination and filters
Args:
db: Database session
tenant_id: Tenant ID
company_id: Optional Company ID
skip: Number of records to skip
limit: Maximum number of records to return
filters: Optional filters dict
Returns:
Tuple of (list of pedimentos, total count)
"""
query = db.query(Pedimentos).filter(Pedimentos.tenant_id == tenant_id)
if company_id is not None:
query = query.filter(Pedimentos.company_id == company_id)
if filters:
if filters.get("status"):
query = query.filter(Pedimentos.status == filters["status"])
if filters.get("client_id"):
query = query.filter(Pedimentos.client_id == filters["client_id"])
if filters.get("year"):
query = query.filter(Pedimentos.year == filters["year"])
if filters.get("pedimento"):
raw_value = str(filters["pedimento"])
# Normalizar: dejar solo dígitos (ignorar guiones, espacios, etc.)
normalized = re.sub(r"\D", "", raw_value)
if normalized:
ped_key_expr = func.concat(
func.coalesce(Pedimentos.year, ""),
func.coalesce(func.substr(Pedimentos.customs_office, 1, 2), ""),
func.coalesce(Pedimentos.license, ""),
func.coalesce(Pedimentos.pedimento_number, ""),
)
query = query.filter(ped_key_expr.ilike(f"%{normalized}%"))
total = query.count()
# Eager load all relationships for the response schema
items = (
query.options(
selectinload(Pedimentos.pedimento_dates),
selectinload(Pedimentos.pedimento_decrementables),
selectinload(Pedimentos.pedimento_incrementables),
selectinload(Pedimentos.pedimento_indexes),
selectinload(Pedimentos.pedimento_validation),
selectinload(Pedimentos.pedimento_customs_offices),
selectinload(Pedimentos.pedimento_payments),
selectinload(Pedimentos.pedimento_rectification_destination),
selectinload(Pedimentos.pedimento_rectification_origin),
selectinload(Pedimentos.pedimento_transport_means),
selectinload(Pedimentos.pedimento_config_additional),
selectinload(Pedimentos.pedimento_config_calculations),
selectinload(Pedimentos.pedimento_config_parameters),
selectinload(Pedimentos.pedimento_config_surcharges),
selectinload(Pedimentos.pedimento_config_update_rectification),
selectinload(Pedimentos.pedimento_config_updates),
selectinload(Pedimentos.pedimento_packages),
selectinload(Pedimentos.pedimento_transport_carriers),
selectinload(Pedimentos.pedimento_guides),
selectinload(Pedimentos.pedimento_contributions),
selectinload(Pedimentos.pedimento_seals),
selectinload(Pedimentos.pedimento_containers),
)
)
# Apply sorting
if sort_by:
column = getattr(Pedimentos, sort_by, None)
if column:
if sort_order == "desc":
query = query.order_by(column.desc())
else:
query = query.order_by(column.asc())
else:
# Default sorting
query = query.order_by(desc(Pedimentos.created_at))
items = query.offset(skip).limit(limit).all()
return items, total
@staticmethod
def get_by_id(
db: Session, pedimento_id: int, tenant_id: int, company_id: int = None
) -> Optional[Pedimentos]:
"""
Get a pedimento by ID
Args:
db: Database session
pedimento_id: Pedimento ID
tenant_id: Tenant ID
company_id: Company ID (optional for backwards compatibility)
Returns:
Pedimento or None if not found
"""
query = db.query(Pedimentos).filter(
Pedimentos.id == pedimento_id, Pedimentos.tenant_id == tenant_id
)
if company_id is not None:
query = query.filter(Pedimentos.company_id == company_id)
# Eager load all relationships for the response schema
query = query.options(
selectinload(Pedimentos.pedimento_dates),
selectinload(Pedimentos.pedimento_decrementables),
selectinload(Pedimentos.pedimento_incrementables),
selectinload(Pedimentos.pedimento_indexes),
selectinload(Pedimentos.pedimento_validation),
selectinload(Pedimentos.pedimento_customs_offices),
selectinload(Pedimentos.pedimento_payments),
selectinload(Pedimentos.pedimento_rectification_destination),
selectinload(Pedimentos.pedimento_rectification_origin),
selectinload(Pedimentos.pedimento_transport_means),
selectinload(Pedimentos.pedimento_config_additional),
selectinload(Pedimentos.pedimento_config_calculations),
selectinload(Pedimentos.pedimento_config_parameters),
selectinload(Pedimentos.pedimento_config_surcharges),
selectinload(Pedimentos.pedimento_config_update_rectification),
selectinload(Pedimentos.pedimento_config_updates),
selectinload(Pedimentos.pedimento_packages),
selectinload(Pedimentos.pedimento_transport_carriers),
selectinload(Pedimentos.pedimento_guides),
selectinload(Pedimentos.pedimento_contributions),
selectinload(Pedimentos.pedimento_seals),
selectinload(Pedimentos.pedimento_containers),
)
return query.first()
@staticmethod
def create(
db: Session, pedimento_data: PedimentosCreate, tenant_id: int, company_id: int
) -> Pedimentos:
"""
Create a new pedimento with related tables
Args:
db: Database session
pedimento_data: Pedimento creation data
tenant_id: Tenant ID
company_id: Company ID
Returns:
Created pedimento
"""
try:
# Check for existing pedimento with same key (Year, Aduana, Patente, Number)
# This avoids IntegrityError in many cases and provides a better error message.
existing = db.query(Pedimentos).filter(
Pedimentos.tenant_id == tenant_id,
Pedimentos.company_id == company_id,
Pedimentos.year == pedimento_data.year,
Pedimentos.customs_office == pedimento_data.customs_office,
Pedimentos.license == pedimento_data.license,
Pedimentos.pedimento_number == pedimento_data.pedimento_number,
Pedimentos.deleted_at.is_(None)
).first()
if existing:
raise ValueError(
f"Ya existe un pedimento con estos datos: {pedimento_data.year}-{pedimento_data.customs_office}-{pedimento_data.license}-{pedimento_data.pedimento_number}"
)
# Extraer datos de tablas relacionadas
# Extraer datos de tablas relacionadas
related_data = {
'pedimento_dates': pedimento_data.pedimento_dates,
'pedimento_decrementables': pedimento_data.pedimento_decrementables,
'pedimento_incrementables': pedimento_data.pedimento_incrementables,
'pedimento_indexes': pedimento_data.pedimento_indexes,
'pedimento_validation': pedimento_data.pedimento_validation,
'pedimento_customs_offices': pedimento_data.pedimento_customs_offices,
'pedimento_payments': pedimento_data.pedimento_payments,
'pedimento_rectification_destination': pedimento_data.pedimento_rectification_destination,
'pedimento_rectification_origin': pedimento_data.pedimento_rectification_origin,
'pedimento_transport_means': pedimento_data.pedimento_transport_means,
'pedimento_config_additional': pedimento_data.pedimento_config_additional,
'pedimento_config_calculations': pedimento_data.pedimento_config_calculations,
'pedimento_config_parameters': pedimento_data.pedimento_config_parameters,
'pedimento_config_surcharges': pedimento_data.pedimento_config_surcharges,
'pedimento_config_update_rectification': pedimento_data.pedimento_config_update_rectification,
'pedimento_config_updates': pedimento_data.pedimento_config_updates,
'pedimento_packages': pedimento_data.pedimento_packages,
'pedimento_transport_carriers': pedimento_data.pedimento_transport_carriers,
'pedimento_guides': pedimento_data.pedimento_guides,
'pedimento_contributions': getattr(pedimento_data, 'pedimento_contributions', None),
'pedimento_seals': pedimento_data.pedimento_seals,
'pedimento_containers': pedimento_data.pedimento_containers,
}
# Crear pedimento principal (excluyendo relaciones)
pedimento_dict = pedimento_data.model_dump(exclude={
'pedimento_dates', 'pedimento_decrementables', 'pedimento_incrementables',
'pedimento_indexes', 'pedimento_validation', 'pedimento_customs_offices',
'pedimento_payments', 'pedimento_rectification_destination',
'pedimento_rectification_origin', 'pedimento_transport_means',
'pedimento_config_additional', 'pedimento_config_calculations',
'pedimento_config_parameters', 'pedimento_config_surcharges',
'pedimento_config_update_rectification', 'pedimento_config_updates',
'pedimento_packages', 'pedimento_transport_carriers', 'pedimento_guides',
'pedimento_contributions', 'pedimento_seals', 'pedimento_containers'
})
pedimento = Pedimentos(**pedimento_dict)
pedimento.tenant_id = tenant_id
pedimento.company_id = company_id
db.add(pedimento)
db.flush() # Flush para obtener el ID sin commit
# Helper function para crear objetos relacionados (uno a uno)
def create_related(model_class, data, extra_fields=None):
if data or extra_fields:
# Inicializar obj_dict desde data si existe, sino como dict vacío
obj_dict = data.model_dump(exclude_none=True) if data else {}
# Agregar campos extra si se proporcionan
if extra_fields:
obj_dict.update(extra_fields)
# Solo crear si hay datos significativos (más que solo IDs)
significant_fields = {k: v for k, v in obj_dict.items()
if k not in ('pedimento_id', 'tenant_id', 'company_id') and v is not None}
if not significant_fields and not extra_fields:
return
obj = model_class(**obj_dict)
obj.pedimento_id = pedimento.id
obj.tenant_id = tenant_id
obj.company_id = company_id
db.add(obj)
# Crear PedimentoDates con capture_time automático
create_related(
PedimentoDates,
related_data['pedimento_dates'],
extra_fields={'capture_time': datetime.now().time()}
)
create_related(PedimentoDecrementables,
related_data['pedimento_decrementables'])
create_related(PedimentoIncrementables,
related_data['pedimento_incrementables'])
create_related(PedimentoIndexes, related_data['pedimento_indexes'])
create_related(PedimentoValidation,
related_data['pedimento_validation'])
create_related(PedimentoCustomsOffices,
related_data['pedimento_customs_offices'])
create_related(PedimentoPayments,
related_data['pedimento_payments'])
create_related(PedimentoRectificationDestination,
related_data['pedimento_rectification_destination'])
create_related(PedimentoRectificationOrigin,
related_data['pedimento_rectification_origin'])
create_related(PedimentoTransportMeans,
related_data['pedimento_transport_means'])
create_related(PedimentoConfigAdditional,
related_data['pedimento_config_additional'])
create_related(PedimentoConfigCalculations,
related_data['pedimento_config_calculations'])
create_related(PedimentoConfigParameters,
related_data['pedimento_config_parameters'])
create_related(PedimentoConfigSurcharges,
related_data['pedimento_config_surcharges'])
create_related(PedimentoConfigUpdateRectification,
related_data['pedimento_config_update_rectification'])
create_related(PedimentoConfigUpdates,
related_data['pedimento_config_updates'])
# Crear PedimentoPackages (uno a uno)
create_related(PedimentoPackages, related_data['pedimento_packages'])
# Crear relaciones uno a muchos
def create_many(model_class, items):
if not items:
return
for item in items:
obj_dict = item.model_dump(exclude_none=True)
obj = model_class(**obj_dict)
obj.pedimento_id = pedimento.id
obj.tenant_id = tenant_id
obj.company_id = company_id
db.add(obj)
create_many(PedimentoTransportCarriers,
related_data['pedimento_transport_carriers'])
create_many(PedimentoGuides, related_data['pedimento_guides'])
create_many(PedimentoContributions, related_data['pedimento_contributions'])
create_many(PedimentoSeals, related_data['pedimento_seals'])
create_many(PedimentoContainers,
related_data['pedimento_containers'])
db.commit()
db.refresh(pedimento)
_ensure_create_audit_log(
db,
table_name=pedimento.__tablename__,
record_id=str(pedimento.id),
record_data={c.name: getattr(pedimento, c.name) for c in Pedimentos.__table__.columns},
username=_get_current_username(),
tenant_id=tenant_id,
company_id=company_id,
)
return pedimento
except IntegrityError as e:
db.rollback()
# Detectar si es un error de integridad de duplicados o similar
error_msg = str(e.orig).lower()
# Case-insensitive check and support for both Spanish and English common error patterns
is_unique_violation = any(kw in error_msg for kw in [
'pedimentos_unique_key',
'unique constraint',
'duplicate key',
'duplicada',
'unicidad',
'ya existe'
])
if is_unique_violation:
logger.warning(f"Attempted to create duplicate pedimento or common record: {e}")
raise ValueError("Ya existe un pedimento o registro relacionado con estos datos. Verifica los campos únicos.")
logger.error(f"Integrity error creating pedimento: {e}")
raise
except Exception as e:
db.rollback()
logger.error(f"Error creating pedimento with related data: {e}")
raise
@staticmethod
def update(
db: Session, pedimento_id: int, tenant_id: int, pedimento_data: PedimentosUpdate, company_id: int = None
) -> Optional[Pedimentos]:
"""
Update a pedimento and its related tables
Args:
db: Database session
pedimento_id: Pedimento ID
tenant_id: Tenant ID
pedimento_data: Updated data
company_id: Company ID (optional for backwards compatibility)
Returns:
Updated pedimento or None if not found
"""
pedimento = PedimentosService.get_by_id(
db, pedimento_id, tenant_id, company_id)
if not pedimento:
return None
# Ensure company_id is set from the existing record
company_id = pedimento.company_id
try:
# Actualizar campos principales del pedimento
update_data = pedimento_data.model_dump(exclude_unset=True, exclude={
'pedimento_dates', 'pedimento_decrementables', 'pedimento_incrementables',
'pedimento_indexes', 'pedimento_validation', 'pedimento_customs_offices',
'pedimento_payments', 'pedimento_rectification_destination',
'pedimento_rectification_origin', 'pedimento_transport_means',
'pedimento_config_additional', 'pedimento_config_calculations',
'pedimento_config_parameters', 'pedimento_config_surcharges',
'pedimento_config_update_rectification', 'pedimento_config_updates',
'pedimento_packages', 'pedimento_transport_carriers', 'pedimento_guides', 'pedimento_contributions', 'pedimento_seals', 'pedimento_containers'
})
for field, value in update_data.items():
setattr(pedimento, field, value)
db.flush()
# Helper function para actualizar o crear objetos relacionados
def update_or_create_related(service_class, model_class, data_attr):
# Obtener datos del payload completo (no solo exclude_unset)
full_data = pedimento_data.model_dump()
if data_attr not in full_data:
return
data = full_data[data_attr]
if not data:
return
existing = None
if service_class:
existing = service_class.get_by_pedimento_id(
db, pedimento_id, tenant_id, company_id)
else:
existing = getattr(pedimento, data_attr, None)
if existing:
# Actualizar existente (excluir pedimento_id, tenant_id, company_id)
# Solo actualizar valores no-None para evitar sobrescribir con None
for field, value in data.items():
if hasattr(existing, field) and field not in ('pedimento_id', 'tenant_id', 'company_id'):
if value is not None:
setattr(existing, field, value)
else:
# Crear nuevo
obj = model_class(**data)
obj.pedimento_id = pedimento_id
obj.tenant_id = tenant_id
obj.company_id = company_id
db.add(obj)
def upsert_one_to_many(model_class, data_attr):
full_data = pedimento_data.model_dump()
if data_attr not in full_data:
return
items = full_data[data_attr]
if items is None:
return
# Reemplazar completamente la colección por simplicidad
existing_items = getattr(pedimento, data_attr)
if existing_items:
for item in list(existing_items):
db.delete(item)
for item in items:
obj = model_class(**item)
obj.pedimento_id = pedimento_id
obj.tenant_id = tenant_id
obj.company_id = company_id
db.add(obj)
# Actualizar o crear tablas relacionadas
update_or_create_related(
PedimentoDatesService, PedimentoDates, 'pedimento_dates')
update_or_create_related(
PedimentoDecrementablesService, PedimentoDecrementables, 'pedimento_decrementables')
update_or_create_related(
PedimentoIncrementablesService, PedimentoIncrementables, 'pedimento_incrementables')
update_or_create_related(
PedimentoIndexesService, PedimentoIndexes, 'pedimento_indexes')
update_or_create_related(
PedimentoValidationService, PedimentoValidation, 'pedimento_validation')
update_or_create_related(
PedimentoCustomsOfficesService, PedimentoCustomsOffices, 'pedimento_customs_offices')
update_or_create_related(
PedimentoPaymentsService, PedimentoPayments, 'pedimento_payments')
update_or_create_related(PedimentoRectificationDestinationService,
PedimentoRectificationDestination, 'pedimento_rectification_destination')
update_or_create_related(PedimentoRectificationOriginService,
PedimentoRectificationOrigin, 'pedimento_rectification_origin')
update_or_create_related(
PedimentoTransportMeansService, PedimentoTransportMeans, 'pedimento_transport_means')
update_or_create_related(PedimentoConfigAdditionalService,
PedimentoConfigAdditional, 'pedimento_config_additional')
update_or_create_related(PedimentoConfigCalculationsService,
PedimentoConfigCalculations, 'pedimento_config_calculations')
update_or_create_related(PedimentoConfigParametersService,
PedimentoConfigParameters, 'pedimento_config_parameters')
update_or_create_related(PedimentoConfigSurchargesService,
PedimentoConfigSurcharges, 'pedimento_config_surcharges')
update_or_create_related(PedimentoConfigUpdateRectificationService,
PedimentoConfigUpdateRectification, 'pedimento_config_update_rectification')
update_or_create_related(
PedimentoConfigUpdatesService, PedimentoConfigUpdates, 'pedimento_config_updates')
# Manejar nuevas relaciones
update_or_create_related(None, PedimentoPackages, 'pedimento_packages')
upsert_one_to_many(PedimentoTransportCarriers,
'pedimento_transport_carriers')
upsert_one_to_many(PedimentoGuides, 'pedimento_guides')
upsert_one_to_many(PedimentoContributions, 'pedimento_contributions')
upsert_one_to_many(PedimentoSeals, 'pedimento_seals')
upsert_one_to_many(PedimentoContainers, 'pedimento_containers')
db.commit()
db.refresh(pedimento)
return pedimento
except IntegrityError as e:
db.rollback()
# Detectar si es un error de pedimento duplicado
error_msg = str(e.orig)
if 'pedimentos_unique_key' in error_msg or 'duplicate key value violates unique constraint' in error_msg:
logger.warning(f"Attempted to update to duplicate pedimento: {e}")
raise ValueError("Ya existe otro pedimento con estos datos (Año, Aduana, Patente, Número)")
logger.error(f"Integrity error updating pedimento: {e}")
raise
except Exception as e:
db.rollback()
logger.error(f"Error updating pedimento with related data: {e}")
raise
@staticmethod
def delete(db: Session, pedimento_id: int, tenant_id: int, company_id: int = None) -> bool:
"""
Delete a pedimento
Args:
db: Database session
pedimento_id: Pedimento ID
tenant_id: Tenant ID
company_id: Company ID (optional for backwards compatibility)
Returns:
True if deleted, False if not found
"""
pedimento = PedimentosService.get_by_id(
db, pedimento_id, tenant_id, company_id)
if not pedimento:
return False
db.delete(pedimento)
db.commit()
return True