Mecanismo sort para tablas
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user