Mecanismo sort para tablas

This commit is contained in:
2026-03-27 08:45:51 -05:00
parent d35f02563a
commit d93c4c6b7b
39 changed files with 1724 additions and 1237 deletions

View File

@@ -53,27 +53,28 @@ def validate_create(
if not line.class_id:
errors.add_required_error(field=f"line[{line_number}].class_id")
if not line.quantity.quantity or line.quantity.quantity <= 0:
if not line.quantity or not line.quantity.quantity or line.quantity.quantity <= 0:
errors.add_required_error(field=f"line[{line_number}].quantity.quantity")
# TODO: Añadir validacion SSisGen:CalcularCostoUnitarioEnBaseAValorTotalScaf <-- de la tabla de preferencias de el sistema
# if SSisGen:CalcularCostoUnitarioEnBaseAValorTotalScaf == False:
if fa_data and not fa_data.is_subitem:
if (
not line.financial.unit_cost_capture
not line.financial
or not line.financial.unit_cost_capture
or line.financial.unit_cost_capture <= 0
):
errors.add_required_error(
field=f"line[{line_number}].financial.unit_cost_capture"
)
if not line.quantity.net_weight or line.quantity.net_weight <= 0:
if not line.quantity or not line.quantity.net_weight or line.quantity.net_weight <= 0:
errors.add_required_error(field=f"line[{line_number}].quantity.net_weight")
if not line.customs.origin_country:
if not line.customs or not line.customs.origin_country:
errors.add_required_error(field=f"line[{line_number}].customs.origin_country")
if not line.customs.fraction_type:
if not line.customs or not line.customs.fraction_type:
errors.add_required_error(field=f"line[{line_number}].customs.fraction_type")
# FA-specific validations
@@ -142,6 +143,23 @@ def validate_create(
validate_common(db, line, tenant_id, company_id, errors, line_number)
if not errors.has_errors():
# Ensure nested objects exist for calculations
from ...schemas import (
LineFinancialCreate,
LineQuantityCreate,
LineCustomCreate,
LineDescriptionCreate,
)
if line.financial is None:
line.financial = LineFinancialCreate()
if line.quantity is None:
line.quantity = LineQuantityCreate()
if line.customs is None:
line.customs = LineCustomCreate()
if line.description is None:
line.description = LineDescriptionCreate()
# Obtener la factura para acceder a tipo de cambio, moneda y peso
invoice: InvoiceHeader = (
db.query(InvoiceHeader)

View File

@@ -27,6 +27,23 @@ def validate_update(
validate_common(db, line, tenant_id, company_id, errors, line_number)
if not errors.has_errors():
# Ensure nested objects exist for calculations/partial updates
from ...schemas import (
LineFinancialCreate,
LineQuantityCreate,
LineCustomCreate,
LineDescriptionCreate,
)
if line.financial is None:
line.financial = LineFinancialCreate()
if line.quantity is None:
line.quantity = LineQuantityCreate()
if line.customs is None:
line.customs = LineCustomCreate()
if line.description is None:
line.description = LineDescriptionCreate()
# Obtener la factura para acceder a tipo de cambio, moneda y peso
invoice: InvoiceHeader = (
db.query(InvoiceHeader)

View File

@@ -61,7 +61,7 @@ def validate_common(
code="UNIT_OF_MEASURE_REQUIRED",
)
if not line.customs.fraction:
if not line.customs or not line.customs.fraction:
if not line_item:
if not class_.fraction:
errors.add_error(
@@ -73,7 +73,7 @@ def validate_common(
else:
fraction = class_.fraction
else:
if not line.customs.fraction:
if not line.customs or not line.customs.fraction:
if not class_.fraction:
errors.add_error(
field=f"line[{line_number}].customs.fraction",
@@ -87,7 +87,7 @@ def validate_common(
if line_item:
fraction = line.customs.fraction
if not line.description.description_spanish and not class_.description_es:
if (not line.description or not line.description.description_spanish) and not class_.description_es:
errors.add_error(
field=f"line[{line_number}].description.description_spanish",
message="La descripción en español es obligatoria para la clase especificada.",
@@ -95,7 +95,7 @@ def validate_common(
code="DESCRIPTION_SPANISH_REQUIRED",
)
if not line.description.description_english and not class_.description_en:
if (not line.description or not line.description.description_english) and not class_.description_en:
errors.add_error(
field=f"line[{line_number}].description.description_english",
message="La descripción en inglés es obligatoria para la clase especificada.",
@@ -103,7 +103,7 @@ def validate_common(
code="DESCRIPTION_ENGLISH_REQUIRED",
)
if line.quantity.quantity and line.quantity.quantity <= 0:
if line.quantity and line.quantity.quantity and line.quantity.quantity <= 0:
errors.add_error(
field=f"line[{line_number}].quantity.quantity",
message="La cantidad debe ser mayor a cero.",
@@ -129,7 +129,7 @@ def validate_common(
code="UNIT_OF_MEASURE_NOT_FOUND",
)
if line.quantity.package_id:
if line.quantity and line.quantity.package_id:
package = (
db.query(func.count(Package.id))
.filter(
@@ -161,7 +161,7 @@ def validate_common(
code="PACKAGE_QUANTITY_MUST_BE_GREATER_THAN_ZERO",
)
else:
if line.quantity.package_quantity and (line.quantity.package_quantity > 0 and not line.quantity.package_id):
if line.quantity and line.quantity.package_quantity and (line.quantity.package_quantity > 0 and not line.quantity.package_id):
errors.add_error(
field=f"line[{line_number}].quantity.package_id",
message="El paquete es obligatorio cuando se proporciona la cantidad de paquetes.",
@@ -173,9 +173,9 @@ def validate_common(
fraction_type = None
sector = None
if fraction:
fraction = line.customs.fraction if line.customs.fraction else fraction
fraction = (line.customs.fraction if line.customs.fraction else fraction) if line.customs else fraction
country = line.customs.origin_country
country = line.customs.origin_country if line.customs else None
if line_item and line_item.customs:
country = (
line_item.customs.origin_country
@@ -183,7 +183,7 @@ def validate_common(
else country
)
fraction_type = line.customs.fraction_type.upper()
fraction_type = (line.customs.fraction_type.upper() if line.customs.fraction_type else None) if line.customs else None
if line_item and line_item.customs:
fraction_type = (
line_item.customs.fraction_type
@@ -191,7 +191,7 @@ def validate_common(
else fraction_type
)
sector = line.customs.sector
sector = line.customs.sector if line.customs else None
if line_item and line_item.customs:
sector = line_item.customs.sector if line_item.customs.sector else sector
@@ -210,7 +210,7 @@ def validate_common(
code="ORIGIN_COUNTRY_NOT_FOUND",
)
else:
if fraction_type.strip().upper() not in vars(FractionType).values():
if fraction_type and fraction_type.strip().upper() not in vars(FractionType).values():
valid_types = [
v
for k, v in vars(FractionType).items()
@@ -225,15 +225,16 @@ def validate_common(
code="FRACTION_TYPE_INVALID",
value=fraction_type,
)
else:
if fraction_type.strip().upper() == FractionType.PROSEC and not sector:
elif fraction_type:
ft_upper = fraction_type.strip().upper()
if ft_upper == FractionType.PROSEC and not sector:
errors.add_error(
field=f"line[{line_number}].customs.sector",
message="El sector es obligatorio cuando el tipo de fracción es 'PROSEC'.",
solution=["Proporciona un sector valido."],
code="SECTOR_REQUIRED_FOR_PROSEC",
)
elif fraction_type.strip().upper() != FractionType.PROSEC and sector:
elif ft_upper != FractionType.PROSEC and sector:
errors.add_error(
field=f"line[{line_number}].customs.sector",
message="El sector solo es aplicable cuando el tipo de fracción es 'PROSEC'.",
@@ -242,7 +243,7 @@ def validate_common(
],
code="SECTOR_ONLY_FOR_PROSEC",
)
elif fraction_type.strip().upper() == FractionType.PROSEC and sector:
elif ft_upper == FractionType.PROSEC and sector:
sector_db: Sector = (
db.query(Sector).filter(
Sector.key == sector,

View File

@@ -52,27 +52,28 @@ def validate_create(
if not line.class_id:
errors.add_required_error(field=f"line[{line_number}].class_id")
if not line.quantity.quantity or line.quantity.quantity <= 0:
if not line.quantity or not line.quantity.quantity or line.quantity.quantity <= 0:
errors.add_required_error(field=f"line[{line_number}].quantity.quantity")
# TODO: Añadir validacion SSisGen:CalcularCostoUnitarioEnBaseAValorTotalScaf <-- de la tabla de preferencias de el sistema
# if SSisGen:CalcularCostoUnitarioEnBaseAValorTotalScaf == False:
if fa_data and not fa_data.is_subitem:
if (
not line.financial.unit_cost_capture
not line.financial
or not line.financial.unit_cost_capture
or line.financial.unit_cost_capture <= 0
):
errors.add_required_error(
field=f"line[{line_number}].financial.unit_cost_capture"
)
if not line.quantity.net_weight or line.quantity.net_weight <= 0:
if not line.quantity or not line.quantity.net_weight or line.quantity.net_weight <= 0:
errors.add_required_error(field=f"line[{line_number}].quantity.net_weight")
if not line.customs.origin_country:
if not line.customs or not line.customs.origin_country:
errors.add_required_error(field=f"line[{line_number}].customs.origin_country")
if not line.customs.fraction_type:
if not line.customs or not line.customs.fraction_type:
errors.add_required_error(field=f"line[{line_number}].customs.fraction_type")
# FA-specific validations
@@ -126,6 +127,23 @@ def validate_create(
validate_common(db, line, tenant_id, company_id, errors, line_number)
if not errors.has_errors():
# Ensure nested objects exist for calculations
from ...schemas import (
LineFinancialCreate,
LineQuantityCreate,
LineCustomCreate,
LineDescriptionCreate,
)
if line.financial is None:
line.financial = LineFinancialCreate()
if line.quantity is None:
line.quantity = LineQuantityCreate()
if line.customs is None:
line.customs = LineCustomCreate()
if line.description is None:
line.description = LineDescriptionCreate()
# Obtener la factura para acceder a tipo de cambio, moneda y peso
invoice: InvoiceHeader = (
db.query(InvoiceHeader)

View File

@@ -26,6 +26,23 @@ def validate_update(
validate_common(db, line, tenant_id, company_id, errors, line_number)
if not errors.has_errors():
# Ensure nested objects exist for calculations/partial updates
from ...schemas import (
LineFinancialCreate,
LineQuantityCreate,
LineCustomCreate,
LineDescriptionCreate,
)
if line.financial is None:
line.financial = LineFinancialCreate()
if line.quantity is None:
line.quantity = LineQuantityCreate()
if line.customs is None:
line.customs = LineCustomCreate()
if line.description is None:
line.description = LineDescriptionCreate()
# Obtener la factura para acceder a tipo de cambio, moneda y peso
invoice: InvoiceHeader = (
db.query(InvoiceHeader)

View File

@@ -83,6 +83,8 @@ async def list_items(
None, description="Filter by system origin (SCAF/SCAII)"),
search: Optional[str] = Query(
None, description="Search term for invoice number, reference, order, or guide"),
sort_by: Optional[str] = Query(None, description="Column to sort by"),
sort_order: Optional[str] = Query("asc", regex="^(asc|desc)$", description="Sort order (asc or desc)"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
@@ -107,7 +109,8 @@ async def list_items(
}
items, total = service.get_all(
db, tenant_id, company_id, skip, limit, filters)
db, tenant_id, company_id, skip, limit, filters, sort_by=sort_by, sort_order=sort_order
)
return LineItemListResponse(
total=total,
@@ -168,23 +171,26 @@ async def delete_item(
# ADDITIONAL ENDPOINTS FOR INVOICE
# ============================================================================
@router.get("/invoice/{invoice_id}/items", response_model=LineItemListResponse)
async def get_items_by_invoice(
@router.get("/invoice/{invoice_id}/items/", response_model=LineItemListResponse)
async def list_items_by_invoice(
invoice_id: int = Path(..., description="Invoice ID"),
company_id: int = Query(..., description="Company ID"),
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
skip: int = Query(0, ge=0, description="Number of records to skip"),
limit: int = Query(100, ge=1, le=1000, description="Maximum records to return"),
sort_by: Optional[str] = Query(None, description="Column to sort by"),
sort_order: Optional[str] = Query("asc", regex="^(asc|desc)$", description="Sort order (asc or desc)"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""
Get all items for a specific invoice
List all items for a specific invoice
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
service = ItemService()
items, total = service.get_by_invoice(
db, invoice_id, tenant_id, company_id, skip, limit)
db, invoice_id, tenant_id, company_id, skip, limit, sort_by=sort_by, sort_order=sort_order
)
return LineItemListResponse(
total=total,

View File

@@ -278,6 +278,8 @@ class ItemService:
skip: int = 0,
limit: int = 100,
filters: Optional[dict] = None,
sort_by: Optional[str] = None,
sort_order: Optional[str] = "asc",
) -> Tuple[List[LineItem], int]:
"""Get all items for a tenant/company with pagination and optional filters"""
query = (
@@ -316,6 +318,25 @@ class ItemService:
LineItem.guide_number.ilike(search_term),
)
)
if filters.get("invoice_number"):
# Join with InvoiceHeader to search by invoice_number
query = query.join(InvoiceHeader).filter(
InvoiceHeader.invoice_number.ilike(f"%{filters['invoice_number']}%")
)
# Apply sorting
if sort_by:
# Map sort_by to actual model column if possible
# Note: Some columns might require joins if they are in related models
column = getattr(LineItem, sort_by, None)
if column:
if sort_order == "desc":
query = query.order_by(column.desc())
else:
query = query.order_by(column.asc())
else:
# Default sorting
query = query.order_by(LineItem.line_number.asc())
total = query.count()
items = query.offset(skip).limit(limit).all()
@@ -332,6 +353,8 @@ class ItemService:
company_id: int,
skip: int = 0,
limit: int = 100,
sort_by: Optional[str] = None,
sort_order: Optional[str] = "asc",
) -> Tuple[List[LineItem], int]:
"""Get all items for a specific invoice"""
query = (
@@ -351,6 +374,18 @@ class ItemService:
)
)
# Apply sorting
if sort_by:
column = getattr(LineItem, sort_by, None)
if column:
if sort_order == "desc":
query = query.order_by(column.desc())
else:
query = query.order_by(column.asc())
else:
# Default sorting
query = query.order_by(LineItem.line_number.asc())
total = query.count()
items = query.offset(skip).limit(limit).all()
for item in items:
@@ -494,8 +529,13 @@ class ItemService:
)
except Exception as e:
db.rollback()
logger.error(f"Unexpected error creating LineItem: {e}")
raise HTTPException(status_code=500, detail="Error creating LineItem")
import traceback
error_msg = f"Unexpected error creating LineItem: {str(e)}"
logger.error(f"{error_msg}\n{traceback.format_exc()}")
raise HTTPException(
status_code=500,
detail=f"Error interno al crear la partida: {type(e).__name__}: {str(e)}"
)
@staticmethod
def update(