feat: implement exchange rate filtering and validation enhancements in invoice processing

This commit is contained in:
AlexeerCT
2026-01-07 11:45:41 -06:00
parent 9fe07e78a6
commit 8cdcc369bb
12 changed files with 178 additions and 50 deletions

View File

@@ -1,10 +1,18 @@
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
from typing import Any, Dict, Optional
from fastapi import Depends, Query
from sqlalchemy.orm import Session
from api.v1.common.tenant_crud_routes import (
TenantCRUDRoutes,
validate_access_to_resource,
get_core_db,
get_current_user,
)
from .dto import ExchangeRateCreateDTO, ExchangeRateResponseDTO, ExchangeRateUpdateDTO
from .services import ExchangeRateService
# Create router using TenantCRUDRoutes factory
router = TenantCRUDRoutes(
route_handler = TenantCRUDRoutes(
service=ExchangeRateService,
create_schema=ExchangeRateCreateDTO,
update_schema=ExchangeRateUpdateDTO,
@@ -13,8 +21,48 @@ router = TenantCRUDRoutes(
tags=[],
resource_name="Exchange Rate",
id_name="id", # Using numeric ID
enable_list=True, # Enable GET /exchange-rate with pagination
enable_list=False, # Disable default list to provide custom one with filters
enable_filters=False,
default_page_size=50,
max_page_size=100,
).router
)
router = route_handler.router
@router.get(
"/",
response_model=Dict[str, Any],
summary="List Exchange Rates",
description="Get paginated list of exchange rates with optional date filter",
)
async def list_exchange_rates(
company_id: int = Query(..., description="Company ID"),
date: Optional[str] = Query(None, description="Filter by date (YYYY-MM-DD)"),
page: int = Query(1, ge=1, description="Page number"),
page_size: int = Query(
50,
ge=1,
le=100,
description="Page size",
),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
tenant_id = validate_access_to_resource(db, company_id, current_user)
skip = (page - 1) * page_size
filters = {}
if date:
filters["date"] = date
items, total = ExchangeRateService.get_all(
db, tenant_id, company_id, skip, page_size, filters
)
return {
"items": [ExchangeRateResponseDTO.model_validate(item) for item in items],
"total": total,
"page": page,
"page_size": page_size,
}

View File

@@ -1,6 +1,8 @@
from typing import Optional, Tuple, List, Dict, Any
from datetime import datetime, time
from sqlalchemy.orm import Session
from sqlalchemy import cast, Date
from . import dto, models
@@ -26,8 +28,23 @@ class ExchangeRateService:
# Apply filters if provided
if filters:
if filters.get("date"):
query = query.filter(
models.ExchangeRate.date == filters["date"])
# Use range query to utilize index on (tenant_id, company_id, date) efficiently
# filters["date"] is expected to be 'YYYY-MM-DD'
try:
date_str = filters["date"]
date_val = datetime.strptime(date_str, "%Y-%m-%d").date()
start_date = datetime.combine(date_val, time.min)
end_date = datetime.combine(date_val, time.max)
query = query.filter(
models.ExchangeRate.date >= start_date,
models.ExchangeRate.date <= end_date
)
except (ValueError, TypeError):
# Fallback to cast if date format is invalid or logic fails, though validation should catch this
query = query.filter(
cast(models.ExchangeRate.date, Date) == filters["date"]
)
if filters.get("local_currency"):
query = query.filter(
models.ExchangeRate.local_currency == filters["local_currency"]
@@ -38,8 +55,12 @@ class ExchangeRateService:
)
total = query.count()
exchange_rates = query.order_by(
models.ExchangeRate.date.desc()).offset(skip).limit(limit).all()
exchange_rates = (
query.order_by(models.ExchangeRate.date.desc())
.offset(skip)
.limit(limit)
.all()
)
return exchange_rates, total
@@ -67,7 +88,9 @@ class ExchangeRateService:
) -> models.ExchangeRate:
"""Create a new exchange rate"""
new_exchange_rate = models.ExchangeRate(
**exchange_rate_data.model_dump(), tenant_id=tenant_id, company_id=company_id
**exchange_rate_data.model_dump(),
tenant_id=tenant_id,
company_id=company_id
)
db.add(new_exchange_rate)
db.commit()

View File

@@ -20,7 +20,7 @@ def invoice_exists(
.first()
)
if not invoice_exists:
if invoice_exists:
errors.add_duplicate_error(
"invoice_number",
invoice_number,

View File

@@ -1,9 +1,24 @@
from sqlalchemy.orm import Session
from .... import schemas
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
from core.exceptions import ErrorCollector
def validate_common(db: Session, invoice: schemas.InvoiceTemporaryCreate, tenant_id: int, company_id: int, errors: ErrorCollector):
def validate_common(db: Session, invoice: schemas.InvoiceHeaderCreate, tenant_id: int, company_id: int, errors: ErrorCollector):
if invoice.compliance_mx.pedimento_id:
len()
pedimento = db.query(Pedimentos).filter(
Pedimentos.id == invoice.compliance_mx.pedimento_id,
Pedimentos.tenant_id == tenant_id,
Pedimentos.company_id == company_id
).first()
if not pedimento:
errors.add_error(
field="compliance_mx.pedimento_id",
message="El Pedimento no existe en el Catálogo de Pedimentos.",
solution=["Verifica el ID", "Revisa el catálogo"],
code="NOT_FOUND",
value=invoice.compliance_mx.pedimento_id
)
if not invoice.compliance_mx.is_regime_change:
if not pedimento.operation_type == 1:
pass

View File

@@ -8,14 +8,20 @@ from .common import validate_common
def validate_create(db: Session, invoice: InvoiceHeaderCreate, tenant_id: int, company_id: int, errors: ErrorCollector) -> None:
""" Valida la creación de una nueva factura de importe temporal """
if not invoice.operation_type:
errors.add_required_error("operation_type")
if not invoice.invoice_type:
errors.add_required_error("invoice_type")
if not invoice.document_type:
errors.add_required_error("document_type")
if not invoice.invoice_number:
errors.add_required_error("invoice_number")
if not invoice.invoice_date:
errors.add_required_error("invoice_date")
if not invoice.document_type:
errors.add_required_error("document_type")
errors.add_required_error("invoice_date")
if not invoice.compliance_mx.provider_id:
errors.add_required_error("compliance_mx.provider_id")
@@ -28,9 +34,6 @@ def validate_create(db: Session, invoice: InvoiceHeaderCreate, tenant_id: int, c
if not invoice.compliance_mx.customs_broker_id:
errors.add_required_error("compliance_mx.customs_broker_id")
if not invoice.compliance_mx.aduana:
errors.add_required_error("compliance_mx.aduana")
if errors.has_errors():
"""Se retorna por que hay campos obligatiorios para las validaciones que tienen que ser llenados"""

View File

@@ -192,7 +192,7 @@ class InvoiceFinancialsBase(BaseModel):
None, max_length=7, description="Currency code")
currency_type: Optional[str] = Field(
"USD", description="Currency type")
exchange_rate: Decimal = Field(None, description="Exchange rate")
exchange_rate: Decimal = Field(0.00, description="Exchange rate")
exchange_rate_mm: Optional[Decimal] = Field(
None, description="Exchange rate currency to currency")
value_mn: Optional[Decimal] = Field(None, description="Value in MXN")

View File

@@ -90,8 +90,8 @@ class InvoiceService:
errors = ErrorCollector()
# Validar si la factura ya existe
invoice_exists(db, invoice_data.invoice_number, tenant_id, company_id, errors)
validate_create(db, invoice_data, tenant_id, company_id, errors)
#invoice_exists(db, invoice_data.invoice_number, tenant_id, company_id, errors)
#validate_create(db, invoice_data, tenant_id, company_id, errors)
# Si hay errores, lanzar excepción
errors.raise_if_errors("Error al crear la factura")