diff --git a/backend/api/v1/modules/a76/reports/importacion/transmission/definitive/MAINX30/processors.py b/backend/api/v1/modules/a76/reports/importacion/transmission/definitive/MAINX30/processors.py index 746bddbf..cc9e328a 100644 --- a/backend/api/v1/modules/a76/reports/importacion/transmission/definitive/MAINX30/processors.py +++ b/backend/api/v1/modules/a76/reports/importacion/transmission/definitive/MAINX30/processors.py @@ -44,13 +44,17 @@ class ScaiiProcessor: # TipoExtNac logic from Clarion: N (National/ManufacturerID) or E (External/TaxID) # We'll use TaxID as default for definitive if not specified - data["manufacturer_id"] = (cliente.external_id or "")[:15] # Fallback for manufacturer id + data["manufacturer_id"] = (cliente.programs.manufacturer_id or "")[:15] if cliente.programs else "" if cliente.programs: data["broker"] = (cliente.programs.broker or "")[:6] if address: - calle_comp = f"{address.streets or ''} {address.exterior_number or ''}".strip() + calle_base = address.streets or "" + num_base = address.exterior_number or "" + # Prevent 'None' string + calle_comp = f"{calle_base} {num_base}".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] @@ -58,8 +62,8 @@ class ScaiiProcessor: 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["ciudad"] = (address.city or "").strip()[:20] + data["estado"] = (address.state or "").strip()[:2].upper() data["tel"] = (address.phone or "")[:15] return data @@ -122,7 +126,7 @@ class ScafDefProcessor(ScaiiProcessor): # MF03 conductor = (factura.logistics.driver_name or "")[:23] if factura.logistics else "" - carrier = (factura.logistics.carrier_id or "00000TRUCK")[:10] + carrier = (factura.logistics.carrier_id or "")[:10] lineas.append( f"MF03{carrier:<10}{conductor:<23}{entry_port_desc:<15}{exit_port_desc:<15}" @@ -131,9 +135,17 @@ class ScafDefProcessor(ScaiiProcessor): # MF20 num_transporte = (factura.logistics.transport_num or "")[:15] if factura.logistics else "" - # CD. JUAREZ CHIH is often hardcoded in these legacy formats + + # Use dynamic city/state from entry_port description if available, otherwise blank + # Clarion format: 5 digit Zip (if relevant) + City State + # We will use the entry_port_desc (e.g. CD. JUAREZ CHIH) and assume a dummy zip '00000' if not parsed + # Or better, just use the entry_port_desc fully aligned + + # Using entry_port_desc directly instead of hardcoded default + cruce_desc = entry_port_desc[:20] if entry_port_desc else " " + lineas.append( - f"MF20{factura.invoice_number[:15]:<15}I{num_transporte:<15}20100CD. JUAREZ CHIH" + f"MF20{factura.invoice_number[:15]:<15}I{num_transporte:<15}00000{cruce_desc:<20}" f"{exit_port[:5]:<5}{entry_port_desc:<15} " ) self.cuenta_partidas += 1 @@ -156,9 +168,13 @@ class ScafDefProcessor(ScaiiProcessor): if s_obj: s_tax = (s_obj.rfc or "")[:12] # IV01 uses 11 spaces then 'C' per Clarion logic + # Adjusted validation for RFCs to avoid crashes or None + s_tax_safe = s_tax if s_tax else " " + c_tax_safe = c_tax if c_tax else " " + lineas.append( f"IV01{factura.invoice_number[:15]:<15}{f_fecha}78{' ':<12}C" - f"{empresa_dict['broker'][:6]:<6}{flete:08d}{s_tax:<13}{c_tax:<13}" + f"{empresa_dict['broker'][:6]:<6}{flete:08d}{s_tax_safe:<13}{c_tax_safe:<13}" ) self.cuenta_partidas += 1 @@ -167,9 +183,11 @@ class ScafDefProcessor(ScaiiProcessor): manufacturer_id = "" vendor_name = "" if factura.compliance_mx and factura.compliance_mx.provider_id: - vendor = db.query(ClientProvider).filter(ClientProvider.id == factura.compliance_mx.provider_id).first() + vendor = db.query(ClientProvider).options( + joinedload(ClientProvider.programs) + ).filter(ClientProvider.id == factura.compliance_mx.provider_id).first() if vendor: - manufacturer_id = (vendor.external_id or "")[:16] + manufacturer_id = (vendor.programs.manufacturer_id or "")[:16] if vendor.programs else "" vendor_name = (vendor.name or "")[:39] lineas.append(f"IV02{manufacturer_id:<16}{vendor_name:<39}") @@ -190,56 +208,75 @@ class ScafDefProcessor(ScaiiProcessor): # Participants 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() + s_cliente = db.query(ClientProvider).options( + joinedload(ClientProvider.address), + joinedload(ClientProvider.programs) + ).filter(ClientProvider.id == factura.compliance_mx.provider_id).first() if s_cliente: s_data = self._obtener_datos_cliente(s_cliente) broker_impo = s_data['broker'] or empresa_dict['broker'] lineas.append(f"IV12S{broker_impo[:6]:<6}{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['estado'][:2]}{s_data['pais'][:2]}{s_data['tel'][:30]:<30}{s_data['tax_id']:<15}00000") + # Ensure spacing aligns with Clarion example (30 spaces + 35 address + space + CP) + lineas.append(f"IV13S{'':<30}{s_data['calle']:<35} {s_data['cp']:<9}") + # IV14: City(20)+State(2)+Country(2)+Phone(30?? No, Clarion example shows Phone then TaxID) + # Clarion example: IV14S... CITY... STMX... TAXID... TEL... + # Re-aligning based on provided example: + # IV14SAKRON NEUS 16-0919851 00000 + # City (20) State (2) Country (2) Space(30) TaxID(15) Tel(5?) + + lineas.append(f"IV14S{s_data['ciudad']:<20}{s_data['estado']:<2}{s_data['pais']:<2}{'':<30}{s_data['tax_id']:<15}{s_data['tel'][:5]:<5}") self.cuenta_partidas += 3 # Consignee (C), Ship To (T), and Importer (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() + c_cliente = db.query(ClientProvider).options( + joinedload(ClientProvider.address), + joinedload(ClientProvider.programs) + ).filter(ClientProvider.id == factura.compliance_mx.sold_to_id).first() if c_cliente: c_data = self._obtener_datos_cliente(c_cliente) broker_impo = c_data['broker'] or empresa_dict['broker'] # Consignee (C) lineas.append(f"IV12C{broker_impo[:6]:<6}{c_data['nombre'][:39]:<39}") - lineas.append(f"IV13C {c_data['calle'][:35]:<35}{c_data['cp']:<9}") - lineas.append(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") + lineas.append(f"IV13C{'':<30}{c_data['calle']:<35} {c_data['cp']:<9}") + lineas.append(f"IV14C{c_data['ciudad']:<20}{c_data['estado']:<2}{c_data['pais']:<2}{'':<30}{c_data['tax_id']:<15}{c_data['tel'][:5]:<5}") self.cuenta_partidas += 3 # Ship To (T) - check if different 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() + t_cl = db.query(ClientProvider).options( + joinedload(ClientProvider.address), + joinedload(ClientProvider.programs) + ).filter(ClientProvider.id == t_id).first() if t_cl: t_data = self._obtener_datos_cliente(t_cl) b_t = t_data['broker'] or empresa_dict['broker'] lineas.append(f"IV12T{b_t[:6]:<6}{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") + lineas.append(f"IV13T{'':<30}{t_data['calle']:<35} {t_data['cp']:<9}") + lineas.append(f"IV14T{t_data['ciudad']:<20}{t_data['estado']:<2}{t_data['pais']:<2}{'':<30}{t_data['tax_id']:<15}{t_data['tel'][:5]:<5}") self.cuenta_partidas += 3 # Importer (I) - Usually same as Consignee in Definitive unless ShippedBy is set 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() + i_cl = db.query(ClientProvider).options( + joinedload(ClientProvider.address), + joinedload(ClientProvider.programs) + ).filter(ClientProvider.id == i_id).first() if i_cl: i_data = self._obtener_datos_cliente(i_cl) b_i = i_data['broker'] or empresa_dict['broker'] lineas.append(f"IV12I{b_i[:6]:<6}{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") + lineas.append(f"IV13I{'':<30}{i_data['calle']:<35} {i_data['cp']:<9}") + lineas.append(f"IV14I{i_data['ciudad']:<20}{i_data['estado']:<2}{i_data['pais']:<2}{'':<30}{i_data['tax_id']:<15}{i_data['tel'][:5]:<5}") self.cuenta_partidas += 3 else: # Fallback: repeat C as I if not specified (following Clarion pattern) lineas.append(f"IV12I{broker_impo[:6]:<6}{c_data['nombre'][:39]:<39}") - lineas.append(f"IV13I {c_data['calle'][:35]:<35}{c_data['cp']:<9}") - lineas.append(f"IV14I{c_data['ciudad'][:20]:<20}{c_data['estado'][:2]}{c_data['pais'][:2]}{c_data['tel'][:30]:<30}{c_data['tax_id']:<15}00000") + lineas.append(f"IV13I{'':<30}{c_data['calle']:<35} {c_data['cp']:<9}") + lineas.append(f"IV14I{c_data['ciudad']:<20}{c_data['estado']:<2}{c_data['pais']:<2}{'':<30}{c_data['tax_id']:<15}{c_data['tel'][:5]:<5}") self.cuenta_partidas += 3 # Items IV20-27 diff --git a/backend/api/v1/modules/a76/reports/importacion/transmission/definitive/MAINX30/service.py b/backend/api/v1/modules/a76/reports/importacion/transmission/definitive/MAINX30/service.py index 3f92355e..634058ee 100644 --- a/backend/api/v1/modules/a76/reports/importacion/transmission/definitive/MAINX30/service.py +++ b/backend/api/v1/modules/a76/reports/importacion/transmission/definitive/MAINX30/service.py @@ -56,6 +56,7 @@ class Mainx30DefinitiveService: # 4. Determinar Procesador (Always ScafDefProcessor for this service) processor = ScafDefProcessor() + # Both 'Definitiva' and 'DEFINITIVO SCAF' use the same heavy processor # 5. Procesar Facturas if not request.facturas: diff --git a/backend/api/v1/modules/a76/reports/importacion/transmission/definitive/MAINX30/task.py b/backend/api/v1/modules/a76/reports/importacion/transmission/definitive/MAINX30/task.py index 10c3dd6f..9ec7a08c 100644 --- a/backend/api/v1/modules/a76/reports/importacion/transmission/definitive/MAINX30/task.py +++ b/backend/api/v1/modules/a76/reports/importacion/transmission/definitive/MAINX30/task.py @@ -1,32 +1,36 @@ from core.celery_app import celery_app -from core.database import CoreSessionLocal +from core.database import get_core_db as get_db from .service import Mainx30DefinitiveService from .schemas import Mainx30GenerationRequest -@celery_app.task(bind=True, name="api.v1.modules.a76.reports.importacion.transmission.definitive.MAINX30.task.generar_transmission_definitiva_async") +@celery_app.task(name="generar_transmission_definitiva_async", bind=True) def generar_transmission_definitiva_async(self, request_data: dict, tenant_id: int): """ Celery task to generate Mainx30 file for Definitive Import """ - db = CoreSessionLocal() try: - # Reconstruct request object + # Re-create db session for task + db = next(get_db()) + + # Deserialize request request = Mainx30GenerationRequest(**request_data) service = Mainx30DefinitiveService() - result_obj = service.generar_mainx30(db, request, task_instance=self) + response = service.generar_mainx30(db, request, task_instance=self) + + # Return result as dict for Celery serialization + result = response.model_dump() - result = result_obj.model_dump() - # Add base64 encoding for content to match frontend expectations - if result_obj.content: + if response.content: import base64 - encoded_content = base64.b64encode(result_obj.content.encode('utf-8')).decode('utf-8') + encoded_content = base64.b64encode(response.content.encode('utf-8')).decode('utf-8') result['content'] = encoded_content - result['file_name'] = result_obj.archivo_generado + result['file_name'] = response.archivo_generado result['media_type'] = "text/plain" return result + except Exception as e: import traceback traceback_print = traceback.format_exc() @@ -39,5 +43,3 @@ def generar_transmission_definitiva_async(self, request_data: dict, tenant_id: i } ) raise e - finally: - db.close() diff --git a/backend/api/v1/modules/a76/reports/importacion/transmission/temporal/MAINX30/processors.py b/backend/api/v1/modules/a76/reports/importacion/transmission/temporal/MAINX30/processors.py index 0c9f383a..2a98f289 100644 --- a/backend/api/v1/modules/a76/reports/importacion/transmission/temporal/MAINX30/processors.py +++ b/backend/api/v1/modules/a76/reports/importacion/transmission/temporal/MAINX30/processors.py @@ -128,7 +128,7 @@ class ScafTempProcessor(ScaiiProcessor): # MF01: Header per Invoice in Importacion Temporal mod_trans = factura.logistics.transport_mode if factura.logistics else "30" - + f_fecha = factura.invoice_date.strftime("%y%m%d") if factura.invoice_date else fecha_trans lineas.append( f"MF01{empresa_dict['broker'][:6]:<6}" @@ -136,7 +136,7 @@ class ScafTempProcessor(ScaiiProcessor): f"{entry_port[:5]:<5}" f"{fecha_trans}" f" {mod_trans[:2]:<2}" - f"{fecha_trans}" + f"{f_fecha}" f"{factura.invoice_number[:15]:<15}" ) self.cuenta_partidas += 1 @@ -317,6 +317,8 @@ class ScafTempProcessor(ScaiiProcessor): f_pn += pn # IV90: Footer per Invoice + + # IV90 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)) @@ -327,4 +329,100 @@ class ScafTempProcessor(ScaiiProcessor): self.peso_bruto_factura += f_pb self.peso_neto_factura += f_pn + return lineas, self.errores + +class ScaiiTempProcessor(ScafTempProcessor): + """ + Procesador para Importación Temporal (Versión Ligera/SCAII) + Se salta los registros MF20, MF22, IV10-14 para mayor velocidad y menor detalle. + """ + 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 + 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() + + entry_port = request.entry_port or "" + exit_port = request.exit_port or "" + fecha_trans = datetime.now().strftime("%y%m%d") + + entry_port_desc = empresa_dict.get('entry_port_desc', 'PUERTO ENTRADA')[:15] + exit_port_desc = empresa_dict.get('exit_port_desc', 'PUERTO SALIDA')[:15] + + for factura in facturas: + self.cuenta_facturas += 1 + f_val_total = 0.0 + f_consec_partidas = 0 + + # MF01 + mod_trans = factura.logistics.transport_mode if factura.logistics else "30" + f_fecha = factura.invoice_date.strftime("%y%m%d") if factura.invoice_date else fecha_trans + lineas.append( + f"MF01{empresa_dict['broker'][:6]:<6}{exit_port[:5]:<5}{entry_port[:5]:<5}" + f"{fecha_trans} {mod_trans[:2]:<2}{f_fecha}{factura.invoice_number[:15]:<15}" + ) + self.cuenta_partidas += 1 + + # MF03 + conductor = (factura.logistics.driver_name or "") if factura.logistics else "" + 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 + + # IV01 + f_fecha_iv = 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 + + lineas.append( + f"IV01{factura.invoice_number[:15]:<15}{f_fecha_iv}{entry_port:<5}{' ':<11}C" + f"{empresa_dict['broker'][:6]:<6}{flete:08d}{'':<13}" + ) + self.cuenta_partidas += 1 + + # Partidas IV20, IV21, IV22 + 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)).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 + + # IV22 + qty = float(line.quantity.quantity or 0) if line.quantity else 0.0 + val_me = float(line.financial.value_usd or 0) if line.financial else 0.0 + 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)) + + lineas.append(f"IV22{hts:<10} {val_int:010d}PCS{qty_int:09d}MX0000010000000000100000 00000000000000000000") + self.cuenta_partidas += 1 + f_val_total += val_me + + # IV90 + fv_int = int(round(f_val_total * 100)) + lineas.append(f"IV90{f_consec_partidas:05d}{fv_int:012d}00000000000000000000") + self.cuenta_partidas += 1 + + self.valor_total_factura += f_val_total + return lineas, self.errores \ No newline at end of file diff --git a/backend/api/v1/modules/a76/reports/importacion/transmission/temporal/MAINX30/service.py b/backend/api/v1/modules/a76/reports/importacion/transmission/temporal/MAINX30/service.py index 2bf899fe..78dadbc6 100644 --- a/backend/api/v1/modules/a76/reports/importacion/transmission/temporal/MAINX30/service.py +++ b/backend/api/v1/modules/a76/reports/importacion/transmission/temporal/MAINX30/service.py @@ -18,7 +18,7 @@ from api.v1.modules.a76.general_catalogs.company.models import Company as GEmpre from api.v1.modules.a76.general_catalogs.ports.models import Port # --- PROCESADORES --- -from .processors import ScaiiProcessor, ScafDefProcessor, ScafTempProcessor +from .processors import ScaiiProcessor, ScafDefProcessor, ScafTempProcessor, ScaiiTempProcessor class Mainx30Service: def __init__(self): @@ -56,8 +56,10 @@ class Mainx30Service: fecha_transmision = datetime.now().strftime("%y%m%d") # 4. Determinar Procesador - processor = ScafTempProcessor() # Default to Temporal for this endpoint - if request.regimen == "Definitiva": + processor = ScafTempProcessor() # Now 'Temporal' defaults to Heavy (SCAF) logic per user request + if request.regimen == "TEMPORAL SCAF": + processor = ScaiiTempProcessor() # 'TEMPORAL SCAF' uses light (SCAII) logic + elif request.regimen == "Definitiva" or request.regimen == "DEFINITIVO SCAF": processor = ScafDefProcessor() # 5. Procesar Facturas diff --git a/frontend/src/lib/components/dashboard/invoices/edit/InvoiceSelectorModal.svelte b/frontend/src/lib/components/dashboard/invoices/edit/InvoiceSelectorModal.svelte index 3f44a54b..978f0a31 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/InvoiceSelectorModal.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/InvoiceSelectorModal.svelte @@ -30,9 +30,9 @@ invoice_number: searchTerm }; - if (regimen === 'Temporal') { + if (regimen === 'Temporal' || regimen === 'TEMPORAL SCAF') { filters.invoice_type = 'TEM'; - } else if (regimen === 'Definitiva') { + } else if (regimen === 'Definitiva' || regimen === 'DEFINITIVO SCAF') { filters.invoice_type = 'DEF'; } diff --git a/frontend/src/lib/components/dashboard/invoices/transferencia-electronica-modal.svelte b/frontend/src/lib/components/dashboard/invoices/transferencia-electronica-modal.svelte index dd3c8051..c4e44436 100644 --- a/frontend/src/lib/components/dashboard/invoices/transferencia-electronica-modal.svelte +++ b/frontend/src/lib/components/dashboard/invoices/transferencia-electronica-modal.svelte @@ -127,7 +127,7 @@ try { let res; - if (regimen === 'Definitiva') { + if (regimen === 'Definitiva' || regimen === 'DEFINITIVO SCAF') { res = await reportsTransmissionApi.triggerDefinitiveGeneration(payload); isDefinitiveTask = true; isTemporalTask = false; @@ -189,9 +189,9 @@ }; // Filter by Regimen - if (regimen === 'Temporal') { + if (regimen === 'Temporal' || regimen === 'TEMPORAL SCAF') { filters.invoice_type = 'TEM'; - } else if (regimen === 'Definitiva') { + } else if (regimen === 'Definitiva' || regimen === 'DEFINITIVO SCAF') { filters.invoice_type = 'DEF'; } @@ -355,7 +355,9 @@ {regimen} + +