feature/catalogo-equivalencias
This commit is contained in:
@@ -1,39 +1,15 @@
|
||||
from typing import Optional, List
|
||||
from typing import Optional
|
||||
from decimal import Decimal
|
||||
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
|
||||
# Equivalency Item DTOs
|
||||
|
||||
|
||||
class EquivalencyItemBase(BaseModel):
|
||||
original_field: str = Field(..., max_length=100,
|
||||
description="Original Field (Unit of Measure)")
|
||||
external_field: str = Field(..., max_length=100,
|
||||
description="External Field")
|
||||
|
||||
|
||||
class EquivalencyItemCreate(EquivalencyItemBase):
|
||||
pass
|
||||
|
||||
|
||||
class EquivalencyItemUpdate(BaseModel):
|
||||
original_field: Optional[str] = Field(None, max_length=100)
|
||||
external_field: Optional[str] = Field(None, max_length=100)
|
||||
|
||||
|
||||
class EquivalencyItemResponse(EquivalencyItemBase):
|
||||
id: int
|
||||
equivalency_id: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
# Equivalency DTOs
|
||||
# Equivalency (parent) DTOs
|
||||
|
||||
|
||||
class EquivalencyBase(BaseModel):
|
||||
fraccion_mex: str = Field(..., max_length=10, description="Fraccion MX (Identifier)")
|
||||
fraccion_us: str = Field(..., max_length=100, description="Fraccion US (External Field)")
|
||||
description: Optional[str] = Field(
|
||||
None, max_length=200, description="Description")
|
||||
# Agrupa múltiples destinos para un mismo origen.
|
||||
identifier: str = Field(..., max_length=10, description="From unit code")
|
||||
description: Optional[str] = Field(None, max_length=200, description="Description (optional)")
|
||||
|
||||
|
||||
class EquivalencyCreate(EquivalencyBase):
|
||||
@@ -41,8 +17,7 @@ class EquivalencyCreate(EquivalencyBase):
|
||||
|
||||
|
||||
class EquivalencyUpdate(BaseModel):
|
||||
fraccion_mex: Optional[str] = Field(None, max_length=10)
|
||||
fraccion_us: Optional[str] = Field(None, max_length=100)
|
||||
identifier: Optional[str] = Field(None, max_length=10)
|
||||
description: Optional[str] = Field(None, max_length=200)
|
||||
|
||||
|
||||
@@ -53,4 +28,30 @@ class EquivalencyResponse(EquivalencyBase):
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
# Equivalency Item DTOs (catálogo por fila)
|
||||
|
||||
|
||||
class EquivalencyItemBase(BaseModel):
|
||||
original_field: str = Field(..., max_length=100, description="Original unit code (from)")
|
||||
external_field: str = Field(..., max_length=100, description="External unit code (to)")
|
||||
conversion_factor: Optional[Decimal] = Field(None, description="Conversion factor")
|
||||
|
||||
|
||||
class EquivalencyItemCreate(EquivalencyItemBase):
|
||||
conversion_factor: Decimal = Field(..., description="Conversion factor")
|
||||
|
||||
|
||||
class EquivalencyItemUpdate(BaseModel):
|
||||
original_field: Optional[str] = Field(None, max_length=100)
|
||||
external_field: Optional[str] = Field(None, max_length=100)
|
||||
conversion_factor: Optional[Decimal] = None
|
||||
|
||||
|
||||
class EquivalencyItemResponse(EquivalencyItemBase):
|
||||
id: int
|
||||
equivalency_id: int
|
||||
tenant_id: int
|
||||
company_id: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from typing import Optional, List
|
||||
from sqlalchemy import Integer, String, ForeignKey, UniqueConstraint, ForeignKeyConstraint
|
||||
from decimal import Decimal
|
||||
from sqlalchemy import Integer, String, ForeignKey, UniqueConstraint, ForeignKeyConstraint, Numeric
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
@@ -50,6 +51,10 @@ class EquivalencyItem(Base, TenantScopedMixin, TimestampMixin):
|
||||
|
||||
external_field: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
|
||||
conversion_factor: Mapped[Optional[Decimal]] = mapped_column(
|
||||
Numeric(13, 6), nullable=True
|
||||
)
|
||||
|
||||
equivalency: Mapped["Equivalency"] = relationship(back_populates="items")
|
||||
|
||||
unit_of_measure: Mapped["UnitOfMeasure"] = relationship()
|
||||
|
||||
@@ -38,6 +38,7 @@ item_crud = TenantCRUDRoutes(
|
||||
tags=["Equivalency Items"],
|
||||
resource_name="EquivalencyItem",
|
||||
enable_list=True,
|
||||
enable_filters=True,
|
||||
)
|
||||
|
||||
# Custom endpoint for creating items nested under equivalency
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from typing import List, Optional, Tuple, Dict, Any
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from fastapi import HTTPException
|
||||
|
||||
@@ -20,27 +19,26 @@ class EquivalencyService:
|
||||
) -> Tuple[List[Equivalency], int]:
|
||||
query = db.query(Equivalency).filter(
|
||||
Equivalency.tenant_id == tenant_id,
|
||||
Equivalency.company_id == company_id
|
||||
).options(joinedload(Equivalency.items))
|
||||
Equivalency.company_id == company_id,
|
||||
)
|
||||
|
||||
if filters:
|
||||
if 'fraccion_mex' in filters:
|
||||
query = query.filter(Equivalency.identifier.ilike(f"%{filters['fraccion_mex']}%"))
|
||||
if 'description' in filters:
|
||||
query = query.filter(Equivalency.description.ilike(f"%{filters['description']}%"))
|
||||
# Soporta filtros de UI por nombre de parámetro.
|
||||
if "from_unit_code" in filters:
|
||||
query = query.filter(
|
||||
Equivalency.identifier.ilike(f"%{filters['from_unit_code']}%")
|
||||
)
|
||||
if "identifier" in filters:
|
||||
query = query.filter(
|
||||
Equivalency.identifier.ilike(f"%{filters['identifier']}%")
|
||||
)
|
||||
if "description" in filters:
|
||||
query = query.filter(
|
||||
Equivalency.description.ilike(f"%{filters['description']}%")
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
items = query.offset(skip).limit(limit).all()
|
||||
|
||||
# Map internal fields to DTO fields
|
||||
for item in items:
|
||||
item.fraccion_mex = item.identifier
|
||||
# Try to find the first item to get fraccion_us
|
||||
if item.items:
|
||||
item.fraccion_us = item.items[0].external_field
|
||||
else:
|
||||
item.fraccion_us = ""
|
||||
|
||||
return items, total
|
||||
|
||||
@staticmethod
|
||||
@@ -54,15 +52,8 @@ class EquivalencyService:
|
||||
Equivalency.id == id,
|
||||
Equivalency.tenant_id == tenant_id,
|
||||
Equivalency.company_id == company_id
|
||||
).options(joinedload(Equivalency.items)).first()
|
||||
|
||||
if item:
|
||||
item.fraccion_mex = item.identifier
|
||||
if item.items:
|
||||
item.fraccion_us = item.items[0].external_field
|
||||
else:
|
||||
item.fraccion_us = ""
|
||||
|
||||
).first()
|
||||
|
||||
return item
|
||||
|
||||
@staticmethod
|
||||
@@ -73,53 +64,27 @@ class EquivalencyService:
|
||||
company_id: int
|
||||
) -> Equivalency:
|
||||
try:
|
||||
# Create Parent
|
||||
db_obj = Equivalency(
|
||||
identifier=data.fraccion_mex,
|
||||
identifier=data.identifier,
|
||||
description=data.description,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id
|
||||
company_id=company_id,
|
||||
)
|
||||
db.add(db_obj)
|
||||
db.flush() # Flush to get ID
|
||||
|
||||
# Create Child Item (mapping fraccion_mex -> original_field, fraccion_us -> external_field)
|
||||
item = EquivalencyItem(
|
||||
equivalency_id=db_obj.id,
|
||||
original_field=data.fraccion_mex, # Must exist in units_of_measure
|
||||
external_field=data.fraccion_us,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id
|
||||
)
|
||||
db.add(item)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
|
||||
# Map for response
|
||||
db_obj.fraccion_mex = db_obj.identifier
|
||||
db_obj.fraccion_us = item.external_field
|
||||
|
||||
return db_obj
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
error_msg = str(e.orig) if hasattr(e, 'orig') else str(e)
|
||||
print(f"IntegrityError in create: {error_msg}")
|
||||
|
||||
if "units_of_measure" in error_msg:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"La Fracción MX '{data.fraccion_mex}' no es válida. Debe existir en el catálogo de Unidades de Medida."
|
||||
)
|
||||
error_msg = str(e.orig) if hasattr(e, "orig") else str(e)
|
||||
if "uq_equivalency_identifier" in error_msg:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Ya existe una equivalencia para la Fracción MX '{data.fraccion_mex}'."
|
||||
)
|
||||
raise HTTPException(status_code=400, detail=f"Error al guardar: {error_msg}")
|
||||
status_code=400,
|
||||
detail=f"Ya existe un registro para el origen '{data.identifier}'.",
|
||||
) from e
|
||||
raise HTTPException(status_code=400, detail=f"Error al guardar: {error_msg}") from e
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print(f"Error in create: {str(e)}")
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
@@ -135,58 +100,19 @@ class EquivalencyService:
|
||||
return None
|
||||
|
||||
try:
|
||||
if data.fraccion_mex:
|
||||
db_obj.identifier = data.fraccion_mex
|
||||
if data.description:
|
||||
db_obj.description = data.description
|
||||
|
||||
# Update Item
|
||||
item = None
|
||||
if db_obj.items:
|
||||
item = db_obj.items[0]
|
||||
|
||||
if item:
|
||||
if data.fraccion_us:
|
||||
item.external_field = data.fraccion_us
|
||||
if data.fraccion_mex:
|
||||
item.original_field = data.fraccion_mex
|
||||
else:
|
||||
# Create if missing
|
||||
if data.fraccion_us or data.fraccion_mex:
|
||||
item = EquivalencyItem(
|
||||
equivalency_id=db_obj.id,
|
||||
original_field=data.fraccion_mex or db_obj.identifier,
|
||||
external_field=data.fraccion_us or "",
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id
|
||||
)
|
||||
db.add(item)
|
||||
update_dict = data.model_dump(exclude_unset=True)
|
||||
for key, value in update_dict.items():
|
||||
setattr(db_obj, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
|
||||
# Map for response
|
||||
db_obj.fraccion_mex = db_obj.identifier
|
||||
if db_obj.items:
|
||||
db_obj.fraccion_us = db_obj.items[0].external_field
|
||||
else:
|
||||
db_obj.fraccion_us = ""
|
||||
|
||||
return db_obj
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
error_msg = str(e.orig) if hasattr(e, 'orig') else str(e)
|
||||
print(f"IntegrityError in update: {error_msg}")
|
||||
|
||||
if "units_of_measure" in error_msg:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"La Fracción MX '{data.fraccion_mex or db_obj.identifier}' no es válida. Debe existir en el catálogo de Unidades de Medida."
|
||||
)
|
||||
raise HTTPException(status_code=400, detail=f"Error al actualizar: {error_msg}")
|
||||
error_msg = str(e.orig) if hasattr(e, "orig") else str(e)
|
||||
raise HTTPException(status_code=400, detail=f"Error al actualizar: {error_msg}") from e
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print(f"Error in update: {str(e)}")
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
@@ -216,8 +142,21 @@ class EquivalencyItemService:
|
||||
) -> Tuple[List[EquivalencyItem], int]:
|
||||
query = db.query(EquivalencyItem).filter(
|
||||
EquivalencyItem.tenant_id == tenant_id,
|
||||
EquivalencyItem.company_id == company_id
|
||||
EquivalencyItem.company_id == company_id,
|
||||
)
|
||||
|
||||
if filters:
|
||||
if "from_unit_code" in filters:
|
||||
query = query.filter(
|
||||
EquivalencyItem.original_field.ilike(
|
||||
f"%{filters['from_unit_code']}%"
|
||||
)
|
||||
)
|
||||
if "to_unit_code" in filters:
|
||||
query = query.filter(
|
||||
EquivalencyItem.external_field.ilike(f"%{filters['to_unit_code']}%")
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
items = query.offset(skip).limit(limit).all()
|
||||
return items, total
|
||||
@@ -242,15 +181,51 @@ class EquivalencyItemService:
|
||||
tenant_id: int,
|
||||
company_id: int
|
||||
) -> EquivalencyItem:
|
||||
db_obj = EquivalencyItem(
|
||||
**data.model_dump(),
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id
|
||||
)
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
try:
|
||||
# Equivalency padre agrupado por `from_unit_code` (original_field)
|
||||
parent = (
|
||||
db.query(Equivalency)
|
||||
.filter(
|
||||
Equivalency.tenant_id == tenant_id,
|
||||
Equivalency.company_id == company_id,
|
||||
Equivalency.identifier == data.original_field,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not parent:
|
||||
parent = Equivalency(
|
||||
identifier=data.original_field,
|
||||
description=None,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
)
|
||||
db.add(parent)
|
||||
db.flush() # obtener id sin commit
|
||||
|
||||
db_obj = EquivalencyItem(
|
||||
equivalency_id=parent.id,
|
||||
original_field=data.original_field,
|
||||
external_field=data.external_field,
|
||||
conversion_factor=data.conversion_factor,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
)
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
error_msg = str(e.orig) if hasattr(e, "orig") else str(e)
|
||||
if "units_of_measure" in error_msg:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=(
|
||||
"Verifica que las unidades existan en el catálogo de U.M."
|
||||
),
|
||||
) from e
|
||||
raise HTTPException(status_code=400, detail=f"Error al guardar: {error_msg}") from e
|
||||
|
||||
@staticmethod
|
||||
def create_nested(
|
||||
@@ -264,6 +239,7 @@ class EquivalencyItemService:
|
||||
equivalency_id=equivalency_id,
|
||||
original_field=data.original_field,
|
||||
external_field=data.external_field,
|
||||
conversion_factor=data.conversion_factor,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id
|
||||
)
|
||||
@@ -283,13 +259,47 @@ class EquivalencyItemService:
|
||||
db_obj = EquivalencyItemService.get_by_id(db, id, tenant_id, company_id)
|
||||
if not db_obj:
|
||||
return None
|
||||
|
||||
for key, value in data.model_dump(exclude_unset=True).items():
|
||||
setattr(db_obj, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
|
||||
update_dict = data.model_dump(exclude_unset=True)
|
||||
try:
|
||||
new_original = update_dict.get("original_field")
|
||||
if new_original and new_original != db_obj.original_field:
|
||||
# Re-asignar al padre del nuevo `from_unit_code`
|
||||
parent = (
|
||||
db.query(Equivalency)
|
||||
.filter(
|
||||
Equivalency.tenant_id == tenant_id,
|
||||
Equivalency.company_id == company_id,
|
||||
Equivalency.identifier == new_original,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not parent:
|
||||
parent = Equivalency(
|
||||
identifier=new_original,
|
||||
description=None,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
)
|
||||
db.add(parent)
|
||||
db.flush()
|
||||
|
||||
db_obj.equivalency_id = parent.id
|
||||
db_obj.original_field = new_original
|
||||
|
||||
# Aplicar resto de cambios
|
||||
for key, value in update_dict.items():
|
||||
if key == "original_field":
|
||||
continue # ya aplicado arriba (y con posible re-parenting)
|
||||
setattr(db_obj, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
error_msg = str(e.orig) if hasattr(e, "orig") else str(e)
|
||||
raise HTTPException(status_code=400, detail=f"Error al actualizar: {error_msg}") from e
|
||||
|
||||
@staticmethod
|
||||
def delete(
|
||||
|
||||
@@ -1,43 +1,44 @@
|
||||
import { api } from '$lib/api';
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
|
||||
export interface Equivalency {
|
||||
export interface EquivalencyItem {
|
||||
id: number;
|
||||
fraccion_mex: string;
|
||||
fraccion_us: string;
|
||||
description?: string;
|
||||
original_field: string; // from_unit_code
|
||||
external_field: string; // to_unit_code
|
||||
conversion_factor: number | null;
|
||||
equivalency_id: number;
|
||||
tenant_id: number;
|
||||
company_id: number;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface EquivalencyCreate {
|
||||
fraccion_mex: string;
|
||||
fraccion_us: string;
|
||||
description?: string;
|
||||
export interface EquivalencyItemCreate {
|
||||
original_field: string;
|
||||
external_field: string;
|
||||
conversion_factor: number;
|
||||
}
|
||||
|
||||
export interface EquivalencyUpdate {
|
||||
fraccion_mex?: string;
|
||||
fraccion_us?: string;
|
||||
description?: string;
|
||||
export interface EquivalencyItemUpdate {
|
||||
original_field?: string;
|
||||
external_field?: string;
|
||||
conversion_factor?: number | null;
|
||||
}
|
||||
|
||||
export interface EquivalencyListResponse {
|
||||
items: Equivalency[];
|
||||
export interface EquivalencyItemListResponse {
|
||||
items: EquivalencyItem[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export async function getEquivalencies(
|
||||
export async function getEquivalencyItems(
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<EquivalencyListResponse>> {
|
||||
) : Promise<ApiResponse<EquivalencyItemListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
@@ -45,28 +46,31 @@ export async function getEquivalencies(
|
||||
...filters
|
||||
});
|
||||
|
||||
return await api.get(`/v1/a76/equivalencies/?${params.toString()}`);
|
||||
return await api.get(`/v1/a76/equivalencies/items/?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function getEquivalency(id: number, companyId: number): Promise<ApiResponse<Equivalency>> {
|
||||
return await api.get(`/v1/a76/equivalencies/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
export async function createEquivalency(
|
||||
data: EquivalencyCreate,
|
||||
export async function getEquivalencyItem(
|
||||
id: number,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<Equivalency>> {
|
||||
return await api.post(`/v1/a76/equivalencies/?company_id=${companyId}`, data);
|
||||
): Promise<ApiResponse<EquivalencyItem>> {
|
||||
return await api.get(`/v1/a76/equivalencies/items/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
export async function updateEquivalency(
|
||||
id: number,
|
||||
data: EquivalencyUpdate,
|
||||
export async function createEquivalencyItem(
|
||||
data: EquivalencyItemCreate,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<Equivalency>> {
|
||||
return await api.put(`/v1/a76/equivalencies/${id}/?company_id=${companyId}`, data);
|
||||
): Promise<ApiResponse<EquivalencyItem>> {
|
||||
return await api.post(`/v1/a76/equivalencies/items/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteEquivalency(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/equivalencies/${id}/?company_id=${companyId}`);
|
||||
export async function updateEquivalencyItem(
|
||||
id: number,
|
||||
data: EquivalencyItemUpdate,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<EquivalencyItem>> {
|
||||
return await api.put(`/v1/a76/equivalencies/items/${id}/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteEquivalencyItem(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/equivalencies/items/${id}?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import type { ColumnDef } from '@tanstack/table-core';
|
||||
import type { Equivalency } from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
|
||||
import type { EquivalencyItem } from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
|
||||
import { renderComponent } from '$lib/components/ui/data-table';
|
||||
import DataTableActions from './data-table-actions.svelte';
|
||||
import { Header } from '$lib/components/ui/alert-dialog';
|
||||
|
||||
export function createColumns(onSuccess?: () => void): ColumnDef<Equivalency>[] {
|
||||
export function createColumns(onSuccess?: () => void): ColumnDef<EquivalencyItem>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'fraccion_mex',
|
||||
header: 'Fracción MX',
|
||||
cell: ({ row }) => row.original.fraccion_mex || 'N/A'
|
||||
accessorKey: 'original_field',
|
||||
header: 'Desde código',
|
||||
cell: ({ row }) => row.original.original_field || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'fraccion_us',
|
||||
header: 'Fracción US',
|
||||
cell: ({ row }) => row.original.fraccion_us || 'N/A'
|
||||
accessorKey: 'external_field',
|
||||
header: 'Hacia código',
|
||||
cell: ({ row }) => row.original.external_field || 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
header: 'Descripción',
|
||||
cell: ({ row }) => row.original.description || 'N/A'
|
||||
accessorKey: 'conversion_factor',
|
||||
header: 'Factor de conversión',
|
||||
cell: ({ row }) => (row.original.conversion_factor ?? 0).toString()
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import UnitMeasureSelectorDialog from '$lib/components/dashboard/goods/modales/unit-measure-dialog.svelte';
|
||||
import { FolderSearch, Scale } from 'lucide-svelte';
|
||||
import {
|
||||
createEquivalency,
|
||||
updateEquivalency,
|
||||
type Equivalency
|
||||
createEquivalencyItem,
|
||||
updateEquivalencyItem,
|
||||
type EquivalencyItem
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
|
||||
import { obtenerAtajosFormularioEquivalencias } from '$lib/config/shortcuts/dashboard/general_catalogs/equivalencies/edit';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
@@ -17,46 +18,50 @@
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: Equivalency | null;
|
||||
item?: EquivalencyItem | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
// Atajos
|
||||
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(
|
||||
isEdit ? `Editar Equivalencia ${item?.fraccion_mex || ''}` : 'Nueva Equivalencia'
|
||||
isEdit
|
||||
? `Editar Equivalencia ${item?.original_field || ''} → ${item?.external_field || ''}`
|
||||
: 'Nueva Equivalencia'
|
||||
);
|
||||
|
||||
// Estado del formulario
|
||||
let formData = $state({
|
||||
fraccion_mex: '',
|
||||
fraccion_us: '',
|
||||
description: ''
|
||||
original_field: '',
|
||||
external_field: '',
|
||||
conversion_factor: ''
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
// Cargar datos
|
||||
let showFromUomModal = $state(false);
|
||||
let showToUomModal = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (item) {
|
||||
formData = {
|
||||
fraccion_mex: item.fraccion_mex || '',
|
||||
fraccion_us: item.fraccion_us || '',
|
||||
description: item.description || ''
|
||||
};
|
||||
} else {
|
||||
// Reset
|
||||
formData = {
|
||||
fraccion_mex: '',
|
||||
fraccion_us: '',
|
||||
description: ''
|
||||
};
|
||||
}
|
||||
error = null;
|
||||
if (!open) return;
|
||||
|
||||
if (item) {
|
||||
formData = {
|
||||
original_field: item.original_field || '',
|
||||
external_field: item.external_field || '',
|
||||
conversion_factor:
|
||||
item.conversion_factor !== null && item.conversion_factor !== undefined
|
||||
? item.conversion_factor.toString()
|
||||
: ''
|
||||
};
|
||||
} else {
|
||||
formData = {
|
||||
original_field: '',
|
||||
external_field: '',
|
||||
conversion_factor: ''
|
||||
};
|
||||
}
|
||||
|
||||
error = null;
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
@@ -70,15 +75,24 @@
|
||||
error = null;
|
||||
|
||||
try {
|
||||
let response;
|
||||
if (isEdit && item) {
|
||||
response = await updateEquivalency(item.id, formData, companyId);
|
||||
} else {
|
||||
response = await createEquivalency(formData, companyId);
|
||||
}
|
||||
if (!formData.original_field.trim()) throw new Error('Código origen requerido');
|
||||
if (!formData.external_field.trim()) throw new Error('Código destino requerido');
|
||||
if (formData.conversion_factor === '' || formData.conversion_factor === null)
|
||||
throw new Error('Factor de conversión requerido');
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
const factor = parseFloat(formData.conversion_factor);
|
||||
if (isNaN(factor)) throw new Error('El factor de conversión debe ser un número válido');
|
||||
|
||||
const dataToSend = {
|
||||
original_field: formData.original_field.trim(),
|
||||
external_field: formData.external_field.trim(),
|
||||
conversion_factor: factor
|
||||
};
|
||||
|
||||
if (isEdit && item) {
|
||||
await updateEquivalencyItem(item.id, dataToSend, companyId);
|
||||
} else {
|
||||
await createEquivalencyItem(dataToSend, companyId);
|
||||
}
|
||||
|
||||
open = false;
|
||||
@@ -111,46 +125,104 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="fraccion_mex" class="text-right">Fracción MX</Label>
|
||||
<Input
|
||||
id="fraccion_mex"
|
||||
bind:value={formData.fraccion_mex}
|
||||
class="col-span-3"
|
||||
maxlength={10}
|
||||
required
|
||||
placeholder="Ej. 8544.11.01"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-4 py-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="from_unit_code">
|
||||
Código origen <span class="text-destructive">*</span>
|
||||
</Label>
|
||||
<div class="flex gap-2">
|
||||
<div class="relative w-full">
|
||||
<Scale class="absolute top-2.5 left-3 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="from_unit_code"
|
||||
bind:value={formData.original_field}
|
||||
readonly
|
||||
onclick={() => (showFromUomModal = true)}
|
||||
class="cursor-pointer pl-9 font-mono"
|
||||
placeholder="Seleccione..."
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
type="button"
|
||||
onclick={() => (showFromUomModal = true)}
|
||||
disabled={loading}
|
||||
class="shrink-0"
|
||||
>
|
||||
<FolderSearch class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="fraccion_us" class="text-right">Fracción US</Label>
|
||||
<Input
|
||||
id="fraccion_us"
|
||||
bind:value={formData.fraccion_us}
|
||||
class="col-span-3"
|
||||
maxlength={100}
|
||||
required
|
||||
placeholder="Ej. 8544.11.00"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="to_unit_code">
|
||||
Código destino <span class="text-destructive">*</span>
|
||||
</Label>
|
||||
<div class="flex gap-2">
|
||||
<div class="relative w-full">
|
||||
<Scale class="absolute top-2.5 left-3 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="to_unit_code"
|
||||
bind:value={formData.external_field}
|
||||
readonly
|
||||
onclick={() => (showToUomModal = true)}
|
||||
class="cursor-pointer pl-9 font-mono"
|
||||
placeholder="Seleccione..."
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
type="button"
|
||||
onclick={() => (showToUomModal = true)}
|
||||
disabled={loading}
|
||||
class="shrink-0"
|
||||
>
|
||||
<FolderSearch class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="description" class="text-right">Descripción</Label>
|
||||
<Input
|
||||
id="description"
|
||||
bind:value={formData.description}
|
||||
class="col-span-3"
|
||||
maxlength={200}
|
||||
/>
|
||||
<div class="grid gap-2">
|
||||
<Label for="conversion_factor">
|
||||
Factor de conversión <span class="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="conversion_factor"
|
||||
type="number"
|
||||
step="0.000001"
|
||||
bind:value={formData.conversion_factor}
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => (open = false)}>Cancelar</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : 'Guardar'}
|
||||
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
<UnitMeasureSelectorDialog
|
||||
bind:open={showFromUomModal}
|
||||
onSelect={(u) => {
|
||||
formData.original_field = u.code;
|
||||
}}
|
||||
/>
|
||||
|
||||
<UnitMeasureSelectorDialog
|
||||
bind:open={showToUomModal}
|
||||
onSelect={(u) => {
|
||||
formData.external_field = u.code;
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import { EllipsisVertical, Pencil, Trash2, LoaderCircle } from 'lucide-svelte';
|
||||
import type { Equivalency } from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
|
||||
import { deleteEquivalency } from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
|
||||
import type { EquivalencyItem } from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
|
||||
import { deleteEquivalencyItem } from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import CreateEditDialog from './create-edit-dialog.svelte';
|
||||
|
||||
@@ -11,14 +11,14 @@
|
||||
item,
|
||||
onSuccess
|
||||
}: {
|
||||
item: Equivalency;
|
||||
item: EquivalencyItem;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let dialogOpen = $state(false);
|
||||
let selectedItem = $state<Equivalency | null>(null);
|
||||
let selectedItem = $state<EquivalencyItem | null>(null);
|
||||
|
||||
async function handleDelete() {
|
||||
if (!confirm('¿Está seguro de eliminar esta equivalencia?')) {
|
||||
@@ -33,7 +33,7 @@
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
const response = await deleteEquivalency(item.id, companyId);
|
||||
const response = await deleteEquivalencyItem(item.id, companyId);
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
|
||||
@@ -24,11 +24,11 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
}
|
||||
|
||||
const filters: Record<string, string> = {};
|
||||
const identifier = url.searchParams.get('identifier');
|
||||
const description = url.searchParams.get('description');
|
||||
const fromUnitCode = url.searchParams.get('from_unit_code');
|
||||
const toUnitCode = url.searchParams.get('to_unit_code');
|
||||
|
||||
if (identifier) filters.identifier = identifier;
|
||||
if (description) filters.description = description;
|
||||
if (fromUnitCode) filters.from_unit_code = fromUnitCode;
|
||||
if (toUnitCode) filters.to_unit_code = toUnitCode;
|
||||
|
||||
const queryParams = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
@@ -37,7 +37,7 @@ export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => {
|
||||
...filters
|
||||
});
|
||||
|
||||
const response = await authenticatedFetch(`v1/a76/equivalencies/?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
|
||||
const response = await authenticatedFetch(`v1/a76/equivalencies/items/?${queryParams.toString()}`, { method: 'GET' }, cookies, fetch);
|
||||
|
||||
if (!response.ok) {
|
||||
return { error: 'Failed to load', equivalencies: { items: [], total: 0, page, page_size: pageSize, pages: 0 } };
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
);
|
||||
|
||||
// Filtros
|
||||
let searchFraccion = $state($page.url.searchParams.get('fraccion_mex') || '');
|
||||
let searchDesc = $state($page.url.searchParams.get('description') || '');
|
||||
let searchFrom = $state($page.url.searchParams.get('from_unit_code') || '');
|
||||
let searchTo = $state($page.url.searchParams.get('to_unit_code') || '');
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
function handleSearch() {
|
||||
@@ -35,11 +35,11 @@
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
const url = new URL($page.url);
|
||||
if (searchFraccion) url.searchParams.set('fraccion_mex', searchFraccion);
|
||||
else url.searchParams.delete('fraccion_mex');
|
||||
if (searchFrom) url.searchParams.set('from_unit_code', searchFrom);
|
||||
else url.searchParams.delete('from_unit_code');
|
||||
|
||||
if (searchDesc) url.searchParams.set('description', searchDesc);
|
||||
else url.searchParams.delete('description');
|
||||
if (searchTo) url.searchParams.set('to_unit_code', searchTo);
|
||||
else url.searchParams.delete('to_unit_code');
|
||||
|
||||
url.searchParams.set('page', '1');
|
||||
goto(url, { keepFocus: true, noScroll: true });
|
||||
@@ -67,15 +67,15 @@
|
||||
<div class="flex gap-4 items-end">
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por fracción MX..."
|
||||
bind:value={searchFraccion}
|
||||
placeholder="Buscar por código origen..."
|
||||
bind:value={searchFrom}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid w-full max-w-sm items-center gap-1.5">
|
||||
<Input
|
||||
placeholder="Buscar por descripción..."
|
||||
bind:value={searchDesc}
|
||||
placeholder="Buscar por código destino..."
|
||||
bind:value={searchTo}
|
||||
oninput={handleSearch}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user