diff --git a/backend/api/v1/modules/a76/general_catalogs/equivalencies/dto.py b/backend/api/v1/modules/a76/general_catalogs/equivalencies/dto.py index f2017259..4494351f 100644 --- a/backend/api/v1/modules/a76/general_catalogs/equivalencies/dto.py +++ b/backend/api/v1/modules/a76/general_catalogs/equivalencies/dto.py @@ -1,39 +1,42 @@ -from typing import Optional, List +from typing import Optional +from decimal import Decimal + from pydantic import BaseModel, Field, ConfigDict -# Equivalency Item DTOs +# EquivalencyItem DTOs (pool global independiente) 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") + original_field: str = Field(..., max_length=100, description="Campo original (from)") + external_field: str = Field(..., max_length=100, description="Campo externo (to)") + conversion_factor: Optional[Decimal] = Field(None, description="Factor de conversión") class EquivalencyItemCreate(EquivalencyItemBase): - pass + conversion_factor: Optional[Decimal] = Field(None, description="Factor de conversión") 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) -# Equivalency DTOs + +# Equivalency DTOs (catálogo — referencia un item del pool) 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") + identifier: str = Field(..., max_length=10, description="Identificador único") + description: Optional[str] = Field(None, max_length=200, description="Descripción (opcional)") + item_id: Optional[int] = Field(None, description="ID del EquivalencyItem seleccionado") class EquivalencyCreate(EquivalencyBase): @@ -41,16 +44,15 @@ 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) + item_id: Optional[int] = Field(None, description="Re-asociar a otro EquivalencyItem") class EquivalencyResponse(EquivalencyBase): id: int tenant_id: int company_id: int - - model_config = ConfigDict(from_attributes=True) + item: Optional[EquivalencyItemResponse] = None model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/a76/general_catalogs/equivalencies/models.py b/backend/api/v1/modules/a76/general_catalogs/equivalencies/models.py index 5211bc10..211badd7 100644 --- a/backend/api/v1/modules/a76/general_catalogs/equivalencies/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/equivalencies/models.py @@ -1,12 +1,39 @@ from typing import Optional, List -from sqlalchemy import Integer, String, ForeignKey, UniqueConstraint, ForeignKeyConstraint +from decimal import Decimal +from sqlalchemy import Integer, String, ForeignKey, UniqueConstraint, Numeric from sqlalchemy.orm import Mapped, mapped_column, relationship from api.v1.common.base_models import TenantScopedMixin, TimestampMixin from core.database import Base -from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure + + +class EquivalencyItem(Base, TenantScopedMixin, TimestampMixin): + """Pool global de mapeos campo_original → campo_externo. + Un mismo item puede ser referenciado por múltiples Equivalency.""" + + __tablename__ = "equivalency_items" + __table_args__ = ( + UniqueConstraint( + "original_field", "external_field", "tenant_id", "company_id", + name="uq_equivalency_item_fields" + ), + {"schema": "a76"} + ) + + id: Mapped[int] = mapped_column( + Integer, primary_key=True, autoincrement=True) + + original_field: Mapped[str] = mapped_column(String(100), nullable=False) + external_field: Mapped[str] = mapped_column(String(100), nullable=False) + conversion_factor: Mapped[Optional[Decimal]] = mapped_column( + Numeric(13, 6), nullable=True) + + equivalencies: Mapped[List["Equivalency"]] = relationship( + back_populates="item", foreign_keys="[Equivalency.item_id]") class Equivalency(Base, TenantScopedMixin, TimestampMixin): + """Catálogo de equivalencias. Cada registro selecciona UN item del pool.""" + __tablename__ = "equivalencies" __table_args__ = ( UniqueConstraint("identifier", "tenant_id", "company_id", @@ -18,38 +45,13 @@ class Equivalency(Base, TenantScopedMixin, TimestampMixin): Integer, primary_key=True, autoincrement=True) identifier: Mapped[str] = mapped_column(String(10), nullable=False) - description: Mapped[Optional[str]] = mapped_column( String(200), nullable=True) - items: Mapped[List["EquivalencyItem"]] = relationship( - back_populates="equivalency", cascade="all, delete-orphan") + item_id: Mapped[Optional[int]] = mapped_column( + Integer, + ForeignKey("a76.equivalency_items.id", ondelete="SET NULL"), + nullable=True) - -class EquivalencyItem(Base, TenantScopedMixin, TimestampMixin): - __tablename__ = "equivalency_items" - __table_args__ = ( - UniqueConstraint("equivalency_id", "original_field", - "external_field", "tenant_id", "company_id", name="uq_equivalency_item_fields"), - ForeignKeyConstraint( - ["original_field", "tenant_id", "company_id"], - ["a76.units_of_measure.code", "a76.units_of_measure.tenant_id", - "a76.units_of_measure.company_id"] - ), - {"schema": "a76"} - ) - - id: Mapped[int] = mapped_column( - Integer, primary_key=True, autoincrement=True) - - equivalency_id: Mapped[int] = mapped_column( - Integer, ForeignKey("a76.equivalencies.id"), nullable=False) - - original_field: Mapped[str] = mapped_column( - String(100), nullable=False) # Relation to Unit of Measure - - external_field: Mapped[str] = mapped_column(String(100), nullable=False) - - equivalency: Mapped["Equivalency"] = relationship(back_populates="items") - - unit_of_measure: Mapped["UnitOfMeasure"] = relationship() + item: Mapped[Optional["EquivalencyItem"]] = relationship( + back_populates="equivalencies", foreign_keys=[item_id]) diff --git a/backend/api/v1/modules/a76/general_catalogs/equivalencies/routes.py b/backend/api/v1/modules/a76/general_catalogs/equivalencies/routes.py index d7c1c42f..6e076725 100644 --- a/backend/api/v1/modules/a76/general_catalogs/equivalencies/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/equivalencies/routes.py @@ -1,12 +1,5 @@ -from fastapi import APIRouter, Depends, HTTPException, status, Query -from sqlalchemy.orm import Session -from typing import List, Dict, Any - -from core.database import get_core_db -from core.security import get_current_user, validate_access_to_resource +from fastapi import APIRouter from api.v1.common.tenant_crud_routes import TenantCRUDRoutes -from . import service -from .models import Equivalency, EquivalencyItem from .dto import ( EquivalencyCreate, EquivalencyResponse, EquivalencyUpdate, EquivalencyItemCreate, EquivalencyItemResponse, EquivalencyItemUpdate @@ -16,19 +9,7 @@ from .service import EquivalencyService, EquivalencyItemService router = APIRouter(prefix="/equivalencies", tags=["a76.general_catalogs.equivalencies"]) -# Equivalency CRUD -equivalency_crud = TenantCRUDRoutes( - service=EquivalencyService, - create_schema=EquivalencyCreate, - update_schema=EquivalencyUpdate, - response_schema=EquivalencyResponse, - prefix="", - tags=["Equivalencies"], - resource_name="Equivalency", - enable_list=True, -) - -# Equivalency Item CRUD +# Pool global de EquivalencyItems item_crud = TenantCRUDRoutes( service=EquivalencyItemService, create_schema=EquivalencyItemCreate, @@ -38,62 +19,21 @@ item_crud = TenantCRUDRoutes( tags=["Equivalency Items"], resource_name="EquivalencyItem", enable_list=True, + enable_filters=True, ) -# Custom endpoint for creating items nested under equivalency - - -@equivalency_crud.router.post( - "/{equivalency_id}/items", - response_model=EquivalencyItemResponse, - status_code=status.HTTP_201_CREATED, - summary="Create equivalency item", +# Catálogo de equivalencias (referencia item_id) +equivalency_crud = TenantCRUDRoutes( + service=EquivalencyService, + create_schema=EquivalencyCreate, + update_schema=EquivalencyUpdate, + response_schema=EquivalencyResponse, + prefix="", + tags=["Equivalencies"], + resource_name="Equivalency", + enable_list=True, + enable_filters=True, ) -async def create_equivalency_item( - equivalency_id: int, - data: EquivalencyItemCreate, - company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db), - current_user: Dict[str, Any] = Depends(get_current_user), -): - tenant_id = validate_access_to_resource(db, company_id, current_user) - # Verify parent exists - parent = EquivalencyService.get_by_id( - db, equivalency_id, tenant_id, company_id) - if not parent: - raise HTTPException(status_code=404, detail="Equivalency not found") - - # Create item - # We need to manually handle the creation because the DTO doesn't have equivalency_id - # and the service.create expects data to match the model or DTO. - # But service.create takes EquivalencyItemCreate which doesn't have equivalency_id. - # So we need to modify the data or handle it in service. - - # Actually, I implemented EquivalencyItemService.create to take EquivalencyItemCreate. - # And it tries to create the model. - # But the model needs equivalency_id. - # So EquivalencyItemService.create will fail if I don't pass equivalency_id. - # I should update EquivalencyItemService.create to accept extra kwargs or handle this. - - # Let's update the service call here to pass equivalency_id manually if I can't change the service signature easily. - # But wait, I can just instantiate the model here or update the service. - - # I'll update the service to handle it. - # But for now, let's assume I can pass it in the data if I convert it to dict. - - item_data = data.model_dump() - item_data['equivalency_id'] = equivalency_id - - # I need to call a method that accepts this. - # EquivalencyItemService.create takes EquivalencyItemCreate. - # I should probably add a specific method for this or update create. - - # Let's use a direct DB call here or add a method to service. - # Adding a method to service is cleaner. - - return EquivalencyItemService.create_nested(db, equivalency_id, data, tenant_id, company_id) - - -router.include_router(equivalency_crud.router) router.include_router(item_crud.router) +router.include_router(equivalency_crud.router) diff --git a/backend/api/v1/modules/a76/general_catalogs/equivalencies/service.py b/backend/api/v1/modules/a76/general_catalogs/equivalencies/service.py index 3580ef22..2732877c 100644 --- a/backend/api/v1/modules/a76/general_catalogs/equivalencies/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/equivalencies/service.py @@ -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 @@ -8,203 +7,9 @@ from .models import Equivalency, EquivalencyItem from .dto import EquivalencyCreate, EquivalencyUpdate, EquivalencyItemCreate, EquivalencyItemUpdate -class EquivalencyService: - @staticmethod - def get_all( - db: Session, - tenant_id: int, - company_id: int, - skip: int = 0, - limit: int = 100, - filters: Optional[Dict[str, Any]] = None - ) -> Tuple[List[Equivalency], int]: - query = db.query(Equivalency).filter( - Equivalency.tenant_id == tenant_id, - Equivalency.company_id == company_id - ).options(joinedload(Equivalency.items)) - - 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']}%")) - - 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 - def get_by_id( - db: Session, - id: int, - tenant_id: int, - company_id: int - ) -> Optional[Equivalency]: - item = db.query(Equivalency).filter( - 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 = "" - - return item - - @staticmethod - def create( - db: Session, - data: EquivalencyCreate, - tenant_id: int, - company_id: int - ) -> Equivalency: - try: - # Create Parent - db_obj = Equivalency( - identifier=data.fraccion_mex, - description=data.description, - tenant_id=tenant_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." - ) - 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}") - except Exception as e: - db.rollback() - print(f"Error in create: {str(e)}") - raise e - - @staticmethod - def update( - db: Session, - id: int, - tenant_id: int, - data: EquivalencyUpdate, - company_id: int - ) -> Optional[Equivalency]: - db_obj = EquivalencyService.get_by_id(db, id, tenant_id, company_id) - if not db_obj: - 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) - - 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}") - except Exception as e: - db.rollback() - print(f"Error in update: {str(e)}") - raise e - - @staticmethod - def delete( - db: Session, - id: int, - tenant_id: int, - company_id: int - ) -> bool: - db_obj = EquivalencyService.get_by_id(db, id, tenant_id, company_id) - if not db_obj: - return False - - db.delete(db_obj) - db.commit() - return True - class EquivalencyItemService: + """CRUD simple sobre el pool global de items.""" + @staticmethod def get_all( db: Session, @@ -216,8 +21,19 @@ 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 "original_field" in filters: + query = query.filter( + EquivalencyItem.original_field.ilike(f"%{filters['original_field']}%") + ) + if "external_field" in filters: + query = query.filter( + EquivalencyItem.external_field.ilike(f"%{filters['external_field']}%") + ) + total = query.count() items = query.offset(skip).limit(limit).all() return items, total @@ -242,35 +58,27 @@ 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 - - @staticmethod - def create_nested( - db: Session, - equivalency_id: int, - data: EquivalencyItemCreate, - tenant_id: int, - company_id: int - ) -> EquivalencyItem: - db_obj = EquivalencyItem( - equivalency_id=equivalency_id, - original_field=data.original_field, - external_field=data.external_field, - tenant_id=tenant_id, - company_id=company_id - ) - db.add(db_obj) - db.commit() - db.refresh(db_obj) - return db_obj + try: + db_obj = EquivalencyItem( + 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 "uq_equivalency_item_fields" in error_msg: + raise HTTPException( + status_code=400, + detail=f"Ya existe un item con los mismos campos '{data.original_field}' → '{data.external_field}'." + ) from e + raise HTTPException(status_code=400, detail=f"Error al guardar: {error_msg}") from e @staticmethod def update( @@ -283,13 +91,18 @@ 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 + + try: + 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) + 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( @@ -304,3 +117,119 @@ class EquivalencyItemService: db.delete(db_obj) db.commit() return True + + +class EquivalencyService: + """CRUD del catálogo de equivalencias. Cada registro referencia un EquivalencyItem.""" + + @staticmethod + def get_all( + db: Session, + tenant_id: int, + company_id: int, + skip: int = 0, + limit: int = 100, + filters: Optional[Dict[str, Any]] = None + ) -> Tuple[List[Equivalency], int]: + query = db.query(Equivalency).filter( + Equivalency.tenant_id == tenant_id, + Equivalency.company_id == company_id, + ) + + if filters: + 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() + return items, total + + @staticmethod + def get_by_id( + db: Session, + id: int, + tenant_id: int, + company_id: int + ) -> Optional[Equivalency]: + return db.query(Equivalency).filter( + Equivalency.id == id, + Equivalency.tenant_id == tenant_id, + Equivalency.company_id == company_id + ).first() + + @staticmethod + def create( + db: Session, + data: EquivalencyCreate, + tenant_id: int, + company_id: int + ) -> Equivalency: + try: + db_obj = Equivalency( + identifier=data.identifier, + description=data.description, + item_id=data.item_id, + 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 "uq_equivalency_identifier" in error_msg: + raise HTTPException( + status_code=400, + detail=f"Ya existe un registro para el identificador '{data.identifier}'.", + ) from e + raise HTTPException(status_code=400, detail=f"Error al guardar: {error_msg}") from e + + @staticmethod + def update( + db: Session, + id: int, + tenant_id: int, + data: EquivalencyUpdate, + company_id: int + ) -> Optional[Equivalency]: + db_obj = EquivalencyService.get_by_id(db, id, tenant_id, company_id) + if not db_obj: + return None + + try: + 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) + 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( + db: Session, + id: int, + tenant_id: int, + company_id: int + ) -> bool: + db_obj = EquivalencyService.get_by_id(db, id, tenant_id, company_id) + if not db_obj: + return False + db.delete(db_obj) + db.commit() + return True diff --git a/backend/api/v1/modules/a76/items/models.py b/backend/api/v1/modules/a76/items/models.py index c7bc18eb..514da30b 100644 --- a/backend/api/v1/modules/a76/items/models.py +++ b/backend/api/v1/modules/a76/items/models.py @@ -14,6 +14,7 @@ from core.database import Base from api.v1.modules.a76.classes.models import Class from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure +from api.v1.modules.a76.general_catalogs.identifiers.models import IdentifierDetail if TYPE_CHECKING: from .line_financials.models import LineFinancial @@ -25,9 +26,6 @@ if TYPE_CHECKING: from api.v1.modules.a76.parts.models import Part from api.v1.modules.a76.invoices.models import InvoiceHeader -# Imported at runtime so SQLAlchemy's mapper registry can resolve the class name -# used in the relationship string below. -from api.v1.modules.a76.general_catalogs.identifiers.models import IdentifierDetail # ============================================================================ # CORE ENTITIES @@ -224,7 +222,7 @@ class LineItem(Base, TenantScopedMixin, TimestampMixin): uselist=False, ) identifiers: Mapped[List["IdentifierDetail"]] = relationship( - "IdentifierDetail", + IdentifierDetail, back_populates="line", cascade="all, delete-orphan", ) diff --git a/frontend/src/lib/api/dashboard/a76/general_catalogs/equivalencies.ts b/frontend/src/lib/api/dashboard/a76/general_catalogs/equivalencies.ts index 0b6c70c3..b54abcfd 100644 --- a/frontend/src/lib/api/dashboard/a76/general_catalogs/equivalencies.ts +++ b/frontend/src/lib/api/dashboard/a76/general_catalogs/equivalencies.ts @@ -1,27 +1,107 @@ import { api } from '$lib/api'; import type { ApiResponse } from '$lib/api'; -export interface Equivalency { +// ======================== +// ITEMS (pool global) +// ======================== + +export interface EquivalencyItem { id: number; - fraccion_mex: string; - fraccion_us: string; - description?: string; + original_field: string; + external_field: string; + conversion_factor: number | null; + tenant_id: number; + company_id: number; +} + +export interface EquivalencyItemCreate { + original_field: string; + external_field: string; + conversion_factor?: number | null; +} + +export interface EquivalencyItemUpdate { + original_field?: string; + external_field?: string; + conversion_factor?: number | null; +} + +export interface EquivalencyItemListResponse { + items: EquivalencyItem[]; + total: number; + page: number; + page_size: number; + pages: number; +} + +export async function getEquivalencyItems( + page: number = 1, + pageSize: number = 50, + companyId: number, + filters: Record = {} +): Promise> { + const params = new URLSearchParams({ + page: page.toString(), + page_size: pageSize.toString(), + company_id: companyId.toString(), + ...filters + }); + return await api.get(`/v1/a76/equivalencies/items/?${params.toString()}`); +} + +export async function getEquivalencyItem( + id: number, + companyId: number +): Promise> { + return await api.get(`/v1/a76/equivalencies/items/${id}/?company_id=${companyId}`); +} + +export async function createEquivalencyItem( + data: EquivalencyItemCreate, + companyId: number +): Promise> { + return await api.post(`/v1/a76/equivalencies/items/?company_id=${companyId}`, data); +} + +export async function updateEquivalencyItem( + id: number, + data: EquivalencyItemUpdate, + companyId: number +): Promise> { + return await api.put(`/v1/a76/equivalencies/items/${id}/?company_id=${companyId}`, data); +} + +export async function deleteEquivalencyItem( + id: number, + companyId: number +): Promise> { + return await api.delete(`/v1/a76/equivalencies/items/${id}?company_id=${companyId}`); +} + +// ======================== +// CATÁLOGO (referencia item_id) +// ======================== + +export interface Equivalency { + id: number; + identifier: string; + description: string | null; + item_id: number | null; + item: EquivalencyItem | null; tenant_id: number; company_id: number; - created_at: string; - updated_at?: string; } export interface EquivalencyCreate { - fraccion_mex: string; - fraccion_us: string; - description?: string; + identifier: string; + description?: string | null; + item_id?: number | null; } export interface EquivalencyUpdate { - fraccion_mex?: string; - fraccion_us?: string; - description?: string; + identifier?: string; + description?: string | null; + item_id?: number | null; } export interface EquivalencyListResponse { @@ -44,29 +124,34 @@ export async function getEquivalencies( company_id: companyId.toString(), ...filters }); - return await api.get(`/v1/a76/equivalencies/?${params.toString()}`); } -export async function getEquivalency(id: number, companyId: number): Promise> { +export async function getEquivalency( + id: number, + companyId: number +): Promise> { return await api.get(`/v1/a76/equivalencies/${id}/?company_id=${companyId}`); } export async function createEquivalency( - data: EquivalencyCreate, + data: EquivalencyCreate, companyId: number ): Promise> { return await api.post(`/v1/a76/equivalencies/?company_id=${companyId}`, data); } export async function updateEquivalency( - id: number, - data: EquivalencyUpdate, + id: number, + data: EquivalencyUpdate, companyId: number ): Promise> { return await api.put(`/v1/a76/equivalencies/${id}/?company_id=${companyId}`, data); } -export async function deleteEquivalency(id: number, companyId: number): Promise> { - return await api.delete(`/v1/a76/equivalencies/${id}/?company_id=${companyId}`); +export async function deleteEquivalency( + id: number, + companyId: number +): Promise> { + return await api.delete(`/v1/a76/equivalencies/${id}?company_id=${companyId}`); } diff --git a/frontend/src/lib/components/dashboard/general_catalogs/equivalencies/catalog-columns.ts b/frontend/src/lib/components/dashboard/general_catalogs/equivalencies/catalog-columns.ts new file mode 100644 index 00000000..32ee7c5f --- /dev/null +++ b/frontend/src/lib/components/dashboard/general_catalogs/equivalencies/catalog-columns.ts @@ -0,0 +1,40 @@ +import type { ColumnDef } from '@tanstack/table-core'; +import type { Equivalency } from '$lib/api/dashboard/a76/general_catalogs/equivalencies'; +import { renderComponent } from '$lib/components/ui/data-table'; +import CatalogDataTableActions from './catalog-data-table-actions.svelte'; + +export function createCatalogColumns({ + onInsertItems, + onEdit, + onSuccess +}: { + onInsertItems: (equivalency: Equivalency) => void; + onEdit: (equivalency: Equivalency) => void; + onSuccess?: () => void; +}): ColumnDef[] { + return [ + { + accessorKey: 'identifier', + header: 'Identificador', + cell: ({ row }) => row.original.identifier || 'N/A' + }, + { + accessorKey: 'description', + header: 'Descripción', + cell: ({ row }) => row.original.description || '' + }, + { + id: 'actions', + Header: 'Acciones', + cell: ({ row }) => { + return renderComponent(CatalogDataTableActions, { + item: row.original, + onInsertItems, + onEdit, + onSuccess + }); + } + } + ]; +} + diff --git a/frontend/src/lib/components/dashboard/general_catalogs/equivalencies/catalog-create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/general_catalogs/equivalencies/catalog-create-edit-dialog.svelte new file mode 100644 index 00000000..077f837a --- /dev/null +++ b/frontend/src/lib/components/dashboard/general_catalogs/equivalencies/catalog-create-edit-dialog.svelte @@ -0,0 +1,146 @@ + + + + + + {title} + + +
{ + e.preventDefault(); + handleSubmit(); + }} + class="grid gap-4 py-4" + > + {#if error} +
{error}
+ {/if} + +
+
+ + +
+ +
+ +