From 8a0c67d4528217aaf24e3019dd30716fd17e2c50 Mon Sep 17 00:00:00 2001 From: hreyes Date: Tue, 17 Mar 2026 09:27:03 -0600 Subject: [PATCH 1/6] WIP: mover cambios desde development --- .../layouts_csv/parts/validators/common.py | 35 + backend/api/v1/modules/a76/parts/dto.py | 12 +- backend/core/error_handlers.py | 20 + .../dashboard/goods/parts/partForm.svelte | 1948 +++++++++-------- 4 files changed, 1106 insertions(+), 909 deletions(-) diff --git a/backend/api/v1/modules/a76/layouts_csv/parts/validators/common.py b/backend/api/v1/modules/a76/layouts_csv/parts/validators/common.py index ef48142f..39aa11ff 100644 --- a/backend/api/v1/modules/a76/layouts_csv/parts/validators/common.py +++ b/backend/api/v1/modules/a76/layouts_csv/parts/validators/common.py @@ -5,6 +5,7 @@ Paridad Clarion: VALIDA_TODA_PARTES (obligatorios A, B, E salvo excepción RFC), from typing import Dict, Any, Optional, Set from ..common.common_validators import check_max_length, check_decimal +from decimal import Decimal, InvalidOperation MSG_NUMPARTE_VACIO = ( @@ -108,9 +109,33 @@ def validate_row_types(row: Dict[str, Any], line_num: int) -> Optional[Dict[str, err = check_decimal(row, "COSTOUNIT", line_num) if err: return err + # No negativos + raw = (row.get("COSTOUNIT") or "").strip() + if raw: + try: + if Decimal(raw) < 0: + return { + "line": line_num, + "col": "COSTOUNIT", + "msg": "Error: (Col. I) El Costo Unitario no puede ser negativo.", + } + except (InvalidOperation, ValueError): + # check_decimal already handles format; ignore here + pass err = check_decimal(row, "PESOUNIT", line_num) if err: return err + raw = (row.get("PESOUNIT") or "").strip() + if raw: + try: + if Decimal(raw) < 0: + return { + "line": line_num, + "col": "PESOUNIT", + "msg": "Error: (Col. K) El Peso Unitario no puede ser negativo.", + } + except (InvalidOperation, ValueError): + pass return None @@ -292,6 +317,16 @@ def validate_row_sector( """Col P (Sector): si O=PROSEC entonces P obligatorio, empresa PROSEC, sector autorizado; si O≠PROSEC y P no vacío error.""" pref = (row.get("PREFERENCIA") or "").strip().upper() sector = (row.get("SECTOR") or "").strip() + # Formato: solo A-Z/0-9 (sin espacios ni especiales), hasta 8 + if sector: + s = sector.strip().upper() + # Alinear con modelo A76: solo dígitos, máx 8 + if not (1 <= len(s) <= 8) or not s.isdigit(): + return { + "line": line_num, + "col": "SECTOR", + "msg": "Error: (Col. P) El Sector contiene caracteres no permitidos. Use solo números (máx. 8 dígitos).", + } if pref == "PROSEC": if not sector: return { diff --git a/backend/api/v1/modules/a76/parts/dto.py b/backend/api/v1/modules/a76/parts/dto.py index 0273bd68..b6bff27d 100644 --- a/backend/api/v1/modules/a76/parts/dto.py +++ b/backend/api/v1/modules/a76/parts/dto.py @@ -1,13 +1,13 @@ from datetime import datetime from decimal import Decimal -from typing import List, Optional +from typing import List, Optional, Literal from pydantic import BaseModel, Field, ConfigDict # --- SUB-DTO: DATOS ADUANALES (FaData) --- class FaDataDTO(BaseModel): - origin_country: Optional[str] = None - sector: Optional[str] = None - fraction_type: Optional[str] = None + origin_country: Optional[str] = Field(default=None, pattern=r"^[A-Z]{3}$") + sector: Optional[str] = Field(default=None, pattern=r"^[0-9]{1,8}$") + fraction_type: Optional[Literal["GENERAL", "PROSEC", "ALADI", "TLCS"]] = None model_config = ConfigDict(from_attributes=True) @@ -70,11 +70,11 @@ class PartBase(BaseModel): part_class: Optional[str] = None unit_of_measure: Optional[str] = "PZ" - unit_cost: Optional[Decimal] = None + unit_cost: Optional[Decimal] = Field(default=None, ge=0) currency_key: Optional[str] = None currency_type: Optional[str] = None - unit_weight: Optional[Decimal] = None + unit_weight: Optional[Decimal] = Field(default=None, ge=0) weight_type: Optional[str] = None fraction: Optional[str] = None diff --git a/backend/core/error_handlers.py b/backend/core/error_handlers.py index 3cbd397c..b9586b8f 100644 --- a/backend/core/error_handlers.py +++ b/backend/core/error_handlers.py @@ -75,6 +75,11 @@ _FIELD_LABELS: Dict[str, str] = { "city": "Ciudad", "state": "Estado", "country": "País", + # Partes (A76) + "unit_cost": "Costo Unitario", + "unit_weight": "Peso Unitario", + "sector": "Sector", + "fraction_type": "Tipo de tarifa", } _FIELD_PATTERN_MESSAGES: Dict[str, str] = { @@ -85,16 +90,31 @@ _FIELD_PATTERN_MESSAGES: Dict[str, str] = { "email": "El correo electrónico no tiene un formato válido. Ejemplo: usuario@dominio.com.", "phone": "El teléfono solo puede contener dígitos, espacios y los símbolos: +, -, (, ).", "contact": "El nombre de contacto contiene caracteres no permitidos. Use solo letras, números y puntuación básica.", + # Partes (A76) + "sector": "El Sector solo puede contener números, máximo 8 dígitos (sin espacios ni caracteres especiales).", } def _friendly_message(field_key: str, error_type: str) -> str: """Devuelve un mensaje de error legible en español según el campo y tipo de error.""" + if error_type in ("greater_than_equal",): + if field_key in ("unit_cost", "unit_weight"): + return f"El campo '{_FIELD_LABELS.get(field_key, field_key)}' no puede ser negativo." + return f"El campo '{_FIELD_LABELS.get(field_key, field_key)}' debe ser mayor o igual a 0." if error_type in ("string_pattern_mismatch", "value_error"): return _FIELD_PATTERN_MESSAGES.get( field_key, f"El campo '{_FIELD_LABELS.get(field_key, field_key)}' contiene un valor con formato inválido.", ) + if error_type in ("decimal_parsing", "decimal_type", "float_parsing", "float_type", "int_parsing", "int_type"): + return ( + f"El campo '{_FIELD_LABELS.get(field_key, field_key)}' debe ser numérico. " + "Si no aplica, déjelo vacío." + ) + if error_type in ("literal_error",): + if field_key == "fraction_type": + return "El campo 'Tipo de tarifa' es inválido. Seleccione una opción predefinida." + return f"El campo '{_FIELD_LABELS.get(field_key, field_key)}' contiene una opción inválida." if error_type == "string_too_long": return f"El campo '{_FIELD_LABELS.get(field_key, field_key)}' excede la longitud máxima permitida." if error_type == "string_too_short": diff --git a/frontend/src/lib/components/dashboard/goods/parts/partForm.svelte b/frontend/src/lib/components/dashboard/goods/parts/partForm.svelte index feb73f3e..ea21fb19 100644 --- a/frontend/src/lib/components/dashboard/goods/parts/partForm.svelte +++ b/frontend/src/lib/components/dashboard/goods/parts/partForm.svelte @@ -1,904 +1,1046 @@ - - -
-
-
-
- -

{title}

- - {isEdit ? "Editar" : "Nueva"} - -
-

- {formType === 'fa' ? 'Gestión de Activo Fijo' : 'Gestión de Inventario'} -

-
-
- - - - {#if error} -
- ⚠️ {error!} -
- {/if} - -
- - {#if formType === 'fa'} -
{ e.preventDefault(); handleSubmit(); }} class="space-y-6"> - - - - -
- - -
-
- -
- - -
-
-
- -
-
- - showClientModal = true} class="pl-9 cursor-pointer hover:bg-muted/50 transition-colors" placeholder="Seleccione un cliente..."/> -
- -
-
-
- -
-
- -