diff --git a/backend/alembic/versions/7937209f9718_seed_initial_data.py b/backend/alembic/versions/7937209f9718_seed_initial_data.py index ddc5fcce..055eb133 100644 --- a/backend/alembic/versions/7937209f9718_seed_initial_data.py +++ b/backend/alembic/versions/7937209f9718_seed_initial_data.py @@ -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 ( 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 ( seed as transport_types_seed, ) @@ -98,6 +101,28 @@ def upgrade() -> None: return "NULL" 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 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( [ f"('{code}', '{desc.replace(chr(39), chr(39)*2)}')" @@ -475,6 +514,16 @@ def upgrade() -> None: def downgrade() -> None: """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("historical_tariff_fractions", schema="a76") op.drop_table("canadian_tariff_fractions", schema="a76") diff --git a/backend/api/v1/modules/a76/layouts_csv/pedmientos/common/fk_loader.py b/backend/api/v1/modules/a76/layouts_csv/pedmientos/common/fk_loader.py index cb55fefe..e72ded53 100644 --- a/backend/api/v1/modules/a76/layouts_csv/pedmientos/common/fk_loader.py +++ b/backend/api/v1/modules/a76/layouts_csv/pedmientos/common/fk_loader.py @@ -35,7 +35,7 @@ def load_pedimentos_fk_sets( valid_clave_regimen_tipo, # (pedimento_code, regimen_code, type_code) valid_aduana_seccion, # customs_code 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) 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.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_transport_catalog.models import ( + PedimentoTransportCatalog, + ) valid_client_ids: Set[int] = set() valid_regimes: Set[str] = set() @@ -96,6 +99,10 @@ def load_pedimentos_fk_sets( for cs in session.query(CustomsSection).all(): 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 ( session.query(CustomsBroker) .filter( diff --git a/backend/api/v1/modules/a76/pedmientos/catalog_service.py b/backend/api/v1/modules/a76/pedmientos/catalog_service.py index cc0dd1ab..d283f2a6 100644 --- a/backend/api/v1/modules/a76/pedmientos/catalog_service.py +++ b/backend/api/v1/modules/a76/pedmientos/catalog_service.py @@ -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.transport_types.models import TransportType 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 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.transport_types.dto import TransportTypeDTO 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.clients_and_providers.dto import ClientProviderResponseDTO from .dtos.pedimentos import PedimentosResponse @@ -71,6 +77,16 @@ class PedimentoCatalogService: except Exception as 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 def fetch_tenant_data(): # Customs Brokers diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_transport_means.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_transport_means.py index ba86131a..d769822f 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_transport_means.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_transport_means.py @@ -10,27 +10,27 @@ class PedimentoTransportMeansBase(BaseModel): pedimento_id: Optional[int] = Field(None, description="Pedimento ID") tenant_id: Optional[int] = Field(None, description="Tenant ID") destination: Optional[int] = Field(None, description="Destination") - entry_exit: Optional[str] = Field(None, max_length=2, description="Entry/exit") - arrival: str = Field(..., max_length=2, description="Arrival") - departure: str = Field(..., max_length=2, description="Departure") + entry_exit: Optional[str] = Field(None, max_length=3, description="Entry/exit") + arrival: str = Field(..., max_length=3, description="Arrival") + departure: str = Field(..., max_length=3, description="Departure") class PedimentoTransportMeansCreate(BaseModel): """Schema for creating a new Pedimento Transport Means - pedimento_id and tenant_id are set by backend""" destination: Optional[int] = Field(None, description="Destination") - entry_exit: Optional[str] = Field(None, max_length=2, description="Entry/exit") - arrival: str = Field(..., max_length=2, description="Arrival") - departure: str = Field(..., max_length=2, description="Departure") + entry_exit: Optional[str] = Field(None, max_length=3, description="Entry/exit") + arrival: str = Field(..., max_length=3, description="Arrival") + departure: str = Field(..., max_length=3, description="Departure") class PedimentoTransportMeansUpdate(BaseModel): """Schema for updating a Pedimento Transport Means""" destination: Optional[int] = None - entry_exit: Optional[str] = Field(None, max_length=2) - arrival: Optional[str] = Field(None, max_length=2) - departure: Optional[str] = Field(None, max_length=2) + entry_exit: Optional[str] = Field(None, max_length=3) + arrival: Optional[str] = Field(None, max_length=3) + departure: Optional[str] = Field(None, max_length=3) class PedimentoTransportMeansResponse(PedimentoTransportMeansBase): diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_transport_means.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_transport_means.py index 271f69d4..a1702aa2 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_transport_means.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_transport_means.py @@ -39,9 +39,9 @@ class PedimentoTransportMeans(Base, TenantScopedMixin, TimestampMixin): pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) destination: Mapped[int] = mapped_column(SmallInteger) - entry_exit: Mapped[str] = mapped_column(String(2)) - arrival: Mapped[str] = mapped_column(String(2)) - departure: Mapped[str] = mapped_column(String(2)) + entry_exit: Mapped[str] = mapped_column(String(3)) + arrival: Mapped[str] = mapped_column(String(3)) + departure: Mapped[str] = mapped_column(String(3)) pedimento: Mapped["Pedimentos"] = relationship( "Pedimentos", back_populates="pedimento_transport_means" diff --git a/backend/api/v1/modules/a76/pedmientos/schemas.py b/backend/api/v1/modules/a76/pedmientos/schemas.py index 8eec714e..e68d7a9e 100644 --- a/backend/api/v1/modules/a76/pedmientos/schemas.py +++ b/backend/api/v1/modules/a76/pedmientos/schemas.py @@ -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.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.pedimento_transport_catalog.dto import ( + PedimentoTransportCatalogDTO, +) from .dtos.pedimentos import PedimentosResponse @@ -26,6 +29,7 @@ class PedimentoCatalogsResponse(BaseModel): clients: List[ClientProviderResponseDTO] = [] transport_types: List[TransportTypeDTO] = [] transport_modes: List[TransportModeDTO] = [] + pedimento_transport_catalog: List[PedimentoTransportCatalogDTO] = [] class PedimentoCreationResponse(PedimentoCatalogsResponse): diff --git a/backend/api/v1/modules/a76/reports/movements/saldos/csv_utils.py b/backend/api/v1/modules/a76/reports/movements/saldos/csv_utils.py index ddc10f80..9ea3bac1 100644 --- a/backend/api/v1/modules/a76/reports/movements/saldos/csv_utils.py +++ b/backend/api/v1/modules/a76/reports/movements/saldos/csv_utils.py @@ -227,7 +227,7 @@ BASE_SELECT = """ COALESCE(icm.vucem_operation_num,'') AS "C41", COALESCE(cl.material_key,'') AS "C42", 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", '' AS "C47", COALESCE(ped.pedimento_code,'') AS "C48", @@ -243,6 +243,8 @@ BASE_JOINS = """ 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.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.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 @@ -440,20 +442,21 @@ def _build_row( peso_usado = (cant_ret * peso_neto / cant_orig) if cant_orig != 0 else Decimal(0) peso_saldo = peso_neto - peso_usado - # TIPO DE CAMBIO — per Clarion logic: - # Si TipoPedimentoTransporteE IN ('4','1','98E') → usar Fecha_Inicio, else Fecha_Pago + # TIPO DE CAMBIO: + # 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. tc = Decimal(1) fecha_pago = row.get("C7") fecha_inicio = row.get("C9") 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 # TIPO DE CAMBIO tc_fecha = None 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: tc_fecha = fecha_factura diff --git a/backend/api/v1/modules/public/reference_data/pedimento_transport_catalog/dto.py b/backend/api/v1/modules/public/reference_data/pedimento_transport_catalog/dto.py new file mode 100644 index 00000000..5289649a --- /dev/null +++ b/backend/api/v1/modules/public/reference_data/pedimento_transport_catalog/dto.py @@ -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) diff --git a/backend/api/v1/modules/public/reference_data/pedimento_transport_catalog/models.py b/backend/api/v1/modules/public/reference_data/pedimento_transport_catalog/models.py new file mode 100644 index 00000000..95554872 --- /dev/null +++ b/backend/api/v1/modules/public/reference_data/pedimento_transport_catalog/models.py @@ -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"" + ) diff --git a/backend/api/v1/modules/public/reference_data/pedimento_transport_catalog/routes.py b/backend/api/v1/modules/public/reference_data/pedimento_transport_catalog/routes.py new file mode 100644 index 00000000..a8667238 --- /dev/null +++ b/backend/api/v1/modules/public/reference_data/pedimento_transport_catalog/routes.py @@ -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 diff --git a/backend/api/v1/modules/public/reference_data/pedimento_transport_catalog/seed.py b/backend/api/v1/modules/public/reference_data/pedimento_transport_catalog/seed.py new file mode 100644 index 00000000..2742e590 --- /dev/null +++ b/backend/api/v1/modules/public/reference_data/pedimento_transport_catalog/seed.py @@ -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"), +] diff --git a/backend/api/v1/modules/public/reference_data/router.py b/backend/api/v1/modules/public/reference_data/router.py index fb4a322f..9ae22189 100644 --- a/backend/api/v1/modules/public/reference_data/router.py +++ b/backend/api/v1/modules/public/reference_data/router.py @@ -15,6 +15,7 @@ from .incoterms.routes import router as incoterms_router from .invoice_types.routes import router as invoice_types_router from .material_types.routes import router as material_types_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_regimens.routes import router as pedimento_regimens_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() # Registrar módulos +router.include_router( + pedimento_transport_catalog_router, + prefix="/reference_data", + tags=["public / reference_data / pedimento_transport_catalog"], +) router.include_router( pedimento_codes_router, prefix="/reference_data", diff --git a/backend/main.py b/backend/main.py index b5c0f325..39890a34 100644 --- a/backend/main.py +++ b/backend/main.py @@ -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.material_types.models import MaterialType 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 # SQLAlchemy resuelva los nombres en relationship() al configurar el mapper 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.material_types.models import MaterialType 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_regimens.models import ( RegimenPedimento, @@ -296,6 +302,7 @@ def register_audit(): InvoiceType, MaterialType, PaymentMethod, + PedimentoTransportCatalog, PedimentoCode, RegimenPedimento, Sector, diff --git a/frontend/src/lib/components/dashboard/pedimentos/edit/general-tab-form.svelte b/frontend/src/lib/components/dashboard/pedimentos/edit/general-tab-form.svelte index 88b31a85..e41e2ae1 100644 --- a/frontend/src/lib/components/dashboard/pedimentos/edit/general-tab-form.svelte +++ b/frontend/src/lib/components/dashboard/pedimentos/edit/general-tab-form.svelte @@ -4,6 +4,7 @@ import { Input } from '$lib/components/ui/input'; import { Label } from '$lib/components/ui/label'; import * as Select from '$lib/components/ui/select'; + import { getLocale } from '$lib/paraglide/runtime'; import { getExchangeRateByDate } from '$lib/api/dashboard/a76/exchange-rate'; import { companyStore } from '$lib/stores/company.svelte'; 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 { ClientProvider } from '$lib/api/dashboard/a76/clients-providers'; 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 ExchangeRateDialog from '$lib/components/dashboard/exchange_rate/create-edit-dialog.svelte'; import { Calendar, Clock } from 'lucide-svelte'; @@ -29,6 +28,13 @@ import { shortcutStore } from '$lib/stores/shortcut-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 { pedimento, formData = $bindable(), @@ -38,8 +44,7 @@ customsBrokers = [], clients = [], codePedimentoRegimens = [], - transportTypes = [], - transportModes = [], + pedimentoTransportCatalog = [], isActive = false }: { pedimento: Pedimento | null; @@ -50,8 +55,7 @@ customsBrokers?: CustomsBroker[]; clients?: ClientProvider[]; codePedimentoRegimens?: CodePedimentoRegimen[]; - transportTypes?: TransportType[]; - transportModes?: TransportMode[]; + pedimentoTransportCatalog?: PedimentoTransportCatalog[]; isActive?: boolean; } = $props(); @@ -352,27 +356,53 @@ let lastFetchedDate: string | 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(() => { - const entryDate = formData?.entry_date; + const effectiveDate = getEffectiveExchangeDate(); const companyId = companyStore.activeCompany?.id; // Solo ejecutar si los valores clave cambiaron if ( formData && - entryDate && + effectiveDate && companyId && - (entryDate !== lastFetchedDate || companyId !== lastCompanyId) + (effectiveDate !== lastFetchedDate || companyId !== lastCompanyId) ) { - lastFetchedDate = entryDate; + lastFetchedDate = effectiveDate; lastCompanyId = companyId; - getExchangeRateByDate(entryDate, companyId) + getExchangeRateByDate(effectiveDate, companyId) .then((usdRate) => { if (usdRate && formData) { formData.exchange_rate = usdRate.value; } else { - console.warn('⚠️ [TIPO CAMBIO] No encontrado para fecha:', entryDate); + console.warn('⚠️ [TIPO CAMBIO] No encontrado para fecha:', effectiveDate); } }) .catch((err) => { @@ -397,13 +427,14 @@ ]; export async function checkPaymentDateRate(date: string): Promise { - if (!date || !companyStore.activeCompany?.id) return true; + const effectiveDate = date || getEffectiveExchangeDate(); + if (!effectiveDate || !companyStore.activeCompany?.id) return true; try { - const rate = await getExchangeRateByDate(date, companyStore.activeCompany.id); + const rate = await getExchangeRateByDate(effectiveDate, companyStore.activeCompany.id); if (!rate) { // Abrir modal preventivamente - missingExchangeRateDate = date; + missingExchangeRateDate = effectiveDate; showExchangeRateDialog = true; return false; } @@ -411,7 +442,7 @@ } catch (error) { console.error('Error checking payment date rate:', error); // Si hay error de red, asumimos que falta para forzar reintento/captura segura - missingExchangeRateDate = date; + missingExchangeRateDate = effectiveDate; showExchangeRateDialog = true; return false; } @@ -568,11 +599,16 @@ id="exchange_rate" type="text" 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 disabled class="cursor-not-allowed bg-muted" /> +

+ Tipo de fecha para TC: {getEffectiveDateLabel() === 'fecha de pago' + ? 'FECHA PAGO' + : 'FECHA ENTRADA'} +

@@ -829,18 +865,16 @@ > - {transportModes.find((m) => m.key === formData.pedimento_transport_means.entry_exit) - ?.name || - transportTypes.find( - (t) => t.transport_code === formData.pedimento_transport_means.entry_exit - )?.description || + {getTransportLabel( + getTransportByCode(formData.pedimento_transport_means.entry_exit) + ) || formData.pedimento_transport_means.entry_exit || 'Seleccionar...'} - {#each transportModes as mode} - {mode.key} - {mode.name} + {#each pedimentoTransportCatalog as mode} + {mode.code} - {getTransportLabel(mode)} {/each} @@ -856,18 +890,14 @@ > - {transportModes.find((m) => m.key === formData.pedimento_transport_means.arrival) - ?.name || - transportTypes.find( - (t) => t.transport_code === formData.pedimento_transport_means.arrival - )?.description || + {getTransportLabel(getTransportByCode(formData.pedimento_transport_means.arrival)) || formData.pedimento_transport_means.arrival || 'Seleccionar...'} - {#each transportModes as mode} - {mode.key} - {mode.name} + {#each pedimentoTransportCatalog as mode} + {mode.code} - {getTransportLabel(mode)} {/each} @@ -883,18 +913,14 @@ > - {transportModes.find((m) => m.key === formData.pedimento_transport_means.departure) - ?.name || - transportTypes.find( - (t) => t.transport_code === formData.pedimento_transport_means.departure - )?.description || + {getTransportLabel(getTransportByCode(formData.pedimento_transport_means.departure)) || formData.pedimento_transport_means.departure || 'Seleccionar...'} - {#each transportModes as mode} - {mode.key} - {mode.name} + {#each pedimentoTransportCatalog as mode} + {mode.code} - {getTransportLabel(mode)} {/each} diff --git a/frontend/src/routes/dashboard/pedimentos/edit/[id]/+page.server.ts b/frontend/src/routes/dashboard/pedimentos/edit/[id]/+page.server.ts index 2e923b7d..e8cbfed3 100644 --- a/frontend/src/routes/dashboard/pedimentos/edit/[id]/+page.server.ts +++ b/frontend/src/routes/dashboard/pedimentos/edit/[id]/+page.server.ts @@ -41,6 +41,7 @@ export const load: PageServerLoad = async ({ params, cookies, fetch }) => { codePedimentoRegimens: [], transportTypes: [], transportModes: [], + pedimentoTransportCatalog: [], 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 || [], codePedimentoRegimens: data.code_pedimento_regimens || [], transportTypes: data.transport_types || [], - transportModes: data.transport_modes || [] + transportModes: data.transport_modes || [], + pedimentoTransportCatalog: data.pedimento_transport_catalog || [] }; } catch (e) { console.error('❌ Error loading new pedimento data:', e); @@ -73,6 +75,7 @@ export const load: PageServerLoad = async ({ params, cookies, fetch }) => { codePedimentoRegimens: [], transportTypes: [], transportModes: [], + pedimentoTransportCatalog: [], 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 || [], codePedimentoRegimens: data.code_pedimento_regimens || [], transportTypes: data.transport_types || [], - transportModes: data.transport_modes || [] + transportModes: data.transport_modes || [], + pedimentoTransportCatalog: data.pedimento_transport_catalog || [] }; } catch (e) { console.error('Error loading pedimento:', e); diff --git a/frontend/src/routes/dashboard/pedimentos/edit/[id]/+page.svelte b/frontend/src/routes/dashboard/pedimentos/edit/[id]/+page.svelte index 8f6c6825..6a868691 100644 --- a/frontend/src/routes/dashboard/pedimentos/edit/[id]/+page.svelte +++ b/frontend/src/routes/dashboard/pedimentos/edit/[id]/+page.svelte @@ -56,6 +56,12 @@ 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'; + type PedimentoTransportCatalog = { + code: string; + transport_en: string; + transport_es: string; + payment_date_code: 'E' | 'P' | string; + }; // Get sidebar context const sidebar = useSidebar(); @@ -71,6 +77,7 @@ codePedimentoRegimens?: CodePedimentoRegimen[]; transportTypes?: TransportType[]; transportModes?: TransportMode[]; + pedimentoTransportCatalog?: PedimentoTransportCatalog[]; user?: any; companies?: any[]; 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 let pedimentoId = $state(data.pedimentoId ?? null); @@ -395,11 +414,10 @@ saving = true; try { - // Verificar tipo de cambio antes de guardar si hay instancia del tab general y hay fecha de pago - if (generalTabInstance && generalFormData?.payment_date) { - const rateExists = await generalTabInstance.checkPaymentDateRate( - generalFormData.payment_date - ); + // Verificar tipo de cambio según catalogo de transporte (E/P) + if (generalTabInstance && generalFormData) { + const exchangeRef = getExchangeDateForPedimento(generalFormData); + const rateExists = await generalTabInstance.checkPaymentDateRate(exchangeRef.date || ''); if (!rateExists) { saving = false; // 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) { + const exchangeRef = getExchangeDateForPedimento(generalFormData); const rate = generalFormData.exchange_rate; if ( rate === null || @@ -443,11 +462,11 @@ ) { saving = false; activeTab = 'general'; - const date = generalFormData.entry_date || ''; + const date = exchangeRef.date || ''; toast.error( 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.' - : 'No hay tipo de cambio registrado para la fecha de entrada. Por favor, regístralo antes de guardar.' + ? `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 ${exchangeRef.label}. Por favor, regístralo antes de guardar.` ); if (date) { missingExchangeRateDate = date; @@ -1129,8 +1148,7 @@ customsBrokers={data.customsBrokers} clients={data.clients} codePedimentoRegimens={data.codePedimentoRegimens} - transportTypes={data.transportTypes} - transportModes={data.transportModes} + pedimentoTransportCatalog={data.pedimentoTransportCatalog} isActive={activeTab === 'general'} />