Merge pull request 'feature/catalogo-pedimento' (#219) from feature/catalogo-pedimento into development

Reviewed-on: ADUANASOFT/anexo76#219
This commit is contained in:
2026-03-18 03:53:00 +00:00
16 changed files with 355 additions and 70 deletions

View File

@@ -49,6 +49,9 @@ from api.v1.modules.public.reference_data.states.seed import seed as states_seed
from api.v1.modules.public.reference_data.transport_modes.seed import ( from api.v1.modules.public.reference_data.transport_modes.seed import (
seed as transport_modes_seed, seed as transport_modes_seed,
) )
from api.v1.modules.public.reference_data.pedimento_transport_catalog.seed import (
seed as pedimento_transport_catalog_seed,
)
from api.v1.modules.public.reference_data.transport_types.seed import ( from api.v1.modules.public.reference_data.transport_types.seed import (
seed as transport_types_seed, seed as transport_types_seed,
) )
@@ -98,6 +101,28 @@ def upgrade() -> None:
return "NULL" return "NULL"
return f"'{str(val).replace(chr(39), chr(39)*2)}'" return f"'{str(val).replace(chr(39), chr(39)*2)}'"
op.execute(
"""
CREATE TABLE IF NOT EXISTS public.pedimento_transport_catalog (
code VARCHAR(3) NOT NULL,
transport_en VARCHAR(80) NOT NULL,
transport_es VARCHAR(120) NOT NULL,
payment_date_code VARCHAR(1) NOT NULL,
CONSTRAINT pedimento_transport_catalog_pkey PRIMARY KEY (code),
CONSTRAINT pedimento_transport_catalog_payment_date_code_chk
CHECK (payment_date_code IN ('E','P'))
);
"""
)
op.execute(
"""
ALTER TABLE IF EXISTS a76.pedimento_transport_means
ALTER COLUMN entry_exit TYPE VARCHAR(3),
ALTER COLUMN arrival TYPE VARCHAR(3),
ALTER COLUMN departure TYPE VARCHAR(3);
"""
)
# --- SEEDS PUBLIC (Tablas base) --- # --- SEEDS PUBLIC (Tablas base) ---
# Seeds # Seeds
values_pc = ", ".join( values_pc = ", ".join(
@@ -285,6 +310,20 @@ def upgrade() -> None:
""" """
) )
values_ptc = ", ".join(
[
f"('{code}', '{en.replace(chr(39), chr(39)*2)}', '{es.replace(chr(39), chr(39)*2)}', '{pdc}')"
for code, en, es, pdc in pedimento_transport_catalog_seed
]
)
op.execute(
f"""
INSERT INTO public.pedimento_transport_catalog (code, transport_en, transport_es, payment_date_code) VALUES
{values_ptc}
ON CONFLICT (code) DO NOTHING;
"""
)
values_tt = ", ".join( values_tt = ", ".join(
[ [
f"('{code}', '{desc.replace(chr(39), chr(39)*2)}')" f"('{code}', '{desc.replace(chr(39), chr(39)*2)}')"
@@ -475,6 +514,16 @@ def upgrade() -> None:
def downgrade() -> None: def downgrade() -> None:
"""Downgrade schema.""" """Downgrade schema."""
op.execute(
"""
ALTER TABLE IF EXISTS a76.pedimento_transport_means
ALTER COLUMN entry_exit TYPE VARCHAR(2),
ALTER COLUMN arrival TYPE VARCHAR(2),
ALTER COLUMN departure TYPE VARCHAR(2);
"""
)
op.drop_table("pedimento_transport_catalog", schema="public")
op.drop_table("us_tariff_fractions", schema="a76") op.drop_table("us_tariff_fractions", schema="a76")
op.drop_table("historical_tariff_fractions", schema="a76") op.drop_table("historical_tariff_fractions", schema="a76")
op.drop_table("canadian_tariff_fractions", schema="a76") op.drop_table("canadian_tariff_fractions", schema="a76")

View File

@@ -35,7 +35,7 @@ def load_pedimentos_fk_sets(
valid_clave_regimen_tipo, # (pedimento_code, regimen_code, type_code) valid_clave_regimen_tipo, # (pedimento_code, regimen_code, type_code)
valid_aduana_seccion, # customs_code valid_aduana_seccion, # customs_code
existing_pedimento_keys, # key strings para actualizar existing_pedimento_keys, # key strings para actualizar
valid_anexo22_claves, # stub vacío hasta tener catálogo valid_anexo22_claves, # catálogo de transporte Anexo 22 (pedimento_transport_catalog.code)
valid_patentes, # CustomsBroker.license (tenant/company) valid_patentes, # CustomsBroker.license (tenant/company)
short_name_to_id, # short_name normalizado (upper) -> client id (primera aparición gana) short_name_to_id, # short_name normalizado (upper) -> client id (primera aparición gana)
""" """
@@ -48,6 +48,9 @@ def load_pedimentos_fk_sets(
from api.v1.modules.public.reference_data.customs_sections.models import CustomsSection from api.v1.modules.public.reference_data.customs_sections.models import CustomsSection
from api.v1.modules.public.reference_data.pedimento_codes.models import PedimentoCode from api.v1.modules.public.reference_data.pedimento_codes.models import PedimentoCode
from api.v1.modules.public.reference_data.pedimento_regimens.models import RegimenPedimento from api.v1.modules.public.reference_data.pedimento_regimens.models import RegimenPedimento
from api.v1.modules.public.reference_data.pedimento_transport_catalog.models import (
PedimentoTransportCatalog,
)
valid_client_ids: Set[int] = set() valid_client_ids: Set[int] = set()
valid_regimes: Set[str] = set() valid_regimes: Set[str] = set()
@@ -96,6 +99,10 @@ def load_pedimentos_fk_sets(
for cs in session.query(CustomsSection).all(): for cs in session.query(CustomsSection).all():
valid_aduana_seccion.add(cs.customs_code.strip()) valid_aduana_seccion.add(cs.customs_code.strip())
for tm in session.query(PedimentoTransportCatalog.code).all():
if (tm[0] or "").strip():
valid_anexo22_claves.add((tm[0] or "").strip().upper())
for cb in ( for cb in (
session.query(CustomsBroker) session.query(CustomsBroker)
.filter( .filter(

View File

@@ -8,6 +8,9 @@ from api.v1.modules.public.reference_data.customs_sections.models import Customs
from api.v1.modules.public.reference_data.code_pedimento_regimens.models import CodePedimentoRegimen from api.v1.modules.public.reference_data.code_pedimento_regimens.models import CodePedimentoRegimen
from api.v1.modules.public.reference_data.transport_types.models import TransportType from api.v1.modules.public.reference_data.transport_types.models import TransportType
from api.v1.modules.public.reference_data.transport_modes.models import TransportMode from api.v1.modules.public.reference_data.transport_modes.models import TransportMode
from api.v1.modules.public.reference_data.pedimento_transport_catalog.models import (
PedimentoTransportCatalog,
)
# Import A76 Services # Import A76 Services
from api.v1.modules.a76.customs_brokers.services import CustomsBrokerService from api.v1.modules.a76.customs_brokers.services import CustomsBrokerService
@@ -19,6 +22,9 @@ from api.v1.modules.public.reference_data.customs_sections.dto import CustomsSec
from api.v1.modules.public.reference_data.code_pedimento_regimens.dto import CodePedimentoRegimenDTO from api.v1.modules.public.reference_data.code_pedimento_regimens.dto import CodePedimentoRegimenDTO
from api.v1.modules.public.reference_data.transport_types.dto import TransportTypeDTO from api.v1.modules.public.reference_data.transport_types.dto import TransportTypeDTO
from api.v1.modules.public.reference_data.transport_modes.dto import TransportModeDTO from api.v1.modules.public.reference_data.transport_modes.dto import TransportModeDTO
from api.v1.modules.public.reference_data.pedimento_transport_catalog.dto import (
PedimentoTransportCatalogDTO,
)
from api.v1.modules.a76.customs_brokers.dto import CustomsBrokerResponseDTO from api.v1.modules.a76.customs_brokers.dto import CustomsBrokerResponseDTO
from api.v1.modules.a76.clients_and_providers.dto import ClientProviderResponseDTO from api.v1.modules.a76.clients_and_providers.dto import ClientProviderResponseDTO
from .dtos.pedimentos import PedimentosResponse from .dtos.pedimentos import PedimentosResponse
@@ -71,6 +77,16 @@ class PedimentoCatalogService:
except Exception as e: except Exception as e:
print(f"Error fetching transport_modes: {e}") print(f"Error fetching transport_modes: {e}")
try:
response.pedimento_transport_catalog = [
PedimentoTransportCatalogDTO.model_validate(obj)
for obj in db.query(PedimentoTransportCatalog)
.order_by(PedimentoTransportCatalog.code.asc())
.all()
]
except Exception as e:
print(f"Error fetching pedimento_transport_catalog: {e}")
# Helper to fetch tenant/company specific data # Helper to fetch tenant/company specific data
def fetch_tenant_data(): def fetch_tenant_data():
# Customs Brokers # Customs Brokers

View File

@@ -10,27 +10,27 @@ class PedimentoTransportMeansBase(BaseModel):
pedimento_id: Optional[int] = Field(None, description="Pedimento ID") pedimento_id: Optional[int] = Field(None, description="Pedimento ID")
tenant_id: Optional[int] = Field(None, description="Tenant ID") tenant_id: Optional[int] = Field(None, description="Tenant ID")
destination: Optional[int] = Field(None, description="Destination") destination: Optional[int] = Field(None, description="Destination")
entry_exit: Optional[str] = Field(None, max_length=2, description="Entry/exit") entry_exit: Optional[str] = Field(None, max_length=3, description="Entry/exit")
arrival: str = Field(..., max_length=2, description="Arrival") arrival: str = Field(..., max_length=3, description="Arrival")
departure: str = Field(..., max_length=2, description="Departure") departure: str = Field(..., max_length=3, description="Departure")
class PedimentoTransportMeansCreate(BaseModel): class PedimentoTransportMeansCreate(BaseModel):
"""Schema for creating a new Pedimento Transport Means - pedimento_id and tenant_id are set by backend""" """Schema for creating a new Pedimento Transport Means - pedimento_id and tenant_id are set by backend"""
destination: Optional[int] = Field(None, description="Destination") destination: Optional[int] = Field(None, description="Destination")
entry_exit: Optional[str] = Field(None, max_length=2, description="Entry/exit") entry_exit: Optional[str] = Field(None, max_length=3, description="Entry/exit")
arrival: str = Field(..., max_length=2, description="Arrival") arrival: str = Field(..., max_length=3, description="Arrival")
departure: str = Field(..., max_length=2, description="Departure") departure: str = Field(..., max_length=3, description="Departure")
class PedimentoTransportMeansUpdate(BaseModel): class PedimentoTransportMeansUpdate(BaseModel):
"""Schema for updating a Pedimento Transport Means""" """Schema for updating a Pedimento Transport Means"""
destination: Optional[int] = None destination: Optional[int] = None
entry_exit: Optional[str] = Field(None, max_length=2) entry_exit: Optional[str] = Field(None, max_length=3)
arrival: Optional[str] = Field(None, max_length=2) arrival: Optional[str] = Field(None, max_length=3)
departure: Optional[str] = Field(None, max_length=2) departure: Optional[str] = Field(None, max_length=3)
class PedimentoTransportMeansResponse(PedimentoTransportMeansBase): class PedimentoTransportMeansResponse(PedimentoTransportMeansBase):

View File

@@ -39,9 +39,9 @@ class PedimentoTransportMeans(Base, TenantScopedMixin, TimestampMixin):
pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False)
destination: Mapped[int] = mapped_column(SmallInteger) destination: Mapped[int] = mapped_column(SmallInteger)
entry_exit: Mapped[str] = mapped_column(String(2)) entry_exit: Mapped[str] = mapped_column(String(3))
arrival: Mapped[str] = mapped_column(String(2)) arrival: Mapped[str] = mapped_column(String(3))
departure: Mapped[str] = mapped_column(String(2)) departure: Mapped[str] = mapped_column(String(3))
pedimento: Mapped["Pedimentos"] = relationship( pedimento: Mapped["Pedimentos"] = relationship(
"Pedimentos", back_populates="pedimento_transport_means" "Pedimentos", back_populates="pedimento_transport_means"

View File

@@ -13,6 +13,9 @@ from api.v1.modules.a76.customs_brokers.dto import CustomsBrokerResponseDTO
from api.v1.modules.a76.clients_and_providers.dto import ClientProviderResponseDTO from api.v1.modules.a76.clients_and_providers.dto import ClientProviderResponseDTO
from api.v1.modules.public.reference_data.transport_types.dto import TransportTypeDTO from api.v1.modules.public.reference_data.transport_types.dto import TransportTypeDTO
from api.v1.modules.public.reference_data.transport_modes.dto import TransportModeDTO from api.v1.modules.public.reference_data.transport_modes.dto import TransportModeDTO
from api.v1.modules.public.reference_data.pedimento_transport_catalog.dto import (
PedimentoTransportCatalogDTO,
)
from .dtos.pedimentos import PedimentosResponse from .dtos.pedimentos import PedimentosResponse
@@ -26,6 +29,7 @@ class PedimentoCatalogsResponse(BaseModel):
clients: List[ClientProviderResponseDTO] = [] clients: List[ClientProviderResponseDTO] = []
transport_types: List[TransportTypeDTO] = [] transport_types: List[TransportTypeDTO] = []
transport_modes: List[TransportModeDTO] = [] transport_modes: List[TransportModeDTO] = []
pedimento_transport_catalog: List[PedimentoTransportCatalogDTO] = []
class PedimentoCreationResponse(PedimentoCatalogsResponse): class PedimentoCreationResponse(PedimentoCatalogsResponse):

View File

@@ -227,7 +227,7 @@ BASE_SELECT = """
COALESCE(icm.vucem_operation_num,'') AS "C41", COALESCE(icm.vucem_operation_num,'') AS "C41",
COALESCE(cl.material_key,'') AS "C42", COALESCE(cl.material_key,'') AS "C42",
CONCAT(ped.year,'-',ped.customs_office,'-',ped.license,'-',ped.pedimento_number) AS "C43", CONCAT(ped.year,'-',ped.customs_office,'-',ped.license,'-',ped.pedimento_number) AS "C43",
'' AS "C44", COALESCE(ptc.payment_date_code, 'P') AS "C44",
COALESCE(ilc.octave_fraction,'') AS "C45", COALESCE(ilc.octave_fraction,'') AS "C45",
'' AS "C47", '' AS "C47",
COALESCE(ped.pedimento_code,'') AS "C48", COALESCE(ped.pedimento_code,'') AS "C48",
@@ -243,6 +243,8 @@ BASE_JOINS = """
JOIN a76.invoice_compliance_mx icm ON icm.invoice_id = ih.id JOIN a76.invoice_compliance_mx icm ON icm.invoice_id = ih.id
LEFT JOIN a76.pedimentos ped ON ped.id = icm.pedimento_id LEFT JOIN a76.pedimentos ped ON ped.id = icm.pedimento_id
LEFT JOIN a76.pedimento_dates pd ON pd.pedimento_id = ped.id LEFT JOIN a76.pedimento_dates pd ON pd.pedimento_id = ped.id
LEFT JOIN a76.pedimento_transport_means ptm ON ptm.pedimento_id = ped.id
LEFT JOIN public.pedimento_transport_catalog ptc ON ptc.code = ptm.entry_exit
LEFT JOIN a76.classes cl ON cl.id = il.class_id LEFT JOIN a76.classes cl ON cl.id = il.class_id
LEFT JOIN a76.item_line_quantities ilq ON ilq.item_line_id = il.id LEFT JOIN a76.item_line_quantities ilq ON ilq.item_line_id = il.id
LEFT JOIN a76.item_line_financials ilf ON ilf.item_line_id = il.id LEFT JOIN a76.item_line_financials ilf ON ilf.item_line_id = il.id
@@ -440,20 +442,21 @@ def _build_row(
peso_usado = (cant_ret * peso_neto / cant_orig) if cant_orig != 0 else Decimal(0) peso_usado = (cant_ret * peso_neto / cant_orig) if cant_orig != 0 else Decimal(0)
peso_saldo = peso_neto - peso_usado peso_saldo = peso_neto - peso_usado
# TIPO DE CAMBIO — per Clarion logic: # TIPO DE CAMBIO:
# Si TipoPedimentoTransporteE IN ('4','1','98E') → usar Fecha_Inicio, else Fecha_Pago # C44 now carries payment_date_code from pedimento_transport_catalog:
# E => Fecha_Inicio, P => Fecha_Pago.
# If explicitly "invoice_date", we use invoice_date (C11) instead. # If explicitly "invoice_date", we use invoice_date (C11) instead.
tc = Decimal(1) tc = Decimal(1)
fecha_pago = row.get("C7") fecha_pago = row.get("C7")
fecha_inicio = row.get("C9") fecha_inicio = row.get("C9")
fecha_factura= row.get("C11") fecha_factura= row.get("C11")
transport_type = str(row.get("C44") or "") payment_date_code = str(row.get("C44") or "").upper()
tc_fecha_display = None tc_fecha_display = None
# TIPO DE CAMBIO # TIPO DE CAMBIO
tc_fecha = None tc_fecha = None
if use_fp: if use_fp:
tc_fecha = fecha_inicio if transport_type in ("1", "4", "98E") else fecha_pago tc_fecha = fecha_inicio if payment_date_code == "E" else fecha_pago
else: else:
tc_fecha = fecha_factura tc_fecha = fecha_factura

View File

@@ -0,0 +1,10 @@
from pydantic import BaseModel, ConfigDict, Field
class PedimentoTransportCatalogDTO(BaseModel):
code: str = Field(..., min_length=1, max_length=3)
transport_en: str
transport_es: str
payment_date_code: str = Field(..., min_length=1, max_length=1)
model_config = ConfigDict(from_attributes=True)

View File

@@ -0,0 +1,26 @@
from core.database import Base
from sqlalchemy import CheckConstraint, PrimaryKeyConstraint, String
from sqlalchemy.orm import Mapped, mapped_column
class PedimentoTransportCatalog(Base):
__tablename__ = "pedimento_transport_catalog"
__table_args__ = (
PrimaryKeyConstraint("code", name="pedimento_transport_catalog_pkey"),
CheckConstraint(
"payment_date_code IN ('E', 'P')",
name="pedimento_transport_catalog_payment_date_code_chk",
),
{"schema": "public", "extend_existing": True},
)
code: Mapped[str] = mapped_column(String(3), nullable=False)
transport_en: Mapped[str] = mapped_column(String(80), nullable=False)
transport_es: Mapped[str] = mapped_column(String(120), nullable=False)
payment_date_code: Mapped[str] = mapped_column(String(1), nullable=False)
def __repr__(self) -> str:
return (
f"<PedimentoTransportCatalog(code={self.code}, transport_en={self.transport_en}, "
f"transport_es={self.transport_es}, payment_date_code={self.payment_date_code})>"
)

View File

@@ -0,0 +1,93 @@
from typing import Any, Dict
from core.database import get_core_db
from core.security import get_current_user
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from .dto import PedimentoTransportCatalogDTO
from .models import PedimentoTransportCatalog
router = APIRouter(prefix="/pedimento-transport-catalog")
@router.get("/", response_model=Dict[str, Any])
async def list_pedimento_transport_catalog(
page: int = Query(1, ge=1, description="Numero de pagina"),
page_size: int = Query(100, ge=1, le=200, description="Tamano de pagina"),
db: Session = Depends(get_core_db),
):
skip = (page - 1) * page_size
query = db.query(PedimentoTransportCatalog).order_by(PedimentoTransportCatalog.code.asc())
items = query.offset(skip).limit(page_size).all()
total = query.count()
return {
"items": [PedimentoTransportCatalogDTO.model_validate(obj) for obj in items],
"total": total,
"page": page,
"page_size": page_size,
}
@router.get("/{code}", response_model=PedimentoTransportCatalogDTO)
async def get_pedimento_transport_catalog(code: str, db: Session = Depends(get_core_db)):
obj = (
db.query(PedimentoTransportCatalog)
.filter(PedimentoTransportCatalog.code == code)
.first()
)
if not obj:
raise HTTPException(status_code=404, detail="Not found")
return obj
@router.post("/", response_model=PedimentoTransportCatalogDTO, status_code=201)
async def create_pedimento_transport_catalog(
data: PedimentoTransportCatalogDTO,
db: Session = Depends(get_core_db),
user=Depends(get_current_user),
):
obj = PedimentoTransportCatalog(**data.model_dump())
db.add(obj)
db.commit()
db.refresh(obj)
return obj
@router.put("/{code}", response_model=PedimentoTransportCatalogDTO)
async def update_pedimento_transport_catalog(
code: str,
data: PedimentoTransportCatalogDTO,
db: Session = Depends(get_core_db),
user=Depends(get_current_user),
):
obj = (
db.query(PedimentoTransportCatalog)
.filter(PedimentoTransportCatalog.code == code)
.first()
)
if not obj:
raise HTTPException(status_code=404, detail="Not found")
for field, value in data.model_dump().items():
setattr(obj, field, value)
db.commit()
db.refresh(obj)
return obj
@router.delete("/{code}", status_code=204)
async def delete_pedimento_transport_catalog(
code: str,
db: Session = Depends(get_core_db),
user=Depends(get_current_user),
):
obj = (
db.query(PedimentoTransportCatalog)
.filter(PedimentoTransportCatalog.code == code)
.first()
)
if not obj:
raise HTTPException(status_code=404, detail="Not found")
db.delete(obj)
db.commit()
return None

View File

@@ -0,0 +1,16 @@
seed = [
("1", "MARITIME", "MARITIMO", "E"),
("2", "DOUBLE-TRACK RAIL", "FERROVIARIO DE DOBLE VIA", "P"),
("3", "ROAD-RAIL", "CARRETERO-FERROVIARIO", "P"),
("4", "AIR", "AEREO.", "E"),
("5", "POSTAL", "POSTAL.", "P"),
("6", "RAIL", "FERROVIARIO.", "P"),
("7", "ROAD", "CARRETERO.", "P"),
("8", "PIPELINE", "TUBERIA.", "P"),
("10", "CABLE", "CABLES.", "P"),
("11", "DUCT", "DUCTOS.", "P"),
("12", "PEDESTRIAN", "PEATONAL.", "P"),
("98", "NOT DECLARED TRANSPORT MODE", "NO SE DECLARA MEDIO DE TRANSPORTE", "P"),
("98E", "NOT DECLARED TRANSPORT MODE", "NO SE DECLARA MEDIO DE TRANSPORTE", "E"),
("99", "OTHERS", "OTROS.", "P"),
]

View File

@@ -15,6 +15,7 @@ from .incoterms.routes import router as incoterms_router
from .invoice_types.routes import router as invoice_types_router from .invoice_types.routes import router as invoice_types_router
from .material_types.routes import router as material_types_router from .material_types.routes import router as material_types_router
from .payment_methods.routes import router as payment_methods_router from .payment_methods.routes import router as payment_methods_router
from .pedimento_transport_catalog.routes import router as pedimento_transport_catalog_router
from .pedimento_codes.routes import router as pedimento_codes_router from .pedimento_codes.routes import router as pedimento_codes_router
from .pedimento_regimens.routes import router as pedimento_regimens_router from .pedimento_regimens.routes import router as pedimento_regimens_router
from .states.routes import router as states_router from .states.routes import router as states_router
@@ -27,6 +28,11 @@ from .valuation_methods.routes import router as valuation_methods_router
router = APIRouter() router = APIRouter()
# Registrar módulos # Registrar módulos
router.include_router(
pedimento_transport_catalog_router,
prefix="/reference_data",
tags=["public / reference_data / pedimento_transport_catalog"],
)
router.include_router( router.include_router(
pedimento_codes_router, pedimento_codes_router,
prefix="/reference_data", prefix="/reference_data",

View File

@@ -17,6 +17,9 @@ from api.v1.modules.public.reference_data.incoterms.models import Incoterm
from api.v1.modules.public.reference_data.invoice_types.models import InvoiceType from api.v1.modules.public.reference_data.invoice_types.models import InvoiceType
from api.v1.modules.public.reference_data.material_types.models import MaterialType from api.v1.modules.public.reference_data.material_types.models import MaterialType
from api.v1.modules.public.reference_data.payment_methods.models import PaymentMethod from api.v1.modules.public.reference_data.payment_methods.models import PaymentMethod
from api.v1.modules.public.reference_data.pedimento_transport_catalog.models import (
PedimentoTransportCatalog,
)
# Orden: PedimentoCode y RegimenPedimento antes de CodePedimentoRegimen para que # Orden: PedimentoCode y RegimenPedimento antes de CodePedimentoRegimen para que
# SQLAlchemy resuelva los nombres en relationship() al configurar el mapper # SQLAlchemy resuelva los nombres en relationship() al configurar el mapper
from api.v1.modules.public.reference_data.pedimento_codes.models import PedimentoCode from api.v1.modules.public.reference_data.pedimento_codes.models import PedimentoCode
@@ -217,6 +220,9 @@ from api.v1.modules.public.reference_data.incoterms.models import Incoterm
from api.v1.modules.public.reference_data.invoice_types.models import InvoiceType from api.v1.modules.public.reference_data.invoice_types.models import InvoiceType
from api.v1.modules.public.reference_data.material_types.models import MaterialType from api.v1.modules.public.reference_data.material_types.models import MaterialType
from api.v1.modules.public.reference_data.payment_methods.models import PaymentMethod from api.v1.modules.public.reference_data.payment_methods.models import PaymentMethod
from api.v1.modules.public.reference_data.pedimento_transport_catalog.models import (
PedimentoTransportCatalog,
)
from api.v1.modules.public.reference_data.pedimento_codes.models import PedimentoCode from api.v1.modules.public.reference_data.pedimento_codes.models import PedimentoCode
from api.v1.modules.public.reference_data.pedimento_regimens.models import ( from api.v1.modules.public.reference_data.pedimento_regimens.models import (
RegimenPedimento, RegimenPedimento,
@@ -296,6 +302,7 @@ def register_audit():
InvoiceType, InvoiceType,
MaterialType, MaterialType,
PaymentMethod, PaymentMethod,
PedimentoTransportCatalog,
PedimentoCode, PedimentoCode,
RegimenPedimento, RegimenPedimento,
Sector, Sector,

View File

@@ -4,6 +4,7 @@
import { Input } from '$lib/components/ui/input'; import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label'; import { Label } from '$lib/components/ui/label';
import * as Select from '$lib/components/ui/select'; import * as Select from '$lib/components/ui/select';
import { getLocale } from '$lib/paraglide/runtime';
import { getExchangeRateByDate } from '$lib/api/dashboard/a76/exchange-rate'; import { getExchangeRateByDate } from '$lib/api/dashboard/a76/exchange-rate';
import { companyStore } from '$lib/stores/company.svelte'; import { companyStore } from '$lib/stores/company.svelte';
import type { Pedimento } from '$lib/api/dashboard/a76/pedimentos'; import type { Pedimento } from '$lib/api/dashboard/a76/pedimentos';
@@ -12,8 +13,6 @@
import type { CustomsBroker } from '$lib/api/dashboard/a76/customs-brokers'; import type { CustomsBroker } from '$lib/api/dashboard/a76/customs-brokers';
import type { ClientProvider } from '$lib/api/dashboard/a76/clients-providers'; import type { ClientProvider } from '$lib/api/dashboard/a76/clients-providers';
import type { CodePedimentoRegimen } from '$lib/api/dashboard/reference_data/code_pedimento_regimens'; import type { CodePedimentoRegimen } from '$lib/api/dashboard/reference_data/code_pedimento_regimens';
import type { TransportType } from '$lib/api/dashboard/reference_data/transport_types';
import type { TransportMode } from '$lib/api/dashboard/reference_data/transport_modes';
import IdentificadoresTabForm from './identifiers-tab-form.svelte'; import IdentificadoresTabForm from './identifiers-tab-form.svelte';
import ExchangeRateDialog from '$lib/components/dashboard/exchange_rate/create-edit-dialog.svelte'; import ExchangeRateDialog from '$lib/components/dashboard/exchange_rate/create-edit-dialog.svelte';
import { Calendar, Clock } from 'lucide-svelte'; import { Calendar, Clock } from 'lucide-svelte';
@@ -29,6 +28,13 @@
import { shortcutStore } from '$lib/stores/shortcut-store'; import { shortcutStore } from '$lib/stores/shortcut-store';
import { focusStore, interactionMode } from '$lib/stores/focus-store'; import { focusStore, interactionMode } from '$lib/stores/focus-store';
type PedimentoTransportCatalog = {
code: string;
transport_en: string;
transport_es: string;
payment_date_code: 'E' | 'P' | string;
};
let { let {
pedimento, pedimento,
formData = $bindable(), formData = $bindable(),
@@ -38,8 +44,7 @@
customsBrokers = [], customsBrokers = [],
clients = [], clients = [],
codePedimentoRegimens = [], codePedimentoRegimens = [],
transportTypes = [], pedimentoTransportCatalog = [],
transportModes = [],
isActive = false isActive = false
}: { }: {
pedimento: Pedimento | null; pedimento: Pedimento | null;
@@ -50,8 +55,7 @@
customsBrokers?: CustomsBroker[]; customsBrokers?: CustomsBroker[];
clients?: ClientProvider[]; clients?: ClientProvider[];
codePedimentoRegimens?: CodePedimentoRegimen[]; codePedimentoRegimens?: CodePedimentoRegimen[];
transportTypes?: TransportType[]; pedimentoTransportCatalog?: PedimentoTransportCatalog[];
transportModes?: TransportMode[];
isActive?: boolean; isActive?: boolean;
} = $props(); } = $props();
@@ -352,27 +356,53 @@
let lastFetchedDate: string | null = null; let lastFetchedDate: string | null = null;
let lastCompanyId: number | null = null; let lastCompanyId: number | null = null;
// Obtener automáticamente el tipo de cambio cuando cambie la fecha de entrada function getTransportByCode(code: string | null | undefined): PedimentoTransportCatalog | undefined {
if (!code) return undefined;
return pedimentoTransportCatalog.find((m) => m.code === code);
}
function getTransportLabel(mode: PedimentoTransportCatalog | undefined): string {
if (!mode) return '';
const locale = getLocale();
return locale === 'en' ? mode.transport_en : mode.transport_es;
}
function getEffectiveExchangeDate(): string | null {
const entryMethod = getTransportByCode(formData?.pedimento_transport_means?.entry_exit);
const paymentCode = (entryMethod?.payment_date_code || 'E').toUpperCase();
if (paymentCode === 'P') {
return formData?.payment_date || null;
}
return formData?.entry_date || null;
}
function getEffectiveDateLabel(): string {
const entryMethod = getTransportByCode(formData?.pedimento_transport_means?.entry_exit);
const paymentCode = (entryMethod?.payment_date_code || 'E').toUpperCase();
return paymentCode === 'P' ? 'fecha de pago' : 'fecha de entrada';
}
// Obtener automáticamente el tipo de cambio cuando cambie la fecha efectiva
$effect(() => { $effect(() => {
const entryDate = formData?.entry_date; const effectiveDate = getEffectiveExchangeDate();
const companyId = companyStore.activeCompany?.id; const companyId = companyStore.activeCompany?.id;
// Solo ejecutar si los valores clave cambiaron // Solo ejecutar si los valores clave cambiaron
if ( if (
formData && formData &&
entryDate && effectiveDate &&
companyId && companyId &&
(entryDate !== lastFetchedDate || companyId !== lastCompanyId) (effectiveDate !== lastFetchedDate || companyId !== lastCompanyId)
) { ) {
lastFetchedDate = entryDate; lastFetchedDate = effectiveDate;
lastCompanyId = companyId; lastCompanyId = companyId;
getExchangeRateByDate(entryDate, companyId) getExchangeRateByDate(effectiveDate, companyId)
.then((usdRate) => { .then((usdRate) => {
if (usdRate && formData) { if (usdRate && formData) {
formData.exchange_rate = usdRate.value; formData.exchange_rate = usdRate.value;
} else { } else {
console.warn('⚠️ [TIPO CAMBIO] No encontrado para fecha:', entryDate); console.warn('⚠️ [TIPO CAMBIO] No encontrado para fecha:', effectiveDate);
} }
}) })
.catch((err) => { .catch((err) => {
@@ -397,13 +427,14 @@
]; ];
export async function checkPaymentDateRate(date: string): Promise<boolean> { export async function checkPaymentDateRate(date: string): Promise<boolean> {
if (!date || !companyStore.activeCompany?.id) return true; const effectiveDate = date || getEffectiveExchangeDate();
if (!effectiveDate || !companyStore.activeCompany?.id) return true;
try { try {
const rate = await getExchangeRateByDate(date, companyStore.activeCompany.id); const rate = await getExchangeRateByDate(effectiveDate, companyStore.activeCompany.id);
if (!rate) { if (!rate) {
// Abrir modal preventivamente // Abrir modal preventivamente
missingExchangeRateDate = date; missingExchangeRateDate = effectiveDate;
showExchangeRateDialog = true; showExchangeRateDialog = true;
return false; return false;
} }
@@ -411,7 +442,7 @@
} catch (error) { } catch (error) {
console.error('Error checking payment date rate:', error); console.error('Error checking payment date rate:', error);
// Si hay error de red, asumimos que falta para forzar reintento/captura segura // Si hay error de red, asumimos que falta para forzar reintento/captura segura
missingExchangeRateDate = date; missingExchangeRateDate = effectiveDate;
showExchangeRateDialog = true; showExchangeRateDialog = true;
return false; return false;
} }
@@ -568,11 +599,16 @@
id="exchange_rate" id="exchange_rate"
type="text" type="text"
value={formData.exchange_rate ? Number(formData.exchange_rate).toFixed(6) : ''} value={formData.exchange_rate ? Number(formData.exchange_rate).toFixed(6) : ''}
placeholder="Se obtiene automáticamente de la fecha de entrada" placeholder={`Se obtiene automáticamente de la ${getEffectiveDateLabel()}`}
readonly readonly
disabled disabled
class="cursor-not-allowed bg-muted" class="cursor-not-allowed bg-muted"
/> />
<p class="text-xs text-muted-foreground">
Tipo de fecha para TC: {getEffectiveDateLabel() === 'fecha de pago'
? 'FECHA PAGO'
: 'FECHA ENTRADA'}
</p>
</div> </div>
</div> </div>
@@ -829,18 +865,16 @@
> >
<Select.Trigger id="entry_exit" class="w-full"> <Select.Trigger id="entry_exit" class="w-full">
<span class="truncate"> <span class="truncate">
{transportModes.find((m) => m.key === formData.pedimento_transport_means.entry_exit) {getTransportLabel(
?.name || getTransportByCode(formData.pedimento_transport_means.entry_exit)
transportTypes.find( ) ||
(t) => t.transport_code === formData.pedimento_transport_means.entry_exit
)?.description ||
formData.pedimento_transport_means.entry_exit || formData.pedimento_transport_means.entry_exit ||
'Seleccionar...'} 'Seleccionar...'}
</span> </span>
</Select.Trigger> </Select.Trigger>
<Select.Content class="max-h-[300px]"> <Select.Content class="max-h-[300px]">
{#each transportModes as mode} {#each pedimentoTransportCatalog as mode}
<Select.Item value={mode.key}>{mode.key} - {mode.name}</Select.Item> <Select.Item value={mode.code}>{mode.code} - {getTransportLabel(mode)}</Select.Item>
{/each} {/each}
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
@@ -856,18 +890,14 @@
> >
<Select.Trigger id="arrival" class="w-full"> <Select.Trigger id="arrival" class="w-full">
<span class="truncate"> <span class="truncate">
{transportModes.find((m) => m.key === formData.pedimento_transport_means.arrival) {getTransportLabel(getTransportByCode(formData.pedimento_transport_means.arrival)) ||
?.name ||
transportTypes.find(
(t) => t.transport_code === formData.pedimento_transport_means.arrival
)?.description ||
formData.pedimento_transport_means.arrival || formData.pedimento_transport_means.arrival ||
'Seleccionar...'} 'Seleccionar...'}
</span> </span>
</Select.Trigger> </Select.Trigger>
<Select.Content class="max-h-[300px]"> <Select.Content class="max-h-[300px]">
{#each transportModes as mode} {#each pedimentoTransportCatalog as mode}
<Select.Item value={mode.key}>{mode.key} - {mode.name}</Select.Item> <Select.Item value={mode.code}>{mode.code} - {getTransportLabel(mode)}</Select.Item>
{/each} {/each}
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>
@@ -883,18 +913,14 @@
> >
<Select.Trigger id="departure" class="w-full"> <Select.Trigger id="departure" class="w-full">
<span class="truncate"> <span class="truncate">
{transportModes.find((m) => m.key === formData.pedimento_transport_means.departure) {getTransportLabel(getTransportByCode(formData.pedimento_transport_means.departure)) ||
?.name ||
transportTypes.find(
(t) => t.transport_code === formData.pedimento_transport_means.departure
)?.description ||
formData.pedimento_transport_means.departure || formData.pedimento_transport_means.departure ||
'Seleccionar...'} 'Seleccionar...'}
</span> </span>
</Select.Trigger> </Select.Trigger>
<Select.Content class="max-h-[300px]"> <Select.Content class="max-h-[300px]">
{#each transportModes as mode} {#each pedimentoTransportCatalog as mode}
<Select.Item value={mode.key}>{mode.key} - {mode.name}</Select.Item> <Select.Item value={mode.code}>{mode.code} - {getTransportLabel(mode)}</Select.Item>
{/each} {/each}
</Select.Content> </Select.Content>
</Select.Root> </Select.Root>

View File

@@ -41,6 +41,7 @@ export const load: PageServerLoad = async ({ params, cookies, fetch }) => {
codePedimentoRegimens: [], codePedimentoRegimens: [],
transportTypes: [], transportTypes: [],
transportModes: [], transportModes: [],
pedimentoTransportCatalog: [],
error: 'Error al cargar catálogos. Verifique la conexión con el backend.' error: 'Error al cargar catálogos. Verifique la conexión con el backend.'
}; };
} }
@@ -57,7 +58,8 @@ export const load: PageServerLoad = async ({ params, cookies, fetch }) => {
clients: data.clients || [], clients: data.clients || [],
codePedimentoRegimens: data.code_pedimento_regimens || [], codePedimentoRegimens: data.code_pedimento_regimens || [],
transportTypes: data.transport_types || [], transportTypes: data.transport_types || [],
transportModes: data.transport_modes || [] transportModes: data.transport_modes || [],
pedimentoTransportCatalog: data.pedimento_transport_catalog || []
}; };
} catch (e) { } catch (e) {
console.error('❌ Error loading new pedimento data:', e); console.error('❌ Error loading new pedimento data:', e);
@@ -73,6 +75,7 @@ export const load: PageServerLoad = async ({ params, cookies, fetch }) => {
codePedimentoRegimens: [], codePedimentoRegimens: [],
transportTypes: [], transportTypes: [],
transportModes: [], transportModes: [],
pedimentoTransportCatalog: [],
error: 'Error al cargar catálogos. Verifique la conexión con el backend.' error: 'Error al cargar catálogos. Verifique la conexión con el backend.'
}; };
} }
@@ -114,7 +117,8 @@ export const load: PageServerLoad = async ({ params, cookies, fetch }) => {
clients: data.clients || [], clients: data.clients || [],
codePedimentoRegimens: data.code_pedimento_regimens || [], codePedimentoRegimens: data.code_pedimento_regimens || [],
transportTypes: data.transport_types || [], transportTypes: data.transport_types || [],
transportModes: data.transport_modes || [] transportModes: data.transport_modes || [],
pedimentoTransportCatalog: data.pedimento_transport_catalog || []
}; };
} catch (e) { } catch (e) {
console.error('Error loading pedimento:', e); console.error('Error loading pedimento:', e);

View File

@@ -56,6 +56,12 @@
import type { CodePedimentoRegimen } from '$lib/api/dashboard/reference_data/code_pedimento_regimens'; import type { CodePedimentoRegimen } from '$lib/api/dashboard/reference_data/code_pedimento_regimens';
import type { TransportType } from '$lib/api/dashboard/reference_data/transport_types'; import type { TransportType } from '$lib/api/dashboard/reference_data/transport_types';
import type { TransportMode } from '$lib/api/dashboard/reference_data/transport_modes'; import type { TransportMode } from '$lib/api/dashboard/reference_data/transport_modes';
type PedimentoTransportCatalog = {
code: string;
transport_en: string;
transport_es: string;
payment_date_code: 'E' | 'P' | string;
};
// Get sidebar context // Get sidebar context
const sidebar = useSidebar(); const sidebar = useSidebar();
@@ -71,6 +77,7 @@
codePedimentoRegimens?: CodePedimentoRegimen[]; codePedimentoRegimens?: CodePedimentoRegimen[];
transportTypes?: TransportType[]; transportTypes?: TransportType[];
transportModes?: TransportMode[]; transportModes?: TransportMode[];
pedimentoTransportCatalog?: PedimentoTransportCatalog[];
user?: any; user?: any;
companies?: any[]; companies?: any[];
authenticated?: boolean; authenticated?: boolean;
@@ -153,6 +160,18 @@
} }
} }
function getExchangeDateForPedimento(formData: any): { date: string | null; label: string } {
const catalog = (data.pedimentoTransportCatalog || []) as PedimentoTransportCatalog[];
const entryMethod = catalog.find(
(item) => item.code === formData?.pedimento_transport_means?.entry_exit
);
const paymentDateCode = (entryMethod?.payment_date_code || 'E').toUpperCase();
if (paymentDateCode === 'P') {
return { date: formData?.payment_date || null, label: 'fecha de pago' };
}
return { date: formData?.entry_date || null, label: 'fecha de entrada' };
}
// ID del pedimento // ID del pedimento
let pedimentoId = $state<number | null>(data.pedimentoId ?? null); let pedimentoId = $state<number | null>(data.pedimentoId ?? null);
@@ -395,11 +414,10 @@
saving = true; saving = true;
try { try {
// Verificar tipo de cambio antes de guardar si hay instancia del tab general y hay fecha de pago // Verificar tipo de cambio según catalogo de transporte (E/P)
if (generalTabInstance && generalFormData?.payment_date) { if (generalTabInstance && generalFormData) {
const rateExists = await generalTabInstance.checkPaymentDateRate( const exchangeRef = getExchangeDateForPedimento(generalFormData);
generalFormData.payment_date const rateExists = await generalTabInstance.checkPaymentDateRate(exchangeRef.date || '');
);
if (!rateExists) { if (!rateExists) {
saving = false; saving = false;
// Asegurar que se muestre el tab general // Asegurar que se muestre el tab general
@@ -432,8 +450,9 @@
} }
} }
// Validar tipo de cambio en create y update // Validar tipo de cambio en create y update segun fecha efectiva del metodo de transporte
if (generalFormData) { if (generalFormData) {
const exchangeRef = getExchangeDateForPedimento(generalFormData);
const rate = generalFormData.exchange_rate; const rate = generalFormData.exchange_rate;
if ( if (
rate === null || rate === null ||
@@ -443,11 +462,11 @@
) { ) {
saving = false; saving = false;
activeTab = 'general'; activeTab = 'general';
const date = generalFormData.entry_date || ''; const date = exchangeRef.date || '';
toast.error( toast.error(
Number(rate) <= 0 && rate !== null && rate !== undefined Number(rate) <= 0 && rate !== null && rate !== undefined
? 'El tipo de cambio debe ser mayor a 0. Registra el tipo de cambio para la fecha de entrada.' ? `El tipo de cambio debe ser mayor a 0. Registra el tipo de cambio para la ${exchangeRef.label}.`
: 'No hay tipo de cambio registrado para la fecha de entrada. Por favor, regístralo antes de guardar.' : `No hay tipo de cambio registrado para la ${exchangeRef.label}. Por favor, regístralo antes de guardar.`
); );
if (date) { if (date) {
missingExchangeRateDate = date; missingExchangeRateDate = date;
@@ -1129,8 +1148,7 @@
customsBrokers={data.customsBrokers} customsBrokers={data.customsBrokers}
clients={data.clients} clients={data.clients}
codePedimentoRegimens={data.codePedimentoRegimens} codePedimentoRegimens={data.codePedimentoRegimens}
transportTypes={data.transportTypes} pedimentoTransportCatalog={data.pedimentoTransportCatalog}
transportModes={data.transportModes}
isActive={activeTab === 'general'} isActive={activeTab === 'general'}
/> />
</Tabs.Content> </Tabs.Content>