feat: Implement detailed and normal report types for invoice movements, enhancing number formatting and API request handling.
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"}
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}
|
||||
"""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
)
|
||||
Reference in New Issue
Block a user