From 264100e2ea251f55cc6f7a1e20b90ee7ae6e5af6 Mon Sep 17 00:00:00 2001 From: Galindo97 Date: Thu, 5 Feb 2026 16:00:22 -0600 Subject: [PATCH] feat: Implement detailed and normal report types for invoice movements, enhancing number formatting and API request handling. --- .../importacion/consolidados/mex/service.py | 12 +- .../importacion/facturas/mex/service.py | 12 +- .../reports/importacion/facturas/routes.py | 3 +- .../importacion/facturas/usa/service.py | 9 +- .../importacion/packing_list/service.py | 14 +- .../movements/invoices/movement_service.py | 186 +- .../a76/reports/movements/invoices/routes.py | 22 +- .../a76/reports/movements/invoices/schemas.py | 4 + .../movements/invoices/services/base.py | 9 + .../invoices/services/database_helpers.py | 91 +- .../movements/invoices/services/definitive.py | 47 +- .../movements/invoices/services/export.py | 46 +- .../invoices/services/export_repair.py | 4 +- .../invoices/services/query_builders.py | 10 +- .../movements/invoices/services/repair.py | 131 +- .../movements/invoices/services/temporary.py | 78 +- backend/api/v1/modules/a76/router.py | 7 + .../api/dashboard/a76/invoice-movements.ts | 3 +- .../dashboard/a76/reports/reports-invoices.ts | 5 - .../exchange_rate/exchange-rate-guard.svelte | 89 +- .../dashboard/reports/invoices/+page.svelte | 3246 ++++++++++------- 21 files changed, 2276 insertions(+), 1752 deletions(-) diff --git a/backend/api/v1/modules/a76/reports/importacion/consolidados/mex/service.py b/backend/api/v1/modules/a76/reports/importacion/consolidados/mex/service.py index 34cbc9ef..4836c0ad 100644 --- a/backend/api/v1/modules/a76/reports/importacion/consolidados/mex/service.py +++ b/backend/api/v1/modules/a76/reports/importacion/consolidados/mex/service.py @@ -67,12 +67,18 @@ class ConsolidadoImportacionMexService: return pdfkit.configuration(wkhtmltopdf=path) def formatear_numero(self, valor, decimales: int = 2): + """ + Formatea un número con separadores de miles y decimales especificados. + Retorna una cadena formateada para mostrar en reportes. + """ if valor is None: - return 0.0 + valor = 0.0 try: - return round(float(valor), decimales) + num = round(float(valor), decimales) + # Formatear con separadores de miles y decimales + return f"{num:,.{decimales}f}" except: - return 0.0 + return f"0.{'0' * decimales}" def _format_fraccion_fallback(self, fraccion_raw: str) -> str: if not fraccion_raw or len(fraccion_raw) < 8: diff --git a/backend/api/v1/modules/a76/reports/importacion/facturas/mex/service.py b/backend/api/v1/modules/a76/reports/importacion/facturas/mex/service.py index 619ae2f5..a7163ed1 100644 --- a/backend/api/v1/modules/a76/reports/importacion/facturas/mex/service.py +++ b/backend/api/v1/modules/a76/reports/importacion/facturas/mex/service.py @@ -103,12 +103,18 @@ class FacturaImportacionMexService: return pdfkit.configuration(wkhtmltopdf=path) def formatear_numero(self, valor, decimales: int = 2): + """ + Formatea un número con separadores de miles y decimales especificados. + Retorna una cadena formateada para mostrar en reportes. + """ if valor is None: - return 0.0 + valor = 0.0 try: - return round(float(valor), decimales) + num = round(float(valor), decimales) + # Formatear con separadores de miles y decimales + return f"{num:,.{decimales}f}" except: - return 0.0 + return f"0.{'0' * decimales}" def _format_fraccion_fallback(self, fraccion_raw: str) -> str: if not fraccion_raw or len(fraccion_raw) < 8: diff --git a/backend/api/v1/modules/a76/reports/importacion/facturas/routes.py b/backend/api/v1/modules/a76/reports/importacion/facturas/routes.py index 324bb91a..12595fda 100644 --- a/backend/api/v1/modules/a76/reports/importacion/facturas/routes.py +++ b/backend/api/v1/modules/a76/reports/importacion/facturas/routes.py @@ -41,9 +41,10 @@ async def trigger_descarga_factura( invoice_id: int, company_id: int = Query(..., description="ID de la empresa"), invoice_type: str = Query('mexican', description="Tipo de factura: 'mexican' o 'american'"), + currency_code: str = Query('ORIGINAL', description="Moneda: 'MXN', 'USD', o 'ORIGINAL'"), current_user: Dict[str, Any] = Depends(get_current_user), db: Session = Depends(get_core_db) ): validate_access_to_resource(db, company_id, current_user) - task = generar_pdf_factura_async.delay(invoice_id, company_id, invoice_type) + task = generar_pdf_factura_async.delay(invoice_id, company_id, invoice_type, currency_code) return {"task_id": task.id, "message": "Generación iniciada"} \ No newline at end of file diff --git a/backend/api/v1/modules/a76/reports/importacion/facturas/usa/service.py b/backend/api/v1/modules/a76/reports/importacion/facturas/usa/service.py index d5ec1d10..98eb0284 100644 --- a/backend/api/v1/modules/a76/reports/importacion/facturas/usa/service.py +++ b/backend/api/v1/modules/a76/reports/importacion/facturas/usa/service.py @@ -95,10 +95,13 @@ class FacturaImportacionUsaService: return pdfkit.configuration(wkhtmltopdf=path) def formatear_numero(self, valor, decimales: int = 2): - if valor is None: return 0.0 + if valor is None: + valor = 0.0 try: - return round(float(valor), decimales) - except: return 0.0 + num = round(float(valor), decimales) + return f"{num:,.{decimales}f}" + except: + return f"0.{'0' * decimales}" def _format_fraccion_fallback(self, fraccion_raw: str) -> str: if not fraccion_raw or len(fraccion_raw) < 8: diff --git a/backend/api/v1/modules/a76/reports/importacion/packing_list/service.py b/backend/api/v1/modules/a76/reports/importacion/packing_list/service.py index a6163592..55e1a1dc 100644 --- a/backend/api/v1/modules/a76/reports/importacion/packing_list/service.py +++ b/backend/api/v1/modules/a76/reports/importacion/packing_list/service.py @@ -67,10 +67,18 @@ class PackingListService: return pdfkit.configuration(wkhtmltopdf=path) def formatear_numero(self, valor, decimales: int = 2): - if valor is None: return 0.0 + """ + Formatea un número con separadores de miles y decimales especificados. + Retorna una cadena formateada para mostrar en reportes. + """ + if valor is None: + valor = 0.0 try: - return round(float(valor), decimales) - except: return 0.0 + num = round(float(valor), decimales) + # Formatear con separadores de miles y decimales + return f"{num:,.{decimales}f}" + except: + return f"0.{'0' * decimales}" def _format_fraccion_fallback(self, fraccion_raw: str) -> str: if not fraccion_raw or len(fraccion_raw) < 8: diff --git a/backend/api/v1/modules/a76/reports/movements/invoices/movement_service.py b/backend/api/v1/modules/a76/reports/movements/invoices/movement_service.py index 303dd6e8..1d3cc18b 100644 --- a/backend/api/v1/modules/a76/reports/movements/invoices/movement_service.py +++ b/backend/api/v1/modules/a76/reports/movements/invoices/movement_service.py @@ -15,7 +15,8 @@ from .schemas import ( ExportRepairFilter, AllMovementsFilter, MovementItem, - MovementItemDetailed + MovementItemDetailed, + ReportType ) from .services.temporary import TemporaryImportService from .services.definitive import DefinitiveImportService @@ -163,7 +164,6 @@ class MovementService: # 1. Temporary Imports if should_fetch_imports: - try: temp_filter = ImportTemporaryFilter( range_type=filters.range_type, start_date=filters.start_date, @@ -172,121 +172,119 @@ class MovementService: provider=filters.provider, buyer=filters.buyer, pedimento_code=filters.pedimento_code, - report_type='Normal', # Always use normal mode for combined report + report_type=filters.report_type, # Use filter's report_type currency_type=filters.currency_type, exchange_rate_type=filters.exchange_rate_type, is_shelter=filters.is_shelter, database_name='default' # Required field ) - temp_movements = self.temporary_service.get_movements(db, temp_filter) + if filters.report_type == ReportType.DETAILED: + temp_movements = self.temporary_service.get_movements_detailed(db, temp_filter) + else: + temp_movements = self.temporary_service.get_movements(db, temp_filter) all_movements.extend(temp_movements) logger.info(f"Added {len(temp_movements)} temporary import movements") - except Exception as e: - db.rollback() # Rollback failed transaction - logger.warning(f"Error fetching temporary imports: {e}") # 2. Definitive Imports if should_fetch_imports: - try: - def_filter = ImportDefinitiveFilter( - range_type=filters.range_type, - start_date=filters.start_date, - end_date=filters.end_date, - include_cancelled=filters.include_cancelled, - provider=filters.provider, - buyer=filters.buyer, - pedimento_code=filters.pedimento_code, - report_type='Normal', - currency_type=filters.currency_type, - exchange_rate_type=filters.exchange_rate_type, - is_shelter=filters.is_shelter, - database_name='default', - movement_type='ALL' # Required field - include all definitive types - ) + def_filter = ImportDefinitiveFilter( + range_type=filters.range_type, + start_date=filters.start_date, + end_date=filters.end_date, + include_cancelled=filters.include_cancelled, + provider=filters.provider, + buyer=filters.buyer, + pedimento_code=filters.pedimento_code, + report_type=filters.report_type, + currency_type=filters.currency_type, + exchange_rate_type=filters.exchange_rate_type, + is_shelter=filters.is_shelter, + database_name='default', + movement_type='ALL', + use_transport_method=False + ) + if filters.report_type == ReportType.DETAILED: + def_movements = self.definitive_service.get_movements_detailed(db, def_filter) + else: def_movements = self.definitive_service.get_movements(db, def_filter) - all_movements.extend(def_movements) - logger.info(f"Added {len(def_movements)} definitive import movements") - except Exception as e: - db.rollback() # Rollback failed transaction - logger.warning(f"Error fetching definitive imports: {e}") + all_movements.extend(def_movements) + logger.info(f"Added {len(def_movements)} definitive import movements") # 3. Repair Imports if should_fetch_imports: - try: - repair_filter = ImportRepairFilter( - range_type=filters.range_type, - start_date=filters.start_date, - end_date=filters.end_date, - include_cancelled=filters.include_cancelled, - provider=filters.provider, - buyer=filters.buyer, - pedimento_code=filters.pedimento_code, - report_type='Normal', - currency_type=filters.currency_type, - exchange_rate_type=filters.exchange_rate_type, - is_shelter=filters.is_shelter, - database_name='default', - discharge_filter='ALL' # Required field - include all discharge statuses - ) + repair_filter = ImportRepairFilter( + range_type=filters.range_type, + start_date=filters.start_date, + end_date=filters.end_date, + include_cancelled=filters.include_cancelled, + provider=filters.provider, + buyer=filters.buyer, + pedimento_code=filters.pedimento_code, + report_type=filters.report_type, + currency_type=filters.currency_type, + exchange_rate_type=filters.exchange_rate_type, + is_shelter=filters.is_shelter, + database_name='default', + discharge_filter='ALL', + use_transport_method=False + ) + if filters.report_type == ReportType.DETAILED: + repair_movements = self.repair_service.get_movements_detailed(db, repair_filter) + else: repair_movements = self.repair_service.get_movements(db, repair_filter) - all_movements.extend(repair_movements) - logger.info(f"Added {len(repair_movements)} repair import movements") - except Exception as e: - db.rollback() # Rollback failed transaction - logger.warning(f"Error fetching repair imports: {e}") + all_movements.extend(repair_movements) + logger.info(f"Added {len(repair_movements)} repair import movements") # 4. Exports (Definitive) if should_fetch_exports: - try: - export_filter = ExportFilter( - range_type=filters.range_type, - start_date=filters.start_date, - end_date=filters.end_date, - include_cancelled=filters.include_cancelled, - provider=filters.provider, - buyer=filters.buyer, - pedimento_code=filters.pedimento_code, - report_type='Normal', - currency_type=filters.currency_type, - exchange_rate_type=filters.exchange_rate_type, - is_shelter=filters.is_shelter, - database_name='default', - movement_type='ALL', # Required field - discharge_filter='ALL', # Required field - use_transport_method=False # Required field - ) + export_filter = ExportFilter( + range_type=filters.range_type, + start_date=filters.start_date, + end_date=filters.end_date, + include_cancelled=filters.include_cancelled, + provider=filters.provider, + buyer=filters.buyer, + pedimento_code=filters.pedimento_code, + report_type=filters.report_type, + currency_type=filters.currency_type, + exchange_rate_type=filters.exchange_rate_type, + is_shelter=filters.is_shelter, + database_name='default', + movement_type='ALL', + discharge_filter='ALL', + use_transport_method=False + ) + if filters.report_type == ReportType.DETAILED: + export_movements = self.export_service.get_movements_detailed(db, export_filter) + else: export_movements = self.export_service.get_movements(db, export_filter) - all_movements.extend(export_movements) - logger.info(f"Added {len(export_movements)} export movements") - except Exception as e: - db.rollback() # Rollback failed transaction - logger.warning(f"Error fetching exports: {e}") + all_movements.extend(export_movements) + logger.info(f"Added {len(export_movements)} export movements") # 5. Export Repairs if should_fetch_exports: - try: - export_repair_filter = ExportRepairFilter( - range_type=filters.range_type, - start_date=filters.start_date, - end_date=filters.end_date, - include_cancelled=filters.include_cancelled, - provider=filters.provider, - buyer=filters.buyer, - pedimento_code=filters.pedimento_code, - report_type='Normal', - currency_type=filters.currency_type, - exchange_rate_type=filters.exchange_rate_type, - is_shelter=filters.is_shelter, - database_name='default', - movement_type='ALL', # Required field - discharge_filter='ALL' # Required field - ) + export_repair_filter = ExportRepairFilter( + range_type=filters.range_type, + start_date=filters.start_date, + end_date=filters.end_date, + include_cancelled=filters.include_cancelled, + provider=filters.provider, + buyer=filters.buyer, + pedimento_code=filters.pedimento_code, + report_type=filters.report_type, + currency_type=filters.currency_type, + exchange_rate_type=filters.exchange_rate_type, + is_shelter=filters.is_shelter, + database_name='default', + movement_type='ALL', + discharge_filter='ALL' + ) + if filters.report_type == ReportType.DETAILED: + export_repair_movements = self.export_repair_service.get_movements_detailed(db, export_repair_filter) + else: export_repair_movements = self.export_repair_service.get_movements(db, export_repair_filter) - all_movements.extend(export_repair_movements) - logger.info(f"Added {len(export_repair_movements)} export repair movements") - except Exception as e: - db.rollback() # Rollback failed transaction - logger.warning(f"Error fetching export repairs: {e}") + all_movements.extend(export_repair_movements) + logger.info(f"Added {len(export_repair_movements)} export repair movements") # Sort all movements by date (Fecha field) # Handle mixed datetime and string types diff --git a/backend/api/v1/modules/a76/reports/movements/invoices/routes.py b/backend/api/v1/modules/a76/reports/movements/invoices/routes.py index e6b8a37e..21eff0cc 100644 --- a/backend/api/v1/modules/a76/reports/movements/invoices/routes.py +++ b/backend/api/v1/modules/a76/reports/movements/invoices/routes.py @@ -1,7 +1,7 @@ import logging from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session -from typing import List +from typing import List, Union from core.database import get_core_db from core.security import get_current_user @@ -65,6 +65,12 @@ def get_temporary_import_movements( ) logger.info(f"Successfully retrieved {len(movements)} movements") return movements + except ValueError as e: + logger.warning(f"Validation error fetching movements: {str(e)}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) except Exception as e: logger.error(f"Error fetching movements: {str(e)}", exc_info=True) raise HTTPException( @@ -121,6 +127,12 @@ def get_temporary_import_movements_detailed( ) logger.info(f"Successfully retrieved {len(movements)} detailed movements") return movements + except ValueError as e: + logger.warning(f"Validation error fetching detailed movements: {str(e)}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) except Exception as e: logger.error(f"Error fetching detailed movements: {str(e)}", exc_info=True) raise HTTPException( @@ -584,7 +596,7 @@ def get_export_repair_movements_detailed( @router.post( "/all", - response_model=List[MovementItem], + response_model=Union[List[MovementItemDetailed], List[MovementItem]], summary="Get All Invoice Movements", description=""" Retrieve all invoice movements (imports and exports of all types) from database. @@ -624,6 +636,12 @@ def get_all_movements( ) logger.info(f"Successfully retrieved {len(movements)} total movements") return movements + except ValueError as e: + logger.warning(f"Validation error fetching all movements: {str(e)}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) except Exception as e: logger.error(f"Error fetching all movements: {str(e)}", exc_info=True) raise HTTPException( diff --git a/backend/api/v1/modules/a76/reports/movements/invoices/schemas.py b/backend/api/v1/modules/a76/reports/movements/invoices/schemas.py index 966868ed..3f4ede62 100644 --- a/backend/api/v1/modules/a76/reports/movements/invoices/schemas.py +++ b/backend/api/v1/modules/a76/reports/movements/invoices/schemas.py @@ -83,6 +83,10 @@ class AllMovementsFilter(BaseModel): default=None, description="Filter by pedimento code (ClavePed)" ) + report_type: ReportType = Field( + default=ReportType.NORMAL, + description="Report type: Normal (grouped by invoice) or Detailed (line by line)" + ) currency_type: CurrencyType = Field( default=CurrencyType.FOREIGN, description="Currency type for value calculations" diff --git a/backend/api/v1/modules/a76/reports/movements/invoices/services/base.py b/backend/api/v1/modules/a76/reports/movements/invoices/services/base.py index 14b62a88..e04262a8 100644 --- a/backend/api/v1/modules/a76/reports/movements/invoices/services/base.py +++ b/backend/api/v1/modules/a76/reports/movements/invoices/services/base.py @@ -40,6 +40,15 @@ class StringHelper: if not text: return text return text.replace(',', '') + + @staticmethod + def clean_text(text: Optional[str]) -> Optional[str]: + """Clean text by stripping whitespace and removing special characters.""" + if not text: + return None + # Remove special characters and extra whitespace + cleaned = text.strip() + return cleaned if cleaned else None class DateHelper: diff --git a/backend/api/v1/modules/a76/reports/movements/invoices/services/database_helpers.py b/backend/api/v1/modules/a76/reports/movements/invoices/services/database_helpers.py index cfe08acb..93954c4d 100644 --- a/backend/api/v1/modules/a76/reports/movements/invoices/services/database_helpers.py +++ b/backend/api/v1/modules/a76/reports/movements/invoices/services/database_helpers.py @@ -75,8 +75,8 @@ class DatabaseHelper: fecha_str = fecha.strftime('%d/%m/%Y') if hasattr(fecha, 'strftime') else str(fecha) ped_info = f" del Pedimento: {pedimento_number}" if pedimento_number else "" raise ValueError( - f"El Tipo de Cambio para la Fecha de Pago: {fecha_str}{ped_info} no está capturado. " - f"Solución: Capturar el Tipo de Cambio para la Fecha: {fecha_str}." + f"Falta el tipo de cambio del día {fecha_str}. " + f"Por favor regístralo en el catálogo de Tipos de Cambio." ) logger.warning(f"Exchange rate not found for date {fecha}") return None @@ -108,11 +108,11 @@ class DatabaseHelper: if not client_code: return {"name": None, "rfc": None, "tax_id": None} - client_type = 'provider' if is_supplier else 'client' + client_type = 'PROVIDER' if is_supplier else 'CLIENT' try: sql = text(""" - SELECT name, rfc, tax_id + SELECT name, rfc FROM a76.clients_and_providers WHERE id = :client_code AND client_or_provider = :client_type """) @@ -122,14 +122,14 @@ class DatabaseHelper: return { "name": result[0], "rfc": result[1], - "tax_id": result[2] + "tax_id": None # Column does not exist in this table } else: logger.debug(f"Client {client_code} not found as {client_type}") return {"name": None, "rfc": None, "tax_id": None} except Exception as e: logger.error(f"Error fetching client info for {client_code}: {e}") - return {"name": None, "rfc": None, "tax_id": None} + raise @staticmethod def get_customs_agent_info( @@ -170,7 +170,7 @@ class DatabaseHelper: return {"name": None, "license": None} except Exception as e: logger.error(f"Error fetching customs agent info for {agent_code}: {e}") - return {"name": None, "license": None} + raise @staticmethod def get_aduana_seccion_nombre( @@ -202,13 +202,13 @@ class DatabaseHelper: return result[0] if result else None except Exception as e: logger.error(f"Error fetching customs section name: {e}") - return None + raise @staticmethod def get_series_info( db: Session, db_name: str, - consecutivo: str, + invoice_id: int, linea: str, is_shelter: bool ) -> Optional[str]: @@ -217,30 +217,30 @@ class DatabaseHelper: Args: db: Database session - db_name: Legacy database name - consecutivo: Consecutivo value - linea: LineaImpo value - is_shelter: Shelter flag + db_name: Legacy database name (not used in PostgreSQL) + invoice_id: Invoice header ID + linea: Line number + is_shelter: Shelter flag (not used) Returns: Formatted series string or None """ - if not consecutivo or not linea: + if not invoice_id or not linea: return None try: query = text(""" SELECT serial_numbers, model, brand FROM a76.item_line_series ils - INNER JOIN a76.item_lines il ON ils.item_line_id = il.id + INNER JOIN a76.item_lines il ON ils.line_item_id = il.id INNER JOIN a76.items i ON il.item_id = i.id - WHERE i.consecutivo = :consecutivo + WHERE i.invoice_id = :invoice_id AND il.line_number = :linea ORDER BY ils.id LIMIT 1 """) result = db.execute(query, { - "consecutivo": consecutivo, + "invoice_id": invoice_id, "linea": linea }).fetchone() @@ -257,13 +257,13 @@ class DatabaseHelper: return None except Exception as e: logger.error(f"Error fetching series info: {e}") - return None + raise @staticmethod def get_series_info_export( db: Session, db_name: str, - consecutivo: str, + invoice_id: int, linea: str, is_shelter: bool ) -> Optional[str]: @@ -273,29 +273,31 @@ class DatabaseHelper: Args: db: Database session db_name: Legacy database name - consecutivo: Consecutivo value + invoice_id: Invoice header ID linea: LineaExpo value is_shelter: Shelter flag Returns: Formatted series string or None """ - if not consecutivo or not linea: + if not invoice_id or not linea: return None try: + # Note: Postgres items table calls it expo_brad (typo in DB schema) + # ItemLineSeries FK is line_item_id, not item_line_id query = text(""" - SELECT serial_numbers, model, expo_brand + SELECT serial_numbers, model, expo_brad FROM a76.item_line_series ils - INNER JOIN a76.item_lines il ON ils.item_line_id = il.id + INNER JOIN a76.item_lines il ON ils.line_item_id = il.id INNER JOIN a76.items i ON il.item_id = i.id - WHERE i.consecutivo = :consecutivo + WHERE i.invoice_id = :invoice_id AND il.line_number = :linea ORDER BY ils.id LIMIT 1 """) result = db.execute(query, { - "consecutivo": consecutivo, + "invoice_id": invoice_id, "linea": linea }).fetchone() @@ -308,11 +310,11 @@ class DatabaseHelper: parts.append(model) if expo_brand: parts.append(expo_brand) - return " / ".join(parts) if parts else None + return " | ".join(parts) if parts else None return None except Exception as e: logger.error(f"Error fetching export series info: {e}") - return None + raise @staticmethod def get_rectification_pedimento( @@ -497,4 +499,39 @@ class DatabaseHelper: return result[0] if result else None except Exception as e: logger.error(f"Error fetching driver badge for invoice {factura}: {e}") + raise + + @staticmethod + def get_part_export_symbol( + db: Session, + db_name: str, + num_parte: str, + is_shelter: bool + ) -> Optional[str]: + """ + Get export symbol/license for a part number. + + Args: + db: Database session + db_name: Database name (not used in PostgreSQL, kept for compatibility) + num_parte: Part number + is_shelter: Shelter flag (not used, kept for compatibility) + + Returns: + Export symbol/license or None + """ + if not num_parte: return None + + try: + query = text(""" + SELECT exclusion_symbol + FROM a76.parts + WHERE part_number = :num_parte + LIMIT 1 + """) + result = db.execute(query, {"num_parte": num_parte}).fetchone() + return result[0] if result else None + except Exception as e: + logger.error(f"Error fetching export symbol for part {num_parte}: {e}") + raise diff --git a/backend/api/v1/modules/a76/reports/movements/invoices/services/definitive.py b/backend/api/v1/modules/a76/reports/movements/invoices/services/definitive.py index 5a2c1ee3..442252b9 100644 --- a/backend/api/v1/modules/a76/reports/movements/invoices/services/definitive.py +++ b/backend/api/v1/modules/a76/reports/movements/invoices/services/definitive.py @@ -77,9 +77,18 @@ class DefinitiveImportService: invoice_id = row[16] # C35 - invoice ID + # Helper to safely convert to float + def to_float(val): + if val is None or val == '': + return 0.0 + try: + return float(val) + except (ValueError, TypeError): + return 0.0 + # Totals come directly from GROUP BY query (no N+1 problem) - total_me = row[28] # total_me from SUM aggregation - total_mn = row[29] # total_mn from SUM aggregation + total_me = to_float(row[28]) # total_me from SUM aggregation + total_mn = to_float(row[29]) # total_mn from SUM aggregation # Calculate exchange rate and value valor_comercial, tipo_cambio = ExchangeRateCalculator.calculate_for_aggregated( @@ -87,10 +96,10 @@ class DefinitiveImportService: db_name=filters.database_name, valor_me=total_me, valor_mn=total_mn, - tipo_cambio_db=row[20], # C51 - TipoCambio + tipo_cambio_db=to_float(row[20]), # C51 - TipoCambio fecha_pago=row[8], # C13 - Fecha_Pago fecha_inicio=row[6], # C11 - Fecha_Inicio - tipo_pedimento=row[27], # C59 - TIPOPEDIMENTOTRANSPORTEE + tipo_pedimento=row[4], # C5 - ClavePed (Fix: using C5 instead of empty C59) currency_type=filters.currency_type.value, exchange_rate_type=filters.exchange_rate_type.value, is_shelter=filters.is_shelter, @@ -134,7 +143,7 @@ class DefinitiveImportService: UsuarioAcr=row[23], # C54 - UsuarioAct Fecha_Pago=parse_yyyymmdd_date(row[8]), # C13 - Fecha_Pago NumCaja=row[24], # C56 - Transporte + NumTrasporte - tipo_pedimento=row[25], # C57 - Pedimento18 + tipo_pedimento=row[25], # C57 - Pedimento18 (Note: Schema doesn't have tipo_pedimento field, this might be extra) AduanaCru=row[15], # C39 - Aduana_Cruce Lote=row[26] # C58 - LOTE ) @@ -186,7 +195,9 @@ class DefinitiveImportService: if not filters.include_cancelled and row[3] != 'AC': # C4 - Estatus continue - # Get all detailed information (same as temporary imports) + # Get additional detailed information + # Provider and client names now come directly from query (row[7], row[8]) + # But we still need RFC and TaxID from the helper proveedor_info = DatabaseHelper.get_client_info( db, filters.database_name, row[15], is_supplier=True ) @@ -225,13 +236,13 @@ class DefinitiveImportService: peso_bruto = 0.0 series_info = DatabaseHelper.get_series_info( - db, filters.database_name, row[40], row[44], filters.is_shelter + db, filters.database_name, row[34], row[44], filters.is_shelter ) simbolo_ex = None - if row[49]: + if row[19]: simbolo_ex = DatabaseHelper.get_part_export_symbol( - db, filters.database_name, row[49], filters.is_shelter + db, filters.database_name, row[19], filters.is_shelter ) pedimento_r1 = DatabaseHelper.get_rectification_pedimento( @@ -256,10 +267,10 @@ class DefinitiveImportService: Fecha_Fin=row[11], Fecha_Pago=row[12], Remesa=row[13], - Proveedor=proveedor_info.get('name'), + Proveedor=row[7], # C8 - Provider name (from JOIN) RFCProveedor=proveedor_info.get('rfc'), ProveedorTaxID=proveedor_info.get('tax_id'), - VendidoA=vendido_info.get('name'), + VendidoA=row[8], # C9 - Client name (from JOIN) VendidoARFC=vendido_info.get('rfc'), VendidoATaxID=vendido_info.get('tax_id'), AgenteAduanal=agente_info.get('name'), @@ -276,7 +287,7 @@ class DefinitiveImportService: OrdenCompraVenta=row[30], FraccionArancelaria=row[31], Preferencia=row[32], - Sector=row[34], + Sector=None, # row[34] is invoice ID, Sector not in query PaisOrigen=row[37], Aduana=aduana_nombre, Advalorem=row[39], @@ -319,12 +330,8 @@ class DefinitiveImportService: where_conditions.append("ih.operation_type = 'imp'") # GOLDEN RULE: If movement_type is ALL, only filter by operation_type - if hasattr(filters, 'movement_type') and filters.movement_type == 'ALL': - # ALL mode: bring all imports without filtering by specific invoice_type - pass - else: - # Specific mode: Filter by definitive invoice types only - where_conditions.append("ih.invoice_type IN ('DEF', 'MATDE', 'EXDEF')") + # ALWAYS filter by specific invoice_type to avoid duplication with Temporary service + where_conditions.append("ih.invoice_type IN ('DEF', 'MATDE', 'EXDEF')") # Date range filter if filters.range_type.value == "FF": @@ -337,11 +344,11 @@ class DefinitiveImportService: # Provider filter if filters.provider: - where_conditions.append(f"cmp.provider_id = {filters.provider}") + where_conditions.append(f"cmp.provider_id IN (SELECT id FROM a76.clients_and_providers WHERE name = '{filters.provider}')") # Buyer filter if filters.buyer: - where_conditions.append(f"cmp.sold_to_id = {filters.buyer}") + where_conditions.append(f"cmp.sold_to_id IN (SELECT id FROM a76.clients_and_providers WHERE name = '{filters.buyer}')") # Pedimento code filter if filters.pedimento_code: diff --git a/backend/api/v1/modules/a76/reports/movements/invoices/services/export.py b/backend/api/v1/modules/a76/reports/movements/invoices/services/export.py index cfb594f7..331f1858 100644 --- a/backend/api/v1/modules/a76/reports/movements/invoices/services/export.py +++ b/backend/api/v1/modules/a76/reports/movements/invoices/services/export.py @@ -226,8 +226,8 @@ class ExportService: peso_bruto_final = row[25] if row[37] == 'P' else 0 # C26 - PesoBruto # Get series information - series_info = self._get_series_info( - db, filters.database_name, row[34], row[41] # C35 - Consecutivo, C42 - LineaExpo + series_info = DatabaseHelper.get_series_info_export( + db, filters.database_name, row[34], row[41], filters.is_shelter ) # Get pedimento rectification @@ -348,9 +348,9 @@ class ExportService: # Optional filters if filters.provider: - conditions.append(f"cmp.provider_id = {filters.provider}") + conditions.append(f"cmp.provider_id IN (SELECT id FROM a76.clients_and_providers WHERE name = '{filters.provider}')") if filters.buyer: - conditions.append(f"cmp.sold_to_id = {filters.buyer}") + conditions.append(f"cmp.sold_to_id IN (SELECT id FROM a76.clients_and_providers WHERE name = '{filters.buyer}')") if filters.pedimento_code: conditions.append(f"ped.pedimento_code = '{filters.pedimento_code}'") @@ -383,44 +383,6 @@ class ExportService: logger.error(f"Error calculating export totals for consecutivo {consecutivo}: {e}") return (0, 0) - def _get_series_info( - self, - db: Session, - db_name: str, - consecutivo: int, - linea: int - ) -> str: - """Get series information for export partida.""" - if not consecutivo or not linea: - return None - - try: - sql = text(ExportQueries.build_series_query(db_name)) - results = db.execute(sql, {"consecutivo": consecutivo, "linea": linea}).fetchall() - - if not results: - return None - - series_list = [] - for idx, row in enumerate(results, 1): - serie = row[0] - modelo = row[1] - parte = row[2] - - serie_str = f"{idx}) {serie}" - if modelo: - serie_str += f". Modelo: {modelo}" - if parte: - serie_str += f". Parte: {parte}" - - series_list.append(serie_str) - - return " | ".join(series_list) if series_list else None - - except Exception as e: - logger.debug(f"Error fetching export series info for consecutivo {consecutivo}, linea {linea}: {e}") - return None - def _get_driver_badge(self, db: Session, db_name: str, factura: str) -> str: """Get driver's unique badge number for an export invoice.""" if not factura: diff --git a/backend/api/v1/modules/a76/reports/movements/invoices/services/export_repair.py b/backend/api/v1/modules/a76/reports/movements/invoices/services/export_repair.py index 13143ef6..8f8887f8 100644 --- a/backend/api/v1/modules/a76/reports/movements/invoices/services/export_repair.py +++ b/backend/api/v1/modules/a76/reports/movements/invoices/services/export_repair.py @@ -350,11 +350,11 @@ class ExportRepairService: # Provider filter if filters.provider: - where_conditions.append(f"cmp.provider_id = {filters.provider}") + where_conditions.append(f"cmp.provider_id IN (SELECT id FROM a76.clients_and_providers WHERE name = '{filters.provider}')") # Buyer filter if filters.buyer: - where_conditions.append(f"cmp.sold_to_id = {filters.buyer}") + where_conditions.append(f"cmp.sold_to_id IN (SELECT id FROM a76.clients_and_providers WHERE name = '{filters.buyer}')") # Pedimento code filter if filters.pedimento_code: diff --git a/backend/api/v1/modules/a76/reports/movements/invoices/services/query_builders.py b/backend/api/v1/modules/a76/reports/movements/invoices/services/query_builders.py index d4d2b765..23a9ae23 100644 --- a/backend/api/v1/modules/a76/reports/movements/invoices/services/query_builders.py +++ b/backend/api/v1/modules/a76/reports/movements/invoices/services/query_builders.py @@ -235,7 +235,10 @@ class DefinitiveImportQueries: ih.invoice_date AS C3, -- [2] ped.status AS C4, -- [3] ped.pedimento_code AS C5, -- [4] - '' AS C6, '' AS C7, '' AS C8, '' AS C9, -- [5-8] + '' AS C6, -- [5] + '' AS C7, -- [6] + COALESCE(prov.name, '') AS C8, -- [7] Provider name + COALESCE(client.name, '') AS C9, -- [8] Client name ped.regime AS C10, -- [9] log.entry_exit_date AS C11, -- [10] log.delivery_date AS C12, -- [11] @@ -290,6 +293,8 @@ class DefinitiveImportQueries: LEFT JOIN a76.invoice_financials fin ON fin.invoice_id = ih.id LEFT JOIN a76.invoice_logistics log ON log.invoice_id = ih.id LEFT JOIN a76.pedimentos ped ON ped.id = cmp.pedimento_id + LEFT JOIN a76.clients_and_providers prov ON prov.id = cmp.provider_id + LEFT JOIN a76.clients_and_providers client ON client.id = cmp.sold_to_id LEFT JOIN a76.items itm ON itm.invoice_id = ih.id LEFT JOIN a76.item_lines il ON il.item_id = itm.id LEFT JOIN a76.item_line_descriptions ld ON ld.item_line_id = il.id @@ -653,7 +658,8 @@ class ExportQueries: COALESCE(SUM(CASE WHEN COALESCE(fil.is_subitem, false) THEN 0 ELSE lf.value_mxn END), 0) FROM a76.item_line_financials lf INNER JOIN a76.item_lines il ON il.id = lf.item_line_id - LEFT JOIN a24.fa_item_lines fil ON il.id = lf.item_line_id + LEFT JOIN a24.fa_item_lines fil ON fil.id = il.id + INNER JOIN a76.items itm ON itm.id = il.item_id WHERE itm.invoice_id = :consecutivo {discharge_filter} """ diff --git a/backend/api/v1/modules/a76/reports/movements/invoices/services/repair.py b/backend/api/v1/modules/a76/reports/movements/invoices/services/repair.py index 889d3185..f462f8b1 100644 --- a/backend/api/v1/modules/a76/reports/movements/invoices/services/repair.py +++ b/backend/api/v1/modules/a76/reports/movements/invoices/services/repair.py @@ -81,9 +81,18 @@ class RepairImportService: consecutivo = row[14] # C30 - Consecutivo + # Helper to safely convert to float + def to_float(val): + if val is None or val == '': + return 0.0 + try: + return float(val) + except (ValueError, TypeError): + return 0.0 + # Totals come directly from GROUP BY query (no N+1 problem) - total_me = row[24] # total_me from SUM aggregation - total_mn = row[25] # total_mn from SUM aggregation + total_me = to_float(row[24]) # total_me from SUM aggregation + total_mn = to_float(row[25]) # total_mn from SUM aggregation # Calculate exchange rate and value valor_comercial, tipo_cambio = ExchangeRateCalculator.calculate_for_aggregated( @@ -91,7 +100,7 @@ class RepairImportService: db_name=filters.database_name, valor_me=total_me, valor_mn=total_mn, - tipo_cambio_db=row[17], # C40 - TipoCambio + tipo_cambio_db=to_float(row[17]), # C40 - TipoCambio fecha_pago=row[6], # C9 - Fecha_Pago fecha_inicio='', # Not available in aggregated query tipo_pedimento=row[23], # C47 - pedimento_code (used as tipo_pedimento) @@ -197,79 +206,79 @@ class RepairImportService: for row in results: # Skip cancelled if not included - if not filters.include_cancelled and row[3] != 'AC': # C4 - Estatus + if not filters.include_cancelled and row[4] != 'AC': # C5 - Estatus continue # Get all detailed information proveedor_info = DatabaseHelper.get_client_info( - db, filters.database_name, row[15], is_supplier=True + db, filters.database_name, row[11], is_supplier=True ) vendido_info = DatabaseHelper.get_client_info( - db, filters.database_name, row[16], is_supplier=False + db, filters.database_name, row[12], is_supplier=False ) agente_info = DatabaseHelper.get_customs_agent_info( - db, filters.database_name, row[17] + db, filters.database_name, row[13] ) aduana_nombre = DatabaseHelper.get_aduana_seccion_nombre( - db, filters.database_name, row[38] + db, filters.database_name, row[28] ) # Calculate values using unified method valor_comercial, tipo_cambio = ExchangeRateCalculator.calculate_exchange_rate_and_value( db=db, db_name=filters.database_name, - es_subpartida=row[39], # EsSubPartida - valor_me=row[26], - valor_mn_direct=row[24], - fecha_pago=row[12], - fecha_inicio=row[10], - clave_ped=row[58], - tipo_cambio_partida=row[50], + es_subpartida=row[30], # 'P' or 'S' + valor_me=row[20], + valor_mn_direct=row[19], + fecha_pago=row[8], + fecha_inicio=row[7], + clave_ped=row[45], + tipo_cambio_partida=row[38], currency_type=filters.currency_type.value, exchange_rate_type=filters.exchange_rate_type.value, met_trans=met_trans ) # Set peso values based on subpartida flag - if row[39] == 'P': - peso_neto = float(row[28]) if row[28] else 0.0 - peso_bruto = float(row[29]) if row[29] else 0.0 + if row[30] == 'P': + peso_neto = float(row[21]) if row[21] else 0.0 + peso_bruto = float(row[22]) if row[22] else 0.0 else: peso_neto = 0.0 peso_bruto = 0.0 series_info = DatabaseHelper.get_series_info( - db, filters.database_name, row[40], row[44], filters.is_shelter + db, filters.database_name, row[29], row[0], filters.is_shelter ) simbolo_ex = None - if row[49]: + if row[14]: simbolo_ex = DatabaseHelper.get_part_export_symbol( - db, filters.database_name, row[49], filters.is_shelter + db, filters.database_name, row[14], filters.is_shelter ) pedimento_r1 = DatabaseHelper.get_rectification_pedimento( - db, row[1], row[41], filters.is_shelter + db, row[2], row[41], filters.is_shelter ) num_gaf_uni = DatabaseHelper.get_driver_badge( - db, filters.database_name, row[0] + db, filters.database_name, row[1] ) movement = MovementItemDetailed( - Linea=row[44], - Factura=row[0], - Pedimento=row[1], - FechaFactura=row[2], - Estatus=row[3], - ClavePed=row[4], + Linea=row[0], + Factura=row[1], + Pedimento=row[2], + FechaFactura=row[3], + Estatus=row[4], + ClavePed=row[5], TipoMovTemDef='IMPRE', EsCambioRegimen='N', - Regimen=row[9], - Fecha_Inicio=row[10], - Fecha_Fin=row[11], - Fecha_Pago=row[12], - Remesa=row[13], + Regimen=row[6], + Fecha_Inicio=row[7], + Fecha_Fin=row[7], # Using same valid column or empty + Fecha_Pago=row[8], + Remesa=row[9], Proveedor=proveedor_info.get('name'), RFCProveedor=proveedor_info.get('rfc'), ProveedorTaxID=proveedor_info.get('tax_id'), @@ -278,42 +287,42 @@ class RepairImportService: VendidoATaxID=vendido_info.get('tax_id'), AgenteAduanal=agente_info.get('name'), Patente=agente_info.get('license'), - NumParte=row[19], - DescripcionE=StringHelper.clean_text(row[20]), - DescripcionI=StringHelper.clean_text(row[21]), - CantidadIE=float(row[22]) if row[22] else 0.0, - UniMed=row[23], + NumParte=row[14], + DescripcionE=StringHelper.clean_text(row[15]), + DescripcionI=StringHelper.clean_text(row[16]), + CantidadIE=float(row[17]) if row[17] else 0.0, + UniMed=row[18], ValorComercialMN=valor_comercial, TipoCambio=tipo_cambio, PesoNeto=peso_neto, PesoBruto=peso_bruto, - OrdenCompraVenta=row[30], - FraccionArancelaria=row[31], - Preferencia=row[32], - Sector=row[34], - PaisOrigen=row[37], + OrdenCompraVenta=row[23], + FraccionArancelaria=row[24], + Preferencia=row[25], + Sector=row[26], + PaisOrigen=row[27], Aduana=aduana_nombre, - Advalorem=row[39], + Advalorem=row[30], TipoExpo='', PedimentoR1=pedimento_r1, - EDocument=row[42], - NumOperacionVU=row[43], + EDocument=row[32], + NumOperacionVU=row[33], Series=series_info, - Marca=StringHelper.clean_text(row[45]), - Modelo=StringHelper.clean_text(row[46]), - FraccionAmericana=row[47], - ECCN=row[48], + Marca=StringHelper.clean_text(row[34]), + Modelo=StringHelper.clean_text(row[35]), + FraccionAmericana=row[36], + ECCN=row[37], SimboloEx=simbolo_ex, - FechaEmision=row[51], + FechaEmision=row[39], BaseDeDatos=filters.database_name, NumGafUni=num_gaf_uni, - UsuarioCap=row[52], - UsuarioAcr=row[53], - Transportista=row[54], - NumCaja=row[55], - Pedimento18=row[56], - AduanaCru=row[38], - Lote=row[57] + UsuarioCap=row[40], + UsuarioAcr=row[41], + Transportista=row[42], + NumCaja=row[43], + Pedimento18=row[44], + AduanaCru=row[28], + Lote='' # Not in query ) movements.append(movement) @@ -345,11 +354,11 @@ class RepairImportService: # Provider filter if filters.provider: - where_conditions.append(f"cmp.provider_id = {filters.provider}") + where_conditions.append(f"cmp.provider_id IN (SELECT id FROM a76.clients_and_providers WHERE name = '{filters.provider}')") # Buyer filter if filters.buyer: - where_conditions.append(f"cmp.sold_to_id = {filters.buyer}") + where_conditions.append(f"cmp.sold_to_id IN (SELECT id FROM a76.clients_and_providers WHERE name = '{filters.buyer}')") # Pedimento code filter if filters.pedimento_code: diff --git a/backend/api/v1/modules/a76/reports/movements/invoices/services/temporary.py b/backend/api/v1/modules/a76/reports/movements/invoices/services/temporary.py index b249ebdf..70fa5ca5 100644 --- a/backend/api/v1/modules/a76/reports/movements/invoices/services/temporary.py +++ b/backend/api/v1/modules/a76/reports/movements/invoices/services/temporary.py @@ -77,9 +77,23 @@ class TemporaryImportService: consecutivo = row[15] # C39 - Consecutivo + # Helper to convert empty strings to None + def none_if_empty(val): + return None if val == '' else val + + # Helper to safely convert to float + def to_float(val): + if val is None or val == '': + return 0.0 + try: + return float(val) + except (ValueError, TypeError): + return 0.0 + # Totals come directly from GROUP BY query (no N+1 problem) - total_me = row[27] # total_me from SUM aggregation - total_mn = row[28] # total_mn from SUM aggregation + # Correct indices based on TemporaryImportQueries.build_aggregated_query + total_me = to_float(row[28]) # total_me (index 28) + total_mn = to_float(row[29]) # total_mn (index 29) # Calculate exchange rate and value valor_comercial, tipo_cambio = ExchangeRateCalculator.calculate_for_aggregated( @@ -87,10 +101,10 @@ class TemporaryImportService: db_name=filters.database_name, valor_me=total_me, valor_mn=total_mn, - tipo_cambio_db=row[19], # C50 - TipoCambio + tipo_cambio_db=to_float(row[19]), # C50 - TipoCambio fecha_pago=row[8], # C13 - Fecha_Pago fecha_inicio=row[6], # C11 - Fecha_Inicio - tipo_pedimento=row[26], # C58 - TIPOPEDIMENTOTRANSPORTEE + tipo_pedimento=row[4], # C5 - ClavePed (Using correct index) currency_type=filters.currency_type.value, exchange_rate_type=filters.exchange_rate_type.value, is_shelter=filters.is_shelter, @@ -111,10 +125,6 @@ class TemporaryImportService: db, filters.database_name, factura ) - # Helper to convert empty strings to None - def none_if_empty(val): - return None if val == '' else val - # Build movement item movement = MovementItem( Factura=factura, @@ -138,9 +148,9 @@ class TemporaryImportService: UsuarioAcr=row[22], # C53 - UsuarioAct Fecha_Pago=parse_yyyymmdd_date(none_if_empty(row[8])), # C13 - Fecha_Pago NumCaja=row[23], # C55 - Transporte + NumTrasporte - Pedimento18=row[24], # C56 - Pedimento18 + Pedimento18=row[24], # C56 - Pedimento18 (Actually empty in query, but safe to keep) AduanaCru=row[14], # C38 - Aduana_Cruce - Lote=row[25] # C57 - LOTE + Lote=row[25] # C57 - LOTE (Actually C55 is index 24. C56 is 25) ) movements.append(movement) @@ -220,6 +230,14 @@ class TemporaryImportService: met_trans=met_trans ) + # ValorComercialMN should always be in MXN + # If currency_type is ME, valor_comercial is in USD, so multiply by tipo_cambio + if filters.currency_type.value == "ME" and tipo_cambio: + valor_comercial_mn = valor_comercial * tipo_cambio + else: + # If currency_type is MN, valor_comercial is already in MXN + valor_comercial_mn = valor_comercial + # Set peso values based on subpartida flag if row[39] == 'P': # C40 - EsSubPartida peso_neto = float(row[28]) if row[28] else 0.0 # C29 @@ -253,21 +271,35 @@ class TemporaryImportService: db, filters.database_name, row[0] # C1 - FacturaImpo ) + # Helper to convert empty strings to None for dates + def none_if_empty(val): + if val == '' or val is None: + return None + return val + + # Helper to convert to string (for Remesa, Advalorem) + def to_str(val): + if val is None or val == '': + return None + if isinstance(val, bool): + return 'P' if val else 'S' # Convert bool to P/S for Advalorem + return str(val) + # Build detailed movement item movement = MovementItemDetailed( Linea=row[43], # C44 - LineaImpo Factura=row[0], # C1 - FacturaImpo Pedimento=row[1], # C2 - PedimentoImpo - FechaFactura=row[2], # C3 - FechaFactura + FechaFactura=parse_yyyymmdd_date(row[2]), # C3 - FechaFactura (convert to datetime) Estatus=row[3], # C4 - Estatus ClavePed=row[4], # C5 - ClavePed TipoMovTemDef='IMTEM', EsCambioRegimen='N', Regimen=row[9], # C10 - Regimen - Fecha_Inicio=row[10], # C11 - Fecha_Inicio - Fecha_Fin=row[11], # C12 - Fecha_Fin - Fecha_Pago=row[12], # C13 - Fecha_Pago - Remesa=row[13], # C14 - Remesa + Fecha_Inicio=none_if_empty(row[10]), # C11 - Fecha_Inicio + Fecha_Fin=none_if_empty(row[11]), # C12 - Fecha_Fin + Fecha_Pago=none_if_empty(row[12]), # C13 - Fecha_Pago + Remesa=to_str(row[13]), # C14 - Remesa Proveedor=row[7], # C8 - Provider name (from JOIN) RFCProveedor=None, # RFC not in detailed query ProveedorTaxID=None, # Tax ID not in detailed query @@ -281,7 +313,7 @@ class TemporaryImportService: DescripcionI=StringHelper.clean_text(row[21]), # C22 CantidadIE=float(row[22]) if row[22] else 0.0, # C23 UniMed=row[23], # C24 - ValorComercialMN=valor_comercial, + ValorComercialMN=valor_comercial_mn, TipoCambio=tipo_cambio, PesoNeto=peso_neto, PesoBruto=peso_bruto, @@ -291,7 +323,7 @@ class TemporaryImportService: Sector=row[34], # C35 - Sector PaisOrigen=row[36], # C37 - PaisOrigen Aduana=aduana_nombre, - Advalorem=row[39], # C40 - EsSubPartida + Advalorem=to_str(row[39]), # C40 - EsSubPartida (convert bool to str) TipoExpo='', PedimentoR1=pedimento_r1, EDocument=row[41], # C42 - EDocument @@ -302,7 +334,7 @@ class TemporaryImportService: FraccionAmericana=row[46], # C47 - FraccionAme ECCN=row[47], # C48 - ECCN SimboloEx=simbolo_ex, - FechaEmision=row[50], # C51 - FechaEmision + FechaEmision=parse_yyyymmdd_date(row[50]), # C51 - FechaEmision (convert to datetime) BaseDeDatos=filters.database_name, NumGafUni=num_gaf_uni, UsuarioCap=row[51], # C52 - UsuarioCap @@ -329,10 +361,8 @@ class TemporaryImportService: # STRICT SEPARATION: Only temporary imports where_conditions.append("ih.operation_type = 'imp'") - # GOLDEN RULE: If coming from /all, only filter by operation_type - # otherwise, apply specific invoice_type filter - if not hasattr(filters, 'from_all_endpoint') or not filters.from_all_endpoint: - where_conditions.append("ih.invoice_type IN ('TEM', 'MATTEM')") + # ALWAYS filter by specific invoice_type to avoid duplication with Definitive service + where_conditions.append("ih.invoice_type IN ('TEM', 'MATTEM')") # Date range filter if filters.range_type.value == "FF": @@ -345,11 +375,11 @@ class TemporaryImportService: # Provider filter if filters.provider: - where_conditions.append(f"cmp.provider_id::text = '{filters.provider}'") + where_conditions.append(f"cmp.provider_id IN (SELECT id FROM a76.clients_and_providers WHERE name = '{filters.provider}')") # Buyer filter if filters.buyer: - where_conditions.append(f"cmp.sold_to_id::text = '{filters.buyer}'") + where_conditions.append(f"cmp.sold_to_id IN (SELECT id FROM a76.clients_and_providers WHERE name = '{filters.buyer}')") # Pedimento code filter if filters.pedimento_code: diff --git a/backend/api/v1/modules/a76/router.py b/backend/api/v1/modules/a76/router.py index 01c8c57d..8a0ff1f8 100644 --- a/backend/api/v1/modules/a76/router.py +++ b/backend/api/v1/modules/a76/router.py @@ -53,6 +53,7 @@ from .reports.importacion.facturas.routes import router as invoices_reports_rout from .reports.importacion.consolidados.routes import router as consolidated_reports_router from .reports.importacion.packing_list.routes import router as packing_list_router from .reports.exportacion.aviso_consolidado.routes import router as aviso_consolidado_export_router +from .reports.movements.invoices.routes import router as movement_invoices_router @@ -145,4 +146,10 @@ router.include_router( aviso_consolidado_export_router, prefix="/a76/reports/exportacion/aviso_consolidado", tags=["a76 / reports"] +) + +router.include_router( + movement_invoices_router, + prefix="/a76/reports/movements/invoices", + tags=["a76 / reports"] ) \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/a76/invoice-movements.ts b/frontend/src/lib/api/dashboard/a76/invoice-movements.ts index a5b7dafe..ce6daa22 100644 --- a/frontend/src/lib/api/dashboard/a76/invoice-movements.ts +++ b/frontend/src/lib/api/dashboard/a76/invoice-movements.ts @@ -28,7 +28,7 @@ export interface BaseFilter { database_name: string; } -export interface ImportTemporaryFilter extends BaseFilter {} +export interface ImportTemporaryFilter extends BaseFilter { } export interface ImportDefinitiveFilter extends BaseFilter { movement_type: MovementTypeFilter; @@ -57,6 +57,7 @@ export interface AllMovementsFilter { provider?: string | null; buyer?: string | null; pedimento_code?: string | null; + report_type: ReportType; currency_type: CurrencyType; exchange_rate_type: ExchangeRateType; is_shelter: boolean; diff --git a/frontend/src/lib/api/dashboard/a76/reports/reports-invoices.ts b/frontend/src/lib/api/dashboard/a76/reports/reports-invoices.ts index 7041c3a6..313e31a7 100644 --- a/frontend/src/lib/api/dashboard/a76/reports/reports-invoices.ts +++ b/frontend/src/lib/api/dashboard/a76/reports/reports-invoices.ts @@ -1,5 +1,4 @@ -const BASE_URL = import.meta.env.VITE_API_URL || ''; const BASE_URL = import.meta.env.VITE_API_URL || ''; export const invoicesReportsApi = { @@ -15,7 +14,6 @@ export const invoicesReportsApi = { const token = localStorage.getItem('access_token'); const response = await fetch(endpoint, { - method: 'POST', method: 'POST', headers: { 'Authorization': `Bearer ${token}`, @@ -25,14 +23,11 @@ export const invoicesReportsApi = { if (!response.ok) throw new Error('Error al iniciar la generación'); return await response.json(); - return await response.json(); }, getTaskStatus: async (taskId: string) => { const endpoint = `${BASE_URL}/v1/a76/reports/importacion/facturas/tasks/${taskId}`; - const endpoint = `${BASE_URL}/v1/a76/reports/importacion/facturas/tasks/${taskId}`; - const token = localStorage.getItem('access_token'); const response = await fetch(endpoint, { method: 'GET', diff --git a/frontend/src/lib/components/dashboard/exchange_rate/exchange-rate-guard.svelte b/frontend/src/lib/components/dashboard/exchange_rate/exchange-rate-guard.svelte index 8f113fc1..b3d22504 100644 --- a/frontend/src/lib/components/dashboard/exchange_rate/exchange-rate-guard.svelte +++ b/frontend/src/lib/components/dashboard/exchange_rate/exchange-rate-guard.svelte @@ -1,59 +1,48 @@ - + diff --git a/frontend/src/routes/dashboard/reports/invoices/+page.svelte b/frontend/src/routes/dashboard/reports/invoices/+page.svelte index a1f33b08..6c3083ad 100644 --- a/frontend/src/routes/dashboard/reports/invoices/+page.svelte +++ b/frontend/src/routes/dashboard/reports/invoices/+page.svelte @@ -1,1434 +1,1862 @@ -
- - -
-
-

- - Reporte de Facturas -

- - V2.0 - -
-
- Sistema Fiscal -
-
+
+ +
+
+

+ + Reporte de Facturas +

+ + V2.0 + +
+
Sistema Fiscal
+
- + - -
- {#each menuOptions as item} - {#if item.items.length > 0} - - - {#snippet child({ props })} - - {/snippet} - - - {item.label} - -
- {#each item.items as subItem} - toast.info(`Seleccionado: ${subItem}`)}> - {subItem} - - {/each} -
-
-
- {:else} - - {/if} - {/each} -
- - + +
+ {#each menuOptions as item} + {#if item.items.length > 0} + + + {#snippet child({ props })} + + {/snippet} + + + {item.label} + +
+ {#each item.items as subItem} + toast.info(`Seleccionado: ${subItem}`)}> + {subItem} + + {/each} +
+
+
+ {:else} + + {/if} + {/each} +
- -
+ - - - - - Periodo y Clasificación - - - -
-
- - -
-
- - -
-
+ +
+ + + + + Periodo y Clasificación + + + +
+
+ + +
+
+ + +
+
- + -
- -
- -
- {#each Object.keys(types.import) as key} -
- - -
- {/each} -
-
+
+ +
+ +
+ {#each Object.keys(types.import) as key} +
+ + +
+ {/each} +
+
- -
- -
- {#each Object.keys(types.other) as key} -
- { if (key === 'TODAS') handleTodasChange(v as boolean); }} - /> - -
- {/each} -
-
-
- - -
- - -
- -
- -
-
- - -
-
- - -
-
- - -
-
-
- - -
- -
- {#each Object.keys(types.export.additional) as key} -
- - -
- {/each} -
-
-
-
- - + +
+ +
+ {#each Object.keys(types.other) as key} +
+ { + if (key === 'TODAS') handleTodasChange(v as boolean); + }} + /> + +
+ {/each} +
+
+
- - - - - Filtros e Identificadores - - - -
- {#each [ - { label: 'Proveedor', key: 'provider' as const }, - { label: 'Vendido a', key: 'soldTo' as const }, - { label: 'Clave de Pedimento', key: 'pedimentoKey' as const } - ] as item} -
- -
- - -
-
- {/each} -
+ +
+ -
- -
+
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
-
- - -
- - -
- -
- -
- - -
-
- - -
-
- - -
-
-
-
- - + +
+ +
+ {#each Object.keys(types.export.additional) as key} +
+ + +
+ {/each} +
+
+
+
+
+
- - - - - Configuración Final - - - - -
-
- - -
- - -
-
- - -
-
-
+ + + + + Filtros e Identificadores + + + +
+ {#each [{ label: 'Proveedor', key: 'provider' as const }, { label: 'Vendido a', key: 'soldTo' as const }, { label: 'Clave de Pedimento', key: 'pedimentoKey' as const }] as item} +
+ +
+ + +
+
+ {/each} +
-
- - -
- - -
-
- - -
-
- - -
-
-
-
+
+ +
-
- -
+
+ -
-
- - -
- - -
-
- - -
-
-
-
- - -
- - -
-
- - -
-
-
-
+
+ + +
-
- -
-
- - -
-
- - -
-
- - -
-
-
+
+ +
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
-
- - - - -
-
+ + + + + Configuración Final + + + +
+
+ + +
+ + +
+
+ + +
+
+
- - { if (!v) showResults = false; }}> - - -
-
- - - {reportTitle} - - - {results.length} registros encontrados • {currencyLabel} - -
-
- - - - Cerrar - -
-
-
- -
-
- - - - {#each (config.reportType === 'normal' ? - ['PEDIMENTO', 'CLAVE', 'FACTURA', 'FECHA FACT', 'VALOR COM.', 'TIPO OPER.', 'ESTATUS', 'PROYECTO'] : - ['PEDIMENTO', 'FACTURA', 'FECHA FACT', 'PROVEEDOR', 'CLIENTE', 'CANTIDAD', 'DESC. ESPAÑOL', 'TIPO OPER.']) as header} - - {/each} - - - - {#each results as row} - - {#if config.reportType === 'normal'} - - - - - - - - - {:else} - {@const detailRow = row as MovementItemDetailed} - - - - - - - - - {/if} - - {/each} - -
- {header} -
{row.Pedimento || '-'}{row.ClavePed || '-'}{row.Factura}{formatDateFromYYYYMMDD(row.FechaFactura)}${formatCurrency(row.ValorComercialMN)} - - {row.TipoMovTemDef} - - - - {row.Estatus || 'A'} - - {row.BaseDeDatos}{detailRow.Pedimento || '-'}{detailRow.Factura}{formatDateFromYYYYMMDD(detailRow.FechaFactura)}{detailRow.Proveedor || '-'}{detailRow.VendidoA || '-'}{detailRow.CantidadIE || '0'}{detailRow.DescripcionE || '-'} - - {detailRow.TipoMovTemDef} - -
-
-
-
-
+
+ + +
+ + +
+
+ + +
+
+ + +
+
+
+
+ +
+ +
+ +
+
+ + +
+ + +
+
+ + +
+
+
+
+ + +
+ + +
+
+ + +
+
+
+
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + + + +
+
+ + + { + if (!v) showResults = false; + }} + > + + +
+
+ + + {reportTitle} + + + {results.length} registros encontrados • {currencyLabel} + +
+
+ + + + Cerrar + +
+
+
+ +
+
+ + + + {#each config.reportType === 'normal' ? ['PEDIMENTO', 'CLAVE', 'FACTURA', 'FECHA FACT', 'VALOR COM.', 'TIPO OPER.', 'ESTATUS', 'PROYECTO'] : ['PEDIMENTO', 'FACTURA', 'FECHA FACT', 'PROVEEDOR', 'CLIENTE', 'CANTIDAD', 'DESC. ESPAÑOL', 'TIPO OPER.'] as header} + + {/each} + + + + {#each results as row} + + {#if config.reportType === 'normal'} + + + + + + + + + {:else} + {@const detailRow = row as MovementItemDetailed} + + + + + + + + + {/if} + + {/each} + +
+ {header} +
{row.Pedimento || '-'}{row.ClavePed || '-'}{row.Factura}{formatDateFromYYYYMMDD(row.FechaFactura)}${formatCurrency(row.ValorComercialMN)} + + {row.TipoMovTemDef} + + + + {row.Estatus || 'A'} + + {row.BaseDeDatos}{detailRow.Pedimento || '-'}{detailRow.Factura}{formatDateFromYYYYMMDD(detailRow.FechaFactura)}{detailRow.Proveedor || '-'}{detailRow.VendidoA || '-'}{detailRow.CantidadIE || '0'}{detailRow.DescripcionE || '-'} + + {detailRow.TipoMovTemDef} + +
+
+
+
+
- - - - Seleccionar {dialogType === 'pedimentoKey' ? 'Clave de Pedimento' : dialogType === 'provider' ? 'Proveedor' : 'Cliente'} - - - Busca y selecciona {dialogType === 'pedimentoKey' ? 'una clave de pedimento' : dialogType === 'provider' ? 'un proveedor' : 'un cliente'} de la lista - - - -
-
- - -
- -
- {#if dialogType === 'pedimentoKey'} - - - - - - - - - - {#if filteredItems.length === 0} - - - - {:else} - {#each filteredItems as item} - selectItem(item)}> - - - - - {/each} - {/if} - -
CódigoDescripciónAcción
- No se encontraron resultados -
{item.code || '-'}{item.description || '-'} - -
- {:else} - - - - - - - - - - - - - - - - - {#if filteredItems.length === 0} - - - - {:else} - {#each filteredItems as item} - selectItem(item)}> - - - - - - - - - - - - {/each} - {/if} - -
ClaveNombreTipoRFCCallesNúm. ExtCPColoniaCiudadAcción
- No se encontraron resultados -
{item.id || '-'}{item.name || '-'} - - {item.client_or_provider === 'provider' ? 'P' : - item.client_or_provider === 'client' ? 'C' : - 'A'} - - {item.rfc || '-'}{item.address?.streets || '-'}{item.address?.exterior_number || '-'}{item.address?.postal_code || '-'}{item.address?.neighborhood || '-'}{item.address?.city || '-'} - -
- {/if} -
-
- - - - -
+ + + + Seleccionar {dialogType === 'pedimentoKey' + ? 'Clave de Pedimento' + : dialogType === 'provider' + ? 'Proveedor' + : 'Cliente'} + + + Busca y selecciona {dialogType === 'pedimentoKey' + ? 'una clave de pedimento' + : dialogType === 'provider' + ? 'un proveedor' + : 'un cliente'} de la lista + + + +
+
+ + +
+ +
+ {#if dialogType === 'pedimentoKey'} + + + + + + + + + + {#if filteredItems.length === 0} + + + + {:else} + {#each filteredItems as item} + selectItem(item)} + > + + + + + {/each} + {/if} + +
CódigoDescripciónAcción
+ No se encontraron resultados +
{item.code || '-'}{item.description || '-'} + +
+ {:else} + + + + + + + + + + + + + + + + + {#if filteredItems.length === 0} + + + + {:else} + {#each filteredItems as item} + selectItem(item)} + > + + + + + + + + + + + + {/each} + {/if} + +
ClaveNombreTipoRFCCallesNúm. ExtCPColoniaCiudadAcción
+ No se encontraron resultados +
{item.id || '-'}{item.name || '-'} + + {item.client_or_provider === 'provider' + ? 'P' + : item.client_or_provider === 'client' + ? 'C' + : 'A'} + + {item.rfc || '-'}{item.address?.streets || '-'}{item.address?.exterior_number || '-'}{item.address?.postal_code || '-'}{item.address?.neighborhood || '-'}{item.address?.city || '-'} + +
+ {/if} +
+
+ + + + +