Merge branch 'development' into current branch and resolve modify/delete conflict in items-tab-form.svelte
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
from typing import Any, Callable, Dict, Generic, Optional, Type, TypeVar, Union
|
||||
import logging
|
||||
import inspect
|
||||
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user, validate_access_to_resource
|
||||
@@ -147,6 +148,8 @@ class TenantCRUDRoutes(
|
||||
le=self.max_page_size,
|
||||
description="Page size",
|
||||
),
|
||||
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(self.db_dependency),
|
||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||
):
|
||||
@@ -175,15 +178,23 @@ class TenantCRUDRoutes(
|
||||
|
||||
# Extraer todos los parámetros de búsqueda dinámicamente
|
||||
# Excluimos los parámetros estándar de paginación y control
|
||||
standard_params = {"company_id", "all_companies", "page", "page_size"}
|
||||
standard_params = {"company_id", "all_companies", "page", "page_size", "sort_by", "sort_order"}
|
||||
filters = {
|
||||
k: v
|
||||
for k, v in request.query_params.items()
|
||||
if k not in standard_params and v is not None and v != ""
|
||||
}
|
||||
|
||||
# Determine what parameters the service method accepts
|
||||
sig = inspect.signature(self.service.get_all)
|
||||
kwargs = {}
|
||||
if "sort_by" in sig.parameters:
|
||||
kwargs["sort_by"] = sort_by
|
||||
if "sort_order" in sig.parameters:
|
||||
kwargs["sort_order"] = sort_order
|
||||
|
||||
items, total = self.service.get_all(
|
||||
db, tenant_id, target_company_id, skip, page_size, filters
|
||||
db, tenant_id, target_company_id, skip, page_size, filters, **kwargs
|
||||
)
|
||||
|
||||
|
||||
@@ -214,6 +225,8 @@ class TenantCRUDRoutes(
|
||||
le=self.max_page_size,
|
||||
description="Page size",
|
||||
),
|
||||
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(self.db_dependency),
|
||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||
):
|
||||
@@ -238,8 +251,16 @@ class TenantCRUDRoutes(
|
||||
|
||||
skip = (page - 1) * page_size
|
||||
|
||||
# Determine what parameters the service method accepts
|
||||
sig = inspect.signature(self.service.get_all)
|
||||
kwargs = {}
|
||||
if "sort_by" in sig.parameters:
|
||||
kwargs["sort_by"] = sort_by
|
||||
if "sort_order" in sig.parameters:
|
||||
kwargs["sort_order"] = sort_order
|
||||
|
||||
items, total = self.service.get_all(
|
||||
db, tenant_id, target_company_id, skip, page_size, None
|
||||
db, tenant_id, target_company_id, skip, page_size, None, **kwargs
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Endpoints API para gestión de clases SCAII y SCAF
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, List
|
||||
from typing import Dict, Any, List, Optional
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
@@ -33,6 +33,8 @@ async def get_classes_with_fa_data(
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
page: int = Query(1, ge=1, description="Page number"),
|
||||
page_size: int = Query(1000, ge=1, le=1000, description="Page size"),
|
||||
sort_by: Optional[str] = Query(None, description="Column to sort by"),
|
||||
sort_order: Optional[str] = Query("asc", description="Sort order (asc/desc)"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
@@ -50,6 +52,8 @@ async def get_classes_with_fa_data(
|
||||
company_id=company_id,
|
||||
skip=skip,
|
||||
limit=page_size,
|
||||
sort_by=sort_by,
|
||||
sort_order=sort_order,
|
||||
)
|
||||
|
||||
return classes_with_fa
|
||||
|
||||
@@ -36,6 +36,8 @@ class ClassService:
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
sort_by: Optional[str] = None,
|
||||
sort_order: Optional[str] = "asc",
|
||||
) -> tuple[List[Class], int]:
|
||||
"""
|
||||
Get all classes for a tenant with pagination and filters
|
||||
@@ -68,6 +70,18 @@ class ClassService:
|
||||
query = query.filter(
|
||||
Class.physical_review == filters["physical_review"]
|
||||
)
|
||||
|
||||
# Apply sorting
|
||||
if sort_by:
|
||||
column = getattr(Class, 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(Class.class_code.asc())
|
||||
|
||||
total = query.count()
|
||||
items = query.offset(skip).limit(limit).all()
|
||||
@@ -82,6 +96,8 @@ class ClassService:
|
||||
skip: int = 0,
|
||||
limit: int = 1000,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
sort_by: Optional[str] = None,
|
||||
sort_order: Optional[str] = "asc",
|
||||
) -> tuple[List[Dict[str, Any]], int]:
|
||||
"""
|
||||
Get all classes with their FA data in a single query using LEFT JOIN.
|
||||
@@ -125,6 +141,23 @@ class ClassService:
|
||||
if filters.get("fraction"):
|
||||
query = query.filter(Class.fraction.ilike(f"%{filters['fraction']}%"))
|
||||
|
||||
# Apply sorting
|
||||
if sort_by:
|
||||
# Check if sort_by belongs to Class or QClasses
|
||||
column = getattr(Class, sort_by, None)
|
||||
if column:
|
||||
if sort_order == "desc":
|
||||
query = query.order_by(column.desc())
|
||||
else:
|
||||
query = query.order_by(column.asc())
|
||||
else:
|
||||
# Handle FA extension fields if sort_by is one of them
|
||||
# (Simple approach for now, assuming base class fields are prioritized)
|
||||
pass
|
||||
else:
|
||||
# Default sorting
|
||||
query = query.order_by(Class.class_code.asc())
|
||||
|
||||
# Count total before pagination
|
||||
total = query.count()
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ class ClientProviderService:
|
||||
skip: int = 0,
|
||||
limit: int = 50,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
sort_by: Optional[str] = None,
|
||||
sort_order: Optional[str] = "asc",
|
||||
) -> Tuple[List[ClientProvider], int]:
|
||||
"""Get all clients/providers for a tenant with pagination"""
|
||||
query = db.query(ClientProvider).filter(ClientProvider.tenant_id == tenant_id)
|
||||
@@ -68,6 +70,17 @@ class ClientProviderService:
|
||||
enabled = 1 if filters["status"] == "enabled" else 0
|
||||
query = query.filter(ClientProvider.is_active == enabled)
|
||||
|
||||
# Apply sorting
|
||||
if sort_by:
|
||||
column = getattr(ClientProvider, sort_by, None)
|
||||
if column:
|
||||
if sort_order == "desc":
|
||||
query = query.order_by(column.desc())
|
||||
else:
|
||||
query = query.order_by(column.asc())
|
||||
else:
|
||||
query = query.order_by(ClientProvider.id.desc())
|
||||
|
||||
total = query.count()
|
||||
clients = (
|
||||
query.options(
|
||||
|
||||
61
backend/api/v1/modules/a76/common/sorting.py
Normal file
61
backend/api/v1/modules/a76/common/sorting.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from typing import Any, Optional, Type
|
||||
from sqlalchemy import asc, desc, inspect
|
||||
from sqlalchemy.orm import Query, RelationshipProperty
|
||||
|
||||
def apply_sorting(
|
||||
query: Query,
|
||||
model: Type[Any],
|
||||
sort_by: Optional[str] = None,
|
||||
sort_desc: bool = True,
|
||||
default_sort_col: str = "created_at"
|
||||
) -> Query:
|
||||
"""
|
||||
Applies dynamic sorting to a SQLAlchemy query.
|
||||
Supports nested attributes via dot notation (e.g., 'compliance_mx.remesa').
|
||||
Automatically handles joins if necessary.
|
||||
"""
|
||||
if not sort_by:
|
||||
if hasattr(model, default_sort_col):
|
||||
col = getattr(model, default_sort_col)
|
||||
return query.order_by(desc(col))
|
||||
return query
|
||||
|
||||
try:
|
||||
parts = sort_by.split('.')
|
||||
current_model = model
|
||||
|
||||
# Traverse relationships if dot notation is used
|
||||
for i, part in enumerate(parts[:-1]):
|
||||
# Check if relationship exists
|
||||
mapper = inspect(current_model)
|
||||
if part in mapper.relationships:
|
||||
rel = mapper.relationships[part]
|
||||
# Join the relationship
|
||||
query = query.join(rel.entity.class_)
|
||||
current_model = rel.entity.class_
|
||||
else:
|
||||
# If part is not a relationship, we can't go deeper
|
||||
# Fallback to default sorting
|
||||
if hasattr(model, default_sort_col):
|
||||
return query.order_by(desc(getattr(model, default_sort_col)))
|
||||
return query
|
||||
|
||||
# The last part is the actual column
|
||||
last_part = parts[-1]
|
||||
if hasattr(current_model, last_part):
|
||||
col = getattr(current_model, last_part)
|
||||
if sort_desc:
|
||||
query = query.order_by(desc(col))
|
||||
else:
|
||||
query = query.order_by(asc(col))
|
||||
else:
|
||||
# Fallback to default sorting on the original model
|
||||
if hasattr(model, default_sort_col):
|
||||
query = query.order_by(desc(getattr(model, default_sort_col)))
|
||||
except Exception as e:
|
||||
# Log error or handle gracefully
|
||||
print(f"Error applying sort for {sort_by}: {e}")
|
||||
if hasattr(model, default_sort_col):
|
||||
query = query.order_by(desc(getattr(model, default_sort_col)))
|
||||
|
||||
return query
|
||||
@@ -26,6 +26,8 @@ class CustomsBrokerService:
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
filters: dict = None,
|
||||
sort_by: str = None,
|
||||
sort_order: str = "asc",
|
||||
):
|
||||
"""Get all customs brokers for a tenant/company with pagination"""
|
||||
query = db.query(models.CustomsBroker).filter(
|
||||
@@ -33,6 +35,23 @@ class CustomsBrokerService:
|
||||
models.CustomsBroker.company_id == company_id,
|
||||
)
|
||||
|
||||
if filters:
|
||||
# Implement filters if needed in the future
|
||||
pass
|
||||
|
||||
# Apply sorting
|
||||
if sort_by:
|
||||
column = getattr(models.CustomsBroker, sort_by, None)
|
||||
if column:
|
||||
if sort_order == "desc":
|
||||
query = query.order_by(column.desc())
|
||||
else:
|
||||
query = query.order_by(column.asc())
|
||||
else:
|
||||
query = query.order_by(models.CustomsBroker.id.desc())
|
||||
else:
|
||||
query = query.order_by(models.CustomsBroker.id.desc())
|
||||
|
||||
total = query.count()
|
||||
items = query.offset(skip).limit(limit).all()
|
||||
return items, total
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Dict, Any
|
||||
from typing import Dict, Any, Optional
|
||||
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user, validate_access_to_resource
|
||||
@@ -95,6 +95,8 @@ def list_invoices(
|
||||
invoice_type: str = Query(None, description="Filter by invoice type"),
|
||||
manifest_number: str = Query(None, description="Filter by manifest number"),
|
||||
pedimento: str = Query(None, description="Filter by pedimento"),
|
||||
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),
|
||||
):
|
||||
@@ -119,7 +121,7 @@ def list_invoices(
|
||||
filters = {k: v for k, v in filters.items() if v is not None}
|
||||
|
||||
items, total = services.InvoiceService.get_all(
|
||||
db, tenant_id, company_id, skip=skip, limit=page_size, filters=filters
|
||||
db, tenant_id, company_id, skip=skip, limit=page_size, filters=filters, sort_by=sort_by, sort_order=sort_order
|
||||
)
|
||||
print(f"DEBUG: InvoiceService returned {len(items)} items, total={total}")
|
||||
|
||||
|
||||
@@ -115,6 +115,8 @@ class InvoiceService:
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
filters: Optional[dict] = None,
|
||||
sort_by: Optional[str] = None,
|
||||
sort_order: Optional[str] = "asc",
|
||||
) -> Tuple[List[models.InvoiceHeader], int]:
|
||||
"""Get all invoices for a tenant/company with pagination and optional filters"""
|
||||
query = db.query(models.InvoiceHeader).filter(
|
||||
@@ -165,6 +167,20 @@ class InvoiceService:
|
||||
models.InvoiceComplianceMx.manifest_number.ilike(f"%{filters['manifest_number']}%")
|
||||
)
|
||||
|
||||
# Apply sorting
|
||||
if sort_by:
|
||||
# Simple column mapping
|
||||
# This can be improved to handle joins if needed
|
||||
column = getattr(models.InvoiceHeader, 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 if none provided
|
||||
query = query.order_by(models.InvoiceHeader.id.desc())
|
||||
|
||||
total = query.count()
|
||||
items = query.offset(skip).limit(limit).all()
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -55,6 +55,8 @@ class PartService:
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
sort_by: Optional[str] = None,
|
||||
sort_order: Optional[str] = "asc",
|
||||
) -> tuple[List[Part], int]:
|
||||
|
||||
query = db.query(Part).filter(Part.tenant_id == tenant_id)
|
||||
@@ -82,6 +84,18 @@ class PartService:
|
||||
)
|
||||
)
|
||||
# Otros filtros...
|
||||
|
||||
# Apply sorting
|
||||
if sort_by:
|
||||
column = getattr(Part, 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: Newest first
|
||||
query = query.order_by(Part.id.desc())
|
||||
|
||||
total = query.count()
|
||||
items = query.offset(skip).limit(limit).all()
|
||||
|
||||
@@ -69,6 +69,8 @@ class PedimentosService:
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
sort_by: Optional[str] = None,
|
||||
sort_order: Optional[str] = "asc",
|
||||
) -> tuple[List[Pedimentos], int]:
|
||||
"""
|
||||
Get all pedimentos for a tenant with pagination and filters
|
||||
@@ -126,12 +128,22 @@ class PedimentosService:
|
||||
selectinload(Pedimentos.pedimento_seals),
|
||||
selectinload(Pedimentos.pedimento_containers),
|
||||
)
|
||||
.order_by(desc(Pedimentos.created_at))
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
# Apply sorting
|
||||
if sort_by:
|
||||
column = getattr(Pedimentos, 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(desc(Pedimentos.created_at))
|
||||
|
||||
items = query.offset(skip).limit(limit).all()
|
||||
|
||||
return items, total
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -262,7 +262,7 @@ BASE_JOINS = """
|
||||
WHERE tenant_id = :tenant_id
|
||||
ORDER BY import_item_line_id, id DESC
|
||||
) lm ON lm.import_item_line_id = il.id
|
||||
LEFT JOIN (
|
||||
JOIN (
|
||||
SELECT
|
||||
import_item_line_id,
|
||||
SUM(
|
||||
@@ -356,6 +356,7 @@ def _query_ped(filters: SaldosFilter) -> tuple:
|
||||
{BASE_JOINS}
|
||||
WHERE ih.tenant_id = :tenant_id
|
||||
AND ih.operation_type = 'imp'
|
||||
AND ent.qty_impo > 0
|
||||
{company_filter}
|
||||
{date_filter}
|
||||
{level_filter}
|
||||
@@ -380,6 +381,7 @@ def _query_fpp(filters: SaldosFilter) -> tuple:
|
||||
{BASE_JOINS}
|
||||
WHERE ih.tenant_id = :tenant_id
|
||||
AND ih.operation_type = 'imp'
|
||||
AND ent.qty_impo > 0
|
||||
{company_filter}
|
||||
{date_filter}
|
||||
{level_filter}
|
||||
@@ -403,6 +405,7 @@ def _query_ffa(filters: SaldosFilter) -> tuple:
|
||||
{BASE_JOINS}
|
||||
WHERE ih.tenant_id = :tenant_id
|
||||
AND ih.operation_type = 'imp'
|
||||
AND ent.qty_impo > 0
|
||||
{company_filter}
|
||||
{date_filter}
|
||||
{level_filter}
|
||||
@@ -434,6 +437,7 @@ def _query_par(filters: SaldosFilter) -> tuple:
|
||||
{BASE_JOINS}
|
||||
WHERE ih.tenant_id = :tenant_id
|
||||
AND ih.operation_type = 'imp'
|
||||
AND ent.qty_impo > 0
|
||||
{company_filter}
|
||||
{id_filter}
|
||||
{date_filter}
|
||||
@@ -466,6 +470,7 @@ def _query_cla(filters: SaldosFilter) -> tuple:
|
||||
{BASE_JOINS}
|
||||
WHERE ih.tenant_id = :tenant_id
|
||||
AND ih.operation_type = 'imp'
|
||||
AND ent.qty_impo > 0
|
||||
{company_filter}
|
||||
{id_filter}
|
||||
{date_filter}
|
||||
|
||||
Reference in New Issue
Block a user