Merge branch 'feature/invoices' into development

This commit is contained in:
AlexeerCT
2026-01-08 16:57:13 -06:00
52 changed files with 2756 additions and 1388 deletions

View File

@@ -5,6 +5,7 @@ from pydantic import BaseModel
class CustomsBrokerBaseDTO(BaseModel):
"""Base fields for CustomsBroker"""
type: Optional[str] = None
name: Optional[str] = None
address: Optional[str] = None
@@ -25,16 +26,20 @@ class CustomsBrokerBaseDTO(BaseModel):
class CustomsBrokerCreateDTO(CustomsBrokerBaseDTO):
"""Schema for creating a new CustomsBroker"""
broker_key: str
class CustomsBrokerUpdateDTO(CustomsBrokerBaseDTO):
"""Schema for updating an existing CustomsBroker"""
pass
class CustomsBrokerResponseDTO(CustomsBrokerBaseDTO):
"""Schema for CustomsBroker response"""
id: int
broker_key: str
tenant_id: int
company_id: int

View File

@@ -1,10 +1,18 @@
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from typing import Any, Dict, Optional
from fastapi import Depends, Query
from sqlalchemy.orm import Session
from api.v1.common.tenant_crud_routes import (
TenantCRUDRoutes,
validate_access_to_resource,
get_core_db,
get_current_user,
)
from .dto import ExchangeRateCreateDTO, ExchangeRateResponseDTO, ExchangeRateUpdateDTO
from .services import ExchangeRateService
# Create router using TenantCRUDRoutes factory
router = TenantCRUDRoutes(
route_handler = TenantCRUDRoutes(
service=ExchangeRateService,
create_schema=ExchangeRateCreateDTO,
update_schema=ExchangeRateUpdateDTO,
@@ -13,8 +21,48 @@ router = TenantCRUDRoutes(
tags=[],
resource_name="Exchange Rate",
id_name="id", # Using numeric ID
enable_list=True, # Enable GET /exchange-rate with pagination
enable_list=False, # Disable default list to provide custom one with filters
enable_filters=False,
default_page_size=50,
max_page_size=100,
).router
)
router = route_handler.router
@router.get(
"/",
response_model=Dict[str, Any],
summary="List Exchange Rates",
description="Get paginated list of exchange rates with optional date filter",
)
async def list_exchange_rates(
company_id: int = Query(..., description="Company ID"),
date: Optional[str] = Query(None, description="Filter by date (YYYY-MM-DD)"),
page: int = Query(1, ge=1, description="Page number"),
page_size: int = Query(
50,
ge=1,
le=100,
description="Page size",
),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
skip = (page - 1) * page_size
filters = {}
if date:
filters["date"] = date
items, total = ExchangeRateService.get_all(
db, tenant_id, company_id, skip, page_size, filters
)
return {
"items": [ExchangeRateResponseDTO.model_validate(item) for item in items],
"total": total,
"page": page,
"page_size": page_size,
}

View File

@@ -1,6 +1,8 @@
from typing import Optional, Tuple, List, Dict, Any
from datetime import datetime, time
from sqlalchemy.orm import Session
from sqlalchemy import cast, Date
from . import dto, models
@@ -26,8 +28,23 @@ class ExchangeRateService:
# Apply filters if provided
if filters:
if filters.get("date"):
query = query.filter(
models.ExchangeRate.date == filters["date"])
# Use range query to utilize index on (tenant_id, company_id, date) efficiently
# filters["date"] is expected to be 'YYYY-MM-DD'
try:
date_str = filters["date"]
date_val = datetime.strptime(date_str, "%Y-%m-%d").date()
start_date = datetime.combine(date_val, time.min)
end_date = datetime.combine(date_val, time.max)
query = query.filter(
models.ExchangeRate.date >= start_date,
models.ExchangeRate.date <= end_date,
)
except (ValueError, TypeError):
# Fallback to cast if date format is invalid or logic fails, though validation should catch this
query = query.filter(
cast(models.ExchangeRate.date, Date) == filters["date"]
)
if filters.get("local_currency"):
query = query.filter(
models.ExchangeRate.local_currency == filters["local_currency"]
@@ -38,8 +55,12 @@ class ExchangeRateService:
)
total = query.count()
exchange_rates = query.order_by(
models.ExchangeRate.date.desc()).offset(skip).limit(limit).all()
exchange_rates = (
query.order_by(models.ExchangeRate.date.desc())
.offset(skip)
.limit(limit)
.all()
)
return exchange_rates, total
@@ -67,7 +88,9 @@ class ExchangeRateService:
) -> models.ExchangeRate:
"""Create a new exchange rate"""
new_exchange_rate = models.ExchangeRate(
**exchange_rate_data.model_dump(), tenant_id=tenant_id, company_id=company_id
**exchange_rate_data.model_dump(),
tenant_id=tenant_id,
company_id=company_id
)
db.add(new_exchange_rate)
db.commit()

View File

@@ -0,0 +1,28 @@
from core.exceptions import ErrorCollector
from .. import models
from sqlalchemy.orm import Session
def invoice_exists(
db: Session,
invoice_number: str,
tenant_id: int,
company_id: int,
errors: ErrorCollector
) -> bool:
invoice_exists = (
db.query(models.InvoiceHeader.id)
.filter(
models.InvoiceHeader.invoice_number == invoice_number,
models.InvoiceHeader.tenant_id == tenant_id,
models.InvoiceHeader.company_id == company_id,
)
.first()
)
if invoice_exists:
errors.add_duplicate_error(
"invoice_number",
invoice_number,
f"Ya existe una factura con el número '{invoice_number}'",
)

View File

@@ -0,0 +1,15 @@
""" """
def clean_dict(data_dict: dict) -> dict:
cleaned = {}
for key, value in data_dict.items():
if isinstance(value, str) and not value.strip():
cleaned[key] = None
elif value == 0 and (key.endswith('_id') or key == 'remesa'):
cleaned[key] = None
else:
cleaned[key] = value
return cleaned

View File

@@ -0,0 +1,393 @@
from sqlalchemy.orm import Session
from .... import schemas
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
from api.v1.modules.a76.general_catalogs.exchange_rate.models import ExchangeRate
from api.v1.modules.a76.clients_and_providers.models import ClientProvider
from api.v1.modules.public.reference_data.incoterms.models import Incoterm
from api.v1.modules.a76.items.models import Item
from ....models import TransportType, Currency, WeightUnit
from core.exceptions import ErrorCollector
def validate_common(
db: Session,
invoice: schemas.InvoiceHeaderCreate,
tenant_id: int,
company_id: int,
errors: ErrorCollector,
):
if invoice.compliance_mx.pedimento_id:
pedimento = (
db.query(Pedimentos)
.filter(
Pedimentos.id == invoice.compliance_mx.pedimento_id,
Pedimentos.tenant_id == tenant_id,
Pedimentos.company_id == company_id,
)
.first()
)
if not pedimento:
errors.add_error(
field="compliance_mx.pedimento_id",
message="El Pedimento no existe en el Catálogo de Pedimentos.",
solution=["Verifica el ID", "Revisa el catálogo"],
code="NOT_FOUND",
value=invoice.compliance_mx.pedimento_id,
)
if not invoice.compliance_mx.is_regime_change:
if not pedimento.operation_type == 1:
errors.add_error(
field="compliance_mx.pedimento_id",
message="El Pedimento seleccionado no corresponde a una Importación.",
solution=["Selecciona un Pedimento de Importación"],
code="INVALID_OPERATION_TYPE",
value=pedimento.operation_type,
)
else:
if pedimento.regime in ["EXD", "ETE", "ETR"]:
errors.add_error(
field="compliance_mx.pedimento_id",
message="El Pedimento seleccionado corresponde a una Exportación, no a una Importación.",
solution=["Selecciona un Pedimento de Importación"],
code="INVALID_REGIME",
value=pedimento.regime,
)
else:
if invoice.document_type.upper().strip() != pedimento.regime:
errors.add_error(
field="document_type",
message=f"El Tipo de Documento '{invoice.document_type}' no coincide con el Régimen '{pedimento.regime}' del Pedimento seleccionado.",
solution=[
"Ajusta el Tipo de Documento o selecciona otro Pedimento"
],
code="REGIME_MISMATCH",
value=invoice.document_type,
)
else:
if pedimento.operation_type != 2:
errors.add_error(
field="compliance_mx.pedimento_id",
message="El Pedimento seleccionado no corresponde a una Importacion Definitiva.",
solution=["Selecciona un Pedimento de Importacion Definitiva"],
code="INVALID_OPERATION_TYPE",
value=pedimento.operation_type,
)
else:
if pedimento.regime != "IMD":
errors.add_error(
field="compliance_mx.pedimento_id",
message=f"El Pedimento {pedimento.customs_office}-{pedimento.license}-{pedimento.pedimento_number} no corresponde a una Importacion Definitiva.",
solution=["Selecciona un Pedimento de Importacion Definitiva"],
code="INVALID_REGIME",
value=pedimento.regime,
)
else:
if invoice.document_type.upper().strip() != pedimento.regime:
errors.add_error(
field="document_type",
message=f"El Tipo de Documento '{invoice.document_type}' no coincide con el Régimen '{pedimento.regime}' del Pedimento seleccionado.",
solution=[
"Ajusta el Tipo de Documento o selecciona otro Pedimento"
],
code="REGIME_MISMATCH",
value=invoice.document_type,
)
else:
if pedimento.pedimento_code not in ["A1", "A3"]:
errors.add_error(
field="compliance_mx.pedimento_id",
message=f"El Pedimento seleccionado no es de tipo A1 o A3 requerido para Cambio de Régimen.",
solution=["Selecciona un Pedimento de tipo A1 o A3"],
code="INVALID_PEDEMENTO_CODE",
value=pedimento.pedimento_code,
)
if pedimento.pedimento_type == "consolidated":
if (
invoice.invoice_date < pedimento.pedimento_dates.entry_date
or invoice.invoice_date > pedimento.pedimento_dates.end_date
):
errors.add_error(
field="invoice_date",
message=f"La Fecha de la Factura {invoice.invoice_date} no está dentro del rango de fechas del Pedimento {pedimento.customs_office}-{pedimento.license}-{pedimento.pedimento_number}.",
solution=[
f"Capturar una Fecha de Factura, entre la Fecha de Inicio: {pedimento.pedimento_dates.entry_date} y la Fecha Final: {pedimento.pedimento_dates.end_date} ."
],
code="DATE_OUT_OF_RANGE",
value=invoice.invoice_date,
)
if not invoice.compliance_mx.remesa:
errors.add_error(
field="compliance_mx.remesa",
message="El campo Remesa es obligatorio cuando se asocia un Pedimento.",
solution=["Proporciona un valor para Remesa"],
code="REQUIRED_FIELD",
value=invoice.compliance_mx.remesa,
)
elif invoice.compliance_mx.remesa == 0:
errors.add_error(
field="compliance_mx.remesa",
message="El campo Remesa no puede ser cero cuando se asocia un Pedimento.",
solution=["Proporciona un valor válido para Remesa"],
code="INVALID_VALUE",
value=invoice.compliance_mx.remesa,
)
duplicated_remesa = (
db.query(Pedimentos)
.filter(
Pedimentos.remesa == invoice.compliance_mx.remesa,
Pedimentos.id != invoice.compliance_mx.pedimento_id,
Pedimentos.tenant_id == tenant_id,
Pedimentos.company_id == company_id,
)
.first()
)
if duplicated_remesa:
errors.add_error(
field="compliance_mx.remesa",
message="El valor de Remesa ya está asociado a otro Pedimento.",
solution=["Proporciona un valor único para Remesa"],
code="DUPLICATE_VALUE",
value=invoice.compliance_mx.remesa,
)
else:
if invoice.compliance_mx.remesa and not invoice.compliance_mx.pedimento_id:
errors.add_error(
field="compliance_mx.pedimento_id",
message="El campo Pedimento es obligatorio cuando se proporciona Remesa.",
solution=["Proporciona un ID de Pedimento"],
code="REQUIRED_FIELD",
value=invoice.compliance_mx.pedimento_id,
)
if len(invoice.invoice_number) > 100:
errors.add_error(
field="invoice_number",
message="El número de factura excede la longitud máxima de 100 caracteres.",
solution=["Acorta el número de factura a 100 caracteres o menos"],
code="MAX_LENGTH_EXCEEDED",
value=invoice.invoice_number,
)
if not invoice.financials.exchange_rate or invoice.financials.exchange_rate <= 0:
exchange_rate_exists = (
db.query(ExchangeRate)
.filter(
ExchangeRate.date == invoice.invoice_date,
ExchangeRate.tenant_id == tenant_id,
ExchangeRate.company_id == company_id,
)
.first()
)
if not exchange_rate_exists:
errors.add_error(
field="financials.exchange_rate",
message=f"No existe un Tipo de Cambio registrado para la fecha {invoice.invoice_date.date()}.",
solution=["Registra el Tipo de Cambio en el catálogo correspondiente"],
code="EXCHANGE_RATE_NOT_FOUND",
value=invoice.financials.exchange_rate,
)
if invoice.compliance_mx.is_regime_change:
if invoice.document_type in ["EXD", "ETE", "ETR"]:
errors.add_error(
field="document_type",
message="El Tipo de Documento no puede ser de Exportación cuando se trata de un Cambio de Régimen.",
solution=[
"Selecciona un Tipo de Documento válido para Cambio de Régimen"
],
code="INVALID_DOCUMENT_TYPE",
value=invoice.document_type,
)
else:
if invoice.document_type == "IMD":
errors.add_error(
field="document_type",
message="El Tipo de Documento no puede ser 'IMD' a menos que sea un Cambio de Régimen.",
solution=["Selecciona un Tipo de Documento válido"],
code="INVALID_DOCUMENT_TYPE",
value=invoice.document_type,
)
provider_exists = (
db.query(ClientProvider)
.filter(
ClientProvider.id == invoice.provider_id,
ClientProvider.tenant_id == tenant_id,
ClientProvider.company_id == company_id,
)
.first()
)
if not provider_exists:
errors.add_error(
field="provider_id",
message="El Proveedor no existe en el Catálogo de Clientes y Proveedores.",
solution=["Verifica el ID del Proveedor", "Revisa el catálogo"],
code="NOT_FOUND",
value=invoice.provider_id,
)
selled_to_exists = (
db.query(ClientProvider)
.filter(
ClientProvider.id == invoice.selled_to_id,
ClientProvider.tenant_id == tenant_id,
ClientProvider.company_id == company_id,
)
.first()
)
if not selled_to_exists:
errors.add_error(
field="selled_to_id",
message="El Cliente no existe en el Catálogo de Clientes y Proveedores.",
solution=["Verifica el ID del Cliente", "Revisa el catálogo"],
code="NOT_FOUND",
value=invoice.selled_to_id,
)
shipped_to_exists = (
db.query(ClientProvider)
.filter(
ClientProvider.id == invoice.shipped_to_id,
ClientProvider.tenant_id == tenant_id,
ClientProvider.company_id == company_id,
)
.first()
)
if not shipped_to_exists:
errors.add_error(
field="shipped_to_id",
message="El Destinatario no existe en el Catálogo de Clientes y Proveedores.",
solution=["Verifica el ID del Destinatario", "Revisa el catálogo"],
code="NOT_FOUND",
value=invoice.shipped_to_id,
)
customs_broker_exists = (
db.query(ClientProvider)
.filter(
ClientProvider.id == invoice.customs_broker_id,
ClientProvider.tenant_id == tenant_id,
ClientProvider.company_id == company_id,
)
.first()
)
if not customs_broker_exists:
errors.add_error(
field="customs_broker_id",
message="El Agente Aduanal no existe en el Catálogo de Clientes y Proveedores.",
solution=["Verifica el ID del Agente Aduanal", "Revisa el catálogo"],
code="NOT_FOUND",
value=invoice.customs_broker_id,
)
if invoice.logistics.carrier_id:
carrier_exists = (
db.query(ClientProvider)
.filter(
ClientProvider.id == invoice.logistics.carrier_id,
ClientProvider.tenant_id == tenant_id,
ClientProvider.company_id == company_id,
)
.first()
)
if not carrier_exists:
errors.add_error(
field="logistics.carrier_id",
message="El Transportista no existe en el Catálogo de Clientes y Proveedores.",
solution=["Verifica el ID del Transportista", "Revisa el catálogo"],
code="NOT_FOUND",
value=invoice.logistics.carrier_id,
)
if invoice.logistics.transport_type not in [t.value for t in TransportType]:
errors.add_error(
field="logistics.transport_type",
message="El Tipo de Transporte proporcionado no es válido.",
solution=[
f"Selecciona un Tipo de Transporte válido: {[t.value for t in TransportType]}"
],
code="INVALID_TRANSPORT_TYPE",
value=invoice.logistics.transport_type,
)
else:
if invoice.logistics.transport_type == "none" and invoice.logistics.transport_num:
errors.add_error(
field="logistics.transport_num",
message="El Número de Transporte no debe proporcionarse cuando el Tipo de Transporte es 'none'.",
solution=["Elimina el Número de Transporte o selecciona un Tipo de Transporte válido"],
code="INVALID_VALUE",
value=invoice.logistics.transport_num,
)
else:
if not invoice.logistics.transport_num and invoice.logistics.transport_type != "none":
errors.add_error(
field="logistics.transport_num",
message="El Número de Transporte es obligatorio cuando se proporciona un Tipo de Transporte distinto de 'none'.",
solution=["Proporciona un Número de Transporte válido"],
code="REQUIRED_FIELD",
value=invoice.logistics.transport_num,
)
invoice.financials.currency = (invoice.financials.currency or "foreign")
if invoice.financials.currency not in [c.value for c in Currency]:
errors.add_error(
field="financials.currency",
message="La Moneda proporcionada no es válida.",
solution=[
f"Selecciona una Moneda válida: {[c.value for c in Currency]}"
],
code="INVALID_CURRENCY",
value=invoice.financials.currency,
)
else:
has_items = db.query(Item).filter(
Item.invoice_id == invoice.id,
Item.tenant_id == tenant_id,
Item.company_id == company_id,
).first()
if has_items:
errors.add_error(
field="items",
message=f"La opcion tipo de moneda {invoice.financials.currency} no puede ser modificada ya que la factura tiene items asociados.",
solution=["Verifica la moneda de los items asociados a la factura."],
code="CURRENCY_CANNOT_BE_CHANGED",
value=invoice.financials.currency,
)
if invoice.logistics.incoterms:
incoterm_exists = (
db.query(Incoterm)
.filter(
Incoterm.code == invoice.logistics.incoterms,
Incoterm.tenant_id == tenant_id,
Incoterm.company_id == company_id,
)
.first()
)
if not incoterm_exists:
errors.add_error(
field="logistics.incoterms",
message="El Incoterm no existe en el Catálogo de Incoterms.",
solution=["Verifica el código del Incoterm", "Revisa el catálogo"],
code="NOT_FOUND",
value=invoice.logistics.incoterms,
)
if invoice.logistics.weight_type not in [w.value for w in WeightUnit]:
errors.add_error(
field="logistics.weight_type",
message="La Unidad de Peso proporcionada no es válida.",
solution=[
f"Selecciona una Unidad de Peso válida: {[w.value for w in WeightUnit]}"
],
code="INVALID_WEIGHT_UNIT",
value=invoice.logistics.weight_type,
)

View File

@@ -0,0 +1,85 @@
from sqlalchemy.orm import Session
from api.v1.modules.a76.general_catalogs.exchange_rate.models import ExchangeRate
from core.exceptions import ErrorCollector
from ....schemas import InvoiceHeaderCreate
from .common import validate_common
def validate_create(db: Session, invoice: InvoiceHeaderCreate, tenant_id: int, company_id: int, errors: ErrorCollector) -> None:
""" Valida la creación de una nueva factura de importe temporal """
if not invoice.operation_type:
errors.add_required_error("operation_type")
if not invoice.invoice_type:
errors.add_required_error("invoice_type")
if not invoice.document_type:
errors.add_required_error("document_type")
if not invoice.invoice_number:
errors.add_required_error("invoice_number")
if not invoice.invoice_date:
errors.add_required_error("invoice_date")
if not invoice.compliance_mx.provider_id:
errors.add_required_error("compliance_mx.provider_id")
if not invoice.compliance_mx.sold_to_id:
errors.add_required_error("compliance_mx.sold_to_id")
if not invoice.compliance_mx.shipped_to_id:
errors.add_required_error("compliance_mx.shipped_to_id")
if not invoice.compliance_mx.customs_broker_id:
errors.add_required_error("compliance_mx.customs_broker_id")
if errors.has_errors():
"""Se retorna por que hay campos obligatiorios para las validaciones que tienen que ser llenados"""
return
validate_common(db, invoice, tenant_id, company_id, errors)
if errors.has_errors():
"""Se retorna por que fallaron las validaciones generales"""
return
if not invoice.compliance_mx.pedimento_id:
invoice.compliance_mx.remesa = None
if not invoice.financials.exchange_rate:
invoice.financials.exchange_rate = db.query(ExchangeRate.value).filter(ExchangeRate.date == invoice.invoice_date).scalar()
invoice.document_type = (invoice.document_type or "").upper()
if not invoice.logistics.transport_type:
invoice.logistics.transport_type = "none"
if invoice.logistics.transport_type == "none" and invoice.logistics.transport_num:
invoice.logistics.transport_num = None
if not invoice.financials.currency:
invoice.financials.currency = "foreign"
if invoice.financials.currency == "local":
invoice.financials.currency_type = "MXN"
elif invoice.financials.currency_type == "foreign":
invoice.financials.currency = "USD"
elif invoice.financials.currency_type == "manual":
invoice.financials.currency_type = invoice.financials.currency_type.upper()
invoice.logistics.incoterm = (invoice.logistics.incoterm or "").upper()
if not invoice.logistics.weight_type:
invoice.logistics.weight_type = "kgs"

View File

@@ -0,0 +1,2 @@
def validate_update():
pass

View File

@@ -6,6 +6,23 @@ from core.database import Base
from datetime import datetime
from ....common.base_models import TenantScopedMixin, TimestampMixin
class Currency(str, Enum):
FOREIGN = "foreign"
LOCAL = "local"
MANUAL = "manual"
class WeightUnit(str, Enum):
KGS = "kgs"
LBS = "lbs"
class DestinationOriginCove(str, Enum):
EDO_BC_PARC_SON = "edo_bc_parc_son"
ESTADO_BCS = "estado_bcs"
ESTADO_ROO = "estado_roo"
MPIO_SALINA_CRUZ_OAX = "mpio_salina_cruz_oxa"
FRANJA_FRONT_NORTE = "franja_front_norte"
INTERIOR_PAIS = "interior_pais"
MPIO_CABORCA_SON = "mpio_caborca_son"
class OperationType(str, Enum):
IMP = "imp" # Importación
@@ -38,10 +55,11 @@ class InvoiceHeader(Base, TenantScopedMixin, TimestampMixin):
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
# Identifiers
system: Mapped[Optional[str]] = mapped_column(String(12)) # SISTEMA / Sistema de origen <-- no tiene campo en la antigua base de datos, sera para fixed_asset(scaf), inventory(scaii)
operation_type: Mapped[OperationType] = mapped_column(String(10)) # TIPOMOVIMIENTO / Clasifica imp/exp/sm/ctm
invoice_type: Mapped[Optional[str]] = mapped_column(ForeignKey("public.invoice_types.key")) # TIPOFACTURA / TIPODOC
invoice_number: Mapped[Optional[str]] = mapped_column(String(20)) # FACTURAIMPO/FACTURAEXPO/FACTURAREMISION/FACTURAENVIO/FACTURASALIDA
system: Mapped[str] = mapped_column(String(12)) # SISTEMA / Sistema de origen <-- no tiene campo en la antigua base de datos, sera para fixed_asset(scaf), inventory(scaii)
operation_type: Mapped[OperationType] = mapped_column(String(11)) # TIPOMOVIMIENTO / Clasifica imp/exp/sm/ctm
invoice_type: Mapped[str] = mapped_column(ForeignKey("public.invoice_types.key")) # TIPOFACTURA / TIPODOC
document_type: Mapped[str] = mapped_column(ForeignKey("public.pedimento_regimens.code")) # CLAVEDOCUMENTO / Clave de documento
invoice_number: Mapped[str] = mapped_column(String(100)) # FACTURAIMPO/FACTURAEXPO/FACTURAREMISION/FACTURAENVIO/FACTURASALIDA
project_number: Mapped[Optional[str]] = mapped_column(String(14)) # NUMPROYECTO
purchase_order: Mapped[Optional[str]] = mapped_column(String(50)) # ORDENCOMPRA
related_doc_id: Mapped[Optional[int]] = mapped_column(Integer) # IDRELDOC / Para Rectificaciones
@@ -50,20 +68,20 @@ class InvoiceHeader(Base, TenantScopedMixin, TimestampMixin):
proforma_number: Mapped[Optional[str]] = mapped_column(String(20)) # NUMEROPROFORMA
# Dates
invoice_date: Mapped[Optional[datetime]] = mapped_column(Date) # FECHAFACTURA
invoice_date: Mapped[datetime] = mapped_column(Date) # FECHAFACTURA
capture_date: Mapped[datetime] = mapped_column(TIMESTAMP(timezone=False), default=datetime.now) # FECHACAPTURA + HORAACTUAL
emission_date: Mapped[Optional[datetime]] = mapped_column(Date) # FECHAEMISION
# Status & Control
is_updated: Mapped[Optional[bool]] = mapped_column(Boolean) # ESTATUS
is_updated: Mapped[bool] = mapped_column(Boolean) # ESTATUS
is_updated_rec: Mapped[Optional[bool]] = mapped_column(Boolean) # ESTATUSREC / Estatus de recepción
is_updated_rep: Mapped[Optional[bool]] = mapped_column(Boolean) # ESTATUSREP / Estatus de reporte
updated_date: Mapped[Optional[datetime]] = mapped_column(TIMESTAMP(timezone=False)) # FECHAACTUALIZACION / FECHAACTUAL
who_updated: Mapped[Optional[str]] = mapped_column(String(20)) # USUARIOACT / Quien actualizó
capture_user: Mapped[Optional[str]] = mapped_column(String(20)) # USUARIOCAP / Usuario que capturó
traffic_light_status: Mapped[Optional[str]] = mapped_column(String(50)) # SEMAFORO / SEMAFOROEXPO/IMPO
process_log: Mapped[Optional[str]] = mapped_column(String(300)) # COMOFUEPROCESADA
status_rec: Mapped[Optional[int]] = mapped_column(Integer) # ESTATUSREC / Estatus de recepción
status_rep: Mapped[Optional[str]] = mapped_column(String(2)) # ESTATUSREP / Estatus de reporte
process_log: Mapped[Optional[str]] = mapped_column(String(300)) # COMOFUEPROCESADA
# Comments
observation_es: Mapped[Optional[str]] = mapped_column(Text) # OBSERVACIONE / Observaciones en español
@@ -81,9 +99,9 @@ class InvoiceHeader(Base, TenantScopedMixin, TimestampMixin):
party_count: Mapped[Optional[int]] = mapped_column(Integer) # CANT_PARTIDAS / Cantidad de partidas
# Generation flags
generate_id: Mapped[Optional[str]] = mapped_column(String(1)) # GENERAID
generate_id: Mapped[Optional[bool]] = mapped_column(Boolean, default=False) # GENERAID
generate_desc_parties: Mapped[Optional[str]] = mapped_column(String(12)) # GENDESCPARTIDAS / Generar descripción de partidas
apply_manual_discount: Mapped[Optional[str]] = mapped_column(String(1)) # APLICADESCMANUAL
apply_manual_discount: Mapped[Optional[bool]] = mapped_column(Boolean, default=False) # APLICADESCMANUAL
# Bulk & Downloads
is_bulk: Mapped[Optional[bool]] = mapped_column(Boolean) # ESAGRANEL / Es a granel
@@ -118,9 +136,9 @@ class InvoiceComplianceMx(Base, TenantScopedMixin, TimestampMixin):
invoice_id: Mapped[int] = mapped_column(ForeignKey("a76.invoice_header.id"), primary_key=True)
# Core Customs Data
pedimento: Mapped[Optional[str]] = mapped_column(String(19)) # PEDIMENTO/PEDIMENTOIMPO/EXPO
pedimento_code: Mapped[Optional[str]] = mapped_column(String(5)) # PEDIMENTOR1
pedimento_k1: Mapped[Optional[str]] = mapped_column(String(15)) # PEDIMENTOK1
pedimento_id: Mapped[Optional[int]] = mapped_column(ForeignKey("a76.pedimentos.id")) # PEDIMENTO/PEDIMENTOIMPO/EXPO
pedimento_r1: Mapped[Optional[int]] = mapped_column(ForeignKey("a76.pedimentos.id")) # PEDIMENTOR1
pedimento_k1: Mapped[Optional[int]] = mapped_column(ForeignKey("a76.pedimentos.id")) # PEDIMENTOK1
remesa: Mapped[Optional[int]] = mapped_column(Integer) # REMESA
aduana: Mapped[Optional[str]] = mapped_column(ForeignKey("public.customs_sections.customs_code")) # ADUANA_CRUCE
port_of_entry: Mapped[Optional[str]] = mapped_column(String(6)) # PUERTOENTRADA / Puerto de entrada
@@ -129,15 +147,15 @@ class InvoiceComplianceMx(Base, TenantScopedMixin, TimestampMixin):
# Clients & Providers
provider_header: Mapped[Optional[str]] = mapped_column(String(20)) # PROVEEDOREXPORTADOR
provider_id: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # PROVEEDOR
provider_id: Mapped[Optional[int]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # PROVEEDOR
sold_to_header: Mapped[Optional[str]] = mapped_column(String(20)) # VENDIDOCONSIGNADO
sold_to_id: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # VENDIDOA
sold_to_id: Mapped[Optional[int]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # VENDIDOA
shipped_to_header: Mapped[Optional[str]] = mapped_column(String(20)) # ENVIADOTRANSFERIDO
shipped_to_id: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # ENVIADOA
shipped_to_id: Mapped[Optional[int]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # ENVIADOA
shipped_by_header: Mapped[Optional[str]] = mapped_column(String(20)) # ENVIADOPORVENDIDOPOR
shipped_by_id: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # VENDIDOPOR/ENVIADOPOR
customs_broker_id: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.customs_brokers.id")) # AADUANAL / Agente aduanal
customs_broker_us_id: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.customs_brokers.id")) # AADUANALAME / Agente aduanal americano
shipped_by_id: Mapped[Optional[int]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # VENDIDOPOR/ENVIADOPOR
customs_broker_id: Mapped[Optional[int]] = mapped_column(ForeignKey("a76.customs_brokers.id")) # AADUANAL / Agente aduanal
customs_broker_us_id: Mapped[Optional[int]] = mapped_column(ForeignKey("a76.customs_brokers.id")) # AADUANALAME / Agente aduanal americano
# Broker Invoice
broker_invoice_num: Mapped[Optional[str]] = mapped_column(String(20)) # NUMFACTURABROKER / Número factura broker
@@ -148,15 +166,15 @@ class InvoiceComplianceMx(Base, TenantScopedMixin, TimestampMixin):
waste_type: Mapped[Optional[str]] = mapped_column(String(1)) # TIPODESPERDICIO / Tipo de desperdicio
scrap_type: Mapped[Optional[str]] = mapped_column(String(1)) # TIPOSCRAP / Tipo de scrap
appendix_17: Mapped[Optional[int]] = mapped_column(Integer) # APENDICE17 / Apéndice 17
is_regime_change: Mapped[Optional[str]] = mapped_column(String(1)) # ESCAMBIOREGIMEN / Es cambio de régimen
is_regime_change: Mapped[Optional[bool]] = mapped_column(Boolean, default=False) # ESCAMBIOREGIMEN / Es cambio de régimen
which_exchange_rate: Mapped[Optional[str]] = mapped_column(String(5)) # CUALTIPOCAMBIO / Cuál tipo de cambio
value_method: Mapped[Optional[str]] = mapped_column(String(2)) # METVALOR / Método de valoración
act_value: Mapped[Optional[str]] = mapped_column(String(5)) # ACTVALOR / Actualizar valor
is_pedimento_pending: Mapped[Optional[bool]] = mapped_column(Boolean, default=False) # PED_PENDIENTE_ASIGNAR (Mapear 1 -> True, 0 -> False)
# Ownership & Balances
is_owner_of_goods: Mapped[Optional[str]] = mapped_column(String(2)) # ESDUENOMCIA / Es dueño de mercancía
generate_balances: Mapped[Optional[str]] = mapped_column(String(2)) # GENERARSALDOS / Generar saldos
is_owner_of_goods: Mapped[Optional[bool]] = mapped_column(Boolean, default=False) # ESDUENOMCIA / Es dueño de mercancía
generate_balances: Mapped[Optional[bool]] = mapped_column(Boolean, default=False) # GENERARSALDOS / Generar saldos
was_reviewed_by_company: Mapped[Optional[bool]] = mapped_column(Boolean) # FUEREVISADAMCIA / Fue revisada por la compañía
# VUCEM / Digital
@@ -166,7 +184,7 @@ class InvoiceComplianceMx(Base, TenantScopedMixin, TimestampMixin):
niu_number: Mapped[Optional[str]] = mapped_column(String(19)) # NUMERONIU / Número NIU
bill_of_lading_count: Mapped[Optional[str]] = mapped_column(String(12)) # CANTGUIASEMBARQUE / Cantidad guías embarque
addendum_vu: Mapped[Optional[str]] = mapped_column(String(204)) # ADENDAVU / Adenda VUCEM
origin_destination_cove: Mapped[Optional[str]] = mapped_column(String(19)) # DESTINOORIGENCOVE / Destino/Origen COVE
origin_destination_cove: Mapped[Optional[DestinationOriginCove]] = mapped_column(String(20)) # DESTINOORIGENCOVE / Destino/Origen COVE
vucem_operation_num: Mapped[Optional[str]] = mapped_column(String(19)) # NUMOPERACIONVU / Número operación VUCEM
customs_person_line: Mapped[Optional[int]] = mapped_column(Integer) # LINEAPERSONAAA / Línea persona agente aduanal
@@ -201,7 +219,7 @@ class InvoiceFinancials(Base, TenantScopedMixin, TimestampMixin):
invoice_id: Mapped[int] = mapped_column(ForeignKey("a76.invoice_header.id"))
# Currency
currency: Mapped[Optional[str]] = mapped_column(String(3)) # CLAVEMONEDA / Clave de moneda
currency: Mapped[Currency] = mapped_column(String(7)) # CLAVEMONEDA / Clave de moneda
currency_type: Mapped[Optional[str]] = mapped_column(ForeignKey("public.currency_types.code")) # TIPOMONEDA / TIPOCLAVEMONEDA
exchange_rate: Mapped[Optional[float]] = mapped_column(Numeric(13, 6)) # TIPOCAMBIO / Tipo de cambio
exchange_rate_mm: Mapped[Optional[float]] = mapped_column(Numeric(13, 6)) # TIPOCAMBIOMM / Tipo de cambio moneda a moneda
@@ -278,7 +296,7 @@ class InvoiceLogistics(Base, TenantScopedMixin, TimestampMixin):
transport_num: Mapped[Optional[str]] = mapped_column(String(20)) # NUMTRASPORTE / Número de transporte
transport_mode: Mapped[Optional[str]] = mapped_column(String(15)) # MODTRANS / Modo de transporte
driver_name: Mapped[Optional[str]] = mapped_column(String(80)) # CONDUCTOR / Nombre del conductor
is_rail: Mapped[Optional[str]] = mapped_column(String(2)) # ESFERROCARRIL / Es ferrocarril
is_rail: Mapped[Optional[bool]] = mapped_column(Boolean, default=False) # ESFERROCARRIL / Es ferrocarril
rail_id: Mapped[Optional[str]] = mapped_column(String(31)) # IDFERRORCARRIL / ID ferrocarril
# Vehicle & Tracking
@@ -302,7 +320,7 @@ class InvoiceLogistics(Base, TenantScopedMixin, TimestampMixin):
complement_2: Mapped[Optional[str]] = mapped_column(String(30)) # COMPLEMENTO2 / Complemento 2
# Weight & Container Info
weight_type: Mapped[Optional[str]] = mapped_column(String(6)) # TIPOPESO / Tipo de peso
weight_type: Mapped[WeightUnit] = mapped_column(String(3)) # TIPOPESO / Tipo de peso
container_types: Mapped[Optional[str]] = mapped_column(String(500)) # CONTENEDORESTIPO / Tipos de contenedores
vehicle_data: Mapped[Optional[str]] = mapped_column(String(500)) # DATOSVEHICULO / Datos del vehículo
@@ -317,7 +335,7 @@ class InvoiceLogistics(Base, TenantScopedMixin, TimestampMixin):
delivery_date: Mapped[Optional[datetime]] = mapped_column(Date) # FECHAENTREGA / Fecha de entrega
# Delivery Control
delivered_status: Mapped[Optional[str]] = mapped_column(String(2)) # ENTREGADO / Estado de entrega
delivered_status: Mapped[Optional[bool]] = mapped_column(Boolean, default=False) # ENTREGADO / Estado de entrega
received_by: Mapped[Optional[str]] = mapped_column(String(50)) # RECIBIDOPOR / Recibido por
# Payment Info
@@ -325,7 +343,7 @@ class InvoiceLogistics(Base, TenantScopedMixin, TimestampMixin):
payment_receipt_num: Mapped[Optional[str]] = mapped_column(String(20)) # NUMRECIBOPAGO / Número de recibo de pago
# CTM Process
is_ctm_process: Mapped[Optional[str]] = mapped_column(String(2)) # SETRATAPROCESOCTM / Se trata de proceso CTM
is_ctm_process: Mapped[Optional[bool]] = mapped_column(Boolean, default=False) # SETRATAPROCESOCTM / Se trata de proceso CTM
# Relationship
header: Mapped["InvoiceHeader"] = relationship(back_populates="logistics")

View File

@@ -1,8 +1,8 @@
from typing import Optional, List
from typing import Literal, Optional, List
from datetime import datetime, date
from decimal import Decimal
from pydantic import BaseModel, Field
from .models import OperationType
from .models import DestinationOriginCove, OperationType, Currency, TransportType, WeightUnit
# --- Base Schemas ---
@@ -11,11 +11,13 @@ class InvoiceHeaderBase(BaseModel):
system: Optional[str] = Field(
None, max_length=12, description="System of origin")
operation_type: Optional[OperationType] = Field(
None, max_length=10, description="Operation type: imp/exp/sm/ctm")
..., description="Operation type: imp/exp/sm/ctm")
invoice_type: Optional[str] = Field(
None, max_length=5, description="Invoice type key")
document_type: str = Field(
..., max_length=3, description="Document type (Regimen Aduanero)")
invoice_number: Optional[str] = Field(
None, max_length=20, description="Invoice number")
None, max_length=100, description="Invoice number")
project_number: Optional[str] = Field(
None, max_length=14, description="Project number")
purchase_order: Optional[str] = Field(
@@ -28,9 +30,9 @@ class InvoiceHeaderBase(BaseModel):
None, max_length=19, description="Invoice reference")
proforma_number: Optional[str] = Field(
None, max_length=20, description="Proforma number")
invoice_date: Optional[date] = Field(None, description="Invoice date")
invoice_date: date = Field(..., description="Invoice date")
emission_date: Optional[date] = Field(None, description="Emission date")
is_updated: Optional[bool] = Field(None, description="Status")
is_updated: bool = Field(False, description="Status")
updated_date: Optional[datetime] = Field(None, description="Update date")
who_updated: Optional[str] = Field(
None, max_length=20, description="Who updated")
@@ -40,8 +42,8 @@ class InvoiceHeaderBase(BaseModel):
None, max_length=50, description="Traffic light status")
process_log: Optional[str] = Field(
None, max_length=300, description="Processing log")
status_rec: Optional[int] = Field(None, description="Reception status")
status_rep: Optional[str] = Field(
is_updated_rec: Optional[int] = Field(None, description="Reception status")
is_updated_rep: Optional[str] = Field(
None, max_length=2, description="Report status")
observation_es: Optional[str] = Field(
None, description="Observations in Spanish")
@@ -60,12 +62,10 @@ class InvoiceHeaderBase(BaseModel):
subcompany: Optional[str] = Field(
None, max_length=5, description="Subcompany")
party_count: Optional[int] = Field(None, description="Quantity of parties")
generate_id: Optional[str] = Field(
None, max_length=1, description="Generate ID")
generate_id: Optional[bool] = Field(False, description="Generate ID")
generate_desc_parties: Optional[str] = Field(
None, max_length=12, description="Generate description of parties")
apply_manual_discount: Optional[str] = Field(
None, max_length=1, description="Apply manual discount")
apply_manual_discount: Optional[bool] = Field(False, description="Apply manual discount")
is_bulk: Optional[bool] = Field(None, description="Is bulk")
download_substance: Optional[bool] = Field(
None, description="Download substance")
@@ -84,66 +84,64 @@ class InvoiceHeaderBase(BaseModel):
class InvoiceComplianceMxBase(BaseModel):
"""Base fields for Compliance MX"""
pedimento: Optional[str] = Field(
None, max_length=19, description="Pedimento number")
pedimento_code: Optional[str] = Field(
None, max_length=5, description="Pedimento code (R1)")
pedimento_k1: Optional[str] = Field(
None, max_length=15, description="Pedimento K1")
pedimento_id: Optional[int] = Field(
None, description="Pedimento id")
pedimento_r1: Optional[int] = Field(
None, description="Pedimento id (R1)")
pedimento_k1: Optional[int] = Field(
None, description="Pedimento id (K1)")
remesa: Optional[int] = Field(None, description="Remesa")
aduana: Optional[str] = Field(
None, max_length=5, description="Customs office")
aduana: Optional[str] = Field(None, max_length=5, description="Customs office")
port_of_entry: Optional[str] = Field(
None, max_length=6, description="Port of entry")
destination: Optional[str] = Field(
None, max_length=3, description="Destination code")
manifest_number: Optional[str] = Field(
None, max_length=15, description="Manifest number")
provider_header: Optional[str] = Field(
provider_header: str = Field(
None, max_length=20, description="Provider header")
provider_id: Optional[str] = Field(
provider_id: int = Field(
None, description="Provider ID")
sold_to_header: Optional[str] = Field(
sold_to_header: str = Field(
None, max_length=20, description="Sold to header")
sold_to_id: Optional[str] = Field(
sold_to_id: int = Field(
None, description="Sold to ID")
shipped_to_header: Optional[str] = Field(
shipped_to_header: str = Field(
None, max_length=20, description="Shipped to header")
shipped_to_id: Optional[str] = Field(
shipped_to_id:int = Field(
None, description="Shipped to ID")
shipped_by_header: Optional[str] = Field(
shipped_by_header: Optional[int] = Field(
None, max_length=20, description="Shipped by header")
shipped_by_id: Optional[str] = Field(
shipped_by_id: Optional[int] = Field(
None, description="Shipped by ID")
customs_broker_id: Optional[str] = Field(
customs_broker_id: int = Field(
None, description="Customs broker ID")
customs_broker_us_id: Optional[str] = Field(
customs_broker_us_id: Optional[int] = Field(
None, description="US customs broker ID")
broker_invoice_num: Optional[str] = Field(
None, max_length=20, description="Broker invoice number")
broker_invoice_date: Optional[date] = Field(
None, description="Broker invoice date")
is_mixed: Optional[bool] = Field(
None, description="Is mixed operation")
False, description="Is mixed operation")
waste_type: Optional[str] = Field(
None, max_length=1, description="Waste type")
scrap_type: Optional[str] = Field(
None, max_length=1, description="Scrap type")
appendix_17: Optional[int] = Field(None, description="Appendix 17")
is_regime_change: Optional[str] = Field(
None, max_length=1, description="Is regime change")
is_regime_change: Optional[bool] = Field(
False, description="Is regime change")
which_exchange_rate: Optional[str] = Field(
None, max_length=5, description="Which exchange rate")
value_method: Optional[str] = Field(
None, max_length=2, description="Value method")
act_value: Optional[str] = Field(
None, max_length=5, description="Act value")
is_pedimento_pending: Optional[bool] = Field(
None, description="Is pedimento pending")
is_owner_of_goods: Optional[str] = Field(
None, max_length=2, description="Is owner of goods")
generate_balances: Optional[str] = Field(
None, max_length=2, description="Generate balances")
is_pedimento_pending: bool = Field(..., description="Is pedimento pending")
is_owner_of_goods: Optional[bool] = Field(
False, description="Is owner of goods")
generate_balances: Optional[bool] = Field(
False, description="Generate balances")
was_reviewed_by_company: Optional[bool] = Field(
None, description="Was reviewed by company")
edocument: Optional[str] = Field(
@@ -158,8 +156,7 @@ class InvoiceComplianceMxBase(BaseModel):
None, max_length=12, description="Bill of lading count")
addendum_vu: Optional[str] = Field(
None, max_length=204, description="VUCEM addendum")
origin_destination_cove: Optional[str] = Field(
None, max_length=19, description="Origin/Destination COVE")
origin_destination_cove: Optional[DestinationOriginCove] = Field('franja_front_norte', max_length=20, description="Origin/Destination COVE")
vucem_operation_num: Optional[str] = Field(
None, max_length=19, description="VUCEM operation number")
customs_person_line: Optional[int] = Field(
@@ -191,11 +188,11 @@ class InvoiceComplianceMxBase(BaseModel):
class InvoiceFinancialsBase(BaseModel):
"""Base fields for Financials"""
currency: Optional[str] = Field(
None, max_length=3, description="Currency code")
currency: Currency = Field(
None, max_length=7, description="Currency code")
currency_type: Optional[str] = Field(
None, description="Currency type")
exchange_rate: Optional[Decimal] = Field(None, description="Exchange rate")
"USD", description="Currency type")
exchange_rate: Decimal = Field(0.00, description="Exchange rate")
exchange_rate_mm: Optional[Decimal] = Field(
None, description="Exchange rate currency to currency")
value_mn: Optional[Decimal] = Field(None, description="Value in MXN")
@@ -245,8 +242,7 @@ class InvoiceFinancialsBase(BaseModel):
None, description="IVA in foreign currency")
iva_mc: Optional[Decimal] = Field(
None, description="IVA in third currency")
iva_factor: Optional[str] = Field(
None, max_length=10, description="IVA factor")
iva_factor: Optional[Decimal] = Field(None, description="IVA factor")
tax_value_me: Optional[Decimal] = Field(
None, description="Tax value in foreign currency")
seal_value_2500: Optional[bool] = Field(
@@ -267,16 +263,16 @@ class InvoiceLogisticsBase(BaseModel):
None, max_length=10, description="Transport ID")
transport_us_id: Optional[str] = Field(
None, max_length=10, description="US transport ID")
transport_type: Optional[str] = Field(
None, max_length=15, description="Transport type")
transport_type: TransportType = Field(
'none', max_length=15, description="Transport type")
transport_num: Optional[str] = Field(
None, max_length=20, description="Transport number")
transport_mode: Optional[str] = Field(
None, max_length=15, description="Transport mode")
30, max_length=15, description="Transport mode")
driver_name: Optional[str] = Field(
None, max_length=80, description="Driver name")
is_rail: Optional[str] = Field(
None, max_length=2, description="Is rail transport")
is_rail: Optional[bool] = Field(
False, description="Is rail transport")
rail_id: Optional[str] = Field(
None, max_length=31, description="Rail ID")
vehicle_num: Optional[str] = Field(
@@ -307,8 +303,8 @@ class InvoiceLogisticsBase(BaseModel):
None, max_length=2, description="Identifier 2")
complement_2: Optional[str] = Field(
None, max_length=30, description="Complement 2")
weight_type: Optional[str] = Field(
None, max_length=6, description="Weight type")
weight_type: WeightUnit = Field(
default="kgs", max_length=3, description="Weight type")
container_types: Optional[str] = Field(
None, max_length=500, description="Container types")
vehicle_data: Optional[str] = Field(
@@ -333,8 +329,8 @@ class InvoiceLogisticsBase(BaseModel):
None, description="Payment date")
payment_receipt_num: Optional[str] = Field(
None, max_length=20, description="Payment receipt number")
is_ctm_process: Optional[str] = Field(
None, max_length=2, description="Is CTM process")
is_ctm_process: Optional[bool] = Field(
False, description="Is CTM process")
class InvoiceSalesDetailsBase(BaseModel):

View File

@@ -1,15 +1,22 @@
import traceback
from typing import Optional, List, Tuple
from sqlalchemy.orm import Session
from sqlalchemy import and_
from core.exceptions import ErrorCollector, DuplicateResourceException
from .common.mappers import clean_dict
from .imports.temporary.validators.create import validate_create
from .imports.temporary.validators.update import validate_update
from .common.common_validators import invoice_exists
from . import models, schemas
class InvoiceService:
"""Service for Invoice Header operations"""
@staticmethod
def get_by_id(db: Session, invoice_id: int, tenant_id: int, company_id: int) -> Optional[models.InvoiceHeader]:
def get_by_id(
db: Session, invoice_id: int, tenant_id: int, company_id: int
) -> Optional[models.InvoiceHeader]:
"""Get an invoice by ID with tenant/company validation"""
return (
db.query(models.InvoiceHeader)
@@ -39,25 +46,32 @@ class InvoiceService:
# Apply filters if provided
if filters:
if filters.get("status"):
query = query.filter(
models.InvoiceHeader.status == filters["status"])
query = query.filter(models.InvoiceHeader.status == filters["status"])
if filters.get("operation_type"):
query = query.filter(
models.InvoiceHeader.operation_type == filters["operation_type"])
models.InvoiceHeader.operation_type == filters["operation_type"]
)
if filters.get("invoice_type"):
query = query.filter(
models.InvoiceHeader.invoice_type == filters["invoice_type"])
models.InvoiceHeader.invoice_type == filters["invoice_type"]
)
if filters.get("invoice_number"):
query = query.filter(models.InvoiceHeader.invoice_number.ilike(
f"%{filters['invoice_number']}%"))
query = query.filter(
models.InvoiceHeader.invoice_number.ilike(
f"%{filters['invoice_number']}%"
)
)
if filters.get("pedimento"):
query = query.join(models.InvoiceComplianceMx).filter(
models.InvoiceComplianceMx.pedimento.ilike(
f"%{filters['pedimento']}%")
f"%{filters['pedimento']}%"
)
)
if not filters.get("invoice_type") and filters.get("operation_type") == "exp":
query = query.filter(
models.InvoiceHeader.operation_type != "REPAR")
if (
not filters.get("invoice_type")
and filters.get("operation_type") == "exp"
):
query = query.filter(models.InvoiceHeader.operation_type != "REPAR")
total = query.count()
items = query.offset(skip).limit(limit).all()
@@ -68,30 +82,19 @@ class InvoiceService:
db: Session,
invoice_data: schemas.InvoiceHeaderCreate,
tenant_id: int,
company_id: int
company_id: int,
) -> models.InvoiceHeader:
"""Create a new invoice with all related data"""
def clean_dict(data_dict: dict) -> dict:
cleaned = {}
for key, value in data_dict.items():
if key == 'customs_agent':
key = 'customs_broker_id'
elif key == 'provider':
key = 'provider_id'
if isinstance(value, str) and not value.strip():
cleaned[key] = None
elif value == 0 and (key.endswith('_id') or key == 'remesa'):
cleaned[key] = None
else:
cleaned[key] = value
return cleaned
# Validaciones con ErrorCollector
errors = ErrorCollector()
# Validar si la factura ya existe
#invoice_exists(db, invoice_data.invoice_number, tenant_id, company_id, errors)
#validate_create(db, invoice_data, tenant_id, company_id, errors)
# Si hay errores, lanzar excepción
errors.raise_if_errors("Error al crear la factura")
try:
# Extract nested data
@@ -103,27 +106,32 @@ class InvoiceService:
# Create main invoice header
raw_invoice_dict = invoice_data.model_dump(
exclude={"compliance_mx", "financials",
"logistics", "details", "collections"}
exclude={
"compliance_mx",
"financials",
"logistics",
"details",
"collections",
}
)
invoice_dict = clean_dict(raw_invoice_dict)
invoice_dict["tenant_id"] = tenant_id
invoice_dict["company_id"] = company_id
new_invoice = models.InvoiceHeader(**invoice_dict)
db.add(new_invoice)
db.flush() # Flush to get the invoice ID
# Create compliance_mx if provided
if compliance_data:
raw_comp_dict = compliance_data.model_dump()
# Pasamos los datos por la lavadora para arreglar pedimento, aduana, etc.
compliance_dict = clean_dict(raw_comp_dict)
compliance_dict["invoice_id"] = new_invoice.id
compliance_dict["tenant_id"] = tenant_id
compliance_dict["company_id"] = company_id
new_compliance = models.InvoiceComplianceMx(**compliance_dict)
db.add(new_compliance)
@@ -131,11 +139,11 @@ class InvoiceService:
if financials_data:
raw_fin_dict = financials_data.model_dump()
financials_dict = clean_dict(raw_fin_dict)
financials_dict["invoice_id"] = new_invoice.id
financials_dict["tenant_id"] = tenant_id
financials_dict["company_id"] = company_id
new_financials = models.InvoiceFinancials(**financials_dict)
db.add(new_financials)
@@ -143,7 +151,7 @@ class InvoiceService:
for logistics_item in logistics_data:
raw_log_dict = logistics_item.model_dump()
logistics_dict = clean_dict(raw_log_dict)
logistics_dict["invoice_id"] = new_invoice.id
logistics_dict["tenant_id"] = tenant_id
logistics_dict["company_id"] = company_id
@@ -154,7 +162,7 @@ class InvoiceService:
for detail_item in details_data:
raw_det_dict = detail_item.model_dump()
detail_dict = clean_dict(raw_det_dict)
detail_dict["invoice_id"] = new_invoice.id
detail_dict["tenant_id"] = tenant_id
detail_dict["company_id"] = company_id
@@ -165,7 +173,7 @@ class InvoiceService:
for collection_item in collections_data:
raw_col_dict = collection_item.model_dump()
collection_dict = clean_dict(raw_col_dict)
collection_dict["invoice_id"] = new_invoice.id
collection_dict["tenant_id"] = tenant_id
collection_dict["company_id"] = company_id
@@ -180,7 +188,7 @@ class InvoiceService:
db.rollback()
print("\n\n🔥 ERROR AL GUARDAR FACTURA 🔥")
print(f"Error: {str(e)}")
traceback.print_exc() # Esto imprime el error real en la consola
traceback.print_exc() # Esto imprime el error real en la consola
print("--------------------------------\n")
raise e
@@ -190,20 +198,24 @@ class InvoiceService:
invoice_id: int,
tenant_id: int,
invoice_data: schemas.InvoiceHeaderUpdate,
company_id: int
company_id: int,
) -> Optional[models.InvoiceHeader]:
# ... (El resto de tu código update se queda igual) ...
# (Te recomiendo implementar clean_dict aquí también si tienes problemas al editar)
invoice = InvoiceService.get_by_id(
db, invoice_id, tenant_id, company_id)
invoice = InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
if not invoice:
return None
# Update main invoice header fields
update_dict = invoice_data.model_dump(
exclude={"compliance_mx", "financials",
"logistics", "details", "collections"},
exclude_unset=True
exclude={
"compliance_mx",
"financials",
"logistics",
"details",
"collections",
},
exclude_unset=True,
)
for key, value in update_dict.items():
setattr(invoice, key, value)
@@ -211,15 +223,21 @@ class InvoiceService:
# Update compliance_mx if provided
if invoice_data.compliance_mx is not None:
if invoice.compliance_mx:
for key, value in invoice_data.compliance_mx.model_dump(exclude_unset=True).items():
for key, value in invoice_data.compliance_mx.model_dump(
exclude_unset=True
).items():
# Parche rápido para update
if value == "": value = None
if value == "":
value = None
setattr(invoice.compliance_mx, key, value)
else:
compliance_dict = invoice_data.compliance_mx.model_dump()
# Aplicar limpieza manual si es necesario
if 'customs_agent' in compliance_dict: compliance_dict['customs_broker_id'] = compliance_dict.pop('customs_agent')
if "customs_agent" in compliance_dict:
compliance_dict["customs_broker_id"] = compliance_dict.pop(
"customs_agent"
)
compliance_dict["invoice_id"] = invoice.id
compliance_dict["tenant_id"] = tenant_id
compliance_dict["company_id"] = company_id
@@ -229,8 +247,11 @@ class InvoiceService:
# Update financials if provided
if invoice_data.financials is not None:
if invoice.financials:
for key, value in invoice_data.financials.model_dump(exclude_unset=True).items():
if value == "": value = None
for key, value in invoice_data.financials.model_dump(
exclude_unset=True
).items():
if value == "":
value = None
setattr(invoice.financials, key, value)
else:
financials_dict = invoice_data.financials.model_dump()
@@ -247,10 +268,9 @@ class InvoiceService:
@staticmethod
def delete(db: Session, invoice_id: int, tenant_id: int, company_id: int) -> bool:
"""Delete an invoice and all related data (cascade delete)"""
invoice = InvoiceService.get_by_id(
db, invoice_id, tenant_id, company_id)
invoice = InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
if invoice:
db.delete(invoice)
db.commit()
return True
return False
return False

View File

@@ -1,159 +1,241 @@
from decimal import Decimal
from typing import Optional
from pydantic import BaseModel, Field, ConfigDict
from pydantic import BaseModel, Field, ConfigDict, field_validator
# Import nested schemas
from ..line_customs.schemas import (
LineCustomCreate,
LineCustomUpdate,
LineCustomResponse
LineCustomResponse,
)
from ..line_descriptions.schemas import (
LineDescriptionCreate,
LineDescriptionUpdate,
LineDescriptionResponse
LineDescriptionResponse,
)
from ..line_quantities.schemas import (
LineQuantityCreate,
LineQuantityUpdate,
LineQuantityResponse
LineQuantityResponse,
)
from ..line_financials.schemas import (
LineFinancialCreate,
LineFinancialUpdate,
LineFinancialResponse
LineFinancialResponse,
)
from ..line_references.schemas import (
LineReferenceCreate,
LineReferenceUpdate,
LineReferenceResponse
LineReferenceResponse,
)
# ============================================================================
# LINE ITEM SCHEMAS
# ============================================================================
class LineItemBase(BaseModel):
"""Base schema for line items"""
line_number: int = Field(..., description="Line number")
# Part identification
part_number: Optional[str] = Field(None, max_length=50, description="Part number")
component_part_number: Optional[str] = Field(None, max_length=50, description="Component part number")
component_part_number: Optional[str] = Field(
None, max_length=50, description="Component part number"
)
class_code: Optional[str] = Field(None, max_length=20, description="Class code")
@field_validator(
"class_code",
"part_number",
"component_part_number",
"unit_of_measure",
"alternate_unit",
mode="before",
)
@classmethod
def convert_to_string(cls, v):
"""Convert integers to strings for FK fields"""
if v is not None and not isinstance(v, str):
return str(v)
return v
# Unit of measure
unit_of_measure: Optional[str] = Field(None, max_length=10, description="Unit of measure")
alternate_unit: Optional[str] = Field(None, max_length=10, description="Alternate unit")
unit_of_measure: Optional[str] = Field(
None, max_length=10, description="Unit of measure"
)
alternate_unit: Optional[str] = Field(
None, max_length=10, description="Alternate unit"
)
uma_key: Optional[str] = Field(None, max_length=2, description="UMA key")
auxiliary_unit: Optional[str] = Field(None, max_length=5, description="Auxiliary unit")
auxiliary_unit: Optional[str] = Field(
None, max_length=5, description="Auxiliary unit"
)
# Permits and certificates
permit_number: Optional[str] = Field(None, max_length=20, description="Permit number")
permit_number: Optional[str] = Field(
None, max_length=20, description="Permit number"
)
page_line: Optional[str] = Field(None, max_length=10, description="Page line")
has_certificate: Optional[bool] = Field(None, description="Has certificate")
certificate_number: Optional[str] = Field(None, max_length=10, description="Certificate number")
octave_permit: Optional[str] = Field(None, max_length=20, description="Octave permit")
certificate_number: Optional[str] = Field(
None, max_length=10, description="Certificate number"
)
octave_permit: Optional[str] = Field(
None, max_length=20, description="Octave permit"
)
permits_ped: Optional[str] = Field(None, max_length=500, description="PED permits")
# FDA
has_fda_code: Optional[bool] = Field(None, description="Has FDA code")
fda_key: Optional[str] = Field(None, max_length=10, description="FDA key")
# Subitem flags
is_subitem: Optional[bool] = Field(None, description="Is subitem")
contains_subitems: Optional[bool] = Field(None, description="Contains subitems")
includes_subitems: Optional[bool] = Field(None, description="Includes subitems")
subitem_number: Optional[bool] = Field(None, description="Subitem number")
# Special flags
is_military_mcia: Optional[bool] = Field(None, description="Is military merchandise")
is_military_mcia: Optional[bool] = Field(
None, description="Is military merchandise"
)
# IV32
iv32_type_key: Optional[str] = Field(None, max_length=5, description="IV32 type key")
iv32_type_key: Optional[str] = Field(
None, max_length=5, description="IV32 type key"
)
iv32_number: Optional[str] = Field(None, max_length=35, description="IV32 number")
# Export specific
scrap_invoice: Optional[str] = Field(None, max_length=15, description="Scrap invoice")
consecutive_destination: Optional[int] = Field(None, description="Consecutive destination")
scrap_invoice: Optional[str] = Field(
None, max_length=15, description="Scrap invoice"
)
consecutive_destination: Optional[int] = Field(
None, description="Consecutive destination"
)
ctm_section: Optional[str] = Field(None, max_length=3, description="CTM section")
# Tax payment
tax_payment: Optional[bool] = Field(None, description="Tax payment")
payment_method: Optional[str] = Field(None, max_length=9, description="Payment method")
payment_method: Optional[str] = Field(
None, max_length=9, description="Payment method"
)
igi_amount: Optional[Decimal] = Field(None, description="IGI amount")
igi_payment_method: Optional[str] = Field(None, max_length=9, description="IGI payment method")
igi_payment_method: Optional[str] = Field(
None, max_length=9, description="IGI payment method"
)
# FCC
fcc_key: Optional[str] = Field(None, max_length=30, description="FCC key")
# Valuation method
valuation_method: Optional[str] = Field(None, max_length=2, description="Valuation method")
valuation_determined_value: Optional[Decimal] = Field(None, description="Valuation determined value")
valuation_reason: Optional[str] = Field(None, max_length=500, description="Valuation reason")
valuation_method: Optional[str] = Field(
None, max_length=2, description="Valuation method"
)
valuation_determined_value: Optional[Decimal] = Field(
None, description="Valuation determined value"
)
valuation_reason: Optional[str] = Field(
None, max_length=500, description="Valuation reason"
)
# Container rules
container_rule: Optional[str] = Field(None, max_length=50, description="Container rule")
container_parts_ii: Optional[str] = Field(None, max_length=50, description="Container parts II")
container_rule: Optional[str] = Field(
None, max_length=50, description="Container rule"
)
container_parts_ii: Optional[str] = Field(
None, max_length=50, description="Container parts II"
)
# APHIS
consecutive_aphis: Optional[int] = Field(None, description="Consecutive APHIS")
# BOM/Commercial
bom_version: Optional[int] = Field(None, description="BOM version")
bill_version: Optional[int] = Field(None, description="Bill version")
# TLCAN value
tlcan_value: Optional[Decimal] = Field(None, description="TLCAN value")
# Identifier
identifier: Optional[str] = Field(None, max_length=2, description="Identifier")
# Validation fields
validation_zero: Optional[int] = Field(None, description="Validation zero")
validation_one: Optional[int] = Field(None, description="Validation one")
# Material type
material_type: Optional[str] = Field(None, max_length=50, description="Material type")
material_type: Optional[str] = Field(
None, max_length=50, description="Material type"
)
# Order concept
order_type: Optional[str] = Field(None, max_length=50, description="Order type")
line_concept: Optional[str] = Field(None, max_length=50, description="Line concept")
# Review dispatch
review_dispatch: Optional[str] = Field(None, max_length=10, description="Review dispatch")
review_dispatch: Optional[str] = Field(
None, max_length=10, description="Review dispatch"
)
# Take component from PT
take_component_pt: Optional[int] = Field(None, description="Take component from PT")
# Pallet
pallet2: Optional[int] = Field(None, description="Pallet 2")
# Wildcard field
wildcard_field: Optional[str] = Field(None, max_length=100, description="Wildcard field")
wildcard_field: Optional[str] = Field(
None, max_length=100, description="Wildcard field"
)
class LineItemCreate(LineItemBase):
"""Schema for creating line item with all nested data"""
financial: Optional[LineFinancialCreate] = Field(None, description="Financial data for this line")
quantity: Optional[LineQuantityCreate] = Field(None, description="Quantity data for this line")
customs: Optional[LineCustomCreate] = Field(None, description="Customs data for this line")
description: Optional[LineDescriptionCreate] = Field(None, description="Description data for this line")
reference: Optional[LineReferenceCreate] = Field(None, description="Reference data for this line")
financial: Optional[LineFinancialCreate] = Field(
None, description="Financial data for this line"
)
quantity: Optional[LineQuantityCreate] = Field(
None, description="Quantity data for this line"
)
customs: Optional[LineCustomCreate] = Field(
None, description="Customs data for this line"
)
description: Optional[LineDescriptionCreate] = Field(
None, description="Description data for this line"
)
reference: Optional[LineReferenceCreate] = Field(
None, description="Reference data for this line"
)
class LineItemUpdate(LineItemBase):
"""Schema for updating line item with all nested data"""
line_number: Optional[int] = Field(None, description="Line number")
financial: Optional[LineFinancialUpdate] = Field(None, description="Financial data for this line")
quantity: Optional[LineQuantityUpdate] = Field(None, description="Quantity data for this line")
customs: Optional[LineCustomUpdate] = Field(None, description="Customs data for this line")
description: Optional[LineDescriptionUpdate] = Field(None, description="Description data for this line")
reference: Optional[LineReferenceUpdate] = Field(None, description="Reference data for this line")
financial: Optional[LineFinancialUpdate] = Field(
None, description="Financial data for this line"
)
quantity: Optional[LineQuantityUpdate] = Field(
None, description="Quantity data for this line"
)
customs: Optional[LineCustomUpdate] = Field(
None, description="Customs data for this line"
)
description: Optional[LineDescriptionUpdate] = Field(
None, description="Description data for this line"
)
reference: Optional[LineReferenceUpdate] = Field(
None, description="Reference data for this line"
)
class LineItemResponse(LineItemBase):
"""Schema for line item response with all nested data"""
id: int
item_id: int
financial: Optional[LineFinancialResponse] = None

View File

@@ -106,7 +106,7 @@ class SubassemblyEntry(Base, TenantScopedMixin, TimestampMixin):
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
remission_line: Mapped[int] = mapped_column(Integer) # LINEAREMISION
exit_invoice: Mapped[Optional[str]] = mapped_column(
String(15)) # FACTURASALIDA
String(15)) # FACTURASALIDA
exit_line: Mapped[Optional[int]] = mapped_column(Integer) # LINEASALIDA
# ============================================================================

View File

@@ -36,10 +36,7 @@ class ItemService:
@staticmethod
def get_by_id(
db: Session,
item_id: int,
tenant_id: int,
company_id: int
db: Session, item_id: int, tenant_id: int, company_id: int
) -> Optional[Item]:
"""Get an item by ID with tenant/company validation"""
return (
@@ -91,8 +88,7 @@ class ItemService:
if filters.get("item_type"):
query = query.filter(Item.item_type == filters["item_type"])
if filters.get("system_origin"):
query = query.filter(Item.system_origin ==
filters["system_origin"])
query = query.filter(Item.system_origin == filters["system_origin"])
if filters.get("search"):
search_term = f"%{filters['search']}%"
query = query.filter(
@@ -151,6 +147,12 @@ class ItemService:
lines_data = item_data.lines or []
item_dict = item_data.model_dump(exclude={"lines"})
# DEBUG: Log incoming data
print(f"\n🔍 DEBUG CREATE ITEM:")
print(f" Item data: {item_dict}")
print(f" Lines count: {len(lines_data)}")
print(f" Tenant ID: {tenant_id}, Company ID: {company_id}")
# Add tenant and company
item_dict["tenant_id"] = tenant_id
item_dict["company_id"] = company_id
@@ -160,8 +162,11 @@ class ItemService:
db.add(db_item)
db.flush() # Get the item ID
print(f" ✅ Item created with ID: {db_item.id}")
# Create line items if provided
for line_data in lines_data:
for idx, line_data in enumerate(lines_data):
print(f"\n 📝 Processing line {idx + 1}/{len(lines_data)}")
# Extract nested data from line
financial_data = line_data.financial
quantity_data = line_data.quantity
@@ -169,16 +174,31 @@ class ItemService:
description_data = line_data.description
reference_data = line_data.reference
print(f" Line data: {line_data.model_dump()}")
print(f" Has financial: {financial_data is not None}")
print(f" Has quantity: {quantity_data is not None}")
print(f" Has customs: {customs_data is not None}")
print(f" Has description: {description_data is not None}")
print(f" Has reference: {reference_data is not None}")
line_dict = line_data.model_dump(
exclude={"financial", "quantity",
"customs", "description", "reference"}
exclude={
"financial",
"quantity",
"customs",
"description",
"reference",
}
)
line_dict["item_id"] = db_item.id
line_dict["tenant_id"] = tenant_id
line_dict["company_id"] = company_id
# Create line item
db_line = LineItem(**line_dict)
db.add(db_line)
db.flush() # Get the line ID
print(f" ✅ Line created with ID: {db_line.id}")
# Create financial data if provided
if financial_data:
@@ -186,6 +206,7 @@ class ItemService:
financial_dict["item_line_id"] = db_line.id
db_financial = LineFinancial(**financial_dict)
db.add(db_financial)
print(f" ✅ Financial data added")
# Create quantity data if provided
if quantity_data:
@@ -193,6 +214,7 @@ class ItemService:
quantity_dict["item_line_id"] = db_line.id
db_quantity = LineQuantity(**quantity_dict)
db.add(db_quantity)
print(f" ✅ Quantity data added")
# Create customs data if provided
if customs_data:
@@ -200,6 +222,7 @@ class ItemService:
customs_dict["item_line_id"] = db_line.id
db_customs = LineCustom(**customs_dict)
db.add(db_customs)
print(f" ✅ Customs data added")
# Create description data if provided
if description_data:
@@ -207,6 +230,7 @@ class ItemService:
description_dict["item_line_id"] = db_line.id
db_description = LineDescription(**description_dict)
db.add(db_description)
print(f" ✅ Description data added")
# Create reference data if provided
if reference_data:
@@ -214,9 +238,12 @@ class ItemService:
reference_dict["item_line_id"] = db_line.id
db_reference = LineReference(**reference_dict)
db.add(db_reference)
print(f" ✅ Reference data added")
print(f"\n 💾 Committing transaction...")
db.commit()
db.refresh(db_item)
print(f" ✅ Transaction committed successfully!")
return db_item
except IntegrityError as e:
@@ -248,8 +275,7 @@ class ItemService:
# Extract lines data
lines_data = item_data.lines
item_dict = item_data.model_dump(
exclude={"lines"}, exclude_unset=True)
item_dict = item_data.model_dump(exclude={"lines"}, exclude_unset=True)
# Update item fields
for key, value in item_dict.items():
@@ -272,43 +298,48 @@ class ItemService:
reference_data = line_data.reference
line_dict = line_data.model_dump(
exclude={"financial", "quantity",
"customs", "description", "reference"},
exclude_unset=True
exclude={
"financial",
"quantity",
"customs",
"description",
"reference",
},
exclude_unset=True,
)
line_dict["item_id"] = db_item.id
line_dict["tenant_id"] = tenant_id
line_dict["company_id"] = company_id
db_line = LineItem(**line_dict)
db.add(db_line)
db.flush()
# Create nested data if provided
if financial_data is not None:
financial_dict = financial_data.model_dump(
exclude_unset=True)
financial_dict = financial_data.model_dump(exclude_unset=True)
financial_dict["item_line_id"] = db_line.id
db.add(LineFinancial(**financial_dict))
if quantity_data is not None:
quantity_dict = quantity_data.model_dump(
exclude_unset=True)
quantity_dict = quantity_data.model_dump(exclude_unset=True)
quantity_dict["item_line_id"] = db_line.id
db.add(LineQuantity(**quantity_dict))
if customs_data is not None:
customs_dict = customs_data.model_dump(
exclude_unset=True)
customs_dict = customs_data.model_dump(exclude_unset=True)
customs_dict["item_line_id"] = db_line.id
db.add(LineCustom(**customs_dict))
if description_data is not None:
description_dict = description_data.model_dump(
exclude_unset=True)
exclude_unset=True
)
description_dict["item_line_id"] = db_line.id
db.add(LineDescription(**description_dict))
if reference_data is not None:
reference_dict = reference_data.model_dump(
exclude_unset=True)
reference_dict = reference_data.model_dump(exclude_unset=True)
reference_dict["item_line_id"] = db_line.id
db.add(LineReference(**reference_dict))

View File

@@ -7,7 +7,7 @@ from pydantic import BaseModel, ConfigDict, Field
class PedimentoDatesBase(BaseModel):
"""Base schema for Pedimento Dates"""
entry_date: Optional[datetime] = Field(None, description="Entry date")
entry_date: datetime = Field(..., description="Entry date")
pedimento_date: Optional[datetime] = Field(None, description="Pedimento date")
payment_date: Optional[datetime] = Field(None, description="Payment date")
rectification_payment_date: Optional[datetime] = Field(
@@ -18,7 +18,7 @@ class PedimentoDatesBase(BaseModel):
eucan_date: Optional[datetime] = Field(None, description="EUCAN date")
original_date: Optional[datetime] = Field(None, description="Original date")
start_date: Optional[datetime] = Field(None, description="Start date")
end_date: Optional[datetime] = Field(None, description="End date")
end_date: datetime = Field(..., description="End date")
class PedimentoDatesCreate(BaseModel):
@@ -33,7 +33,7 @@ class PedimentoDatesCreate(BaseModel):
eucan_date: Optional[datetime] = Field(None, description="EUCAN date")
original_date: Optional[datetime] = Field(None, description="Original date")
start_date: Optional[datetime] = Field(None, description="Start date")
end_date: Optional[datetime] = Field(None, description="End date")
end_date: datetime = Field(..., description="End date")
class PedimentoDatesUpdate(BaseModel):

View File

@@ -1,20 +1,47 @@
from datetime import datetime
from decimal import Decimal
from enum import IntEnum
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field
from .pedimento_config_additional import PedimentoConfigAdditionalCreate, PedimentoConfigAdditionalResponse
from .pedimento_config_calculations import PedimentoConfigCalculationsCreate, PedimentoConfigCalculationsResponse
from .pedimento_config_parameters import PedimentoConfigParametersCreate, PedimentoConfigParametersResponse
from .pedimento_config_surcharges import PedimentoConfigSurchargesCreate, PedimentoConfigSurchargesResponse
from .pedimento_config_update_rectification import PedimentoConfigUpdateRectificationCreate, PedimentoConfigUpdateRectificationResponse
from .pedimento_config_updates import PedimentoConfigUpdatesCreate, PedimentoConfigUpdatesResponse
from .pedimento_customs_offices import PedimentoCustomsOfficesCreate, PedimentoCustomsOfficesResponse
from ..models.pedimentos import OperationType, PedimentoType
from .pedimento_config_additional import (
PedimentoConfigAdditionalCreate,
PedimentoConfigAdditionalResponse,
)
from .pedimento_config_calculations import (
PedimentoConfigCalculationsCreate,
PedimentoConfigCalculationsResponse,
)
from .pedimento_config_parameters import (
PedimentoConfigParametersCreate,
PedimentoConfigParametersResponse,
)
from .pedimento_config_surcharges import (
PedimentoConfigSurchargesCreate,
PedimentoConfigSurchargesResponse,
)
from .pedimento_config_update_rectification import (
PedimentoConfigUpdateRectificationCreate,
PedimentoConfigUpdateRectificationResponse,
)
from .pedimento_config_updates import (
PedimentoConfigUpdatesCreate,
PedimentoConfigUpdatesResponse,
)
from .pedimento_customs_offices import (
PedimentoCustomsOfficesCreate,
PedimentoCustomsOfficesResponse,
)
from .pedimento_dates import PedimentoDatesCreate, PedimentoDatesResponse
from .pedimento_decrementables import PedimentoDecrementablesCreate, PedimentoDecrementablesResponse
from .pedimento_incrementables import PedimentoIncrementablesCreate, PedimentoIncrementablesResponse
from .pedimento_decrementables import (
PedimentoDecrementablesCreate,
PedimentoDecrementablesResponse,
)
from .pedimento_incrementables import (
PedimentoIncrementablesCreate,
PedimentoIncrementablesResponse,
)
from .pedimento_indexes import PedimentoIndexesCreate, PedimentoIndexesResponse
from .pedimento_packages_transport import (
PedimentoContainerCreate,
@@ -33,17 +60,21 @@ from .pedimento_contributions import (
PedimentoContributionResponse,
)
from .pedimento_payments import PedimentoPaymentsCreate, PedimentoPaymentsResponse
from .pedimento_rectification_destination import PedimentoRectificationDestinationCreate, PedimentoRectificationDestinationResponse
from .pedimento_rectification_origin import PedimentoRectificationOriginCreate, PedimentoRectificationOriginResponse
from .pedimento_transport_means import PedimentoTransportMeansCreate, PedimentoTransportMeansResponse
from .pedimento_rectification_destination import (
PedimentoRectificationDestinationCreate,
PedimentoRectificationDestinationResponse,
)
from .pedimento_rectification_origin import (
PedimentoRectificationOriginCreate,
PedimentoRectificationOriginResponse,
)
from .pedimento_transport_means import (
PedimentoTransportMeansCreate,
PedimentoTransportMeansResponse,
)
from .pedimento_validation import PedimentoValidationCreate, PedimentoValidationResponse
class OperationType(IntEnum):
EXPORTACION = 1
IMPORTACION = 2
class PedimentosBase(BaseModel):
"""Base schema for Pedimentos"""
@@ -56,18 +87,17 @@ class PedimentosBase(BaseModel):
None, max_length=7, description="Pedimento number"
)
client_id: Optional[int] = Field(None, description="Client ID")
operation_type: Optional[int] = Field(None, description="Operation type")
pedimento_type: Optional[str] = Field(None, max_length=20, description="Pedimento type")
pedimento_code: str = Field(
..., max_length=2, description="Pedimento key"
)
operation_type: Optional[OperationType] = Field(None, description="Operation type")
pedimento_type: Optional[PedimentoType] = Field(None, description="Pedimento type")
pedimento_code: str = Field(..., max_length=2, description="Pedimento key")
regime: str = Field(..., max_length=3, description="Regime")
status: Optional[str] = Field(None, max_length=30, description="Status")
usd_value: Optional[Decimal] = Field(None, description="USD value")
paid_price: Optional[Decimal] = Field(None, description="Paid price")
gross_weight: Optional[Decimal] = Field(None, description="Gross weight")
exchange_rate: Optional[Decimal] = Field(None, description="Exchange rate")
observations: Optional[str] = Field(None, description="Observations")
observations: Optional[str] = Field(None, description="Observations")
class PedimentosCreate(PedimentosBase):
"""Schema for creating a new Pedimento"""
@@ -79,26 +109,28 @@ class PedimentosCreate(PedimentosBase):
pedimento_number: str = Field(..., max_length=7, description="Pedimento number")
client_id: int = Field(..., description="Client ID")
# operation_type, pedimento_type, status son opcionales - se pueden llenar después
pedimento_code: str = Field(
..., max_length=2, description="Pedimento key"
)
regime: str = Field(..., max_length=3, description="Regime")
pedimento_code: str = Field(..., max_length=2, description="Pedimento key")
regime: str = Field(..., max_length=3, description="Regime")
pedimento_dates: Optional[PedimentoDatesCreate] = None
pedimento_decrementables: Optional[PedimentoDecrementablesCreate] = None
pedimento_incrementables: Optional[PedimentoIncrementablesCreate] = None
pedimento_indexes: Optional[PedimentoIndexesCreate] = None
pedimento_validation: Optional[PedimentoValidationCreate] = None
pedimento_customs_offices: Optional[PedimentoCustomsOfficesCreate] = None
pedimento_validation: Optional[PedimentoValidationCreate] = None
pedimento_customs_offices: Optional[PedimentoCustomsOfficesCreate] = None
pedimento_payments: Optional[PedimentoPaymentsCreate] = None
pedimento_rectification_destination: Optional[PedimentoRectificationDestinationCreate] = None
pedimento_rectification_destination: Optional[
PedimentoRectificationDestinationCreate
] = None
pedimento_rectification_origin: Optional[PedimentoRectificationOriginCreate] = None
pedimento_transport_means: Optional[PedimentoTransportMeansCreate] = None
pedimento_transport_means: Optional[PedimentoTransportMeansCreate] = None
pedimento_config_additional: Optional[PedimentoConfigAdditionalCreate] = None
pedimento_config_calculations: Optional[PedimentoConfigCalculationsCreate] = None
pedimento_config_parameters: Optional[PedimentoConfigParametersCreate] = None
pedimento_config_surcharges: Optional[PedimentoConfigSurchargesCreate] = None
pedimento_config_update_rectification: Optional[PedimentoConfigUpdateRectificationCreate] = None
pedimento_config_update_rectification: Optional[
PedimentoConfigUpdateRectificationCreate
] = None
pedimento_config_updates: Optional[PedimentoConfigUpdatesCreate] = None
pedimento_packages: Optional[PedimentoPackagesCreate] = None
pedimento_transport_carriers: Optional[list[PedimentoTransportCarrierCreate]] = None
@@ -107,6 +139,7 @@ class PedimentosCreate(PedimentosBase):
pedimento_seals: Optional[list[PedimentoSealCreate]] = None
pedimento_containers: Optional[list[PedimentoContainerCreate]] = None
class PedimentosUpdate(BaseModel):
"""Schema for updating a Pedimento"""
@@ -115,7 +148,7 @@ class PedimentosUpdate(BaseModel):
license: Optional[str] = Field(None, max_length=4)
pedimento_number: Optional[str] = Field(None, max_length=7)
client_id: Optional[int] = None
operation_type: Optional[int] = None
operation_type: Optional[str] = Field(None, max_length=3)
pedimento_type: Optional[str] = Field(None, max_length=20)
pedimento_code: Optional[str] = Field(None, max_length=2)
regime: Optional[str] = Field(None, max_length=3)
@@ -125,23 +158,27 @@ class PedimentosUpdate(BaseModel):
gross_weight: Optional[Decimal] = None
exchange_rate: Optional[Decimal] = None
observations: Optional[str] = None
# Sub-resources
pedimento_dates: Optional[PedimentoDatesCreate] = None
pedimento_decrementables: Optional[PedimentoDecrementablesCreate] = None
pedimento_incrementables: Optional[PedimentoIncrementablesCreate] = None
pedimento_indexes: Optional[PedimentoIndexesCreate] = None
pedimento_validation: Optional[PedimentoValidationCreate] = None
pedimento_customs_offices: Optional[PedimentoCustomsOfficesCreate] = None
pedimento_validation: Optional[PedimentoValidationCreate] = None
pedimento_customs_offices: Optional[PedimentoCustomsOfficesCreate] = None
pedimento_payments: Optional[PedimentoPaymentsCreate] = None
pedimento_rectification_destination: Optional[PedimentoRectificationDestinationCreate] = None
pedimento_rectification_destination: Optional[
PedimentoRectificationDestinationCreate
] = None
pedimento_rectification_origin: Optional[PedimentoRectificationOriginCreate] = None
pedimento_transport_means: Optional[PedimentoTransportMeansCreate] = None
pedimento_transport_means: Optional[PedimentoTransportMeansCreate] = None
pedimento_config_additional: Optional[PedimentoConfigAdditionalCreate] = None
pedimento_config_calculations: Optional[PedimentoConfigCalculationsCreate] = None
pedimento_config_parameters: Optional[PedimentoConfigParametersCreate] = None
pedimento_config_surcharges: Optional[PedimentoConfigSurchargesCreate] = None
pedimento_config_update_rectification: Optional[PedimentoConfigUpdateRectificationCreate] = None
pedimento_config_update_rectification: Optional[
PedimentoConfigUpdateRectificationCreate
] = None
pedimento_config_updates: Optional[PedimentoConfigUpdatesCreate] = None
pedimento_packages: Optional[PedimentoPackagesCreate] = None
pedimento_transport_carriers: Optional[list[PedimentoTransportCarrierCreate]] = None
@@ -157,22 +194,28 @@ class PedimentosResponse(PedimentosBase):
id: int
tenant_id: int
created_at: datetime
pedimento_dates: Optional[PedimentoDatesResponse] = None
pedimento_decrementables: Optional[PedimentoDecrementablesResponse] = None
pedimento_incrementables: Optional[PedimentoIncrementablesResponse] = None
pedimento_indexes: Optional[PedimentoIndexesResponse] = None
pedimento_validation: Optional[PedimentoValidationResponse] = None
pedimento_customs_offices: Optional[PedimentoCustomsOfficesResponse] = None
pedimento_validation: Optional[PedimentoValidationResponse] = None
pedimento_customs_offices: Optional[PedimentoCustomsOfficesResponse] = None
pedimento_payments: Optional[PedimentoPaymentsResponse] = None
pedimento_rectification_destination: Optional[PedimentoRectificationDestinationResponse] = None
pedimento_rectification_origin: Optional[PedimentoRectificationOriginResponse] = None
pedimento_transport_means: Optional[PedimentoTransportMeansResponse] = None
pedimento_rectification_destination: Optional[
PedimentoRectificationDestinationResponse
] = None
pedimento_rectification_origin: Optional[PedimentoRectificationOriginResponse] = (
None
)
pedimento_transport_means: Optional[PedimentoTransportMeansResponse] = None
pedimento_config_additional: Optional[PedimentoConfigAdditionalResponse] = None
pedimento_config_calculations: Optional[PedimentoConfigCalculationsResponse] = None
pedimento_config_parameters: Optional[PedimentoConfigParametersResponse] = None
pedimento_config_surcharges: Optional[PedimentoConfigSurchargesResponse] = None
pedimento_config_update_rectification: Optional[PedimentoConfigUpdateRectificationResponse] = None
pedimento_config_update_rectification: Optional[
PedimentoConfigUpdateRectificationResponse
] = None
pedimento_config_updates: Optional[PedimentoConfigUpdatesResponse] = None
pedimento_packages: Optional[PedimentoPackagesResponse] = None
pedimento_transport_carriers: Optional[list[PedimentoTransportCarrierResponse]] = None

View File

@@ -42,7 +42,7 @@ class PedimentoDates(Base, TenantScopedMixin, TimestampMixin):
id: Mapped[int] = mapped_column(Integer)
pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False)
entry_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
entry_date: Mapped[datetime] = mapped_column(DateTime)
pedimento_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
payment_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
rectification_payment_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
@@ -51,7 +51,7 @@ class PedimentoDates(Base, TenantScopedMixin, TimestampMixin):
eucan_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
original_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
start_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
end_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
end_date: Mapped[datetime] = mapped_column(DateTime)
capture_time: Mapped[datetime_time] = mapped_column(Time)

View File

@@ -1,4 +1,5 @@
from decimal import Decimal
from enum import Enum
from typing import TYPE_CHECKING, Optional
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
@@ -80,6 +81,17 @@ if TYPE_CHECKING:
)
class PedimentoType(str, Enum):
NORMAL = "normal"
CONSOLIDATED = "consolidated"
COMPLEMENTARY = "complementary"
AUTOMOBILE = "automobile"
class OperationType(str, Enum):
IMP = "imp" # Importación
EXP = "exp" # Exportación
class Pedimentos(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "pedimentos"
__table_args__ = (
@@ -117,8 +129,8 @@ class Pedimentos(Base, TenantScopedMixin, TimestampMixin):
license: Mapped[str] = mapped_column(String(4))
pedimento_number: Mapped[str] = mapped_column(String(7))
client_id: Mapped[int] = mapped_column(Integer)
operation_type: Mapped[int] = mapped_column(Integer)
pedimento_type: Mapped[str] = mapped_column(String(20))
operation_type: Mapped[OperationType] = mapped_column(String(3))
pedimento_type: Mapped[PedimentoType] = mapped_column(String(20))
pedimento_code: Mapped[str] = mapped_column(String(2))
regime: Mapped[str] = mapped_column(String(3))
status: Mapped[Optional[str]] = mapped_column(String(30))
@@ -129,61 +141,106 @@ class Pedimentos(Base, TenantScopedMixin, TimestampMixin):
observations: Mapped[Optional[str]] = mapped_column(Text)
pedimento_config_additional: Mapped["PedimentoConfigAdditional"] = relationship(
"PedimentoConfigAdditional", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
"PedimentoConfigAdditional",
uselist=False,
back_populates="pedimento",
cascade="all, delete-orphan",
)
pedimento_config_calculations: Mapped["PedimentoConfigCalculations"] = relationship(
"PedimentoConfigCalculations", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
"PedimentoConfigCalculations",
uselist=False,
back_populates="pedimento",
cascade="all, delete-orphan",
)
pedimento_config_parameters: Mapped["PedimentoConfigParameters"] = relationship(
"PedimentoConfigParameters", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
"PedimentoConfigParameters",
uselist=False,
back_populates="pedimento",
cascade="all, delete-orphan",
)
pedimento_config_surcharges: Mapped["PedimentoConfigSurcharges"] = relationship(
"PedimentoConfigSurcharges", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
"PedimentoConfigSurcharges",
uselist=False,
back_populates="pedimento",
cascade="all, delete-orphan",
)
pedimento_config_update_rectification: Mapped[
"PedimentoConfigUpdateRectification"
] = relationship(
"PedimentoConfigUpdateRectification", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
"PedimentoConfigUpdateRectification",
uselist=False,
back_populates="pedimento",
cascade="all, delete-orphan",
)
pedimento_config_updates: Mapped["PedimentoConfigUpdates"] = relationship(
"PedimentoConfigUpdates", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
"PedimentoConfigUpdates",
uselist=False,
back_populates="pedimento",
cascade="all, delete-orphan",
)
pedimento_customs_offices: Mapped["PedimentoCustomsOffices"] = relationship(
"PedimentoCustomsOffices", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
"PedimentoCustomsOffices",
uselist=False,
back_populates="pedimento",
cascade="all, delete-orphan",
)
pedimento_dates: Mapped["PedimentoDates"] = relationship(
"PedimentoDates", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
"PedimentoDates",
uselist=False,
back_populates="pedimento",
cascade="all, delete-orphan",
)
pedimento_decrementables: Mapped["PedimentoDecrementables"] = relationship(
"PedimentoDecrementables", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
"PedimentoDecrementables",
uselist=False,
back_populates="pedimento",
cascade="all, delete-orphan",
)
pedimento_incrementables: Mapped["PedimentoIncrementables"] = relationship(
"PedimentoIncrementables", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
"PedimentoIncrementables",
uselist=False,
back_populates="pedimento",
cascade="all, delete-orphan",
)
pedimento_indexes: Mapped["PedimentoIndexes"] = relationship(
"PedimentoIndexes", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
"PedimentoIndexes",
uselist=False,
back_populates="pedimento",
cascade="all, delete-orphan",
)
pedimento_payments: Mapped["PedimentoPayments"] = relationship(
"PedimentoPayments", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
"PedimentoPayments",
uselist=False,
back_populates="pedimento",
cascade="all, delete-orphan",
)
pedimento_rectification_destination: Mapped["PedimentoRectificationDestination"] = (
relationship(
"PedimentoRectificationDestination",
uselist=False,
back_populates="pedimento",
cascade="all, delete-orphan"
cascade="all, delete-orphan",
)
)
pedimento_rectification_origin: Mapped["PedimentoRectificationOrigin"] = (
relationship(
"PedimentoRectificationOrigin", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
"PedimentoRectificationOrigin",
uselist=False,
back_populates="pedimento",
cascade="all, delete-orphan",
)
)
pedimento_transport_means: Mapped["PedimentoTransportMeans"] = relationship(
"PedimentoTransportMeans", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
"PedimentoTransportMeans",
uselist=False,
back_populates="pedimento",
cascade="all, delete-orphan",
)
pedimento_validation: Mapped["PedimentoValidation"] = relationship(
"PedimentoValidation", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
"PedimentoValidation",
uselist=False,
back_populates="pedimento",
cascade="all, delete-orphan",
)
pedimento_packages: Mapped["PedimentoPackages"] = relationship(
"PedimentoPackages", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"

View File

@@ -55,7 +55,7 @@ seed = [
("LTT", "LITAS", "LITUANIA"),
("LYD", "DINAR", "LIBIA"),
("MAD", "DIRHAM", "MARRUECOS"),
("MXP", "PESO", "MEXICO"),
("MXN", "PESO", "MEXICO"),
("MYR", "RINGGIT", "MALASIA"),
("NGN", "NAIRA", "NIGERIA (FED)"),
("NIC", "CORDOBA", "NICARAGUA"),