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 @@
-