339 lines
13 KiB
Python
339 lines
13 KiB
Python
import traceback
|
|
from typing import Optional, List, Tuple
|
|
from sqlalchemy.orm import Session
|
|
from core.exceptions import ErrorCollector, DuplicateResourceException
|
|
from .common.mappers import clean_dict
|
|
from .imports.temporary.validators.create import validate_create
|
|
from .imports.temporary.validators.update import validate_update
|
|
from .common.common_validators import invoice_exists
|
|
|
|
from . import models, schemas
|
|
|
|
|
|
class InvoiceService:
|
|
"""Service for Invoice Header operations"""
|
|
|
|
@staticmethod
|
|
def get_by_id(
|
|
db: Session, invoice_id: int, tenant_id: int, company_id: int
|
|
) -> Optional[models.InvoiceHeader]:
|
|
"""Get an invoice by ID with tenant/company validation"""
|
|
return (
|
|
db.query(models.InvoiceHeader)
|
|
.filter(
|
|
models.InvoiceHeader.id == invoice_id,
|
|
models.InvoiceHeader.tenant_id == tenant_id,
|
|
models.InvoiceHeader.company_id == company_id,
|
|
)
|
|
.first()
|
|
)
|
|
|
|
@staticmethod
|
|
def get_all(
|
|
db: Session,
|
|
tenant_id: int,
|
|
company_id: int,
|
|
skip: int = 0,
|
|
limit: int = 100,
|
|
filters: Optional[dict] = None,
|
|
) -> Tuple[List[models.InvoiceHeader], int]:
|
|
"""Get all invoices for a tenant/company with pagination and optional filters"""
|
|
query = db.query(models.InvoiceHeader).filter(
|
|
models.InvoiceHeader.tenant_id == tenant_id,
|
|
models.InvoiceHeader.company_id == company_id,
|
|
)
|
|
|
|
# Apply filters if provided
|
|
if filters:
|
|
if filters.get("status") is not None:
|
|
query = query.filter(models.InvoiceHeader.is_updated == filters["status"])
|
|
if filters.get("operation_type"):
|
|
ot = filters["operation_type"]
|
|
ot_val = ot.value if hasattr(ot, "value") else ot
|
|
query = query.filter(
|
|
models.InvoiceHeader.operation_type == ot_val
|
|
)
|
|
if filters.get("invoice_type"):
|
|
query = query.filter(
|
|
models.InvoiceHeader.invoice_type == filters["invoice_type"]
|
|
)
|
|
if filters.get("invoice_number"):
|
|
query = query.filter(
|
|
models.InvoiceHeader.invoice_number.ilike(
|
|
f"%{filters['invoice_number']}%"
|
|
)
|
|
)
|
|
if filters.get("pedimento"):
|
|
query = query.join(models.InvoiceComplianceMx).filter(
|
|
models.InvoiceComplianceMx.pedimento.ilike(
|
|
f"%{filters['pedimento']}%"
|
|
)
|
|
)
|
|
ot_exp = filters.get("operation_type")
|
|
ot_exp_val = ot_exp.value if hasattr(ot_exp, "value") else ot_exp
|
|
if not filters.get("invoice_type") and ot_exp_val == "exp":
|
|
query = query.filter(models.InvoiceHeader.operation_type != "REPAR")
|
|
|
|
if filters.get("manifest_number"):
|
|
# Avoid duplicate joins if pedimento filter was also applied (though rare in this context)
|
|
# For safety, we can just use the relationship attribute directly if mapped,
|
|
# but explicit join is clearer given the previous pattern.
|
|
# Assuming SQLAlchemy handles the join overlap or we just accept it for now.
|
|
# To be safe and consistent with previous 'pedimento' block:
|
|
query = query.join(models.InvoiceComplianceMx).filter(
|
|
models.InvoiceComplianceMx.manifest_number.ilike(f"%{filters['manifest_number']}%")
|
|
)
|
|
|
|
total = query.count()
|
|
items = query.offset(skip).limit(limit).all()
|
|
return items, total
|
|
|
|
@staticmethod
|
|
def create(
|
|
db: Session,
|
|
invoice_data: schemas.InvoiceHeaderCreate,
|
|
tenant_id: int,
|
|
company_id: int,
|
|
) -> models.InvoiceHeader:
|
|
"""Create a new invoice with all related data"""
|
|
|
|
# Validaciones con ErrorCollector
|
|
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)
|
|
|
|
# Si hay errores, lanzar excepción ANTES de intentar crear
|
|
errors.raise_if_errors("Error al crear la factura")
|
|
|
|
# Extract nested data
|
|
compliance_data = invoice_data.compliance_mx
|
|
financials_data = invoice_data.financials
|
|
logistics_data = invoice_data.logistics
|
|
details_data = invoice_data.details or []
|
|
collections_data = invoice_data.collections or []
|
|
|
|
try:
|
|
|
|
# Create main invoice header
|
|
raw_invoice_dict = invoice_data.model_dump(
|
|
exclude={
|
|
"compliance_mx",
|
|
"financials",
|
|
"logistics",
|
|
"details",
|
|
"collections",
|
|
}
|
|
)
|
|
invoice_dict = clean_dict(raw_invoice_dict)
|
|
invoice_dict["tenant_id"] = tenant_id
|
|
invoice_dict["company_id"] = company_id
|
|
|
|
# Ensure document_type respects DB constraints for MEX invoices (bypass clean_dict)
|
|
if invoice_dict.get("invoice_type") == "MEX" and not invoice_dict.get("document_type"):
|
|
invoice_dict["document_type"] = None
|
|
|
|
new_invoice = models.InvoiceHeader(**invoice_dict)
|
|
|
|
db.add(new_invoice)
|
|
db.flush() # Flush to get the invoice ID
|
|
|
|
# Create compliance_mx if provided
|
|
if compliance_data:
|
|
raw_comp_dict = compliance_data.model_dump()
|
|
compliance_dict = clean_dict(raw_comp_dict)
|
|
|
|
compliance_dict["invoice_id"] = new_invoice.id
|
|
compliance_dict["tenant_id"] = tenant_id
|
|
compliance_dict["company_id"] = company_id
|
|
|
|
new_compliance = models.InvoiceComplianceMx(**compliance_dict)
|
|
db.add(new_compliance)
|
|
|
|
# Create financials if provided
|
|
if financials_data:
|
|
raw_fin_dict = financials_data.model_dump()
|
|
financials_dict = clean_dict(raw_fin_dict)
|
|
|
|
financials_dict["invoice_id"] = new_invoice.id
|
|
financials_dict["tenant_id"] = tenant_id
|
|
financials_dict["company_id"] = company_id
|
|
|
|
new_financials = models.InvoiceFinancials(**financials_dict)
|
|
db.add(new_financials)
|
|
|
|
# Create logistics entries
|
|
if logistics_data:
|
|
raw_log_dict = logistics_data.model_dump()
|
|
logistics_dict = clean_dict(raw_log_dict)
|
|
|
|
logistics_dict["invoice_id"] = new_invoice.id
|
|
logistics_dict["tenant_id"] = tenant_id
|
|
logistics_dict["company_id"] = company_id
|
|
new_logistics = models.InvoiceLogistics(**logistics_dict)
|
|
db.add(new_logistics)
|
|
|
|
# Create sales details
|
|
for detail_item in details_data:
|
|
raw_det_dict = detail_item.model_dump()
|
|
detail_dict = clean_dict(raw_det_dict)
|
|
|
|
detail_dict["invoice_id"] = new_invoice.id
|
|
detail_dict["tenant_id"] = tenant_id
|
|
detail_dict["company_id"] = company_id
|
|
new_detail = models.InvoiceSalesDetails(**detail_dict)
|
|
db.add(new_detail)
|
|
|
|
# Create collections
|
|
for collection_item in collections_data:
|
|
raw_col_dict = collection_item.model_dump()
|
|
collection_dict = clean_dict(raw_col_dict)
|
|
|
|
collection_dict["invoice_id"] = new_invoice.id
|
|
collection_dict["tenant_id"] = tenant_id
|
|
collection_dict["company_id"] = company_id
|
|
new_collection = models.InvoiceCollections(**collection_dict)
|
|
db.add(new_collection)
|
|
|
|
db.commit()
|
|
db.refresh(new_invoice)
|
|
return new_invoice
|
|
|
|
except Exception as e:
|
|
db.rollback()
|
|
print("\n\n🔥 ERROR AL GUARDAR FACTURA 🔥")
|
|
print(f"Error: {str(e)}")
|
|
traceback.print_exc() # Esto imprime el error real en la consola
|
|
print("--------------------------------\n")
|
|
raise e
|
|
|
|
@staticmethod
|
|
def update(
|
|
db: Session,
|
|
invoice_id: int,
|
|
tenant_id: int,
|
|
invoice_data: schemas.InvoiceHeaderUpdate,
|
|
company_id: int,
|
|
) -> Optional[models.InvoiceHeader]:
|
|
"""Update an existing invoice with validation"""
|
|
|
|
# Validaciones con ErrorCollector
|
|
errors = ErrorCollector()
|
|
|
|
# Obtener la factura existente
|
|
invoice = InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
|
|
if not invoice:
|
|
return None
|
|
|
|
# Si se cambió el número de factura, validar que no exista otra con ese número
|
|
if invoice_data.invoice_number and invoice_data.invoice_number != invoice.invoice_number:
|
|
# Verificar que no exista otra factura con el nuevo número
|
|
existing_invoice = (
|
|
db.query(models.InvoiceHeader.id)
|
|
.filter(
|
|
models.InvoiceHeader.invoice_number == invoice_data.invoice_number,
|
|
models.InvoiceHeader.tenant_id == tenant_id,
|
|
models.InvoiceHeader.company_id == company_id,
|
|
models.InvoiceHeader.id != invoice_id, # Excluir la factura actual
|
|
)
|
|
.first()
|
|
)
|
|
|
|
if existing_invoice:
|
|
errors.add_duplicate_error(
|
|
"invoice_number",
|
|
invoice_data.invoice_number,
|
|
f"Ya existe otra factura con el número '{invoice_data.invoice_number}'",
|
|
)
|
|
validate_update(invoice_data, invoice, errors)
|
|
|
|
# Si hay errores, lanzar excepción ANTES de actualizar
|
|
errors.raise_if_errors("Error al actualizar la factura")
|
|
|
|
# Update main invoice header fields
|
|
update_dict = invoice_data.model_dump(
|
|
exclude={
|
|
"compliance_mx",
|
|
"financials",
|
|
"logistics",
|
|
"details",
|
|
"collections",
|
|
},
|
|
exclude_unset=True,
|
|
)
|
|
for key, value in update_dict.items():
|
|
setattr(invoice, key, value)
|
|
|
|
# Update compliance_mx if provided
|
|
if invoice_data.compliance_mx is not None:
|
|
if invoice.compliance_mx:
|
|
for key, value in invoice_data.compliance_mx.model_dump(
|
|
exclude_unset=True
|
|
).items():
|
|
# Parche rápido para update
|
|
if value == "":
|
|
value = None
|
|
setattr(invoice.compliance_mx, key, value)
|
|
else:
|
|
compliance_dict = invoice_data.compliance_mx.model_dump()
|
|
# Aplicar limpieza manual si es necesario
|
|
if "customs_agent" in compliance_dict:
|
|
compliance_dict["customs_broker_id"] = compliance_dict.pop(
|
|
"customs_agent"
|
|
)
|
|
|
|
compliance_dict["invoice_id"] = invoice.id
|
|
compliance_dict["tenant_id"] = tenant_id
|
|
compliance_dict["company_id"] = company_id
|
|
new_compliance = models.InvoiceComplianceMx(**compliance_dict)
|
|
db.add(new_compliance)
|
|
|
|
# Update financials if provided
|
|
if invoice_data.financials is not None:
|
|
if invoice.financials:
|
|
for key, value in invoice_data.financials.model_dump(
|
|
exclude_unset=True
|
|
).items():
|
|
if value == "":
|
|
value = None
|
|
setattr(invoice.financials, key, value)
|
|
else:
|
|
financials_dict = invoice_data.financials.model_dump()
|
|
financials_dict["invoice_id"] = invoice.id
|
|
financials_dict["tenant_id"] = tenant_id
|
|
financials_dict["company_id"] = company_id
|
|
new_financials = models.InvoiceFinancials(**financials_dict)
|
|
db.add(new_financials)
|
|
|
|
# Update logistics if provided
|
|
if invoice_data.logistics is not None:
|
|
if invoice.logistics:
|
|
for key, value in invoice_data.logistics.model_dump(
|
|
exclude_unset=True
|
|
).items():
|
|
if value == "":
|
|
value = None
|
|
setattr(invoice.logistics, key, value)
|
|
else:
|
|
logistics_dict = invoice_data.logistics.model_dump()
|
|
logistics_dict["invoice_id"] = invoice.id
|
|
logistics_dict["tenant_id"] = tenant_id
|
|
logistics_dict["company_id"] = company_id
|
|
new_logistics = models.InvoiceLogistics(**logistics_dict)
|
|
db.add(new_logistics)
|
|
|
|
db.commit()
|
|
db.refresh(invoice)
|
|
return invoice
|
|
|
|
@staticmethod
|
|
def delete(db: Session, invoice_id: int, tenant_id: int, company_id: int) -> bool:
|
|
"""Delete an invoice and all related data (cascade delete)"""
|
|
invoice = InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
|
|
if invoice:
|
|
db.delete(invoice)
|
|
db.commit()
|
|
return True
|
|
return False
|