diff --git a/backend/api/v1/modules/a76/customs_brokers/dto.py b/backend/api/v1/modules/a76/customs_brokers/dto.py index 07923e14..fa11b268 100644 --- a/backend/api/v1/modules/a76/customs_brokers/dto.py +++ b/backend/api/v1/modules/a76/customs_brokers/dto.py @@ -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 diff --git a/backend/api/v1/modules/a76/general_catalogs/exchange_rate/routes.py b/backend/api/v1/modules/a76/general_catalogs/exchange_rate/routes.py index 546ead36..00870adf 100644 --- a/backend/api/v1/modules/a76/general_catalogs/exchange_rate/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/exchange_rate/routes.py @@ -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, + } diff --git a/backend/api/v1/modules/a76/general_catalogs/exchange_rate/services.py b/backend/api/v1/modules/a76/general_catalogs/exchange_rate/services.py index 469b212f..f7749b43 100644 --- a/backend/api/v1/modules/a76/general_catalogs/exchange_rate/services.py +++ b/backend/api/v1/modules/a76/general_catalogs/exchange_rate/services.py @@ -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() diff --git a/backend/api/v1/modules/a76/invoices/common/common_validators.py b/backend/api/v1/modules/a76/invoices/common/common_validators.py new file mode 100644 index 00000000..d0c0bbf8 --- /dev/null +++ b/backend/api/v1/modules/a76/invoices/common/common_validators.py @@ -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}'", + ) \ No newline at end of file diff --git a/backend/api/v1/modules/a76/invoices/common/create_validators.py b/backend/api/v1/modules/a76/invoices/common/create_validators.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/backend/api/v1/modules/a76/invoices/common/create_validators.py @@ -0,0 +1 @@ + diff --git a/backend/api/v1/modules/a76/invoices/common/mappers.py b/backend/api/v1/modules/a76/invoices/common/mappers.py new file mode 100644 index 00000000..3bea0dd9 --- /dev/null +++ b/backend/api/v1/modules/a76/invoices/common/mappers.py @@ -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 + diff --git a/backend/api/v1/modules/a76/invoices/imports/temporary/validators/common.py b/backend/api/v1/modules/a76/invoices/imports/temporary/validators/common.py new file mode 100644 index 00000000..4e3888ea --- /dev/null +++ b/backend/api/v1/modules/a76/invoices/imports/temporary/validators/common.py @@ -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, + ) + + diff --git a/backend/api/v1/modules/a76/invoices/imports/temporary/validators/create.py b/backend/api/v1/modules/a76/invoices/imports/temporary/validators/create.py new file mode 100644 index 00000000..be6f6fc1 --- /dev/null +++ b/backend/api/v1/modules/a76/invoices/imports/temporary/validators/create.py @@ -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" + + + + + + + + + + \ No newline at end of file diff --git a/backend/api/v1/modules/a76/invoices/imports/temporary/validators/update.py b/backend/api/v1/modules/a76/invoices/imports/temporary/validators/update.py new file mode 100644 index 00000000..57877ae4 --- /dev/null +++ b/backend/api/v1/modules/a76/invoices/imports/temporary/validators/update.py @@ -0,0 +1,2 @@ +def validate_update(): + pass \ No newline at end of file diff --git a/backend/api/v1/modules/a76/invoices/models.py b/backend/api/v1/modules/a76/invoices/models.py index 10d22c0c..3bb23271 100644 --- a/backend/api/v1/modules/a76/invoices/models.py +++ b/backend/api/v1/modules/a76/invoices/models.py @@ -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") diff --git a/backend/api/v1/modules/a76/invoices/schemas.py b/backend/api/v1/modules/a76/invoices/schemas.py index 16c77e70..8c8a3a93 100644 --- a/backend/api/v1/modules/a76/invoices/schemas.py +++ b/backend/api/v1/modules/a76/invoices/schemas.py @@ -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): diff --git a/backend/api/v1/modules/a76/invoices/services.py b/backend/api/v1/modules/a76/invoices/services.py index 478bdae8..e911ed9f 100644 --- a/backend/api/v1/modules/a76/invoices/services.py +++ b/backend/api/v1/modules/a76/invoices/services.py @@ -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 \ No newline at end of file + return False diff --git a/backend/api/v1/modules/a76/items/line_items/schemas.py b/backend/api/v1/modules/a76/items/line_items/schemas.py index c490352f..4921233f 100644 --- a/backend/api/v1/modules/a76/items/line_items/schemas.py +++ b/backend/api/v1/modules/a76/items/line_items/schemas.py @@ -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 diff --git a/backend/api/v1/modules/a76/items/models.py b/backend/api/v1/modules/a76/items/models.py index eb9c57a4..6cb67175 100644 --- a/backend/api/v1/modules/a76/items/models.py +++ b/backend/api/v1/modules/a76/items/models.py @@ -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 # ============================================================================ diff --git a/backend/api/v1/modules/a76/items/service.py b/backend/api/v1/modules/a76/items/service.py index d8a30b99..74ba2da1 100644 --- a/backend/api/v1/modules/a76/items/service.py +++ b/backend/api/v1/modules/a76/items/service.py @@ -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)) diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_dates.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_dates.py index 943c293d..b1c1b633 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_dates.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_dates.py @@ -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): diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimentos.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimentos.py index 00acb60d..4767f77d 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimentos.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimentos.py @@ -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 diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_dates.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_dates.py index 495acb43..2a43a656 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_dates.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_dates.py @@ -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) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimentos.py b/backend/api/v1/modules/a76/pedmientos/models/pedimentos.py index 2ee63de2..f97600dc 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimentos.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimentos.py @@ -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" diff --git a/backend/api/v1/modules/public/reference_data/currency_types/seed.py b/backend/api/v1/modules/public/reference_data/currency_types/seed.py index 3385ec31..86262be9 100644 --- a/backend/api/v1/modules/public/reference_data/currency_types/seed.py +++ b/backend/api/v1/modules/public/reference_data/currency_types/seed.py @@ -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"), diff --git a/backend/core/error_handlers.py b/backend/core/error_handlers.py new file mode 100644 index 00000000..91f49ee1 --- /dev/null +++ b/backend/core/error_handlers.py @@ -0,0 +1,175 @@ +""" +Manejadores globales de excepciones para FastAPI +""" + +import logging +from typing import Any, Dict + +from fastapi import Request, status +from fastapi.responses import JSONResponse +from fastapi.exceptions import RequestValidationError +from sqlalchemy.exc import IntegrityError, SQLAlchemyError + +from .exceptions import BaseAPIException + +logger = logging.getLogger(__name__) + + +async def base_exception_handler( + request: Request, + exc: BaseAPIException, +) -> JSONResponse: + """ + Manejador para todas las excepciones personalizadas de la API + """ + logger.warning( + f"API Exception: {exc.error_code} - {exc.message}", + extra={ + "path": request.url.path, + "method": request.method, + "status_code": exc.status_code, + }, + ) + + return JSONResponse( + status_code=exc.status_code, + content=exc.to_dict(), + ) + + +async def validation_exception_handler( + request: Request, + exc: RequestValidationError, +) -> JSONResponse: + """ + Manejador para errores de validación de Pydantic/FastAPI + """ + errors = [] + for error in exc.errors(): + field = ".".join(str(loc) for loc in error["loc"] if loc != "body") + errors.append( + { + "field": field, + "message": error["msg"], + "type": error["type"], + } + ) + + logger.warning( + f"Validation Error en {request.url.path}", + extra={"errors": errors}, + ) + + return JSONResponse( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + content={ + "error": "VALIDATION_ERROR", + "message": "Error de validación en los datos recibidos", + "status_code": status.HTTP_422_UNPROCESSABLE_ENTITY, + "errors": errors, + }, + ) + + +async def integrity_error_handler( + request: Request, + exc: IntegrityError, +) -> JSONResponse: + """ + Manejador para errores de integridad de la base de datos + """ + logger.error( + f"Database Integrity Error: {str(exc.orig)}", + extra={ + "path": request.url.path, + "method": request.method, + }, + ) + + # Intentar extraer información útil del error + error_message = "Error de integridad en la base de datos" + + orig_msg = str(exc.orig).lower() + if "unique constraint" in orig_msg or "duplicate key" in orig_msg: + error_message = "El registro ya existe. Verifica los campos únicos." + elif "foreign key" in orig_msg: + error_message = "Referencia inválida a otro registro." + elif "not null" in orig_msg: + error_message = "Falta un campo requerido." + + return JSONResponse( + status_code=status.HTTP_409_CONFLICT, + content={ + "error": "DATABASE_INTEGRITY_ERROR", + "message": error_message, + "status_code": status.HTTP_409_CONFLICT, + }, + ) + + +async def sqlalchemy_error_handler( + request: Request, + exc: SQLAlchemyError, +) -> JSONResponse: + """ + Manejador para errores generales de SQLAlchemy + """ + logger.error( + f"Database Error: {str(exc)}", + extra={ + "path": request.url.path, + "method": request.method, + }, + exc_info=True, + ) + + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={ + "error": "DATABASE_ERROR", + "message": "Error en la operación de base de datos", + "status_code": status.HTTP_500_INTERNAL_SERVER_ERROR, + }, + ) + + +async def general_exception_handler( + request: Request, + exc: Exception, +) -> JSONResponse: + """ + Manejador para excepciones no capturadas + """ + logger.error( + f"Unhandled Exception: {str(exc)}", + extra={ + "path": request.url.path, + "method": request.method, + }, + exc_info=True, + ) + + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={ + "error": "INTERNAL_SERVER_ERROR", + "message": "Error interno del servidor", + "status_code": status.HTTP_500_INTERNAL_SERVER_ERROR, + }, + ) + + +def register_exception_handlers(app) -> None: + """ + Registra todos los manejadores de excepciones en la aplicación FastAPI + + Args: + app: Instancia de FastAPI + """ + app.add_exception_handler(BaseAPIException, base_exception_handler) + app.add_exception_handler(RequestValidationError, validation_exception_handler) + app.add_exception_handler(IntegrityError, integrity_error_handler) + app.add_exception_handler(SQLAlchemyError, sqlalchemy_error_handler) + app.add_exception_handler(Exception, general_exception_handler) + + logger.info("Exception handlers registered successfully") diff --git a/backend/core/exceptions.py b/backend/core/exceptions.py new file mode 100644 index 00000000..e53d20fe --- /dev/null +++ b/backend/core/exceptions.py @@ -0,0 +1,300 @@ +""" +Sistema centralizado de excepciones personalizadas para Anexo76 +""" + +from typing import Optional, List, Dict, Any +from fastapi import status + + +class BaseAPIException(Exception): + """Excepción base para todas las excepciones de la API""" + + def __init__( + self, + message: str, + status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR, + errors: Optional[List[Dict[str, Any]]] = None, + error_code: Optional[str] = None, + ): + self.message = message + self.status_code = status_code + self.errors = errors or [] + self.error_code = error_code or self.__class__.__name__ + super().__init__(self.message) + + def to_dict(self) -> Dict[str, Any]: + """Convierte la excepción a un diccionario para respuesta JSON""" + response = { + "error": self.error_code, + "message": self.message, + "status_code": self.status_code, + } + if self.errors: + response["errors"] = self.errors + return response + + +class ValidationException(BaseAPIException): + """Excepción para errores de validación""" + + def __init__( + self, + message: str = "Error de validación", + errors: Optional[List[Dict[str, Any]]] = None, + ): + super().__init__( + message=message, + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + errors=errors, + error_code="VALIDATION_ERROR", + ) + + +class DuplicateResourceException(BaseAPIException): + """Excepción cuando se intenta crear un recurso duplicado""" + + def __init__( + self, + resource: str, + identifier: str, + message: Optional[str] = None, + ): + self.resource = resource + self.identifier = identifier + final_message = ( + message or f"{resource} con identificador '{identifier}' ya existe" + ) + super().__init__( + message=final_message, + status_code=status.HTTP_409_CONFLICT, + error_code="DUPLICATE_RESOURCE", + ) + + +class ResourceNotFoundException(BaseAPIException): + """Excepción cuando no se encuentra un recurso""" + + def __init__( + self, + resource: str, + identifier: str, + message: Optional[str] = None, + ): + self.resource = resource + self.identifier = identifier + final_message = ( + message or f"{resource} con identificador '{identifier}' no encontrado" + ) + super().__init__( + message=final_message, + status_code=status.HTTP_404_NOT_FOUND, + error_code="RESOURCE_NOT_FOUND", + ) + + +class UnauthorizedException(BaseAPIException): + """Excepción para errores de autenticación""" + + def __init__(self, message: str = "No autorizado"): + super().__init__( + message=message, + status_code=status.HTTP_401_UNAUTHORIZED, + error_code="UNAUTHORIZED", + ) + + +class ForbiddenException(BaseAPIException): + """Excepción para errores de permisos""" + + def __init__(self, message: str = "Acceso prohibido"): + super().__init__( + message=message, + status_code=status.HTTP_403_FORBIDDEN, + error_code="FORBIDDEN", + ) + + +class BusinessRuleException(BaseAPIException): + """Excepción para errores de reglas de negocio""" + + def __init__( + self, + message: str, + errors: Optional[List[Dict[str, Any]]] = None, + ): + super().__init__( + message=message, + status_code=status.HTTP_400_BAD_REQUEST, + errors=errors, + error_code="BUSINESS_RULE_ERROR", + ) + + +class DatabaseException(BaseAPIException): + """Excepción para errores de base de datos""" + + def __init__(self, message: str = "Error en la base de datos"): + super().__init__( + message=message, + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + error_code="DATABASE_ERROR", + ) + + +class ErrorCollector: + """ + Colector de errores para acumular múltiples errores de validación + antes de lanzar una excepción + + Uso: + collector = ErrorCollector() + + if not valid_email: + collector.add_error("email", "Email inválido", "INVALID_EMAIL") + + if not valid_phone: + collector.add_error("phone", "Teléfono inválido", "INVALID_PHONE") + + collector.raise_if_errors() # Lanza ValidationException si hay errores + """ + + def __init__(self): + self._errors: List[Dict[str, Any]] = [] + + def add_error( + self, + field: str, + message: str, + solution: Optional[List[str]], + code: Optional[str] = None, + value: Optional[Any] = None, + ) -> "ErrorCollector": + """ + Agrega un error al colector + + Args: + field: Campo donde ocurrió el error (ej: "invoice_number", "email") + message: Mensaje descriptivo del error + code: Código opcional del error (ej: "REQUIRED", "INVALID_FORMAT") + value: Valor que causó el error (opcional) + + Returns: + Self para permitir encadenamiento + """ + error = { + "field": field, + "message": message, + } + if solution: + error["solution"] = solution + if code: + error["code"] = code + if value is not None: + error["value"] = value + + self._errors.append(error) + return self + + def add_field_error( + self, + field: str, + message: str, + code: str = "INVALID", + ) -> "ErrorCollector": + """Atajo para agregar error de campo""" + return self.add_error(field, message, solution=None, code=code) + + def add_required_error(self, field: str) -> "ErrorCollector": + """Atajo para agregar error de campo requerido""" + return self.add_error( + field, f"El campo '{field}' es requerido", solution=None, code="REQUIRED" + ) + + def add_duplicate_error( + self, + field: str, + value: Any, + message: Optional[str] = None, + ) -> "ErrorCollector": + """Atajo para agregar error de duplicado""" + final_message = ( + message or f"El valor '{value}' ya existe para el campo '{field}'" + ) + return self.add_error( + field, final_message, solution=None, code="DUPLICATE", value=value + ) + + def add_invalid_format_error( + self, + field: str, + expected_format: str, + ) -> "ErrorCollector": + """Atajo para agregar error de formato inválido""" + return self.add_error( + field, + f"Formato inválido. Se esperaba: {expected_format}", + solution=None, + code="INVALID_FORMAT", + ) + + def add_range_error( + self, + field: str, + min_value: Optional[Any] = None, + max_value: Optional[Any] = None, + ) -> "ErrorCollector": + """Atajo para agregar error de rango""" + if min_value is not None and max_value is not None: + message = f"El valor debe estar entre {min_value} y {max_value}" + elif min_value is not None: + message = f"El valor debe ser mayor o igual a {min_value}" + elif max_value is not None: + message = f"El valor debe ser menor o igual a {max_value}" + else: + message = "Valor fuera de rango" + + return self.add_error(field, message, solution=None, code="OUT_OF_RANGE") + + def has_errors(self) -> bool: + """Verifica si hay errores acumulados""" + return len(self._errors) > 0 + + def get_errors(self) -> List[Dict[str, Any]]: + """Obtiene la lista de errores""" + return self._errors.copy() + + def get_error_count(self) -> int: + """Obtiene el número de errores""" + return len(self._errors) + + def clear(self) -> "ErrorCollector": + """Limpia todos los errores""" + self._errors.clear() + return self + + def raise_if_errors( + self, + message: str = "Se encontraron errores de validación", + ) -> None: + """ + Lanza ValidationException si hay errores acumulados + + Args: + message: Mensaje principal de la excepción + + Raises: + ValidationException: Si hay errores acumulados + """ + if self.has_errors(): + raise ValidationException(message=message, errors=self._errors) + + def __bool__(self) -> bool: + """Permite usar el colector en contextos booleanos""" + return self.has_errors() + + def __len__(self) -> int: + """Permite usar len() en el colector""" + return self.get_error_count() + + def __repr__(self) -> str: + return f"ErrorCollector(errors={self.get_error_count()})" diff --git a/backend/main.py b/backend/main.py index f0fee380..c497cc46 100644 --- a/backend/main.py +++ b/backend/main.py @@ -8,6 +8,7 @@ import logging from api.v1.router import router as api_v1_router from core.config import settings from core.database import init_db +from core.error_handlers import register_exception_handlers from core.middleware import ( LicenseValidationMiddleware, RequestLoggingMiddleware, @@ -18,8 +19,12 @@ from fastapi.middleware.cors import CORSMiddleware from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse -from api.v1.modules.a76.items.models import Item # Importar rutas para registrar con el router -from api.v1.modules.a76.items.series.models import Serie # Importar modelos para registrar con SQLAlchemy +from api.v1.modules.a76.items.models import ( + Item, +) # Importar rutas para registrar con el router +from api.v1.modules.a76.items.series.models import ( + Serie, +) # Importar modelos para registrar con SQLAlchemy # Configurar logging logging.basicConfig( @@ -39,6 +44,9 @@ app = FastAPI( openapi_url="/api/openapi.json" if settings.DEBUG else None, ) +# Registrar manejadores de excepciones +register_exception_handlers(app) + # Add validation error handler @app.exception_handler(RequestValidationError) diff --git a/frontend/src/lib/api/dashboard/a76/customs-brokers.ts b/frontend/src/lib/api/dashboard/a76/customs-brokers.ts index da400dbc..a3faa96e 100644 --- a/frontend/src/lib/api/dashboard/a76/customs-brokers.ts +++ b/frontend/src/lib/api/dashboard/a76/customs-brokers.ts @@ -21,6 +21,26 @@ export interface CustomsBroker { contact?: string | null; tenant_id: string; company_id: string; + id: number; + type?: string | null; + broker_key: string; + name?: string | null; + address?: string | null; + postal_code?: string | null; + city?: string | null; + state?: string | null; + phone?: string | null; + fax?: string | null; + email?: string | null; + country?: string | null; + tax_id?: string | null; + personal_id?: string | null; + position?: string | null; + license: string; + company?: string | null; + contact?: string | null; + tenant_id: string; + company_id: string; } export interface CustomsBrokerVU { @@ -104,6 +124,20 @@ export const customsBrokersApi = { delete: (brokerKey: string, companyId: string) => { return api.delete(`/v1/a76/customs-brokers/${brokerKey}?company_id=${companyId}`); }, + /** + * Elimina un agente aduanal + */ + delete: (brokerKey: string, companyId: string) => { + return api.delete(`/v1/a76/customs-brokers/${brokerKey}/?company_id=${companyId}`); + }, + + /** + * Actualiza la información de un agente aduanal + */ + update: (brokerKey: string, data: CreateCustomsBrokerData) => { + const companyId = data.company_id; + return api.put(`/v1/a76/customs-brokers/${brokerKey}/?company_id=${companyId}`, data); + }, updateVU: (brokerKey: string, data: CustomsBrokerVU, companyId: string) => { return api.put(`/v1/a76/customs-broker-vu/${brokerKey}?company_id=${companyId}`, data); diff --git a/frontend/src/lib/api/dashboard/a76/exchange-rate.ts b/frontend/src/lib/api/dashboard/a76/exchange-rate.ts index 89dae9dc..40fc3657 100644 --- a/frontend/src/lib/api/dashboard/a76/exchange-rate.ts +++ b/frontend/src/lib/api/dashboard/a76/exchange-rate.ts @@ -26,20 +26,13 @@ export interface ExchangeRateListResponse { */ export async function getExchangeRateByDate(date: string, companyId: number): Promise { try { - // Get all exchange rates and filter by date on client side - const response = await api.get(`/v1/a76/exchange-rate/?company_id=${companyId}`); + const dateOnly = date.split('T')[0]; // Ensure YYYY-MM-DD + // Filter by date on server side + const response = await api.get(`/v1/a76/exchange-rate/?company_id=${companyId}&date=${dateOnly}`); if (response.data && response.data.items && response.data.items.length > 0) { - // Filter by date and find USD exchange rate - const dateOnly = date.split('T')[0]; // Get YYYY-MM-DD part - - const matchingRates = response.data.items.filter(rate => { - const rateDate = rate.date.split('T')[0]; - const matches = rateDate === dateOnly && rate.foreign_currency === 'USD'; - return matches; - }); - - return matchingRates.length > 0 ? matchingRates[0] : null; + // Find USD exchange rate (backend might return multiple currencies for same date if they exist) + return response.data.items[0]; } return null; diff --git a/frontend/src/lib/api/dashboard/a76/invoices.ts b/frontend/src/lib/api/dashboard/a76/invoices.ts index 7dbb1c0d..4af6238b 100644 --- a/frontend/src/lib/api/dashboard/a76/invoices.ts +++ b/frontend/src/lib/api/dashboard/a76/invoices.ts @@ -11,31 +11,31 @@ export type TransportType = 'none' | 'transport' | 'box' | 'licence plates' | 't export interface InvoiceComplianceMx { invoice_id?: number; - pedimento?: string | null; - pedimento_code?: string | null; - pedimento_k1?: string | null; + pedimento_id?: number | null; + pedimento_r1?: number | null; + pedimento_k1?: number | null; remesa?: number | null; aduana?: string | null; port_of_entry?: string | null; destination?: string | null; manifest_number?: string | null; provider_header?: string | null; - provider_id?: string | null; + provider_id?: number | null; sold_to_header?: string | null; - sold_to_id?: string | null; + sold_to_id?: number | null; shipped_to_header?: string | null; - shipped_to_id?: string | null; + shipped_to_id?: number | null; shipped_by_header?: string | null; - shipped_by_id?: string | null; - customs_broker_id?: string | null; - customs_broker_us_id?: string | null; + shipped_by_id?: number | null; + customs_broker_id?: number | null; + customs_broker_us_id?: number | null; broker_invoice_num?: string | null; broker_invoice_date?: string | null; is_mixed?: boolean | null; waste_type?: string | null; scrap_type?: string | null; appendix_17?: number | null; - is_regime_change?: string | null; + is_regime_change?: boolean | null; which_exchange_rate?: string | null; value_method?: string | null; act_value?: string | null; @@ -179,6 +179,7 @@ export interface Invoice { system?: string | null; operation_type?: OperationType | null; invoice_type?: string | null; + document_type?: string | null; invoice_number?: string | null; project_number?: string | null; purchase_order?: string | null; @@ -195,8 +196,8 @@ export interface Invoice { capture_user?: string | null; traffic_light_status?: string | null; process_log?: string | null; - status_rec?: number | null; - status_rep?: string | null; + is_updated_rec?: number | null; + is_updated_rep?: string | null; observation_es?: string | null; observation_en?: string | null; comments_status?: string | null; @@ -206,9 +207,9 @@ export interface Invoice { path_xml?: string | null; subcompany?: string | null; party_count?: number | null; - generate_id?: string | null; + generate_id?: boolean | null; generate_desc_parties?: string | null; - apply_manual_discount?: string | null; + apply_manual_discount?: boolean | null; is_bulk?: boolean | null; download_substance?: boolean | null; download_class?: boolean | null; @@ -235,6 +236,7 @@ export interface CreateInvoiceData { system: string; operation_type: OperationType; invoice_type: string; + document_type: string; invoice_number: string; project_number?: string | null; purchase_order?: string | null; @@ -250,8 +252,8 @@ export interface CreateInvoiceData { capture_user?: string | null; traffic_light_status?: string | null; process_log?: string | null; - status_rec?: number | null; - status_rep?: string | null; + is_updated_rec?: number | null; + is_updated_rep?: string | null; observation_es?: string | null; observation_en?: string | null; comments_status?: string | null; @@ -261,9 +263,9 @@ export interface CreateInvoiceData { path_xml?: string | null; subcompany?: string | null; party_count?: number | null; - generate_id?: string | null; + generate_id?: boolean | null; generate_desc_parties?: string | null; - apply_manual_discount?: string | null; + apply_manual_discount?: boolean | null; is_bulk?: boolean | null; download_substance?: boolean | null; download_class?: boolean | null; @@ -282,6 +284,7 @@ export interface CreateInvoiceData { export interface UpdateInvoiceData { operation_type?: OperationType | null; invoice_type?: string | null; + document_type?: string | null; invoice_number?: string | null; project_number?: string | null; purchase_order?: string | null; @@ -355,7 +358,7 @@ export const invoicesApi = { const params = new URLSearchParams({ company_id: companyId.toString() }); - return api.put(`/v1/a76/invoices/${invoiceId}?${params.toString()}`, data); + return api.put(`/v1/a76/invoices/${invoiceId}/?${params.toString()}`, data); }, /** diff --git a/frontend/src/lib/api/dashboard/a76/items.ts b/frontend/src/lib/api/dashboard/a76/items.ts index e474411c..3fe23462 100644 --- a/frontend/src/lib/api/dashboard/a76/items.ts +++ b/frontend/src/lib/api/dashboard/a76/items.ts @@ -6,6 +6,115 @@ import { api } from '$lib/api'; // --- Interfaces --- +// --- Nested Interfaces for Line Items --- + +export interface LineCustoms { + id?: number; + line_item_id?: number; + fraction?: string; + fraction_type?: string; + american_fraction?: string; + origin_country?: string; + destination_country?: string; + advalorem?: string; + advalorem_american?: number; // Backend: Decimal + sector?: string; +} + +export interface LineFinancials { + id?: number; + line_item_id?: number; + // Costs + unit_cost_usd?: number; + unit_cost_mxn?: number; + unit_cost_capture?: number; + unit_cost_commercial_usd?: number; + + // Values + value_usd?: number; + value_mxn?: number; + value_returned_usd?: number; + value_returned_mxn?: number; + customs_value_usd?: number; +} + +export interface LineQuantities { + id?: number; + line_item_id?: number; + quantity?: number; + unit_of_measure?: string; + + // Special quantities + quantity_temp_export?: number; + quantity_returned?: number; + + // Weight + net_weight?: number; + gross_weight?: number; + + // Packaging + package_key?: string; + package_quantity?: number; + package_description?: string; +} + +export interface LineDescriptions { + id?: number; + line_item_id?: number; + description_spanish?: string; + description_english?: string; + extra_description?: string; + additional_info_spanish?: string; + brand?: string; + model?: string; + has_serial?: boolean; +} + +export interface LineReferences { + id?: number; + line_item_id?: number; + serie_id?: number; +} + +export interface LineItem { + id?: number; + item_id?: number; + line_number: number; + + // Identification + part_number?: string; + component_part_number?: string; + class_code?: string; + identifier?: string; + + // Unit of Measure + unit_of_measure?: string; + alternate_unit?: string; + + // Permits + permit_number?: string; + page_line?: string; + has_certificate?: boolean; + certificate_number?: string; + + // Flags + is_subitem?: boolean; + includes_subitems?: boolean; + tax_payment?: boolean; + + // Payment + payment_method?: string; + igi_amount?: number; + + + // Nested relations (Singular names to match backend Pydantic models) + customs?: LineCustoms; + financial?: LineFinancials; + quantity?: LineQuantities; + description?: LineDescriptions; + reference?: LineReferences; +} + export interface Item { id?: number; invoice_id: number; @@ -18,6 +127,8 @@ export interface Item { location?: string; created_at?: string; updated_at?: string; + + lines?: LineItem[]; } export interface ItemListResponse { @@ -36,6 +147,7 @@ export interface CreateItemData { rectification?: number; warehouse?: string; location?: string; + lines?: LineItem[]; } export interface UpdateItemData { @@ -46,6 +158,7 @@ export interface UpdateItemData { rectification?: boolean; warehouse?: string; location?: string; + lines?: LineItem[]; } /** diff --git a/frontend/src/lib/api/dashboard/a76/pedimentos.ts b/frontend/src/lib/api/dashboard/a76/pedimentos.ts index bf810449..574c447f 100644 --- a/frontend/src/lib/api/dashboard/a76/pedimentos.ts +++ b/frontend/src/lib/api/dashboard/a76/pedimentos.ts @@ -161,7 +161,7 @@ export interface Pedimento { license?: string | null; pedimento_number?: string | null; client_id?: number | null; - operation_type?: number | null; + operation_type?: string | null; pedimento_type?: string | null; pedimento_code?: string | null; regime?: string | null; @@ -202,7 +202,7 @@ export interface CreatePedimentoData { license?: string | null; pedimento_number?: string | null; client_id?: number | null; - operation_type?: number | null; + operation_type?: string | null; pedimento_type?: string | null; pedimento_code?: string | null; regime?: string | null; @@ -235,7 +235,7 @@ export interface UpdatePedimentoData { license?: string | null; pedimento_number?: string | null; client_id?: number | null; - operation_type?: number | null; + operation_type?: string | null; pedimento_type?: string | null; pedimento_code?: string | null; regime?: string | null; diff --git a/frontend/src/lib/components/dashboard/invoices/columns.ts b/frontend/src/lib/components/dashboard/invoices/columns.ts index bee37d2d..c087fae8 100644 --- a/frontend/src/lib/components/dashboard/invoices/columns.ts +++ b/frontend/src/lib/components/dashboard/invoices/columns.ts @@ -100,20 +100,6 @@ export function createColumns(onSuccess?: () => void): ColumnDef[] { return renderSnippet(typeSnippet, { label, colorClass }); } }, - { - accessorKey: "invoice_number", - header: "Número de Factura", - cell: ({ row }) => { - const numberSnippet = createRawSnippet<[{ number?: string | null }]>((getNumber) => { - const { number } = getNumber(); - return { - render: () => - `${number || 'N/A'}` - }; - }); - return renderSnippet(numberSnippet, { number: row.original.invoice_number }); - } - }, { accessorKey: "invoice_type", header: "Tipo", @@ -128,6 +114,20 @@ export function createColumns(onSuccess?: () => void): ColumnDef[] { return renderSnippet(typeSnippet, { type: row.original.invoice_type }); } }, + { + accessorKey: "invoice_number", + header: "Número de Factura", + cell: ({ row }) => { + const numberSnippet = createRawSnippet<[{ number?: string | null }]>((getNumber) => { + const { number } = getNumber(); + return { + render: () => + `${number || 'N/A'}` + }; + }); + return renderSnippet(numberSnippet, { number: row.original.invoice_number }); + } + }, { accessorKey: "project_number", header: "Proyecto", diff --git a/frontend/src/lib/components/dashboard/invoices/edit/continuation-tab-form.svelte b/frontend/src/lib/components/dashboard/invoices/edit/continuation-tab-form.svelte index 63d613fd..efd1ed4f 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/continuation-tab-form.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/continuation-tab-form.svelte @@ -16,56 +16,9 @@ exists?: boolean; } = $props(); - if (!formData && invoice?.financials) { + if (!formData && invoice) { formData = { - // Currency & Exchange - currency: invoice.financials.currency || '', - currency_type: invoice.financials.currency_type || '', - exchange_rate: invoice.financials.exchange_rate || null, - exchange_rate_mm: invoice.financials.exchange_rate_mm || null, - // Values - value_mn: invoice.financials.value_mn || null, - value_me: invoice.financials.value_me || null, - value_mc: invoice.financials.value_mc || null, - customs_value_mn: invoice.financials.customs_value_mn || null, - customs_value_me: invoice.financials.customs_value_me || null, - // Raw materials - raw_material_value_mn: invoice.financials.raw_material_value_mn || null, - raw_material_value_me: invoice.financials.raw_material_value_me || null, - // Aggregate values - aggregate_value_mn: invoice.financials.aggregate_value_mn || null, - aggregate_value_me: invoice.financials.aggregate_value_me || null, - aggregate_value_mc: invoice.financials.aggregate_value_mc || null, - // Mexican values - mexican_value_mn: invoice.financials.mexican_value_mn || null, - mexican_value_me: invoice.financials.mexican_value_me || null, - mexican_value_mc: invoice.financials.mexican_value_mc || null, - // National packaging - national_packaging_mn: invoice.financials.national_packaging_mn || null, - national_packaging_me: invoice.financials.national_packaging_me || null, - national_packaging_mc: invoice.financials.national_packaging_mc || null, - // Costs & increments - freight: invoice.financials.freight || null, - insurance: invoice.financials.insurance || null, - insurance_value: invoice.financials.insurance_value || null, - packaging: invoice.financials.packaging || null, - other_increments: invoice.financials.other_increments || null, - total_increments_mn: invoice.financials.total_increments_mn || null, - total_increments_me: invoice.financials.total_increments_me || null, - // Taxes - iva_mn: invoice.financials.iva_mn || null, - iva_me: invoice.financials.iva_me || null, - iva_mc: invoice.financials.iva_mc || null, - iva_factor: invoice.financials.iva_factor || '', - tax_value_me: invoice.financials.tax_value_me || null, - seal_value_2500: invoice.financials.seal_value_2500 || false, - // Weights & quantities - total_quantity: invoice.financials.total_quantity || null, - gross_weight: invoice.financials.gross_weight || null, - net_weight: invoice.financials.net_weight || null, - bundle_count: invoice.financials.bundle_count || null, - weight_factor: invoice.financials.weight_factor || null, - // Additional fields not in backend + // Campos de esta pestaña numero_tipo_transporte: '', es_ferrocarril: 'no', numero_bl: '', @@ -88,54 +41,7 @@ exists = true; } else if (!formData) { formData = { - // Currency & Exchange - currency: '', - currency_type: '', - exchange_rate: null, - exchange_rate_mm: null, - // Values - value_mn: null, - value_me: null, - value_mc: null, - customs_value_mn: null, - customs_value_me: null, - // Raw materials - raw_material_value_mn: null, - raw_material_value_me: null, - // Aggregate values - aggregate_value_mn: null, - aggregate_value_me: null, - aggregate_value_mc: null, - // Mexican values - mexican_value_mn: null, - mexican_value_me: null, - mexican_value_mc: null, - // National packaging - national_packaging_mn: null, - national_packaging_me: null, - national_packaging_mc: null, - // Costs & increments - freight: null, - insurance: null, - insurance_value: null, - packaging: null, - other_increments: null, - total_increments_mn: null, - total_increments_me: null, - // Taxes - iva_mn: null, - iva_me: null, - iva_mc: null, - iva_factor: '', - tax_value_me: null, - seal_value_2500: false, - // Weights & quantities - total_quantity: null, - gross_weight: null, - net_weight: null, - bundle_count: null, - weight_factor: null, - // Additional fields not in backend + // Campos de esta pestaña numero_tipo_transporte: '', es_ferrocarril: 'no', numero_bl: '', diff --git a/frontend/src/lib/components/dashboard/invoices/edit/general-tab-form.svelte b/frontend/src/lib/components/dashboard/invoices/edit/general-tab-form.svelte index 73c5fd42..b5aa7eed 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/general-tab-form.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/general-tab-form.svelte @@ -24,7 +24,9 @@ customsSections = [], codePedimentoRegimens = [], defaultOperationType = undefined, - defaultInvoiceType = undefined + defaultInvoiceType = undefined, + operationType = undefined, + exchangeRate = undefined }: { invoice: Invoice | null; formData?: any; @@ -40,50 +42,38 @@ trailers?: any[]; customsSections?: any[]; codePedimentoRegimens?: any[]; - defaultOperationType?: number | null; + defaultOperationType?: string | null; defaultInvoiceType?: string | null; - } = $props(); + operationType?: number | null; + exchangeRate?: number | null; + } = $props(); + + // Sync exchangeRate prop to formData + $effect(() => { + if (exchangeRate !== undefined && formData) { + formData.exchange_rate = exchangeRate; + } + }); if (!formData) { if (invoice) { // Editando una factura existente - let operationType: number | null = null; - if (invoice.operation_type) { - operationType = invoice.operation_type === 'exp' ? 1 : 2; - } - formData = { - // TOP fields - is_pedimento_pending: false, - pedimento: invoice.compliance_mx?.pedimento || '', - remesa: invoice.compliance_mx?.remesa || '', - invoice_number: invoice.invoice_number || '', - invoice_date: invoice.invoice_date || '', - emission_date: '', - - // Extra fields - operation_type: operationType, - - // RANGO DE FECHAS fields - fecha_pedimento_del: '', - fecha_pedimento_al: '', - clave_pedimento: '', - regimen_pedimento: '', - // LEFT fields - provider_header: invoice.compliance_mx?.provider_header || '', + provider_header: invoice.compliance_mx?.provider_header || 'proveedor', provider_id: invoice.compliance_mx?.provider_id || null, - sold_to_header: invoice.compliance_mx?.sold_to_header || '', + sold_to_header: invoice.compliance_mx?.sold_to_header || 'consignado_a', sold_to_id: invoice.compliance_mx?.sold_to_id || null, - shipped_to_header: invoice.compliance_mx?.shipped_to_header || '', + shipped_to_header: invoice.compliance_mx?.shipped_to_header || 'enviado_a', shipped_to_id: invoice.compliance_mx?.shipped_to_id || null, customs_broker_id: invoice.compliance_mx?.customs_broker_id || null, - customs_broker_us_id: null, + customs_broker_us_id: invoice.compliance_mx?.customs_broker_us_id || null, // RIGHT fields currency_type: invoice.financials?.currency_type || '', - currency_mode: 'extranjera', // extranjera, nacional, captura - weight_type: '', + currency: invoice.financials?.currency || 'foreign', // foreign, local, manual + exchange_rate: invoice.financials?.exchange_rate || null, // Added exchange_rate + weight_type: 'kgs', iva_factor: invoice.financials?.iva_factor || null, carrier_id: invoice.logistics?.[0]?.carrier_id || null, transport_id: '', @@ -91,43 +81,26 @@ transport_type: invoice.logistics?.[0]?.transport_type || '', transport_num: invoice.logistics?.[0]?.vehicle_num || '', aduana: invoice.compliance_mx?.aduana || '', - invoice_type: invoice.invoice_type || '', - clave_regimen_aduanero: '', - }; + document_type: invoice.document_type || '', + }; } else { // Creando una nueva factura formData = { - // TOP fields - is_pedimento_pending: false, - pedimento: '', - remesa: '', - invoice_number: '', - invoice_date: '', - emission_date: '', - - // Extra fields - operation_type: defaultOperationType ?? null, - - // RANGO DE FECHAS fields - fecha_pedimento_del: '', - fecha_pedimento_al: '', - clave_pedimento: '', - regimen_pedimento: '', - // LEFT fields - provider_header: '', + provider_header: 'proveedor', provider_id: null, - sold_to_header: '', + sold_to_header: 'consignado_a', sold_to_id: null, - shipped_to_header: '', + shipped_to_header: 'enviado_a', shipped_to_id: null, customs_broker_id: null, customs_broker_us_id: null, // RIGHT fields currency_type: '', - currency_mode: 'extranjera', // extranjera, nacional, captura - weight_type: '', + currency: 'foreign', // foreign, local, manual + exchange_rate: null, // Added exchange_rate + weight_type: 'kgs', iva_factor: null, carrier_id: null, transport_id: '', @@ -135,21 +108,29 @@ transport_type: '', transport_num: '', aduana: '', - invoice_type: defaultInvoiceType ?? '', - clave_regimen_aduanero: '', + document_type: '', }; } } else { - // Si formData ya existe, asegurar que tiene currency_mode - if (formData.currency_mode === undefined) { - formData.currency_mode = 'extranjera'; + // Si formData ya existe, asegurar que tiene valores por defecto + if (formData.currency === undefined) { + formData.currency = 'foreign'; + } + if (!formData.provider_header) { + formData.provider_header = 'proveedor'; + } + if (!formData.sold_to_header) { + formData.sold_to_header = 'consignado_a'; + } + if (!formData.shipped_to_header) { + formData.shipped_to_header = 'enviado_a'; } } // Opciones de tipo de peso const weightTypeOptions = [ - { value: 'kg', label: 'Kilogramos (kg)' }, - { value: 'lb', label: 'Libras (lb)' } + { value: 'kgs', label: 'Kilogramos (kg)' }, + { value: 'lbs', label: 'Libras (lb)' } ]; // Opciones de encabezados @@ -161,11 +142,11 @@ const soldToHeaderOptions = $derived([ { value: 'consignado_a', label: 'Consignado a' }, { value: 'vendido_a', label: 'Vendido a' }, - { value: formData.operation_type === 1 ? 'exportado_a' : 'importador', label: formData.operation_type === 1 ? 'Exportado a' : 'Importador' } + { value: operationType === 1 ? 'exportado_a' : 'importador', label: operationType === 1 ? 'Exportado a' : 'Importador' } ]); const shippedToHeaderOptions = $derived( - formData.operation_type === 1 + operationType === 1 ? [ { value: 'enviado_por', label: 'Enviado Por' }, { value: 'destinatario', label: 'Destinatario' }, @@ -187,12 +168,31 @@ // Combinar clientes y proveedores para shipped_to const allClientsProviders = [...clients, ...providers]; - // Filtrar regímenes por tipo de operación (1='1' exp, 2='2' imp) - const filteredRegimens = $derived( - codePedimentoRegimens.filter(r => - r.type_code === String(formData.operation_type) - ) - ); + // Filtrar regímenes por tipo de operación (1='E' exp, 2='I' imp) y obtener valores únicos + const filteredRegimens = $derived.by(() => { + const typeCode = operationType === 1 ? 'E' : operationType === 2 ? 'I' : null; + const filtered = codePedimentoRegimens.filter(r => r.type_code === typeCode); + + // Obtener solo regímenes únicos por regimen_code + const uniqueMap = new Map(); + filtered.forEach(r => { + if (r.regimen_code && !uniqueMap.has(r.regimen_code)) { + uniqueMap.set(r.regimen_code, r); + } + }); + + return Array.from(uniqueMap.values()); + }); + + // Efecto: Limpiar régimen si no existe en los regímenes filtrados al cambiar operation_type + $effect(() => { + if (formData.document_type && filteredRegimens.length > 0) { + const regimenExists = filteredRegimens.some(r => r.regimen_code === formData.document_type); + if (!regimenExists) { + formData.document_type = ''; + } + } + }); @@ -264,7 +264,8 @@ {/each} - + + *
@@ -309,7 +310,8 @@ {/each} - + + *
@@ -355,52 +357,55 @@ {/each} + *
- + { - formData.customs_broker_id = v || null; + formData.customs_broker_id = v ? parseInt(v) : null; }} > - {customsBrokers.find(cb => cb.broker_key === (formData.customs_broker_id || customsBrokers[0]?.broker_key))?.name || '...'} + {formData.customs_broker_id + ? customsBrokers.find(cb => cb.id === formData.customs_broker_id)?.name || 'Selecciona...' + : 'Selecciona...'} {#each customsBrokers as broker} - + {broker.name} {/each} - +
{ - formData.customs_broker_us_id = v || null; + formData.customs_broker_us_id = v ? parseInt(v) : null; }} > {formData.customs_broker_us_id - ? customsBrokers.find(cb => cb.broker_key === formData.customs_broker_us_id)?.name || '...' - : '...'} + ? customsBrokers.find(cb => cb.id === formData.customs_broker_us_id)?.name || 'Selecciona...' + : 'Selecciona...'} {#each customsBrokers as broker} - + {broker.name} {/each} @@ -415,27 +420,34 @@

Tipo de Moneda - Pesos Netos y Brutos

-

Tipo de cambio:

+

+ Tipo de cambio: + + {(exchangeRate !== undefined && exchangeRate !== null) + ? (exchangeRate === 0 ? 'N/A' : Number(exchangeRate).toFixed(4)) + : (formData.exchange_rate ? Number(formData.exchange_rate).toFixed(4) : 'N/A')} + +

- +
- - + +
- - + +
- - + +
- {#if formData.currency_mode === 'captura'} + {#if formData.currency === 'manual'}
Tipo Peso: { - formData.weight_type = v ?? ''; + formData.weight_type = v ?? 'kgs'; }} > - {formData.weight_type || '...'} + {weightTypeOptions.find(w => w.value === formData.weight_type)?.label || 'Kilogramos (kg)'} {#each weightTypeOptions as weightType} - + {weightType.label} {/each} @@ -489,7 +501,7 @@
-
+
@@ -681,22 +693,22 @@
- + { - formData.clave_regimen_aduanero = v ?? ''; + formData.document_type = v ?? ''; }} > - + - {#if formData.clave_regimen_aduanero} - {formData.clave_regimen_aduanero} + {#if formData.document_type} + {codePedimentoRegimens.find(r => r.regimen_code === formData.document_type)?.regimen_code || formData.document_type} {:else if filteredRegimens.length > 0} Selecciona régimen... - {:else if formData.operation_type} - Sin regímenes para tipo {formData.operation_type} + {:else if operationType} + Sin regímenes para tipo {operationType} {:else} Selecciona tipo de operación primero {/if} diff --git a/frontend/src/lib/components/dashboard/invoices/edit/invoice-top-fields.svelte b/frontend/src/lib/components/dashboard/invoices/edit/invoice-top-fields.svelte index 8e4700a4..47743eef 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/invoice-top-fields.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/invoice-top-fields.svelte @@ -19,7 +19,7 @@ formData?: any; invoiceTypes?: InvoiceType[]; pedimentos?: Pedimento[]; - defaultOperationType?: number | null; + defaultOperationType?: string | null; defaultInvoiceType?: string | null; } = $props(); @@ -36,26 +36,31 @@ formData.regimen_pedimento = selectedPedimento.regime || ''; // Construir el número de pedimento completo - const pedimentoNumber = `${selectedPedimento.year || ''}-${selectedPedimento.customs_office || ''}-${selectedPedimento.license || ''}-${selectedPedimento.pedimento_number || ''}`.replace(/^-+|-+$/g, ''); + const pedimentoNumber = `${selectedPedimento.customs_office?.slice(0,2) || ''}-${selectedPedimento.license || ''}-${selectedPedimento.pedimento_number || ''}`.replace(/^-+|-+$/g, ''); formData.pedimento = pedimentoNumber; } + $effect(() => { + if (formData?.pedimento_id && pedimentos.length > 0 && !formData.pedimento) { + handlePedimentoChange(formData.pedimento_id); + } + }); + if (!formData) { - let operationType: number | null = null; + let operationType: string | null = null; if (invoice?.operation_type) { - operationType = invoice.operation_type === 'exp' ? 1 : 2; + operationType = invoice.operation_type; } else if (defaultOperationType !== undefined) { operationType = defaultOperationType ?? null; } formData = { is_pedimento_pending: false, - pedimento_id: null, - pedimento: invoice?.compliance_mx?.pedimento || '', + pedimento_id: invoice?.compliance_mx?.pedimento_id || '', remesa: invoice?.compliance_mx?.remesa || '', invoice_number: invoice?.invoice_number || '', - invoice_date: invoice?.invoice_date || '', - emission_date: '', + invoice_date: invoice?.invoice_date || new Date().toISOString().split('T')[0], + emission_date: new Date().toISOString().split('T')[0], operation_type: operationType, invoice_type: invoice?.invoice_type || (defaultInvoiceType ?? ''), // Campos del pedimento (se llenarán al seleccionar un pedimento) @@ -70,29 +75,29 @@
- + { - formData.operation_type = v ? parseInt(v) : null; + formData.operation_type = v; }} > - {formData.operation_type !== null - ? (formData.operation_type === 1 ? 'Exp' : 'Imp') + {formData.operation_type + ? (formData.operation_type === 'exp' ? 'Exp' : 'Imp') : '...'} - Exportación - Importación + Exportación + Importación
- + {#each pedimentos as pedimento} - {pedimento.year}-{pedimento.customs_office}-{pedimento.license}-{pedimento.pedimento_number} + {pedimento.customs_office?.slice(0, 2)}-{pedimento.license}-{pedimento.pedimento_number} {/each} @@ -160,12 +165,12 @@
- +
- +
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-configuration.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-configuration.svelte index 374d4067..178a48de 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-configuration.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-configuration.svelte @@ -2,14 +2,26 @@ import * as RadioGroup from '$lib/components/ui/radio-group'; import { Input } from '$lib/components/ui/input'; import { Label } from '$lib/components/ui/label'; + import type { LineItem, LineDescriptions } from '$lib/api/dashboard/a76/items'; let { - isSubPartida = $bindable(), - continueSubPartidas = $bindable() + lineItem = $bindable(), + descriptions = $bindable() }: { - isSubPartida: string; - continueSubPartidas: string; + lineItem: LineItem; + descriptions: LineDescriptions; } = $props(); + + // Helper to map boolean to string for RadioGroup + let isSubPartidaValue = $derived(lineItem.is_subitem ? 'subpartida' : 'partida'); + function setIsSubPartida(val: string) { + lineItem.is_subitem = val === 'subpartida'; + } + + let continueSubPartidasValue = $derived(lineItem.includes_subitems ? 'si' : 'no'); + function setContinueSubPartidas(val: string) { + lineItem.includes_subitems = val === 'si'; + }
@@ -17,7 +29,10 @@
Is - +
@@ -31,7 +46,10 @@
Continue Sub-Items - +
@@ -49,7 +67,7 @@
- +
@@ -57,6 +75,7 @@
@@ -65,6 +84,7 @@
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-sheet-fa.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-sheet-fa.svelte index ae9cdac2..194af261 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-sheet-fa.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-sheet-fa.svelte @@ -49,7 +49,7 @@ Temporary Import Item - Order Number: {invoice?.invoice_number || 'N/A'} | Line: 1 + Order Number: {invoice?.invoice_number || 'N/A'} | Line: currentline @@ -58,11 +58,23 @@
- + {#if editingItem.lines && editingItem.lines.length > 0} + + {/if}
- + {#if editingItem.lines && editingItem.lines.length > 0} + + {/if}
@@ -78,29 +90,48 @@
- - + {#if editingItem.lines && editingItem.lines.length > 0} + + + {/if}
- + {#if editingItem.lines && editingItem.lines.length > 0} + + {/if} - + {#if editingItem.lines && editingItem.lines.length > 0} + + {/if} - + {#if editingItem.lines && editingItem.lines.length > 0} + + {/if} - + {#if editingItem.lines && editingItem.lines.length > 0} + + {/if}
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/main-data.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/main-data.svelte index 6230db8e..891d798c 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/main-data.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/main-data.svelte @@ -1,6 +1,19 @@
@@ -11,7 +24,7 @@
- +
@@ -20,13 +33,13 @@
- +
- - +
@@ -37,14 +50,14 @@
- + USD
- +
@@ -54,20 +67,20 @@
- +
- + +
- 0.00 +
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/packages-section.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/packages-section.svelte index dfc5fd75..40183ca1 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/packages-section.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/packages-section.svelte @@ -1,6 +1,21 @@
@@ -9,11 +24,11 @@
- +
- +
@@ -25,6 +40,7 @@
+
@@ -34,11 +50,11 @@
- +
- +
@@ -50,37 +66,37 @@
- +
- +
- +
- Advalorem: 0.00 + Advalorem: {customs.advalorem_american || '0.00'}
- +
- +
- +
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/summary-section.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/summary-section.svelte index 186aedfe..3c37fa81 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/summary-section.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/summary-section.svelte @@ -1,4 +1,7 @@
@@ -7,19 +10,19 @@
RETURN QUANTITY SUB-ITEMS
-
Temporary: 0.00000000
+
Temporary: {quantities.quantity_temp_export?.toFixed(8) || '0.00000000'}
Replacement or Change: 0.00000000
-
Definitive: 0.00000000
-
Returned Values: 0.00000000
-
Returned Values: 0.00000000
+
Definitive: {quantities.quantity_returned?.toFixed(8) || '0.00000000'}
+
Returned Values: {financials.value_returned_usd?.toFixed(8) || '0.00000000'}
+
Returned Values: {financials.value_returned_mxn?.toFixed(8) || '0.00000000'}
WEIGHTS (KILOS)
WEIGHTS (Pounds)
-
Net: 0.00000000
+
Net: {quantities.net_weight?.toFixed(8) || '0.00000000'}
0.00000000
-
Whole: 0.00000000
+
Whole: {quantities.gross_weight?.toFixed(8) || '0.00000000'}
0.00000000
@@ -31,16 +34,16 @@
(Dollars)
(Pesos)
-
Cost: 0.00000000
-
0.00000000
-
Value: 0.00000000
-
0.00000000
+
Cost: {financials.unit_cost_usd?.toFixed(8) || '0.00000000'}
+
{financials.unit_cost_mxn?.toFixed(8) || '0.00000000'}
+
Value: {financials.value_usd?.toFixed(8) || '0.00000000'}
+
{financials.value_mxn?.toFixed(8) || '0.00000000'}
-
Capture Cost: 0.00000000 USD
-
Capture Value: 0.00000000 USD
-
Customs Value: 0.00000000 USD
+
Capture Cost: {financials.unit_cost_capture?.toFixed(8) || '0.00000000'} USD
+
Capture Value: {financials.value_usd?.toFixed(8) || '0.00000000'} USD
+
Customs Value: {financials.customs_value_usd?.toFixed(8) || '0.00000000'} USD
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-continuation.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-continuation.svelte index cd4ff233..b44702d5 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-continuation.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-continuation.svelte @@ -3,6 +3,23 @@ import { Input } from '$lib/components/ui/input'; import { Label } from '$lib/components/ui/label'; import { Checkbox } from '$lib/components/ui/checkbox'; + import type { LineItem } from '$lib/api/dashboard/a76/items'; + + let { + lineItem = $bindable() + }: { + lineItem: LineItem; + } = $props(); + + let taxPaidValue = $derived(lineItem.tax_payment ? 'si' : 'no'); + function setTaxPaid(val: string) { + lineItem.tax_payment = val === 'si'; + } + + let hasCertificateValue = $derived(lineItem.has_certificate ? 'si' : 'no'); + function setHasCertificate(val: string) { + lineItem.has_certificate = val === 'si'; + }
@@ -12,7 +29,10 @@
TAX PAID - +
@@ -27,7 +47,7 @@
- +
@@ -37,7 +57,8 @@
- + +
@@ -45,7 +66,10 @@
Has Certificate of Origin? - +
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-identifiers.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-identifiers.svelte index 63974265..3a3034c3 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-identifiers.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-identifiers.svelte @@ -1,6 +1,9 @@
@@ -9,7 +12,7 @@
- +
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-labeling.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-labeling.svelte index 3dd58e63..cacb0306 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-labeling.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-labeling.svelte @@ -1,6 +1,9 @@
@@ -21,6 +24,7 @@ diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-series.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-series.svelte index d5f242f6..240a6c50 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-series.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-series.svelte @@ -1,5 +1,8 @@
@@ -9,6 +12,7 @@ diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte index e30f3a3d..3b10300c 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte @@ -112,23 +112,139 @@ isEditMode = false; showItemSheet = true; - // Auto-asignar valores desde la factura + // Auto-asignar valores desde la factura con estructura completa editingItem = { invoice_id: invoice.id, reference_number: '', order: invoice.purchase_order || '', warehouse: '', - location: '' + location: '', + lines: [{ + line_number: 1, + // LineItem fields + part_number: undefined, + component_part_number: undefined, + class_code: undefined, + identifier: undefined, + unit_of_measure: undefined, + alternate_unit: undefined, + permit_number: undefined, + page_line: undefined, + has_certificate: false, + certificate_number: undefined, + is_subitem: false, + includes_subitems: false, + tax_payment: false, + payment_method: undefined, + igi_amount: undefined, + // Nested relations + financial: { + unit_cost_usd: undefined, + unit_cost_mxn: undefined, + unit_cost_capture: undefined, + unit_cost_commercial_usd: undefined, + value_usd: undefined, + value_mxn: undefined, + value_returned_usd: undefined, + value_returned_mxn: undefined, + customs_value_usd: undefined, + }, + quantity: { + quantity: undefined, + unit_of_measure: undefined, + quantity_temp_export: undefined, + quantity_returned: undefined, + net_weight: undefined, + gross_weight: undefined, + package_key: undefined, + package_quantity: undefined, + package_description: undefined, + }, + customs: { + fraction: undefined, + fraction_type: 'GENERAL', + american_fraction: undefined, + origin_country: undefined, + destination_country: undefined, + advalorem: undefined, + advalorem_american: undefined, + sector: undefined, + }, + description: { + description_spanish: undefined, + description_english: undefined, + extra_description: undefined, + additional_info_spanish: undefined, + brand: undefined, + model: undefined, + has_serial: false, + }, + reference: { + serie_id: undefined, + }, + }] }; } function handleEdit(item: Item) { isEditMode = true; selectedItem = item; - editingItem = { ...item }; + // Deep clone and normalize numeric values + editingItem = normalizeItemData({ ...item }); showItemSheet = true; } + // Normalize numeric values from strings to numbers + function normalizeItemData(item: Partial): Partial { + if (item.lines && item.lines.length > 0) { + item.lines = item.lines.map(line => { + const normalizedLine = { ...line }; + + // Normalize financials + if (normalizedLine.financial) { + normalizedLine.financial = { + ...normalizedLine.financial, + unit_cost_usd: normalizedLine.financial.unit_cost_usd != null + ? Number(normalizedLine.financial.unit_cost_usd) + : undefined, + unit_cost_mxn: normalizedLine.financial.unit_cost_mxn != null + ? Number(normalizedLine.financial.unit_cost_mxn) + : undefined, + value_usd: normalizedLine.financial.value_usd != null + ? Number(normalizedLine.financial.value_usd) + : undefined, + value_mxn: normalizedLine.financial.value_mxn != null + ? Number(normalizedLine.financial.value_mxn) + : undefined, + }; + } + + // Normalize quantities + if (normalizedLine.quantity) { + normalizedLine.quantity = { + ...normalizedLine.quantity, + quantity: normalizedLine.quantity.quantity != null + ? Number(normalizedLine.quantity.quantity) + : undefined, + net_weight: normalizedLine.quantity.net_weight != null + ? Number(normalizedLine.quantity.net_weight) + : undefined, + gross_weight: normalizedLine.quantity.gross_weight != null + ? Number(normalizedLine.quantity.gross_weight) + : undefined, + package_quantity: normalizedLine.quantity.package_quantity != null + ? Number(normalizedLine.quantity.package_quantity) + : undefined, + }; + } + + return normalizedLine; + }); + } + + return item; + } + function handleDelete(item: Item) { selectedItem = item; showDeleteDialog = true; @@ -144,7 +260,8 @@ reference_number: editingItem.reference_number, order: editingItem.order, warehouse: editingItem.warehouse, - location: editingItem.location + location: editingItem.location, + lines: editingItem.lines || [] }); // Recargar items @@ -174,7 +291,8 @@ reference_number: editingItem.reference_number, order: editingItem.order, warehouse: editingItem.warehouse, - location: editingItem.location + location: editingItem.location, + lines: editingItem.lines || [] }); // Recargar items diff --git a/frontend/src/lib/components/dashboard/invoices/edit/observations-tab-form.svelte b/frontend/src/lib/components/dashboard/invoices/edit/observations-tab-form.svelte index 1f9c1204..91b069f9 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/observations-tab-form.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/observations-tab-form.svelte @@ -23,64 +23,10 @@ if (!formData && invoice) { formData = { - // Invoice header fields + // Campos de observaciones observation_es: invoice.observation_es || '', - observation_en: invoice.observation_en || '', - alternate_invoice: invoice.alternate_invoice || '', - // Compliance MX fields - pedimento: invoice.compliance_mx?.pedimento || '', - pedimento_code: invoice.compliance_mx?.pedimento_code || '', - pedimento_k1: invoice.compliance_mx?.pedimento_k1 || '', - remesa: invoice.compliance_mx?.remesa || null, - aduana: invoice.compliance_mx?.aduana || '', - port_of_entry: invoice.compliance_mx?.port_of_entry || '', - destination: invoice.compliance_mx?.destination || '', - manifest_number: invoice.compliance_mx?.manifest_number || '', - provider_header: invoice.compliance_mx?.provider_header || '', - provider_id: invoice.compliance_mx?.provider_id || null, - sold_to_header: invoice.compliance_mx?.sold_to_header || '', - sold_to_id: invoice.compliance_mx?.sold_to_id || null, - shipped_to_header: invoice.compliance_mx?.shipped_to_header || '', - shipped_to_id: invoice.compliance_mx?.shipped_to_id || null, - shipped_by_header: invoice.compliance_mx?.shipped_by_header || '', - shipped_by_id: invoice.compliance_mx?.shipped_by_id || null, - customs_broker_id: invoice.compliance_mx?.customs_broker_id || null, - broker_invoice_num: invoice.compliance_mx?.broker_invoice_num || '', - broker_invoice_date: invoice.compliance_mx?.broker_invoice_date || '', - is_mixed: invoice.compliance_mx?.is_mixed || null, - waste_type: invoice.compliance_mx?.waste_type || '', - scrap_type: invoice.compliance_mx?.scrap_type || '', - appendix_17: invoice.compliance_mx?.appendix_17 || null, - is_regime_change: invoice.compliance_mx?.is_regime_change || '', - which_exchange_rate: invoice.compliance_mx?.which_exchange_rate || '', - value_method: invoice.compliance_mx?.value_method || '', - act_value: invoice.compliance_mx?.act_value || '', - is_pedimento_pending: invoice.compliance_mx?.is_pedimento_pending || false, - is_owner_of_goods: invoice.compliance_mx?.is_owner_of_goods || '', - generate_balances: invoice.compliance_mx?.generate_balances || '', - was_reviewed_by_company: invoice.compliance_mx?.was_reviewed_by_company || false, - edocument: invoice.compliance_mx?.edocument || '', - electronic_signature: invoice.compliance_mx?.electronic_signature || '', - certificate_number: invoice.compliance_mx?.certificate_number || '', - niu_number: invoice.compliance_mx?.niu_number || '', - bill_of_lading_count: invoice.compliance_mx?.bill_of_lading_count || '', - addendum_vu: invoice.compliance_mx?.addendum_vu || '', - origin_destination_cove: invoice.compliance_mx?.origin_destination_cove || '', - vucem_operation_num: invoice.compliance_mx?.vucem_operation_num || '', - customs_person_line: invoice.compliance_mx?.customs_person_line || null, - contingency_mode: invoice.compliance_mx?.contingency_mode || false, - enclosure: invoice.compliance_mx?.enclosure || '', - guide_type_to_identify: invoice.compliance_mx?.guide_type_to_identify || '', - location: invoice.compliance_mx?.location || '', - dot_code: invoice.compliance_mx?.dot_code || '', - subdivision: invoice.compliance_mx?.subdivision || '', - acts_as: invoice.compliance_mx?.acts_as || '', - movement_type: invoice.compliance_mx?.movement_type || '', - office_document: invoice.compliance_mx?.office_document || '', - reason_export: invoice.compliance_mx?.reason_export || '', - signature_key: invoice.compliance_mx?.signature_key || '', - sem_id: invoice.compliance_mx?.sem_id || null, - // Financials fields (incrementables) + observation_en: invoice.observation_en || '', + // Incrementables freight: invoice.financials?.freight || null, insurance_value: invoice.financials?.insurance_value || null, insurance: invoice.financials?.insurance || null, @@ -88,70 +34,22 @@ other_increments: invoice.financials?.other_increments || null, total_increments_mn: invoice.financials?.total_increments_mn || null, total_increments_me: invoice.financials?.total_increments_me || null, - // Logistics fields - incoterm: invoice.logistics?.[0]?.incoterm || '' + // Incoterm y recinto + incoterm: invoice.logistics?.[0]?.incoterm || null, + enclosure: invoice.compliance_mx?.enclosure || null, + // Campos de esta pestaña + num_seals: null, + movement_type: invoice.compliance_mx?.movement_type || '', + alternate_invoice: invoice.alternate_invoice || '', + valuation_method: null }; exists = true; } else if (!formData) { formData = { - // Invoice header fields + // Campos de observaciones observation_es: '', - observation_en: '', - alternate_invoice: '', - // Compliance MX fields - pedimento: '', - pedimento_code: '', - pedimento_k1: '', - remesa: null, - aduana: '', - port_of_entry: '', - destination: '', - manifest_number: '', - provider_header: '', - provider_id: null, - sold_to_header: '', - sold_to_id: null, - shipped_to_header: '', - shipped_to_id: null, - shipped_by_header: '', - shipped_by_id: null, - customs_broker_id: null, - broker_invoice_num: '', - broker_invoice_date: '', - is_mixed: null, - waste_type: '', - scrap_type: '', - appendix_17: null, - is_regime_change: '', - which_exchange_rate: '', - value_method: '', - act_value: '', - is_pedimento_pending: false, - is_owner_of_goods: '', - generate_balances: '', - was_reviewed_by_company: false, - edocument: '', - electronic_signature: '', - certificate_number: '', - niu_number: '', - bill_of_lading_count: '', - addendum_vu: '', - origin_destination_cove: '', - vucem_operation_num: '', - customs_person_line: null, - contingency_mode: false, - enclosure: '', - guide_type_to_identify: '', - location: '', - dot_code: '', - subdivision: '', - acts_as: '', - movement_type: '', - office_document: '', - reason_export: '', - signature_key: '', - sem_id: null, - // Financials fields + observation_en: '', + // Incrementables freight: null, insurance_value: null, insurance: null, @@ -159,8 +57,14 @@ other_increments: null, total_increments_mn: null, total_increments_me: null, - // Logistics fields - incoterm: '' + // Incoterm y recinto + incoterm: null, + enclosure: null, + // Campos de esta pestaña + num_seals: null, + movement_type: '', + alternate_invoice: '', + valuation_method: null }; exists = false; } diff --git a/frontend/src/lib/components/dashboard/invoices/edit/others-tab-form.svelte b/frontend/src/lib/components/dashboard/invoices/edit/others-tab-form.svelte index 46e84244..23fed8a6 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/others-tab-form.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/others-tab-form.svelte @@ -13,116 +13,63 @@ let { invoice, formData = $bindable(), - exists = $bindable() + exists = $bindable(), + transportModes = [] }: { invoice: Invoice | null; formData?: any; exists?: boolean; + transportModes?: any[]; } = $props(); - if (!formData && invoice?.logistics && invoice.logistics.length > 0) { - formData = invoice.logistics.map(l => ({ - // Carrier info - carrier_id: l.carrier_id || '', - transport_id: l.transport_id || '', - transport_us_id: l.transport_us_id || '', - transport_type: l.transport_type || null, - transport_num: l.transport_num || '', - transport_mode: l.transport_mode || '', - driver_name: l.driver_name || '', - is_rail: l.is_rail || '', - rail_id: l.rail_id || '', - // Vehicle & tracking - vehicle_num: l.vehicle_num || '', - license_plate: l.license_plate || '', - license_plate_complete: l.license_plate_complete || '', - trailer_num: l.trailer_num || '', - seal_number: l.seal_number || '', - guide_number: l.guide_number || '', - bill_number: l.bill_number || '', - reference_number: l.reference_number || '', - shipment_number: l.shipment_number || '', - // Incoterms - incoterm: l.incoterm || '', - // Identifiers - identifier_1: l.identifier_1 || '', - complement_1: l.complement_1 || '', - identifier_2: l.identifier_2 || '', - complement_2: l.complement_2 || '', - // Weight & container - weight_type: l.weight_type || '', - container_types: l.container_types || '', - vehicle_data: l.vehicle_data || '', - // Locations - origin_location: l.origin_location || '', - destination_location: l.destination_location || '', - transport_itinerary: l.transport_itinerary || '', - destination_goods: l.destination_goods || '', - // Dates - entry_exit_date: l.entry_exit_date || '', - delivery_date: l.delivery_date || '', - // Delivery control - delivered_status: l.delivered_status || '', - received_by: l.received_by || '', - // Payment - payment_date: l.payment_date || '', - payment_receipt_num: l.payment_receipt_num || '', - // CTM - is_ctm_process: l.is_ctm_process || '' - })); + if (!formData && invoice) { + formData = { + // Campo de comentario estatus + comments_status: invoice.comments_status || '', + // Campos que van en diferentes recursos pero se editan aquí + transport_mode: invoice.logistics?.[0]?.transport_mode || null, + is_mixed: invoice.compliance_mx?.is_mixed || null, + print_stamp: invoice.financials?.seal_value_2500 || false, + rule_3121_parties_ii: false, + related_doc_id: invoice.related_doc_id || null, + code_signature: invoice.compliance_mx?.code_signature || '', + electronic_signature: invoice.compliance_mx?.electronic_signature || '', + mandatory_person: '', + contingency_mode: invoice.compliance_mx?.contingency_mode || false, + cove: invoice.compliance_mx?.origin_destination_cove || '', + operation_num: invoice.compliance_mx?.vucem_operation_num || '', + adendas: invoice.compliance_mx?.addendum_vu || '', + observations_vu: invoice.vu_observations || '', + certified_number: invoice.compliance_mx?.certificate_number || '', + }; exists = true; } else if (!formData) { - formData = []; + formData = { + // Campo de comentario estatus + comments_status: '', + // Campos que van en diferentes recursos pero se editan aquí + transport_mode: 'TRUCK', + is_mixed: null, + print_stamp: false, + rule_3121_parties_ii: false, + related_doc_id: null, + code_signature: '', + electronic_signature: '', + mandatory_person: '', + contingency_mode: false, + cove: '', + operation_num: '', + adendas: '', + observations_vu: '', + certified_number: '', + }; exists = false; } - // Campos adicionales que van en otros recursos - let transportMode = $state('TRUCK'); - // is_mixed va en compliance_mx - let isMixed = $state(invoice?.compliance_mx?.is_mixed ? 'yes' : 'no'); - // related_doc_id va en invoice header - let relationDocsId = $state(invoice?.related_doc_id?.toString() || '0'); - // electronic_signature va en compliance_mx - let code_signature = $state(invoice?.compliance_mx?.code_signature || ''); - let electronicSignature = $state(invoice?.compliance_mx?.electronic_signature || ''); - // Estos campos no existen en el schema del backend - let mandatoryPerson = $state('0'); + // Campos que no están en el backend let rfc = $state(''); - let contingencyMode = $state(invoice?.compliance_mx?.contingency_mode || false); let curp = $state(''); - let rule3121PartiesII = $state(false); - // origin_destination_cove va en compliance_mx - let cove = $state(invoice?.compliance_mx?.origin_destination_cove || ''); - // vucem_operation_num va en compliance_mx - let operationNum = $state(invoice?.compliance_mx?.vucem_operation_num || ''); - // addendum_vu va en compliance_mx - let adendas = $state(invoice?.compliance_mx?.addendum_vu || ''); - // vu_observations va en invoice header - let observationsVU = $state(invoice?.vu_observations || ''); - // certificate_number va en compliance_mx - let certifiedNumber = $state(invoice?.compliance_mx?.certificate_number || ''); - // seal_value_2500 va en financials - let printStamp = $state(invoice?.financials?.seal_value_2500 || false); - // comments_status va en invoice header - let commentsStatus = $state(invoice?.comments_status || ''); - const transportModes = [ - { value: 'TRUCK', label: 'Camión' }, - { value: 'TRAIN', label: 'Tren' }, - { value: 'SHIP', label: 'Marítimo' }, - { value: 'AIR', label: 'Aéreo' }, - { value: 'OTHER', label: 'Otro' } - ]; - - function addLogistic() { - formData = [...formData, { - carrier_id: '', - transport_type: null, - driver_name: '', - vehicle_num: '', - license_plate: '' - }]; - } function loadInfo() { // Función para cargar información @@ -136,14 +83,18 @@
- transportMode = value || 'TRUCK'}> + formData.transport_mode = value || 'TRUCK'} + > - {transportModes.find(m => m.value === transportMode)?.label || 'Seleccionar modo'} + {transportModes.find(m => m.key === formData.transport_mode)?.name || 'Seleccionar modo'} {#each transportModes as mode} - - {mode.label} + + {mode.name} {/each} @@ -154,7 +105,7 @@
- + @@ -165,7 +116,11 @@
- + formData.is_mixed = v === 'yes'} + class="flex gap-4" + >
@@ -179,7 +134,7 @@
- +
@@ -188,8 +143,8 @@