From 8dca19413ba31bba216124601e1b836ff26d50dc Mon Sep 17 00:00:00 2001 From: AlexeerCT Date: Thu, 8 Jan 2026 21:14:08 -0600 Subject: [PATCH] feat: add Fixed Asset Classes management page with CRUD functionality --- backend/api/v1/modules/a76/classes/dto.py | 4 - backend/api/v1/modules/a76/classes/models.py | 13 +- backend/api/v1/modules/a76/classes/routes.py | 23 - backend/api/v1/modules/a76/classes/service.py | 112 +- frontend/src/lib/api/dashboard/a76/classes.ts | 13 +- .../goods/classes/create-edit-dialog.svelte | 48 +- .../classes/forms/FixedAssetClassForm.svelte | 1177 +++++++++++++++++ .../goods/fixed-asset-classes/+page.svelte | 871 ++++++++++++ 8 files changed, 2078 insertions(+), 183 deletions(-) create mode 100644 frontend/src/lib/components/dashboard/goods/classes/forms/FixedAssetClassForm.svelte create mode 100644 frontend/src/routes/dashboard/goods/fixed-asset-classes/+page.svelte diff --git a/backend/api/v1/modules/a76/classes/dto.py b/backend/api/v1/modules/a76/classes/dto.py index 9b5a92a8..7fc3ccc0 100644 --- a/backend/api/v1/modules/a76/classes/dto.py +++ b/backend/api/v1/modules/a76/classes/dto.py @@ -13,7 +13,6 @@ from pydantic import BaseModel, ConfigDict, Field class ClassCreateDTO(BaseModel): """DTO para crear una clase""" - client_id: int = Field(..., description="Client key") class_code: str = Field(..., max_length=8, description="Class code") description_es: str = Field( ..., max_length=500, description="Description in Spanish (required)" @@ -127,7 +126,6 @@ class ClassResponseDTO(BaseModel): id: int tenant_id: int company_id: int - client_id: int class_code: str description_es: Optional[str] = None description_en: Optional[str] = None @@ -164,7 +162,6 @@ class ClassResponseDTOFA(ClassResponseDTO): class ClassBasicDTO(BaseModel): """DTO para información básica de clase""" - client_id: int class_code: str description_es: Optional[str] = None description_en: Optional[str] = None @@ -190,7 +187,6 @@ class ClassListDTO(BaseModel): class ClassSearchDTO(BaseModel): """DTO para búsqueda de clases""" - client_id: Optional[int] = Field(None, description="Filter by client key") class_code: Optional[str] = Field(None, description="Search by class code") description: Optional[str] = Field(None, description="Search in descriptions") material_key: Optional[str] = Field(None, description="Filter by material key") diff --git a/backend/api/v1/modules/a76/classes/models.py b/backend/api/v1/modules/a76/classes/models.py index a5d1c851..58459b11 100644 --- a/backend/api/v1/modules/a76/classes/models.py +++ b/backend/api/v1/modules/a76/classes/models.py @@ -31,9 +31,6 @@ class Class(Base, TenantScopedMixin, TimestampMixin): __tablename__ = "classes" __table_args__ = ( PrimaryKeyConstraint("id", name="classes_pkey"), - ForeignKeyConstraint( - ["client_id"], ["a76.clients_and_providers.id"], name="fk_classes_client" - ), ForeignKeyConstraint( ["material_key"], ["public.material_types.key"], @@ -47,15 +44,13 @@ class Class(Base, TenantScopedMixin, TimestampMixin): UniqueConstraint( "tenant_id", "company_id", - "client_id", "class_code", - name="ufa_classes_client_id_class_code", + name="uq_classes_tenant_company_code", ), {"schema": "a76"}, ) id: Mapped[int] = mapped_column(Integer, primary_key=True) - client_id: Mapped[int] = mapped_column(Integer) # Unique constraint compuesta class_code: Mapped[str] = mapped_column(String(8)) # CLASE @@ -98,11 +93,11 @@ class Class(Base, TenantScopedMixin, TimestampMixin): # Inverse relationship with GParts that have this class parts: Mapped[list["Part"]] = relationship( - primaryjoin="and_(Class.client_id == Part.client_id, Class.class_code == Part.part_class)", - foreign_keys="[Part.client_id, Part.part_class]", + primaryjoin="and_(Class.class_code == Part.part_class)", + foreign_keys="[Part.part_class]", viewonly=True, back_populates="part_class_info", ) def __repr__(self) -> str: - return f"" + return f"" diff --git a/backend/api/v1/modules/a76/classes/routes.py b/backend/api/v1/modules/a76/classes/routes.py index df1779ed..fb3d3d65 100644 --- a/backend/api/v1/modules/a76/classes/routes.py +++ b/backend/api/v1/modules/a76/classes/routes.py @@ -31,29 +31,6 @@ crud_routes = TenantCRUDRoutes( router = crud_routes.router - -@router.post( - "/seed", - summary="Seed Fixed Asset Classes", - description="Initialize fixed asset class catalog with default data", -) -async def seed_classes( - company_id: int = Query(..., description="Company ID"), - client_id: int = Query(..., description="Client ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - """Seed initial data for fixed asset classes""" - tenant_id = validate_access_to_resource(db, company_id, current_user) - - count = ClassService.seed_initial_data(db, tenant_id, company_id, client_id) - - return { - "message": f"Successfully created {count} fixed asset classes", - "count": count, - } - - @router.post( "/fa", response_model=ClassResponseDTOFA, diff --git a/backend/api/v1/modules/a76/classes/service.py b/backend/api/v1/modules/a76/classes/service.py index 9f124632..d8ecc25f 100644 --- a/backend/api/v1/modules/a76/classes/service.py +++ b/backend/api/v1/modules/a76/classes/service.py @@ -46,8 +46,6 @@ class ClassService: ) if filters: - if filters.get("client_id"): - query = query.filter(Class.client_id == filters["client_id"]) if filters.get("class_code"): query = query.filter( Class.class_code.ilike(f"%{filters['class_code']}%") @@ -106,7 +104,6 @@ class ClassService: existing = db.query(Class).filter( Class.tenant_id == tenant_id, Class.company_id == company_id, - Class.client_id == data_dict["client_id"], Class.class_code == data_dict["class_code"] ).first() @@ -177,16 +174,15 @@ class ClassService: if "class_code" in update_data and update_data["class_code"]: new_code = update_data["class_code"] # Check if another class with this code exists (excluding current class) - # The unique constraint is on (tenant_id, company_id, client_id, class_code) + # The unique constraint is on (tenant_id, company_id, class_code) existing_class = db.query(Class).filter( Class.class_code == new_code, Class.tenant_id == tenant_id, Class.company_id == company_id, - Class.client_id == class_obj.client_id, # Same client Class.id != class_id # Exclude current class ).first() - logger.info(f"Checking for duplicate class_code '{new_code}' for client {class_obj.client_id}") + logger.info(f"Checking for duplicate class_code '{new_code}'") if existing_class: logger.warning(f"Duplicate class_code found: {existing_class.id}") raise HTTPException( @@ -265,7 +261,7 @@ class ClassService: # Extract base class fields base_fields = { - "client_id", "class_code", "description_es", "description_en", + "class_code", "description_es", "description_en", "material_key", "unit_of_measure", "fraction", "us_fraction", "sub_key", "physical_review", "iva_exempt_fraction" } @@ -300,7 +296,6 @@ class ClassService: "id": base_class.id, "tenant_id": base_class.tenant_id, "company_id": base_class.company_id, - "client_id": base_class.client_id, "class_code": base_class.class_code, "description_es": base_class.description_es, "description_en": base_class.description_en, @@ -352,62 +347,6 @@ class ClassService: detail=f"Error al crear clase de activo fijo: {error_msg}" ) - @staticmethod - def seed_initial_data( - db: Session, tenant_id: int, company_id: int, client_id: int - ) -> int: - """ - Seed initial fixed asset class data - Returns: number of records created - """ - from .seed import seed - - created_count = 0 - for record in seed: - ( - class_code, - description_es, - description_en, - material_key, - unit_of_measure, - fraction, - us_fraction, - bom, - ) = record - - # Check if already exists - existing = ( - db.query(Class) - .filter( - Class.tenant_id == tenant_id, - Class.company_id == company_id, - Class.client_id == client_id, - Class.class_code == class_code, - ) - .first() - ) - - if not existing: - class_obj = Class( - tenant_id=tenant_id, - company_id=company_id, - client_id=client_id, - class_code=class_code, - description_es=description_es, - description_en=description_en, - material_key=material_key if material_key else None, - unit_of_measure=unit_of_measure if unit_of_measure else None, - fraction=fraction if fraction else None, - us_fraction=us_fraction if us_fraction else None, - ) - db.add(class_obj) - created_count += 1 - - if created_count > 0: - db.commit() - - return created_count - def __init__(self, db: Session): self.db = db @@ -430,7 +369,6 @@ class ClassService: self.db.query(Class) .filter( and_( - Class.client_id == class_data.client_id, Class.class_code == class_data.class_code, ) ) @@ -440,12 +378,11 @@ class ClassService: if existing: raise HTTPException( status_code=400, - detail=f"Class with client_id '{class_data.client_id}' and class_code '{class_data.class_code}' already exists", + detail=f"Class with class_code '{class_data.class_code}' already exists", ) # Crear clase db_class = Class( - client_id=class_data.client_id, class_code=class_data.class_code, description_spanish=class_data.description_spanish, description_english=class_data.description_english, @@ -469,7 +406,7 @@ class ClassService: logger.error(f"IntegrityError creating class: {str(e)}") raise HTTPException( status_code=400, - detail="Class with this client_id and class_code already exists", + detail="Class with this class_code already exists", ) except HTTPException: raise @@ -478,12 +415,11 @@ class ClassService: logger.error(f"Error creating class: {str(e)}") raise HTTPException(status_code=500, detail="Error creating class") - def get_class(self, client_id: int, class_code: str) -> Optional[ClassResponseDTO]: + def get_class(self, class_code: str) -> Optional[ClassResponseDTO]: """ Obtiene una clase por clave compuesta Args: - client_id: Clave del cliente class_code: Código de clase Returns: @@ -491,7 +427,7 @@ class ClassService: """ class_obj = ( self.db.query(Class) - .filter(and_(Class.client_id == client_id, Class.class_code == class_code)) + .filter(and_(Class.class_code == class_code)) .first() ) @@ -520,9 +456,6 @@ class ClassService: # Aplicar filtros si se proporcionan if search_params: - if search_params.client_id: - query = query.filter(Class.client_id == search_params.client_id) - if search_params.class_code: query = query.filter( Class.class_code.ilike(f"%{search_params.class_code}%") @@ -569,13 +502,12 @@ class ClassService: ) def update_class( - self, client_id: int, class_code: str, class_data: ClassUpdateDTO + self, class_code: str, class_data: ClassUpdateDTO ) -> Optional[ClassResponseDTO]: """ Actualiza una clase Args: - client_id: Clave del cliente class_code: Código de clase class_data: Datos a actualizar @@ -584,7 +516,7 @@ class ClassService: """ class_obj = ( self.db.query(Class) - .filter(and_(Class.client_id == client_id, Class.class_code == class_code)) + .filter(and_(Class.class_code == class_code)) .first() ) @@ -604,15 +536,14 @@ class ClassService: except Exception as e: self.db.rollback() - logger.error(f"Error updating class {client_id}-{class_code}: {str(e)}") + logger.error(f"Error updating class {class_code}: {str(e)}") raise HTTPException(status_code=500, detail="Error updating class") - def delete_class(self, client_id: int, class_code: str) -> bool: + def delete_class(self, class_code: str) -> bool: """ Elimina una clase Args: - client_id: Clave del cliente class_code: Código de clase Returns: @@ -620,7 +551,7 @@ class ClassService: """ class_obj = ( self.db.query(Class) - .filter(and_(Class.client_id == client_id, Class.class_code == class_code)) + .filter(and_(Class.class_code == class_code)) .first() ) @@ -633,7 +564,7 @@ class ClassService: return True except Exception as e: self.db.rollback() - logger.error(f"Error deleting class {client_id}-{class_code}: {str(e)}") + logger.error(f"Error deleting class {class_code}: {str(e)}") raise HTTPException(status_code=500, detail="Error deleting class") def search_by_fraction(self, fraction: str) -> List[ClassBasicDTO]: @@ -643,19 +574,6 @@ class ClassService: ) return [ClassBasicDTO.model_validate(class_obj) for class_obj in classes] - def search_by_client( - self, client_id: int, skip: int = 0, limit: int = 100 - ) -> List[ClassBasicDTO]: - """Obtiene todas las clases de un cliente específico""" - classes = ( - self.db.query(Class) - .filter(Class.client_id == client_id) - .offset(skip) - .limit(limit) - .all() - ) - return [ClassBasicDTO.model_validate(class_obj) for class_obj in classes] - def search_by_material(self, material_key: str) -> List[ClassBasicDTO]: """Busca clases por clave de material""" classes = ( @@ -678,9 +596,6 @@ class ClassService: """Obtiene estadísticas básicas de clases""" total_classes = self.db.query(Class).count() - # Contar por clientes - clients_count = self.db.query(Class.client_id).distinct().count() - # Contar por revisión física physical_review_stats = {} for i in range(3): # Asumiendo valores 0, 1, 2 @@ -695,7 +610,6 @@ class ClassService: return { "total_classes": total_classes, - "clients_with_classes": clients_count, "classes_with_fraction": with_fraction, "classes_with_us_fraction": with_us_fraction, **physical_review_stats, diff --git a/frontend/src/lib/api/dashboard/a76/classes.ts b/frontend/src/lib/api/dashboard/a76/classes.ts index 9473979a..fc787b97 100644 --- a/frontend/src/lib/api/dashboard/a76/classes.ts +++ b/frontend/src/lib/api/dashboard/a76/classes.ts @@ -7,7 +7,6 @@ export interface A76Class { id: number; tenant_id: number; company_id: number; - client_id: number; class_code: string; description_es: string | null; description_en: string | null; @@ -26,7 +25,6 @@ export interface A76Class { // DTO para crear (match con tu formulario) export interface A76ClassCreate { company_id: number; - client_id: number; class_code: string; description_es?: string | null; description_en?: string | null; @@ -94,7 +92,14 @@ export const classesApi = { /** * Inicializar datos semilla de clases de activo fijo */ - seed: (company_id: number, client_id: number): Promise> => { - return api.post(`/v1/a76/classes/seed?company_id=${company_id}&client_id=${client_id}`, {}); + seed: (company_id: number): Promise> => { + return api.post(`/v1/a76/classes/seed?company_id=${company_id}`, {}); + }, + + /** + * Crear una clase de activo fijo (crea tanto A76Class como FAClass en una transacción) + */ + createFA: (data: any, company_id: number): Promise> => { + return api.post(`/v1/a76/classes/fa?company_id=${company_id}`, data); } }; \ No newline at end of file diff --git a/frontend/src/lib/components/dashboard/goods/classes/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/goods/classes/create-edit-dialog.svelte index aaaaafa4..15119178 100644 --- a/frontend/src/lib/components/dashboard/goods/classes/create-edit-dialog.svelte +++ b/frontend/src/lib/components/dashboard/goods/classes/create-edit-dialog.svelte @@ -7,7 +7,7 @@ import * as Select from "$lib/components/ui/select"; import { classesApi, type A76Class, type A76ClassCreate, type A76ClassUpdate } from "$lib/api/dashboard/a76/classes"; import { materialTypesApi, type MaterialType } from "$lib/api/dashboard/refrence_data/material_types"; - import { clientsProvidersApi, type ClientProviderBasic } from "$lib/api/dashboard/a76/clients-providers"; + import { clientsProvidersApi, type ClientProvider } from "$lib/api/dashboard/a76/clients-providers"; import { companyStore } from "$lib/stores/company.svelte"; import { onMount } from 'svelte'; @@ -27,7 +27,6 @@ // Estado del formulario let formData = $state({ - client_id: item?.client_id || null, class_code: item?.class_code || '', description_es: item?.description_es || '', description_en: item?.description_en || '', @@ -44,7 +43,7 @@ let error = $state(null); let materialTypes = $state([]); let loadingMaterialTypes = $state(false); - let clients = $state([]); + let clients = $state([]); let loadingClients = $state(false); // Variables para controlar los selects @@ -73,9 +72,9 @@ // Cargar clientes loadingClients = true; try { - const response = await clientsProvidersApi.listClients(companyId, 0, 500); + const response = await clientsProvidersApi.list(companyId, 1, 500); if (response.data) { - clients = response.data; + clients = response.data.items; } } catch (e) { console.error('Error loading clients:', e); @@ -88,7 +87,6 @@ $effect(() => { if (item) { formData = { - client_id: item.client_id, class_code: item.class_code, description_es: item.description_es || '', description_en: item.description_en || '', @@ -107,7 +105,6 @@ } else { // Reset para modo crear formData = { - client_id: null, class_code: '', description_es: '', description_en: '', @@ -142,12 +139,6 @@ error = 'No hay compañía seleccionada'; return; } - - // Validaciones básicas - if (!formData.client_id) { - error = 'Debes seleccionar un cliente'; - return; - } if (!formData.class_code.trim()) { error = 'El código de clase es requerido'; return; @@ -178,7 +169,6 @@ if (isEdit && item) { // Actualizar const updateData: A76ClassUpdate = { - client_id: formData.client_id!, class_code: formData.class_code, description_es: formData.description_es || null, description_en: formData.description_en || null, @@ -192,10 +182,8 @@ }; response = await classesApi.update(item.id, updateData, companyId); } else { - // Crear con el client_id seleccionado const createData: A76ClassCreate = { company_id: companyId, - client_id: formData.client_id!, class_code: formData.class_code, description_es: formData.description_es || null, description_en: formData.description_en || null, @@ -303,34 +291,6 @@ {/if} - -
- - {#if loadingClients} -
-
- Cargando clientes... -
- {:else if clients.length > 0} - - {:else} -
- No hay clientes disponibles -
- {/if} -
-
diff --git a/frontend/src/lib/components/dashboard/goods/classes/forms/FixedAssetClassForm.svelte b/frontend/src/lib/components/dashboard/goods/classes/forms/FixedAssetClassForm.svelte new file mode 100644 index 00000000..16632751 --- /dev/null +++ b/frontend/src/lib/components/dashboard/goods/classes/forms/FixedAssetClassForm.svelte @@ -0,0 +1,1177 @@ + + +
+ +
+
+ + { + // Limpiar error local si existe + if (validationErrors.class_code) { + const errors = { ...validationErrors }; + delete errors.class_code; + validationErrors = errors; + } + }} + onblur={() => validateField('class_code')} + /> + {#if validationErrors.class_code} +

{validationErrors.class_code}

+ {/if} +
+
+ + +
+
+ + +
+ + validateField('description_es')} + /> + {#if validationErrors.description_es} +

{validationErrors.description_es}

+ {/if} +
+ + +
+ + +
+ + +
+ +
+ validateField('material_key')} + /> + + + {formData.material_description || ''} + +
+ {#if validationErrors.material_key} +

{validationErrors.material_key}

+ {/if} +
+ + +
+ +
+ validateField('unit_of_measure')} + /> + + + {formData.unit_of_measure_description || ''} + + + Clave U.M.A: {formData.unit_measure_key || ''} + +
+ {#if validationErrors.unit_of_measure} +

{validationErrors.unit_of_measure}

+ {/if} +
+ + +
+ +
+ validateField('fraction')} + /> + + + U.M.T: {formData.fraction_umt || ''} + + + Clave U.M.A: {formData.fraction_uma_key || ''} + +
+ {#if validationErrors.fraction} +

{validationErrors.fraction}

+ {/if} +
+ + +
+ +
+ + + + Ad/valorem: {formData.us_fraction_ad_valorem || '0.00'} + + + Tasa Fija: {formData.us_fraction_fixed_rate || '0.00000000'} + +
+
+ + +
+ +
+ + % + +
+ + +
+
+
+ + +
+ +
+ + +
+
+ + +
+ +
+
+ (formData.iva_exempt_fraction = true)} + class="h-4 w-4" + /> + +
+
+ (formData.iva_exempt_fraction = false)} + class="h-4 w-4" + /> + +
+
+
+ + +
+ +
+ + +
+
+
+ + + + + + + CATALOGO DE ACTIVO FIJO + +
+
+ + +
+
+ + + + + + + + + {#each filteredMaterialTypes as material (material.key)} + selectMaterial(material)} + > + + + + {/each} + +
ClaveDescripción
{material.key}{material.description}
+
+
+ + + +
+
+ + + + + + UNIDADES DE MEDIDA + +
+
+ + +
+
+ + + + + + + + + + + {#each filteredUnits as unit (unit.code)} + selectUnit(unit)} + > + + + + + + {/each} + +
CódigoDescripciónDescription (English)Clave Mexicana
{unit.code}{unit.description}{unit.descriptionEnglish}{unit.claveMexicana}
+
+
+ + + +
+
+ + + + + + CATALOGO DE FRACCIONES SITAR - SCAII + +
+
+ + +
+
+ + + + + + + + + + + {#each tariffFractions as fraction (fraction.code)} + selectFraction(fraction)} + > + + + + + + {:else} + + + + {/each} + +
FracciónNICODescripciónU.M.T
{fraction.fraction}{fraction.nico}{fraction.description}{fraction.umt}
+ {#if isLoadingFractions} + Cargando fracciones... + {:else} + No hay fracciones disponibles + {/if} +
+
+
+ + + +
+
+ + + + + + CATALOGO DE FRACCIONES AMERICANAS + +
+
+ + +
+
+ + + + + + + + + + + + {#each usTariffFractions as fraction (fraction.id)} + selectUSFraction(fraction)} + > + + + + + + + {:else} + + + + {/each} + +
CódigoPrefijoAd valoremCosto FijoDescripción
{fraction.code}{fraction.prefix || ''}{fraction.ad_valorem || '0.00'}{fraction.fixed_cost || '0.00'}{fraction.description || ''}
+ {#if isLoadingUSFractions} + Cargando fracciones... + {:else} + No hay fracciones disponibles + {/if} +
+
+
+ + + +
+
+ + + + + + CATALOGO DE DEPRECIACION + +
+
+ + +
+
+ + + + + + + + + + {#each depreciationCatalog as item (item.id)} + selectDepreciation(item)} + > + + + + + {:else} + + + + {/each} + +
FracciónDescripción% Depreciación
{item.fraction}{item.description}{item.depreciation_rate}%
+ {#if isLoadingDepreciation} + Cargando... + {:else} + No hay registros disponibles + {/if} +
+
+
+ + + +
+
+ + + + + + CATALOGO FDA + +
+
+ + +
+
+ + + + + + + + + {#each fdaCatalog as item (item.id)} + selectFDA(item)} + > + + + + {:else} + + + + {/each} + +
Clave FDADescripción
{item.fda_key}{item.description}
+ {#if isLoadingFDA} + Cargando... + {:else} + No hay registros disponibles + {/if} +
+
+
+ + + +
+
+ + + + + + CATALOGO DE CARTA PORTE + +
+
+ + +
+
+ + + + + + + + + {#each cartaPorteCatalog as item (item.id)} + { + formData.carta_porte_code = item.code; + showCartaPorteDialog = false; + }} + > + + + + {:else} + + + + {/each} + +
CódigoDescripción
{item.code}{item.description}
+ No hay registros disponibles +
+
+
+ + + +
+
diff --git a/frontend/src/routes/dashboard/goods/fixed-asset-classes/+page.svelte b/frontend/src/routes/dashboard/goods/fixed-asset-classes/+page.svelte new file mode 100644 index 00000000..2224b5a7 --- /dev/null +++ b/frontend/src/routes/dashboard/goods/fixed-asset-classes/+page.svelte @@ -0,0 +1,871 @@ + + +
+ +
+

CATALOGO DE CLASES DE ACTIVO FIJO

+

+ Gestiona y consulta las clases de activo fijo +

+
+ + +
+ +
+ +
+
+
+

Filtros

+ + Filtra las clases por diferentes criterios (los filtros se aplican automáticamente) + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+

Listado de Clases

+
+ + Mostrando de {filteredClasses.length} registros + + +
+
+ + +
+ + + + + + + + + + + + + + {#if isLoading} + + + + {:else if filteredClasses.length === 0} + + + + {:else} + {#each filteredClasses as cls (cls.id)} + selectClass(cls)} + > + + + + + + + + + {/each} + {/if} + +
+ + ClaseDescripción EspañolDescripción InglésTipoU.MFracción U.M.T. Fracción US
Cargando...
+ No hay clases de activo fijo registradas +
+ + + + {cls.class_code} + + {cls.description_es || ''}{cls.description_en || ''} + + {cls.material_key || ''} + + {cls.unit_of_measure || ''}{cls.fraction || ''} - {cls.us_fraction || '-'}
+
+
+
+ + +
+
+

Código de Clase

+

+ {formData.class_code || '---'} +

+
+ +
+
+
+ +

{formData.description_es || 'Sin descripción'}

+
+
+ +

{formData.description_en || 'No translation available'}

+
+
+ +
+
+ +
+ + {formData.material_key || '-'} +
+
+
+ + {formData.unit_of_measure || '-'} +
+
+ +
+ +

+ {formData.fraction || '0000.00.00'} +

+
+
+
+
+
+ + +
+
+ +
+ + + +
+
+
+ + + + + + {selectedClass ? 'Editar' : 'Nueva'} Clase de Activo Fijo + + + + {#if validationError} +
+
+
+ ! +
+
+

Error de Validación

+

{validationError}

+
+ +
+
+ {/if} + +
+ validationError = ''} + onSave={async (data: Partial) => { + // Evitar múltiples clics + if (isSaving) { + console.log('⚠️ Ya está guardando, ignorando clic'); + return; + } + isSaving = true; + validationError = ''; + + console.log('========================================'); + console.log('=== INICIO ONSAVE ==='); + console.log('Datos recibidos:', data); + console.log('selectedClass:', selectedClass); + console.log('========================================'); + + try { + const cleanData = $state.snapshot(data); + const companyId = companyStore.activeCompany?.id; + + if (!companyId) { + throw new Error('No hay empresa seleccionada'); + } + + let response; + + if (selectedClass?.id) { + // === ACTUALIZACIÓN === + console.log('🔄 MODO: ACTUALIZACIÓN'); + console.log('ID de clase:', selectedClass.id); + + response = await classesApi.update(selectedClass.id, { + class_code: cleanData.class_code?.trim() || '', + description_es: cleanData.description_es?.trim() || '', + description_en: cleanData.description_en?.trim() || '', + material_key: cleanData.material_key?.trim() || '', + unit_of_measure: cleanData.unit_of_measure?.trim() || '', + fraction: cleanData.fraction?.trim() || '', + us_fraction: cleanData.us_fraction || '', + physical_review: cleanData.physical_review ? 1 : 0, + iva_exempt_fraction: cleanData.iva_exempt_fraction || '' + }, companyId); + + // ¡IMPORTANTE! fetchApi NO lanza excepciones, retorna { error, status } + if (response.error) { + console.error('❌ Error en respuesta de actualización:', response); + throw new Error(response.error); + } + + console.log('✅ Actualización exitosa'); + } else { + // === CREACIÓN === + console.log('➕ MODO: CREACIÓN'); + + const payload = { + class_code: cleanData.class_code?.trim() || '', + description_es: cleanData.description_es?.trim() || '', + description_en: cleanData.description_en?.trim() || '', + material_key: cleanData.material_key?.trim() || '', + unit_of_measure: cleanData.unit_of_measure?.trim() || '', + fraction: cleanData.fraction?.trim() || '', + us_fraction: cleanData.us_fraction?.trim() || '', + sub_key: cleanData.sub_key || '', + physical_review: cleanData.physical_review ? 1 : 0, + iva_exempt_fraction: cleanData.iva_exempt_fraction || '', + depreciation_rate: cleanData.depreciation_rate || null, + fda_code: cleanData.fda_code || null, + class_enabled: true + }; + + console.log('Payload:', payload); + + response = await classesApi.createFA(payload, companyId); + + if (response.error) { + console.error('❌ Error del servidor:', response.error); + throw new Error(response.error); + } + + console.log('✅ Creación exitosa'); + } + + // === ÉXITO TOTAL === + console.log('✅ GUARDADO EXITOSO - Cerrando diálogo'); + const wasUpdate = !!selectedClass?.id; + await loadClasses(); + showInsertDialog = false; + selectedClass = null; + validationError = ''; + toast.success(wasUpdate ? 'Clase actualizada correctamente' : 'Clase creada correctamente'); + + } catch (error: any) { + // === ERROR === + console.error('========================================'); + console.error('❌ ERROR CAPTURADO'); + console.error('Error:', error); + console.error('Error.response:', error?.response); + console.error('Error.response.data:', error?.response?.data); + console.error('Error.detail:', error?.detail); + console.error('========================================'); + + let errorMsg = 'Error al guardar'; + + // Primero intentar con error.detail (fetch directo) + if (error?.detail) { + if (typeof error.detail === 'string') { + errorMsg = error.detail; + } else if (Array.isArray(error.detail)) { + errorMsg = error.detail.map((e: any) => e.msg || e).join(', '); + } + } + // Luego con error.response.data.detail (axios) + else if (error?.response?.data?.detail) { + if (typeof error.response.data.detail === 'string') { + errorMsg = error.response.data.detail; + } else if (Array.isArray(error.response.data.detail)) { + errorMsg = error.response.data.detail.map((e: any) => e.msg || e).join(', '); + } + } + // Por último el mensaje genérico + else if (error?.message) { + errorMsg = error.message; + } + + console.error('📝 Mensaje de error extraído:', errorMsg); + + validationError = errorMsg; + console.error('🔴 validationError asignado:', validationError); + console.error('🔴 showInsertDialog permanece:', showInsertDialog); + console.error('========================================'); + + // NO cerramos el diálogo, permanece abierto + } finally { + isSaving = false; + console.log('✅ isSaving = false'); + } + }} + onCancel={() => { + showInsertDialog = false; + selectedClass = null; + }} + /> +
+ + + + +
+
+ + + + + + ¿Confirmar eliminación? + +
+

+ ¿Estás seguro que deseas eliminar la clase {selectedClass?.class_code}? +

+

+ {selectedClass?.description_es} +

+

+ Esta acción no se puede deshacer. +

+
+ + + + +
+