Merge pull request 'fix: update part_number and component_part_number fields in various schemas and components' (#64) from fix/production into development
Reviewed-on: ADUANASOFT/anexo76#64
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Validaciones para creación de items vía API.
|
||||
"""
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from core.exceptions import ErrorCollector
|
||||
from api.v1.modules.a76.items.line_items.schemas import LineItemCreate
|
||||
@@ -12,11 +13,11 @@ def validate_create(
|
||||
line: LineItemCreate,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
errors: ErrorCollector
|
||||
errors: ErrorCollector,
|
||||
) -> None:
|
||||
"""
|
||||
Validaciones para crear LineItems vía API (actualmente en uso).
|
||||
|
||||
|
||||
Args:
|
||||
db: Sesión de base de datos
|
||||
line: Datos del line item
|
||||
@@ -30,43 +31,34 @@ def validate_create(
|
||||
field="line_number",
|
||||
message="El número de línea es obligatorio",
|
||||
solution="Proporciona un número de línea válido",
|
||||
code="REQUIRED"
|
||||
code="REQUIRED",
|
||||
)
|
||||
|
||||
# 2. Validar part_number_id
|
||||
if not line.part_number_id:
|
||||
errors.add_error(
|
||||
field="part_number_id",
|
||||
message="Part Number ID es obligatorio",
|
||||
solution="Selecciona un número de parte válido del catálogo",
|
||||
code="REQUIRED"
|
||||
)
|
||||
|
||||
# 3. Validar class_id
|
||||
|
||||
# 2. Validar class_id
|
||||
if not line.class_id:
|
||||
errors.add_error(
|
||||
field="class_id",
|
||||
message="Clase (ID) es obligatorio",
|
||||
solution="Selecciona una clasificación válida del catálogo",
|
||||
code="REQUIRED"
|
||||
code="REQUIRED",
|
||||
)
|
||||
|
||||
|
||||
# 4. Validar unit_of_measure
|
||||
if not line.unit_of_measure:
|
||||
errors.add_error(
|
||||
field="unit_of_measure",
|
||||
message="U.M. es obligatorio",
|
||||
solution="Proporciona una unidad de medida válida",
|
||||
code="REQUIRED"
|
||||
code="REQUIRED",
|
||||
)
|
||||
|
||||
|
||||
# 5. Validar quantity.quantity
|
||||
if not line.quantity:
|
||||
errors.add_error(
|
||||
field="quantity",
|
||||
message="Quantity es obligatorio",
|
||||
solution="Proporciona una cantidad válida",
|
||||
code="REQUIRED"
|
||||
code="REQUIRED",
|
||||
)
|
||||
else:
|
||||
# Validar con nombre amigable
|
||||
@@ -75,44 +67,51 @@ def validate_create(
|
||||
field="quantity.quantity",
|
||||
message="Quantity debe ser mayor a cero",
|
||||
solution="Proporciona una cantidad válida",
|
||||
code="INVALID_VALUE" if line.quantity.quantity is not None else "REQUIRED"
|
||||
code=(
|
||||
"INVALID_VALUE"
|
||||
if line.quantity.quantity is not None
|
||||
else "REQUIRED"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# 6. Validar financial.unit_cost
|
||||
if not line.financial:
|
||||
errors.add_error(
|
||||
field="financial",
|
||||
message="Unit Cost es obligatorio",
|
||||
solution="Proporciona el costo unitario del item",
|
||||
code="REQUIRED"
|
||||
code="REQUIRED",
|
||||
)
|
||||
else:
|
||||
has_cost = (
|
||||
line.financial.unit_cost_usd or
|
||||
line.financial.unit_cost_mxn or
|
||||
line.financial.unit_cost_capture
|
||||
line.financial.unit_cost_usd
|
||||
or line.financial.unit_cost_mxn
|
||||
or line.financial.unit_cost_capture
|
||||
)
|
||||
if not has_cost:
|
||||
errors.add_error(
|
||||
field="financial.unit_cost",
|
||||
message="Unit Cost es obligatorio",
|
||||
solution="Proporciona al menos un costo unitario (USD, MXN o captura)",
|
||||
code="REQUIRED"
|
||||
code="REQUIRED",
|
||||
)
|
||||
|
||||
|
||||
# 7. Validar description.description_spanish
|
||||
if line.description:
|
||||
if not line.description.description_spanish or not line.description.description_spanish.strip():
|
||||
if (
|
||||
not line.description.description_spanish
|
||||
or not line.description.description_spanish.strip()
|
||||
):
|
||||
errors.add_error(
|
||||
field="description.description_spanish",
|
||||
message="Description in Spanish es obligatorio",
|
||||
solution="Proporciona una descripción del item en español",
|
||||
code="REQUIRED"
|
||||
code="REQUIRED",
|
||||
)
|
||||
else:
|
||||
errors.add_error(
|
||||
field="description.description_spanish",
|
||||
message="Description in Spanish es obligatorio",
|
||||
solution="Proporciona una descripción del item en español",
|
||||
code="REQUIRED"
|
||||
)
|
||||
code="REQUIRED",
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Validaciones para actualización de items vía API.
|
||||
"""
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from core.exceptions import ErrorCollector
|
||||
from api.v1.modules.a76.items.line_items.schemas import LineItemUpdate
|
||||
@@ -13,12 +14,12 @@ def validate_update(
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
errors: ErrorCollector,
|
||||
invoice_id: int = None
|
||||
invoice_id: int = None,
|
||||
) -> None:
|
||||
"""
|
||||
Validaciones para actualizar LineItems vía API.
|
||||
Incluye todas las validaciones de negocio de Clarion.
|
||||
|
||||
|
||||
Args:
|
||||
db: Sesión de base de datos
|
||||
line: Datos del line item a actualizar
|
||||
@@ -33,36 +34,27 @@ def validate_update(
|
||||
field="line_number",
|
||||
message="El número de línea no puede estar vacío",
|
||||
solution="Proporciona un número de línea válido",
|
||||
code="REQUIRED"
|
||||
code="REQUIRED",
|
||||
)
|
||||
|
||||
# 2. Validar part_number_id si se proporciona
|
||||
if line.part_number_id is not None and not line.part_number_id:
|
||||
errors.add_error(
|
||||
field="part_number_id",
|
||||
message="Part Number ID no puede estar vacío",
|
||||
solution="Selecciona un número de parte válido del catálogo",
|
||||
code="REQUIRED"
|
||||
)
|
||||
|
||||
# 3. Validar class_id si se proporciona
|
||||
|
||||
# 2. Validar class_id si se proporciona
|
||||
if line.class_id is not None and not line.class_id:
|
||||
errors.add_error(
|
||||
field="class_id",
|
||||
message="Clase (ID) no puede estar vacío",
|
||||
solution="Selecciona una clasificación válida del catálogo",
|
||||
code="REQUIRED"
|
||||
code="REQUIRED",
|
||||
)
|
||||
|
||||
|
||||
# 4. Validar unit_of_measure si se proporciona
|
||||
if line.unit_of_measure is not None and not line.unit_of_measure:
|
||||
errors.add_error(
|
||||
field="unit_of_measure",
|
||||
message="U.M. no puede estar vacío",
|
||||
solution="Proporciona una unidad de medida válida",
|
||||
code="REQUIRED"
|
||||
code="REQUIRED",
|
||||
)
|
||||
|
||||
|
||||
# 5. Validar cantidad si se proporciona
|
||||
if line.quantity:
|
||||
# Si se proporciona el objeto quantity, validar que quantity.quantity sea válido
|
||||
@@ -72,7 +64,7 @@ def validate_update(
|
||||
field="quantity.quantity",
|
||||
message="Quantity debe ser mayor a cero",
|
||||
solution="Proporciona una cantidad válida",
|
||||
code="INVALID_VALUE"
|
||||
code="INVALID_VALUE",
|
||||
)
|
||||
else:
|
||||
# Si se proporciona quantity pero quantity.quantity es None, es requerido
|
||||
@@ -80,9 +72,9 @@ def validate_update(
|
||||
field="quantity.quantity",
|
||||
message="Quantity es obligatorio",
|
||||
solution="Proporciona una cantidad mayor a 0",
|
||||
code="REQUIRED"
|
||||
code="REQUIRED",
|
||||
)
|
||||
|
||||
|
||||
# 6. Validar peso neto si se proporciona
|
||||
if line.quantity and line.quantity.net_weight is not None:
|
||||
if line.quantity.net_weight <= 0:
|
||||
@@ -90,25 +82,25 @@ def validate_update(
|
||||
field="quantity.net_weight",
|
||||
message="Net Weight debe ser mayor a cero",
|
||||
solution="Proporciona un peso neto válido",
|
||||
code="INVALID_VALUE"
|
||||
code="INVALID_VALUE",
|
||||
)
|
||||
|
||||
|
||||
# 7. Validar costo unitario si se proporciona financial (excepto subpartidas)
|
||||
if line.financial:
|
||||
is_subitem = line.fa_data and line.fa_data.is_subitem if line.fa_data else False
|
||||
|
||||
|
||||
if not is_subitem:
|
||||
has_cost = (
|
||||
line.financial.unit_cost_usd or
|
||||
line.financial.unit_cost_mxn or
|
||||
line.financial.unit_cost_capture
|
||||
line.financial.unit_cost_usd
|
||||
or line.financial.unit_cost_mxn
|
||||
or line.financial.unit_cost_capture
|
||||
)
|
||||
if not has_cost:
|
||||
errors.add_error(
|
||||
field="financial.unit_cost",
|
||||
message="Unit Cost es obligatorio",
|
||||
solution="Proporciona al menos un costo unitario (USD, MXN o captura)",
|
||||
code="REQUIRED"
|
||||
code="REQUIRED",
|
||||
)
|
||||
# Validar que sean positivos
|
||||
if line.financial.unit_cost_usd is not None:
|
||||
@@ -117,7 +109,7 @@ def validate_update(
|
||||
field="financial.unit_cost_usd",
|
||||
message="Unit Cost (USD) debe ser mayor a cero",
|
||||
solution="Proporciona un costo unitario válido",
|
||||
code="INVALID_VALUE"
|
||||
code="INVALID_VALUE",
|
||||
)
|
||||
if line.financial.unit_cost_mxn is not None:
|
||||
if line.financial.unit_cost_mxn <= 0:
|
||||
@@ -125,7 +117,7 @@ def validate_update(
|
||||
field="financial.unit_cost_mxn",
|
||||
message="Unit Cost (MXN) debe ser mayor a cero",
|
||||
solution="Proporciona un costo unitario válido",
|
||||
code="INVALID_VALUE"
|
||||
code="INVALID_VALUE",
|
||||
)
|
||||
if line.financial.unit_cost_capture is not None:
|
||||
if line.financial.unit_cost_capture <= 0:
|
||||
@@ -133,9 +125,9 @@ def validate_update(
|
||||
field="financial.unit_cost_capture",
|
||||
message="Unit Cost (Captura) debe ser mayor a cero",
|
||||
solution="Proporciona un costo unitario válido",
|
||||
code="INVALID_VALUE"
|
||||
code="INVALID_VALUE",
|
||||
)
|
||||
|
||||
|
||||
# 8. Validar datos aduanales si se proporcionan
|
||||
if line.customs:
|
||||
# Validar país de origen
|
||||
@@ -144,56 +136,65 @@ def validate_update(
|
||||
field="customs.origin_country",
|
||||
message="Origin Country no puede estar vacío",
|
||||
solution="Selecciona el país de origen del item",
|
||||
code="REQUIRED"
|
||||
code="REQUIRED",
|
||||
)
|
||||
|
||||
|
||||
# Validar preferencia arancelaria
|
||||
if line.customs.preference is not None and not line.customs.preference:
|
||||
errors.add_error(
|
||||
field="customs.preference",
|
||||
message="La preferencia arancelaria no puede estar vacía",
|
||||
solution="Selecciona la preferencia arancelaria",
|
||||
code="REQUIRED"
|
||||
code="REQUIRED",
|
||||
)
|
||||
|
||||
|
||||
# Validar formato de pago de impuestos
|
||||
if line.customs.tax_paid:
|
||||
val_tax = line.customs.tax_paid.upper()
|
||||
if val_tax not in ['SI', 'NO', 'S', 'N']:
|
||||
if val_tax not in ["SI", "NO", "S", "N"]:
|
||||
errors.add_error(
|
||||
field="customs.tax_paid",
|
||||
message="El valor de pago de impuesto debe ser SI/NO o S/N",
|
||||
solution="Proporciona un valor válido: SI, NO, S o N",
|
||||
code="INVALID_VALUE"
|
||||
code="INVALID_VALUE",
|
||||
)
|
||||
|
||||
|
||||
# Validar forma de pago si existe
|
||||
if line.customs.payment_form:
|
||||
from api.v1.modules.a76.general_catalogs.forms_of_payment.models import PaymentForm
|
||||
|
||||
payment = db.query(PaymentForm).filter(
|
||||
PaymentForm.code == line.customs.payment_form,
|
||||
PaymentForm.tenant_id == tenant_id
|
||||
).first()
|
||||
|
||||
from api.v1.modules.a76.general_catalogs.forms_of_payment.models import (
|
||||
PaymentForm,
|
||||
)
|
||||
|
||||
payment = (
|
||||
db.query(PaymentForm)
|
||||
.filter(
|
||||
PaymentForm.code == line.customs.payment_form,
|
||||
PaymentForm.tenant_id == tenant_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not payment:
|
||||
errors.add_error(
|
||||
field="customs.payment_form",
|
||||
message=f"La forma de pago '{line.customs.payment_form}' no es válida",
|
||||
solution="Selecciona una forma de pago válida del catálogo",
|
||||
code="INVALID_VALUE"
|
||||
code="INVALID_VALUE",
|
||||
)
|
||||
|
||||
|
||||
# 9. Validar descripción en español si se proporciona
|
||||
if line.description and hasattr(line.description, 'description_spanish'):
|
||||
if line.description.description_spanish is not None and not line.description.description_spanish:
|
||||
if line.description and hasattr(line.description, "description_spanish"):
|
||||
if (
|
||||
line.description.description_spanish is not None
|
||||
and not line.description.description_spanish
|
||||
):
|
||||
errors.add_error(
|
||||
field="description.description_spanish",
|
||||
message="La descripción en español no puede estar vacía",
|
||||
solution="Proporciona una descripción del item en español",
|
||||
code="REQUIRED"
|
||||
code="REQUIRED",
|
||||
)
|
||||
|
||||
|
||||
# 10. Validar subpartidas si se actualizan
|
||||
if line.fa_data and line.fa_data.is_subitem:
|
||||
# Es subpartida, debe tener partida principal
|
||||
@@ -202,30 +203,35 @@ def validate_update(
|
||||
field="fa_data.main_line_id",
|
||||
message="La subpartida debe tener asignada una partida principal",
|
||||
solution="Selecciona la partida principal de esta subpartida",
|
||||
code="REQUIRED"
|
||||
code="REQUIRED",
|
||||
)
|
||||
elif invoice_id:
|
||||
# Validar que la partida principal exista en la misma factura
|
||||
from api.v1.modules.a76.items.line_items.models import LineItem
|
||||
from api.v1.modules.a76.items.models import Item
|
||||
|
||||
parent = db.query(LineItem).join(LineItem.item).filter(
|
||||
LineItem.line_number == line.fa_data.main_line_id,
|
||||
Item.invoice_id == invoice_id,
|
||||
LineItem.company_id == company_id
|
||||
).first()
|
||||
|
||||
|
||||
parent = (
|
||||
db.query(LineItem)
|
||||
.join(LineItem.item)
|
||||
.filter(
|
||||
LineItem.line_number == line.fa_data.main_line_id,
|
||||
Item.invoice_id == invoice_id,
|
||||
LineItem.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not parent:
|
||||
errors.add_error(
|
||||
field="fa_data.main_line_id",
|
||||
message=f"La partida principal {line.fa_data.main_line_id} no existe en esta factura",
|
||||
solution="Verifica el número de la partida principal",
|
||||
code="NOT_FOUND"
|
||||
code="NOT_FOUND",
|
||||
)
|
||||
elif parent.fa_data and parent.fa_data.is_subitem:
|
||||
errors.add_error(
|
||||
field="fa_data.main_line_id",
|
||||
message="La partida principal no puede ser otra subpartida",
|
||||
solution="Selecciona una partida normal como principal",
|
||||
code="INVALID_VALUE"
|
||||
code="INVALID_VALUE",
|
||||
)
|
||||
|
||||
@@ -47,19 +47,15 @@ class LineItemBase(BaseModel):
|
||||
line_number: int = Field(..., description="Line number")
|
||||
|
||||
# Part identification
|
||||
part_number_id: Optional[int] = Field(None, description="Part number")
|
||||
component_part_number_id: Optional[int] = Field(
|
||||
part_number: Optional[int] = Field(None, description="Part number")
|
||||
component_part_number: Optional[int] = Field(
|
||||
None, description="Component part number"
|
||||
)
|
||||
class_id: Optional[int] = Field(None, description="Class code")
|
||||
|
||||
# Unit of measure
|
||||
unit_of_measure: Optional[int] = Field(
|
||||
None, description="Unit of measure"
|
||||
)
|
||||
alternate_unit: Optional[int] = Field(
|
||||
None, description="Alternate unit"
|
||||
)
|
||||
unit_of_measure: Optional[int] = Field(None, description="Unit of measure")
|
||||
alternate_unit: Optional[int] = Field(None, description="Alternate unit")
|
||||
uma_key: Optional[str] = Field(None, max_length=2, description="UMA key")
|
||||
auxiliary_unit: Optional[str] = Field(
|
||||
None, max_length=5, description="Auxiliary unit"
|
||||
|
||||
@@ -33,7 +33,7 @@ from api.v1.modules.a24.fa.fa_item_lines.models import FaLineItem
|
||||
from .models import Item
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader
|
||||
from api.v1.modules.a76.general_catalogs.company.models import Company
|
||||
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -158,79 +158,86 @@ class ItemService:
|
||||
company_id: int,
|
||||
) -> Item:
|
||||
"""Create a new item with all related nested data (multiple lines)"""
|
||||
|
||||
|
||||
# Validaciones con ErrorCollector
|
||||
errors = ErrorCollector()
|
||||
|
||||
|
||||
# Validar que la factura exista y no esté actualizada (si viene invoice_id)
|
||||
invoice = None
|
||||
if item_data.invoice_id:
|
||||
invoice = db.query(InvoiceHeader).filter(
|
||||
InvoiceHeader.id == item_data.invoice_id,
|
||||
InvoiceHeader.tenant_id == tenant_id,
|
||||
InvoiceHeader.company_id == company_id
|
||||
).first()
|
||||
|
||||
invoice = (
|
||||
db.query(InvoiceHeader)
|
||||
.filter(
|
||||
InvoiceHeader.id == item_data.invoice_id,
|
||||
InvoiceHeader.tenant_id == tenant_id,
|
||||
InvoiceHeader.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not invoice:
|
||||
errors.add_error(
|
||||
field="invoice_id",
|
||||
message="La factura especificada no existe",
|
||||
code="NOT_FOUND",
|
||||
value=str(item_data.invoice_id)
|
||||
value=str(item_data.invoice_id),
|
||||
)
|
||||
|
||||
|
||||
# Validar cada line item que se va a crear
|
||||
if item_data.lines:
|
||||
for idx, line_data in enumerate(item_data.lines):
|
||||
# Convertir a LineItemCreate para validar
|
||||
line_create = LineItemCreate(**line_data.model_dump())
|
||||
|
||||
|
||||
validate_create(db, line_create, tenant_id, company_id, errors)
|
||||
|
||||
|
||||
# Validaciones adicionales específicas del negocio
|
||||
|
||||
|
||||
# Validar apóstrofes en número de parte
|
||||
if line_data.part_number_id and "'" in str(line_data.part_number_id):
|
||||
if line_data.part_number and "'" in str(line_data.part_number):
|
||||
errors.add_error(
|
||||
field=f"lines[{idx}].part_number_id",
|
||||
field=f"lines[{idx}].part_number",
|
||||
message=f"Advertencia: El Número de Parte contiene apóstrofes y serán omitidos",
|
||||
code="WARNING_APOSTROPHE"
|
||||
code="WARNING_APOSTROPHE",
|
||||
)
|
||||
|
||||
|
||||
# Validar tipo de partida
|
||||
if hasattr(line_data, 'item_type'):
|
||||
if hasattr(line_data, "item_type"):
|
||||
tipo_partida = line_data.item_type
|
||||
if tipo_partida and tipo_partida not in ['N', 'S']:
|
||||
if tipo_partida and tipo_partida not in ["N", "S"]:
|
||||
errors.add_error(
|
||||
field=f"lines[{idx}].item_type",
|
||||
message=f"Tipo de partida debe ser 'N' (Normal) o 'S' (Subpartida), recibido: '{tipo_partida}'",
|
||||
code="INVALID_ITEM_TYPE",
|
||||
value=str(tipo_partida)
|
||||
value=str(tipo_partida),
|
||||
)
|
||||
|
||||
|
||||
# Si es subpartida (S), debe tener partida principal
|
||||
if tipo_partida == 'S':
|
||||
if not hasattr(line_data, 'main_line_id') or not line_data.main_line_id:
|
||||
if tipo_partida == "S":
|
||||
if (
|
||||
not hasattr(line_data, "main_line_id")
|
||||
or not line_data.main_line_id
|
||||
):
|
||||
errors.add_error(
|
||||
field=f"lines[{idx}].main_line_id",
|
||||
message="Las subpartidas (tipo 'S') deben tener una partida principal",
|
||||
code="MISSING_MAIN_LINE"
|
||||
code="MISSING_MAIN_LINE",
|
||||
)
|
||||
|
||||
|
||||
# Validar que el line_number sea consecutivo (si se especifica)
|
||||
if hasattr(line_data, 'line_number') and line_data.line_number:
|
||||
if hasattr(line_data, "line_number") and line_data.line_number:
|
||||
expected_line = idx + 1
|
||||
if line_data.line_number != expected_line:
|
||||
errors.add_error(
|
||||
field=f"lines[{idx}].line_number",
|
||||
message=f"Número de línea esperado: {expected_line}, recibido: {line_data.line_number}",
|
||||
code="INVALID_LINE_SEQUENCE",
|
||||
value=str(line_data.line_number)
|
||||
value=str(line_data.line_number),
|
||||
)
|
||||
|
||||
|
||||
# Si hay errores, lanzar excepción ANTES de intentar crear
|
||||
errors.raise_if_errors("Error al crear el item")
|
||||
|
||||
|
||||
try:
|
||||
# Extract lines data
|
||||
lines_data = item_data.lines or []
|
||||
@@ -311,7 +318,9 @@ class ItemService:
|
||||
|
||||
# Create FA data if provided
|
||||
if fa_data:
|
||||
fa_dict = fa_data.model_dump(exclude={"line_item_id"}) # Exclude line_item_id from DTO
|
||||
fa_dict = fa_data.model_dump(
|
||||
exclude={"line_item_id"}
|
||||
) # Exclude line_item_id from DTO
|
||||
fa_dict["id"] = db_line.id # FA table uses same ID as line item
|
||||
fa_dict["tenant_id"] = tenant_id
|
||||
fa_dict["company_id"] = company_id
|
||||
@@ -343,47 +352,54 @@ class ItemService:
|
||||
company_id: int,
|
||||
) -> Item:
|
||||
"""Update an item and optionally its nested data (multiple lines)"""
|
||||
|
||||
|
||||
# Get existing item
|
||||
db_item = ItemService.get_by_id(db, item_id, tenant_id, company_id)
|
||||
if not db_item:
|
||||
raise HTTPException(status_code=404, detail="Item not found")
|
||||
|
||||
|
||||
# Validaciones con ErrorCollector
|
||||
errors = ErrorCollector()
|
||||
|
||||
|
||||
# Si se está actualizando el invoice_id, validar la factura
|
||||
invoice = None
|
||||
if item_data.invoice_id:
|
||||
invoice = db.query(InvoiceHeader).filter(
|
||||
InvoiceHeader.id == item_data.invoice_id,
|
||||
InvoiceHeader.tenant_id == tenant_id,
|
||||
InvoiceHeader.company_id == company_id
|
||||
).first()
|
||||
|
||||
invoice = (
|
||||
db.query(InvoiceHeader)
|
||||
.filter(
|
||||
InvoiceHeader.id == item_data.invoice_id,
|
||||
InvoiceHeader.tenant_id == tenant_id,
|
||||
InvoiceHeader.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not invoice:
|
||||
errors.add_error(
|
||||
field="invoice_id",
|
||||
message="La factura especificada no existe",
|
||||
code="NOT_FOUND",
|
||||
value=str(item_data.invoice_id)
|
||||
value=str(item_data.invoice_id),
|
||||
)
|
||||
else:
|
||||
# Si no se está actualizando invoice_id, obtener la factura actual por invoice_id
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader
|
||||
invoice = db.query(InvoiceHeader).filter(
|
||||
InvoiceHeader.id == db_item.invoice_id
|
||||
).first()
|
||||
|
||||
|
||||
invoice = (
|
||||
db.query(InvoiceHeader)
|
||||
.filter(InvoiceHeader.id == db_item.invoice_id)
|
||||
.first()
|
||||
)
|
||||
|
||||
# Validar cada line item que se va a actualizar
|
||||
if item_data.lines:
|
||||
for idx, line_data in enumerate(item_data.lines):
|
||||
# Si el line tiene ID, es actualización; si no, es creación
|
||||
if hasattr(line_data, 'id') and line_data.id:
|
||||
if hasattr(line_data, "id") and line_data.id:
|
||||
# Buscar el line item existente
|
||||
existing_line = next(
|
||||
(line for line in db_item.lines if line.id == line_data.id),
|
||||
None
|
||||
None,
|
||||
)
|
||||
if existing_line:
|
||||
# Convertir a LineItemUpdate para validar
|
||||
@@ -393,43 +409,46 @@ class ItemService:
|
||||
# Es un nuevo line item, validar como creación
|
||||
line_create = LineItemCreate(**line_data.model_dump())
|
||||
validate_create(db, line_create, tenant_id, company_id, errors)
|
||||
|
||||
|
||||
# Validaciones adicionales específicas del negocio
|
||||
# (Aplican tanto para crear como actualizar)
|
||||
|
||||
|
||||
# Validar apóstrofes en número de parte
|
||||
if line_data.part_number_id and "'" in str(line_data.part_number_id):
|
||||
if line_data.part_number and "'" in str(line_data.part_number):
|
||||
errors.add_error(
|
||||
field=f"lines[{idx}].part_number_id",
|
||||
field=f"lines[{idx}].part_number",
|
||||
message=f"Advertencia: El Número de Parte contiene apóstrofes y serán omitidos",
|
||||
solution=None,
|
||||
code="WARNING_APOSTROPHE"
|
||||
code="WARNING_APOSTROPHE",
|
||||
)
|
||||
|
||||
|
||||
# Validar tipo de partida
|
||||
if hasattr(line_data, 'item_type'):
|
||||
if hasattr(line_data, "item_type"):
|
||||
tipo_partida = line_data.item_type
|
||||
if tipo_partida and tipo_partida not in ['N', 'S']:
|
||||
if tipo_partida and tipo_partida not in ["N", "S"]:
|
||||
errors.add_error(
|
||||
field=f"lines[{idx}].item_type",
|
||||
message=f"Tipo de partida debe ser 'N' (Normal) o 'S' (Subpartida), recibido: '{tipo_partida}'",
|
||||
solution=None,
|
||||
code="INVALID_ITEM_TYPE",
|
||||
value=str(tipo_partida)
|
||||
value=str(tipo_partida),
|
||||
)
|
||||
|
||||
|
||||
# Si es subpartida (S), debe tener partida principal
|
||||
if tipo_partida == 'S':
|
||||
if not hasattr(line_data, 'main_line_id') or not line_data.main_line_id:
|
||||
if tipo_partida == "S":
|
||||
if (
|
||||
not hasattr(line_data, "main_line_id")
|
||||
or not line_data.main_line_id
|
||||
):
|
||||
errors.add_error(
|
||||
field=f"lines[{idx}].main_line_id",
|
||||
message="Las subpartidas (tipo 'S') deben tener una partida principal",
|
||||
solution=None,
|
||||
code="MISSING_MAIN_LINE"
|
||||
code="MISSING_MAIN_LINE",
|
||||
)
|
||||
|
||||
|
||||
# Validar que el line_number sea consecutivo (si se especifica)
|
||||
if hasattr(line_data, 'line_number') and line_data.line_number:
|
||||
if hasattr(line_data, "line_number") and line_data.line_number:
|
||||
expected_line = idx + 1
|
||||
if line_data.line_number != expected_line:
|
||||
errors.add_error(
|
||||
@@ -437,12 +456,12 @@ class ItemService:
|
||||
message=f"Número de línea esperado: {expected_line}, recibido: {line_data.line_number}",
|
||||
solution=None,
|
||||
code="INVALID_LINE_SEQUENCE",
|
||||
value=str(line_data.line_number)
|
||||
value=str(line_data.line_number),
|
||||
)
|
||||
|
||||
|
||||
# Si hay errores, lanzar excepción ANTES de actualizar
|
||||
errors.raise_if_errors("Error al actualizar el item")
|
||||
|
||||
|
||||
try:
|
||||
|
||||
# Extract lines data
|
||||
@@ -519,7 +538,9 @@ class ItemService:
|
||||
|
||||
# Create FA data if provided
|
||||
if fa_data is not None:
|
||||
fa_dict = fa_data.model_dump(exclude_unset=True, exclude={"line_item_id"})
|
||||
fa_dict = fa_data.model_dump(
|
||||
exclude_unset=True, exclude={"line_item_id"}
|
||||
)
|
||||
fa_dict["id"] = db_line.id # FA table uses same ID as line item
|
||||
fa_dict["tenant_id"] = tenant_id
|
||||
fa_dict["company_id"] = company_id
|
||||
|
||||
@@ -125,8 +125,8 @@ export interface LineItem {
|
||||
line_number: number;
|
||||
|
||||
// Identification
|
||||
part_number_id?: string;
|
||||
component_part_number_id?: string;
|
||||
part_number?: string;
|
||||
component_part_number?: string;
|
||||
class_id?: number;
|
||||
identifier?: string;
|
||||
|
||||
@@ -244,7 +244,7 @@ export const itemsApi = {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.get<ItemListResponse>(`/v1/a76/items/invoice/${invoiceId}/items/?${params.toString()}`);
|
||||
return api.get<ItemListResponse>(`/v1/a76/items/invoice/${invoiceId}/items?${params.toString()}`);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -264,7 +264,7 @@ export const itemsApi = {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.post<Item>(`/v1/a76/items/?${params.toString()}`, data);
|
||||
return api.post<Item>(`/v1/a76/items?${params.toString()}`, data);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -274,7 +274,7 @@ export const itemsApi = {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.put<Item>(`/v1/a76/items/${itemId}/?${params.toString()}`, data);
|
||||
return api.put<Item>(`/v1/a76/items/${itemId}?${params.toString()}`, data);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -284,6 +284,6 @@ export const itemsApi = {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.delete(`/v1/a76/items/${itemId}/?${params.toString()}`);
|
||||
return api.delete(`/v1/a76/items/${itemId}?${params.toString()}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -232,7 +232,6 @@
|
||||
const activeCompanyId = companyStore.activeCompany?.id;
|
||||
if (!activeCompanyId) { error = 'No hay una compañía activa seleccionada'; return; }
|
||||
if (!formData.client_id) { error = 'Debe seleccionar un Cliente'; return; }
|
||||
if (!formData.part_number.trim()) { error = 'Número de Parte requerido'; return; }
|
||||
|
||||
loading = true;
|
||||
try {
|
||||
@@ -272,7 +271,7 @@
|
||||
} catch (e: any) {
|
||||
console.error("Submit Error:", e);
|
||||
error = e.message || 'Error al guardar';
|
||||
toast.error(error);
|
||||
toast.error(error!);
|
||||
} finally { loading = false; }
|
||||
}
|
||||
</script>
|
||||
@@ -299,7 +298,7 @@
|
||||
|
||||
{#if error}
|
||||
<div class="p-4 rounded-md bg-destructive/10 text-destructive border border-destructive/20 text-sm font-medium animate-in slide-in-from-top-2">
|
||||
⚠️ {error}
|
||||
⚠️ {error!}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -540,7 +539,7 @@
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-12 gap-6">
|
||||
<div class="md:col-span-4 space-y-2">
|
||||
<Label for="part_number" class="text-base font-semibold required">Número de Parte</Label>
|
||||
<Label for="part_number" class="text-base font-semibold">Número de Parte</Label>
|
||||
<div class="relative">
|
||||
<Package class="absolute left-3 top-2.5 h-5 w-5 text-muted-foreground" />
|
||||
<Input id="part_number" bind:value={formData.part_number} disabled={isEdit} maxlength={50} class="pl-10 text-lg font-mono bg-muted/20" placeholder="Ej: 123-ABC-456"/>
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
<div class="space-y-1">
|
||||
<Label for="num_parte" class="text-xs">Part Number ID: <span class="text-red-500">*</span></Label>
|
||||
<div class="flex gap-1">
|
||||
<Input id="num_parte" type="number" bind:value={lineItem.part_number_id} class="h-7 text-xs" placeholder="ID de número de parte" />
|
||||
<Input id="num_parte" type="number" bind:value={lineItem.part_number} class="h-7 text-xs" placeholder="ID de número de parte" />
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">ID de número de parte existente en catálogo</p>
|
||||
</div>
|
||||
|
||||
@@ -139,8 +139,8 @@
|
||||
lines: [{
|
||||
line_number: 1,
|
||||
// LineItem fields
|
||||
part_number_id: undefined,
|
||||
component_part_number_id: undefined,
|
||||
part_number: undefined,
|
||||
component_part_number: undefined,
|
||||
class_id: undefined,
|
||||
identifier: undefined,
|
||||
unit_of_measure: undefined,
|
||||
@@ -294,8 +294,8 @@
|
||||
};
|
||||
|
||||
// Convert integer fields
|
||||
cleaned.part_number_id = toNumberOrUndefined(cleaned.part_number_id);
|
||||
cleaned.component_part_number_id = toNumberOrUndefined(cleaned.component_part_number_id);
|
||||
cleaned.part_number = toNumberOrUndefined(cleaned.part_number);
|
||||
cleaned.component_part_number = toNumberOrUndefined(cleaned.component_part_number);
|
||||
cleaned.class_id = toNumberOrUndefined(cleaned.class_id);
|
||||
cleaned.unit_of_measure = toNumberOrUndefined(cleaned.unit_of_measure);
|
||||
cleaned.alternate_unit = toNumberOrUndefined(cleaned.alternate_unit);
|
||||
|
||||
Reference in New Issue
Block a user