Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -7,7 +7,7 @@ from typing import List, Optional
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user, validate_access_to_resource
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
|
||||
from .models import ClientOrProviderEnum
|
||||
|
||||
@@ -40,7 +40,10 @@ async def get_clients_and_providers(
|
||||
"""Get clients and providers"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
query = db.query(ClientProvider).filter(
|
||||
query = db.query(ClientProvider).options(
|
||||
joinedload(ClientProvider.address),
|
||||
joinedload(ClientProvider.programs)
|
||||
).filter(
|
||||
ClientProvider.tenant_id == tenant_id,
|
||||
ClientProvider.company_id == company_id,
|
||||
)
|
||||
|
||||
@@ -113,17 +113,17 @@ class InvoiceComplianceMxBase(BaseModel):
|
||||
manifest_number: Optional[str] = Field(
|
||||
None, max_length=15, description="Manifest number"
|
||||
)
|
||||
provider_header: str = Field(None, max_length=20, description="Provider header")
|
||||
provider_id: int = Field(None, description="Provider ID")
|
||||
sold_to_header: str = Field(None, max_length=20, description="Sold to header")
|
||||
sold_to_id: int = Field(None, description="Sold to ID")
|
||||
shipped_to_header: str = Field(None, max_length=20, description="Shipped to header")
|
||||
shipped_to_id: int = Field(None, description="Shipped to ID")
|
||||
shipped_by_header: Optional[int] = Field(
|
||||
provider_header: Optional[str] = Field(None, max_length=20, description="Provider header")
|
||||
provider_id: Optional[int] = Field(None, description="Provider ID")
|
||||
sold_to_header: Optional[str] = Field(None, max_length=20, description="Sold to header")
|
||||
sold_to_id: Optional[int] = Field(None, description="Sold to ID")
|
||||
shipped_to_header: Optional[str] = Field(None, max_length=20, description="Shipped to header")
|
||||
shipped_to_id: Optional[int] = Field(None, description="Shipped to ID")
|
||||
shipped_by_header: Optional[str] = Field(
|
||||
None, max_length=20, description="Shipped by header"
|
||||
)
|
||||
shipped_by_id: Optional[int] = Field(None, description="Shipped by ID")
|
||||
customs_broker_id: int = Field(None, description="Customs broker ID")
|
||||
customs_broker_id: Optional[int] = Field(None, description="Customs broker ID")
|
||||
customs_broker_us_id: Optional[int] = Field(
|
||||
None, description="US customs broker ID"
|
||||
)
|
||||
@@ -141,7 +141,7 @@ class InvoiceComplianceMxBase(BaseModel):
|
||||
)
|
||||
value_method: Optional[str] = Field(None, max_length=2, description="Value method")
|
||||
act_value: Optional[str] = Field(None, max_length=5, description="Act value")
|
||||
is_pedimento_pending: bool = Field(..., description="Is pedimento pending")
|
||||
is_pedimento_pending: Optional[bool] = Field(False, description="Is pedimento pending")
|
||||
is_owner_of_goods: Optional[bool] = Field(False, description="Is owner of goods")
|
||||
generate_balances: Optional[bool] = Field(False, description="Generate balances")
|
||||
was_reviewed_by_company: Optional[bool] = Field(
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
"""
|
||||
Unified service for invoice movement operations.
|
||||
This service delegates to specialized handlers for each import type.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
|
||||
from .schemas import (
|
||||
ImportTemporaryFilter,
|
||||
ImportDefinitiveFilter,
|
||||
ImportRepairFilter,
|
||||
ExportFilter,
|
||||
ExportRepairFilter,
|
||||
AllMovementsFilter,
|
||||
MovementItem,
|
||||
MovementItemDetailed
|
||||
)
|
||||
from .services.temporary import TemporaryImportService
|
||||
from .services.definitive import DefinitiveImportService
|
||||
from .services.repair import RepairImportService
|
||||
from .services.export import ExportService
|
||||
from .services.export_repair import ExportRepairService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MovementService:
|
||||
"""
|
||||
Unified service for handling all types of movements.
|
||||
Delegates to specialized services for each movement type.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.temporary_service = TemporaryImportService()
|
||||
self.definitive_service = DefinitiveImportService()
|
||||
self.repair_service = RepairImportService()
|
||||
self.export_service = ExportService()
|
||||
self.export_repair_service = ExportRepairService()
|
||||
|
||||
# ===== TEMPORARY IMPORTS =====
|
||||
|
||||
def get_temporary_import_movements(
|
||||
self,
|
||||
db: Session,
|
||||
filters: ImportTemporaryFilter
|
||||
) -> List[MovementItem]:
|
||||
"""Get temporary import movements (normal mode - grouped by invoice)."""
|
||||
return self.temporary_service.get_movements(db, filters)
|
||||
|
||||
def get_temporary_import_movements_detailed(
|
||||
self,
|
||||
db: Session,
|
||||
filters: ImportTemporaryFilter
|
||||
) -> List[MovementItemDetailed]:
|
||||
"""Get temporary import movements (detailed mode - line by line)."""
|
||||
return self.temporary_service.get_movements_detailed(db, filters)
|
||||
|
||||
# ===== DEFINITIVE IMPORTS =====
|
||||
|
||||
def get_definitive_import_movements(
|
||||
self,
|
||||
db: Session,
|
||||
filters: ImportDefinitiveFilter
|
||||
) -> List[MovementItem]:
|
||||
"""Get definitive import movements (normal mode - grouped by invoice)."""
|
||||
return self.definitive_service.get_movements(db, filters)
|
||||
|
||||
def get_definitive_import_movements_detailed(
|
||||
self,
|
||||
db: Session,
|
||||
filters: ImportDefinitiveFilter
|
||||
) -> List[MovementItemDetailed]:
|
||||
"""Get definitive import movements (detailed mode - line by line)."""
|
||||
return self.definitive_service.get_movements_detailed(db, filters)
|
||||
|
||||
# ===== REPAIR IMPORTS =====
|
||||
|
||||
def get_repair_import_movements(
|
||||
self,
|
||||
db: Session,
|
||||
filters: ImportRepairFilter
|
||||
) -> List[MovementItem]:
|
||||
"""Get repair import movements (normal mode - grouped by invoice)."""
|
||||
return self.repair_service.get_movements(db, filters)
|
||||
|
||||
def get_repair_import_movements_detailed(
|
||||
self,
|
||||
db: Session,
|
||||
filters: ImportRepairFilter
|
||||
) -> List[MovementItemDetailed]:
|
||||
"""Get repair import movements (detailed mode - line by line)."""
|
||||
return self.repair_service.get_movements_detailed(db, filters)
|
||||
|
||||
# ===== EXPORTS =====
|
||||
|
||||
def get_export_movements(
|
||||
self,
|
||||
db: Session,
|
||||
filters: ExportFilter
|
||||
) -> List[MovementItem]:
|
||||
"""Get export movements (normal mode - grouped by invoice)."""
|
||||
return self.export_service.get_movements(db, filters)
|
||||
|
||||
def get_export_movements_detailed(
|
||||
self,
|
||||
db: Session,
|
||||
filters: ExportFilter
|
||||
) -> List[MovementItemDetailed]:
|
||||
"""Get export movements (detailed mode - line by line)."""
|
||||
return self.export_service.get_movements_detailed(db, filters)
|
||||
|
||||
# ===== EXPORT REPAIRS =====
|
||||
|
||||
def get_export_repair_movements(
|
||||
self,
|
||||
db: Session,
|
||||
filters: ExportRepairFilter
|
||||
) -> List[MovementItem]:
|
||||
"""Get export repair movements (normal mode - grouped by invoice)."""
|
||||
return self.export_repair_service.get_movements(db, filters)
|
||||
|
||||
def get_export_repair_movements_detailed(
|
||||
self,
|
||||
db: Session,
|
||||
filters: ExportRepairFilter
|
||||
) -> List[MovementItemDetailed]:
|
||||
"""Get export repair movements (detailed mode - line by line)."""
|
||||
return self.export_repair_service.get_movements_detailed(db, filters)
|
||||
|
||||
# ===== ALL MOVEMENTS =====
|
||||
|
||||
def get_all_movements(
|
||||
self,
|
||||
db: Session,
|
||||
filters: AllMovementsFilter
|
||||
) -> List[MovementItem]:
|
||||
"""
|
||||
Get all invoice movements (all types combined).
|
||||
|
||||
This combines:
|
||||
- Temporary imports
|
||||
- Definitive imports
|
||||
- Repair imports
|
||||
- All export types
|
||||
- Export repairs
|
||||
|
||||
Returns a unified list sorted by date.
|
||||
"""
|
||||
all_movements = []
|
||||
|
||||
# Convert AllMovementsFilter to individual filter types
|
||||
# We'll use the same filter parameters for all queries
|
||||
|
||||
# Determine which services to call based on operation_type filter
|
||||
should_fetch_imports = filters.operation_type in [None, 'imp']
|
||||
should_fetch_exports = filters.operation_type in [None, 'exp']
|
||||
|
||||
logger.info(f"Operation type filter: {filters.operation_type}")
|
||||
logger.info(f"Should fetch imports: {should_fetch_imports}")
|
||||
logger.info(f"Should fetch exports: {should_fetch_exports}")
|
||||
|
||||
# 1. Temporary Imports
|
||||
if should_fetch_imports:
|
||||
try:
|
||||
temp_filter = ImportTemporaryFilter(
|
||||
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', # Always use normal mode for combined report
|
||||
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)
|
||||
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_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}")
|
||||
|
||||
# 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_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}")
|
||||
|
||||
# 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_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}")
|
||||
|
||||
# 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_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}")
|
||||
|
||||
# Sort all movements by date (Fecha field)
|
||||
# Handle mixed datetime and string types
|
||||
def get_sort_key(movement):
|
||||
fecha = movement.FechaFactura
|
||||
if not fecha:
|
||||
return ""
|
||||
# Convert datetime to string for consistent comparison
|
||||
if hasattr(fecha, 'strftime'):
|
||||
return fecha.strftime('%Y%m%d')
|
||||
return str(fecha)
|
||||
|
||||
all_movements.sort(key=get_sort_key)
|
||||
|
||||
logger.info(f"Total movements combined: {len(all_movements)}")
|
||||
return all_movements
|
||||
|
||||
|
||||
# Singleton instance
|
||||
movement_service = MovementService()
|
||||
632
backend/api/v1/modules/a76/reports/movements/invoices/routes.py
Normal file
632
backend/api/v1/modules/a76/reports/movements/invoices/routes.py
Normal file
@@ -0,0 +1,632 @@
|
||||
import logging
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user
|
||||
from .schemas import (
|
||||
ImportTemporaryFilter,
|
||||
ImportDefinitiveFilter,
|
||||
ImportRepairFilter,
|
||||
ExportFilter,
|
||||
ExportRepairFilter,
|
||||
AllMovementsFilter,
|
||||
MovementItem,
|
||||
MovementItemDetailed
|
||||
)
|
||||
from .movement_service import movement_service
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(
|
||||
tags=["Reports - Movement Invoices"]
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/temporary",
|
||||
response_model=List[MovementItem],
|
||||
summary="Get Temporary Import Movements",
|
||||
description="""
|
||||
Retrieve temporary import movements from legacy database based on filter criteria.
|
||||
This endpoint corresponds to the 'LLENADOTEMPORAL' (Fill Temporary) logic from the legacy system.
|
||||
|
||||
**Note**: Requires connection to legacy SQL Server database.
|
||||
"""
|
||||
)
|
||||
def get_temporary_import_movements(
|
||||
filters: ImportTemporaryFilter,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get temporary import movements based on filters.
|
||||
|
||||
Args:
|
||||
filters: Filter criteria for querying movements
|
||||
db: Database session
|
||||
current_user: Authenticated user information
|
||||
|
||||
Returns:
|
||||
List of movement items matching the criteria
|
||||
|
||||
Raises:
|
||||
HTTPException: If database query fails or user is unauthorized
|
||||
"""
|
||||
try:
|
||||
logger.info(
|
||||
f"User {current_user.get('preferred_username', 'unknown')} "
|
||||
f"requesting temporary import movements"
|
||||
)
|
||||
movements = movement_service.get_temporary_import_movements(
|
||||
db=db,
|
||||
filters=filters
|
||||
)
|
||||
logger.info(f"Successfully retrieved {len(movements)} movements")
|
||||
return movements
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching movements: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error processing import temporary movements: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/temporary-detailed",
|
||||
response_model=List[MovementItemDetailed],
|
||||
summary="Get Detailed Temporary Import Movements",
|
||||
description="""
|
||||
Retrieve detailed temporary import movements (line by line) from legacy database.
|
||||
This endpoint corresponds to the 'LLENADOTEMPORAL - DETALLADO' logic from the legacy system.
|
||||
|
||||
Each line/partida is returned separately with complete information including:
|
||||
- Provider and buyer details (name, RFC, Tax ID)
|
||||
- Customs broker information
|
||||
- Item descriptions and specifications
|
||||
- Series information
|
||||
- All related metadata
|
||||
|
||||
**Note**: Requires connection to legacy SQL Server database.
|
||||
"""
|
||||
)
|
||||
def get_temporary_import_movements_detailed(
|
||||
filters: ImportTemporaryFilter,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get detailed temporary import movements (line by line) based on filters.
|
||||
|
||||
Args:
|
||||
filters: Filter criteria for querying movements
|
||||
db: Database session
|
||||
current_user: Authenticated user information
|
||||
|
||||
Returns:
|
||||
List of detailed movement items matching the criteria
|
||||
|
||||
Raises:
|
||||
HTTPException: If database query fails or user is unauthorized
|
||||
"""
|
||||
try:
|
||||
logger.info(
|
||||
f"User {current_user.get('preferred_username', 'unknown')} "
|
||||
f"requesting DETAILED temporary import movements"
|
||||
)
|
||||
movements = movement_service.get_temporary_import_movements_detailed(
|
||||
db=db,
|
||||
filters=filters
|
||||
)
|
||||
logger.info(f"Successfully retrieved {len(movements)} detailed movements")
|
||||
return movements
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching detailed movements: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error processing detailed import temporary movements: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/definitive",
|
||||
response_model=List[MovementItem],
|
||||
summary="Get Definitive Import Movements",
|
||||
description="""
|
||||
Retrieve definitive import movements from legacy database based on filter criteria.
|
||||
This endpoint corresponds to the 'LLENADODEFINITIVO - NORMAL' logic from the legacy system.
|
||||
|
||||
Definitive imports are aggregated by invoice number and can be filtered by:
|
||||
- Movement type (COMEX or IMPDF based on ProvImpoDefCR field)
|
||||
- Date range (invoice date or payment date)
|
||||
- Provider and buyer
|
||||
- Pedimento code
|
||||
- Status (active or including cancelled)
|
||||
|
||||
**Special Features**:
|
||||
- Supports shelter company logic for exchange rate calculations
|
||||
- MetTrans# = 1 logic for specific pedimento types (1, 4, 98E)
|
||||
- Retrieves driver badge information
|
||||
- Handles rectification pedimento lookups
|
||||
|
||||
**Note**: Requires connection to legacy SQL Server database.
|
||||
"""
|
||||
)
|
||||
def get_definitive_import_movements(
|
||||
filters: ImportDefinitiveFilter,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get definitive import movements based on filters.
|
||||
|
||||
Args:
|
||||
filters: Filter criteria for querying movements
|
||||
db: Database session
|
||||
current_user: Authenticated user information
|
||||
|
||||
Returns:
|
||||
List of movement items matching the criteria
|
||||
|
||||
Raises:
|
||||
HTTPException: If database query fails or user is unauthorized
|
||||
"""
|
||||
try:
|
||||
logger.info(
|
||||
f"User {current_user.get('preferred_username', 'unknown')} "
|
||||
f"requesting definitive import movements"
|
||||
)
|
||||
movements = movement_service.get_definitive_import_movements(
|
||||
db=db,
|
||||
filters=filters
|
||||
)
|
||||
logger.info(f"Successfully retrieved {len(movements)} definitive movements")
|
||||
return movements
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching definitive movements: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error processing definitive import movements: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/definitive-detailed",
|
||||
response_model=List[MovementItemDetailed],
|
||||
summary="Get Detailed Definitive Import Movements",
|
||||
description="""
|
||||
Retrieve detailed definitive import movements (line by line) from legacy database.
|
||||
This endpoint corresponds to the 'LLENADODEFINITIVO - DETALLADO' logic from the legacy system.
|
||||
|
||||
Each line/partida is returned separately with complete information including:
|
||||
- Provider and buyer details (name, RFC, Tax ID)
|
||||
- Customs broker information
|
||||
- Item descriptions and specifications
|
||||
- Series information from QSeriesDef table
|
||||
- All related metadata
|
||||
|
||||
**Special Logic**:
|
||||
- Only Partidas (EsSubPartida = 'P') have values calculated
|
||||
- Subpartidas (EsSubPartida = 'S') return with zero values
|
||||
- Series formatted as: "1) SERIE123. Modelo: MOD1. Parte: PART1 | 2) SERIE456..."
|
||||
- Exchange rate calculation supports shelter and non-shelter logic
|
||||
- MetTrans# = 1 logic for pedimento types 1, 4, 98E
|
||||
|
||||
**Note**: Requires connection to legacy SQL Server database.
|
||||
"""
|
||||
)
|
||||
def get_definitive_import_movements_detailed(
|
||||
filters: ImportDefinitiveFilter,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get detailed definitive import movements (line by line) based on filters.
|
||||
|
||||
Args:
|
||||
filters: Filter criteria for querying movements
|
||||
db: Database session
|
||||
current_user: Authenticated user information
|
||||
|
||||
Returns:
|
||||
List of detailed movement items matching the criteria
|
||||
|
||||
Raises:
|
||||
HTTPException: If database query fails or user is unauthorized
|
||||
"""
|
||||
try:
|
||||
logger.info(
|
||||
f"User {current_user.get('preferred_username', 'unknown')} "
|
||||
f"requesting DETAILED definitive import movements"
|
||||
)
|
||||
movements = movement_service.get_definitive_import_movements_detailed(
|
||||
db=db,
|
||||
filters=filters
|
||||
)
|
||||
logger.info(f"Successfully retrieved {len(movements)} detailed definitive movements")
|
||||
return movements
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching detailed definitive movements: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error processing detailed definitive import movements: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/repair",
|
||||
response_model=List[MovementItem],
|
||||
summary="Get Repair Import Movements",
|
||||
description="""
|
||||
Retrieve repair import movements from legacy database based on filter criteria.
|
||||
This endpoint corresponds to the 'LLENADOIMP_REPARACION - NORMAL' logic from the legacy system.
|
||||
|
||||
Repair imports are aggregated by invoice number and can be filtered by:
|
||||
- Discharge status (SiDes: discharged, NoDes: not discharged, ALL: no filter)
|
||||
- Date range (invoice date or payment date)
|
||||
- Provider and buyer
|
||||
- Pedimento code
|
||||
- Status (active or including cancelled)
|
||||
|
||||
**Special Features**:
|
||||
- Excludes regime changes (EsCambioRegimen <> 'S')
|
||||
- Supports discharge filter (unique to repair imports)
|
||||
- Exchange rate calculation with shelter/non-shelter logic
|
||||
- MetTrans# = 1 logic for specific pedimento types (1, 4, 98E)
|
||||
- Retrieves driver badge information
|
||||
|
||||
**Database Tables**:
|
||||
- QFacImpRep: Repair import invoices
|
||||
- QEqiMaqRep: Repair import items/partidas
|
||||
- QPedimentos: Pedimentos (customs declarations)
|
||||
|
||||
**Note**: Requires connection to legacy SQL Server database.
|
||||
"""
|
||||
)
|
||||
def get_repair_import_movements(
|
||||
filters: ImportRepairFilter,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get repair import movements based on filters.
|
||||
|
||||
Args:
|
||||
filters: Filter criteria for querying movements
|
||||
db: Database session
|
||||
current_user: Authenticated user information
|
||||
|
||||
Returns:
|
||||
List of movement items matching the criteria
|
||||
|
||||
Raises:
|
||||
HTTPException: If database query fails or user is unauthorized
|
||||
"""
|
||||
try:
|
||||
logger.info(
|
||||
f"User {current_user.get('preferred_username', 'unknown')} "
|
||||
f"requesting repair import movements"
|
||||
)
|
||||
movements = movement_service.get_repair_import_movements(
|
||||
db=db,
|
||||
filters=filters
|
||||
)
|
||||
logger.info(f"Successfully retrieved {len(movements)} repair movements")
|
||||
return movements
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching repair movements: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error processing repair import movements: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/repair-detailed",
|
||||
response_model=List[MovementItemDetailed],
|
||||
summary="Get detailed repair import movements",
|
||||
description="""
|
||||
Retrieve detailed repair import movements (IMPRE) with individual partida lines.
|
||||
|
||||
**LLENADOIMP_REPARACION - DETALLADO**
|
||||
|
||||
Returns individual partida (line item) records for repair imports with full detail including:
|
||||
- Complete invoice and customs clearance information
|
||||
- Series, model, and part numbers for each item
|
||||
- Client/supplier and sold-to information with tax IDs
|
||||
- Exchange rate calculations (MN/ME) based on filter options
|
||||
- Customs agent and customs section details
|
||||
- Driver badge unique number
|
||||
- All partida-level fields (part number, descriptions, quantities, weights, etc.)
|
||||
|
||||
**Discharge Filter Options:**
|
||||
- `SiDes`: Only include discharged items (Descarga = 1)
|
||||
- `NoDes`: Only include non-discharged items (Descarga = 0)
|
||||
- `ALL`: Include all items regardless of discharge status
|
||||
|
||||
**Database Tables Used:**
|
||||
- QFacImpRep: Repair import invoices
|
||||
- QPedimentos: Customs declarations
|
||||
- QEqiMaqRep: Repair import partidas (line items)
|
||||
- QSeriesImpoRep: Series information
|
||||
- GClientesPro: Suppliers
|
||||
- GCliVendido: Sold-to clients
|
||||
- GAAduanal: Customs agents
|
||||
- GAduanaSec: Customs sections
|
||||
- GConductor: Drivers (for badge numbers)
|
||||
- GTipoCambio: Exchange rates
|
||||
""",
|
||||
tags=["Import Movements - Repair"]
|
||||
)
|
||||
async def get_import_repair_movements_detailed(
|
||||
filters: ImportRepairFilter,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get detailed repair import movements based on filter criteria.
|
||||
Returns partida-level detail with series information and full client/customs data.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"User {current_user.get('sub')} requesting detailed repair movements")
|
||||
movements = movement_service.get_repair_import_movements_detailed(
|
||||
db=db,
|
||||
filters=filters
|
||||
)
|
||||
logger.info(f"Successfully retrieved {len(movements)} detailed repair partidas")
|
||||
return movements
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching detailed repair movements: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error processing detailed repair import movements: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/export",
|
||||
response_model=List[MovementItem],
|
||||
summary="Get export movements",
|
||||
description="""
|
||||
Retrieve export movements (EXPO DEF) grouped by invoice.
|
||||
|
||||
**LLENADOEXPORTACION - NORMAL**
|
||||
|
||||
Returns aggregated data grouped by invoice number for export movements.
|
||||
|
||||
**Movement Type Options:**
|
||||
- `AFIJO`: Fixed assets
|
||||
- `NODES`: No discharge
|
||||
- `SCRAP`: Scrap materials
|
||||
- `REEXP`: Re-exports
|
||||
- `DONAC`: Donations
|
||||
- `VEMEX`: Sales to Mexico
|
||||
- `ALL`: All movement types
|
||||
|
||||
**Discharge Filter Options:**
|
||||
- `SiDes`: Only discharged items (Descarga = 1)
|
||||
- `NoDes`: Only non-discharged items (Descarga = 0)
|
||||
- `ALL`: All items regardless of discharge status
|
||||
|
||||
**Database Tables Used:**
|
||||
- QFacExp: Export invoices
|
||||
- QEqeMaq: Export partidas (line items)
|
||||
- QPedimentos: Customs declarations
|
||||
- QClaAct: Part classifications
|
||||
- GAAduanal: Customs agents
|
||||
- GAduanaSec: Customs sections
|
||||
- GConductor: Drivers
|
||||
- GTipoCambio: Exchange rates
|
||||
|
||||
Automatically excludes regime changes (EsCambioRegimen = 'N')
|
||||
""",
|
||||
tags=["Export Movements"]
|
||||
)
|
||||
async def get_export_movements(
|
||||
filters: ExportFilter,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get export movements based on filter criteria.
|
||||
Returns aggregated data grouped by invoice.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"User {current_user.get('sub')} requesting export movements")
|
||||
movements = movement_service.get_export_movements(
|
||||
db=db,
|
||||
filters=filters
|
||||
)
|
||||
logger.info(f"Successfully retrieved {len(movements)} export movements")
|
||||
return movements
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching export movements: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error processing export movements: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/export-detailed",
|
||||
response_model=List[MovementItemDetailed],
|
||||
summary="Get detailed export movements",
|
||||
description="""
|
||||
Retrieve detailed export movements with individual partida lines.
|
||||
|
||||
**LLENADOEXPORTACION - DETALLADO**
|
||||
|
||||
Returns individual partida (line item) records for exports with full detail including:
|
||||
- Complete invoice and customs clearance information
|
||||
- Series, model, and part numbers for each item
|
||||
- Client/supplier and buyer information with tax IDs
|
||||
- Exchange rate calculations (MN/ME) based on filter options
|
||||
- Customs agent and customs section details
|
||||
- Driver badge unique number
|
||||
- All partida-level fields
|
||||
|
||||
**Movement Type Options:**
|
||||
- `AFIJO`: Fixed assets
|
||||
- `NODES`: No discharge
|
||||
- `SCRAP`: Scrap materials
|
||||
- `REEXP`: Re-exports
|
||||
- `DONAC`: Donations
|
||||
- `VEMEX`: Sales to Mexico
|
||||
- `ALL`: All movement types
|
||||
|
||||
**Discharge Filter Options:**
|
||||
- `SiDes`: Only discharged items
|
||||
- `NoDes`: Only non-discharged items
|
||||
- `ALL`: All items
|
||||
|
||||
**Database Tables Used:**
|
||||
- QFacExp: Export invoices
|
||||
- QEqeMaq: Export partidas
|
||||
- QSeriesExpo: Serial numbers
|
||||
- QPedimentos: Customs declarations
|
||||
- GClientesPro: Suppliers
|
||||
- GCliVendido: Buyers
|
||||
- GAAduanal: Customs agents
|
||||
- GAduanaSec: Customs sections
|
||||
- GConductor: Drivers
|
||||
- GTipoCambio: Exchange rates
|
||||
""",
|
||||
tags=["Export Movements"]
|
||||
)
|
||||
async def get_export_movements_detailed(
|
||||
filters: ExportFilter,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get detailed export movements based on filter criteria.
|
||||
Returns partida-level detail with series information and full client/customs data.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"User {current_user.get('sub')} requesting detailed export movements")
|
||||
movements = movement_service.get_export_movements_detailed(
|
||||
db=db,
|
||||
filters=filters
|
||||
)
|
||||
logger.info(f"Successfully retrieved {len(movements)} detailed export partidas")
|
||||
return movements
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching detailed export movements: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error processing detailed export movements: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/export-repair", response_model=List[MovementItem])
|
||||
def get_export_repair_movements(
|
||||
filters: ExportRepairFilter,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
**LLENADOEXP_REPARACION - NORMAL**
|
||||
|
||||
Get export repair movements (EXPO REP) based on filter criteria.
|
||||
Groups results by invoice (FacturaExpo).
|
||||
|
||||
Clarion logic:
|
||||
- Query from QFacExpRep, QEqeMaqRep tables
|
||||
- Filters: date range (FF/FP), provider, buyer, pedimento code
|
||||
- Movement types: AFIJO, NODES
|
||||
- Discharge filter: SiDes, NoDes, or ALL
|
||||
- Calculates totals from partidas where EsSubpartida = 'P'
|
||||
- Exchange rate logic based on currency type and Scaii.ini MetTrans
|
||||
- Always filters by EsCambioRegimen = 'N'
|
||||
"""
|
||||
try:
|
||||
logger.info(f"User {current_user.get('sub')} requesting export repair movements")
|
||||
movements = movement_service.get_export_repair_movements(
|
||||
db=db,
|
||||
filters=filters
|
||||
)
|
||||
logger.info(f"Successfully retrieved {len(movements)} export repair invoices")
|
||||
return movements
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching export repair movements: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error processing export repair movements: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post("/export-repair-detailed", response_model=List[MovementItemDetailed])
|
||||
def get_export_repair_movements_detailed(
|
||||
filters: ExportRepairFilter,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get detailed export repair movements based on filter criteria.
|
||||
Returns partida-level detail with series information and full client/customs data.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"User {current_user.get('sub')} requesting detailed export repair movements")
|
||||
movements = movement_service.get_export_repair_movements_detailed(
|
||||
db=db,
|
||||
filters=filters
|
||||
)
|
||||
logger.info(f"Successfully retrieved {len(movements)} detailed export repair partidas")
|
||||
return movements
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching detailed export repair movements: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error processing detailed export repair movements: {str(e)}"
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/all",
|
||||
response_model=List[MovementItem],
|
||||
summary="Get All Invoice Movements",
|
||||
description="""
|
||||
Retrieve all invoice movements (imports and exports of all types) from database.
|
||||
This endpoint combines temporary, definitive, and repair imports with all export types.
|
||||
|
||||
Use this when "TODAS" checkbox is selected to get a comprehensive view of all movements
|
||||
regardless of their specific type.
|
||||
"""
|
||||
)
|
||||
def get_all_movements(
|
||||
filters: AllMovementsFilter,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get all invoice movements (all types combined) based on filters.
|
||||
|
||||
Args:
|
||||
filters: Filter criteria for querying movements
|
||||
db: Database session
|
||||
current_user: Authenticated user information
|
||||
|
||||
Returns:
|
||||
List of all movement items matching the criteria
|
||||
|
||||
Raises:
|
||||
HTTPException: If database query fails or user is unauthorized
|
||||
"""
|
||||
try:
|
||||
logger.info(
|
||||
f"User {current_user.get('preferred_username', 'unknown')} "
|
||||
f"requesting all invoice movements"
|
||||
)
|
||||
movements = movement_service.get_all_movements(
|
||||
db=db,
|
||||
filters=filters
|
||||
)
|
||||
logger.info(f"Successfully retrieved {len(movements)} total movements")
|
||||
return movements
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching all movements: {str(e)}", exc_info=True)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error processing all movements: {str(e)}"
|
||||
)
|
||||
564
backend/api/v1/modules/a76/reports/movements/invoices/schemas.py
Normal file
564
backend/api/v1/modules/a76/reports/movements/invoices/schemas.py
Normal file
@@ -0,0 +1,564 @@
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import datetime, date
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class RangeType(str, Enum):
|
||||
"""Date range type for filtering"""
|
||||
INVOICE_DATE = "FF" # Filter by invoice date
|
||||
PAYMENT_DATE = "FP" # Filter by payment date
|
||||
|
||||
|
||||
class ReportType(str, Enum):
|
||||
"""Report type"""
|
||||
NORMAL = "Normal"
|
||||
DETAILED = "Detallado"
|
||||
|
||||
|
||||
class CurrencyType(str, Enum):
|
||||
"""Currency type for calculations"""
|
||||
FOREIGN = "ME" # Foreign currency (Moneda Extranjera)
|
||||
LOCAL = "MN" # Local currency (Moneda Nacional)
|
||||
|
||||
|
||||
class ExchangeRateType(str, Enum):
|
||||
"""Exchange rate calculation type"""
|
||||
PAYMENT = "FP" # Use payment date
|
||||
INVOICE = "FF" # Use invoice date
|
||||
|
||||
|
||||
class MovementTypeFilter(str, Enum):
|
||||
"""Movement type filter for definitive imports"""
|
||||
COMEX = "COMEX" # ProvImpoDefCR = 'P'
|
||||
IMPDF = "IMPDF" # ProvImpoDefCR != 'P'
|
||||
ALL = "ALL" # No filter
|
||||
|
||||
|
||||
class DischargeFilter(str, Enum):
|
||||
"""Discharge filter for repair imports"""
|
||||
DISCHARGED = "SiDes" # RepPim.Descarga = 1
|
||||
NOT_DISCHARGED = "NoDes" # RepPim.Descarga = 0
|
||||
ALL = "ALL" # No filter
|
||||
|
||||
|
||||
class ExportMovementType(str, Enum):
|
||||
"""Export movement type filter"""
|
||||
AFIJO = "AFIJO" # Fixed assets
|
||||
NODES = "NODES" # No discharge
|
||||
SCRAP = "SCRAP" # Scrap
|
||||
REEXP = "REEXP" # Re-export
|
||||
DONAC = "DONAC" # Donation
|
||||
VEMEX = "VEMEX" # Sale to Mexico
|
||||
ALL = "ALL" # All types
|
||||
|
||||
|
||||
class AllMovementsFilter(BaseModel):
|
||||
"""Filters for all movements query (all types combined)"""
|
||||
range_type: RangeType = Field(
|
||||
default=RangeType.INVOICE_DATE,
|
||||
description="Date range type: FF for invoice date, FP for payment date"
|
||||
)
|
||||
start_date: str = Field(
|
||||
...,
|
||||
description="Start date in YYYYMMDD format or ISO format"
|
||||
)
|
||||
end_date: str = Field(
|
||||
...,
|
||||
description="End date in YYYYMMDD format or ISO format"
|
||||
)
|
||||
include_cancelled: bool = Field(
|
||||
default=False,
|
||||
description="Include cancelled invoices (Estatus != 'AC')"
|
||||
)
|
||||
provider: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Filter by provider code"
|
||||
)
|
||||
buyer: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Filter by buyer code"
|
||||
)
|
||||
pedimento_code: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Filter by pedimento code (ClavePed)"
|
||||
)
|
||||
currency_type: CurrencyType = Field(
|
||||
default=CurrencyType.FOREIGN,
|
||||
description="Currency type for value calculations"
|
||||
)
|
||||
exchange_rate_type: ExchangeRateType = Field(
|
||||
default=ExchangeRateType.PAYMENT,
|
||||
description="Exchange rate calculation method"
|
||||
)
|
||||
is_shelter: bool = Field(
|
||||
default=False,
|
||||
description="Use shelter company logic"
|
||||
)
|
||||
operation_type: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Filter by operation type: 'imp' for imports only, 'exp' for exports only, None for all"
|
||||
)
|
||||
|
||||
|
||||
class ImportTemporaryFilter(BaseModel):
|
||||
"""Filters for temporary import movements query"""
|
||||
range_type: RangeType = Field(
|
||||
default=RangeType.INVOICE_DATE,
|
||||
description="Date range type: FF for invoice date, FP for payment date"
|
||||
)
|
||||
start_date: str = Field(
|
||||
...,
|
||||
description="Start date in YYYYMMDD format or ISO format"
|
||||
)
|
||||
end_date: str = Field(
|
||||
...,
|
||||
description="End date in YYYYMMDD format or ISO format"
|
||||
)
|
||||
include_cancelled: bool = Field(
|
||||
default=False,
|
||||
description="Include cancelled invoices (Estatus != 'AC')"
|
||||
)
|
||||
provider: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Filter by provider code"
|
||||
)
|
||||
buyer: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Filter by buyer code"
|
||||
)
|
||||
pedimento_code: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Filter by pedimento code (ClavePed)"
|
||||
)
|
||||
report_type: ReportType = Field(
|
||||
default=ReportType.NORMAL,
|
||||
description="Report type: Normal or Detailed"
|
||||
)
|
||||
currency_type: CurrencyType = Field(
|
||||
default=CurrencyType.FOREIGN,
|
||||
description="Currency type for value calculations"
|
||||
)
|
||||
exchange_rate_type: ExchangeRateType = Field(
|
||||
default=ExchangeRateType.PAYMENT,
|
||||
description="Exchange rate calculation method"
|
||||
)
|
||||
is_shelter: bool = Field(
|
||||
default=False,
|
||||
description="Use shelter company logic"
|
||||
)
|
||||
database_name: str = Field(
|
||||
...,
|
||||
description="Legacy database name to query from"
|
||||
)
|
||||
|
||||
|
||||
class ImportDefinitiveFilter(BaseModel):
|
||||
"""Filters for definitive import movements query"""
|
||||
range_type: RangeType = Field(
|
||||
default=RangeType.INVOICE_DATE,
|
||||
description="Date range type: FF for invoice date, FP for payment date"
|
||||
)
|
||||
start_date: str = Field(
|
||||
...,
|
||||
description="Start date in YYYYMMDD format or ISO format"
|
||||
)
|
||||
end_date: str = Field(
|
||||
...,
|
||||
description="End date in YYYYMMDD format or ISO format"
|
||||
)
|
||||
include_cancelled: bool = Field(
|
||||
default=False,
|
||||
description="Include cancelled invoices (Estatus != 'AC')"
|
||||
)
|
||||
provider: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Filter by provider code"
|
||||
)
|
||||
buyer: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Filter by buyer code (VendidoA)"
|
||||
)
|
||||
pedimento_code: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Filter by pedimento code (ClavePed)"
|
||||
)
|
||||
movement_type: MovementTypeFilter = Field(
|
||||
default=MovementTypeFilter.ALL,
|
||||
description="Movement type filter: COMEX, IMPDF, or ALL"
|
||||
)
|
||||
report_type: ReportType = Field(
|
||||
default=ReportType.NORMAL,
|
||||
description="Report type: Normal or Detailed"
|
||||
)
|
||||
currency_type: CurrencyType = Field(
|
||||
default=CurrencyType.FOREIGN,
|
||||
description="Currency type for value calculations"
|
||||
)
|
||||
exchange_rate_type: ExchangeRateType = Field(
|
||||
default=ExchangeRateType.PAYMENT,
|
||||
description="Exchange rate calculation method"
|
||||
)
|
||||
is_shelter: bool = Field(
|
||||
default=False,
|
||||
description="Use shelter company logic"
|
||||
)
|
||||
use_transport_method: bool = Field(
|
||||
default=False,
|
||||
description="Use MetTrans# = 1 logic for specific pedimento types"
|
||||
)
|
||||
database_name: str = Field(
|
||||
...,
|
||||
description="Legacy database name to query from"
|
||||
)
|
||||
|
||||
class MovementItem(BaseModel):
|
||||
"""Movement item representing a temporary import invoice"""
|
||||
Factura: Optional[str] = Field(None, description="Invoice number")
|
||||
Pedimento: Optional[str] = Field(None, description="Pedimento number")
|
||||
FechaFactura: Optional[datetime] = Field(None, description="Invoice date")
|
||||
Estatus: Optional[str] = Field(None, description="Status (AC=Active, etc)")
|
||||
ClavePed: Optional[str] = Field(None, description="Pedimento code")
|
||||
TipoMovTemDef: Optional[str] = Field(None, description="Movement type (IMTEM=Temporary Import)")
|
||||
EsCambioRegimen: Optional[str] = Field(None, description="Is regime change (S/N)")
|
||||
ValorMPTemp: Optional[float] = Field(None, description="Temporary raw material value")
|
||||
ValorComercialMN: Optional[float] = Field(None, description="Commercial value in MN")
|
||||
TipoCambio: Optional[float] = Field(None, description="Exchange rate used")
|
||||
ValorAgre: Optional[float] = Field(default=0.0, description="Aggregate value")
|
||||
TipoExpo: Optional[str] = Field(default='', description="Export type")
|
||||
PedimentoR1: Optional[str] = Field(None, description="Rectification pedimento")
|
||||
EDocument: Optional[str] = Field(None, description="Electronic document")
|
||||
NumOperacionVU: Optional[str] = Field(None, description="VU operation number")
|
||||
BaseDeDatos: Optional[str] = Field(None, description="Source database name")
|
||||
NumGafUni: Optional[str] = Field(None, description="Unique badge number (driver)")
|
||||
UsuarioCap: Optional[str] = Field(None, description="Capture user")
|
||||
UsuarioAcr: Optional[str] = Field(None, description="Update user")
|
||||
Fecha_Pago: Optional[datetime] = Field(None, description="Payment date")
|
||||
NumCaja: Optional[str] = Field(None, description="Box/Container number")
|
||||
Pedimento18: Optional[str] = Field(None, description="18-digit pedimento")
|
||||
AduanaCru: Optional[str] = Field(None, description="Crossing customs")
|
||||
Lote: Optional[str] = Field(None, description="Lot number")
|
||||
|
||||
model_config = {
|
||||
"json_schema_extra": {
|
||||
"example": {
|
||||
"Factura": "F-2024-001",
|
||||
"Pedimento": "24 47 3807 8001234",
|
||||
"FechaFactura": "2024-01-15T00:00:00",
|
||||
"Estatus": "AC",
|
||||
"ClavePed": "IM",
|
||||
"TipoMovTemDef": "IMTEM",
|
||||
"ValorMPTemp": 10000.50,
|
||||
"TipoCambio": 17.25
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ImportRepairFilter(BaseModel):
|
||||
"""Filters for repair import movements query"""
|
||||
range_type: RangeType = Field(
|
||||
default=RangeType.INVOICE_DATE,
|
||||
description="Date range type: FF for invoice date, FP for payment date"
|
||||
)
|
||||
start_date: str = Field(
|
||||
...,
|
||||
description="Start date in YYYYMMDD format or ISO format"
|
||||
)
|
||||
end_date: str = Field(
|
||||
...,
|
||||
description="End date in YYYYMMDD format or ISO format"
|
||||
)
|
||||
include_cancelled: bool = Field(
|
||||
default=False,
|
||||
description="Include cancelled invoices (Estatus != 'AC')"
|
||||
)
|
||||
provider: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Filter by provider code"
|
||||
)
|
||||
buyer: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Filter by buyer code (VendidoA)"
|
||||
)
|
||||
pedimento_code: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Filter by pedimento code (ClavePed)"
|
||||
)
|
||||
discharge_filter: DischargeFilter = Field(
|
||||
default=DischargeFilter.ALL,
|
||||
description="Discharge filter: SiDes (discharged), NoDes (not discharged), or ALL"
|
||||
)
|
||||
report_type: ReportType = Field(
|
||||
default=ReportType.NORMAL,
|
||||
description="Report type: Normal or Detailed"
|
||||
)
|
||||
currency_type: CurrencyType = Field(
|
||||
default=CurrencyType.FOREIGN,
|
||||
description="Currency type for value calculations"
|
||||
)
|
||||
exchange_rate_type: ExchangeRateType = Field(
|
||||
default=ExchangeRateType.PAYMENT,
|
||||
description="Exchange rate calculation method"
|
||||
)
|
||||
is_shelter: bool = Field(
|
||||
default=False,
|
||||
description="Use shelter company logic"
|
||||
)
|
||||
use_transport_method: bool = Field(
|
||||
default=False,
|
||||
description="Use MetTrans# = 1 logic for specific pedimento types"
|
||||
)
|
||||
database_name: str = Field(
|
||||
...,
|
||||
description="Legacy database name to query from"
|
||||
)
|
||||
|
||||
|
||||
class MovementItemDetailed(BaseModel):
|
||||
"""Detailed movement item with all line-level information"""
|
||||
Linea: Optional[int] = Field(None, description="Line number")
|
||||
Factura: Optional[str] = Field(None, description="Invoice number")
|
||||
Pedimento: Optional[str] = Field(None, description="Pedimento number")
|
||||
FechaFactura: Optional[datetime] = Field(None, description="Invoice date")
|
||||
Estatus: Optional[str] = Field(None, description="Status (AC=Active, etc)")
|
||||
ClavePed: Optional[str] = Field(None, description="Pedimento code")
|
||||
TipoMovTemDef: Optional[str] = Field(None, description="Movement type")
|
||||
EsCambioRegimen: Optional[str] = Field(None, description="Is regime change (S/N)")
|
||||
Regimen: Optional[str] = Field(None, description="Regime")
|
||||
Fecha_Inicio: Optional[datetime] = Field(None, description="Start date")
|
||||
Fecha_Fin: Optional[datetime] = Field(None, description="End date")
|
||||
Fecha_Pago: Optional[datetime] = Field(None, description="Payment date")
|
||||
Remesa: Optional[str] = Field(None, description="Remesa")
|
||||
|
||||
# Provider information
|
||||
Proveedor: Optional[str] = Field(None, description="Provider name")
|
||||
RFCProveedor: Optional[str] = Field(None, description="Provider RFC")
|
||||
ProveedorTaxID: Optional[str] = Field(None, description="Provider Tax ID")
|
||||
|
||||
# Buyer information
|
||||
VendidoA: Optional[str] = Field(None, description="Buyer name")
|
||||
VendidoARFC: Optional[str] = Field(None, description="Buyer RFC")
|
||||
VendidoATaxID: Optional[str] = Field(None, description="Buyer Tax ID")
|
||||
|
||||
# Customs broker
|
||||
AgenteAduanal: Optional[str] = Field(None, description="Customs broker name")
|
||||
Patente: Optional[str] = Field(None, description="Customs broker patent")
|
||||
|
||||
# Item details
|
||||
NumParte: Optional[str] = Field(None, description="Part number")
|
||||
DescripcionE: Optional[str] = Field(None, description="Spanish description")
|
||||
DescripcionI: Optional[str] = Field(None, description="English description")
|
||||
CantidadIE: Optional[float] = Field(None, description="Quantity")
|
||||
UniMed: Optional[str] = Field(None, description="Unit of measure")
|
||||
ValorComercialMN: Optional[float] = Field(None, description="Commercial value in MN")
|
||||
TipoCambio: Optional[float] = Field(None, description="Exchange rate")
|
||||
PesoNeto: Optional[float] = Field(None, description="Net weight")
|
||||
PesoBruto: Optional[float] = Field(None, description="Gross weight")
|
||||
|
||||
# Additional fields
|
||||
OrdenCompraVenta: Optional[str] = Field(None, description="Purchase order")
|
||||
FraccionArancelaria: Optional[str] = Field(None, description="Tariff fraction")
|
||||
Preferencia: Optional[str] = Field(None, description="Preference")
|
||||
Sector: Optional[str] = Field(None, description="Sector")
|
||||
PaisOrigen: Optional[str] = Field(None, description="Country of origin")
|
||||
Aduana: Optional[str] = Field(None, description="Customs office")
|
||||
Advalorem: Optional[str] = Field(None, description="Ad valorem")
|
||||
TipoExpo: Optional[str] = Field(default='', description="Export type")
|
||||
PedimentoR1: Optional[str] = Field(None, description="Rectification pedimento")
|
||||
EDocument: Optional[str] = Field(None, description="Electronic document")
|
||||
NumOperacionVU: Optional[str] = Field(None, description="VU operation number")
|
||||
|
||||
# Series information
|
||||
Series: Optional[str] = Field(None, description="Serial numbers")
|
||||
Marca: Optional[str] = Field(None, description="Brand")
|
||||
Modelo: Optional[str] = Field(None, description="Model")
|
||||
FraccionAmericana: Optional[str] = Field(None, description="American tariff fraction")
|
||||
ECCN: Optional[str] = Field(None, description="ECCN code")
|
||||
SimboloEx: Optional[str] = Field(None, description="Export symbol/license")
|
||||
FechaEmision: Optional[datetime] = Field(None, description="Emission date")
|
||||
|
||||
# Metadata
|
||||
BaseDeDatos: Optional[str] = Field(None, description="Source database")
|
||||
NumGafUni: Optional[str] = Field(None, description="Unique badge number")
|
||||
UsuarioCap: Optional[str] = Field(None, description="Capture user")
|
||||
UsuarioAcr: Optional[str] = Field(None, description="Update user")
|
||||
Transportista: Optional[str] = Field(None, description="Transporter")
|
||||
NumCaja: Optional[str] = Field(None, description="Box number")
|
||||
Pedimento18: Optional[str] = Field(None, description="18-digit pedimento")
|
||||
AduanaCru: Optional[str] = Field(None, description="Crossing customs")
|
||||
Lote: Optional[str] = Field(None, description="Lot number")
|
||||
|
||||
model_config = {
|
||||
"json_schema_extra": {
|
||||
"example": {
|
||||
"Linea": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ExportFilter(BaseModel):
|
||||
"""Filters for export movements query"""
|
||||
range_type: RangeType = Field(
|
||||
default=RangeType.INVOICE_DATE,
|
||||
description="Date range type: FF for invoice date, FP for payment date"
|
||||
)
|
||||
start_date: str = Field(
|
||||
...,
|
||||
description="Start date in YYYYMMDD format or ISO format"
|
||||
)
|
||||
end_date: str = Field(
|
||||
...,
|
||||
description="End date in YYYYMMDD format or ISO format"
|
||||
)
|
||||
include_cancelled: bool = Field(
|
||||
default=False,
|
||||
description="Include cancelled invoices (Estatus = 'NA')"
|
||||
)
|
||||
provider: Optional[str] = Field(
|
||||
None,
|
||||
description="Filter by provider code"
|
||||
)
|
||||
buyer: Optional[str] = Field(
|
||||
None,
|
||||
description="Filter by buyer code (VendidoA)"
|
||||
)
|
||||
pedimento_code: Optional[str] = Field(
|
||||
None,
|
||||
description="Filter by pedimento code (ClavePed)"
|
||||
)
|
||||
movement_type: ExportMovementType = Field(
|
||||
default=ExportMovementType.ALL,
|
||||
description="Filter by export movement type (AFIJO, NODES, SCRAP, REEXP, DONAC, VEMEX)"
|
||||
)
|
||||
discharge_filter: DischargeFilter = Field(
|
||||
default=DischargeFilter.ALL,
|
||||
description="Filter by discharge status: SiDes, NoDes, or ALL"
|
||||
)
|
||||
report_type: ReportType = Field(
|
||||
default=ReportType.NORMAL,
|
||||
description="Normal (grouped by invoice) or Detallado (line by line)"
|
||||
)
|
||||
currency_type: CurrencyType = Field(
|
||||
default=CurrencyType.FOREIGN,
|
||||
description="Currency type: ME (foreign) or MN (local)"
|
||||
)
|
||||
exchange_rate_type: ExchangeRateType = Field(
|
||||
default=ExchangeRateType.PAYMENT,
|
||||
description="Exchange rate type: FP (payment date) or FF (invoice date)"
|
||||
)
|
||||
is_shelter: bool = Field(
|
||||
default=False,
|
||||
description="Shelter company flag"
|
||||
)
|
||||
use_transport_method: bool = Field(
|
||||
default=False,
|
||||
description="Use transport method for exchange rate logic"
|
||||
)
|
||||
database_name: str = Field(
|
||||
...,
|
||||
description="Legacy database name"
|
||||
)
|
||||
|
||||
model_config = {
|
||||
"json_schema_extra": {
|
||||
"example": {
|
||||
"range_type": "FF",
|
||||
"start_date": "20240101",
|
||||
"end_date": "20240131",
|
||||
"include_cancelled": False,
|
||||
"provider": None,
|
||||
"buyer": None,
|
||||
"pedimento_code": None,
|
||||
"movement_type": "ALL",
|
||||
"discharge_filter": "ALL",
|
||||
"report_type": "Normal",
|
||||
"currency_type": "ME",
|
||||
"exchange_rate_type": "FP",
|
||||
"is_shelter": False,
|
||||
"use_transport_method": False,
|
||||
"database_name": "MYDB"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ExportRepairFilter(BaseModel):
|
||||
"""Filters for export repair movements query (EXPO REP)"""
|
||||
range_type: RangeType = Field(
|
||||
default=RangeType.INVOICE_DATE,
|
||||
description="Date range type: FF for invoice date, FP for payment date"
|
||||
)
|
||||
start_date: str = Field(
|
||||
...,
|
||||
description="Start date in YYYYMMDD format or ISO format"
|
||||
)
|
||||
end_date: str = Field(
|
||||
...,
|
||||
description="End date in YYYYMMDD format or ISO format"
|
||||
)
|
||||
include_cancelled: bool = Field(
|
||||
default=False,
|
||||
description="Include cancelled invoices (Estatus = 'NA')"
|
||||
)
|
||||
provider: Optional[str] = Field(
|
||||
None,
|
||||
description="Filter by provider code"
|
||||
)
|
||||
buyer: Optional[str] = Field(
|
||||
None,
|
||||
description="Filter by buyer code (VendidoA)"
|
||||
)
|
||||
pedimento_code: Optional[str] = Field(
|
||||
None,
|
||||
description="Filter by pedimento code (ClavePed)"
|
||||
)
|
||||
movement_type: ExportMovementType = Field(
|
||||
default=ExportMovementType.ALL,
|
||||
description="Filter by movement type (AFIJO, NODES for repair exports)"
|
||||
)
|
||||
discharge_filter: DischargeFilter = Field(
|
||||
default=DischargeFilter.ALL,
|
||||
description="Filter by discharge status: SiDes, NoDes, or ALL"
|
||||
)
|
||||
report_type: ReportType = Field(
|
||||
default=ReportType.NORMAL,
|
||||
description="Normal (grouped by invoice) or Detallado (line by line)"
|
||||
)
|
||||
currency_type: CurrencyType = Field(
|
||||
default=CurrencyType.FOREIGN,
|
||||
description="Currency type: ME (foreign) or MN (local)"
|
||||
)
|
||||
exchange_rate_type: ExchangeRateType = Field(
|
||||
default=ExchangeRateType.PAYMENT,
|
||||
description="Exchange rate type: FP (payment date) or FF (invoice date)"
|
||||
)
|
||||
is_shelter: bool = Field(
|
||||
default=False,
|
||||
description="Shelter company flag"
|
||||
)
|
||||
database_name: str = Field(
|
||||
...,
|
||||
description="Legacy database name"
|
||||
)
|
||||
|
||||
model_config = {
|
||||
"json_schema_extra": {
|
||||
"example": {
|
||||
"range_type": "FF",
|
||||
"start_date": "20240101",
|
||||
"end_date": "20240131",
|
||||
"include_cancelled": False,
|
||||
"provider": None,
|
||||
"buyer": None,
|
||||
"pedimento_code": None,
|
||||
"movement_type": "ALL",
|
||||
"discharge_filter": "ALL",
|
||||
"report_type": "Normal",
|
||||
"currency_type": "ME",
|
||||
"exchange_rate_type": "FP",
|
||||
"is_shelter": False,
|
||||
"database_name": "MYDB"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
Unified service for invoice movement operations.
|
||||
This service delegates to specialized handlers for each import type.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
|
||||
from .schemas import (
|
||||
ImportTemporaryFilter,
|
||||
ImportDefinitiveFilter,
|
||||
ImportRepairFilter,
|
||||
ExportFilter,
|
||||
MovementItem,
|
||||
MovementItemDetailed
|
||||
)
|
||||
from .services.temporary import TemporaryImportService
|
||||
from .services.definitive import DefinitiveImportService
|
||||
from .services.repair import RepairImportService
|
||||
from .services.export import ExportService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MovementService:
|
||||
"""
|
||||
Unified service for handling all types of movements.
|
||||
Delegates to specialized services for each movement type.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.temporary_service = TemporaryImportService()
|
||||
self.definitive_service = DefinitiveImportService()
|
||||
self.repair_service = RepairImportService()
|
||||
self.export_service = ExportService()
|
||||
|
||||
# ===== TEMPORARY IMPORTS =====
|
||||
|
||||
def get_temporary_import_movements(
|
||||
self,
|
||||
db: Session,
|
||||
filters: ImportTemporaryFilter
|
||||
) -> List[MovementItem]:
|
||||
"""Get temporary import movements (normal mode - grouped by invoice)."""
|
||||
return self.temporary_service.get_movements(db, filters)
|
||||
|
||||
def get_temporary_import_movements_detailed(
|
||||
self,
|
||||
db: Session,
|
||||
filters: ImportTemporaryFilter
|
||||
) -> List[MovementItemDetailed]:
|
||||
"""Get temporary import movements (detailed mode - line by line)."""
|
||||
return self.temporary_service.get_movements_detailed(db, filters)
|
||||
|
||||
# ===== DEFINITIVE IMPORTS =====
|
||||
|
||||
def get_definitive_import_movements(
|
||||
self,
|
||||
db: Session,
|
||||
filters: ImportDefinitiveFilter
|
||||
) -> List[MovementItem]:
|
||||
"""Get definitive import movements (normal mode - grouped by invoice)."""
|
||||
return self.definitive_service.get_movements(db, filters)
|
||||
|
||||
def get_definitive_import_movements_detailed(
|
||||
self,
|
||||
db: Session,
|
||||
filters: ImportDefinitiveFilter
|
||||
) -> List[MovementItemDetailed]:
|
||||
"""Get definitive import movements (detailed mode - line by line)."""
|
||||
return self.definitive_service.get_movements_detailed(db, filters)
|
||||
|
||||
# ===== REPAIR IMPORTS =====
|
||||
|
||||
def get_repair_import_movements(
|
||||
self,
|
||||
db: Session,
|
||||
filters: ImportRepairFilter
|
||||
) -> List[MovementItem]:
|
||||
"""Get repair import movements (normal mode - grouped by invoice)."""
|
||||
return self.repair_service.get_movements(db, filters)
|
||||
|
||||
def get_repair_import_movements_detailed(
|
||||
self,
|
||||
db: Session,
|
||||
filters: ImportRepairFilter
|
||||
) -> List[MovementItemDetailed]:
|
||||
"""Get repair import movements (detailed mode - line by line)."""
|
||||
return self.repair_service.get_movements_detailed(db, filters)
|
||||
|
||||
# ===== EXPORTS =====
|
||||
|
||||
def get_export_movements(
|
||||
self,
|
||||
db: Session,
|
||||
filters: ExportFilter
|
||||
) -> List[MovementItem]:
|
||||
"""Get export movements (normal mode - grouped by invoice)."""
|
||||
return self.export_service.get_movements(db, filters)
|
||||
|
||||
def get_export_movements_detailed(
|
||||
self,
|
||||
db: Session,
|
||||
filters: ExportFilter
|
||||
) -> List[MovementItemDetailed]:
|
||||
"""Get export movements (detailed mode - line by line)."""
|
||||
return self.export_service.get_movements_detailed(db, filters)
|
||||
|
||||
|
||||
# Singleton instance
|
||||
movement_service = MovementService()
|
||||
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
Invoice Movement Services Module
|
||||
|
||||
This package contains the business logic for handling different types of movements:
|
||||
- Temporary imports (IMTEM)
|
||||
- Definitive imports (COMEX/IMPDF)
|
||||
- Repair imports (IMPRE)
|
||||
- Exports (EXPO DEF)
|
||||
- Export repairs (EXPO REP)
|
||||
|
||||
The services are organized into specialized modules for better maintainability.
|
||||
"""
|
||||
|
||||
from .temporary import TemporaryImportService
|
||||
from .definitive import DefinitiveImportService
|
||||
from .repair import RepairImportService
|
||||
from .export import ExportService
|
||||
from .export_repair import ExportRepairService
|
||||
|
||||
__all__ = [
|
||||
'TemporaryImportService',
|
||||
'DefinitiveImportService',
|
||||
'RepairImportService',
|
||||
'ExportService',
|
||||
'ExportRepairService',
|
||||
]
|
||||
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Base utilities and configuration helpers for invoice movement services.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import configparser
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConfigHelper:
|
||||
"""Helper for reading configuration files."""
|
||||
|
||||
@staticmethod
|
||||
def get_met_trans_config() -> int:
|
||||
"""
|
||||
Read MetTrans configuration from Scaii.ini file.
|
||||
|
||||
Returns:
|
||||
MetTrans value (0 or 1)
|
||||
"""
|
||||
try:
|
||||
config = configparser.ConfigParser()
|
||||
config.read('Scaii.ini')
|
||||
met_trans = config.getint('METTRANS', 'TipoCambio', fallback=0)
|
||||
logger.debug(f"INI met_trans value: {met_trans}")
|
||||
return met_trans
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not read Scaii.ini, using default met_trans=0: {e}")
|
||||
return 0
|
||||
|
||||
|
||||
class StringHelper:
|
||||
"""Helper for string manipulation."""
|
||||
|
||||
@staticmethod
|
||||
def remove_commas(text: Optional[str]) -> Optional[str]:
|
||||
"""Remove commas from text for CSV compatibility."""
|
||||
if not text:
|
||||
return text
|
||||
return text.replace(',', '')
|
||||
|
||||
|
||||
class DateHelper:
|
||||
"""Helper for date-related operations."""
|
||||
|
||||
@staticmethod
|
||||
def get_fecha_tipo_cambio(
|
||||
fecha_pago,
|
||||
fecha_inicio,
|
||||
tipo_pedimento: str,
|
||||
use_transport_method: bool,
|
||||
met_trans: int
|
||||
):
|
||||
"""
|
||||
Determine which date to use for exchange rate lookup based on MetTrans logic.
|
||||
|
||||
Args:
|
||||
fecha_pago: Payment date
|
||||
fecha_inicio: Start/entry date
|
||||
tipo_pedimento: Pedimento type code
|
||||
use_transport_method: Whether to apply transport method logic
|
||||
met_trans: MetTrans configuration value
|
||||
|
||||
Returns:
|
||||
Date to use for exchange rate lookup
|
||||
"""
|
||||
fecha = fecha_pago
|
||||
|
||||
# MetTrans# = 1 logic: use fecha_inicio for specific pedimento types
|
||||
if use_transport_method and met_trans == 1:
|
||||
if tipo_pedimento in ('1', '4', '98E'):
|
||||
fecha = fecha_inicio
|
||||
|
||||
return fecha
|
||||
@@ -0,0 +1,500 @@
|
||||
"""
|
||||
Database query helpers for invoice movements.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional, Dict
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DatabaseHelper:
|
||||
"""Helper for common database operations."""
|
||||
|
||||
@staticmethod
|
||||
def get_database_name(db: Session) -> Optional[str]:
|
||||
"""
|
||||
Get the current database name from the session.
|
||||
|
||||
Returns:
|
||||
Database name or None if not found
|
||||
"""
|
||||
try:
|
||||
result = db.execute(text("SELECT current_database()")).fetchone()
|
||||
return result[0] if result else None
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting database name: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_exchange_rate(
|
||||
db: Session,
|
||||
db_name: str,
|
||||
fecha,
|
||||
is_shelter: bool = False,
|
||||
raise_on_missing: bool = False,
|
||||
pedimento_number: Optional[str] = None
|
||||
) -> Optional[float]:
|
||||
"""
|
||||
Get exchange rate for the given date from exchange_rate table.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
db_name: Legacy database name (kept for compatibility, not used)
|
||||
fecha: Date for exchange rate lookup
|
||||
is_shelter: Shelter company flag (when True and rate not found, raises detailed error)
|
||||
raise_on_missing: If True, raises ValueError when rate not found
|
||||
pedimento_number: Pedimento number for error messages
|
||||
|
||||
Returns:
|
||||
Exchange rate as float, or None if not found
|
||||
|
||||
Raises:
|
||||
ValueError: When is_shelter=True and exchange rate not found
|
||||
"""
|
||||
if not fecha:
|
||||
return None
|
||||
|
||||
try:
|
||||
# TODO: Verify exchange_rate table structure and column names
|
||||
sql_tc = text("""
|
||||
SELECT rate
|
||||
FROM a76.exchange_rate
|
||||
WHERE rate_date = :fecha
|
||||
ORDER BY rate_date DESC
|
||||
LIMIT 1
|
||||
""")
|
||||
res = db.execute(sql_tc, {"fecha": fecha}).fetchone()
|
||||
if res and res[0]:
|
||||
return float(res[0])
|
||||
else:
|
||||
# Clarion logic: For Shelter operations with FP, missing exchange rate is an error
|
||||
if is_shelter and raise_on_missing:
|
||||
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}."
|
||||
)
|
||||
logger.warning(f"Exchange rate not found for date {fecha}")
|
||||
return None
|
||||
except ValueError:
|
||||
raise # Re-raise validation errors
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching exchange rate for date {fecha}: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_client_info(
|
||||
db: Session,
|
||||
db_name: str,
|
||||
client_code: str,
|
||||
is_supplier: bool = True
|
||||
) -> Dict[str, Optional[str]]:
|
||||
"""
|
||||
Get client or supplier information (name, RFC, TaxID).
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
db_name: Database name (kept for compatibility, not used)
|
||||
client_code: Client/supplier code
|
||||
is_supplier: True for suppliers, False for clients
|
||||
|
||||
Returns:
|
||||
Dict with 'name', 'rfc', 'tax_id' keys
|
||||
"""
|
||||
if not client_code:
|
||||
return {"name": None, "rfc": None, "tax_id": None}
|
||||
|
||||
client_type = 'provider' if is_supplier else 'client'
|
||||
|
||||
try:
|
||||
sql = text("""
|
||||
SELECT name, rfc, tax_id
|
||||
FROM a76.clients_and_providers
|
||||
WHERE id = :client_code AND client_or_provider = :client_type
|
||||
""")
|
||||
result = db.execute(sql, {"client_code": client_code, "client_type": client_type}).fetchone()
|
||||
|
||||
if result:
|
||||
return {
|
||||
"name": result[0],
|
||||
"rfc": result[1],
|
||||
"tax_id": result[2]
|
||||
}
|
||||
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}
|
||||
|
||||
@staticmethod
|
||||
def get_customs_agent_info(
|
||||
db: Session,
|
||||
db_name: str,
|
||||
agent_code: str
|
||||
) -> Dict[str, Optional[str]]:
|
||||
"""
|
||||
Get customs agent information (name, license).
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
db_name: Database name (kept for compatibility, not used)
|
||||
agent_code: Customs agent code
|
||||
|
||||
Returns:
|
||||
Dict with 'name', 'license' keys
|
||||
"""
|
||||
if not agent_code:
|
||||
return {"name": None, "license": None}
|
||||
|
||||
try:
|
||||
sql = text("""
|
||||
SELECT name, license
|
||||
FROM a76.customs_brokers
|
||||
WHERE id = :agent_code
|
||||
LIMIT 1
|
||||
""")
|
||||
result = db.execute(sql, {"agent_code": agent_code}).fetchone()
|
||||
|
||||
if result:
|
||||
return {
|
||||
"name": result[0],
|
||||
"license": result[1]
|
||||
}
|
||||
else:
|
||||
logger.debug(f"Customs agent {agent_code} not found")
|
||||
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}
|
||||
|
||||
@staticmethod
|
||||
def get_aduana_seccion_nombre(
|
||||
db: Session,
|
||||
db_name: str,
|
||||
aduana_seccion: str
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Get customs section name.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
db_name: Database name
|
||||
aduana_seccion: Customs section code
|
||||
|
||||
Returns:
|
||||
Customs section name or None
|
||||
"""
|
||||
if not aduana_seccion:
|
||||
return None
|
||||
|
||||
try:
|
||||
query = text("""
|
||||
SELECT section_name
|
||||
FROM public.customs_sections
|
||||
WHERE customs_code = :code
|
||||
""")
|
||||
result = db.execute(query, {"code": aduana_seccion}).fetchone()
|
||||
return result[0] if result else None
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching customs section name: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_series_info(
|
||||
db: Session,
|
||||
db_name: str,
|
||||
consecutivo: str,
|
||||
linea: str,
|
||||
is_shelter: bool
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Get series information for import items.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
db_name: Legacy database name
|
||||
consecutivo: Consecutivo value
|
||||
linea: LineaImpo value
|
||||
is_shelter: Shelter flag
|
||||
|
||||
Returns:
|
||||
Formatted series string or None
|
||||
"""
|
||||
if not consecutivo 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.items i ON il.item_id = i.id
|
||||
WHERE i.consecutivo = :consecutivo
|
||||
AND il.line_number = :linea
|
||||
ORDER BY ils.id
|
||||
LIMIT 1
|
||||
""")
|
||||
result = db.execute(query, {
|
||||
"consecutivo": consecutivo,
|
||||
"linea": linea
|
||||
}).fetchone()
|
||||
|
||||
if result:
|
||||
serial_numbers, model, brand = result
|
||||
parts = []
|
||||
if serial_numbers:
|
||||
parts.append(serial_numbers)
|
||||
if model:
|
||||
parts.append(model)
|
||||
if brand:
|
||||
parts.append(brand)
|
||||
return " / ".join(parts) if parts else None
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching series info: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_series_info_export(
|
||||
db: Session,
|
||||
db_name: str,
|
||||
consecutivo: str,
|
||||
linea: str,
|
||||
is_shelter: bool
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Get series information for export items.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
db_name: Legacy database name
|
||||
consecutivo: Consecutivo value
|
||||
linea: LineaExpo value
|
||||
is_shelter: Shelter flag
|
||||
|
||||
Returns:
|
||||
Formatted series string or None
|
||||
"""
|
||||
if not consecutivo or not linea:
|
||||
return None
|
||||
|
||||
try:
|
||||
query = text("""
|
||||
SELECT serial_numbers, model, expo_brand
|
||||
FROM a76.item_line_series ils
|
||||
INNER JOIN a76.item_lines il ON ils.item_line_id = il.id
|
||||
INNER JOIN a76.items i ON il.item_id = i.id
|
||||
WHERE i.consecutivo = :consecutivo
|
||||
AND il.line_number = :linea
|
||||
ORDER BY ils.id
|
||||
LIMIT 1
|
||||
""")
|
||||
result = db.execute(query, {
|
||||
"consecutivo": consecutivo,
|
||||
"linea": linea
|
||||
}).fetchone()
|
||||
|
||||
if result:
|
||||
serial_numbers, model, expo_brand = result
|
||||
parts = []
|
||||
if serial_numbers:
|
||||
parts.append(serial_numbers)
|
||||
if model:
|
||||
parts.append(model)
|
||||
if expo_brand:
|
||||
parts.append(expo_brand)
|
||||
return " / ".join(parts) if parts else None
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching export series info: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_rectification_pedimento(
|
||||
db: Session,
|
||||
pedimento: str,
|
||||
ped_rectifica: Optional[str],
|
||||
is_shelter: bool = False
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Get final pedimento rectification number following the chain recursively.
|
||||
|
||||
Clarion logic:
|
||||
- IF Loc:OpcionShelter = 1 THEN: use direct field value (PedRectifica)
|
||||
- ELSE: call BuscarRectificacion() - follows rectification chain recursively
|
||||
|
||||
BuscarRectificacion follows the chain:
|
||||
Example: A1 -> A2 -> A3 -> A4 (returns A4, the final rectification)
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
pedimento: Original pedimento number
|
||||
ped_rectifica: Initial rectification pedimento from database field
|
||||
is_shelter: Shelter company flag
|
||||
|
||||
Returns:
|
||||
Final rectification pedimento number in the chain, or None/empty if no rectification
|
||||
"""
|
||||
if is_shelter:
|
||||
# Shelter: use direct value from PedRectifica field
|
||||
return ped_rectifica
|
||||
else:
|
||||
# Non-Shelter: implement BuscarRectificacion logic
|
||||
return DatabaseHelper._buscar_rectificacion(db, pedimento, ped_rectifica)
|
||||
|
||||
@staticmethod
|
||||
def _buscar_rectificacion(
|
||||
db: Session,
|
||||
pedimento_orig: str,
|
||||
ped_rec: Optional[str]
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
BUSCA ULTIMO PEDIMENTO DE RECTIFICACION
|
||||
Follows the rectification chain recursively until finding the final pedimento.
|
||||
|
||||
Clarion logic:
|
||||
- If PPedRec is empty, return ''
|
||||
- Otherwise, follow the chain using BUSCA_PEDIMENTO_R1 recursively
|
||||
- Return the last Pedimento2 in the chain if no circular reference
|
||||
- Return Pedimento1 if error (circular reference detected)
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
pedimento_orig: Original pedimento number
|
||||
ped_rec: Initial rectification pedimento
|
||||
|
||||
Returns:
|
||||
Final pedimento in rectification chain or empty string
|
||||
"""
|
||||
if not ped_rec:
|
||||
return ''
|
||||
|
||||
try:
|
||||
# Track visited pedimentos to detect circular references
|
||||
visited = set()
|
||||
visited.add(pedimento_orig)
|
||||
|
||||
# Start recursive search
|
||||
final_pedimento = DatabaseHelper._busca_pedimento_r1(
|
||||
db, ped_rec, visited
|
||||
)
|
||||
|
||||
# If successful, return final pedimento; otherwise return original rectification
|
||||
return final_pedimento if final_pedimento else ped_rec
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in BuscarRectificacion for {pedimento_orig}: {e}")
|
||||
return pedimento_orig
|
||||
|
||||
@staticmethod
|
||||
def _busca_pedimento_r1(
|
||||
db: Session,
|
||||
pedimento: str,
|
||||
visited: set
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
BUSCA_PEDIMENTO_R1 ROUTINE - Recursive search for final rectification pedimento.
|
||||
|
||||
Clarion logic:
|
||||
- Fetch pedimento from QPedimentos table
|
||||
- If it has PedRectifica:
|
||||
- Check if already visited (circular reference = error)
|
||||
- Add to visited set and recurse with PedRectifica
|
||||
- Return the deepest pedimento found
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
pedimento: Current pedimento to check
|
||||
visited: Set of already visited pedimentos (prevents infinite loops)
|
||||
|
||||
Returns:
|
||||
Final pedimento in chain, or None if circular reference detected
|
||||
"""
|
||||
if pedimento in visited:
|
||||
# Circular reference detected (ERRORCODE = 30 equivalent)
|
||||
logger.warning(f"Circular reference detected in rectification chain: {pedimento}")
|
||||
return None
|
||||
|
||||
try:
|
||||
# Query pedimentos table for ped_rectifica
|
||||
sql = text("""
|
||||
SELECT ped_rectifica
|
||||
FROM a76.pedimentos
|
||||
WHERE pedimento_number = :pedimento
|
||||
LIMIT 1
|
||||
""")
|
||||
result = db.execute(sql, {"pedimento": pedimento}).fetchone()
|
||||
|
||||
if result and result[0]:
|
||||
ped_rectifica_next = result[0]
|
||||
|
||||
# Add current pedimento to visited set
|
||||
visited.add(pedimento)
|
||||
|
||||
# Recurse with next rectification
|
||||
final_ped = DatabaseHelper._busca_pedimento_r1(
|
||||
db, ped_rectifica_next, visited
|
||||
)
|
||||
|
||||
# If recursion failed (circular ref), return None
|
||||
# Otherwise return the final pedimento found
|
||||
return final_ped if final_ped else pedimento
|
||||
else:
|
||||
# No more rectifications, this is the final pedimento
|
||||
return pedimento
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching rectification for pedimento {pedimento}: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_driver_badge(
|
||||
db: Session,
|
||||
db_name: str,
|
||||
factura: str
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Get driver unique badge number (NUMGAFETEUNICO) for invoice.
|
||||
|
||||
Clarion query:
|
||||
SELECT NUMGAFETEUNICO FROM GConductor
|
||||
LEFT JOIN QFacImp ON QFacImp.CONDUCTOR = GConductor.CONDUCTOR
|
||||
WHERE FacturaImpo = '<factura>'
|
||||
|
||||
Modern schema:
|
||||
- invoice_header has invoice_number
|
||||
- invoice_logistics links to invoice via invoice_id and has driver_name
|
||||
- driver table has unique_badge_number and driver_name
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
db_name: Database name (not used in modern schema)
|
||||
factura: Invoice number
|
||||
|
||||
Returns:
|
||||
Driver unique badge number or None
|
||||
"""
|
||||
if not factura:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Join invoice_header -> invoice_logistics -> driver via driver_name
|
||||
query = text("""
|
||||
SELECT d.unique_badge_number
|
||||
FROM a76.invoice_header ih
|
||||
LEFT JOIN a76.invoice_logistics log ON log.invoice_id = ih.id
|
||||
LEFT JOIN a76.driver d ON d.driver_name = log.driver_name
|
||||
WHERE ih.invoice_number = :factura
|
||||
AND d.unique_badge_number IS NOT NULL
|
||||
LIMIT 1
|
||||
""")
|
||||
result = db.execute(query, {"factura": factura}).fetchone()
|
||||
return result[0] if result else None
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching driver badge for invoice {factura}: {e}")
|
||||
return None
|
||||
@@ -0,0 +1,372 @@
|
||||
"""
|
||||
Definitive import service - handles COMEX/IMPDF movements.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..schemas import ImportDefinitiveFilter, MovementItem, MovementItemDetailed
|
||||
|
||||
from .base import ConfigHelper, StringHelper
|
||||
from .database_helpers import DatabaseHelper
|
||||
from .exchange_rate import ExchangeRateCalculator
|
||||
from .query_builders import DefinitiveImportQueries
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def parse_yyyymmdd_date(date_str: str) -> Optional[datetime]:
|
||||
"""Parse date string in YYYYMMDD format to datetime."""
|
||||
if not date_str or date_str == '':
|
||||
return None
|
||||
try:
|
||||
return datetime.strptime(date_str, '%Y%m%d')
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
class DefinitiveImportService:
|
||||
"""Service for handling definitive import movements (COMEX/IMPDF)."""
|
||||
|
||||
def get_movements(
|
||||
self,
|
||||
db: Session,
|
||||
filters: "ImportDefinitiveFilter"
|
||||
) -> List["MovementItem"]:
|
||||
"""
|
||||
Get definitive import movements (normal mode - grouped by invoice).
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
filters: Filter criteria
|
||||
|
||||
Returns:
|
||||
List of movement items grouped by invoice
|
||||
"""
|
||||
from ..schemas import MovementItem
|
||||
|
||||
try:
|
||||
logger.info(f"Fetching definitive import movements with filters: {filters.model_dump()}")
|
||||
|
||||
# Get MetTrans configuration
|
||||
met_trans = ConfigHelper.get_met_trans_config()
|
||||
|
||||
# Build WHERE clause
|
||||
where_clause = self._build_where_clause(filters)
|
||||
|
||||
# Execute optimized aggregated query for NORMAL mode
|
||||
sql = text(DefinitiveImportQueries.build_aggregated_query(filters.database_name, where_clause))
|
||||
results = db.execute(sql).fetchall()
|
||||
logger.info(f"Found {len(results)} definitive import invoices")
|
||||
|
||||
movements = []
|
||||
|
||||
for row in results:
|
||||
factura = row[0] # C1 - FacturaImpoDef
|
||||
estatus = row[3] # C4 - Estatus (AC o NA)
|
||||
|
||||
# Filtrar facturas según include_cancelled
|
||||
# Si include_cancelled=False, solo mostrar AC (is_updated=true)
|
||||
# Si include_cancelled=True, mostrar todas (AC y NA)
|
||||
if not filters.include_cancelled and estatus != 'AC':
|
||||
continue
|
||||
|
||||
invoice_id = row[16] # C35 - invoice ID
|
||||
|
||||
# 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
|
||||
|
||||
# Calculate exchange rate and value
|
||||
valor_comercial, tipo_cambio = ExchangeRateCalculator.calculate_for_aggregated(
|
||||
db=db,
|
||||
db_name=filters.database_name,
|
||||
valor_me=total_me,
|
||||
valor_mn=total_mn,
|
||||
tipo_cambio_db=row[20], # C51 - TipoCambio
|
||||
fecha_pago=row[8], # C13 - Fecha_Pago
|
||||
fecha_inicio=row[6], # C11 - Fecha_Inicio
|
||||
tipo_pedimento=row[27], # C59 - TIPOPEDIMENTOTRANSPORTEE
|
||||
currency_type=filters.currency_type.value,
|
||||
exchange_rate_type=filters.exchange_rate_type.value,
|
||||
is_shelter=filters.is_shelter,
|
||||
use_transport_method=False,
|
||||
met_trans=met_trans
|
||||
)
|
||||
|
||||
# Get pedimento rectification
|
||||
pedimento_r1 = DatabaseHelper.get_rectification_pedimento(
|
||||
db,
|
||||
row[1], # C2 - PedimentoImpoDef
|
||||
row[17], # C42 - PedRectifica
|
||||
filters.is_shelter
|
||||
)
|
||||
|
||||
# Get driver badge
|
||||
num_gaf_uni = DatabaseHelper.get_driver_badge(
|
||||
db, filters.database_name, factura
|
||||
)
|
||||
|
||||
# Build movement item
|
||||
movement = MovementItem(
|
||||
Factura=factura,
|
||||
Pedimento=row[1], # C2 - PedimentoImpoDef
|
||||
FechaFactura=parse_yyyymmdd_date(row[2]), # C3 - FechaFactura
|
||||
Estatus=row[3], # C4 - Estatus
|
||||
ClavePed=row[4], # C5 - ClavePed
|
||||
TipoMovTemDef='IMPDF',
|
||||
EsCambioRegimen='N',
|
||||
ValorMPTemp=valor_comercial,
|
||||
ValorComercialMN=valor_comercial,
|
||||
TipoCambio=tipo_cambio,
|
||||
ValorAgre=0.0,
|
||||
TipoExpo='',
|
||||
PedimentoR1=pedimento_r1,
|
||||
EDocument=row[18], # C43 - EDocument
|
||||
NumOperacionVU=row[19], # C44 - NumOperacionVU
|
||||
BaseDeDatos=filters.database_name,
|
||||
NumGafUni=num_gaf_uni,
|
||||
UsuarioCap=row[22], # C53 - UsuarioCap
|
||||
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
|
||||
AduanaCru=row[15], # C39 - Aduana_Cruce
|
||||
Lote=row[26] # C58 - LOTE
|
||||
)
|
||||
|
||||
movements.append(movement)
|
||||
|
||||
logger.info(f"Successfully processed {len(movements)} definitive import movements")
|
||||
return movements
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching definitive import movements: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
def get_movements_detailed(
|
||||
self,
|
||||
db: Session,
|
||||
filters: "ImportDefinitiveFilter"
|
||||
) -> List["MovementItemDetailed"]:
|
||||
"""
|
||||
Get definitive import movements (detailed mode - line by line).
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
filters: Filter criteria
|
||||
|
||||
Returns:
|
||||
List of detailed movement items (one per partida)
|
||||
"""
|
||||
from ..schemas import MovementItemDetailed
|
||||
|
||||
try:
|
||||
logger.info(f"Fetching detailed definitive import movements with filters: {filters.model_dump()}")
|
||||
|
||||
# Get MetTrans configuration
|
||||
met_trans = ConfigHelper.get_met_trans_config()
|
||||
|
||||
# Build WHERE clause
|
||||
where_clause = self._build_where_clause(filters)
|
||||
|
||||
# Execute main query
|
||||
sql = text(DefinitiveImportQueries.build_main_query(filters.database_name, where_clause))
|
||||
results = db.execute(sql).fetchall()
|
||||
logger.info(f"Found {len(results)} detailed definitive import partidas")
|
||||
|
||||
movements = []
|
||||
|
||||
for row in results:
|
||||
# Skip cancelled if not included
|
||||
if not filters.include_cancelled and row[3] != 'AC': # C4 - Estatus
|
||||
continue
|
||||
|
||||
# Get all detailed information (same as temporary imports)
|
||||
proveedor_info = DatabaseHelper.get_client_info(
|
||||
db, filters.database_name, row[15], is_supplier=True
|
||||
)
|
||||
vendido_info = DatabaseHelper.get_client_info(
|
||||
db, filters.database_name, row[16], is_supplier=False
|
||||
)
|
||||
agente_info = DatabaseHelper.get_customs_agent_info(
|
||||
db, filters.database_name, row[17]
|
||||
)
|
||||
aduana_nombre = DatabaseHelper.get_aduana_seccion_nombre(
|
||||
db, filters.database_name, row[38]
|
||||
)
|
||||
|
||||
# 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],
|
||||
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
|
||||
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
|
||||
)
|
||||
|
||||
simbolo_ex = None
|
||||
if row[49]:
|
||||
simbolo_ex = DatabaseHelper.get_part_export_symbol(
|
||||
db, filters.database_name, row[49], filters.is_shelter
|
||||
)
|
||||
|
||||
pedimento_r1 = DatabaseHelper.get_rectification_pedimento(
|
||||
db, row[1], row[41], filters.is_shelter
|
||||
)
|
||||
|
||||
num_gaf_uni = DatabaseHelper.get_driver_badge(
|
||||
db, filters.database_name, row[0]
|
||||
)
|
||||
|
||||
movement = MovementItemDetailed(
|
||||
Linea=row[44],
|
||||
Factura=row[0],
|
||||
Pedimento=row[1],
|
||||
FechaFactura=row[2],
|
||||
Estatus=row[3],
|
||||
ClavePed=row[4],
|
||||
TipoMovTemDef='IMPDF',
|
||||
EsCambioRegimen='N',
|
||||
Regimen=row[9],
|
||||
Fecha_Inicio=row[10],
|
||||
Fecha_Fin=row[11],
|
||||
Fecha_Pago=row[12],
|
||||
Remesa=row[13],
|
||||
Proveedor=proveedor_info.get('name'),
|
||||
RFCProveedor=proveedor_info.get('rfc'),
|
||||
ProveedorTaxID=proveedor_info.get('tax_id'),
|
||||
VendidoA=vendido_info.get('name'),
|
||||
VendidoARFC=vendido_info.get('rfc'),
|
||||
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],
|
||||
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],
|
||||
Aduana=aduana_nombre,
|
||||
Advalorem=row[39],
|
||||
TipoExpo='',
|
||||
PedimentoR1=pedimento_r1,
|
||||
EDocument=row[42],
|
||||
NumOperacionVU=row[43],
|
||||
Series=series_info,
|
||||
Marca=StringHelper.clean_text(row[45]),
|
||||
Modelo=StringHelper.clean_text(row[46]),
|
||||
FraccionAmericana=row[47],
|
||||
ECCN=row[48],
|
||||
SimboloEx=simbolo_ex,
|
||||
FechaEmision=row[51],
|
||||
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]
|
||||
)
|
||||
|
||||
movements.append(movement)
|
||||
|
||||
logger.info(f"Successfully processed {len(movements)} detailed definitive import movements")
|
||||
return movements
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching detailed definitive import movements: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
def _build_where_clause(self, filters: "ImportDefinitiveFilter") -> str:
|
||||
"""Build WHERE clause for definitive imports query."""
|
||||
where_conditions = []
|
||||
|
||||
# STRICT SEPARATION: Only imports
|
||||
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')")
|
||||
|
||||
# Date range filter
|
||||
if filters.range_type.value == "FF":
|
||||
where_conditions.append(f"ih.invoice_date >= TO_DATE('{filters.start_date}', 'YYYYMMDD') AND ih.invoice_date <= TO_DATE('{filters.end_date}', 'YYYYMMDD')")
|
||||
else:
|
||||
where_conditions.append(f"log.payment_date >= TO_DATE('{filters.start_date}', 'YYYYMMDD') AND log.payment_date <= TO_DATE('{filters.end_date}', 'YYYYMMDD')")
|
||||
|
||||
# Note: Status filter applied at Python level after CASE WHEN in SELECT
|
||||
# because is_updated doesn't directly represent AC/NA status
|
||||
|
||||
# Provider filter
|
||||
if filters.provider:
|
||||
where_conditions.append(f"cmp.provider_id = {filters.provider}")
|
||||
|
||||
# Buyer filter
|
||||
if filters.buyer:
|
||||
where_conditions.append(f"cmp.sold_to_id = {filters.buyer}")
|
||||
|
||||
# Pedimento code filter
|
||||
if filters.pedimento_code:
|
||||
where_conditions.append(f"ped.pedimento_code = '{filters.pedimento_code}'")
|
||||
|
||||
return " AND ".join(where_conditions)
|
||||
|
||||
def _calculate_totals(self, db: Session, db_name: str, consecutivo: int) -> tuple:
|
||||
"""Calculate totals for main partidas only.
|
||||
|
||||
Only sums partidas where is_subpartida is false (equivalent to EsSubpartida = 'P' in Clarion).
|
||||
"""
|
||||
sql = text("""
|
||||
SELECT
|
||||
COALESCE(SUM(lf.value_usd), 0),
|
||||
COALESCE(SUM(lf.value_mxn), 0)
|
||||
FROM a76.item_line_financials lf
|
||||
INNER JOIN a76.item_lines il ON il.id = lf.item_line_id
|
||||
INNER JOIN a76.items itm ON itm.id = il.item_id
|
||||
WHERE itm.invoice_id = :consecutivo
|
||||
AND COALESCE(il.is_subpartida, false) = false
|
||||
""")
|
||||
|
||||
result = db.execute(sql, {"consecutivo": consecutivo}).fetchone()
|
||||
total_me = float(result[0]) if result and result[0] is not None else 0.0
|
||||
total_mn = float(result[1]) if result and result[1] is not None else 0.0
|
||||
|
||||
return total_me, total_mn
|
||||
@@ -0,0 +1,172 @@
|
||||
"""
|
||||
Exchange rate calculation logic for invoice movements.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Tuple, Optional
|
||||
from .base import DateHelper
|
||||
from .database_helpers import DatabaseHelper
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ExchangeRateCalculator:
|
||||
"""Handles exchange rate calculations and commercial value conversions."""
|
||||
|
||||
@staticmethod
|
||||
def calculate_exchange_rate_and_value(
|
||||
db: Session,
|
||||
db_name: str,
|
||||
es_subpartida: str,
|
||||
valor_me: Optional[float],
|
||||
valor_mn_direct: Optional[float],
|
||||
fecha_pago,
|
||||
fecha_inicio,
|
||||
clave_ped: str,
|
||||
tipo_cambio_partida: Optional[float],
|
||||
currency_type: str,
|
||||
exchange_rate_type: str,
|
||||
met_trans: int
|
||||
) -> Tuple[float, Optional[float]]:
|
||||
"""
|
||||
Unified method to calculate exchange rate and commercial value.
|
||||
Eliminates duplicated logic across all import types.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
db_name: Database name
|
||||
es_subpartida: Subpartida flag ('P' for partida, 'S' for subpartida)
|
||||
valor_me: Value in foreign currency (ME)
|
||||
valor_mn_direct: Direct value in local currency (MN)
|
||||
fecha_pago: Payment date
|
||||
fecha_inicio: Start/entry date
|
||||
clave_ped: Pedimento type code
|
||||
tipo_cambio_partida: Exchange rate from partida record
|
||||
currency_type: "ME" or "MN"
|
||||
exchange_rate_type: "FP" (payment date) or "FT" (transaction date)
|
||||
met_trans: MetTrans configuration value
|
||||
|
||||
Returns:
|
||||
Tuple of (valor_comercial_mn, tipo_cambio_final)
|
||||
"""
|
||||
# Handle subpartidas - always return zero
|
||||
if es_subpartida == 'S':
|
||||
return (0.0, None)
|
||||
|
||||
# Handle foreign currency (ME) case
|
||||
if currency_type == "ME":
|
||||
valor_comercial = valor_me or 0.0
|
||||
tipo_cambio_final = tipo_cambio_partida
|
||||
|
||||
# Try to get exchange rate from GTipoCambio if using payment date
|
||||
if exchange_rate_type == "FP" and fecha_pago:
|
||||
fecha_tc = DateHelper.get_fecha_tipo_cambio(
|
||||
fecha_pago=fecha_pago,
|
||||
fecha_inicio=fecha_inicio,
|
||||
tipo_pedimento=clave_ped,
|
||||
use_transport_method=True, # Always use for detailed calculations
|
||||
met_trans=met_trans
|
||||
)
|
||||
tc_value = DatabaseHelper.get_exchange_rate(db, db_name, fecha_tc)
|
||||
if tc_value:
|
||||
tipo_cambio_final = tc_value
|
||||
|
||||
return (valor_comercial, tipo_cambio_final)
|
||||
|
||||
# Handle local currency (MN) case
|
||||
if exchange_rate_type == "FP" and fecha_pago:
|
||||
fecha_tc = DateHelper.get_fecha_tipo_cambio(
|
||||
fecha_pago=fecha_pago,
|
||||
fecha_inicio=fecha_inicio,
|
||||
tipo_pedimento=clave_ped,
|
||||
use_transport_method=True,
|
||||
met_trans=met_trans
|
||||
)
|
||||
tc_value = DatabaseHelper.get_exchange_rate(db, db_name, fecha_tc)
|
||||
|
||||
if tc_value and valor_me is not None:
|
||||
# Calculate MN value from ME * exchange rate
|
||||
return (valor_me * tc_value, tc_value)
|
||||
else:
|
||||
# Fall back to direct MN value and partida exchange rate
|
||||
if tc_value is None:
|
||||
logger.warning(f"Exchange rate not found for date {fecha_tc}, using partida values")
|
||||
return (valor_mn_direct or 0.0, tipo_cambio_partida)
|
||||
else:
|
||||
# Use direct MN value and partida exchange rate
|
||||
return (valor_mn_direct or 0.0, tipo_cambio_partida)
|
||||
|
||||
@staticmethod
|
||||
def calculate_for_aggregated(
|
||||
db: Session,
|
||||
db_name: str,
|
||||
valor_me: float,
|
||||
valor_mn: float,
|
||||
tipo_cambio_db: float,
|
||||
fecha_pago,
|
||||
fecha_inicio,
|
||||
tipo_pedimento: str,
|
||||
currency_type: str,
|
||||
exchange_rate_type: str,
|
||||
is_shelter: bool,
|
||||
use_transport_method: bool,
|
||||
met_trans: int
|
||||
) -> Tuple[float, Optional[float]]:
|
||||
"""
|
||||
Calculate exchange rate and value for aggregated (normal mode) movements.
|
||||
|
||||
This method is used when movements are grouped by invoice rather than
|
||||
showing individual partidas.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
db_name: Database name
|
||||
valor_me: Aggregated value in foreign currency
|
||||
valor_mn: Aggregated value in local currency
|
||||
tipo_cambio_db: Exchange rate from database
|
||||
fecha_pago: Payment date
|
||||
fecha_inicio: Start date
|
||||
tipo_pedimento: Pedimento type
|
||||
currency_type: "ME" or "MN"
|
||||
exchange_rate_type: "FP" or "FT"
|
||||
is_shelter: Shelter company flag (kept for compatibility)
|
||||
use_transport_method: Use transport method flag
|
||||
met_trans: MetTrans value from config
|
||||
|
||||
Returns:
|
||||
Tuple of (valor_comercial_mn, tipo_cambio)
|
||||
"""
|
||||
# Foreign currency case
|
||||
if currency_type == "ME":
|
||||
valor_comercial = valor_me
|
||||
tipo_cambio = tipo_cambio_db
|
||||
|
||||
# Try to get exchange rate if using payment date
|
||||
if exchange_rate_type == "FP" and fecha_pago:
|
||||
fecha_tc = DateHelper.get_fecha_tipo_cambio(
|
||||
fecha_pago=fecha_pago,
|
||||
fecha_inicio=fecha_inicio,
|
||||
tipo_pedimento=tipo_pedimento,
|
||||
use_transport_method=use_transport_method,
|
||||
met_trans=met_trans
|
||||
)
|
||||
# For Shelter + FP: validate exchange rate exists (Clarion logic)
|
||||
tc_value = DatabaseHelper.get_exchange_rate(
|
||||
db, db_name, fecha_tc,
|
||||
is_shelter=is_shelter,
|
||||
raise_on_missing=is_shelter # Raise error if shelter and not found
|
||||
)
|
||||
|
||||
if tc_value:
|
||||
return (valor_me * tc_value, tc_value)
|
||||
else:
|
||||
logger.warning(f"Exchange rate not found for date {fecha_tc}, using DB values")
|
||||
return (valor_mn, tipo_cambio_db)
|
||||
else:
|
||||
# FT or no payment date: use DB values
|
||||
return (valor_comercial, tipo_cambio)
|
||||
|
||||
# Local currency case
|
||||
else:
|
||||
return (valor_mn, tipo_cambio_db)
|
||||
@@ -0,0 +1,435 @@
|
||||
"""
|
||||
Export service - handles export movements (EXPO DEF).
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
|
||||
from ..schemas import ExportFilter, MovementItem, MovementItemDetailed
|
||||
from .base import ConfigHelper, StringHelper
|
||||
from .database_helpers import DatabaseHelper
|
||||
from .exchange_rate import ExchangeRateCalculator
|
||||
from .query_builders import ExportQueries
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def parse_yyyymmdd_date(date_str: str) -> Optional[datetime]:
|
||||
"""Parse date string in YYYYMMDD format to datetime."""
|
||||
if not date_str or date_str == '':
|
||||
return None
|
||||
try:
|
||||
return datetime.strptime(date_str, '%Y%m%d')
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
class ExportService:
|
||||
"""Service for handling export movements (EXPO DEF)."""
|
||||
|
||||
def get_movements(
|
||||
self,
|
||||
db: Session,
|
||||
filters: ExportFilter
|
||||
) -> List[MovementItem]:
|
||||
"""
|
||||
Get export movements (normal mode - grouped by invoice).
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
filters: Filter criteria
|
||||
|
||||
Returns:
|
||||
List of movement items grouped by invoice
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Fetching export movements with filters: {filters.model_dump()}")
|
||||
|
||||
# Get MetTrans configuration
|
||||
met_trans = ConfigHelper.get_met_trans_config()
|
||||
|
||||
# Build WHERE clause
|
||||
where_clause = self._build_where_clause(filters)
|
||||
|
||||
# Execute optimized aggregated query for NORMAL mode
|
||||
# Note: discharge_clause not used in aggregated query for exports
|
||||
sql = text(ExportQueries.build_aggregated_query(filters.database_name, where_clause))
|
||||
results = db.execute(sql).fetchall()
|
||||
logger.info(f"Found {len(results)} export invoices")
|
||||
|
||||
movements = []
|
||||
|
||||
for row in results:
|
||||
factura = row[0] # C1 - FacturaExpo
|
||||
tipo_mov = row[15] # C34 - TipoFactura
|
||||
|
||||
# Skip cancelled if not included
|
||||
if not filters.include_cancelled and row[3] != 'AC': # C6 - Estatus
|
||||
continue
|
||||
|
||||
consecutivo = row[16] # C35 - Consecutivo
|
||||
|
||||
# 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
|
||||
|
||||
# Calculate exchange rate and value
|
||||
valor_comercial, tipo_cambio = ExchangeRateCalculator.calculate_for_aggregated(
|
||||
db=db,
|
||||
db_name=filters.database_name,
|
||||
valor_me=total_me,
|
||||
valor_mn=total_mn,
|
||||
tipo_cambio_db=row[19], # C48 - TipoCambio
|
||||
fecha_pago=row[8], # C11 - Fecha_Pago
|
||||
fecha_inicio=row[6], # C9 - Fecha_Inicio
|
||||
tipo_pedimento='', # Not in aggregated query
|
||||
currency_type=filters.currency_type.value,
|
||||
exchange_rate_type=filters.exchange_rate_type.value,
|
||||
is_shelter=filters.is_shelter,
|
||||
use_transport_method=filters.use_transport_method,
|
||||
met_trans=met_trans
|
||||
)
|
||||
|
||||
# Get pedimento rectification
|
||||
rectified_pedimento = DatabaseHelper.get_rectification_pedimento(
|
||||
db,
|
||||
row[1], # C2 - PedimentoExpo
|
||||
'', # PedRectifica not in aggregated query
|
||||
filters.is_shelter
|
||||
)
|
||||
|
||||
# Get driver badge
|
||||
driver_badge = self._get_driver_badge(db, filters.database_name, factura)
|
||||
|
||||
# Build movement item
|
||||
movement = MovementItem(
|
||||
Factura=factura,
|
||||
Pedimento=row[1], # C2 - PedimentoExpo
|
||||
FechaFactura=parse_yyyymmdd_date(row[2]), # C3 - FechaFactura
|
||||
Estatus=row[3], # C6 - Estatus
|
||||
ClavePed=row[4], # C7 - ClavePed
|
||||
TipoMovTemDef=tipo_mov,
|
||||
EsCambioRegimen='N',
|
||||
ValorMPTemp=valor_comercial,
|
||||
ValorComercialMN=valor_comercial,
|
||||
TipoCambio=tipo_cambio,
|
||||
ValorAgre=0.0,
|
||||
TipoExpo='EXPO DEF',
|
||||
PedimentoR1=rectified_pedimento,
|
||||
EDocument=row[17], # C40 - EDocument
|
||||
NumOperacionVU=row[18], # C41 - NumOperacionVU
|
||||
BaseDeDatos=filters.database_name,
|
||||
NumGafUni=driver_badge,
|
||||
UsuarioCap=row[21], # C50 - UsuarioCap
|
||||
UsuarioAcr=row[22], # C51 - UsuarioAct
|
||||
Fecha_Pago=parse_yyyymmdd_date(row[8]), # C11 - Fecha_Pago
|
||||
NumCaja=row[23], # C53 - Transporte + NumTrasporte
|
||||
Pedimento18='', # Not in aggregated query
|
||||
AduanaCru=row[14], # C33 - Aduana_Cruce
|
||||
Lote='' # Not in aggregated query
|
||||
)
|
||||
|
||||
movements.append(movement)
|
||||
|
||||
logger.info(f"Successfully processed {len(movements)} export movements")
|
||||
return movements
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching export movements: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
def get_movements_detailed(
|
||||
self,
|
||||
db: Session,
|
||||
filters: ExportFilter
|
||||
) -> List[MovementItemDetailed]:
|
||||
"""
|
||||
Get export movements (detailed mode - line by line).
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
filters: Filter criteria
|
||||
|
||||
Returns:
|
||||
List of detailed movement items (one per partida)
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Fetching detailed export movements with filters: {filters.model_dump()}")
|
||||
|
||||
# Get MetTrans configuration
|
||||
met_trans = ConfigHelper.get_met_trans_config()
|
||||
|
||||
# Build WHERE clause
|
||||
where_clause = self._build_where_clause(filters)
|
||||
|
||||
# Build discharge filter for main query
|
||||
discharge_clause = ""
|
||||
if filters.discharge_filter == "SiDes":
|
||||
discharge_clause = " AND EqiPex.Descarga = 1"
|
||||
elif filters.discharge_filter == "NoDes":
|
||||
discharge_clause = " AND EqiPex.Descarga = 0"
|
||||
|
||||
# Modify main query to include discharge filter
|
||||
where_with_discharge = where_clause + discharge_clause
|
||||
|
||||
# Execute main query
|
||||
sql = text(ExportQueries.build_main_query(filters.database_name, where_with_discharge))
|
||||
results = db.execute(sql).fetchall()
|
||||
logger.info(f"Found {len(results)} detailed export partidas")
|
||||
|
||||
movements = []
|
||||
|
||||
for row in results:
|
||||
# Skip cancelled if not included
|
||||
if not filters.include_cancelled and row[5] == 'NA': # C6 - Estatus
|
||||
continue
|
||||
|
||||
# Get client/supplier information
|
||||
proveedor_info = DatabaseHelper.get_client_info(
|
||||
db, filters.database_name, row[13], is_supplier=True # C14 - Proveedor
|
||||
)
|
||||
vendido_info = DatabaseHelper.get_client_info(
|
||||
db, filters.database_name, row[14], is_supplier=False # C15 - VendidoA
|
||||
)
|
||||
|
||||
# Get customs agent information
|
||||
agente_info = DatabaseHelper.get_customs_agent_info(
|
||||
db, filters.database_name, row[15] # C16 - AAduanal
|
||||
)
|
||||
|
||||
# Get customs section name
|
||||
customs_name = DatabaseHelper.get_aduana_seccion_nombre(
|
||||
db, filters.database_name, row[32] # C33 - Aduana_Cruce
|
||||
)
|
||||
|
||||
# Calculate exchange rate and value for this partida
|
||||
valor_mn, tipo_cambio_final = ExchangeRateCalculator.calculate_exchange_rate_and_value(
|
||||
db=db,
|
||||
db_name=filters.database_name,
|
||||
es_subpartida=row[37], # C38 - EsSubPartida
|
||||
valor_me=row[36], # C37 - ValorExpoME
|
||||
valor_mn_direct=row[35], # C36 - ValorExpoMN
|
||||
fecha_pago=row[10], # C11 - Fecha_Pago
|
||||
fecha_inicio=row[8], # C9 - Fecha_Inicio
|
||||
clave_ped=row[55], # C56 - TIPOPEDIMENTOTRANSPORTEE
|
||||
tipo_cambio_partida=row[47], # C48 - TipoCambio
|
||||
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
|
||||
peso_neto_final = row[24] if row[37] == 'P' else 0 # C25 - PesoNeto
|
||||
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
|
||||
)
|
||||
|
||||
# Get pedimento rectification
|
||||
rectified_pedimento = DatabaseHelper.get_rectification_pedimento(
|
||||
db,
|
||||
row[1], # C2 - PedimentoExpo
|
||||
row[38], # C39 - PedRectifica
|
||||
filters.is_shelter
|
||||
)
|
||||
|
||||
# Get driver badge
|
||||
driver_badge = self._get_driver_badge(db, filters.database_name, row[0]) # C1 - FacturaExpo
|
||||
|
||||
# Build detailed movement item
|
||||
movement = MovementItemDetailed(
|
||||
Linea=row[41], # C42 - LineaExpo
|
||||
Factura=row[0], # C1 - FacturaExpo
|
||||
Pedimento=row[1], # C2 - PedimentoExpo
|
||||
FechaFactura=row[2], # C3 - FechaFactura
|
||||
Estatus=row[5], # C6 - Estatus
|
||||
ClavePed=row[6], # C7 - ClavePed
|
||||
TipoMovTemDef=row[33], # C34 - TipoFactura
|
||||
EsCambioRegimen='N',
|
||||
Regimen=row[7], # C8 - Regimen
|
||||
Fecha_Inicio=row[8], # C9 - Fecha_Inicio
|
||||
Fecha_Fin=row[9], # C10 - Fecha_Fin
|
||||
Fecha_Pago=row[10], # C11 - Fecha_Pago
|
||||
Remesa=row[11], # C12 - Remesa
|
||||
TipoCambio=tipo_cambio_final,
|
||||
Proveedor=proveedor_info.get("name"),
|
||||
RFCProveedor=proveedor_info.get("rfc"),
|
||||
ProveedorTaxID=proveedor_info.get("tax_id"),
|
||||
VendidoA=vendido_info.get("name"),
|
||||
VendidoARFC=vendido_info.get("rfc"),
|
||||
VendidoATaxID=vendido_info.get("tax_id"),
|
||||
AgenteAduanal=agente_info.get("name"),
|
||||
Patente=agente_info.get("license"),
|
||||
NumParte=row[17], # C18 - Clase
|
||||
DescripcionE=StringHelper.remove_commas(row[18]), # C19 - DescripcionE
|
||||
DescripcionI=StringHelper.remove_commas(row[19]), # C20 - DescripcionI
|
||||
CantidadIE=row[20], # C21 - CantExpo
|
||||
UniMed=row[21], # C22 - UnidadMedida
|
||||
ValorComercialMN=valor_mn,
|
||||
PesoNeto=peso_neto_final,
|
||||
PesoBruto=peso_bruto_final,
|
||||
OrdenCompraVenta=row[26], # C27 - OrdenCompra
|
||||
FraccionArancelaria=row[27], # C28 - FraccionExpo
|
||||
Preferencia=row[28], # C29 - TipoFraccion
|
||||
Sector=row[30], # C31 - Sector
|
||||
PaisOrigen=row[31], # C32 - PaisOrigen
|
||||
Aduana=customs_name,
|
||||
Advalorem=row[37], # C38 - EsSubPartida
|
||||
TipoExpo='EXPO DEF',
|
||||
PedimentoR1=rectified_pedimento,
|
||||
EDocument=row[39], # C40 - EDocument
|
||||
NumOperacionVU=row[40], # C41 - NumOperacionVU
|
||||
Series=series_info,
|
||||
Marca=row[42], # C43 - Marca
|
||||
Modelo=row[43], # C44 - Modelo
|
||||
FraccionAmericana=row[44], # C45 - FraccionAme
|
||||
ECCN=row[45], # C46 - ECCN
|
||||
FechaEmision=row[48], # C49 - FechaEmision
|
||||
BaseDeDatos=filters.database_name,
|
||||
NumGafUni=driver_badge,
|
||||
UsuarioCap=row[49], # C50 - UsuarioCap
|
||||
UsuarioAcr=row[50], # C51 - UsuarioAct
|
||||
Transportista=row[51], # C52 - Transportista
|
||||
NumCaja=row[52], # C53 - Transporte + NumTrasporte
|
||||
Pedimento18=row[53], # C54 - Pedimento18
|
||||
AduanaCru=row[32], # C33 - Aduana_Cruce
|
||||
Lote=row[54] # C55 - Lote
|
||||
)
|
||||
|
||||
movements.append(movement)
|
||||
|
||||
logger.info(f"Successfully processed {len(movements)} detailed export movements")
|
||||
return movements
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching detailed export movements: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
def _build_where_clause(self, filters: ExportFilter) -> str:
|
||||
"""
|
||||
Build WHERE clause for export query.
|
||||
|
||||
IMPORTANT: Returns conditions WITHOUT the WHERE keyword (already in base query)
|
||||
AC (Active) = is_updated = true
|
||||
NA (Not Applicable/Deactivated) = is_updated = false
|
||||
"""
|
||||
conditions = []
|
||||
|
||||
# STRICT SEPARATION: Only exports
|
||||
conditions.append("ih.operation_type = 'exp'")
|
||||
|
||||
# Exclude REP (export reports)
|
||||
conditions.append("ih.invoice_type NOT IN ('REP')")
|
||||
|
||||
# GOLDEN RULE: If movement_type is ALL, only filter by operation_type
|
||||
if filters.movement_type.value != "ALL":
|
||||
# Filter by invoice type for exports
|
||||
conditions.append("ih.invoice_type IN ('EXP', 'EXREP')")
|
||||
|
||||
# CRITICAL VALIDATION: AC/NA status filter
|
||||
# If include_cancelled is False (checkbox unchecked), only show AC invoices
|
||||
# AC (Active) = is_updated = true
|
||||
# NA (Not Applicable/Deactivated) = is_updated = false
|
||||
if not filters.include_cancelled:
|
||||
conditions.append("ih.is_updated = true")
|
||||
logger.debug("Filtering only active invoices (is_updated = true)")
|
||||
else:
|
||||
logger.debug("Including cancelled invoices (include_cancelled = true)")
|
||||
|
||||
# Date range
|
||||
date_field = "ih.invoice_date" if filters.range_type.value == "FF" else "log.payment_date"
|
||||
conditions.append(f"{date_field} >= TO_DATE('{filters.start_date}', 'YYYYMMDD')")
|
||||
conditions.append(f"{date_field} <= TO_DATE('{filters.end_date}', 'YYYYMMDD')")
|
||||
|
||||
# Optional filters
|
||||
if filters.provider:
|
||||
conditions.append(f"cmp.provider_id = {filters.provider}")
|
||||
if filters.buyer:
|
||||
conditions.append(f"cmp.sold_to_id = {filters.buyer}")
|
||||
if filters.pedimento_code:
|
||||
conditions.append(f"ped.pedimento_code = '{filters.pedimento_code}'")
|
||||
|
||||
return " AND ".join(conditions)
|
||||
|
||||
def _build_discharge_clause(self, discharge_filter: str) -> str:
|
||||
"""Build discharge filter clause for totals query."""
|
||||
if discharge_filter == "SiDes":
|
||||
return " AND il.is_discharged = true"
|
||||
elif discharge_filter == "NoDes":
|
||||
return " AND il.is_discharged = false"
|
||||
return ""
|
||||
|
||||
def _calculate_totals(
|
||||
self,
|
||||
db: Session,
|
||||
db_name: str,
|
||||
consecutivo: int,
|
||||
discharge_clause: str
|
||||
) -> tuple:
|
||||
"""Calculate total values for an export invoice."""
|
||||
try:
|
||||
sql = text(ExportQueries.build_totals_query(db_name, discharge_clause))
|
||||
result = db.execute(sql, {"consecutivo": consecutivo}).fetchone()
|
||||
|
||||
if result:
|
||||
return (result[0] or 0, result[1] or 0)
|
||||
return (0, 0)
|
||||
except Exception as e:
|
||||
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:
|
||||
return None
|
||||
|
||||
try:
|
||||
sql = text(ExportQueries.build_driver_badge_query(db_name))
|
||||
result = db.execute(sql, {"factura": factura}).fetchone()
|
||||
return result[0] if result else None
|
||||
except Exception as e:
|
||||
logger.debug(f"Error fetching driver badge for export invoice {factura}: {e}")
|
||||
return None
|
||||
@@ -0,0 +1,400 @@
|
||||
"""
|
||||
Export repair service - handles EXPO REP movements (repair exports).
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional
|
||||
|
||||
from ..schemas import ExportRepairFilter, MovementItem, MovementItemDetailed
|
||||
from .base import ConfigHelper, StringHelper
|
||||
from .database_helpers import DatabaseHelper
|
||||
from .exchange_rate import ExchangeRateCalculator
|
||||
from .query_builders import ExportRepairQueries
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def parse_yyyymmdd_date(date_str: str) -> Optional[datetime]:
|
||||
"""Parse date string in YYYYMMDD format to datetime."""
|
||||
if not date_str or date_str == '':
|
||||
return None
|
||||
try:
|
||||
return datetime.strptime(date_str, '%Y%m%d')
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
class ExportRepairService:
|
||||
"""Service for handling export repair movements (EXPO REP)."""
|
||||
|
||||
def get_movements(
|
||||
self,
|
||||
db: Session,
|
||||
filters: ExportRepairFilter
|
||||
) -> List[MovementItem]:
|
||||
"""
|
||||
Get export repair movements (normal mode - grouped by invoice).
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
filters: Filter criteria
|
||||
|
||||
Returns:
|
||||
List of movement items grouped by invoice
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Fetching export repair movements with filters: {filters.model_dump()}")
|
||||
|
||||
# Get MetTrans configuration
|
||||
met_trans = ConfigHelper.get_met_trans_config()
|
||||
|
||||
# Build WHERE clause
|
||||
where_clause = self._build_where_clause(filters)
|
||||
|
||||
# Execute optimized aggregated query for NORMAL mode
|
||||
sql = text(ExportRepairQueries.build_aggregated_query(filters.database_name, where_clause))
|
||||
results = db.execute(sql).fetchall()
|
||||
logger.info(f"Found {len(results)} export repair invoices")
|
||||
|
||||
movements = []
|
||||
|
||||
for row in results:
|
||||
factura = row[0] # C1 - FacturaExpo
|
||||
tipo_mov = row[14] # C34 - TipoFactura
|
||||
estatus = row[3] # C6 - Estatus (AC o NA)
|
||||
|
||||
# Filtrar facturas según include_cancelled
|
||||
# Si include_cancelled=False, solo mostrar AC (is_updated=true)
|
||||
# Si include_cancelled=True, mostrar todas (AC y NA)
|
||||
if not filters.include_cancelled and estatus != 'AC':
|
||||
continue
|
||||
|
||||
consecutivo = row[15] # C35 - Consecutivo
|
||||
|
||||
# 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
|
||||
|
||||
# Calculate exchange rate and value
|
||||
valor_comercial, tipo_cambio = ExchangeRateCalculator.calculate_for_aggregated(
|
||||
db=db,
|
||||
db_name=filters.database_name,
|
||||
valor_me=total_me,
|
||||
valor_mn=total_mn,
|
||||
tipo_cambio_db=row[18], # C48 - TipoCambio
|
||||
fecha_pago=row[6], # C11 - Fecha_Pago
|
||||
fecha_inicio='', # Not in aggregated query
|
||||
tipo_pedimento='', # Not in aggregated query
|
||||
currency_type=filters.currency_type.value,
|
||||
exchange_rate_type=filters.exchange_rate_type.value,
|
||||
is_shelter=filters.is_shelter,
|
||||
use_transport_method=False,
|
||||
met_trans=met_trans
|
||||
)
|
||||
|
||||
# Get pedimento rectification
|
||||
pedimento_r1 = DatabaseHelper.get_rectification_pedimento(
|
||||
db,
|
||||
row[1], # C2 - PedimentoExpo
|
||||
'', # PedRectifica not in aggregated query
|
||||
filters.is_shelter
|
||||
)
|
||||
|
||||
# Get driver badge
|
||||
num_gaf_uni = self._get_driver_badge(db, filters.database_name, factura)
|
||||
|
||||
# Build movement item
|
||||
movement = MovementItem(
|
||||
Factura=factura,
|
||||
Pedimento=row[1], # C2 - PedimentoExpo
|
||||
FechaFactura=parse_yyyymmdd_date(row[2]), # C3 - FechaFactura
|
||||
Estatus=row[3], # C6 - Estatus
|
||||
ClavePed=row[4], # C7 - ClavePed
|
||||
TipoMovTemDef=tipo_mov,
|
||||
EsCambioRegimen='N',
|
||||
ValorMPTemp=valor_comercial,
|
||||
ValorComercialMN=valor_comercial,
|
||||
TipoCambio=tipo_cambio,
|
||||
ValorAgre=0.0,
|
||||
TipoExpo='EXPO REP',
|
||||
PedimentoR1=pedimento_r1,
|
||||
EDocument=row[16], # C40 - EDocument
|
||||
NumOperacionVU=row[17], # C41 - NumOperacionVU
|
||||
BaseDeDatos=filters.database_name,
|
||||
NumGafUni=num_gaf_uni,
|
||||
UsuarioCap=row[20], # C50 - UsuarioCap
|
||||
UsuarioAcr=row[21], # C51 - UsuarioAct
|
||||
Fecha_Pago=parse_yyyymmdd_date(row[6]), # C11 - Fecha_Pago
|
||||
NumCaja=row[23], # C53 - Transporte + NumTrasporte
|
||||
Pedimento18='', # Not in aggregated query
|
||||
AduanaCru=row[13], # C33 - customs_office
|
||||
Lote='' # Not in aggregated query
|
||||
)
|
||||
|
||||
movements.append(movement)
|
||||
|
||||
logger.info(f"Successfully processed {len(movements)} export repair movements")
|
||||
return movements
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching export repair movements: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
def get_movements_detailed(
|
||||
self,
|
||||
db: Session,
|
||||
filters: ExportRepairFilter
|
||||
) -> List[MovementItemDetailed]:
|
||||
"""
|
||||
Get export repair movements (detailed mode - line by line).
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
filters: Filter criteria
|
||||
|
||||
Returns:
|
||||
List of detailed movement items (one per partida)
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Fetching detailed export repair movements with filters: {filters.model_dump()}")
|
||||
|
||||
# Get MetTrans configuration
|
||||
met_trans = ConfigHelper.get_met_trans_config()
|
||||
|
||||
# Build WHERE clause
|
||||
where_clause = self._build_where_clause(filters)
|
||||
|
||||
# Add discharge filter to WHERE clause
|
||||
discharge_clause = ""
|
||||
if filters.discharge_filter.value == "SiDes":
|
||||
discharge_clause = " AND RepPex.Descarga = 1"
|
||||
elif filters.discharge_filter.value == "NoDes":
|
||||
discharge_clause = " AND RepPex.Descarga = 0"
|
||||
|
||||
where_with_discharge = where_clause + discharge_clause
|
||||
|
||||
# Execute main query
|
||||
sql = text(ExportRepairQueries.build_main_query(filters.database_name, where_with_discharge))
|
||||
results = db.execute(sql).fetchall()
|
||||
logger.info(f"Found {len(results)} detailed export repair partidas")
|
||||
|
||||
movements = []
|
||||
|
||||
for row in results:
|
||||
# Skip cancelled if not included
|
||||
if not filters.include_cancelled and row[5] == 'NA': # C6 - Estatus
|
||||
continue
|
||||
|
||||
# Get provider information
|
||||
proveedor_info = DatabaseHelper.get_client_info(
|
||||
db, filters.database_name, row[13], is_supplier=True # C14 - Proveedor
|
||||
)
|
||||
|
||||
# Get buyer information
|
||||
vendido_info = DatabaseHelper.get_client_info(
|
||||
db, filters.database_name, row[14], is_supplier=False # C15 - VendidoA
|
||||
)
|
||||
|
||||
# Get customs agent information
|
||||
agente_info = DatabaseHelper.get_customs_agent_info(
|
||||
db, filters.database_name, row[15] # C16 - AAduanal
|
||||
)
|
||||
|
||||
# Get customs section name
|
||||
aduana_nombre = DatabaseHelper.get_aduana_seccion_nombre(
|
||||
db, filters.database_name, row[32] # C33 - Aduana_Cruce
|
||||
)
|
||||
|
||||
# Calculate values (only for main partidas 'P')
|
||||
if row[37] == 'P': # C38 - EsSubPartida
|
||||
valor_comercial, tipo_cambio = ExchangeRateCalculator.calculate_for_partida(
|
||||
db=db,
|
||||
db_name=filters.database_name,
|
||||
valor_me=row[36], # C37 - ValorExpoME
|
||||
valor_mn=row[35], # C36 - ValorExpoMN
|
||||
tipo_cambio_db=row[47], # C48 - TipoCambio
|
||||
fecha_pago=row[10], # C11 - Fecha_Pago
|
||||
fecha_inicio=row[8], # C9 - Fecha_Inicio
|
||||
tipo_pedimento=row[55], # C56 - TIPOPEDIMENTOTRANSPORTEE
|
||||
currency_type=filters.currency_type.value,
|
||||
exchange_rate_type=filters.exchange_rate_type.value,
|
||||
is_shelter=filters.is_shelter,
|
||||
use_transport_method=False,
|
||||
met_trans=met_trans
|
||||
)
|
||||
peso_neto = float(row[24]) if row[24] else 0.0 # C25
|
||||
peso_bruto = float(row[25]) if row[25] else 0.0 # C26
|
||||
else: # Subpartida
|
||||
valor_comercial = 0.0
|
||||
tipo_cambio = 0.0
|
||||
peso_neto = 0.0
|
||||
peso_bruto = 0.0
|
||||
|
||||
# Get series information
|
||||
series_info = DatabaseHelper.get_series_info_export(
|
||||
db, filters.database_name, row[34], row[41], filters.is_shelter # C35, C42
|
||||
)
|
||||
|
||||
# Get part export symbol
|
||||
simbolo_ex = None
|
||||
if row[46]: # C47 - NumParte
|
||||
simbolo_ex = DatabaseHelper.get_part_export_symbol(
|
||||
db, filters.database_name, row[46], filters.is_shelter
|
||||
)
|
||||
|
||||
# Get pedimento rectification
|
||||
pedimento_r1 = DatabaseHelper.get_rectification_pedimento(
|
||||
db,
|
||||
row[1], # C2 - PedimentoExpo
|
||||
row[38], # C39 - PedRectifica
|
||||
filters.is_shelter
|
||||
)
|
||||
|
||||
# Get driver badge
|
||||
num_gaf_uni = self._get_driver_badge(db, filters.database_name, row[0])
|
||||
|
||||
# Build detailed movement item
|
||||
movement = MovementItemDetailed(
|
||||
Linea=row[41], # C42 - LineaExpo
|
||||
Factura=row[0], # C1 - FacturaExpo
|
||||
Pedimento=row[1], # C2 - PedimentoExpo
|
||||
FechaFactura=row[2], # C3 - FechaFactura
|
||||
Estatus=row[5], # C6 - Estatus
|
||||
ClavePed=row[6], # C7 - ClavePed
|
||||
TipoMovTemDef=row[33], # C34 - TipoFactura
|
||||
EsCambioRegimen='N',
|
||||
Regimen=row[7], # C8 - Regimen
|
||||
Fecha_Inicio=row[8], # C9 - Fecha_Inicio
|
||||
Fecha_Fin=row[9], # C10 - Fecha_Fin
|
||||
Fecha_Pago=row[10], # C11 - Fecha_Pago
|
||||
Remesa=row[11], # C12 - Remesa
|
||||
Proveedor=proveedor_info.get('name'),
|
||||
RFCProveedor=proveedor_info.get('rfc'),
|
||||
ProveedorTaxID=proveedor_info.get('tax_id'),
|
||||
VendidoA=vendido_info.get('name'),
|
||||
VendidoARFC=vendido_info.get('rfc'),
|
||||
VendidoATaxID=vendido_info.get('tax_id'),
|
||||
AgenteAduanal=agente_info.get('name'),
|
||||
Patente=agente_info.get('license'),
|
||||
NumParte=row[17], # C18 - Clase (NumParte)
|
||||
DescripcionE=StringHelper.clean_text(row[18]), # C19
|
||||
DescripcionI=StringHelper.clean_text(row[19]), # C20
|
||||
CantidadIE=float(row[20]) if row[20] else 0.0, # C21
|
||||
UniMed=row[21], # C22
|
||||
ValorComercialMN=valor_comercial,
|
||||
TipoCambio=tipo_cambio,
|
||||
PesoNeto=peso_neto,
|
||||
PesoBruto=peso_bruto,
|
||||
OrdenCompraVenta=row[26], # C27 - OrdenCompra
|
||||
FraccionArancelaria=row[27], # C28 - FraccionExpo
|
||||
Preferencia=row[28], # C29 - TipoFraccion
|
||||
Sector=row[30], # C31 - Sector
|
||||
PaisOrigen=row[31], # C32 - PaisOrigen
|
||||
Aduana=aduana_nombre,
|
||||
Advalorem=row[37], # C38 - EsSubPartida
|
||||
TipoExpo='EXPO REP',
|
||||
PedimentoR1=pedimento_r1,
|
||||
EDocument=row[39], # C40 - EDocument
|
||||
NumOperacionVU=row[40], # C41 - NumOperacionVU
|
||||
Series=series_info,
|
||||
Marca=StringHelper.clean_text(row[42]), # C43
|
||||
Modelo=StringHelper.clean_text(row[43]), # C44
|
||||
FraccionAmericana=row[44], # C45 - FraccionAme
|
||||
ECCN=row[45], # C46 - ECCN
|
||||
SimboloEx=simbolo_ex,
|
||||
FechaEmision=row[48], # C49 - FechaFactura
|
||||
BaseDeDatos=filters.database_name,
|
||||
NumGafUni=num_gaf_uni,
|
||||
UsuarioCap=row[49], # C50 - UsuarioCap
|
||||
UsuarioAcr=row[50], # C51 - UsuarioAct
|
||||
Transportista=row[51], # C52 - Transportista
|
||||
NumCaja=row[52], # C53 - Transporte + NumTrasporte
|
||||
Pedimento18=row[53], # C54 - Pedimento18
|
||||
AduanaCru=row[32], # C33 - Aduana_Cruce
|
||||
Lote=row[54] # C55 - Lote
|
||||
)
|
||||
|
||||
movements.append(movement)
|
||||
|
||||
logger.info(f"Successfully processed {len(movements)} detailed export repair movements")
|
||||
return movements
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching detailed export repair movements: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
def _build_where_clause(self, filters: ExportRepairFilter) -> str:
|
||||
"""Build WHERE clause for export repair query."""
|
||||
where_conditions = []
|
||||
|
||||
# STRICT SEPARATION: Only exports for repair
|
||||
where_conditions.append("ih.operation_type = 'exp'")
|
||||
# GOLDEN RULE: If movement_type is ALL, only filter by operation_type
|
||||
if filters.movement_type.value == "ALL":
|
||||
# ALL mode: bring all exports without filtering by specific invoice_type
|
||||
pass
|
||||
else:
|
||||
where_conditions.append("ih.invoice_type IN ('EXREP', 'MATEXREP')")
|
||||
|
||||
# Date range filter
|
||||
if filters.range_type.value == "FF":
|
||||
where_conditions.append(f"ih.invoice_date >= TO_DATE('{filters.start_date}', 'YYYYMMDD') AND ih.invoice_date <= TO_DATE('{filters.end_date}', 'YYYYMMDD')")
|
||||
else:
|
||||
where_conditions.append(f"log.payment_date >= TO_DATE('{filters.start_date}', 'YYYYMMDD') AND log.payment_date <= TO_DATE('{filters.end_date}', 'YYYYMMDD')")
|
||||
|
||||
# Note: Status filter applied at Python level after CASE WHEN in SELECT
|
||||
# because is_updated doesn't directly represent AC/NA status
|
||||
|
||||
# Provider filter
|
||||
if filters.provider:
|
||||
where_conditions.append(f"cmp.provider_id = {filters.provider}")
|
||||
|
||||
# Buyer filter
|
||||
if filters.buyer:
|
||||
where_conditions.append(f"cmp.sold_to_id = {filters.buyer}")
|
||||
|
||||
# Pedimento code filter
|
||||
if filters.pedimento_code:
|
||||
where_conditions.append(f"ped.pedimento_code = '{filters.pedimento_code}'")
|
||||
|
||||
# Movement type filter
|
||||
if filters.movement_type.value == "AFIJO":
|
||||
where_conditions.append("ih.document_type = 'AFIJO'")
|
||||
elif filters.movement_type.value == "NODES":
|
||||
where_conditions.append("ih.document_type = 'NODES'")
|
||||
|
||||
return " AND ".join(where_conditions)
|
||||
|
||||
def _calculate_totals(self, db: Session, db_name: str, consecutivo: int, discharge_filter: str) -> tuple:
|
||||
"""Calculate totals for main partidas with discharge filter."""
|
||||
discharge_clause = ""
|
||||
if discharge_filter == "SiDes":
|
||||
discharge_clause = " AND il.is_discharged = true"
|
||||
elif discharge_filter == "NoDes":
|
||||
discharge_clause = " AND il.is_discharged = false"
|
||||
|
||||
sql = text(f"""
|
||||
SELECT
|
||||
COALESCE(SUM(lf.value_usd), 0),
|
||||
COALESCE(SUM(lf.value_mxn), 0)
|
||||
FROM a76.item_line_financials lf
|
||||
INNER JOIN a76.item_lines il ON il.id = lf.item_line_id
|
||||
INNER JOIN a76.items itm ON itm.id = il.item_id
|
||||
WHERE itm.invoice_id = :consecutivo
|
||||
AND il.is_subpart = false
|
||||
{discharge_clause}
|
||||
""")
|
||||
|
||||
result = db.execute(sql, {"consecutivo": consecutivo}).fetchone()
|
||||
total_me = float(result[0]) if result and result[0] is not None else 0.0
|
||||
total_mn = float(result[1]) if result and result[1] is not None else 0.0
|
||||
|
||||
return total_me, total_mn
|
||||
|
||||
def _get_driver_badge(self, db: Session, db_name: str, factura: str) -> str:
|
||||
"""Get driver badge number for invoice."""
|
||||
# TODO: GConductor table not migrated to PostgreSQL yet
|
||||
return None
|
||||
@@ -0,0 +1,877 @@
|
||||
"""
|
||||
SQL Query builders for invoice movement services.
|
||||
Centralizes all SQL query construction logic.
|
||||
"""
|
||||
|
||||
|
||||
class TemporaryImportQueries:
|
||||
"""SQL queries for temporary imports using PostgreSQL tables."""
|
||||
|
||||
@staticmethod
|
||||
def build_aggregated_query(db_name: str, where_str: str) -> str:
|
||||
"""Build optimized query for NORMAL mode (grouped by invoice with totals)."""
|
||||
# Note: db_name parameter kept for compatibility but not used in PostgreSQL
|
||||
return f"""
|
||||
SELECT
|
||||
ih.invoice_number AS C1,
|
||||
COALESCE(ped.pedimento_number, '') AS C2,
|
||||
TO_CHAR(ih.invoice_date, 'YYYYMMDD') AS C3,
|
||||
CASE WHEN ih.is_updated THEN 'AC' ELSE 'NA' END AS C4,
|
||||
COALESCE(ped.pedimento_code, '') AS C5,
|
||||
COALESCE(ped.regime, '') AS C10,
|
||||
COALESCE(TO_CHAR(log.entry_exit_date, 'YYYYMMDD'), '') AS C11,
|
||||
COALESCE(TO_CHAR(log.delivery_date, 'YYYYMMDD'), '') AS C12,
|
||||
COALESCE(TO_CHAR(log.payment_date, 'YYYYMMDD'), '') AS C13,
|
||||
COALESCE(cmp.remesa, 0) AS C14,
|
||||
COALESCE(fin.exchange_rate, 0) AS C15,
|
||||
COALESCE(cmp.provider_id::text, '') AS C16,
|
||||
COALESCE(cmp.sold_to_id::text, '') AS C17,
|
||||
COALESCE(cmp.customs_broker_id::text, '') AS C18,
|
||||
COALESCE(cmp.aduana, '') AS C38,
|
||||
ih.id AS C39,
|
||||
COALESCE(ped_r1.pedimento_number, '') AS C41,
|
||||
COALESCE(cmp.edocument, '') AS C42,
|
||||
COALESCE(cmp.vucem_operation_num, '') AS C43,
|
||||
COALESCE(fin.exchange_rate, 0) AS C50,
|
||||
COALESCE(TO_CHAR(ih.emission_date, 'YYYYMMDD'), '') AS C51,
|
||||
COALESCE(ih.capture_user, '') AS C52,
|
||||
COALESCE(ih.who_updated, '') AS C53,
|
||||
COALESCE(log.carrier_id, '') AS C54,
|
||||
COALESCE(log.transport_num || ' ' || log.license_plate, '') AS C55,
|
||||
'' AS C56,
|
||||
'' AS C57,
|
||||
'' AS C58,
|
||||
COALESCE(SUM(CASE WHEN COALESCE(fil.is_subitem, false) THEN 0 ELSE lf.value_usd END), 0) AS total_me,
|
||||
COALESCE(SUM(CASE WHEN COALESCE(fil.is_subitem, false) THEN 0 ELSE lf.value_mxn END), 0) AS total_mn
|
||||
FROM a76.invoice_header ih
|
||||
LEFT JOIN a76.invoice_compliance_mx cmp ON cmp.invoice_id = ih.id
|
||||
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.pedimentos ped_r1 ON ped_r1.id = cmp.pedimento_r1
|
||||
LEFT JOIN a76.items i ON i.invoice_id = ih.id
|
||||
LEFT JOIN a76.item_lines il ON il.item_id = i.id
|
||||
LEFT JOIN a24.fa_item_lines fil ON fil.id = il.id
|
||||
LEFT JOIN a76.item_line_financials lf ON lf.item_line_id = il.id
|
||||
WHERE ih.operation_type = 'imp'
|
||||
AND ih.invoice_type = 'TEM'
|
||||
AND {where_str}
|
||||
GROUP BY ih.id, ih.invoice_number, ped.pedimento_number, ped.pedimento_code, ped.regime,
|
||||
log.entry_exit_date, log.delivery_date, log.payment_date, cmp.remesa, fin.exchange_rate,
|
||||
cmp.provider_id, cmp.sold_to_id, cmp.customs_broker_id, cmp.aduana, ped_r1.pedimento_number,
|
||||
cmp.edocument, cmp.vucem_operation_num, ih.emission_date, ih.capture_user, ih.who_updated,
|
||||
log.carrier_id, log.transport_num, log.license_plate
|
||||
ORDER BY ih.invoice_number
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_main_query(db_name: str, where_str: str) -> str:
|
||||
"""Build main SQL query for DETAILED mode (all partidas) from PostgreSQL."""
|
||||
# Note: db_name parameter is kept for compatibility but not used in PostgreSQL
|
||||
return f"""
|
||||
SELECT
|
||||
ih.invoice_number AS C1,
|
||||
COALESCE(ped.pedimento_number, '') AS C2,
|
||||
TO_CHAR(ih.invoice_date, 'YYYYMMDD') AS C3,
|
||||
CASE WHEN ih.is_updated THEN 'AC' ELSE 'NA' END AS C4,
|
||||
COALESCE(ped.pedimento_code, '') AS C5,
|
||||
COALESCE(fin.value_me, 0) AS C6,
|
||||
COALESCE(fin.value_mn, 0) AS C7,
|
||||
COALESCE(cmp.provider_id::text, '') AS C8,
|
||||
COALESCE(cmp.sold_to_id::text, '') AS C9,
|
||||
COALESCE(ped.regime, '') AS C10,
|
||||
COALESCE(TO_CHAR(log.entry_exit_date, 'YYYYMMDD'), '') AS C11,
|
||||
COALESCE(TO_CHAR(log.delivery_date, 'YYYYMMDD'), '') AS C12,
|
||||
COALESCE(TO_CHAR(log.payment_date, 'YYYYMMDD'), '') AS C13,
|
||||
COALESCE(cmp.remesa, 0) AS C14,
|
||||
COALESCE(fin.exchange_rate, 0) AS C15,
|
||||
COALESCE(cmp.provider_id::text, '') AS C16,
|
||||
COALESCE(cmp.sold_to_id::text, '') AS C17,
|
||||
COALESCE(cmp.customs_broker_id::text, '') AS C18,
|
||||
'' AS C19,
|
||||
COALESCE(il.class_id::text, '') AS C20,
|
||||
REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(ld.description_spanish, ''), CHR(44), ' '), CHR(9), ' '), CHR(10), ' '), CHR(13), ' ') AS C21,
|
||||
REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(ld.description_english, ''), CHR(44), ' '), CHR(9), ' '), CHR(10), ' '), CHR(13), ' ') AS C22,
|
||||
COALESCE(lq.quantity, 0) AS C23,
|
||||
COALESCE(il.unit_of_measure::text, '') AS C24,
|
||||
COALESCE(lf.value_mxn, 0) AS C25,
|
||||
COALESCE(lf.customs_value_mxn, 0) AS C26,
|
||||
COALESCE(lf.value_usd, 0) AS C27,
|
||||
COALESCE(lf.customs_value_usd, 0) AS C28,
|
||||
COALESCE(lq.net_weight, 0) AS C29,
|
||||
COALESCE(lq.gross_weight, 0) AS C30,
|
||||
COALESCE(ih.purchase_order, '') AS C31,
|
||||
COALESCE(lc.fraction, '') AS C32,
|
||||
COALESCE(lc.fraction_type, '') AS C33,
|
||||
COALESCE(lc.advalorem_numeric, 0) AS C34,
|
||||
COALESCE(lc.sector, '') AS C35,
|
||||
COALESCE(lf.igi_amount_usd, 0) AS C36,
|
||||
COALESCE(lc.origin_country, '') AS C37,
|
||||
COALESCE(cmp.aduana, '') AS C38,
|
||||
ih.id AS C39,
|
||||
FALSE AS C40,
|
||||
'' AS C41,
|
||||
COALESCE(cmp.edocument, '') AS C42,
|
||||
COALESCE(cmp.vucem_operation_num, '') AS C43,
|
||||
COALESCE(il.line_number, 0) AS C44,
|
||||
'' AS C45,
|
||||
'' AS C46,
|
||||
COALESCE(cls.us_fraction, '') AS C47,
|
||||
COALESCE(prt.eccn, '') AS C48,
|
||||
COALESCE(il.part_number::text, '') AS C49,
|
||||
COALESCE(fin.exchange_rate, 0) AS C50,
|
||||
COALESCE(TO_CHAR(ih.emission_date, 'YYYYMMDD'), '') AS C51,
|
||||
COALESCE(ih.capture_user, '') AS C52,
|
||||
COALESCE(ih.who_updated, '') AS C53,
|
||||
COALESCE(log.carrier_id, '') AS C54,
|
||||
COALESCE(log.transport_num || ' ' || log.license_plate, '') AS C55,
|
||||
'' AS C56,
|
||||
'' AS C57,
|
||||
'' AS C58
|
||||
FROM a76.invoice_header ih
|
||||
LEFT JOIN a76.invoice_compliance_mx cmp ON cmp.invoice_id = ih.id
|
||||
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.item_lines il ON il.item_id = (
|
||||
SELECT id FROM a76.items WHERE invoice_id = ih.id LIMIT 1
|
||||
)
|
||||
LEFT JOIN a76.item_line_descriptions ld ON ld.item_line_id = il.id
|
||||
LEFT JOIN a76.item_line_quantities lq ON lq.item_line_id = il.id
|
||||
LEFT JOIN a76.item_line_financials lf ON lf.item_line_id = il.id
|
||||
LEFT JOIN a76.item_line_customs lc ON lc.item_line_id = il.id
|
||||
LEFT JOIN a76.classes cls ON cls.id = il.class_id
|
||||
LEFT JOIN a76.parts prt ON prt.id = il.part_number
|
||||
LEFT JOIN a76.units_of_measure um ON um.id = il.unit_of_measure
|
||||
WHERE ih.operation_type = 'imp'
|
||||
AND ih.invoice_type = 'TEM'
|
||||
AND {where_str}
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_totals_query(db_name: str) -> str:
|
||||
"""Build query to get totals for an invoice."""
|
||||
return f"""
|
||||
SELECT
|
||||
COALESCE(SUM(EqiPim.ValorImpoME), 0),
|
||||
COALESCE(SUM(EqiPim.ValorImpoMN), 0)
|
||||
FROM [{db_name}].dbo.QEqiMaq EqiPim
|
||||
WHERE EqiPim.Consecutivo = :consecutivo
|
||||
AND EqiPim.EsSubpartida = 'P'
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_series_query(db_name: str) -> str:
|
||||
"""Build query to get series information."""
|
||||
return f"""
|
||||
SELECT SerieImpo, ModeloImpo, ParteImpo
|
||||
FROM [{db_name}].dbo.QSeriesImpo
|
||||
WHERE Consecutivo = :consecutivo
|
||||
AND LineaImpo = :linea
|
||||
ORDER BY RenImpo
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_driver_badge_query(db_name: str) -> str:
|
||||
"""Build query to get driver badge number."""
|
||||
return f"""
|
||||
SELECT TOP 1 NUMGAFETEUNICO
|
||||
FROM [{db_name}].dbo.GConductor
|
||||
LEFT JOIN [{db_name}].dbo.QFacImp
|
||||
ON QFacImp.CONDUCTOR = GConductor.CONDUCTOR
|
||||
WHERE FacturaImpo = :factura
|
||||
"""
|
||||
|
||||
|
||||
class DefinitiveImportQueries:
|
||||
"""SQL queries for definitive imports (PostgreSQL schema)."""
|
||||
|
||||
@staticmethod
|
||||
def build_aggregated_query(db_name: str, where_clause: str) -> str:
|
||||
"""Build optimized query for NORMAL mode (grouped by invoice with totals)."""
|
||||
return f"""
|
||||
SELECT
|
||||
ih.invoice_number AS C1,
|
||||
COALESCE(ped.pedimento_number, '') AS C2,
|
||||
TO_CHAR(ih.invoice_date, 'YYYYMMDD') AS C3,
|
||||
CASE WHEN ih.is_updated THEN 'AC' ELSE 'NA' END AS C4,
|
||||
COALESCE(ped.pedimento_code, '') AS C5,
|
||||
COALESCE(ped.regime, '') AS C10,
|
||||
COALESCE(TO_CHAR(log.entry_exit_date, 'YYYYMMDD'), '') AS C11,
|
||||
COALESCE(TO_CHAR(log.delivery_date, 'YYYYMMDD'), '') AS C12,
|
||||
COALESCE(TO_CHAR(log.payment_date, 'YYYYMMDD'), '') AS C13,
|
||||
COALESCE(log.payment_receipt_num, '') AS C14,
|
||||
COALESCE(fin.exchange_rate, 0) AS C15,
|
||||
COALESCE(cmp.provider_id::text, '') AS C16,
|
||||
COALESCE(cmp.sold_to_id::text, '') AS C17,
|
||||
COALESCE(cmp.customs_broker_id::text, '') AS C18,
|
||||
COALESCE(ih.purchase_order, '') AS C31,
|
||||
COALESCE(cmp.aduana, '') AS C39,
|
||||
ih.id AS C35,
|
||||
COALESCE(ped_r1.pedimento_number, '') AS C42,
|
||||
COALESCE(cmp.edocument, '') AS C43,
|
||||
COALESCE(cmp.vucem_operation_num, '') AS C44,
|
||||
COALESCE(fin.exchange_rate, 0) AS C51,
|
||||
COALESCE(TO_CHAR(ih.emission_date, 'YYYYMMDD'), '') AS C52,
|
||||
COALESCE(ih.capture_user, '') AS C53,
|
||||
COALESCE(ih.who_updated, '') AS C54,
|
||||
COALESCE(log.transport_id || ' ' || log.transport_num, '') AS C56,
|
||||
'' AS C57,
|
||||
'' AS C58,
|
||||
'' AS C59,
|
||||
COALESCE(SUM(CASE WHEN COALESCE(fil.is_subitem, false) THEN 0 ELSE lf.value_usd END), 0) AS total_me,
|
||||
COALESCE(SUM(CASE WHEN COALESCE(fil.is_subitem, false) THEN 0 ELSE lf.value_mxn END), 0) AS total_mn
|
||||
FROM a76.invoice_header ih
|
||||
LEFT JOIN a76.invoice_compliance_mx cmp ON cmp.invoice_id = ih.id
|
||||
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.pedimentos ped_r1 ON ped_r1.id = cmp.pedimento_r1
|
||||
LEFT JOIN a76.items i ON i.invoice_id = ih.id
|
||||
LEFT JOIN a76.item_lines il ON il.item_id = i.id
|
||||
LEFT JOIN a24.fa_item_lines fil ON fil.id = il.id
|
||||
LEFT JOIN a76.item_line_financials lf ON lf.item_line_id = il.id
|
||||
WHERE ih.operation_type = 'imp'
|
||||
AND ih.invoice_type IN ('DEF', 'EXDEF', 'MATDE')
|
||||
AND {where_clause}
|
||||
GROUP BY ih.id, ih.invoice_number, ped.pedimento_number, ped.pedimento_code, ped.regime,
|
||||
log.entry_exit_date, log.delivery_date, log.payment_date, log.payment_receipt_num,
|
||||
fin.exchange_rate, cmp.provider_id, cmp.sold_to_id, cmp.customs_broker_id,
|
||||
ih.purchase_order, cmp.aduana, ped_r1.pedimento_number, cmp.edocument,
|
||||
cmp.vucem_operation_num, ih.emission_date, ih.capture_user, ih.who_updated,
|
||||
log.transport_id, log.transport_num
|
||||
ORDER BY ih.invoice_number
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_main_query(db_name: str, where_clause: str) -> str:
|
||||
return f"""
|
||||
SELECT
|
||||
ih.invoice_number AS C1, -- [0]
|
||||
ped.pedimento_number AS C2, -- [1]
|
||||
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]
|
||||
ped.regime AS C10, -- [9]
|
||||
log.entry_exit_date AS C11, -- [10]
|
||||
log.delivery_date AS C12, -- [11]
|
||||
log.payment_date AS C13, -- [12]
|
||||
log.payment_receipt_num AS C14, -- [13]
|
||||
'' AS C15, -- [14]
|
||||
cmp.provider_id AS C16, -- [15]
|
||||
cmp.sold_to_id AS C17, -- [16]
|
||||
cmp.customs_broker_id AS C18, -- [17]
|
||||
'' AS C19, -- [18]
|
||||
prt.part_number AS C20, -- [19]
|
||||
ld.description_spanish AS C21, -- [20]
|
||||
ld.description_english AS C22, -- [21]
|
||||
lq.quantity AS C23, -- [22]
|
||||
um.code AS C24, -- [23]
|
||||
lf.value_mxn AS C25, -- [24]
|
||||
'' AS C26, -- [25]
|
||||
lf.value_usd AS C27, -- [26]
|
||||
'' AS C28, -- [27]
|
||||
lq.net_weight AS C29, -- [28]
|
||||
lq.gross_weight AS C30, -- [29]
|
||||
ih.purchase_order AS C31, -- [30]
|
||||
lc.fraction AS C32, -- [31]
|
||||
'' AS C33, '' AS C34, -- [32-33]
|
||||
ih.id AS C35, -- [34]
|
||||
'' AS C36, '' AS C37, -- [35-36]
|
||||
lc.origin_country AS C38, -- [37]
|
||||
cmp.aduana AS C39, -- [38]
|
||||
il.material_type AS C40, -- [39]
|
||||
il.id AS C41, -- [40]
|
||||
'' AS C42, -- [41] rectification_id
|
||||
cmp.edocument AS C43, -- [42]
|
||||
cmp.vucem_operation_num AS C44, -- [43]
|
||||
il.line_number AS C45, -- [44]
|
||||
ld.brand AS C46, -- [45]
|
||||
ld.model AS C47, -- [46]
|
||||
prt.us_fraction AS C48, -- [47]
|
||||
prt.eccn AS C49, -- [48]
|
||||
prt.id AS C50, -- [49]
|
||||
fin.exchange_rate AS C51, -- [50]
|
||||
ih.emission_date AS C52, -- [51]
|
||||
ih.capture_user AS C53, -- [52]
|
||||
ih.who_updated AS C54, -- [53]
|
||||
'' AS C55, -- [54]
|
||||
COALESCE(log.transport_id || ' ' || log.transport_num, '') AS C56, -- [55]
|
||||
'' AS C57, -- [56] Pedimento18 (row[56])
|
||||
COALESCE(ld.lot, '') AS C58, -- [57] Lote (row[57])
|
||||
'' AS C59, -- [58] TipoPed (row[58])
|
||||
'' AS C60 -- [59] Relleno final
|
||||
FROM a76.invoice_header ih
|
||||
LEFT JOIN a76.invoice_compliance_mx cmp ON cmp.invoice_id = ih.id
|
||||
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.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
|
||||
LEFT JOIN a76.item_line_quantities lq ON lq.item_line_id = il.id
|
||||
LEFT JOIN a76.item_line_financials lf ON lf.item_line_id = il.id
|
||||
LEFT JOIN a76.item_line_customs lc ON lc.item_line_id = il.id
|
||||
LEFT JOIN a76.classes cls ON cls.id = il.class_id
|
||||
LEFT JOIN a76.parts prt ON prt.id = il.part_number
|
||||
LEFT JOIN a76.units_of_measure um ON um.id = il.unit_of_measure
|
||||
WHERE {where_clause}
|
||||
ORDER BY ih.invoice_number, il.line_number
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_totals_query(db_name: str) -> str:
|
||||
"""Build query to get totals for a definitive import invoice."""
|
||||
return f"""
|
||||
SELECT
|
||||
COALESCE(SUM(CASE WHEN COALESCE(fil.is_subitem, false) THEN 0 ELSE lf.value_usd END), 0),
|
||||
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 fil.id = il.id
|
||||
INNER JOIN a76.items itm ON itm.id = il.item_id
|
||||
WHERE itm.invoice_id = :consecutivo
|
||||
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_series_query(db_name: str) -> str:
|
||||
"""Build query to get series information for definitive imports."""
|
||||
# TODO: QSeriesDef table not migrated to PostgreSQL yet
|
||||
return """
|
||||
SELECT '' as serie, '' as modelo, '' as parte
|
||||
WHERE 1=0
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_driver_badge_query(db_name: str) -> str:
|
||||
"""Build query to get driver badge number for definitive imports."""
|
||||
# TODO: GConductor table not migrated to PostgreSQL yet
|
||||
return """
|
||||
SELECT '' as badge
|
||||
WHERE 1=0
|
||||
"""
|
||||
|
||||
|
||||
class RepairImportQueries:
|
||||
"""SQL queries for repair imports (PostgreSQL schema)."""
|
||||
|
||||
@staticmethod
|
||||
def build_aggregated_query(db_name: str, where_str: str, discharge_clause: str = "") -> str:
|
||||
"""Build optimized query for NORMAL mode (grouped by invoice with totals)."""
|
||||
discharge_filter = "" # Temporarily disabled until schema migration
|
||||
return f"""
|
||||
SELECT
|
||||
ih.invoice_number AS C2,
|
||||
COALESCE(ped.pedimento_number, '') AS C3,
|
||||
TO_CHAR(ih.invoice_date, 'YYYYMMDD') AS C4,
|
||||
CASE WHEN ih.is_updated THEN 'AC' ELSE 'NA' END AS C5,
|
||||
COALESCE(ped.pedimento_code, '') AS C6,
|
||||
COALESCE(ped.regime, '') AS C7,
|
||||
COALESCE(TO_CHAR(log.payment_date, 'YYYYMMDD'), '') AS C9,
|
||||
COALESCE(cmp.remesa::text, '') AS C10,
|
||||
COALESCE(fin.exchange_rate, 0) AS C11,
|
||||
COALESCE(cmp.provider_id::text, '') AS C12,
|
||||
COALESCE(cmp.sold_to_id::text, '') AS C13,
|
||||
COALESCE(cmp.customs_broker_id::text, '') AS C14,
|
||||
COALESCE(ih.purchase_order, '') AS C24,
|
||||
COALESCE(ped.customs_office, '') AS C29,
|
||||
ih.id AS C30,
|
||||
COALESCE(cmp.edocument, '') AS C33,
|
||||
COALESCE(cmp.vucem_operation_num, '') AS C34,
|
||||
COALESCE(fin.exchange_rate, 0) AS C40,
|
||||
COALESCE(TO_CHAR(ih.emission_date, 'YYYYMMDD'), '') AS C41,
|
||||
COALESCE(ih.capture_user, '') AS C42,
|
||||
COALESCE(ih.who_updated, '') AS C43,
|
||||
COALESCE(log.carrier_id, '') AS C44,
|
||||
COALESCE(log.transport_num, '') AS C45,
|
||||
COALESCE(ped.pedimento_code, '') AS C47,
|
||||
COALESCE(SUM(CASE WHEN COALESCE(fil.is_subitem, false) THEN 0 ELSE lf.value_usd END), 0) AS total_me,
|
||||
COALESCE(SUM(CASE WHEN COALESCE(fil.is_subitem, false) THEN 0 ELSE lf.value_mxn END), 0) AS total_mn
|
||||
FROM a76.invoice_header ih
|
||||
LEFT JOIN a76.invoice_compliance_mx cmp ON cmp.invoice_id = ih.id
|
||||
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.items i ON i.invoice_id = ih.id
|
||||
LEFT JOIN a76.item_lines il ON il.item_id = i.id
|
||||
LEFT JOIN a24.fa_item_lines fil ON fil.id = il.id
|
||||
LEFT JOIN a76.item_line_financials lf ON lf.item_line_id = il.id
|
||||
WHERE ih.operation_type = 'imp'
|
||||
AND ih.invoice_type = 'REP'
|
||||
AND COALESCE(cmp.is_regime_change, false) = false
|
||||
{"AND " + where_str if where_str else ""}
|
||||
{discharge_filter}
|
||||
GROUP BY ih.id, ih.invoice_number, ped.pedimento_number, ped.pedimento_code, ped.regime,
|
||||
log.payment_date, cmp.remesa, fin.exchange_rate, cmp.provider_id, cmp.sold_to_id,
|
||||
cmp.customs_broker_id, ih.purchase_order, ped.customs_office, cmp.edocument,
|
||||
cmp.vucem_operation_num, ih.emission_date, ih.capture_user, ih.who_updated,
|
||||
log.carrier_id, log.transport_num
|
||||
ORDER BY ih.invoice_number
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_main_query(db_name: str, where_str: str, discharge_clause: str = "") -> str:
|
||||
"""Build main SQL query for repair import data."""
|
||||
# Note: is_discharged field not yet migrated to PostgreSQL schema
|
||||
# discharge_filter = "AND il.is_discharged = true" if "descargado" in discharge_clause.lower() else ""
|
||||
discharge_filter = "" # Temporarily disabled until schema migration
|
||||
return f"""
|
||||
SELECT
|
||||
il.line_number,
|
||||
ih.invoice_number,
|
||||
COALESCE(ped.pedimento_number, ''),
|
||||
TO_CHAR(ih.invoice_date, 'YYYYMMDD'),
|
||||
CASE WHEN ih.is_updated THEN 'AC' ELSE 'NA' END,
|
||||
COALESCE(ped.pedimento_code, ''),
|
||||
COALESCE(ped.regime, ''),
|
||||
'',
|
||||
COALESCE(TO_CHAR(log.payment_date, 'YYYYMMDD'), ''),
|
||||
COALESCE(cmp.remesa::text, ''),
|
||||
COALESCE(fin.exchange_rate, 0),
|
||||
COALESCE(cmp.provider_id::text, ''),
|
||||
COALESCE(cmp.sold_to_id::text, ''),
|
||||
COALESCE(cmp.customs_broker_id::text, ''),
|
||||
COALESCE(il.part_number::text, ''),
|
||||
REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(ld.description_spanish, ''), CHR(44), ' '), CHR(9), ' '), CHR(10), ' '), CHR(13), ' '),
|
||||
REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(ld.description_english, ''), CHR(44), ' '), CHR(9), ' '), CHR(10), ' '), CHR(13), ' '),
|
||||
COALESCE(lq.quantity, 0),
|
||||
COALESCE(il.unit_of_measure, 0),
|
||||
COALESCE(lf.value_mxn, 0),
|
||||
COALESCE(lf.value_usd, 0),
|
||||
COALESCE(lq.net_weight, 0),
|
||||
COALESCE(lq.gross_weight, 0),
|
||||
COALESCE(ih.purchase_order, ''),
|
||||
COALESCE(lc.fraction, ''),
|
||||
'',
|
||||
COALESCE(lc.sector, ''),
|
||||
COALESCE(lc.origin_country, ''),
|
||||
COALESCE(ped.customs_office, ''),
|
||||
ih.id,
|
||||
'P',
|
||||
'',
|
||||
COALESCE(cmp.edocument, ''),
|
||||
COALESCE(cmp.vucem_operation_num, ''),
|
||||
REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(ld.brand, ''), CHR(44), ' '), CHR(9), ' '), CHR(10), ' '), CHR(13), ' '),
|
||||
REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(ld.model, ''), CHR(44), ' '), CHR(9), ' '), CHR(10), ' '), CHR(13), ' '),
|
||||
COALESCE(lc.american_fraction, ''),
|
||||
COALESCE(prt.eccn, ''),
|
||||
COALESCE(fin.exchange_rate, 0),
|
||||
COALESCE(TO_CHAR(ih.emission_date, 'YYYYMMDD'), ''),
|
||||
COALESCE(ih.capture_user, ''),
|
||||
COALESCE(ih.who_updated, ''),
|
||||
COALESCE(log.carrier_id, ''),
|
||||
COALESCE(log.transport_num, ''),
|
||||
'',
|
||||
COALESCE(ped.pedimento_code, '')
|
||||
FROM a76.invoice_header ih
|
||||
LEFT JOIN a76.invoice_compliance_mx cmp ON cmp.invoice_id = ih.id
|
||||
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.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
|
||||
LEFT JOIN a76.item_line_quantities lq ON lq.item_line_id = il.id
|
||||
LEFT JOIN a76.item_line_financials lf ON lf.item_line_id = il.id
|
||||
LEFT JOIN a76.item_line_customs lc ON lc.item_line_id = il.id
|
||||
LEFT JOIN a76.parts prt ON prt.id = il.part_number
|
||||
LEFT JOIN a76.units_of_measure um ON um.id = il.unit_of_measure
|
||||
WHERE ih.operation_type = 'imp'
|
||||
AND ih.invoice_type = 'REP'
|
||||
AND COALESCE(cmp.is_regime_change, false) = false
|
||||
{"AND " + where_str if where_str else ""}
|
||||
{discharge_filter}
|
||||
ORDER BY ih.invoice_number, il.line_number
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_totals_query(db_name: str, discharge_clause: str = "") -> str:
|
||||
"""Build query to get totals for a repair import invoice."""
|
||||
# Note: is_discharged field not yet migrated to PostgreSQL schema
|
||||
# discharge_filter = "AND il.is_discharged = true" if "descargado" in discharge_clause.lower() else ""
|
||||
discharge_filter = "" # Temporarily disabled until schema migration
|
||||
return f"""
|
||||
SELECT
|
||||
COALESCE(SUM(CASE WHEN COALESCE(fil.is_subitem, false) THEN 0 ELSE lf.value_usd END), 0),
|
||||
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 fil.id = il.id
|
||||
INNER JOIN a76.items itm ON itm.id = il.item_id
|
||||
WHERE itm.invoice_id = :consecutivo
|
||||
|
||||
{discharge_filter}
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_series_query(db_name: str) -> str:
|
||||
"""Build query to get series information for repair imports."""
|
||||
# TODO: QSeriesImpoRep table not migrated to PostgreSQL yet
|
||||
return """
|
||||
SELECT '' as serie, '' as modelo, '' as parte
|
||||
WHERE 1=0
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_driver_badge_query(db_name: str) -> str:
|
||||
"""Build query to get driver badge number for repair imports."""
|
||||
# TODO: GConductor table not migrated to PostgreSQL yet
|
||||
return """
|
||||
SELECT '' as badge
|
||||
WHERE 1=0
|
||||
"""
|
||||
|
||||
|
||||
class ExportQueries:
|
||||
"""SQL queries for exports (PostgreSQL schema)."""
|
||||
|
||||
@staticmethod
|
||||
def build_aggregated_query(db_name: str, where_clause: str) -> str:
|
||||
"""
|
||||
Build optimized query for NORMAL mode (grouped by invoice with totals).
|
||||
|
||||
Args:
|
||||
db_name: Database name (not used in PostgreSQL version)
|
||||
where_clause: Additional WHERE conditions (without WHERE keyword)
|
||||
"""
|
||||
return f"""
|
||||
SELECT
|
||||
ih.invoice_number AS C1,
|
||||
COALESCE(ped.pedimento_number, '') AS C2,
|
||||
TO_CHAR(ih.invoice_date, 'YYYYMMDD') AS C3,
|
||||
CASE WHEN ih.is_updated THEN 'AC' ELSE 'NA' END AS C6,
|
||||
COALESCE(ped.pedimento_code, '') AS C7,
|
||||
COALESCE(ped.regime, '') AS C8,
|
||||
COALESCE(TO_CHAR(log.entry_exit_date, 'YYYYMMDD'), '') AS C9,
|
||||
COALESCE(TO_CHAR(log.delivery_date, 'YYYYMMDD'), '') AS C10,
|
||||
COALESCE(TO_CHAR(log.payment_date, 'YYYYMMDD'), '') AS C11,
|
||||
COALESCE(log.payment_receipt_num, '') AS C12,
|
||||
COALESCE(cmp.provider_id::text, '') AS C14,
|
||||
COALESCE(cmp.sold_to_id::text, '') AS C15,
|
||||
COALESCE(cmp.customs_broker_id::text, '') AS C16,
|
||||
COALESCE(ih.purchase_order, '') AS C27,
|
||||
COALESCE(cmp.aduana, '') AS C33,
|
||||
COALESCE(ih.invoice_type, '') AS C34,
|
||||
ih.id AS C35,
|
||||
COALESCE(cmp.edocument, '') AS C40,
|
||||
COALESCE(cmp.vucem_operation_num, '') AS C41,
|
||||
COALESCE(fin.exchange_rate, 0) AS C48,
|
||||
COALESCE(TO_CHAR(ih.emission_date, 'YYYYMMDD'), '') AS C49,
|
||||
COALESCE(ih.capture_user, '') AS C50,
|
||||
COALESCE(ih.who_updated, '') AS C51,
|
||||
COALESCE(log.transport_id || ' ' || log.transport_num, '') AS C53,
|
||||
COALESCE(SUM(CASE WHEN COALESCE(fil.is_subitem, false) THEN 0 ELSE lf.value_usd END), 0) AS total_me,
|
||||
COALESCE(SUM(CASE WHEN COALESCE(fil.is_subitem, false) THEN 0 ELSE lf.value_mxn END), 0) AS total_mn
|
||||
FROM a76.invoice_header ih
|
||||
LEFT JOIN a76.invoice_compliance_mx cmp ON cmp.invoice_id = ih.id
|
||||
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.items i ON i.invoice_id = ih.id
|
||||
LEFT JOIN a76.item_lines il ON il.item_id = i.id
|
||||
LEFT JOIN a24.fa_item_lines fil ON fil.id = il.id
|
||||
LEFT JOIN a76.item_line_financials lf ON lf.item_line_id = il.id
|
||||
WHERE {where_clause}
|
||||
GROUP BY ih.id, ih.invoice_number, ih.is_updated, ped.pedimento_number, ped.pedimento_code, ped.regime,
|
||||
log.entry_exit_date, log.delivery_date, log.payment_date, log.payment_receipt_num,
|
||||
cmp.provider_id, cmp.sold_to_id, cmp.customs_broker_id, ih.purchase_order, cmp.aduana,
|
||||
ih.invoice_type, cmp.edocument, cmp.vucem_operation_num, fin.exchange_rate,
|
||||
ih.emission_date, ih.capture_user, ih.who_updated, log.transport_id, log.transport_num,
|
||||
ih.invoice_date
|
||||
ORDER BY ih.invoice_number
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_main_query(db_name: str, where_clause: str) -> str:
|
||||
return f"""
|
||||
SELECT
|
||||
ih.invoice_number AS C1, -- [0]
|
||||
ped.pedimento_number AS C2, -- [1]
|
||||
ih.invoice_date AS C3, -- [2]
|
||||
'' AS C4, -- [3]
|
||||
'' AS C5, -- [4]
|
||||
ped.status AS C6, -- [5]
|
||||
ped.pedimento_code AS C7, -- [6]
|
||||
ped.regime AS C8, -- [7]
|
||||
log.entry_exit_date AS C9, -- [8]
|
||||
log.delivery_date AS C10, -- [9]
|
||||
log.payment_date AS C11, -- [10]
|
||||
log.payment_receipt_num AS C12, -- [11]
|
||||
'' AS C13, -- [12]
|
||||
cmp.provider_id AS C14, -- [13]
|
||||
cmp.sold_to_id AS C15, -- [14]
|
||||
cmp.customs_broker_id AS C16, -- [15]
|
||||
'' AS C17, -- [16]
|
||||
prt.part_number AS C18, -- [17]
|
||||
ld.description_spanish AS C19, -- [18]
|
||||
ld.description_english AS C20, -- [19]
|
||||
lq.quantity AS C21, -- [20]
|
||||
um.code AS C22, -- [21]
|
||||
'' AS C23, -- [22]
|
||||
'' AS C24, -- [23]
|
||||
lq.net_weight AS C25, -- [24]
|
||||
lq.gross_weight AS C26, -- [25]
|
||||
ih.purchase_order AS C27, -- [26]
|
||||
lc.fraction AS C28, -- [27]
|
||||
'' AS C29, -- [28]
|
||||
'' AS C30, -- [29]
|
||||
'' AS C31, -- [30]
|
||||
'' AS C32, -- [31]
|
||||
cmp.aduana AS C33, -- [32]
|
||||
ih.invoice_type AS C34, -- [33]
|
||||
ih.id AS C35, -- [34]
|
||||
lf.value_mxn AS C36, -- [35]
|
||||
lf.value_usd AS C37, -- [36]
|
||||
il.material_type AS C38, -- [37]
|
||||
'' AS C39, -- [38] rectification_id
|
||||
cmp.edocument AS C40, -- [39]
|
||||
cmp.vucem_operation_num AS C41, -- [40]
|
||||
il.line_number AS C42, -- [41]
|
||||
ld.brand AS C43, -- [42]
|
||||
ld.model AS C44, -- [43]
|
||||
prt.us_fraction AS C45, -- [44]
|
||||
prt.eccn AS C46, -- [45]
|
||||
prt.id AS C47, -- [46]
|
||||
fin.exchange_rate AS C48, -- [47]
|
||||
ih.emission_date AS C49, -- [48]
|
||||
ih.capture_user AS C50, -- [49]
|
||||
ih.who_updated AS C51, -- [50]
|
||||
'' AS C52, -- [51]
|
||||
COALESCE(log.transport_id || ' ' || log.transport_num, '') AS C53, -- [52] NumCaja
|
||||
'' AS C54, -- [53] Pedimento18
|
||||
COALESCE(ld.lot, '') AS C55, -- [54] Lote
|
||||
'' AS C56, -- [55] TipoPedimentoTransporte
|
||||
'' AS C57, -- [56]
|
||||
'' AS C58, -- [57]
|
||||
'' AS C59, -- [58]
|
||||
'' AS C60 -- [59] Relleno final
|
||||
FROM a76.invoice_header ih
|
||||
LEFT JOIN a76.invoice_compliance_mx cmp ON cmp.invoice_id = ih.id
|
||||
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.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
|
||||
LEFT JOIN a76.item_line_quantities lq ON lq.item_line_id = il.id
|
||||
LEFT JOIN a76.item_line_financials lf ON lf.item_line_id = il.id
|
||||
LEFT JOIN a76.item_line_customs lc ON lc.item_line_id = il.id
|
||||
LEFT JOIN a76.classes cls ON cls.id = il.class_id
|
||||
LEFT JOIN a76.parts prt ON prt.id = il.part_number
|
||||
LEFT JOIN a76.units_of_measure um ON um.id = il.unit_of_measure
|
||||
WHERE {where_clause}
|
||||
ORDER BY ih.invoice_number, il.line_number
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_totals_query(db_name: str, discharge_clause: str = "") -> str:
|
||||
"""Build query to get totals for an export invoice.
|
||||
|
||||
Only sums partidas where is_subitem is false (main partidas, not sub-items).
|
||||
"""
|
||||
discharge_filter = "AND il.is_discharged = true" if "descargado" in discharge_clause.lower() else ""
|
||||
return f"""
|
||||
SELECT
|
||||
COALESCE(SUM(CASE WHEN COALESCE(fil.is_subitem, false) THEN 0 ELSE lf.value_usd END), 0),
|
||||
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
|
||||
WHERE itm.invoice_id = :consecutivo
|
||||
{discharge_filter}
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_series_query(db_name: str) -> str:
|
||||
"""Build query to get series information for exports."""
|
||||
# TODO: QSeriesExpo table not migrated to PostgreSQL yet
|
||||
return """
|
||||
SELECT '' as serie, '' as modelo, '' as parte
|
||||
WHERE 1=0
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_driver_badge_query(db_name: str) -> str:
|
||||
"""Build query to get driver badge number for exports."""
|
||||
# TODO: GConductor table not migrated to PostgreSQL yet
|
||||
return """
|
||||
SELECT '' as badge
|
||||
WHERE 1=0
|
||||
"""
|
||||
|
||||
|
||||
class ExportRepairQueries:
|
||||
"""SQL queries for export repairs (PostgreSQL schema)."""
|
||||
|
||||
@staticmethod
|
||||
def build_aggregated_query(db_name: str, where_str: str, discharge_clause: str = "") -> str:
|
||||
"""Build optimized query for NORMAL mode (grouped by invoice with totals)."""
|
||||
# Note: discharge_clause temporarily disabled until is_discharged field migrated
|
||||
discharge_filter = "" # Will be: " AND il.is_discharged = true/false" when ready
|
||||
return f"""
|
||||
SELECT
|
||||
ih.invoice_number AS C1,
|
||||
COALESCE(ped.pedimento_number, '') AS C2,
|
||||
TO_CHAR(ih.invoice_date, 'YYYYMMDD') AS C3,
|
||||
CASE WHEN ih.is_updated THEN 'AC' ELSE 'NA' END AS C6,
|
||||
COALESCE(ped.pedimento_code, '') AS C7,
|
||||
COALESCE(ped.regime, '') AS C8,
|
||||
COALESCE(TO_CHAR(log.payment_date, 'YYYYMMDD'), '') AS C11,
|
||||
COALESCE(cmp.remesa::text, '') AS C12,
|
||||
COALESCE(fin.exchange_rate, 0) AS C13,
|
||||
COALESCE(cmp.provider_id::text, '') AS C14,
|
||||
COALESCE(cmp.sold_to_id::text, '') AS C15,
|
||||
COALESCE(cmp.customs_broker_id::text, '') AS C16,
|
||||
COALESCE(ih.purchase_order, '') AS C27,
|
||||
COALESCE(ped.customs_office, '') AS C33,
|
||||
COALESCE(ih.document_type, '') AS C34,
|
||||
ih.id AS C35,
|
||||
COALESCE(cmp.edocument, '') AS C40,
|
||||
COALESCE(cmp.vucem_operation_num, '') AS C41,
|
||||
COALESCE(fin.exchange_rate, 0) AS C48,
|
||||
COALESCE(TO_CHAR(ih.invoice_date, 'YYYYMMDD'), '') AS C49,
|
||||
COALESCE(ih.capture_user, '') AS C50,
|
||||
COALESCE(ih.who_updated, '') AS C51,
|
||||
COALESCE(log.carrier_id, '') AS C52,
|
||||
COALESCE(log.transport_id || ' ' || log.transport_num, '') AS C53,
|
||||
COALESCE(SUM(CASE WHEN COALESCE(fil.is_subitem, false) THEN 0 ELSE lf.value_usd END), 0) AS total_me,
|
||||
COALESCE(SUM(CASE WHEN COALESCE(fil.is_subitem, false) THEN 0 ELSE lf.value_mxn END), 0) AS total_mn
|
||||
FROM a76.invoice_header ih
|
||||
LEFT JOIN a76.invoice_compliance_mx cmp ON cmp.invoice_id = ih.id
|
||||
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.items i ON i.invoice_id = ih.id
|
||||
LEFT JOIN a76.item_lines il ON il.item_id = i.id
|
||||
LEFT JOIN a24.fa_item_lines fil ON fil.id = il.id
|
||||
LEFT JOIN a76.item_line_financials lf ON lf.item_line_id = il.id
|
||||
WHERE ih.operation_type = 'exp'
|
||||
AND ih.invoice_type = 'REP'
|
||||
{"AND " + where_str if where_str else ""}
|
||||
GROUP BY ih.id, ih.invoice_number, ped.pedimento_number, ped.pedimento_code, ped.regime,
|
||||
log.payment_date, cmp.remesa, fin.exchange_rate, cmp.provider_id, cmp.sold_to_id,
|
||||
cmp.customs_broker_id, ih.purchase_order, ped.customs_office, ih.document_type,
|
||||
cmp.edocument, cmp.vucem_operation_num, ih.invoice_date, ih.capture_user,
|
||||
ih.who_updated, log.carrier_id, log.transport_id, log.transport_num
|
||||
ORDER BY ih.invoice_number
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_main_query(db_name: str, where_str: str) -> str:
|
||||
"""Build main SQL query for export repair data."""
|
||||
return f"""
|
||||
SELECT
|
||||
ih.invoice_number AS C1,
|
||||
COALESCE(ped.pedimento_number, '') AS C2,
|
||||
TO_CHAR(ih.invoice_date, 'YYYYMMDD') AS C3,
|
||||
COALESCE(fin.value_me, 0) AS C4,
|
||||
COALESCE(fin.value_mn, 0) AS C5,
|
||||
CASE WHEN ih.is_updated THEN 'AC' ELSE 'NA' END AS C6,
|
||||
COALESCE(ped.pedimento_code, '') AS C7,
|
||||
COALESCE(ped.regime, '') AS C8,
|
||||
'' AS C9,
|
||||
'' AS C10,
|
||||
COALESCE(TO_CHAR(log.payment_date, 'YYYYMMDD'), '') AS C11,
|
||||
COALESCE(cmp.remesa::text, '') AS C12,
|
||||
COALESCE(fin.exchange_rate, 0) AS C13,
|
||||
COALESCE(cmp.provider_id::text, '') AS C14,
|
||||
COALESCE(cmp.sold_to_id::text, '') AS C15,
|
||||
COALESCE(cmp.customs_broker_id::text, '') AS C16,
|
||||
'' AS C17,
|
||||
COALESCE(cls.class_code, '') AS C18,
|
||||
REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(ld.description_spanish, ''), CHR(44), ' '), CHR(9), ' '), CHR(10), ' '), CHR(13), ' ') AS C19,
|
||||
REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(cls.description_en, ''), CHR(44), ' '), CHR(9), ' '), CHR(10), ' '), CHR(13), ' ') AS C20,
|
||||
COALESCE(lq.quantity, 0) AS C21,
|
||||
COALESCE(il.unit_of_measure, 0) AS C22,
|
||||
COALESCE(lf.customs_value_mxn, 0) AS C23,
|
||||
COALESCE(lf.customs_value_usd, 0) AS C24,
|
||||
COALESCE(lq.net_weight, 0) AS C25,
|
||||
COALESCE(lq.gross_weight, 0) AS C26,
|
||||
COALESCE(ih.purchase_order, '') AS C27,
|
||||
COALESCE(lc.fraction, '') AS C28,
|
||||
COALESCE(lc.fraction_type, '') AS C29,
|
||||
COALESCE(lc.advalorem_numeric, 0) AS C30,
|
||||
COALESCE(lc.sector, '') AS C31,
|
||||
COALESCE(lc.origin_country, '') AS C32,
|
||||
COALESCE(ped.customs_office, '') AS C33,
|
||||
COALESCE(ih.document_type, '') AS C34,
|
||||
ih.id AS C35,
|
||||
COALESCE(lf.value_mxn, 0) AS C36,
|
||||
COALESCE(lf.value_usd, 0) AS C37,
|
||||
'P' AS C38,
|
||||
'' AS C39,
|
||||
COALESCE(cmp.edocument, '') AS C40,
|
||||
COALESCE(cmp.vucem_operation_num, '') AS C41,
|
||||
COALESCE(il.line_number, 0) AS C42,
|
||||
REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(ld.brand, ''), CHR(44), ' '), CHR(9), ' '), CHR(10), ' '), CHR(13), ' ') AS C43,
|
||||
REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(ld.model, ''), CHR(44), ' '), CHR(9), ' '), CHR(10), ' '), CHR(13), ' ') AS C44,
|
||||
COALESCE(cls.us_fraction, '') AS C45,
|
||||
COALESCE(prt.eccn, '') AS C46,
|
||||
COALESCE(il.part_number::text, '') AS C47,
|
||||
COALESCE(fin.exchange_rate, 0) AS C48,
|
||||
COALESCE(TO_CHAR(ih.invoice_date, 'YYYYMMDD'), '') AS C49,
|
||||
COALESCE(ih.capture_user, '') AS C50,
|
||||
COALESCE(ih.who_updated, '') AS C51,
|
||||
COALESCE(log.carrier_id, '') AS C52,
|
||||
COALESCE(log.transport_id || ' ' || log.transport_num, '') AS C53,
|
||||
'' AS C54,
|
||||
COALESCE(ld.lot, '') AS C55,
|
||||
'' AS C56,
|
||||
'' AS C57,
|
||||
'' AS C58,
|
||||
'' AS C59
|
||||
FROM a76.invoice_header ih
|
||||
LEFT JOIN a76.invoice_compliance_mx cmp ON cmp.invoice_id = ih.id
|
||||
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.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
|
||||
LEFT JOIN a76.item_line_quantities lq ON lq.item_line_id = il.id
|
||||
LEFT JOIN a76.item_line_financials lf ON lf.item_line_id = il.id
|
||||
LEFT JOIN a76.item_line_customs lc ON lc.item_line_id = il.id
|
||||
LEFT JOIN a76.classes cls ON cls.id = il.class_id
|
||||
LEFT JOIN a76.parts prt ON prt.id = il.part_number
|
||||
LEFT JOIN a76.units_of_measure um ON um.id = il.unit_of_measure
|
||||
WHERE UPPER(ih.operation_type) IN ('EXP', 'TRA', 'RET')
|
||||
AND UPPER(ih.invoice_type) IN ('DEF', 'REP', 'EXDEF', 'MATDE')
|
||||
AND {where_str}
|
||||
ORDER BY ih.invoice_number, il.line_number
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_totals_query(db_name: str, discharge_clause: str = "") -> str:
|
||||
"""Build query to get totals for an export repair invoice."""
|
||||
discharge_filter = "AND il.is_discharged = true" if "descargado" in discharge_clause.lower() else ""
|
||||
return f"""
|
||||
SELECT
|
||||
COALESCE(SUM(CASE WHEN COALESCE(fil.is_subitem, false) THEN 0 ELSE lf.value_usd END), 0),
|
||||
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
|
||||
WHERE itm.invoice_id = :consecutivo
|
||||
|
||||
{discharge_filter}
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_series_query(db_name: str) -> str:
|
||||
"""Build query to get series information for export repairs."""
|
||||
# TODO: QSeriesExpoRep table not migrated to PostgreSQL yet
|
||||
return """
|
||||
SELECT '' as serie, '' as modelo, '' as parte
|
||||
WHERE 1=0
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_driver_badge_query(db_name: str) -> str:
|
||||
"""Build query to get driver badge number for export repairs."""
|
||||
# TODO: GConductor table not migrated to PostgreSQL yet
|
||||
return """
|
||||
SELECT '' as badge
|
||||
WHERE 1=0
|
||||
"""
|
||||
@@ -0,0 +1,877 @@
|
||||
"""
|
||||
SQL Query builders for invoice movement services.
|
||||
Centralizes all SQL query construction logic.
|
||||
"""
|
||||
|
||||
|
||||
class TemporaryImportQueries:
|
||||
"""SQL queries for temporary imports using PostgreSQL tables."""
|
||||
|
||||
@staticmethod
|
||||
def build_aggregated_query(db_name: str, where_str: str) -> str:
|
||||
"""Build optimized query for NORMAL mode (grouped by invoice with totals)."""
|
||||
# Note: db_name parameter kept for compatibility but not used in PostgreSQL
|
||||
return f"""
|
||||
SELECT
|
||||
ih.invoice_number AS C1,
|
||||
COALESCE(ped.pedimento_number, '') AS C2,
|
||||
TO_CHAR(ih.invoice_date, 'YYYYMMDD') AS C3,
|
||||
CASE WHEN ih.is_updated THEN 'AC' ELSE 'NA' END AS C4,
|
||||
COALESCE(ped.pedimento_code, '') AS C5,
|
||||
COALESCE(ped.regime, '') AS C10,
|
||||
COALESCE(TO_CHAR(log.entry_exit_date, 'YYYYMMDD'), '') AS C11,
|
||||
COALESCE(TO_CHAR(log.delivery_date, 'YYYYMMDD'), '') AS C12,
|
||||
COALESCE(TO_CHAR(log.payment_date, 'YYYYMMDD'), '') AS C13,
|
||||
COALESCE(cmp.remesa, 0) AS C14,
|
||||
COALESCE(fin.exchange_rate, 0) AS C15,
|
||||
COALESCE(cmp.provider_id::text, '') AS C16,
|
||||
COALESCE(cmp.sold_to_id::text, '') AS C17,
|
||||
COALESCE(cmp.customs_broker_id::text, '') AS C18,
|
||||
COALESCE(cmp.aduana, '') AS C38,
|
||||
ih.id AS C39,
|
||||
COALESCE(ped_r1.pedimento_number, '') AS C41,
|
||||
COALESCE(cmp.edocument, '') AS C42,
|
||||
COALESCE(cmp.vucem_operation_num, '') AS C43,
|
||||
COALESCE(fin.exchange_rate, 0) AS C50,
|
||||
COALESCE(TO_CHAR(ih.emission_date, 'YYYYMMDD'), '') AS C51,
|
||||
COALESCE(ih.capture_user, '') AS C52,
|
||||
COALESCE(ih.who_updated, '') AS C53,
|
||||
COALESCE(log.carrier_id, '') AS C54,
|
||||
COALESCE(log.transport_num || ' ' || log.license_plate, '') AS C55,
|
||||
'' AS C56,
|
||||
'' AS C57,
|
||||
'' AS C58,
|
||||
COALESCE(SUM(CASE WHEN COALESCE(fil.is_subitem, false) THEN 0 ELSE lf.value_usd END), 0) AS total_me,
|
||||
COALESCE(SUM(CASE WHEN COALESCE(fil.is_subitem, false) THEN 0 ELSE lf.value_mxn END), 0) AS total_mn
|
||||
FROM a76.invoice_header ih
|
||||
LEFT JOIN a76.invoice_compliance_mx cmp ON cmp.invoice_id = ih.id
|
||||
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.pedimentos ped_r1 ON ped_r1.id = cmp.pedimento_r1
|
||||
LEFT JOIN a76.items i ON i.invoice_id = ih.id
|
||||
LEFT JOIN a76.item_lines il ON il.item_id = i.id
|
||||
LEFT JOIN a24.fa_item_lines fil ON fil.id = il.id
|
||||
LEFT JOIN a76.item_line_financials lf ON lf.item_line_id = il.id
|
||||
WHERE ih.operation_type = 'imp'
|
||||
AND ih.invoice_type = 'TEM'
|
||||
AND {where_str}
|
||||
GROUP BY ih.id, ih.invoice_number, ped.pedimento_number, ped.pedimento_code, ped.regime,
|
||||
log.entry_exit_date, log.delivery_date, log.payment_date, cmp.remesa, fin.exchange_rate,
|
||||
cmp.provider_id, cmp.sold_to_id, cmp.customs_broker_id, cmp.aduana, ped_r1.pedimento_number,
|
||||
cmp.edocument, cmp.vucem_operation_num, ih.emission_date, ih.capture_user, ih.who_updated,
|
||||
log.carrier_id, log.transport_num, log.license_plate
|
||||
ORDER BY ih.invoice_number
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_main_query(db_name: str, where_str: str) -> str:
|
||||
"""Build main SQL query for DETAILED mode (all partidas) from PostgreSQL."""
|
||||
# Note: db_name parameter is kept for compatibility but not used in PostgreSQL
|
||||
return f"""
|
||||
SELECT
|
||||
ih.invoice_number AS C1,
|
||||
COALESCE(ped.pedimento_number, '') AS C2,
|
||||
TO_CHAR(ih.invoice_date, 'YYYYMMDD') AS C3,
|
||||
CASE WHEN ih.is_updated THEN 'AC' ELSE 'NA' END AS C4,
|
||||
COALESCE(ped.pedimento_code, '') AS C5,
|
||||
COALESCE(fin.value_me, 0) AS C6,
|
||||
COALESCE(fin.value_mn, 0) AS C7,
|
||||
COALESCE(cmp.provider_id::text, '') AS C8,
|
||||
COALESCE(cmp.sold_to_id::text, '') AS C9,
|
||||
COALESCE(ped.regime, '') AS C10,
|
||||
COALESCE(TO_CHAR(log.entry_exit_date, 'YYYYMMDD'), '') AS C11,
|
||||
COALESCE(TO_CHAR(log.delivery_date, 'YYYYMMDD'), '') AS C12,
|
||||
COALESCE(TO_CHAR(log.payment_date, 'YYYYMMDD'), '') AS C13,
|
||||
COALESCE(cmp.remesa, 0) AS C14,
|
||||
COALESCE(fin.exchange_rate, 0) AS C15,
|
||||
COALESCE(cmp.provider_id::text, '') AS C16,
|
||||
COALESCE(cmp.sold_to_id::text, '') AS C17,
|
||||
COALESCE(cmp.customs_broker_id::text, '') AS C18,
|
||||
'' AS C19,
|
||||
COALESCE(il.class_id::text, '') AS C20,
|
||||
REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(ld.description_spanish, ''), CHR(44), ' '), CHR(9), ' '), CHR(10), ' '), CHR(13), ' ') AS C21,
|
||||
REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(ld.description_english, ''), CHR(44), ' '), CHR(9), ' '), CHR(10), ' '), CHR(13), ' ') AS C22,
|
||||
COALESCE(lq.quantity, 0) AS C23,
|
||||
COALESCE(il.unit_of_measure::text, '') AS C24,
|
||||
COALESCE(lf.value_mxn, 0) AS C25,
|
||||
COALESCE(lf.customs_value_mxn, 0) AS C26,
|
||||
COALESCE(lf.value_usd, 0) AS C27,
|
||||
COALESCE(lf.customs_value_usd, 0) AS C28,
|
||||
COALESCE(lq.net_weight, 0) AS C29,
|
||||
COALESCE(lq.gross_weight, 0) AS C30,
|
||||
COALESCE(ih.purchase_order, '') AS C31,
|
||||
COALESCE(lc.fraction, '') AS C32,
|
||||
COALESCE(lc.fraction_type, '') AS C33,
|
||||
COALESCE(lc.advalorem_numeric, 0) AS C34,
|
||||
COALESCE(lc.sector, '') AS C35,
|
||||
COALESCE(lf.igi_amount_usd, 0) AS C36,
|
||||
COALESCE(lc.origin_country, '') AS C37,
|
||||
COALESCE(cmp.aduana, '') AS C38,
|
||||
ih.id AS C39,
|
||||
FALSE AS C40,
|
||||
'' AS C41,
|
||||
COALESCE(cmp.edocument, '') AS C42,
|
||||
COALESCE(cmp.vucem_operation_num, '') AS C43,
|
||||
COALESCE(il.line_number, 0) AS C44,
|
||||
'' AS C45,
|
||||
'' AS C46,
|
||||
COALESCE(cls.us_fraction, '') AS C47,
|
||||
COALESCE(prt.eccn, '') AS C48,
|
||||
COALESCE(il.part_number::text, '') AS C49,
|
||||
COALESCE(fin.exchange_rate, 0) AS C50,
|
||||
COALESCE(TO_CHAR(ih.emission_date, 'YYYYMMDD'), '') AS C51,
|
||||
COALESCE(ih.capture_user, '') AS C52,
|
||||
COALESCE(ih.who_updated, '') AS C53,
|
||||
COALESCE(log.carrier_id, '') AS C54,
|
||||
COALESCE(log.transport_num || ' ' || log.license_plate, '') AS C55,
|
||||
'' AS C56,
|
||||
'' AS C57,
|
||||
'' AS C58
|
||||
FROM a76.invoice_header ih
|
||||
LEFT JOIN a76.invoice_compliance_mx cmp ON cmp.invoice_id = ih.id
|
||||
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.item_lines il ON il.item_id = (
|
||||
SELECT id FROM a76.items WHERE invoice_id = ih.id LIMIT 1
|
||||
)
|
||||
LEFT JOIN a76.item_line_descriptions ld ON ld.item_line_id = il.id
|
||||
LEFT JOIN a76.item_line_quantities lq ON lq.item_line_id = il.id
|
||||
LEFT JOIN a76.item_line_financials lf ON lf.item_line_id = il.id
|
||||
LEFT JOIN a76.item_line_customs lc ON lc.item_line_id = il.id
|
||||
LEFT JOIN a76.classes cls ON cls.id = il.class_id
|
||||
LEFT JOIN a76.parts prt ON prt.id = il.part_number
|
||||
LEFT JOIN a76.units_of_measure um ON um.id = il.unit_of_measure
|
||||
WHERE ih.operation_type = 'imp'
|
||||
AND ih.invoice_type = 'TEM'
|
||||
AND {where_str}
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_totals_query(db_name: str) -> str:
|
||||
"""Build query to get totals for an invoice."""
|
||||
return f"""
|
||||
SELECT
|
||||
COALESCE(SUM(EqiPim.ValorImpoME), 0),
|
||||
COALESCE(SUM(EqiPim.ValorImpoMN), 0)
|
||||
FROM [{db_name}].dbo.QEqiMaq EqiPim
|
||||
WHERE EqiPim.Consecutivo = :consecutivo
|
||||
AND EqiPim.EsSubpartida = 'P'
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_series_query(db_name: str) -> str:
|
||||
"""Build query to get series information."""
|
||||
return f"""
|
||||
SELECT SerieImpo, ModeloImpo, ParteImpo
|
||||
FROM [{db_name}].dbo.QSeriesImpo
|
||||
WHERE Consecutivo = :consecutivo
|
||||
AND LineaImpo = :linea
|
||||
ORDER BY RenImpo
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_driver_badge_query(db_name: str) -> str:
|
||||
"""Build query to get driver badge number."""
|
||||
return f"""
|
||||
SELECT TOP 1 NUMGAFETEUNICO
|
||||
FROM [{db_name}].dbo.GConductor
|
||||
LEFT JOIN [{db_name}].dbo.QFacImp
|
||||
ON QFacImp.CONDUCTOR = GConductor.CONDUCTOR
|
||||
WHERE FacturaImpo = :factura
|
||||
"""
|
||||
|
||||
|
||||
class DefinitiveImportQueries:
|
||||
"""SQL queries for definitive imports (PostgreSQL schema)."""
|
||||
|
||||
@staticmethod
|
||||
def build_aggregated_query(db_name: str, where_clause: str) -> str:
|
||||
"""Build optimized query for NORMAL mode (grouped by invoice with totals)."""
|
||||
return f"""
|
||||
SELECT
|
||||
ih.invoice_number AS C1,
|
||||
COALESCE(ped.pedimento_number, '') AS C2,
|
||||
TO_CHAR(ih.invoice_date, 'YYYYMMDD') AS C3,
|
||||
CASE WHEN ih.is_updated THEN 'AC' ELSE 'NA' END AS C4,
|
||||
COALESCE(ped.pedimento_code, '') AS C5,
|
||||
COALESCE(ped.regime, '') AS C10,
|
||||
COALESCE(TO_CHAR(log.entry_exit_date, 'YYYYMMDD'), '') AS C11,
|
||||
COALESCE(TO_CHAR(log.delivery_date, 'YYYYMMDD'), '') AS C12,
|
||||
COALESCE(TO_CHAR(log.payment_date, 'YYYYMMDD'), '') AS C13,
|
||||
COALESCE(log.payment_receipt_num, '') AS C14,
|
||||
COALESCE(fin.exchange_rate, 0) AS C15,
|
||||
COALESCE(cmp.provider_id::text, '') AS C16,
|
||||
COALESCE(cmp.sold_to_id::text, '') AS C17,
|
||||
COALESCE(cmp.customs_broker_id::text, '') AS C18,
|
||||
COALESCE(ih.purchase_order, '') AS C31,
|
||||
COALESCE(cmp.aduana, '') AS C39,
|
||||
ih.id AS C35,
|
||||
COALESCE(ped_r1.pedimento_number, '') AS C42,
|
||||
COALESCE(cmp.edocument, '') AS C43,
|
||||
COALESCE(cmp.vucem_operation_num, '') AS C44,
|
||||
COALESCE(fin.exchange_rate, 0) AS C51,
|
||||
COALESCE(TO_CHAR(ih.emission_date, 'YYYYMMDD'), '') AS C52,
|
||||
COALESCE(ih.capture_user, '') AS C53,
|
||||
COALESCE(ih.who_updated, '') AS C54,
|
||||
COALESCE(log.transport_id || ' ' || log.transport_num, '') AS C56,
|
||||
'' AS C57,
|
||||
'' AS C58,
|
||||
'' AS C59,
|
||||
COALESCE(SUM(CASE WHEN COALESCE(fil.is_subitem, false) THEN 0 ELSE lf.value_usd END), 0) AS total_me,
|
||||
COALESCE(SUM(CASE WHEN COALESCE(fil.is_subitem, false) THEN 0 ELSE lf.value_mxn END), 0) AS total_mn
|
||||
FROM a76.invoice_header ih
|
||||
LEFT JOIN a76.invoice_compliance_mx cmp ON cmp.invoice_id = ih.id
|
||||
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.pedimentos ped_r1 ON ped_r1.id = cmp.pedimento_r1
|
||||
LEFT JOIN a76.items i ON i.invoice_id = ih.id
|
||||
LEFT JOIN a76.item_lines il ON il.item_id = i.id
|
||||
LEFT JOIN a24.fa_item_lines fil ON fil.id = il.id
|
||||
LEFT JOIN a76.item_line_financials lf ON lf.item_line_id = il.id
|
||||
WHERE ih.operation_type = 'imp'
|
||||
AND ih.invoice_type IN ('DEF', 'EXDEF', 'MATDE')
|
||||
AND {where_clause}
|
||||
GROUP BY ih.id, ih.invoice_number, ped.pedimento_number, ped.pedimento_code, ped.regime,
|
||||
log.entry_exit_date, log.delivery_date, log.payment_date, log.payment_receipt_num,
|
||||
fin.exchange_rate, cmp.provider_id, cmp.sold_to_id, cmp.customs_broker_id,
|
||||
ih.purchase_order, cmp.aduana, ped_r1.pedimento_number, cmp.edocument,
|
||||
cmp.vucem_operation_num, ih.emission_date, ih.capture_user, ih.who_updated,
|
||||
log.transport_id, log.transport_num
|
||||
ORDER BY ih.invoice_number
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_main_query(db_name: str, where_clause: str) -> str:
|
||||
return f"""
|
||||
SELECT
|
||||
ih.invoice_number AS C1, -- [0]
|
||||
ped.pedimento_number AS C2, -- [1]
|
||||
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]
|
||||
ped.regime AS C10, -- [9]
|
||||
log.entry_exit_date AS C11, -- [10]
|
||||
log.delivery_date AS C12, -- [11]
|
||||
log.payment_date AS C13, -- [12]
|
||||
log.payment_receipt_num AS C14, -- [13]
|
||||
'' AS C15, -- [14]
|
||||
cmp.provider_id AS C16, -- [15]
|
||||
cmp.sold_to_id AS C17, -- [16]
|
||||
cmp.customs_broker_id AS C18, -- [17]
|
||||
'' AS C19, -- [18]
|
||||
prt.part_number AS C20, -- [19]
|
||||
ld.description_spanish AS C21, -- [20]
|
||||
ld.description_english AS C22, -- [21]
|
||||
lq.quantity AS C23, -- [22]
|
||||
um.code AS C24, -- [23]
|
||||
lf.value_mxn AS C25, -- [24]
|
||||
'' AS C26, -- [25]
|
||||
lf.value_usd AS C27, -- [26]
|
||||
'' AS C28, -- [27]
|
||||
lq.net_weight AS C29, -- [28]
|
||||
lq.gross_weight AS C30, -- [29]
|
||||
ih.purchase_order AS C31, -- [30]
|
||||
lc.fraction AS C32, -- [31]
|
||||
'' AS C33, '' AS C34, -- [32-33]
|
||||
ih.id AS C35, -- [34]
|
||||
'' AS C36, '' AS C37, -- [35-36]
|
||||
lc.origin_country AS C38, -- [37]
|
||||
cmp.aduana AS C39, -- [38]
|
||||
il.material_type AS C40, -- [39]
|
||||
il.id AS C41, -- [40]
|
||||
'' AS C42, -- [41] rectification_id
|
||||
cmp.edocument AS C43, -- [42]
|
||||
cmp.vucem_operation_num AS C44, -- [43]
|
||||
il.line_number AS C45, -- [44]
|
||||
ld.brand AS C46, -- [45]
|
||||
ld.model AS C47, -- [46]
|
||||
prt.us_fraction AS C48, -- [47]
|
||||
prt.eccn AS C49, -- [48]
|
||||
prt.id AS C50, -- [49]
|
||||
fin.exchange_rate AS C51, -- [50]
|
||||
ih.emission_date AS C52, -- [51]
|
||||
ih.capture_user AS C53, -- [52]
|
||||
ih.who_updated AS C54, -- [53]
|
||||
'' AS C55, -- [54]
|
||||
COALESCE(log.transport_id || ' ' || log.transport_num, '') AS C56, -- [55]
|
||||
'' AS C57, -- [56] Pedimento18 (row[56])
|
||||
COALESCE(ld.lot, '') AS C58, -- [57] Lote (row[57])
|
||||
'' AS C59, -- [58] TipoPed (row[58])
|
||||
'' AS C60 -- [59] Relleno final
|
||||
FROM a76.invoice_header ih
|
||||
LEFT JOIN a76.invoice_compliance_mx cmp ON cmp.invoice_id = ih.id
|
||||
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.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
|
||||
LEFT JOIN a76.item_line_quantities lq ON lq.item_line_id = il.id
|
||||
LEFT JOIN a76.item_line_financials lf ON lf.item_line_id = il.id
|
||||
LEFT JOIN a76.item_line_customs lc ON lc.item_line_id = il.id
|
||||
LEFT JOIN a76.classes cls ON cls.id = il.class_id
|
||||
LEFT JOIN a76.parts prt ON prt.id = il.part_number
|
||||
LEFT JOIN a76.units_of_measure um ON um.id = il.unit_of_measure
|
||||
WHERE {where_clause}
|
||||
ORDER BY ih.invoice_number, il.line_number
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_totals_query(db_name: str) -> str:
|
||||
"""Build query to get totals for a definitive import invoice."""
|
||||
return f"""
|
||||
SELECT
|
||||
COALESCE(SUM(CASE WHEN COALESCE(fil.is_subitem, false) THEN 0 ELSE lf.value_usd END), 0),
|
||||
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 fil.id = il.id
|
||||
INNER JOIN a76.items itm ON itm.id = il.item_id
|
||||
WHERE itm.invoice_id = :consecutivo
|
||||
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_series_query(db_name: str) -> str:
|
||||
"""Build query to get series information for definitive imports."""
|
||||
# TODO: QSeriesDef table not migrated to PostgreSQL yet
|
||||
return """
|
||||
SELECT '' as serie, '' as modelo, '' as parte
|
||||
WHERE 1=0
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_driver_badge_query(db_name: str) -> str:
|
||||
"""Build query to get driver badge number for definitive imports."""
|
||||
# TODO: GConductor table not migrated to PostgreSQL yet
|
||||
return """
|
||||
SELECT '' as badge
|
||||
WHERE 1=0
|
||||
"""
|
||||
|
||||
|
||||
class RepairImportQueries:
|
||||
"""SQL queries for repair imports (PostgreSQL schema)."""
|
||||
|
||||
@staticmethod
|
||||
def build_aggregated_query(db_name: str, where_str: str, discharge_clause: str = "") -> str:
|
||||
"""Build optimized query for NORMAL mode (grouped by invoice with totals)."""
|
||||
discharge_filter = "" # Temporarily disabled until schema migration
|
||||
return f"""
|
||||
SELECT
|
||||
ih.invoice_number AS C2,
|
||||
COALESCE(ped.pedimento_number, '') AS C3,
|
||||
TO_CHAR(ih.invoice_date, 'YYYYMMDD') AS C4,
|
||||
CASE WHEN ih.is_updated THEN 'AC' ELSE 'NA' END AS C5,
|
||||
COALESCE(ped.pedimento_code, '') AS C6,
|
||||
COALESCE(ped.regime, '') AS C7,
|
||||
COALESCE(TO_CHAR(log.payment_date, 'YYYYMMDD'), '') AS C9,
|
||||
COALESCE(cmp.remesa::text, '') AS C10,
|
||||
COALESCE(fin.exchange_rate, 0) AS C11,
|
||||
COALESCE(cmp.provider_id::text, '') AS C12,
|
||||
COALESCE(cmp.sold_to_id::text, '') AS C13,
|
||||
COALESCE(cmp.customs_broker_id::text, '') AS C14,
|
||||
COALESCE(ih.purchase_order, '') AS C24,
|
||||
COALESCE(ped.customs_office, '') AS C29,
|
||||
ih.id AS C30,
|
||||
COALESCE(cmp.edocument, '') AS C33,
|
||||
COALESCE(cmp.vucem_operation_num, '') AS C34,
|
||||
COALESCE(fin.exchange_rate, 0) AS C40,
|
||||
COALESCE(TO_CHAR(ih.emission_date, 'YYYYMMDD'), '') AS C41,
|
||||
COALESCE(ih.capture_user, '') AS C42,
|
||||
COALESCE(ih.who_updated, '') AS C43,
|
||||
COALESCE(log.carrier_id, '') AS C44,
|
||||
COALESCE(log.transport_num, '') AS C45,
|
||||
COALESCE(ped.pedimento_code, '') AS C47,
|
||||
COALESCE(SUM(CASE WHEN COALESCE(fil.is_subitem, false) THEN 0 ELSE lf.value_usd END), 0) AS total_me,
|
||||
COALESCE(SUM(CASE WHEN COALESCE(fil.is_subitem, false) THEN 0 ELSE lf.value_mxn END), 0) AS total_mn
|
||||
FROM a76.invoice_header ih
|
||||
LEFT JOIN a76.invoice_compliance_mx cmp ON cmp.invoice_id = ih.id
|
||||
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.items i ON i.invoice_id = ih.id
|
||||
LEFT JOIN a76.item_lines il ON il.item_id = i.id
|
||||
LEFT JOIN a24.fa_item_lines fil ON fil.id = il.id
|
||||
LEFT JOIN a76.item_line_financials lf ON lf.item_line_id = il.id
|
||||
WHERE ih.operation_type = 'imp'
|
||||
AND ih.invoice_type = 'REP'
|
||||
AND COALESCE(cmp.is_regime_change, false) = false
|
||||
{"AND " + where_str if where_str else ""}
|
||||
{discharge_filter}
|
||||
GROUP BY ih.id, ih.invoice_number, ped.pedimento_number, ped.pedimento_code, ped.regime,
|
||||
log.payment_date, cmp.remesa, fin.exchange_rate, cmp.provider_id, cmp.sold_to_id,
|
||||
cmp.customs_broker_id, ih.purchase_order, ped.customs_office, cmp.edocument,
|
||||
cmp.vucem_operation_num, ih.emission_date, ih.capture_user, ih.who_updated,
|
||||
log.carrier_id, log.transport_num
|
||||
ORDER BY ih.invoice_number
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_main_query(db_name: str, where_str: str, discharge_clause: str = "") -> str:
|
||||
"""Build main SQL query for repair import data."""
|
||||
# Note: is_discharged field not yet migrated to PostgreSQL schema
|
||||
# discharge_filter = "AND il.is_discharged = true" if "descargado" in discharge_clause.lower() else ""
|
||||
discharge_filter = "" # Temporarily disabled until schema migration
|
||||
return f"""
|
||||
SELECT
|
||||
il.line_number,
|
||||
ih.invoice_number,
|
||||
COALESCE(ped.pedimento_number, ''),
|
||||
TO_CHAR(ih.invoice_date, 'YYYYMMDD'),
|
||||
CASE WHEN ih.is_updated THEN 'AC' ELSE 'NA' END,
|
||||
COALESCE(ped.pedimento_code, ''),
|
||||
COALESCE(ped.regime, ''),
|
||||
'',
|
||||
COALESCE(TO_CHAR(log.payment_date, 'YYYYMMDD'), ''),
|
||||
COALESCE(cmp.remesa::text, ''),
|
||||
COALESCE(fin.exchange_rate, 0),
|
||||
COALESCE(cmp.provider_id::text, ''),
|
||||
COALESCE(cmp.sold_to_id::text, ''),
|
||||
COALESCE(cmp.customs_broker_id::text, ''),
|
||||
COALESCE(il.part_number::text, ''),
|
||||
REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(ld.description_spanish, ''), CHR(44), ' '), CHR(9), ' '), CHR(10), ' '), CHR(13), ' '),
|
||||
REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(ld.description_english, ''), CHR(44), ' '), CHR(9), ' '), CHR(10), ' '), CHR(13), ' '),
|
||||
COALESCE(lq.quantity, 0),
|
||||
COALESCE(il.unit_of_measure, 0),
|
||||
COALESCE(lf.value_mxn, 0),
|
||||
COALESCE(lf.value_usd, 0),
|
||||
COALESCE(lq.net_weight, 0),
|
||||
COALESCE(lq.gross_weight, 0),
|
||||
COALESCE(ih.purchase_order, ''),
|
||||
COALESCE(lc.fraction, ''),
|
||||
'',
|
||||
COALESCE(lc.sector, ''),
|
||||
COALESCE(lc.origin_country, ''),
|
||||
COALESCE(ped.customs_office, ''),
|
||||
ih.id,
|
||||
'P',
|
||||
'',
|
||||
COALESCE(cmp.edocument, ''),
|
||||
COALESCE(cmp.vucem_operation_num, ''),
|
||||
REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(ld.brand, ''), CHR(44), ' '), CHR(9), ' '), CHR(10), ' '), CHR(13), ' '),
|
||||
REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(ld.model, ''), CHR(44), ' '), CHR(9), ' '), CHR(10), ' '), CHR(13), ' '),
|
||||
COALESCE(lc.american_fraction, ''),
|
||||
COALESCE(prt.eccn, ''),
|
||||
COALESCE(fin.exchange_rate, 0),
|
||||
COALESCE(TO_CHAR(ih.emission_date, 'YYYYMMDD'), ''),
|
||||
COALESCE(ih.capture_user, ''),
|
||||
COALESCE(ih.who_updated, ''),
|
||||
COALESCE(log.carrier_id, ''),
|
||||
COALESCE(log.transport_num, ''),
|
||||
'',
|
||||
COALESCE(ped.pedimento_code, '')
|
||||
FROM a76.invoice_header ih
|
||||
LEFT JOIN a76.invoice_compliance_mx cmp ON cmp.invoice_id = ih.id
|
||||
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.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
|
||||
LEFT JOIN a76.item_line_quantities lq ON lq.item_line_id = il.id
|
||||
LEFT JOIN a76.item_line_financials lf ON lf.item_line_id = il.id
|
||||
LEFT JOIN a76.item_line_customs lc ON lc.item_line_id = il.id
|
||||
LEFT JOIN a76.parts prt ON prt.id = il.part_number
|
||||
LEFT JOIN a76.units_of_measure um ON um.id = il.unit_of_measure
|
||||
WHERE ih.operation_type = 'imp'
|
||||
AND ih.invoice_type = 'REP'
|
||||
AND COALESCE(cmp.is_regime_change, false) = false
|
||||
{"AND " + where_str if where_str else ""}
|
||||
{discharge_filter}
|
||||
ORDER BY ih.invoice_number, il.line_number
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_totals_query(db_name: str, discharge_clause: str = "") -> str:
|
||||
"""Build query to get totals for a repair import invoice."""
|
||||
# Note: is_discharged field not yet migrated to PostgreSQL schema
|
||||
# discharge_filter = "AND il.is_discharged = true" if "descargado" in discharge_clause.lower() else ""
|
||||
discharge_filter = "" # Temporarily disabled until schema migration
|
||||
return f"""
|
||||
SELECT
|
||||
COALESCE(SUM(CASE WHEN COALESCE(fil.is_subitem, false) THEN 0 ELSE lf.value_usd END), 0),
|
||||
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 fil.id = il.id
|
||||
INNER JOIN a76.items itm ON itm.id = il.item_id
|
||||
WHERE itm.invoice_id = :consecutivo
|
||||
|
||||
{discharge_filter}
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_series_query(db_name: str) -> str:
|
||||
"""Build query to get series information for repair imports."""
|
||||
# TODO: QSeriesImpoRep table not migrated to PostgreSQL yet
|
||||
return """
|
||||
SELECT '' as serie, '' as modelo, '' as parte
|
||||
WHERE 1=0
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_driver_badge_query(db_name: str) -> str:
|
||||
"""Build query to get driver badge number for repair imports."""
|
||||
# TODO: GConductor table not migrated to PostgreSQL yet
|
||||
return """
|
||||
SELECT '' as badge
|
||||
WHERE 1=0
|
||||
"""
|
||||
|
||||
|
||||
class ExportQueries:
|
||||
"""SQL queries for exports (PostgreSQL schema)."""
|
||||
|
||||
@staticmethod
|
||||
def build_aggregated_query(db_name: str, where_clause: str) -> str:
|
||||
"""
|
||||
Build optimized query for NORMAL mode (grouped by invoice with totals).
|
||||
|
||||
Args:
|
||||
db_name: Database name (not used in PostgreSQL version)
|
||||
where_clause: Additional WHERE conditions (without WHERE keyword)
|
||||
"""
|
||||
return f"""
|
||||
SELECT
|
||||
ih.invoice_number AS C1,
|
||||
COALESCE(ped.pedimento_number, '') AS C2,
|
||||
TO_CHAR(ih.invoice_date, 'YYYYMMDD') AS C3,
|
||||
CASE WHEN ih.is_updated THEN 'AC' ELSE 'NA' END AS C6,
|
||||
COALESCE(ped.pedimento_code, '') AS C7,
|
||||
COALESCE(ped.regime, '') AS C8,
|
||||
COALESCE(TO_CHAR(log.entry_exit_date, 'YYYYMMDD'), '') AS C9,
|
||||
COALESCE(TO_CHAR(log.delivery_date, 'YYYYMMDD'), '') AS C10,
|
||||
COALESCE(TO_CHAR(log.payment_date, 'YYYYMMDD'), '') AS C11,
|
||||
COALESCE(log.payment_receipt_num, '') AS C12,
|
||||
COALESCE(cmp.provider_id::text, '') AS C14,
|
||||
COALESCE(cmp.sold_to_id::text, '') AS C15,
|
||||
COALESCE(cmp.customs_broker_id::text, '') AS C16,
|
||||
COALESCE(ih.purchase_order, '') AS C27,
|
||||
COALESCE(cmp.aduana, '') AS C33,
|
||||
COALESCE(ih.invoice_type, '') AS C34,
|
||||
ih.id AS C35,
|
||||
COALESCE(cmp.edocument, '') AS C40,
|
||||
COALESCE(cmp.vucem_operation_num, '') AS C41,
|
||||
COALESCE(fin.exchange_rate, 0) AS C48,
|
||||
COALESCE(TO_CHAR(ih.emission_date, 'YYYYMMDD'), '') AS C49,
|
||||
COALESCE(ih.capture_user, '') AS C50,
|
||||
COALESCE(ih.who_updated, '') AS C51,
|
||||
COALESCE(log.transport_id || ' ' || log.transport_num, '') AS C53,
|
||||
COALESCE(SUM(CASE WHEN COALESCE(fil.is_subitem, false) THEN 0 ELSE lf.value_usd END), 0) AS total_me,
|
||||
COALESCE(SUM(CASE WHEN COALESCE(fil.is_subitem, false) THEN 0 ELSE lf.value_mxn END), 0) AS total_mn
|
||||
FROM a76.invoice_header ih
|
||||
LEFT JOIN a76.invoice_compliance_mx cmp ON cmp.invoice_id = ih.id
|
||||
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.items i ON i.invoice_id = ih.id
|
||||
LEFT JOIN a76.item_lines il ON il.item_id = i.id
|
||||
LEFT JOIN a24.fa_item_lines fil ON fil.id = il.id
|
||||
LEFT JOIN a76.item_line_financials lf ON lf.item_line_id = il.id
|
||||
WHERE {where_clause}
|
||||
GROUP BY ih.id, ih.invoice_number, ih.is_updated, ped.pedimento_number, ped.pedimento_code, ped.regime,
|
||||
log.entry_exit_date, log.delivery_date, log.payment_date, log.payment_receipt_num,
|
||||
cmp.provider_id, cmp.sold_to_id, cmp.customs_broker_id, ih.purchase_order, cmp.aduana,
|
||||
ih.invoice_type, cmp.edocument, cmp.vucem_operation_num, fin.exchange_rate,
|
||||
ih.emission_date, ih.capture_user, ih.who_updated, log.transport_id, log.transport_num,
|
||||
ih.invoice_date
|
||||
ORDER BY ih.invoice_number
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_main_query(db_name: str, where_clause: str) -> str:
|
||||
return f"""
|
||||
SELECT
|
||||
ih.invoice_number AS C1, -- [0]
|
||||
ped.pedimento_number AS C2, -- [1]
|
||||
ih.invoice_date AS C3, -- [2]
|
||||
'' AS C4, -- [3]
|
||||
'' AS C5, -- [4]
|
||||
ped.status AS C6, -- [5]
|
||||
ped.pedimento_code AS C7, -- [6]
|
||||
ped.regime AS C8, -- [7]
|
||||
log.entry_exit_date AS C9, -- [8]
|
||||
log.delivery_date AS C10, -- [9]
|
||||
log.payment_date AS C11, -- [10]
|
||||
log.payment_receipt_num AS C12, -- [11]
|
||||
'' AS C13, -- [12]
|
||||
cmp.provider_id AS C14, -- [13]
|
||||
cmp.sold_to_id AS C15, -- [14]
|
||||
cmp.customs_broker_id AS C16, -- [15]
|
||||
'' AS C17, -- [16]
|
||||
prt.part_number AS C18, -- [17]
|
||||
ld.description_spanish AS C19, -- [18]
|
||||
ld.description_english AS C20, -- [19]
|
||||
lq.quantity AS C21, -- [20]
|
||||
um.code AS C22, -- [21]
|
||||
'' AS C23, -- [22]
|
||||
'' AS C24, -- [23]
|
||||
lq.net_weight AS C25, -- [24]
|
||||
lq.gross_weight AS C26, -- [25]
|
||||
ih.purchase_order AS C27, -- [26]
|
||||
lc.fraction AS C28, -- [27]
|
||||
'' AS C29, -- [28]
|
||||
'' AS C30, -- [29]
|
||||
'' AS C31, -- [30]
|
||||
'' AS C32, -- [31]
|
||||
cmp.aduana AS C33, -- [32]
|
||||
ih.invoice_type AS C34, -- [33]
|
||||
ih.id AS C35, -- [34]
|
||||
lf.value_mxn AS C36, -- [35]
|
||||
lf.value_usd AS C37, -- [36]
|
||||
il.material_type AS C38, -- [37]
|
||||
'' AS C39, -- [38] rectification_id
|
||||
cmp.edocument AS C40, -- [39]
|
||||
cmp.vucem_operation_num AS C41, -- [40]
|
||||
il.line_number AS C42, -- [41]
|
||||
ld.brand AS C43, -- [42]
|
||||
ld.model AS C44, -- [43]
|
||||
prt.us_fraction AS C45, -- [44]
|
||||
prt.eccn AS C46, -- [45]
|
||||
prt.id AS C47, -- [46]
|
||||
fin.exchange_rate AS C48, -- [47]
|
||||
ih.emission_date AS C49, -- [48]
|
||||
ih.capture_user AS C50, -- [49]
|
||||
ih.who_updated AS C51, -- [50]
|
||||
'' AS C52, -- [51]
|
||||
COALESCE(log.transport_id || ' ' || log.transport_num, '') AS C53, -- [52] NumCaja
|
||||
'' AS C54, -- [53] Pedimento18
|
||||
COALESCE(ld.lot, '') AS C55, -- [54] Lote
|
||||
'' AS C56, -- [55] TipoPedimentoTransporte
|
||||
'' AS C57, -- [56]
|
||||
'' AS C58, -- [57]
|
||||
'' AS C59, -- [58]
|
||||
'' AS C60 -- [59] Relleno final
|
||||
FROM a76.invoice_header ih
|
||||
LEFT JOIN a76.invoice_compliance_mx cmp ON cmp.invoice_id = ih.id
|
||||
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.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
|
||||
LEFT JOIN a76.item_line_quantities lq ON lq.item_line_id = il.id
|
||||
LEFT JOIN a76.item_line_financials lf ON lf.item_line_id = il.id
|
||||
LEFT JOIN a76.item_line_customs lc ON lc.item_line_id = il.id
|
||||
LEFT JOIN a76.classes cls ON cls.id = il.class_id
|
||||
LEFT JOIN a76.parts prt ON prt.id = il.part_number
|
||||
LEFT JOIN a76.units_of_measure um ON um.id = il.unit_of_measure
|
||||
WHERE {where_clause}
|
||||
ORDER BY ih.invoice_number, il.line_number
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_totals_query(db_name: str, discharge_clause: str = "") -> str:
|
||||
"""Build query to get totals for an export invoice.
|
||||
|
||||
Only sums partidas where is_subitem is false (main partidas, not sub-items).
|
||||
"""
|
||||
discharge_filter = "AND il.is_discharged = true" if "descargado" in discharge_clause.lower() else ""
|
||||
return f"""
|
||||
SELECT
|
||||
COALESCE(SUM(lf.value_usd), 0),
|
||||
COALESCE(SUM(lf.value_mxn), 0)
|
||||
FROM a76.item_line_financials lf
|
||||
INNER JOIN a76.item_lines il ON il.id = lf.item_line_id
|
||||
INNER JOIN a76.items itm ON itm.id = il.item_id
|
||||
WHERE itm.invoice_id = :consecutivo
|
||||
{discharge_filter}
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_series_query(db_name: str) -> str:
|
||||
"""Build query to get series information for exports."""
|
||||
# TODO: QSeriesExpo table not migrated to PostgreSQL yet
|
||||
return """
|
||||
SELECT '' as serie, '' as modelo, '' as parte
|
||||
WHERE 1=0
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_driver_badge_query(db_name: str) -> str:
|
||||
"""Build query to get driver badge number for exports."""
|
||||
# TODO: GConductor table not migrated to PostgreSQL yet
|
||||
return """
|
||||
SELECT '' as badge
|
||||
WHERE 1=0
|
||||
"""
|
||||
|
||||
|
||||
class ExportRepairQueries:
|
||||
"""SQL queries for export repairs (PostgreSQL schema)."""
|
||||
|
||||
@staticmethod
|
||||
def build_aggregated_query(db_name: str, where_str: str, discharge_clause: str = "") -> str:
|
||||
"""Build optimized query for NORMAL mode (grouped by invoice with totals)."""
|
||||
# Note: discharge_clause temporarily disabled until is_discharged field migrated
|
||||
discharge_filter = "" # Will be: " AND il.is_discharged = true/false" when ready
|
||||
return f"""
|
||||
SELECT
|
||||
ih.invoice_number AS C1,
|
||||
COALESCE(ped.pedimento_number, '') AS C2,
|
||||
TO_CHAR(ih.invoice_date, 'YYYYMMDD') AS C3,
|
||||
CASE WHEN ih.is_updated THEN 'AC' ELSE 'NA' END AS C6,
|
||||
COALESCE(ped.pedimento_code, '') AS C7,
|
||||
COALESCE(ped.regime, '') AS C8,
|
||||
COALESCE(TO_CHAR(log.payment_date, 'YYYYMMDD'), '') AS C11,
|
||||
COALESCE(cmp.remesa::text, '') AS C12,
|
||||
COALESCE(fin.exchange_rate, 0) AS C13,
|
||||
COALESCE(cmp.provider_id::text, '') AS C14,
|
||||
COALESCE(cmp.sold_to_id::text, '') AS C15,
|
||||
COALESCE(cmp.customs_broker_id::text, '') AS C16,
|
||||
COALESCE(ih.purchase_order, '') AS C27,
|
||||
COALESCE(ped.customs_office, '') AS C33,
|
||||
COALESCE(ih.document_type, '') AS C34,
|
||||
ih.id AS C35,
|
||||
COALESCE(cmp.edocument, '') AS C40,
|
||||
COALESCE(cmp.vucem_operation_num, '') AS C41,
|
||||
COALESCE(fin.exchange_rate, 0) AS C48,
|
||||
COALESCE(TO_CHAR(ih.invoice_date, 'YYYYMMDD'), '') AS C49,
|
||||
COALESCE(ih.capture_user, '') AS C50,
|
||||
COALESCE(ih.who_updated, '') AS C51,
|
||||
COALESCE(log.carrier_id, '') AS C52,
|
||||
COALESCE(log.transport_id || ' ' || log.transport_num, '') AS C53,
|
||||
COALESCE(SUM(CASE WHEN COALESCE(fil.is_subitem, false) THEN 0 ELSE lf.value_usd END), 0) AS total_me,
|
||||
COALESCE(SUM(CASE WHEN COALESCE(fil.is_subitem, false) THEN 0 ELSE lf.value_mxn END), 0) AS total_mn
|
||||
FROM a76.invoice_header ih
|
||||
LEFT JOIN a76.invoice_compliance_mx cmp ON cmp.invoice_id = ih.id
|
||||
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.items i ON i.invoice_id = ih.id
|
||||
LEFT JOIN a76.item_lines il ON il.item_id = i.id
|
||||
LEFT JOIN a24.fa_item_lines fil ON fil.id = il.id
|
||||
LEFT JOIN a76.item_line_financials lf ON lf.item_line_id = il.id
|
||||
WHERE ih.operation_type = 'exp'
|
||||
AND ih.invoice_type = 'REP'
|
||||
{"AND " + where_str if where_str else ""}
|
||||
GROUP BY ih.id, ih.invoice_number, ped.pedimento_number, ped.pedimento_code, ped.regime,
|
||||
log.payment_date, cmp.remesa, fin.exchange_rate, cmp.provider_id, cmp.sold_to_id,
|
||||
cmp.customs_broker_id, ih.purchase_order, ped.customs_office, ih.document_type,
|
||||
cmp.edocument, cmp.vucem_operation_num, ih.invoice_date, ih.capture_user,
|
||||
ih.who_updated, log.carrier_id, log.transport_id, log.transport_num
|
||||
ORDER BY ih.invoice_number
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_main_query(db_name: str, where_str: str) -> str:
|
||||
"""Build main SQL query for export repair data."""
|
||||
return f"""
|
||||
SELECT
|
||||
ih.invoice_number AS C1,
|
||||
COALESCE(ped.pedimento_number, '') AS C2,
|
||||
TO_CHAR(ih.invoice_date, 'YYYYMMDD') AS C3,
|
||||
COALESCE(fin.value_me, 0) AS C4,
|
||||
COALESCE(fin.value_mn, 0) AS C5,
|
||||
CASE WHEN ih.is_updated THEN 'AC' ELSE 'NA' END AS C6,
|
||||
COALESCE(ped.pedimento_code, '') AS C7,
|
||||
COALESCE(ped.regime, '') AS C8,
|
||||
'' AS C9,
|
||||
'' AS C10,
|
||||
COALESCE(TO_CHAR(log.payment_date, 'YYYYMMDD'), '') AS C11,
|
||||
COALESCE(cmp.remesa::text, '') AS C12,
|
||||
COALESCE(fin.exchange_rate, 0) AS C13,
|
||||
COALESCE(cmp.provider_id::text, '') AS C14,
|
||||
COALESCE(cmp.sold_to_id::text, '') AS C15,
|
||||
COALESCE(cmp.customs_broker_id::text, '') AS C16,
|
||||
'' AS C17,
|
||||
COALESCE(cls.class_code, '') AS C18,
|
||||
REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(ld.description_spanish, ''), CHR(44), ' '), CHR(9), ' '), CHR(10), ' '), CHR(13), ' ') AS C19,
|
||||
REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(cls.description_en, ''), CHR(44), ' '), CHR(9), ' '), CHR(10), ' '), CHR(13), ' ') AS C20,
|
||||
COALESCE(lq.quantity, 0) AS C21,
|
||||
COALESCE(il.unit_of_measure, 0) AS C22,
|
||||
COALESCE(lf.customs_value_mxn, 0) AS C23,
|
||||
COALESCE(lf.customs_value_usd, 0) AS C24,
|
||||
COALESCE(lq.net_weight, 0) AS C25,
|
||||
COALESCE(lq.gross_weight, 0) AS C26,
|
||||
COALESCE(ih.purchase_order, '') AS C27,
|
||||
COALESCE(lc.fraction, '') AS C28,
|
||||
COALESCE(lc.fraction_type, '') AS C29,
|
||||
COALESCE(lc.advalorem_numeric, 0) AS C30,
|
||||
COALESCE(lc.sector, '') AS C31,
|
||||
COALESCE(lc.origin_country, '') AS C32,
|
||||
COALESCE(ped.customs_office, '') AS C33,
|
||||
COALESCE(ih.document_type, '') AS C34,
|
||||
ih.id AS C35,
|
||||
COALESCE(lf.value_mxn, 0) AS C36,
|
||||
COALESCE(lf.value_usd, 0) AS C37,
|
||||
'P' AS C38,
|
||||
'' AS C39,
|
||||
COALESCE(cmp.edocument, '') AS C40,
|
||||
COALESCE(cmp.vucem_operation_num, '') AS C41,
|
||||
COALESCE(il.line_number, 0) AS C42,
|
||||
REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(ld.brand, ''), CHR(44), ' '), CHR(9), ' '), CHR(10), ' '), CHR(13), ' ') AS C43,
|
||||
REPLACE(REPLACE(REPLACE(REPLACE(COALESCE(ld.model, ''), CHR(44), ' '), CHR(9), ' '), CHR(10), ' '), CHR(13), ' ') AS C44,
|
||||
COALESCE(cls.us_fraction, '') AS C45,
|
||||
COALESCE(prt.eccn, '') AS C46,
|
||||
COALESCE(il.part_number::text, '') AS C47,
|
||||
COALESCE(fin.exchange_rate, 0) AS C48,
|
||||
COALESCE(TO_CHAR(ih.invoice_date, 'YYYYMMDD'), '') AS C49,
|
||||
COALESCE(ih.capture_user, '') AS C50,
|
||||
COALESCE(ih.who_updated, '') AS C51,
|
||||
COALESCE(log.carrier_id, '') AS C52,
|
||||
COALESCE(log.transport_id || ' ' || log.transport_num, '') AS C53,
|
||||
'' AS C54,
|
||||
COALESCE(ld.lot, '') AS C55,
|
||||
'' AS C56,
|
||||
'' AS C57,
|
||||
'' AS C58,
|
||||
'' AS C59
|
||||
FROM a76.invoice_header ih
|
||||
LEFT JOIN a76.invoice_compliance_mx cmp ON cmp.invoice_id = ih.id
|
||||
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.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
|
||||
LEFT JOIN a76.item_line_quantities lq ON lq.item_line_id = il.id
|
||||
LEFT JOIN a76.item_line_financials lf ON lf.item_line_id = il.id
|
||||
LEFT JOIN a76.item_line_customs lc ON lc.item_line_id = il.id
|
||||
LEFT JOIN a76.classes cls ON cls.id = il.class_id
|
||||
LEFT JOIN a76.parts prt ON prt.id = il.part_number
|
||||
LEFT JOIN a76.units_of_measure um ON um.id = il.unit_of_measure
|
||||
WHERE UPPER(ih.operation_type) IN ('EXP', 'TRA', 'RET')
|
||||
AND UPPER(ih.invoice_type) IN ('DEF', 'REP', 'EXDEF', 'MATDE')
|
||||
AND {where_str}
|
||||
ORDER BY ih.invoice_number, il.line_number
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_totals_query(db_name: str, discharge_clause: str = "") -> str:
|
||||
"""Build query to get totals for an export repair invoice."""
|
||||
discharge_filter = "AND il.is_discharged = true" if "descargado" in discharge_clause.lower() else ""
|
||||
return f"""
|
||||
SELECT
|
||||
COALESCE(SUM(lf.value_usd), 0),
|
||||
COALESCE(SUM(lf.value_mxn), 0)
|
||||
FROM a76.item_line_financials lf
|
||||
INNER JOIN a76.item_lines il ON il.id = lf.item_line_id
|
||||
INNER JOIN a76.items itm ON itm.id = il.item_id
|
||||
WHERE itm.invoice_id = :consecutivo
|
||||
|
||||
{discharge_filter}
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_series_query(db_name: str) -> str:
|
||||
"""Build query to get series information for export repairs."""
|
||||
# TODO: QSeriesExpoRep table not migrated to PostgreSQL yet
|
||||
return """
|
||||
SELECT '' as serie, '' as modelo, '' as parte
|
||||
WHERE 1=0
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def build_driver_badge_query(db_name: str) -> str:
|
||||
"""Build query to get driver badge number for export repairs."""
|
||||
# TODO: GConductor table not migrated to PostgreSQL yet
|
||||
return """
|
||||
SELECT '' as badge
|
||||
WHERE 1=0
|
||||
"""
|
||||
@@ -0,0 +1,391 @@
|
||||
"""
|
||||
Repair import service - handles IMPRE movements.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..schemas import ImportRepairFilter, MovementItem, MovementItemDetailed
|
||||
|
||||
from .base import ConfigHelper, StringHelper
|
||||
from .database_helpers import DatabaseHelper
|
||||
from .exchange_rate import ExchangeRateCalculator
|
||||
from .query_builders import RepairImportQueries
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def parse_yyyymmdd_date(date_str: str) -> Optional[datetime]:
|
||||
"""Parse date string in YYYYMMDD format to datetime."""
|
||||
if not date_str or date_str == '':
|
||||
return None
|
||||
try:
|
||||
return datetime.strptime(date_str, '%Y%m%d')
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
class RepairImportService:
|
||||
"""Service for handling repair import movements (IMPRE)."""
|
||||
|
||||
def get_movements(
|
||||
self,
|
||||
db: Session,
|
||||
filters: "ImportRepairFilter"
|
||||
) -> List["MovementItem"]:
|
||||
"""
|
||||
Get repair import movements (normal mode - grouped by invoice).
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
filters: Filter criteria
|
||||
|
||||
Returns:
|
||||
List of movement items grouped by invoice
|
||||
"""
|
||||
from ..schemas import MovementItem
|
||||
|
||||
try:
|
||||
logger.info(f"Fetching repair import movements with filters: {filters.model_dump()}")
|
||||
|
||||
# Get MetTrans configuration
|
||||
met_trans = ConfigHelper.get_met_trans_config()
|
||||
|
||||
# Build WHERE clause
|
||||
where_clause = self._build_where_clause(filters)
|
||||
|
||||
# Execute optimized aggregated query for NORMAL mode
|
||||
sql = text(RepairImportQueries.build_aggregated_query(
|
||||
filters.database_name,
|
||||
where_clause,
|
||||
filters.discharge_filter.value
|
||||
))
|
||||
results = db.execute(sql).fetchall()
|
||||
logger.info(f"Found {len(results)} repair import invoices")
|
||||
|
||||
movements = []
|
||||
|
||||
for row in results:
|
||||
factura = row[0] # C2 - FacturaImpoRep
|
||||
estatus = row[3] # C5 - Estatus (AC o NA)
|
||||
|
||||
# Filtrar facturas según include_cancelled
|
||||
# Si include_cancelled=False, solo mostrar AC (is_updated=true)
|
||||
# Si include_cancelled=True, mostrar todas (AC y NA)
|
||||
if not filters.include_cancelled and estatus != 'AC':
|
||||
continue
|
||||
|
||||
consecutivo = row[14] # C30 - Consecutivo
|
||||
|
||||
# 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
|
||||
|
||||
# Calculate exchange rate and value
|
||||
valor_comercial, tipo_cambio = ExchangeRateCalculator.calculate_for_aggregated(
|
||||
db=db,
|
||||
db_name=filters.database_name,
|
||||
valor_me=total_me,
|
||||
valor_mn=total_mn,
|
||||
tipo_cambio_db=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)
|
||||
currency_type=filters.currency_type.value,
|
||||
exchange_rate_type=filters.exchange_rate_type.value,
|
||||
is_shelter=filters.is_shelter,
|
||||
use_transport_method=False,
|
||||
met_trans=met_trans
|
||||
)
|
||||
|
||||
# Get pedimento rectification
|
||||
pedimento_r1 = DatabaseHelper.get_rectification_pedimento(
|
||||
db,
|
||||
row[1], # C3 - PedimentoImpoRep
|
||||
'', # PedRectifica not in aggregated query
|
||||
filters.is_shelter
|
||||
)
|
||||
|
||||
# Get driver badge
|
||||
num_gaf_uni = DatabaseHelper.get_driver_badge(
|
||||
db, filters.database_name, factura
|
||||
)
|
||||
|
||||
# Build movement item
|
||||
movement = MovementItem(
|
||||
Factura=factura,
|
||||
Pedimento=row[1], # C3 - PedimentoImpoRep
|
||||
FechaFactura=parse_yyyymmdd_date(row[2]), # C4 - FechaFactura
|
||||
Estatus=row[3], # C5 - Estatus
|
||||
ClavePed=row[4], # C6 - ClavePed
|
||||
TipoMovTemDef='IMPRE',
|
||||
EsCambioRegimen='N',
|
||||
ValorMPTemp=valor_comercial,
|
||||
ValorComercialMN=valor_comercial,
|
||||
TipoCambio=tipo_cambio,
|
||||
ValorAgre=0.0,
|
||||
TipoExpo='',
|
||||
PedimentoR1=pedimento_r1,
|
||||
EDocument=row[15], # C33 - EDocument
|
||||
NumOperacionVU=row[16], # C34 - NumOperacionVU
|
||||
BaseDeDatos=filters.database_name,
|
||||
NumGafUni=num_gaf_uni,
|
||||
UsuarioCap=row[19], # C42 - UsuarioCap
|
||||
UsuarioAcr=row[20], # C43 - UsuarioAct
|
||||
Fecha_Pago=parse_yyyymmdd_date(row[6]), # C9 - Fecha_Pago
|
||||
NumCaja=row[22], # C45 - Transport num
|
||||
Pedimento18='', # Not in aggregated query
|
||||
AduanaCru=row[13], # C29 - customs_office
|
||||
Lote='' # Not in aggregated query
|
||||
)
|
||||
|
||||
movements.append(movement)
|
||||
|
||||
logger.info(f"Successfully processed {len(movements)} repair import movements")
|
||||
return movements
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching repair import movements: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
def get_movements_detailed(
|
||||
self,
|
||||
db: Session,
|
||||
filters: "ImportRepairFilter"
|
||||
) -> List["MovementItemDetailed"]:
|
||||
"""
|
||||
Get repair import movements (detailed mode - line by line).
|
||||
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
filters: Filter criteria
|
||||
|
||||
Returns:
|
||||
List of detailed movement items (one per partida)
|
||||
"""
|
||||
from ..schemas import MovementItemDetailed
|
||||
|
||||
try:
|
||||
logger.info(f"Fetching detailed repair import movements with filters: {filters.model_dump()}")
|
||||
|
||||
# Get MetTrans configuration
|
||||
met_trans = ConfigHelper.get_met_trans_config()
|
||||
|
||||
# Build WHERE clause with discharge filter
|
||||
where_clause = self._build_where_clause(filters)
|
||||
|
||||
# Add discharge filter to WHERE clause
|
||||
discharge_clause = ""
|
||||
if filters.discharge_filter.value == "SiDes":
|
||||
discharge_clause = " AND RepPim.Descarga = 1"
|
||||
elif filters.discharge_filter.value == "NoDes":
|
||||
discharge_clause = " AND RepPim.Descarga = 0"
|
||||
|
||||
where_with_discharge = where_clause + discharge_clause
|
||||
|
||||
# Execute main query
|
||||
sql = text(RepairImportQueries.build_main_query(filters.database_name, where_with_discharge))
|
||||
results = db.execute(sql).fetchall()
|
||||
logger.info(f"Found {len(results)} detailed repair import partidas")
|
||||
|
||||
movements = []
|
||||
|
||||
for row in results:
|
||||
# Skip cancelled if not included
|
||||
if not filters.include_cancelled and row[3] != 'AC': # C4 - Estatus
|
||||
continue
|
||||
|
||||
# Get all detailed information
|
||||
proveedor_info = DatabaseHelper.get_client_info(
|
||||
db, filters.database_name, row[15], is_supplier=True
|
||||
)
|
||||
vendido_info = DatabaseHelper.get_client_info(
|
||||
db, filters.database_name, row[16], is_supplier=False
|
||||
)
|
||||
agente_info = DatabaseHelper.get_customs_agent_info(
|
||||
db, filters.database_name, row[17]
|
||||
)
|
||||
aduana_nombre = DatabaseHelper.get_aduana_seccion_nombre(
|
||||
db, filters.database_name, row[38]
|
||||
)
|
||||
|
||||
# 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],
|
||||
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
|
||||
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
|
||||
)
|
||||
|
||||
simbolo_ex = None
|
||||
if row[49]:
|
||||
simbolo_ex = DatabaseHelper.get_part_export_symbol(
|
||||
db, filters.database_name, row[49], filters.is_shelter
|
||||
)
|
||||
|
||||
pedimento_r1 = DatabaseHelper.get_rectification_pedimento(
|
||||
db, row[1], row[41], filters.is_shelter
|
||||
)
|
||||
|
||||
num_gaf_uni = DatabaseHelper.get_driver_badge(
|
||||
db, filters.database_name, row[0]
|
||||
)
|
||||
|
||||
movement = MovementItemDetailed(
|
||||
Linea=row[44],
|
||||
Factura=row[0],
|
||||
Pedimento=row[1],
|
||||
FechaFactura=row[2],
|
||||
Estatus=row[3],
|
||||
ClavePed=row[4],
|
||||
TipoMovTemDef='IMPRE',
|
||||
EsCambioRegimen='N',
|
||||
Regimen=row[9],
|
||||
Fecha_Inicio=row[10],
|
||||
Fecha_Fin=row[11],
|
||||
Fecha_Pago=row[12],
|
||||
Remesa=row[13],
|
||||
Proveedor=proveedor_info.get('name'),
|
||||
RFCProveedor=proveedor_info.get('rfc'),
|
||||
ProveedorTaxID=proveedor_info.get('tax_id'),
|
||||
VendidoA=vendido_info.get('name'),
|
||||
VendidoARFC=vendido_info.get('rfc'),
|
||||
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],
|
||||
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],
|
||||
Aduana=aduana_nombre,
|
||||
Advalorem=row[39],
|
||||
TipoExpo='',
|
||||
PedimentoR1=pedimento_r1,
|
||||
EDocument=row[42],
|
||||
NumOperacionVU=row[43],
|
||||
Series=series_info,
|
||||
Marca=StringHelper.clean_text(row[45]),
|
||||
Modelo=StringHelper.clean_text(row[46]),
|
||||
FraccionAmericana=row[47],
|
||||
ECCN=row[48],
|
||||
SimboloEx=simbolo_ex,
|
||||
FechaEmision=row[51],
|
||||
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]
|
||||
)
|
||||
|
||||
movements.append(movement)
|
||||
|
||||
logger.info(f"Successfully processed {len(movements)} detailed repair import movements")
|
||||
return movements
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching detailed repair import movements: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
def _build_where_clause(self, filters: "ImportRepairFilter") -> str:
|
||||
"""Build WHERE clause for repair imports query."""
|
||||
where_conditions = []
|
||||
|
||||
# STRICT SEPARATION: Only imports for repair
|
||||
where_conditions.append("ih.operation_type = 'imp'")
|
||||
# GOLDEN RULE: If coming from /all, only filter by operation_type
|
||||
if hasattr(filters, 'discharge_filter') and filters.discharge_filter == 'ALL':
|
||||
# ALL mode: bring all imports without filtering by specific invoice_type
|
||||
pass
|
||||
else:
|
||||
where_conditions.append("ih.invoice_type IN ('REP', 'MATREP')")
|
||||
|
||||
# Date range filter
|
||||
if filters.range_type.value == "FF":
|
||||
where_conditions.append(f"ih.invoice_date >= TO_DATE('{filters.start_date}', 'YYYYMMDD') AND ih.invoice_date <= TO_DATE('{filters.end_date}', 'YYYYMMDD')")
|
||||
else:
|
||||
where_conditions.append(f"log.payment_date >= TO_DATE('{filters.start_date}', 'YYYYMMDD') AND log.payment_date <= TO_DATE('{filters.end_date}', 'YYYYMMDD')")
|
||||
|
||||
# Note: Status filter applied at Python level after CASE WHEN in SELECT
|
||||
# because is_updated doesn't directly represent AC/NA status
|
||||
|
||||
# Provider filter
|
||||
if filters.provider:
|
||||
where_conditions.append(f"cmp.provider_id = {filters.provider}")
|
||||
|
||||
# Buyer filter
|
||||
if filters.buyer:
|
||||
where_conditions.append(f"cmp.sold_to_id = {filters.buyer}")
|
||||
|
||||
# Pedimento code filter
|
||||
if filters.pedimento_code:
|
||||
where_conditions.append(f"ped.pedimento_code = '{filters.pedimento_code}'")
|
||||
|
||||
return " AND ".join(where_conditions)
|
||||
|
||||
def _calculate_totals(self, db: Session, db_name: str, consecutivo: int, discharge_filter: str) -> tuple:
|
||||
"""Calculate totals for main partidas with discharge filter."""
|
||||
# Build discharge clause
|
||||
# Note: is_discharged field not yet migrated to PostgreSQL schema
|
||||
discharge_clause = ""
|
||||
# Temporarily disabled until schema migration:
|
||||
# if discharge_filter == "SiDes":
|
||||
# discharge_clause = " AND il.is_discharged = true"
|
||||
# elif discharge_filter == "NoDes":
|
||||
# discharge_clause = " AND il.is_discharged = false"
|
||||
|
||||
sql = text(f"""
|
||||
SELECT
|
||||
COALESCE(SUM(lf.value_usd), 0),
|
||||
COALESCE(SUM(lf.value_mxn), 0)
|
||||
FROM a76.item_line_financials lf
|
||||
INNER JOIN a76.item_lines il ON il.id = lf.item_line_id
|
||||
INNER JOIN a76.items itm ON itm.id = il.item_id
|
||||
WHERE itm.invoice_id = :consecutivo
|
||||
AND il.is_subitem = false
|
||||
{discharge_clause}
|
||||
""")
|
||||
|
||||
result = db.execute(sql, {"consecutivo": consecutivo}).fetchone()
|
||||
total_me = float(result[0]) if result and result[0] is not None else 0.0
|
||||
total_mn = float(result[1]) if result and result[1] is not None else 0.0
|
||||
|
||||
return total_me, total_mn
|
||||
@@ -0,0 +1,386 @@
|
||||
"""
|
||||
Temporary import service - handles IMTEM movements.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List, Optional, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..schemas import ImportTemporaryFilter, MovementItem, MovementItemDetailed
|
||||
|
||||
from .base import ConfigHelper, StringHelper
|
||||
from .database_helpers import DatabaseHelper
|
||||
from .exchange_rate import ExchangeRateCalculator
|
||||
from .query_builders import TemporaryImportQueries
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def parse_yyyymmdd_date(date_str: str) -> Optional[datetime]:
|
||||
"""Parse date string in YYYYMMDD format to datetime."""
|
||||
if not date_str or date_str == '':
|
||||
return None
|
||||
try:
|
||||
return datetime.strptime(date_str, '%Y%m%d')
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
|
||||
class TemporaryImportService:
|
||||
"""Service for handling temporary import movements (IMTEM)."""
|
||||
|
||||
def get_movements(
|
||||
self,
|
||||
db: Session,
|
||||
filters: "ImportTemporaryFilter"
|
||||
) -> List["MovementItem"]:
|
||||
"""
|
||||
Get temporary import movements (normal mode - grouped by invoice).
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
filters: Filter criteria
|
||||
|
||||
Returns:
|
||||
List of movement items grouped by invoice
|
||||
"""
|
||||
from ..schemas import MovementItem
|
||||
|
||||
try:
|
||||
logger.info(f"Fetching temporary import movements with filters: {filters.model_dump()}")
|
||||
|
||||
# Get MetTrans configuration
|
||||
met_trans = ConfigHelper.get_met_trans_config()
|
||||
|
||||
# Build WHERE clause
|
||||
where_clause = self._build_where_clause(filters)
|
||||
|
||||
# Execute optimized aggregated query for NORMAL mode (GROUP BY with totals)
|
||||
sql = text(TemporaryImportQueries.build_aggregated_query(filters.database_name, where_clause))
|
||||
results = db.execute(sql).fetchall()
|
||||
logger.info(f"Found {len(results)} temporary import invoices")
|
||||
|
||||
movements = []
|
||||
|
||||
for row in results:
|
||||
factura = row[0] # C1 - FacturaImpo
|
||||
estatus = row[3] # C4 - Estatus (AC o NA)
|
||||
|
||||
# Filtrar facturas según include_cancelled
|
||||
# Si include_cancelled=False, solo mostrar AC (is_updated=true)
|
||||
# Si include_cancelled=True, mostrar todas (AC y NA)
|
||||
if not filters.include_cancelled and estatus != 'AC':
|
||||
continue
|
||||
|
||||
consecutivo = row[15] # C39 - Consecutivo
|
||||
|
||||
# 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
|
||||
|
||||
# Calculate exchange rate and value
|
||||
valor_comercial, tipo_cambio = ExchangeRateCalculator.calculate_for_aggregated(
|
||||
db=db,
|
||||
db_name=filters.database_name,
|
||||
valor_me=total_me,
|
||||
valor_mn=total_mn,
|
||||
tipo_cambio_db=row[19], # C50 - TipoCambio
|
||||
fecha_pago=row[8], # C13 - Fecha_Pago
|
||||
fecha_inicio=row[6], # C11 - Fecha_Inicio
|
||||
tipo_pedimento=row[26], # C58 - TIPOPEDIMENTOTRANSPORTEE
|
||||
currency_type=filters.currency_type.value,
|
||||
exchange_rate_type=filters.exchange_rate_type.value,
|
||||
is_shelter=filters.is_shelter,
|
||||
use_transport_method=False, # Not used for temporary imports
|
||||
met_trans=met_trans
|
||||
)
|
||||
|
||||
# Get pedimento rectification
|
||||
pedimento_r1 = DatabaseHelper.get_rectification_pedimento(
|
||||
db,
|
||||
row[1], # C2 - PedimentoImpo
|
||||
row[16], # C41 - PedRectifica
|
||||
filters.is_shelter
|
||||
)
|
||||
|
||||
# Get driver badge
|
||||
num_gaf_uni = DatabaseHelper.get_driver_badge(
|
||||
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,
|
||||
Pedimento=row[1], # C2 - PedimentoImpo
|
||||
FechaFactura=parse_yyyymmdd_date(row[2]), # C3 - FechaFactura
|
||||
Estatus=row[3], # C4 - Estatus
|
||||
ClavePed=row[4], # C5 - ClavePed
|
||||
TipoMovTemDef='IMTEM',
|
||||
EsCambioRegimen='N',
|
||||
ValorMPTemp=valor_comercial,
|
||||
ValorComercialMN=valor_comercial,
|
||||
TipoCambio=tipo_cambio,
|
||||
ValorAgre=0.0,
|
||||
TipoExpo='',
|
||||
PedimentoR1=pedimento_r1,
|
||||
EDocument=row[17], # C42 - EDocument
|
||||
NumOperacionVU=row[18], # C43 - NumOperacionVU
|
||||
BaseDeDatos=filters.database_name,
|
||||
NumGafUni=num_gaf_uni,
|
||||
UsuarioCap=row[21], # C52 - UsuarioCap
|
||||
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
|
||||
AduanaCru=row[14], # C38 - Aduana_Cruce
|
||||
Lote=row[25] # C57 - LOTE
|
||||
)
|
||||
|
||||
movements.append(movement)
|
||||
|
||||
logger.info(f"Successfully processed {len(movements)} temporary import movements")
|
||||
return movements
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching temporary import movements: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
def get_movements_detailed(
|
||||
self,
|
||||
db: Session,
|
||||
filters: "ImportTemporaryFilter"
|
||||
) -> List["MovementItemDetailed"]:
|
||||
"""
|
||||
Get temporary import movements (detailed mode - line by line).
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
filters: Filter criteria
|
||||
|
||||
Returns:
|
||||
List of detailed movement items (one per partida)
|
||||
"""
|
||||
from ..schemas import MovementItemDetailed
|
||||
|
||||
try:
|
||||
logger.info(f"Fetching detailed temporary import movements with filters: {filters.model_dump()}")
|
||||
|
||||
# Get MetTrans configuration
|
||||
met_trans = ConfigHelper.get_met_trans_config()
|
||||
|
||||
# Build WHERE clause
|
||||
where_clause = self._build_where_clause(filters)
|
||||
|
||||
# Execute main query
|
||||
sql = text(TemporaryImportQueries.build_main_query(filters.database_name, where_clause))
|
||||
results = db.execute(sql).fetchall()
|
||||
logger.info(f"Found {len(results)} detailed temporary import partidas")
|
||||
|
||||
movements = []
|
||||
|
||||
for row in results:
|
||||
# Skip cancelled if not included
|
||||
if not filters.include_cancelled and row[3] != 'AC': # C4 - Estatus
|
||||
continue
|
||||
|
||||
# Get provider information
|
||||
proveedor_info = DatabaseHelper.get_client_info(
|
||||
db, filters.database_name, row[15], is_supplier=True # C16 - Proveedor
|
||||
)
|
||||
|
||||
# Get buyer information
|
||||
vendido_info = DatabaseHelper.get_client_info(
|
||||
db, filters.database_name, row[16], is_supplier=False # C17 - VendidoA
|
||||
)
|
||||
|
||||
# Get customs agent information
|
||||
agente_info = DatabaseHelper.get_customs_agent_info(
|
||||
db, filters.database_name, row[17] # C18 - AAduanal
|
||||
)
|
||||
|
||||
# Get customs section name
|
||||
aduana_nombre = DatabaseHelper.get_aduana_seccion_nombre(
|
||||
db, filters.database_name, row[37] # C38 - Aduana_Cruce
|
||||
)
|
||||
|
||||
# Calculate values (only for main partidas 'P', not subpartidas 'S')
|
||||
valor_comercial, tipo_cambio = ExchangeRateCalculator.calculate_exchange_rate_and_value(
|
||||
db=db,
|
||||
db_name=filters.database_name,
|
||||
es_subpartida=row[39], # C40 - EsSubPartida
|
||||
valor_me=row[26], # C27 - ValorImpoME
|
||||
valor_mn_direct=row[24], # C25 - ValorImpoMN
|
||||
fecha_pago=row[12], # C13 - Fecha_Pago
|
||||
fecha_inicio=row[10], # C11 - Fecha_Inicio
|
||||
clave_ped=row[57], # C58 - TIPOPEDIMENTOTRANSPORTEE
|
||||
tipo_cambio_partida=row[49], # C50 - TipoCambio
|
||||
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': # C40 - EsSubPartida
|
||||
peso_neto = float(row[28]) if row[28] else 0.0 # C29
|
||||
peso_bruto = float(row[29]) if row[29] else 0.0 # C30
|
||||
else:
|
||||
peso_neto = 0.0
|
||||
peso_bruto = 0.0
|
||||
|
||||
# Get series information
|
||||
series_info = DatabaseHelper.get_series_info(
|
||||
db, filters.database_name, row[38], row[43], filters.is_shelter # C39, C44
|
||||
)
|
||||
|
||||
# Get part export symbol
|
||||
simbolo_ex = None
|
||||
if row[48]: # C49 - NumParte
|
||||
simbolo_ex = DatabaseHelper.get_part_export_symbol(
|
||||
db, filters.database_name, row[48], filters.is_shelter
|
||||
)
|
||||
|
||||
# Get pedimento rectification
|
||||
pedimento_r1 = DatabaseHelper.get_rectification_pedimento(
|
||||
db,
|
||||
row[1], # C2 - PedimentoImpo
|
||||
row[40], # C41 - PedRectifica
|
||||
filters.is_shelter
|
||||
)
|
||||
|
||||
# Get driver badge
|
||||
num_gaf_uni = DatabaseHelper.get_driver_badge(
|
||||
db, filters.database_name, row[0] # C1 - FacturaImpo
|
||||
)
|
||||
|
||||
# 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
|
||||
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
|
||||
Proveedor=proveedor_info.get('name'),
|
||||
RFCProveedor=proveedor_info.get('rfc'),
|
||||
ProveedorTaxID=proveedor_info.get('tax_id'),
|
||||
VendidoA=vendido_info.get('name'),
|
||||
VendidoARFC=vendido_info.get('rfc'),
|
||||
VendidoATaxID=vendido_info.get('tax_id'),
|
||||
AgenteAduanal=agente_info.get('name'),
|
||||
Patente=agente_info.get('license'),
|
||||
NumParte=row[19], # C20 - Clase (NumParte)
|
||||
DescripcionE=StringHelper.clean_text(row[20]), # C21
|
||||
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,
|
||||
TipoCambio=tipo_cambio,
|
||||
PesoNeto=peso_neto,
|
||||
PesoBruto=peso_bruto,
|
||||
OrdenCompraVenta=row[30], # C31 - OrdenCompra
|
||||
FraccionArancelaria=row[31], # C32 - Fraccion
|
||||
Preferencia=row[32], # C33 - TipoFraccion
|
||||
Sector=row[34], # C35 - Sector
|
||||
PaisOrigen=row[36], # C37 - PaisOrigen
|
||||
Aduana=aduana_nombre,
|
||||
Advalorem=row[39], # C40 - EsSubPartida
|
||||
TipoExpo='',
|
||||
PedimentoR1=pedimento_r1,
|
||||
EDocument=row[41], # C42 - EDocument
|
||||
NumOperacionVU=row[42], # C43 - NumOperacionVU
|
||||
Series=series_info,
|
||||
Marca=StringHelper.clean_text(row[44]), # C45
|
||||
Modelo=StringHelper.clean_text(row[45]), # C46
|
||||
FraccionAmericana=row[46], # C47 - FraccionAme
|
||||
ECCN=row[47], # C48 - ECCN
|
||||
SimboloEx=simbolo_ex,
|
||||
FechaEmision=row[50], # C51 - FechaEmision
|
||||
BaseDeDatos=filters.database_name,
|
||||
NumGafUni=num_gaf_uni,
|
||||
UsuarioCap=row[51], # C52 - UsuarioCap
|
||||
UsuarioAcr=row[52], # C53 - UsuarioAct
|
||||
Transportista=row[53], # C54 - Transportista
|
||||
NumCaja=row[54], # C55 - Transporte + NumTrasporte
|
||||
Pedimento18=row[55], # C56 - Pedimento18
|
||||
AduanaCru=row[37], # C38 - Aduana_Cruce
|
||||
Lote=row[56] # C57 - LOTE
|
||||
)
|
||||
|
||||
movements.append(movement)
|
||||
|
||||
logger.info(f"Successfully processed {len(movements)} detailed temporary import movements")
|
||||
return movements
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching detailed temporary import movements: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
def _build_where_clause(self, filters: "ImportTemporaryFilter") -> str:
|
||||
"""Build WHERE clause for temporary imports query using PostgreSQL tables."""
|
||||
where_conditions = []
|
||||
|
||||
# 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')")
|
||||
|
||||
# Date range filter
|
||||
if filters.range_type.value == "FF":
|
||||
where_conditions.append(f"ih.invoice_date >= TO_DATE('{filters.start_date}', 'YYYYMMDD') AND ih.invoice_date <= TO_DATE('{filters.end_date}', 'YYYYMMDD')")
|
||||
else:
|
||||
where_conditions.append(f"log.payment_date >= TO_DATE('{filters.start_date}', 'YYYYMMDD') AND log.payment_date <= TO_DATE('{filters.end_date}', 'YYYYMMDD')")
|
||||
|
||||
# Note: Status filter applied at Python level after CASE WHEN in SELECT
|
||||
# because is_updated doesn't directly represent AC/NA status
|
||||
|
||||
# Provider filter
|
||||
if filters.provider:
|
||||
where_conditions.append(f"cmp.provider_id::text = '{filters.provider}'")
|
||||
|
||||
# Buyer filter
|
||||
if filters.buyer:
|
||||
where_conditions.append(f"cmp.sold_to_id::text = '{filters.buyer}'")
|
||||
|
||||
# Pedimento code filter
|
||||
if filters.pedimento_code:
|
||||
where_conditions.append(f"ped.pedimento_code = '{filters.pedimento_code}'")
|
||||
|
||||
return " AND ".join(where_conditions) if where_conditions else "1=1"
|
||||
|
||||
def _calculate_totals(self, db: Session, db_name: str, consecutivo: int) -> tuple:
|
||||
"""Calculate totals for main partidas only using PostgreSQL.
|
||||
|
||||
Only sums partidas where is_subpartida is false (equivalent to EsSubpartida = 'P' in Clarion).
|
||||
"""
|
||||
sql = text("""
|
||||
SELECT
|
||||
COALESCE(SUM(lf.value_usd), 0),
|
||||
COALESCE(SUM(lf.value_mxn), 0)
|
||||
FROM a76.item_line_financials lf
|
||||
JOIN a76.item_lines il ON il.id = lf.item_line_id
|
||||
JOIN a76.items i ON i.id = il.item_id
|
||||
WHERE i.invoice_id = :consecutivo
|
||||
AND COALESCE(il.is_subpartida, false) = false
|
||||
""")
|
||||
|
||||
result = db.execute(sql, {"consecutivo": consecutivo}).fetchone()
|
||||
total_me = float(result[0]) if result and result[0] is not None else 0.0
|
||||
total_mn = float(result[1]) if result and result[1] is not None else 0.0
|
||||
|
||||
return total_me, total_mn
|
||||
File diff suppressed because it is too large
Load Diff
@@ -51,6 +51,7 @@ from api.v1.modules.public.reference_data.material_types.routes import router as
|
||||
# --- NUEVO IMPORT PARA REPORTES DE FACTURAS ---
|
||||
from .reports.importacion.facturas.routes import router as invoices_reports_router
|
||||
from .reports.importacion.consolidados.routes import router as consolidated_reports_router
|
||||
from .reports.movements.invoices.routes import router as invoice_movements_router
|
||||
|
||||
|
||||
# Router principal
|
||||
@@ -130,4 +131,10 @@ router.include_router(
|
||||
consolidated_reports_router,
|
||||
prefix="/a76/reports/importacion/consolidados",
|
||||
tags=["a76 / reports"]
|
||||
)
|
||||
|
||||
router.include_router(
|
||||
invoice_movements_router,
|
||||
prefix="/a76/reports/movements/invoices",
|
||||
tags=["a76 / reports / movements"]
|
||||
)
|
||||
184
frontend/src/lib/api/dashboard/a76/invoice-movements.ts
Normal file
184
frontend/src/lib/api/dashboard/a76/invoice-movements.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* API Client para Reportes de Movimientos de Facturas
|
||||
*/
|
||||
import { api } from '$lib/api';
|
||||
|
||||
// ===== TYPES =====
|
||||
|
||||
export type RangeType = 'FF' | 'FP'; // FF = fecha factura, FP = fecha pago
|
||||
export type ReportType = 'Normal' | 'Detallado';
|
||||
export type CurrencyType = 'ME' | 'MN'; // ME = moneda extranjera, MN = moneda nacional
|
||||
export type ExchangeRateType = 'FP' | 'FF';
|
||||
export type MovementTypeFilter = 'COMEX' | 'IMPDF' | 'ALL';
|
||||
export type DischargeFilter = 'SiDes' | 'NoDes' | 'ALL';
|
||||
export type ExportMovementType = 'AFIJO' | 'NODES' | 'SCRAP' | 'REEXP' | 'DONAC' | 'VEMEX' | 'ALL';
|
||||
|
||||
export interface BaseFilter {
|
||||
range_type: RangeType;
|
||||
start_date: string; // YYYYMMDD format
|
||||
end_date: string; // YYYYMMDD format
|
||||
include_cancelled: boolean;
|
||||
provider?: string | null;
|
||||
buyer?: string | null;
|
||||
pedimento_code?: string | null;
|
||||
report_type: ReportType;
|
||||
currency_type: CurrencyType;
|
||||
exchange_rate_type: ExchangeRateType;
|
||||
is_shelter: boolean;
|
||||
database_name: string;
|
||||
}
|
||||
|
||||
export interface ImportTemporaryFilter extends BaseFilter {}
|
||||
|
||||
export interface ImportDefinitiveFilter extends BaseFilter {
|
||||
movement_type: MovementTypeFilter;
|
||||
}
|
||||
|
||||
export interface ImportRepairFilter extends BaseFilter {
|
||||
discharge_filter: DischargeFilter;
|
||||
}
|
||||
|
||||
export interface ExportFilter extends BaseFilter {
|
||||
movement_type: ExportMovementType;
|
||||
discharge_filter: DischargeFilter;
|
||||
use_transport_method: boolean;
|
||||
}
|
||||
|
||||
export interface ExportRepairFilter extends Omit<BaseFilter, 'use_transport_method'> {
|
||||
movement_type: ExportMovementType;
|
||||
discharge_filter: DischargeFilter;
|
||||
}
|
||||
|
||||
export interface AllMovementsFilter {
|
||||
range_type: RangeType;
|
||||
start_date: string; // YYYYMMDD format
|
||||
end_date: string; // YYYYMMDD format
|
||||
include_cancelled: boolean;
|
||||
provider?: string | null;
|
||||
buyer?: string | null;
|
||||
pedimento_code?: string | null;
|
||||
currency_type: CurrencyType;
|
||||
exchange_rate_type: ExchangeRateType;
|
||||
is_shelter: boolean;
|
||||
operation_type?: 'imp' | 'exp' | null;
|
||||
}
|
||||
|
||||
export interface MovementItem {
|
||||
Factura: string;
|
||||
Pedimento: string;
|
||||
FechaFactura: string | null;
|
||||
Estatus: string;
|
||||
ClavePed: string;
|
||||
TipoMovTemDef: string;
|
||||
EsCambioRegimen: string;
|
||||
ValorMPTemp: number;
|
||||
ValorComercialMN: number;
|
||||
TipoCambio: number;
|
||||
ValorAgre: number;
|
||||
TipoExpo: string;
|
||||
PedimentoR1: string | null;
|
||||
EDocument: string | null;
|
||||
NumOperacionVU: string | null;
|
||||
BaseDeDatos: string;
|
||||
NumGafUni: string | null;
|
||||
UsuarioCap: string | null;
|
||||
UsuarioAcr: string | null;
|
||||
Fecha_Pago: string | null;
|
||||
NumCaja: string | null;
|
||||
Pedimento18: string | null;
|
||||
AduanaCru: string | null;
|
||||
Lote: string | null;
|
||||
}
|
||||
|
||||
export interface MovementItemDetailed extends MovementItem {
|
||||
Linea: number;
|
||||
Regimen: string | null;
|
||||
Fecha_Inicio: string | null;
|
||||
Fecha_Fin: string | null;
|
||||
Remesa: string | null;
|
||||
Proveedor: string | null;
|
||||
RFCProveedor: string | null;
|
||||
ProveedorTaxID: string | null;
|
||||
VendidoA: string | null;
|
||||
VendidoARFC: string | null;
|
||||
VendidoATaxID: string | null;
|
||||
AgenteAduanal: string | null;
|
||||
Patente: string | null;
|
||||
NumParte: string | null;
|
||||
DescripcionE: string | null;
|
||||
DescripcionI: string | null;
|
||||
CantidadIE: number;
|
||||
UniMed: string | null;
|
||||
PesoNeto: number;
|
||||
PesoBruto: number;
|
||||
OrdenCompraVenta: string | null;
|
||||
FraccionArancelaria: string | null;
|
||||
Preferencia: string | null;
|
||||
Sector: string | null;
|
||||
PaisOrigen: string | null;
|
||||
Aduana: string | null;
|
||||
Advalorem: string | null;
|
||||
Series: string | null;
|
||||
Marca: string | null;
|
||||
Modelo: string | null;
|
||||
FraccionAmericana: string | null;
|
||||
ECCN: string | null;
|
||||
SimboloEx: string | null;
|
||||
FechaEmision: string | null;
|
||||
Transportista: string | null;
|
||||
}
|
||||
|
||||
// ===== API METHODS =====
|
||||
|
||||
export const invoiceMovementsApi = {
|
||||
// Temporary Imports
|
||||
getTemporaryImports: (filters: ImportTemporaryFilter) =>
|
||||
api.post<MovementItem[]>('/v1/a76/reports/movements/invoices/temporary', filters),
|
||||
|
||||
getTemporaryImportsDetailed: (filters: ImportTemporaryFilter) =>
|
||||
api.post<MovementItemDetailed[]>(
|
||||
'/v1/a76/reports/movements/invoices/temporary-detailed',
|
||||
filters
|
||||
),
|
||||
|
||||
// Definitive Imports
|
||||
getDefinitiveImports: (filters: ImportDefinitiveFilter) =>
|
||||
api.post<MovementItem[]>('/v1/a76/reports/movements/invoices/definitive', filters),
|
||||
|
||||
getDefinitiveImportsDetailed: (filters: ImportDefinitiveFilter) =>
|
||||
api.post<MovementItemDetailed[]>(
|
||||
'/v1/a76/reports/movements/invoices/definitive-detailed',
|
||||
filters
|
||||
),
|
||||
|
||||
// Repair Imports
|
||||
getRepairImports: (filters: ImportRepairFilter) =>
|
||||
api.post<MovementItem[]>('/v1/a76/reports/movements/invoices/repair', filters),
|
||||
|
||||
getRepairImportsDetailed: (filters: ImportRepairFilter) =>
|
||||
api.post<MovementItemDetailed[]>(
|
||||
'/v1/a76/reports/movements/invoices/repair-detailed',
|
||||
filters
|
||||
),
|
||||
|
||||
// Exports
|
||||
getExports: (filters: ExportFilter) =>
|
||||
api.post<MovementItem[]>('/v1/a76/reports/movements/invoices/export', filters),
|
||||
|
||||
getExportsDetailed: (filters: ExportFilter) =>
|
||||
api.post<MovementItemDetailed[]>('/v1/a76/reports/movements/invoices/export-detailed', filters),
|
||||
|
||||
// Export Repairs
|
||||
getExportRepairs: (filters: ExportRepairFilter) =>
|
||||
api.post<MovementItem[]>('/v1/a76/reports/movements/invoices/export-repair', filters),
|
||||
|
||||
getExportRepairsDetailed: (filters: ExportRepairFilter) =>
|
||||
api.post<MovementItemDetailed[]>(
|
||||
'/v1/a76/reports/movements/invoices/export-repair-detailed',
|
||||
filters
|
||||
),
|
||||
|
||||
// All Movements
|
||||
getAllMovements: (filters: AllMovementsFilter) =>
|
||||
api.post<MovementItem[]>('/v1/a76/reports/movements/invoices/all', filters)
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
BadgeCheck,
|
||||
ChartPie,
|
||||
Database,
|
||||
FileSearch,
|
||||
FileText,
|
||||
Frame,
|
||||
GalleryVerticalEnd,
|
||||
@@ -384,6 +385,17 @@ export function getSidebarData(): SidebarData {
|
||||
icon: BadgeCheck,
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: "Reportes",
|
||||
url: "#",
|
||||
icon: FileSearch,
|
||||
items: [
|
||||
{
|
||||
title: "Facturas Impo/Expo",
|
||||
url: "/dashboard/reports/invoices",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: m["sidebar.reference_data.configuracion"](),
|
||||
url: "#",
|
||||
|
||||
6
frontend/src/lib/components/ui/icons/FolderIcon.svelte
Normal file
6
frontend/src/lib/components/ui/icons/FolderIcon.svelte
Normal file
@@ -0,0 +1,6 @@
|
||||
<script lang="ts">
|
||||
export let className: string = '';
|
||||
</script>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class={className}>
|
||||
<path d="M2.75 4A1.75 1.75 0 0 0 1 5.75v8.5A1.75 1.75 0 0 0 2.75 16h14.5A1.75 1.75 0 0 0 19 14.25V7.75A1.75 1.75 0 0 0 17.25 6H9.914a.75.75 0 0 1-.53-.22l-1.414-1.414A1.75 1.75 0 0 0 6.086 4H2.75z"/>
|
||||
</svg>
|
||||
@@ -57,7 +57,7 @@
|
||||
</header>
|
||||
<div class="flex flex-1 flex-col gap-4 p-4 pt-0">
|
||||
<!-- Contenido de cada página -->
|
||||
{@render children()}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</Sidebar.Inset>
|
||||
</Sidebar.Provider>
|
||||
|
||||
@@ -203,7 +203,7 @@
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
error = e.message || "Error al guardar";
|
||||
toast.error(error);
|
||||
toast.error(error || "Error al guardar");
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { getAuthTokens } from '$lib/server/api';
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies }) => {
|
||||
const { accessToken } = getAuthTokens(cookies);
|
||||
|
||||
if (!accessToken) {
|
||||
throw redirect(302, '/login');
|
||||
}
|
||||
|
||||
return {
|
||||
title: 'Reportes de Facturas'
|
||||
};
|
||||
};
|
||||
1394
frontend/src/routes/dashboard/reports/invoices/+page.svelte
Normal file
1394
frontend/src/routes/dashboard/reports/invoices/+page.svelte
Normal file
File diff suppressed because it is too large
Load Diff
157
pnpm-lock.yaml
generated
157
pnpm-lock.yaml
generated
@@ -1,157 +0,0 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
lucide-svelte:
|
||||
specifier: ^0.552.0
|
||||
version: 0.552.0(svelte@5.43.2)
|
||||
|
||||
packages:
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||
|
||||
'@jridgewell/remapping@2.3.5':
|
||||
resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2':
|
||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5':
|
||||
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@sveltejs/acorn-typescript@1.0.6':
|
||||
resolution: {integrity: sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==}
|
||||
peerDependencies:
|
||||
acorn: ^8.9.0
|
||||
|
||||
'@types/estree@1.0.8':
|
||||
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
|
||||
|
||||
acorn@8.15.0:
|
||||
resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
aria-query@5.3.2:
|
||||
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
axobject-query@4.1.0:
|
||||
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
clsx@2.1.1:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
esm-env@1.2.2:
|
||||
resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==}
|
||||
|
||||
esrap@2.1.2:
|
||||
resolution: {integrity: sha512-DgvlIQeowRNyvLPWW4PT7Gu13WznY288Du086E751mwwbsgr29ytBiYeLzAGIo0qk3Ujob0SDk8TiSaM5WQzNg==}
|
||||
|
||||
is-reference@3.0.3:
|
||||
resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==}
|
||||
|
||||
locate-character@3.0.0:
|
||||
resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
|
||||
|
||||
lucide-svelte@0.552.0:
|
||||
resolution: {integrity: sha512-zynJ64KOsuQG3I4tSqfvvl7Kc9x4mWkppbxsuyrbegQwma9HFhBp4aE6HuQNF4c3pS0AHWHki5CAMs5m3QXA5w==}
|
||||
peerDependencies:
|
||||
svelte: ^3 || ^4 || ^5.0.0-next.42
|
||||
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
svelte@5.43.2:
|
||||
resolution: {integrity: sha512-ro1umEzX8rT5JpCmlf0PPv7ncD8MdVob9e18bhwqTKNoLjS8kDvhVpaoYVPc+qMwDAOfcwJtyY7ZFSDbOaNPgA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
zimmerframe@1.1.4:
|
||||
resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
|
||||
'@jridgewell/remapping@2.3.5':
|
||||
dependencies:
|
||||
'@jridgewell/gen-mapping': 0.3.13
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2': {}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
dependencies:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@sveltejs/acorn-typescript@1.0.6(acorn@8.15.0)':
|
||||
dependencies:
|
||||
acorn: 8.15.0
|
||||
|
||||
'@types/estree@1.0.8': {}
|
||||
|
||||
acorn@8.15.0: {}
|
||||
|
||||
aria-query@5.3.2: {}
|
||||
|
||||
axobject-query@4.1.0: {}
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
esm-env@1.2.2: {}
|
||||
|
||||
esrap@2.1.2:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
is-reference@3.0.3:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
|
||||
locate-character@3.0.0: {}
|
||||
|
||||
lucide-svelte@0.552.0(svelte@5.43.2):
|
||||
dependencies:
|
||||
svelte: 5.43.2
|
||||
|
||||
magic-string@0.30.21:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
svelte@5.43.2:
|
||||
dependencies:
|
||||
'@jridgewell/remapping': 2.3.5
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
'@sveltejs/acorn-typescript': 1.0.6(acorn@8.15.0)
|
||||
'@types/estree': 1.0.8
|
||||
acorn: 8.15.0
|
||||
aria-query: 5.3.2
|
||||
axobject-query: 4.1.0
|
||||
clsx: 2.1.1
|
||||
esm-env: 1.2.2
|
||||
esrap: 2.1.2
|
||||
is-reference: 3.0.3
|
||||
locate-character: 3.0.0
|
||||
magic-string: 0.30.21
|
||||
zimmerframe: 1.1.4
|
||||
|
||||
zimmerframe@1.1.4: {}
|
||||
Reference in New Issue
Block a user