Se integro el modulo de importacion

This commit is contained in:
2026-02-16 10:36:23 -06:00
parent febae9fc01
commit 3d691d6881
11 changed files with 1274 additions and 118 deletions

View File

@@ -0,0 +1,330 @@
from typing import List, Dict, Any, Tuple
from sqlalchemy.orm import Session, joinedload
from datetime import datetime
from .schemas import Mainx30GenerationRequest, ErrorValidacion
# --- MODELOS A76 ---
from api.v1.modules.a76.invoices.models import InvoiceHeader, InvoiceComplianceMx
from api.v1.modules.a76.items.models import Item
from api.v1.modules.a76.items.line_items.models import LineItem
from api.v1.modules.a76.clients_and_providers.models import ClientProvider
class ScaiiProcessor:
def __init__(self):
self.cuenta_partidas = 0
self.cuenta_facturas = 0
self.valor_total_factura = 0.0
self.flete_total = 0.0
self.peso_bruto_factura = 0.0
self.peso_neto_factura = 0.0
self.errores: List[ErrorValidacion] = []
def _obtener_datos_cliente(self, cliente: ClientProvider) -> dict:
"""Extrae de manera segura los datos del cliente/dirección"""
address = cliente.address
pais_raw = (address.country or "MX").upper() if address else "MX"
pais = "MX"
if pais_raw in ["MEXICO", "MEX", "MX"]:
pais = "MX"
elif pais_raw in ["USA", "US", "UNITED STATES"]:
pais = "US"
else:
pais = pais_raw[:2]
tax_id = cliente.rfc or ""
data = {
"nombre": (cliente.name or "")[:39],
"tax_id": tax_id[:15],
"broker": "", "calle": "", "cp": "", "ciudad": "", "estado": "", "pais": pais, "tel": ""
}
if cliente.programs:
data["broker"] = (cliente.programs.broker or "")[:6]
if address:
calle_comp = f"{address.streets or ''} {address.exterior_number or ''}".strip()
# Clarion expects 20 chars for city_state: 5 CP + 11 City + 4 State
cp_formatted = (address.postal_code or "")[:5]
city_formatted = (address.city or "")[:11]
state_formatted = (address.state or "")[:4]
data["city_state"] = f"{cp_formatted:<5}{city_formatted:<11}{state_formatted:<4}"
data["calle"] = calle_comp[:35]
data["cp"] = (address.postal_code or "")[:9]
data["ciudad"] = (address.city or "")[:20]
data["estado"] = (address.state or "")[:2].upper()
data["tel"] = (address.phone or "")[:15]
return data
def procesar_facturas(
self, db: Session, manifiesto: str, empresa_dict: Dict[str, Any], request: Mainx30GenerationRequest
) -> Tuple[List[str], List[ErrorValidacion]]:
# This base method is used for Manifest-based processing (Exportacion/Legacy)
lineas = []
self.errores = []
return lineas, self.errores
def _agregar_error(self, partida, id_err, desc, sol, tipo):
self.errores.append(ErrorValidacion(partida=partida, linea=0, descripcion=f"[ {id_err} ] {desc}", soluciones=sol, identificador=tipo))
# --- PROCESADORES ESPECIFICOS ---
class ScafDefProcessor(ScaiiProcessor):
"""Procesador para Importación Definitiva"""
def __init__(self):
super().__init__()
def procesar_facturas(
self, db: Session, facturas_nums: List[str], empresa_dict: Dict[str, Any], request: Mainx30GenerationRequest
) -> Tuple[List[str], List[ErrorValidacion]]:
# Placeholder for Definitiva logic
lineas = []
self.errores = []
return lineas, self.errores
class ScafTempProcessor(ScaiiProcessor):
"""Procesador para Importación Temporal basado en lógica Clarion"""
def __init__(self):
super().__init__()
def procesar_facturas(
self, db: Session, facturas_nums: List[str], empresa_dict: Dict[str, Any], request: Mainx30GenerationRequest
) -> Tuple[List[str], List[ErrorValidacion]]:
lineas = []
self.errores = []
# 1. Traer Facturas por número (Importación Temporal trabaja por factura)
facturas = db.query(InvoiceHeader).join(
InvoiceComplianceMx, InvoiceHeader.id == InvoiceComplianceMx.invoice_id
).options(
joinedload(InvoiceHeader.financials),
joinedload(InvoiceHeader.compliance_mx),
joinedload(InvoiceHeader.logistics)
).filter(
InvoiceHeader.invoice_number.in_(facturas_nums)
).all()
# Helper for ports from request
entry_port = request.entry_port or ""
exit_port = request.exit_port or ""
# Date for transmission records (YYMMDD)
fecha_trans = datetime.now().strftime("%y%m%d")
# Port descriptions from empresa_dict (populated in service.py)
entry_port_desc = empresa_dict.get('entry_port_desc', 'PUERTO ENTRADA')[:15]
exit_port_desc = empresa_dict.get('exit_port_desc', 'PUERTO SALIDA')[:15]
main_activity = empresa_dict.get('main_activity', 'RAW MATERIAL')[:30]
city_state = empresa_dict.get('city_state', '')[:30]
for factura in facturas:
self.cuenta_facturas += 1
f_val_total = 0.0
f_pb = 0.0
f_pn = 0.0
f_consec_partidas = 0
# MF01: Header per Invoice in Importacion Temporal
mod_trans = factura.logistics.transport_mode if factura.logistics else "30"
lineas.append(
f"MF01{empresa_dict['broker'][:6]:<6}"
f"{exit_port[:5]:<5}"
f"{entry_port[:5]:<5}"
f"{fecha_trans}"
f" {mod_trans[:2]:<2}"
f"{fecha_trans}"
f"{factura.invoice_number[:15]:<15}"
)
self.cuenta_partidas += 1
# MF03: Dynamic Driver/Carrier info
conductor = (factura.logistics.driver_name or "") if factura.logistics else ""
# Carrier Logic from Clarion:
# IF ERRORCODE() = 35 THEN Loc:NumTransporte = '00000TRUCK'
# ELSE IF GenTra:NombreCorto = '' THEN Loc:NumTransporte = GenTra:Nombre
# ELSE Loc:NumTransporte = GenTra:NombreCorto
carrier = "00000TRUCK"
if factura.logistics:
# Logic simplified: assume carrier_id holds the correct code/name or fallback
carrier = (factura.logistics.carrier_id or "00000TRUCK")
lineas.append(
f"MF03{carrier[:10]:<10}{conductor[:23]:<23}{entry_port_desc:<15}{exit_port_desc:<15}"
)
self.cuenta_partidas += 1
# MF20
num_transporte = (factura.logistics.transport_num or "") if factura.logistics else ""
lineas.append(
f"MF20{factura.invoice_number[:15]:<15}I{num_transporte[:15]:<15}{city_state[:20]:<20}"
f"{exit_port[:5]:<5}{entry_port_desc:<15} "
)
self.cuenta_partidas += 1
# MF22
lineas.append(f"MF22{main_activity:<60}")
self.cuenta_partidas += 1
# IV01: Header
f_fecha = factura.invoice_date.strftime("%y%m%d") if factura.invoice_date else "000000"
flete = int(round(float(factura.financials.freight or 0))) if factura.financials else 0
self.flete_total += float(factura.financials.freight or 0) if factura.financials else 0.0
s_tax = ""; c_tax = ""
if factura.compliance_mx:
if factura.compliance_mx.sold_to_id:
c_obj = db.query(ClientProvider).filter(ClientProvider.id == factura.compliance_mx.sold_to_id).first()
if c_obj: c_tax = c_obj.rfc[:12] if c_obj.rfc else ""
if factura.compliance_mx.provider_id:
s_obj = db.query(ClientProvider).filter(ClientProvider.id == factura.compliance_mx.provider_id).first()
if s_obj: s_tax = s_obj.rfc[:12] if s_obj.rfc else ""
lineas.append(
f"IV01{factura.invoice_number[:15]:<15}{f_fecha}{entry_port:<5}{' ':<11}C"
f"{empresa_dict['broker'][:6]:<6}{flete:08d}{c_tax:<13}"
)
self.cuenta_partidas += 1
# IV02: Shipper Name
lineas.append(f"IV02 {empresa_dict['nombre_empresa'][:40]:<40}")
self.cuenta_partidas += 1
# IV10: Responsible
lineas.append(f"IV10 {main_activity[:30]:<30}{empresa_dict['responsable'][:30]:<30}")
self.cuenta_partidas += 1
# IV11: Obs
lineas.append(f"IV11H")
lineas.append(f"IV11F")
self.cuenta_partidas += 2
# IV12-14 (S, C, T, I)
# Shipper (S)
if factura.compliance_mx and factura.compliance_mx.provider_id:
s_cliente = db.query(ClientProvider).filter(ClientProvider.id == factura.compliance_mx.provider_id).first()
if s_cliente:
s_data = self._obtener_datos_cliente(s_cliente)
lineas.append(f"IV12S {s_data['nombre'][:39]:<39}")
lineas.append(f"IV13S {s_data['calle'][:35]:<35}{s_data['cp']:<9}")
lineas.append(f"IV14S{s_data['ciudad'][:20]:<20}{s_data['state_full'][:2] if 'state_full' in s_data else s_data['estado'][:2]}{s_data['pais'][:2]}{s_data['tel'][:30]:<30}{s_data['tax_id']:<15}00000")
self.cuenta_partidas += 3
# Consignee (C), Ship To (T), and Intermediate (I)
if factura.compliance_mx and factura.compliance_mx.sold_to_id:
c_cliente = db.query(ClientProvider).filter(ClientProvider.id == factura.compliance_mx.sold_to_id).first()
if c_cliente:
c_data = self._obtener_datos_cliente(c_cliente)
l12c = f"IV12C {c_data['nombre'][:39]:<39}"
l13c = f"IV13C {c_data['calle'][:35]:<35}{c_data['cp']:<9}"
l14c = f"IV14C{c_data['ciudad'][:20]:<20}{c_data['estado'][:2]}{c_data['pais'][:2]}{c_data['tel'][:30]:<30}{c_data['tax_id']:<15}00000"
# Output C
lineas.extend([l12c, l13c, l14c])
self.cuenta_partidas += 3
# T (Ship To) - Only if different from C
t_id = factura.compliance_mx.shipped_to_id
if t_id and t_id != factura.compliance_mx.sold_to_id:
t_cl = db.query(ClientProvider).filter(ClientProvider.id == t_id).first()
if t_cl:
t_data = self._obtener_datos_cliente(t_cl)
lineas.append(f"IV12T {t_data['nombre'][:39]:<39}")
lineas.append(f"IV13T {t_data['calle'][:35]:<35}{t_data['cp']:<9}")
lineas.append(f"IV14T{t_data['ciudad'][:20]:<20}{t_data['estado'][:2]}{t_data['pais'][:2]}{t_data['tel'][:30]:<30}{t_data['tax_id']:<15}00000")
self.cuenta_partidas += 3
# I (Intermediate) - Only if different from C and T
i_id = factura.compliance_mx.shipped_by_id
if i_id and i_id != factura.compliance_mx.sold_to_id and i_id != t_id:
i_cl = db.query(ClientProvider).filter(ClientProvider.id == i_id).first()
if i_cl:
i_data = self._obtener_datos_cliente(i_cl)
lineas.append(f"IV12I {i_data['nombre'][:39]:<39}")
lineas.append(f"IV13I {i_data['calle'][:35]:<35}{i_data['cp']:<9}")
lineas.append(f"IV14I{i_data['ciudad'][:20]:<20}{i_data['estado'][:2]}{i_data['pais'][:2]}{i_data['tel'][:30]:<30}{i_data['tax_id']:<15}00000")
self.cuenta_partidas += 3
# Partidas IV20, IV21, IV22, IV24, IV26, IV27
# Let's get Item IDs first to ensure we find them
items_headers = db.query(Item).filter(Item.invoice_id == factura.id).all()
item_ids = [ih.id for ih in items_headers]
if item_ids:
items_query = db.query(LineItem).filter(
LineItem.item_id.in_(item_ids)
).options(
joinedload(LineItem.part_info),
joinedload(LineItem.description),
joinedload(LineItem.financial),
joinedload(LineItem.quantity),
joinedload(LineItem.customs),
joinedload(LineItem.unit_of_measure_info)
).all()
for line in items_query:
f_consec_partidas += 1
part_num = line.part_info.part_number if line.part_info else "S/N"
# IV20
lineas.append(f"IV20{f_consec_partidas:03d} {part_num[:25]:<25}C")
self.cuenta_partidas += 1
# IV21
lineas.append(f"IV21")
self.cuenta_partidas += 1
# Data
val_me = float(line.financial.value_usd or 0) if line.financial else 0.0
qty = float(line.quantity.quantity or 0) if line.quantity else 0.0
pb = float(line.quantity.gross_weight or 0) if line.quantity else 0.0
pn = float(line.quantity.net_weight or 0) if line.quantity else 0.0
costo_u = float(line.financial.unit_price_usd or 0) if line.financial else 0.0
um = line.unit_of_measure_info.american_code if line.unit_of_measure_info else "PCS"
pais = (line.customs.origin_country or "MX")[:2] if line.customs else "MX"
hts = (line.customs.american_fraction or "").replace(".", "")[:10] if line.customs else ""
val_int = int(round(val_me * 10000))
qty_int = int(round(qty * 10000))
pb_int = int(round(pb * 10000))
pn_int = int(round(pn * 10000))
costo_int = int(round(costo_u * 100000))
# IV22
lineas.append(
f"IV22{hts:<10} "
f"{val_int:010d}{um[:3]:<3}{qty_int:09d}{pais:<2}0000010000000000100000 "
f"{pb_int:010d}{pn_int:010d}"
)
self.cuenta_partidas += 1
# IV24, IV26 (Zeros)
lineas.append(f"IV24 0000000000 000000000 0000000000000000000000")
lineas.append(f"IV26 0000000000 000000000 0000000000000000000000")
self.cuenta_partidas += 2
# IV27
lineas.append(f"IV27{hts:<10}{costo_int:011d}0000000000000000000000000000000000000")
self.cuenta_partidas += 1
f_val_total += val_me
f_pb += pb
f_pn += pn
# IV90: Footer per Invoice
f_val_int = int(round(f_val_total * 100))
f_pb_int = int(round(f_pb * 10000))
f_pn_int = int(round(f_pn * 10000))
lineas.append(f"IV90{f_consec_partidas:05d}{f_val_int:012d}{f_pb_int:010d}{f_pn_int:010d}")
self.cuenta_partidas += 1
self.valor_total_factura += f_val_total
self.peso_bruto_factura += f_pb
self.peso_neto_factura += f_pn
return lineas, self.errores

View File

@@ -0,0 +1,43 @@
from typing import Dict, Any
from fastapi import APIRouter, Depends, Body
from celery.result import AsyncResult
from core.celery_app import celery_app
from core.security import get_current_user
from .task import generar_transmission_temporal_async
from .schemas import Mainx30GenerationRequest
router = APIRouter()
@router.get("/tasks/{task_id}")
async def get_task_status(
task_id: str,
current_user: Dict[str, Any] = Depends(get_current_user)
):
task_result = AsyncResult(task_id, app=celery_app)
response = {
"task_id": task_id,
"state": task_result.state,
"result": None,
"info": None
}
if task_result.state == 'FAILURE':
response["result"] = str(task_result.result)
elif task_result.state == 'SUCCESS':
response["result"] = task_result.result
elif task_result.state == 'PROCESSING':
# Ensure info is serializable
response["info"] = task_result.info
return response
@router.post("/generate")
async def trigger_generation(
request: Mainx30GenerationRequest,
current_user: Dict[str, Any] = Depends(get_current_user)
):
tenant_id = current_user.get("tenant_id")
# Pass request as dict to Celery task
task = generar_transmission_temporal_async.delay(request.model_dump(), tenant_id)
return {"task_id": task.id, "message": "Generación Temporal iniciada"}

View File

@@ -0,0 +1,77 @@
from typing import List, Optional, Any
from pydantic import BaseModel, Field
class Mainx30GenerationRequest(BaseModel):
"""
Schema for the Mainx30 file generation request
"""
manifiestos: Optional[List[str]] = Field(None, description="Lista de números de manifiesto a procesar")
facturas: Optional[List[str]] = Field(None, description="Lista de números de factura a procesar")
entry_port: Optional[str] = Field(None, description="Puerto de entrada")
exit_port: Optional[str] = Field(None, description="Puerto de salida")
regimen: Optional[str] = Field("Temporal", description="Regimen de importación (Temporal/Definitiva)")
nomenclatura_factura: bool = Field(False, description="Usar nomenclatura basada en factura")
consolidar_rbs: bool = Field(False, description="Consolidar por fracción RB System")
emanifest_fast_blanco: bool = Field(False, description="E-Manifest y FAST en blanco")
no_enviar_emanifest: bool = Field(False, description="No enviar E-Manifest")
consolidar_partidas: bool = Field(False, description="Consolidar partidas (XML OPTIMA Y RBS2)")
main_x40_emanifest: bool = Field(False, description="Main X40 E-Manifest")
main_x30_fedex: bool = Field(False, description="Main X30 (FEDEX)")
iv11: bool = Field(False, description="IV11")
iv42: bool = Field(False, description="IV42")
class ErrorValidacion(BaseModel):
"""
Schema for validation errors during file generation
"""
partida: int
linea: int
descripcion: str
soluciones: str
identificador: str
campos: str = ""
campos2: str = ""
class Mainx30Response(BaseModel):
"""
Schema for the generation response
"""
success: bool
message: str
task_id: Optional[str] = None
archivo_generado: Optional[str] = None
ruta_archivo: Optional[str] = None
content: Optional[str] = None
# Statistics
cuenta_partidas: int = 0
valor_total: float = 0.0
flete_total: float = 0.0
peso_bruto_total: float = 0.0
peso_neto_total: float = 0.0
cuenta_facturas: int = 0
# Validation
errores: List[ErrorValidacion] = []
tiene_inconsistencias: bool = False
class BrokerValidationResult(BaseModel):
es_valido: bool
mensaje_error: Optional[str] = None
broker_cliente: Optional[str] = None
class EmpresaDatos(BaseModel):
broker: str
responsable: str
rfc: str
tiene_linea_express: str
nombre_empresa: str = "AAKRON RULE CORPORATION"
manufacturer_id: str = "I10900"
ftp_key: str = "00SCSI"
main_activity: str = "RAW MATERIAL"
city_state: str = ""
class ConfiguracionSistema(BaseModel):
path_arch_transmision: str
utilizar_nombre_generico_mainx30: bool
utilizar_codigo_broker_cliente: bool

View File

@@ -0,0 +1,175 @@
import os
import tempfile
from datetime import date, datetime
from typing import List, Tuple, Optional, Dict, Any
from pathlib import Path
from sqlalchemy.orm import Session, joinedload
from fastapi import HTTPException
from .schemas import (
Mainx30GenerationRequest, Mainx30Response, ErrorValidacion,
EmpresaDatos, ConfiguracionSistema
)
# --- MODELOS A76 ---
from api.v1.modules.a76.manifests.manifest.models import Manifest
from api.v1.modules.a76.general_catalogs.company.models import Company as GEmpresa
from api.v1.modules.a76.general_catalogs.ports.models import Port
# --- PROCESADORES ---
from .processors import ScaiiProcessor, ScafDefProcessor, ScafTempProcessor
class Mainx30Service:
def __init__(self):
self.errores_validacion: List[ErrorValidacion] = []
self.cuenta_partidas = 0
self.cuenta_facturas = 0
self.valor_total = 0.0
self.flete_total = 0.0
self.peso_bruto_total = 0.0
self.peso_neto_total = 0.0
def generar_mainx30(
self,
db: Session,
request: Mainx30GenerationRequest,
task_instance=None
) -> Mainx30Response:
try:
self._inicializar_variables()
# 1. Obtener Datos de Empresa
datos_empresa = self._obtener_datos_company(db)
emp_dict = datos_empresa.model_dump()
# 2. Obtener Descripciones de Puertos
if request.entry_port:
p_ent = db.query(Port).filter(Port.port_code == request.entry_port).first()
if p_ent: emp_dict['entry_port_desc'] = p_ent.description or p_ent.location_description or ""
if request.exit_port:
p_sal = db.query(Port).filter(Port.port_code == request.exit_port).first()
if p_sal: emp_dict['exit_port_desc'] = p_sal.description or p_sal.location_description or ""
# 3. Fecha de Transmisión (Clarion @D11 = mm/dd/yy, but example uses YYMMDD)
fecha_transmision = datetime.now().strftime("%y%m%d")
# 4. Determinar Procesador
processor = ScafTempProcessor() # Default to Temporal for this endpoint
if request.regimen == "Definitiva":
processor = ScafDefProcessor()
# 5. Procesar Facturas
if not request.facturas:
raise HTTPException(status_code=400, detail="No se proporcionaron facturas para procesar.")
l_facturas, e_facturas = processor.procesar_facturas(db, request.facturas, emp_dict, request)
self.errores_validacion.extend(e_facturas)
# 6. Construir Líneas del Archivo
lineas = []
# Línea A
broker = (datos_empresa.broker or "")[:6]
ftp_key = (datos_empresa.ftp_key or "00SCSI")[:6]
lineas.append(f"A {fecha_transmision}03{broker:<6}{broker:<10}{ftp_key:<6}")
self.cuenta_partidas += 1
# Agregar Líneas de Facturas
lineas.extend(l_facturas)
self.cuenta_partidas += processor.cuenta_partidas
self.cuenta_facturas = processor.cuenta_facturas
self.valor_total = processor.valor_total_factura
self.peso_bruto_total = processor.peso_bruto_factura
self.peso_neto_total = processor.peso_neto_factura
# MF80 (Totales Globales)
val_int = int(round(self.valor_total * 100))
pb_int = int(round(self.peso_bruto_total * 10000))
pn_int = int(round(self.peso_neto_total * 10000))
flete_int = 0
lineas.append(
f"MF80{val_int:012d}"
f"{self.cuenta_facturas:04d}"
f"{pb_int:012d}"
f"{flete_int:08d}"
f"{pn_int:012d}"
)
self.cuenta_partidas += 1
# Línea Z (Total de líneas)
lineas.append(f"Z {self.cuenta_partidas:05d}")
self.cuenta_partidas += 1
# 7. Generar Nombre y Guardar
nombre_archivo = f"TEMPORAL_{request.facturas[0][:15]}.txt"
if len(request.facturas) > 1:
nombre_archivo = f"TEMPORAL_MULTIPLE.txt"
if request.nomenclatura_factura and len(request.facturas) == 1:
nombre_archivo = f"{request.facturas[0][:15]}.txt"
content = '\r\n'.join(lineas)
return Mainx30Response(
success=len(self.errores_validacion) == 0,
message="Archivo generado" if len(self.errores_validacion) == 0 else "Archivo generado con errores de validación",
archivo_generado=nombre_archivo,
ruta_archivo="",
content=content,
errores_validacion=self.errores_validacion,
cuenta_partidas=self.cuenta_partidas,
valor_total=self.valor_total,
peso_bruto_total=self.peso_bruto_total,
peso_neto_total=self.peso_neto_total,
cuenta_facturas=self.cuenta_facturas
)
except HTTPException:
raise
except Exception as e:
import traceback
traceback.print_exc()
raise HTTPException(status_code=500, detail=f"Error generando Mainx30: {str(e)}")
def _inicializar_variables(self):
self.errores_validacion = []
self.cuenta_partidas = 0
self.valor_total = 0.0
self.flete_total = 0.0
self.peso_bruto_total = 0.0
self.peso_neto_total = 0.0
self.cuenta_facturas = 0
def _obtener_datos_company(self, db: Session) -> EmpresaDatos:
empresa = db.query(GEmpresa).options(joinedload(GEmpresa.addresses)).first()
if not empresa:
return EmpresaDatos(broker="", responsable="", rfc="", tiene_linea_express="N", nombre_empresa="", manufacturer_id="", ftp_key="", main_activity="", city_state="")
# Get city/state from main address, or first found
city_state = ""
main_addr = next((a for a in (empresa.addresses or []) if a.address_type == 'main'), None)
if not main_addr and empresa.addresses:
main_addr = empresa.addresses[0]
if main_addr:
# Clarion expects 20 chars for city_state: 5 CP + 11 City + 4 State
cp = (main_addr.postal_code or "")[:5]
city = (main_addr.city or "")[:11]
state = (main_addr.state or "")[:4]
city_state = f"{cp:<5}{city:<11}{state:<4}"
return EmpresaDatos(
broker=(empresa.broker_company or "")[:5],
responsable=(empresa.responsible or "")[:30],
rfc=(empresa.rfc or "")[:13],
tiene_linea_express=empresa.has_express_line or "N",
nombre_empresa=(empresa.name or "")[:40],
manufacturer_id=(empresa.manufacturer_id or "")[:10],
ftp_key=(empresa.ftp_key or "")[:10],
main_activity=(empresa.main_activity or "")[:30],
city_state=city_state[:30]
)

View File

@@ -0,0 +1,46 @@
from celery import Task
from core.celery_app import celery_app
from core.celery_app import celery_app
from core.database import get_core_db as get_db
from .service import Mainx30Service
from .schemas import Mainx30GenerationRequest, Mainx30Response
@celery_app.task(name="generar_transmission_temporal_async", bind=True)
def generar_transmission_temporal_async(self, request_data: dict, tenant_id: int):
"""
Generates the transmission .dat file asynchronously using Mainx30Service
"""
try:
# Re-create db session for task
# Using next(get_db()) is a common pattern for obtaining a session in tasks
# but ensure context management
db = next(get_db())
# Deserialize request
request = Mainx30GenerationRequest(**request_data)
service = Mainx30Service()
response = service.generar_mainx30(db, request, task_instance=self)
# Return result as dict for Celery serialization
# Ensure we return valid JSON serializable dict
result = response.model_dump()
# If we returned content directly, encode it if it's bytes (it's str here)
if response.content:
import base64
# Mainx30Service returns content as string with \r\n
encoded_content = base64.b64encode(response.content.encode('utf-8')).decode('utf-8')
# Add to result to match expected format by frontend dialog
result['content'] = encoded_content
result['file_name'] = response.archivo_generado
result['media_type'] = "text/plain"
return result
except Exception as e:
import traceback
traceback.print_exc()
self.update_state(state='FAILURE', meta={'exc_type': type(e).__name__, 'exc_message': str(e)})
# Re-raise to mark task as failed in Celery
raise e

View File

@@ -60,6 +60,7 @@ from .manifests.manifest.routes import router as manifests_router
from .manifests.driver.routes import router as manifest_drivers_router
from .manifests.manifiesto_anexo.routes import router as manifest_anexos_router
from .reports.exportacion.transmission.MAINX30.routes import router as transmission_router
from .reports.importacion.transmission.temporal.MAINX30.routes import router as transmission_temporal_router
@@ -186,6 +187,12 @@ router.include_router(
tags=["a76 / reports"]
)
router.include_router(
transmission_temporal_router,
prefix="/a76/reports/importacion/transmission/temporal",
tags=["a76 / reports"]
)
# Registrar router de bitácora
from .audit_log.router import router as audit_log_router
router.include_router(audit_log_router, prefix="/a76/audit-log", tags=["Audit Log"])

View File

@@ -14,7 +14,8 @@ celery_app = Celery(
"api.v1.modules.a76.reports.importacion.packing_list.task",
"api.v1.modules.a76.reports.exportacion.aviso_consolidado.task",
"api.v1.modules.a76.reports.exportacion.descargo.task",
"api.v1.modules.a76.reports.exportacion.transmission.MAINX30.task"
"api.v1.modules.a76.reports.exportacion.transmission.MAINX30.task",
"api.v1.modules.a76.reports.importacion.transmission.temporal.MAINX30.task"
] # Ruta al módulo donde están las tareas
)

View File

@@ -31,5 +31,35 @@ export const reportsTransmissionApi = {
if (!response.ok) throw new Error('Error al consultar estado de la transmisión');
return await response.json();
},
triggerTemporalGeneration: async (request: any) => {
const endpoint = `${BASE_URL}/v1/a76/reports/importacion/transmission/temporal/generate`;
const token = localStorage.getItem('access_token');
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(request)
});
if (!response.ok) throw new Error('Error al iniciar la generación temporal');
return await response.json();
},
getTemporalTaskStatus: async (taskId: string) => {
const endpoint = `${BASE_URL}/v1/a76/reports/importacion/transmission/temporal/tasks/${taskId}`;
const token = localStorage.getItem('access_token');
const response = await fetch(endpoint, {
method: 'GET',
headers: { 'Authorization': `Bearer ${token}` }
});
if (!response.ok) throw new Error('Error al consultar estado de la transmisión temporal');
return await response.json();
}
};

View File

@@ -0,0 +1,138 @@
<script lang="ts">
import * as Dialog from '$lib/components/ui/dialog';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
import { Search, Loader2 } from 'lucide-svelte';
import { invoicesApi, type Invoice } from '$lib/api/dashboard/a76/invoices';
import { companyStore } from '$lib/stores/company.svelte';
import { toast } from 'svelte-sonner';
let {
open = $bindable(false),
regimen = 'Temporal',
onSelect
}: {
open: boolean;
regimen: string;
onSelect: (invoice: Invoice) => void;
} = $props();
let invoices = $state<Invoice[]>([]);
let loading = $state(false);
let searchTerm = $state('');
async function searchInvoices() {
if (!companyStore.activeCompany) return;
loading = true;
try {
let filters: any = {
operation_type: 'imp',
invoice_number: searchTerm
};
if (regimen === 'Temporal') {
filters.invoice_type = 'TEM';
} else if (regimen === 'Definitiva') {
filters.invoice_type = 'DEF';
}
const res = await invoicesApi.list(companyStore.activeCompany.id, 1, 50, filters);
if (res.data) {
invoices = res.data.items || [];
}
} catch (e) {
console.error('Error searching invoices:', e);
toast.error('Error al buscar facturas');
} finally {
loading = false;
}
}
function handleSelect(invoice: Invoice) {
onSelect(invoice);
open = false;
}
$effect(() => {
if (open) {
searchInvoices();
}
});
</script>
<Dialog.Root bind:open>
<Dialog.Content class="max-w-2xl">
<Dialog.Header>
<Dialog.Title>Seleccionar Factura ({regimen})</Dialog.Title>
<Dialog.Description>
Busca y selecciona una factura del catálogo de importación para el régimen {regimen}.
</Dialog.Description>
</Dialog.Header>
<div class="space-y-4 py-4">
<div class="flex gap-2">
<div class="relative flex-1">
<Search class="absolute top-2.5 left-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Buscar por número..."
class="h-9 pl-8"
bind:value={searchTerm}
onkeydown={(e) => e.key === 'Enter' && searchInvoices()}
/>
</div>
<Button size="sm" onclick={searchInvoices} disabled={loading}>
{#if loading}
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
{/if}
Buscar
</Button>
</div>
<div class="max-h-[400px] overflow-y-auto rounded-md border text-xs">
<table class="w-full">
<thead class="sticky top-0 bg-muted/90 text-left backdrop-blur-sm">
<tr>
<th class="p-3 font-semibold tracking-wider text-muted-foreground uppercase"
>Número de Factura</th
>
<th class="p-3 font-semibold tracking-wider text-muted-foreground uppercase"
>Pedimento</th
>
</tr>
</thead>
<tbody class="divide-y">
{#if invoices.length === 0}
<tr>
<td colspan="2" class="p-12 text-center text-muted-foreground">
{#if loading}
Buscando facturas...
{:else}
No se encontraron resultados
{/if}
</td>
</tr>
{:else}
{#each invoices as invoice}
<tr
class="group cursor-pointer transition-colors hover:bg-muted/50"
onclick={() => handleSelect(invoice)}
>
<td
class="p-3 font-mono font-bold text-primary transition-colors group-hover:text-primary/80"
>
{invoice.invoice_number}
</td>
<td class="max-w-[400px] truncate p-3 text-muted-foreground italic">
{invoice.compliance_mx?.pedimento_r1 ||
invoice.compliance_mx?.pedimento_id ||
'-'}
</td>
</tr>
{/each}
{/if}
</tbody>
</table>
</div>
</div>
</Dialog.Content>
</Dialog.Root>

View File

@@ -7,10 +7,23 @@
import { Label } from '$lib/components/ui/label';
import { Checkbox } from '$lib/components/ui/checkbox';
import { toast } from 'svelte-sonner';
import { Send, FileText, Settings, Database } from 'lucide-svelte';
import { Send, FileText, Settings, Database, Folder, X } from 'lucide-svelte';
import ManifestSelectorModal from './edit/ManifestSelectorModal.svelte';
import InvoiceSelectorModal from './edit/InvoiceSelectorModal.svelte';
import PdfProgressDialog from '$lib/components/dashboard/invoices/pdf-progress-dialog.svelte';
import PortSelectorDialog from '$lib/components/dashboard/export/manifest/modals/port-selector-dialog.svelte';
import { reportsTransmissionApi } from '$lib/api/dashboard/a76/reports/reports-transmission';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from '$lib/components/ui/table';
import { invoicesApi, type Invoice } from '$lib/api/dashboard/a76/invoices';
import { companyStore } from '$lib/stores/company.svelte';
import { manifestApi as manifestsApi } from '$lib/api/dashboard/a76/manifests';
let { open = $bindable(false), invoice = null } = $props<{
open: boolean;
@@ -20,6 +33,7 @@
// --- STATE ---
let interfaceType = $state('MAINX30');
let movementType = $state('Exportacion');
let regimen = $state('Temporal');
let activeTab = $state('movimiento');
// Manifests State (12 slots, organized in 4 tabs of 3 inputs)
@@ -31,15 +45,38 @@
let isManifestSelectorOpen = $state(false);
let currentManifestIndex = $state(0);
// Invoice Manual State (12 slots)
let manualInvoices = $state(Array(12).fill(''));
let activeInvoiceTab = $state('tab1');
let isInvoiceSelectorOpen = $state(false);
let currentInvoiceIndex = $state(0);
// Progress Dialog State
let isProgressOpen = $state(false);
let taskId = $state<string | null>(null);
let downloadUrl = $state<string | null>(null);
let fileName = $state<string | null>(null);
let progressTitle = $state('Generando Archivo de Transmisión');
let isTemporalTask = $state(false);
// Checkboxes State
// Ports State
let entryPort = $state('');
let exitPort = $state('');
let openEntryPortDialog = $state(false);
let openExitPortDialog = $state(false);
// Invoices State
let invoices = $state<Invoice[]>([]);
let loading = $state(false);
let items = $state<any[]>([]); // manifest items
let selectedItems = $state<Set<string>>(new Set());
let selectedInvoices = $state<Set<number>>(new Set());
// Assuming selectedItems and requestEmail are defined elsewhere or will be added
// requestEmail removed as placeholder
// Checkboxes State (matching backend schemas)
let checks = $state({
nomenclatura_factura: false,
consolidar_rbs: false,
emanifest_fast_blanco: false,
no_enviar_emanifest: false,
@@ -71,40 +108,131 @@
}
async function handleAction() {
// Only allow Exportacion for MAINX30 for now
if (interfaceType === 'MAINX30' && movementType === 'Importacion') {
toast.error('Facturación de Importación: Esta interfase solo está disponible para movimientos de EXPORTACIÓN.');
return;
}
// Filter out empty manifests
const validManifests = manifests.filter((m) => m && m.trim() !== '');
if (validManifests.length === 0) {
toast.error('Debe seleccionar al menos un manifiesto');
return;
}
const payload = {
manifiestos: validManifests,
...checks
};
try {
const res = await reportsTransmissionApi.triggerGeneration(payload);
if (res.task_id) {
taskId = res.task_id;
isProgressOpen = true;
// Reset previous download state
downloadUrl = null;
fileName = null;
if (movementType === 'Importacion') {
const validInvoices = manualInvoices.filter((i) => i && i.trim() !== '');
if (validInvoices.length === 0) {
toast.error('Debe seleccionar al menos una factura');
return;
}
if (!entryPort || !exitPort) {
toast.error('Debe seleccionar tanto el puerto de entrada como el de salida');
return;
}
const payload: any = {
regimen,
facturas: validInvoices,
entry_port: entryPort,
exit_port: exitPort,
...checks
};
try {
const res = await reportsTransmissionApi.triggerTemporalGeneration(payload);
isTemporalTask = true;
if (res.task_id) {
taskId = res.task_id;
isProgressOpen = true;
downloadUrl = null;
fileName = null;
}
} catch (error) {
console.error('Error triggering transmission file generation:', error);
toast.error('Error al iniciar la generación del archivo');
}
} else {
// Exportacion Logic (Manifests)
const validManifests = manifests.filter((m) => m && m.trim() !== '');
if (validManifests.length === 0) {
toast.error('Debe seleccionar al menos un manifiesto');
return;
}
const payload: any = {
manifiestos: validManifests,
...checks
};
try {
const res = await reportsTransmissionApi.triggerGeneration(payload);
isTemporalTask = false;
if (res.task_id) {
taskId = res.task_id;
isProgressOpen = true;
downloadUrl = null;
fileName = null;
}
} catch (error) {
console.error('Error triggering transmission file generation:', error);
toast.error('Error al iniciar la generación del archivo');
}
} catch (error) {
console.error('Error triggering transmission file generation:', error);
toast.error('Error al iniciar la generación del archivo');
}
}
async function loadManifests() {
if (!companyStore.activeCompany?.id) return;
loading = true;
try {
// If Importacion, load Invoices instead
if (movementType === 'Importacion') {
let filters: any = {
operation_type: 'imp'
};
// Filter by Regimen
if (regimen === 'Temporal') {
filters.invoice_type = 'TEM';
} else if (regimen === 'Definitiva') {
filters.invoice_type = 'DEF';
}
const res = await invoicesApi.list(companyStore.activeCompany.id, 1, 100, filters);
invoices = res?.data?.items || [];
} else {
// Existing Manifest Logic
const res = await manifestsApi.list(companyStore.activeCompany.id, {
page: 1,
page_size: 100,
status: 'open'
});
items = res?.data?.items || [];
}
} catch (error) {
console.error('Error loading data:', error);
toast.error('Error al cargar datos');
} finally {
loading = false;
}
}
$effect(() => {
if (open && companyStore.activeCompany?.id) {
loadManifests();
// Pre-fill first slot if we have a specific invoice and it's empty
if (invoice?.invoice_number && manualInvoices[0] === '') {
manualInvoices[0] = invoice.invoice_number;
}
}
});
// Re-load when movement type changes
// Re-load when movement type or regimen changes
$effect(() => {
if (open && movementType) {
// Trigger re-load when movementType or regimen changes
// We access regimen here so it becomes a dependency
const currentRegimen = regimen;
loadManifests();
// Clear selections
selectedItems.clear();
selectedInvoices.clear();
}
});
function openManifestSelector(index: number) {
currentManifestIndex = index;
isManifestSelectorOpen = true;
@@ -115,8 +243,21 @@
isManifestSelectorOpen = false;
}
function openInvoiceSelector(index: number) {
currentInvoiceIndex = index;
isInvoiceSelectorOpen = true;
}
function handleInvoiceSelect(invoice: any) {
manualInvoices[currentInvoiceIndex] = invoice.invoice_number;
isInvoiceSelectorOpen = false;
}
// This function is expected by PdfProgressDialog to check status
async function checkTaskStatus(id: string) {
if (isTemporalTask) {
return await reportsTransmissionApi.getTemporalTaskStatus(id);
}
return await reportsTransmissionApi.getTaskStatus(id);
}
@@ -158,7 +299,7 @@
</script>
<Dialog.Root bind:open>
<Dialog.Content class="sm:max-w-[900px]">
<Dialog.Content class="sm:max-w-[1200px]">
<Dialog.Header>
<Dialog.Title>Interfase Broker Americano</Dialog.Title>
<Dialog.Description>
@@ -166,9 +307,9 @@
</Dialog.Description>
</Dialog.Header>
<div class="space-y-6 py-4">
<div class="space-y-4 py-2">
<!-- TOP HEADER INPUTS -->
<div class="grid grid-cols-1 gap-4 rounded-lg border bg-muted/30 p-4 md:grid-cols-2">
<div class="grid grid-cols-1 gap-4 rounded-lg border p-3 md:grid-cols-2">
<div class="grid gap-2">
<Label>INTERFASE</Label>
<Select.Root type="single" bind:value={interfaceType}>
@@ -191,90 +332,235 @@
</Select.Content>
</Select.Root>
</div>
{#if movementType === 'Importacion'}
<div class="grid gap-2">
<Label>Regimen</Label>
<Select.Root type="single" bind:value={regimen}>
<Select.Trigger>{regimen}</Select.Trigger>
<Select.Content>
<Select.Item value="Temporal" label="Temporal" />
<Select.Item value="Definitiva" label="Definitiva" />
</Select.Content>
</Select.Root>
</div>
{/if}
</div>
<!-- MAIN TABS -->
<Tabs.Root bind:value={activeTab} class="w-full">
<!-- TAB: MOVIMIENTO -->
<Tabs.Content value="movimiento" class="space-y-6 pt-4">
<div class="flex flex-col gap-6 md:flex-row">
<Tabs.Content value="movimiento" class="space-y-4 pt-2">
<div class="flex flex-col gap-4 md:flex-row">
<!-- LEFT: Manifests Table -->
<div class="flex-1 space-y-4">
<div class="flex items-center justify-between">
<Label class="text-base font-medium">MANIFIESTOS (ENTRYS)</Label>
<div class="flex-1 space-y-3">
{#if movementType === 'Exportacion'}
<div class="flex items-center justify-between py-1">
<Label class="text-base font-medium">MANIFIESTOS (ENTRYS)</Label>
</div>
<div class="rounded-md border p-3">
<Tabs.Root bind:value={activeManifestTab}>
<Tabs.List class="mb-2 grid grid-cols-4">
<Tabs.Trigger value="tab1">1 - 3</Tabs.Trigger>
<Tabs.Trigger value="tab2">4 - 6</Tabs.Trigger>
<Tabs.Trigger value="tab3">7 - 9</Tabs.Trigger>
<Tabs.Trigger value="tab4">10 - 12</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="tab1" class="space-y-2">
{#each [0, 1, 2] as i}
<div class="flex items-center gap-2">
<Label class="w-8 text-muted-foreground">{i + 1}.</Label>
<Input
bind:value={manifests[i]}
placeholder={`MANIFIESTO (ENTRY) ${i + 1}`}
readonly
onclick={() => openManifestSelector(i)}
class="cursor-pointer"
/>
</div>
{/each}
</Tabs.Content>
<Tabs.Content value="tab2" class="space-y-2">
{#each [3, 4, 5] as i}
<div class="flex items-center gap-2">
<Label class="w-8 text-muted-foreground">{i + 1}.</Label>
<Input
bind:value={manifests[i]}
placeholder={`MANIFIESTO (ENTRY) ${i + 1}`}
readonly
onclick={() => openManifestSelector(i)}
class="cursor-pointer"
/>
</div>
{/each}
</Tabs.Content>
<Tabs.Content value="tab3" class="space-y-2">
{#each [6, 7, 8] as i}
<div class="flex items-center gap-2">
<Label class="w-8 text-muted-foreground">{i + 1}.</Label>
<Input
bind:value={manifests[i]}
placeholder={`MANIFIESTO (ENTRY) ${i + 1}`}
readonly
onclick={() => openManifestSelector(i)}
class="cursor-pointer"
/>
</div>
{/each}
</Tabs.Content>
<Tabs.Content value="tab4" class="space-y-2">
{#each [9, 10, 11] as i}
<div class="flex items-center gap-2">
<Label class="w-8 text-muted-foreground">{i + 1}.</Label>
<Input
bind:value={manifests[i]}
placeholder={`MANIFIESTO (ENTRY) ${i + 1}`}
readonly
onclick={() => openManifestSelector(i)}
class="cursor-pointer"
/>
</div>
{/each}
</Tabs.Content>
</Tabs.Root>
</div>
{/if}
<div class="rounded-md border p-3">
{#if movementType === 'Importacion'}
<div class="mb-2 text-base font-medium">FACTURAS ({regimen?.toUpperCase()})</div>
<Tabs.Root bind:value={activeInvoiceTab}>
<Tabs.List class="mb-2 grid grid-cols-4">
<Tabs.Trigger value="tab1">1 - 3</Tabs.Trigger>
<Tabs.Trigger value="tab2">4 - 6</Tabs.Trigger>
<Tabs.Trigger value="tab3">7 - 9</Tabs.Trigger>
<Tabs.Trigger value="tab4">10 - 12</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="tab1" class="space-y-2">
{#each [0, 1, 2] as i}
<div class="flex items-center gap-2">
<Label class="w-8 text-muted-foreground">{i + 1}.</Label>
<Input
bind:value={manualInvoices[i]}
placeholder={`FACTURA ${i + 1}`}
readonly
onclick={() => openInvoiceSelector(i)}
class="cursor-pointer"
/>
</div>
{/each}
</Tabs.Content>
<Tabs.Content value="tab2" class="space-y-2">
{#each [3, 4, 5] as i}
<div class="flex items-center gap-2">
<Label class="w-8 text-muted-foreground">{i + 1}.</Label>
<Input
bind:value={manualInvoices[i]}
placeholder={`FACTURA ${i + 1}`}
readonly
onclick={() => openInvoiceSelector(i)}
class="cursor-pointer"
/>
</div>
{/each}
</Tabs.Content>
<Tabs.Content value="tab3" class="space-y-2">
{#each [6, 7, 8] as i}
<div class="flex items-center gap-2">
<Label class="w-8 text-muted-foreground">{i + 1}.</Label>
<Input
bind:value={manualInvoices[i]}
placeholder={`FACTURA ${i + 1}`}
readonly
onclick={() => openInvoiceSelector(i)}
class="cursor-pointer"
/>
</div>
{/each}
</Tabs.Content>
<Tabs.Content value="tab4" class="space-y-2">
{#each [9, 10, 11] as i}
<div class="flex items-center gap-2">
<Label class="w-8 text-muted-foreground">{i + 1}.</Label>
<Input
bind:value={manualInvoices[i]}
placeholder={`FACTURA ${i + 1}`}
readonly
onclick={() => openInvoiceSelector(i)}
class="cursor-pointer"
/>
</div>
{/each}
</Tabs.Content>
</Tabs.Root>
{/if}
</div>
<div class="rounded-md border bg-card p-4">
<Tabs.Root bind:value={activeManifestTab}>
<Tabs.List class="mb-4 grid grid-cols-4">
<Tabs.Trigger value="tab1">1 - 3</Tabs.Trigger>
<Tabs.Trigger value="tab2">4 - 6</Tabs.Trigger>
<Tabs.Trigger value="tab3">7 - 9</Tabs.Trigger>
<Tabs.Trigger value="tab4">10 - 12</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="tab1" class="space-y-3">
{#each [0, 1, 2] as i}
<div class="flex items-center gap-2">
<Label class="w-8 text-muted-foreground">{i + 1}.</Label>
<Input
bind:value={manifests[i]}
placeholder={`MANIFIESTO (ENTRY) ${i + 1}`}
readonly
onclick={() => openManifestSelector(i)}
class="cursor-pointer"
/>
<!-- Ports Selection (Only for Importacion) -->
{#if movementType === 'Importacion'}
<div class="mt-2 grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label>Puerto Entrada</Label>
<div class="relative">
<Input bind:value={entryPort} placeholder="Seleccionar puerto..." readonly />
<div class="absolute top-0 right-0 flex h-full">
{#if entryPort}
<Button
variant="ghost"
size="icon"
onclick={() => (entryPort = '')}
title="Limpiar"
>
<X class="h-4 w-4 text-muted-foreground hover:text-destructive" />
</Button>
{/if}
<Button
variant="ghost"
size="icon"
onclick={() => (openEntryPortDialog = true)}
title="Seleccionar"
>
<Folder class="h-4 w-4" />
</Button>
</div>
{/each}
</Tabs.Content>
<Tabs.Content value="tab2" class="space-y-3">
{#each [3, 4, 5] as i}
<div class="flex items-center gap-2">
<Label class="w-8 text-muted-foreground">{i + 1}.</Label>
<Input
bind:value={manifests[i]}
placeholder={`MANIFIESTO (ENTRY) ${i + 1}`}
readonly
onclick={() => openManifestSelector(i)}
class="cursor-pointer"
/>
</div>
</div>
<div class="space-y-2">
<Label>Puerto Salida</Label>
<div class="relative">
<Input bind:value={exitPort} placeholder="Seleccionar puerto..." readonly />
<div class="absolute top-0 right-0 flex h-full">
{#if exitPort}
<Button
variant="ghost"
size="icon"
onclick={() => (exitPort = '')}
title="Limpiar"
>
<X class="h-4 w-4 text-muted-foreground hover:text-destructive" />
</Button>
{/if}
<Button
variant="ghost"
size="icon"
onclick={() => (openExitPortDialog = true)}
title="Seleccionar"
>
<Folder class="h-4 w-4" />
</Button>
</div>
{/each}
</Tabs.Content>
<Tabs.Content value="tab3" class="space-y-3">
{#each [6, 7, 8] as i}
<div class="flex items-center gap-2">
<Label class="w-8 text-muted-foreground">{i + 1}.</Label>
<Input
bind:value={manifests[i]}
placeholder={`MANIFIESTO (ENTRY) ${i + 1}`}
readonly
onclick={() => openManifestSelector(i)}
class="cursor-pointer"
/>
</div>
{/each}
</Tabs.Content>
<Tabs.Content value="tab4" class="space-y-3">
{#each [9, 10, 11] as i}
<div class="flex items-center gap-2">
<Label class="w-8 text-muted-foreground">{i + 1}.</Label>
<Input
bind:value={manifests[i]}
placeholder={`MANIFIESTO (ENTRY) ${i + 1}`}
readonly
onclick={() => openManifestSelector(i)}
class="cursor-pointer"
/>
</div>
{/each}
</Tabs.Content>
</Tabs.Root>
</div>
</div>
</div>
</div>
{/if}
<div class="flex justify-end">
<Button
size="lg"
@@ -289,9 +575,16 @@
</div>
<!-- BOTTOM: Checklists -->
<div class="border-t pt-4">
<Label class="mb-4 block text-base font-medium">Opciones de Procesamiento</Label>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
<div class="border-t pt-2">
<Label class="mb-2 block text-base font-medium">Opciones de Procesamiento</Label>
<div class="grid grid-cols-1 gap-2 md:grid-cols-2 lg:grid-cols-4">
<div class="flex items-start space-x-2">
<Checkbox id="chk0" bind:checked={checks.nomenclatura_factura} />
<Label for="chk0" class="cursor-pointer pt-0.5 text-sm leading-none font-normal">
Consolidar por Factura (Nomenclatura)
</Label>
</div>
<div class="flex items-start space-x-2">
<Checkbox id="chk1" bind:checked={checks.consolidar_rbs} />
<Label for="chk1" class="cursor-pointer pt-0.5 text-sm leading-none font-normal">
@@ -374,7 +667,7 @@
</Tabs.Content>
<!-- TABS LIST MOVED TO BOTTOM -->
<Tabs.List class="mt-6 grid w-full grid-cols-3">
<Tabs.List class="mt-4 grid w-full grid-cols-3">
<Tabs.Trigger value="movimiento">
<FileText class="mr-2 h-4 w-4" /> Movimiento
</Tabs.Trigger>
@@ -395,14 +688,30 @@
<ManifestSelectorModal bind:open={isManifestSelectorOpen} onSelect={handleManifestSelect} />
<InvoiceSelectorModal
bind:open={isInvoiceSelectorOpen}
{regimen}
onSelect={handleInvoiceSelect}
/>
{#if taskId}
<PdfProgressDialog
bind:open={isProgressOpen}
{taskId}
getStatus={checkTaskStatus}
title={progressTitle}
getStatus={checkTaskStatus}
onComplete={handleDownloadComplete}
onClose={() => (isProgressOpen = false)}
/>
{/if}
<PortSelectorDialog
bind:open={openEntryPortDialog}
onSelect={(item) => (entryPort = item.port_code)}
/>
<PortSelectorDialog
bind:open={openExitPortDialog}
onSelect={(item) => (exitPort = item.port_code)}
/>
</Dialog.Root>

View File

@@ -837,8 +837,8 @@
<Button
variant="outline"
size="sm"
onclick={() => selectedInvoice && (isTransferenciaModalOpen = true)}
disabled={!selectedInvoice}
onclick={() => (isTransferenciaModalOpen = true)}
disabled={!companyStore.activeCompany}
>
<Send class="mr-2 h-4 w-4" />
Transferencia Electrónica
@@ -861,7 +861,7 @@
<!-- ... -->
{#if selectedInvoice && companyStore.activeCompany}
{#if companyStore.activeCompany}
<InvoiceDownloadModal bind:open={isDownloadModalOpen} onConfirm={handleModalConfirm} />
<TransferenciaElectronicaModal bind:open={isTransferenciaModalOpen} invoice={selectedInvoice} />
{/if}