diff --git a/backend/api/v1/modules/a24/inv/location/__init__.py b/backend/api/v1/modules/a24/inv/location/__init__.py deleted file mode 100644 index 935e07ae..00000000 --- a/backend/api/v1/modules/a24/inv/location/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -Módulo de localización -""" diff --git a/backend/api/v1/modules/a24/inv/location/dto.py b/backend/api/v1/modules/a24/inv/location/dto.py deleted file mode 100644 index a6fc6735..00000000 --- a/backend/api/v1/modules/a24/inv/location/dto.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -DTOs (Data Transfer Objects) para módulo de localización -""" - -from typing import Optional - -from pydantic import BaseModel, Field - - -class LocationCreateDTO(BaseModel): - """DTO para crear una localización""" - - code: str = Field(..., max_length=5, description="Location code") - description: Optional[str] = Field( - None, max_length=200, description="Location description" - ) - - class Config: - from_attributes = True - - -class LocationUpdateDTO(BaseModel): - """DTO para actualizar una localización""" - - code: Optional[str] = Field( - None, max_length=5, description="Location code") - description: Optional[str] = Field( - None, max_length=200, description="Location description" - ) - - class Config: - from_attributes = True - - -class LocationResponseDTO(BaseModel): - """DTO para responder con datos de una localización""" - - id: int - code: str - description: Optional[str] = None - - class Config: - from_attributes = True diff --git a/backend/api/v1/modules/a24/inv/location/models.py b/backend/api/v1/modules/a24/inv/location/models.py deleted file mode 100644 index 53d3202c..00000000 --- a/backend/api/v1/modules/a24/inv/location/models.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -Modelos ORM para gestión de localización -""" - -from typing import Optional - -from api.v1.common.base_models import TenantScopedMixin, TimestampMixin -from core.database import Base -from sqlalchemy import Integer, PrimaryKeyConstraint, String, UniqueConstraint -from sqlalchemy.orm import Mapped, mapped_column - - -class Location(Base, TenantScopedMixin, TimestampMixin): - """ - Modelo para la tabla Location - Localización - """ - - __tablename__ = "location" # SLocalizacion - __table_args__ = ( - PrimaryKeyConstraint("id", name="location_pkey"), - UniqueConstraint("code", name="location_code_unique"), - {"schema": "a24"}, - ) - - # Primary key - id: Mapped[int] = mapped_column( - Integer, primary_key=True, autoincrement=True) - - # Location code (unique) - code: Mapped[str] = mapped_column(String(5), nullable=False, unique=True) - - # Location description - description: Mapped[Optional[str]] = mapped_column(String(200)) - - def __repr__(self): - return f"" diff --git a/backend/api/v1/modules/a24/inv/location/routes.py b/backend/api/v1/modules/a24/inv/location/routes.py deleted file mode 100644 index 0920bd4e..00000000 --- a/backend/api/v1/modules/a24/inv/location/routes.py +++ /dev/null @@ -1,136 +0,0 @@ -""" -Rutas para gestión de localización -""" - -from typing import List - -from fastapi import APIRouter, Depends, HTTPException, Query, status -from sqlalchemy.orm import Session - -from core.database import get_core_db -from .dto import LocationCreateDTO, LocationResponseDTO, LocationUpdateDTO -from .models import Location -from .service import LocationService - -router = APIRouter(prefix="/locations", tags=["locations"]) - - -@router.get( - "", - response_model=dict, - summary="Get all locations", -) -async def get_all_locations( - skip: int = Query(0, ge=0), - limit: int = Query(50, ge=1, le=100), - code: str = Query(None), - description: str = Query(None), - db: Session = Depends(get_core_db), -): - """Get all locations with optional filtering and pagination""" - filters = {} - if code: - filters["code"] = code - if description: - filters["description"] = description - - locations, total = LocationService.get_all(db, skip, limit, filters) - - return { - "data": [LocationResponseDTO.model_validate(location) for location in locations], - "total": total, - "skip": skip, - "limit": limit, - } - - -@router.get( - "/{location_id}", - response_model=LocationResponseDTO, - summary="Get location by ID", -) -async def get_location( - location_id: int, - db: Session = Depends(get_core_db), -): - """Get a location by its ID""" - location = LocationService.get_by_id(db, location_id) - if not location: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Location not found", - ) - return LocationResponseDTO.model_validate(location) - - -@router.get( - "/code/{code}", - response_model=LocationResponseDTO, - summary="Get location by code", -) -async def get_location_by_code( - code: str, - db: Session = Depends(get_core_db), -): - """Get a location by its code""" - location = LocationService.get_by_code(db, code) - if not location: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Location not found", - ) - return LocationResponseDTO.model_validate(location) - - -@router.post( - "", - response_model=LocationResponseDTO, - status_code=status.HTTP_201_CREATED, - summary="Create location", -) -async def create_location( - location_data: LocationCreateDTO, - db: Session = Depends(get_core_db), -): - """Create a new location""" - location = LocationService.create(db, location_data) - return LocationResponseDTO.model_validate(location) - - -@router.put( - "/{location_id}", - response_model=LocationResponseDTO, - summary="Update location", -) -async def update_location( - location_id: int, - location_data: LocationUpdateDTO, - db: Session = Depends(get_core_db), -): - """Update a location""" - location = LocationService.update(db, location_id, location_data) - if not location: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Location not found", - ) - return LocationResponseDTO.model_validate(location) - - -@router.delete( - "/{location_id}", - status_code=status.HTTP_204_NO_CONTENT, - summary="Delete location", -) -async def delete_location( - location_id: int, - db: Session = Depends(get_core_db), -): - """Delete a location""" - success = LocationService.delete(db, location_id) - if not success: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="Location not found", - ) - return None diff --git a/backend/api/v1/modules/a24/inv/location/service.py b/backend/api/v1/modules/a24/inv/location/service.py deleted file mode 100644 index 0ef5726c..00000000 --- a/backend/api/v1/modules/a24/inv/location/service.py +++ /dev/null @@ -1,136 +0,0 @@ -""" -Capa de servicio para lógica de negocio de localización -""" - -import logging -from typing import Any, Dict, List, Optional, Tuple - -from fastapi import HTTPException -from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import Session - -from .dto import LocationCreateDTO, LocationResponseDTO, LocationUpdateDTO -from .models import Location - -logger = logging.getLogger(__name__) - - -class LocationService: - """Servicio para gestión de localización""" - - def __init__(self, db: Session): - self.db = db - - @staticmethod - def get_all( - db: Session, - skip: int = 0, - limit: int = 50, - filters: Optional[Dict[str, Any]] = None, - ) -> Tuple[List[Location], int]: - """Get all locations with pagination""" - query = db.query(Location) - - if filters: - if filters.get("code"): - query = query.filter( - Location.code.ilike(f"%{filters['code']}%")) - if filters.get("description"): - query = query.filter( - Location.description.ilike(f"%{filters['description']}%") - ) - - total = query.count() - locations = query.offset(skip).limit(limit).all() - - return locations, total - - @staticmethod - def get_by_id(db: Session, location_id: int) -> Optional[Location]: - """Get location by ID""" - return db.query(Location).filter(Location.id == location_id).first() - - @staticmethod - def get_by_code(db: Session, code: str) -> Optional[Location]: - """Get location by code""" - return db.query(Location).filter(Location.code == code).first() - - @staticmethod - def create(db: Session, location_data: LocationCreateDTO) -> Location: - """Create a new location""" - try: - db_location = Location( - **location_data.model_dump(exclude_unset=True)) - - db.add(db_location) - db.commit() - db.refresh(db_location) - - return db_location - - except IntegrityError as e: - db.rollback() - logger.error(f"IntegrityError creating location: {str(e)}") - raise HTTPException( - status_code=400, - detail="Location code already exists", - ) - except Exception as e: - db.rollback() - logger.error(f"Error creating location: {str(e)}") - raise HTTPException( - status_code=500, detail="Error creating location") - - @staticmethod - def update( - db: Session, location_id: int, location_data: LocationUpdateDTO - ) -> Optional[Location]: - """Update a location""" - try: - db_location = db.query(Location).filter( - Location.id == location_id).first() - - if not db_location: - return None - - for key, value in location_data.model_dump(exclude_unset=True).items(): - setattr(db_location, key, value) - - db.commit() - db.refresh(db_location) - - return db_location - - except IntegrityError as e: - db.rollback() - logger.error(f"IntegrityError updating location: {str(e)}") - raise HTTPException( - status_code=400, - detail="Error updating location", - ) - except Exception as e: - db.rollback() - logger.error(f"Error updating location: {str(e)}") - raise HTTPException( - status_code=500, detail="Error updating location") - - @staticmethod - def delete(db: Session, location_id: int) -> bool: - """Delete a location""" - try: - db_location = db.query(Location).filter( - Location.id == location_id).first() - - if not db_location: - return False - - db.delete(db_location) - db.commit() - - return True - - except Exception as e: - db.rollback() - logger.error(f"Error deleting location: {str(e)}") - raise HTTPException( - status_code=500, detail="Error deleting location") diff --git a/backend/api/v1/modules/a76/general_catalogs/location/__init__.py b/backend/api/v1/modules/a76/general_catalogs/location/__init__.py new file mode 100644 index 00000000..5d2af8dd --- /dev/null +++ b/backend/api/v1/modules/a76/general_catalogs/location/__init__.py @@ -0,0 +1 @@ +# a76 general_catalogs.location diff --git a/backend/api/v1/modules/a76/general_catalogs/location/dto.py b/backend/api/v1/modules/a76/general_catalogs/location/dto.py new file mode 100644 index 00000000..b1be6e6b --- /dev/null +++ b/backend/api/v1/modules/a76/general_catalogs/location/dto.py @@ -0,0 +1,36 @@ +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field + +from .models import LocationSystem + + +class LocationBase(BaseModel): + clave_localizacion: str = Field( + ..., max_length=20, description="Clave/código de la localización" + ) + localizacion: Optional[str] = Field( + None, max_length=200, description="Nombre o descripción" + ) + system: LocationSystem = Field( + ..., description="Contexto: fixed_asset (FA) o inventory" + ) + + +class LocationCreate(LocationBase): + """Optional extra fields for fixed_asset; used only when system == FIXED_ASSET.""" + department: Optional[str] = Field(None, max_length=100) + responsible: Optional[str] = Field(None, max_length=200) + observations: Optional[str] = Field(None, description="Free text") + + +class LocationUpdate(BaseModel): + clave_localizacion: Optional[str] = Field(None, max_length=20) + localizacion: Optional[str] = Field(None, max_length=200) + system: Optional[LocationSystem] = None + + +class LocationResponse(LocationBase): + id: int + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/a76/general_catalogs/location/models.py b/backend/api/v1/modules/a76/general_catalogs/location/models.py new file mode 100644 index 00000000..47119425 --- /dev/null +++ b/backend/api/v1/modules/a76/general_catalogs/location/models.py @@ -0,0 +1,84 @@ +""" +Modelo ORM para catálogo de localización (a76). +Tabla compartida para contexto Fixed Asset (FA) e inventory. +""" + +import enum +from typing import Optional + +from sqlalchemy import Integer, String, Text, UniqueConstraint, ForeignKey +from sqlalchemy.orm import Mapped, mapped_column + +from api.v1.common.base_models import TenantScopedMixin, TimestampMixin +from core.database import Base + + +class LocationSystem(str, enum.Enum): + """Contexto de uso de la localización.""" + FIXED_ASSET = "fixed_asset" + INVENTORY = "inventory" + + +class Location(Base, TenantScopedMixin, TimestampMixin): + """ + Catálogo de localización (clave + localizacion), compartido por FA e inventory. + """ + + __tablename__ = "location" + __table_args__ = ( + UniqueConstraint( + "clave_localizacion", + "tenant_id", + "company_id", + "system", + name="uq_location_clave_tenant_company_system", + ), + {"schema": "a76"}, + ) + + id: Mapped[int] = mapped_column( + Integer, primary_key=True, autoincrement=True + ) + clave_localizacion: Mapped[str] = mapped_column(String(20), nullable=False) + localizacion: Mapped[Optional[str]] = mapped_column(String(200), nullable=True) + system: Mapped[str] = mapped_column( + String(20), nullable=False + ) # 'fixed_asset' | 'inventory' + + def __repr__(self) -> str: + return ( + f"" + ) + + +class FaLocationExt(Base, TenantScopedMixin, TimestampMixin): + """ + Extra info for Fixed Asset locations only (1:1 with Location). + Table: Fa_Location_Ext (fa_location_ext). + """ + + __tablename__ = "fa_location_ext" + __table_args__ = ( + UniqueConstraint("location_id", name="uq_fa_location_ext_location_id"), + {"schema": "a76"}, + ) + + id: Mapped[int] = mapped_column( + Integer, primary_key=True, autoincrement=True + ) + location_id: Mapped[int] = mapped_column( + Integer, + ForeignKey("a76.location.id", ondelete="CASCADE"), + nullable=False, + unique=True, + ) + department: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) + responsible: Mapped[Optional[str]] = mapped_column(String(200), nullable=True) + observations: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + + def __repr__(self) -> str: + return ( + f"" + ) diff --git a/backend/api/v1/modules/a76/general_catalogs/location/routes.py b/backend/api/v1/modules/a76/general_catalogs/location/routes.py new file mode 100644 index 00000000..bd9949a5 --- /dev/null +++ b/backend/api/v1/modules/a76/general_catalogs/location/routes.py @@ -0,0 +1,20 @@ +from fastapi import APIRouter + +from api.v1.common.tenant_crud_routes import TenantCRUDRoutes + +from .dto import LocationCreate, LocationResponse, LocationUpdate +from .models import Location +from .service import LocationService + +router = TenantCRUDRoutes( + service=LocationService, + create_schema=LocationCreate, + update_schema=LocationUpdate, + response_schema=LocationResponse, + prefix="/locations", + tags=["a76.general_catalogs.locations"], + resource_name="Location", + enable_list=True, + enable_filters=True, + max_page_size=1000, +).router diff --git a/backend/api/v1/modules/a76/general_catalogs/location/service.py b/backend/api/v1/modules/a76/general_catalogs/location/service.py new file mode 100644 index 00000000..fb48d4ba --- /dev/null +++ b/backend/api/v1/modules/a76/general_catalogs/location/service.py @@ -0,0 +1,154 @@ +from typing import Any, Dict, List, Optional, Tuple + +from sqlalchemy.orm import Session +from sqlalchemy.exc import IntegrityError + +from .models import Location, FaLocationExt +from .dto import LocationCreate, LocationUpdate + + +class LocationService: + @staticmethod + def get_all( + db: Session, + tenant_id: int, + company_id: Optional[int], + skip: int = 0, + limit: int = 100, + filters: Optional[Dict[str, Any]] = None, + ) -> Tuple[List[Location], int]: + query = db.query(Location).filter(Location.tenant_id == tenant_id) + + if company_id is not None: + query = query.filter(Location.company_id == company_id) + + if filters: + if filters.get("clave_localizacion"): + query = query.filter( + Location.clave_localizacion.ilike( + f"%{filters['clave_localizacion']}%" + ) + ) + if filters.get("localizacion"): + query = query.filter( + Location.localizacion.ilike(f"%{filters['localizacion']}%") + ) + if filters.get("system"): + query = query.filter(Location.system == filters["system"]) + + 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[Location]: + return ( + db.query(Location) + .filter( + Location.id == id, + Location.tenant_id == tenant_id, + Location.company_id == company_id, + ) + .first() + ) + + @staticmethod + def get_by_clave( + db: Session, + clave_localizacion: str, + tenant_id: int, + company_id: int, + system: Optional[str] = None, + ) -> Optional[Location]: + query = db.query(Location).filter( + Location.clave_localizacion == clave_localizacion, + Location.tenant_id == tenant_id, + Location.company_id == company_id, + ) + if system is not None: + query = query.filter(Location.system == system) + return query.first() + + @staticmethod + def create( + db: Session, + data: LocationCreate, + tenant_id: int, + company_id: int, + ) -> Location: + db_obj = Location( + clave_localizacion=data.clave_localizacion, + localizacion=data.localizacion, + system=data.system.value, + tenant_id=tenant_id, + company_id=company_id, + ) + db.add(db_obj) + try: + db.flush() # get db_obj.id without committing + if data.system.value == "fixed_asset" and ( + data.department is not None + or data.responsible is not None + or data.observations is not None + ): + ext = FaLocationExt( + location_id=db_obj.id, + department=data.department, + responsible=data.responsible, + observations=data.observations, + tenant_id=tenant_id, + company_id=company_id, + ) + db.add(ext) + db.commit() + db.refresh(db_obj) + return db_obj + except IntegrityError: + db.rollback() + raise ValueError("Ya existe una localización con esa clave y system") + + @staticmethod + def update( + db: Session, + id: int, + tenant_id: int, + data: LocationUpdate, + company_id: int, + ) -> Optional[Location]: + db_obj = LocationService.get_by_id(db, id, tenant_id, company_id) + if not db_obj: + return None + + update_dict = data.model_dump(exclude_unset=True) + for key, value in update_dict.items(): + if key == "system" and value is not None: + setattr(db_obj, key, value.value) + else: + setattr(db_obj, key, value) + + try: + db.commit() + db.refresh(db_obj) + return db_obj + except IntegrityError: + db.rollback() + raise ValueError("Ya existe una localización con esa clave y system") + + @staticmethod + def delete( + db: Session, + id: int, + tenant_id: int, + company_id: int, + ) -> bool: + db_obj = LocationService.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/general_catalogs/router.py b/backend/api/v1/modules/a76/general_catalogs/router.py index aaa47326..291dde07 100644 --- a/backend/api/v1/modules/a76/general_catalogs/router.py +++ b/backend/api/v1/modules/a76/general_catalogs/router.py @@ -25,12 +25,14 @@ from .error_catalogs.routes import router as error_catalogs_router from .doda.routes import router as doda_router from .prevalidators.routes import router as prevalidators_router from .electronic_notices.routes import router as electronic_notices_router +from .location.routes import router as location_router router = APIRouter() router.include_router(company_router, tags=["a76 / company"]) router.include_router(package_router) router.include_router(ports_router) +router.include_router(location_router) router.include_router(tariff_fractions_router) router.include_router(us_tariff_fractions_router) router.include_router(historical_tariff_fractions_router, prefix="/fractions/historical-tariff-fractions", tags=["a76 / historical_tariff_fractions"]) diff --git a/backend/main.py b/backend/main.py index e158475c..08987b2e 100644 --- a/backend/main.py +++ b/backend/main.py @@ -47,6 +47,7 @@ from api.v1.modules.a76.general_catalogs.legends.models import Legend from api.v1.modules.a76.general_catalogs.multi_currency_types.models import MultiCurrencyType from api.v1.modules.a76.general_catalogs.packages.models import Package from api.v1.modules.a76.general_catalogs.ports.models import Port +from api.v1.modules.a76.general_catalogs.location.models import Location, FaLocationExt from api.v1.modules.a76.general_catalogs.prevalidators.models import Prevalidator from api.v1.modules.a76.general_catalogs.seal.models import Seal from api.v1.modules.a76.general_catalogs.signatures.models import Signature @@ -255,6 +256,7 @@ from api.v1.modules.a76.general_catalogs.multi_currency_types.models import ( ) from api.v1.modules.a76.general_catalogs.packages.models import Package from api.v1.modules.a76.general_catalogs.ports.models import Port +from api.v1.modules.a76.general_catalogs.location.models import Location, FaLocationExt from api.v1.modules.a76.general_catalogs.prevalidators.models import Prevalidator from api.v1.modules.a76.general_catalogs.seal.models import Seal from api.v1.modules.a76.general_catalogs.signatures.models import Signature diff --git a/frontend/src/lib/api/dashboard/a76/general_catalogs/locations.ts b/frontend/src/lib/api/dashboard/a76/general_catalogs/locations.ts index 2139acb9..f45e8768 100644 --- a/frontend/src/lib/api/dashboard/a76/general_catalogs/locations.ts +++ b/frontend/src/lib/api/dashboard/a76/general_catalogs/locations.ts @@ -1,64 +1,84 @@ -import type { PaginatedResponse } from '$lib/types'; import { api } from '$lib/api'; +/** Contexto de uso: Fixed Asset (activo fijo) o inventario */ +export type LocationSystem = 'fixed_asset' | 'inventory'; + export interface Location { id: number; - location_code: string; - location_description: string | null; - company_id: number; - tenant_id: number; + clave_localizacion: string; + localizacion: string | null; + system: LocationSystem; } export interface LocationCreate { - location_code: string; - location_description?: string | null; + clave_localizacion: string; + localizacion?: string | null; + system: LocationSystem; + /** Used when system === 'fixed_asset' */ + department?: string | null; + responsible?: string | null; + observations?: string | null; } export interface LocationUpdate { - location_description?: string | null; + clave_localizacion?: string; + localizacion?: string | null; + system?: LocationSystem; } -export interface LocationListResponse extends PaginatedResponse { +export interface LocationListResponse { items: Location[]; + total: number; + page: number; + page_size: number; } export interface LocationFilters { - location_code?: string; - location_description?: string; + clave_localizacion?: string; + localizacion?: string; + system?: LocationSystem; page?: number; page_size?: number; } -import { portsApi, PortType } from './ports'; +const BASE_URL = '/v1/a76/locations'; + +function buildQuery(companyId: number, params?: LocationFilters): string { + const search = new URLSearchParams(); + search.set('company_id', String(companyId)); + if (params?.page != null) search.set('page', String(params.page)); + if (params?.page_size != null) search.set('page_size', String(params.page_size)); + if (params?.clave_localizacion) search.set('clave_localizacion', params.clave_localizacion); + if (params?.localizacion) search.set('localizacion', params.localizacion); + if (params?.system) search.set('system', params.system); + return search.toString(); +} export async function getLocations( companyId: number, filters?: LocationFilters ): Promise { - const res = await portsApi.list(companyId, filters || {}); - return (res.data || res) as unknown as LocationListResponse; + const q = buildQuery(companyId, filters); + const res = await api.get(`${BASE_URL}/?${q}`); + return (res.data ?? res) as LocationListResponse; } export async function getLocation( locationId: number, companyId: number ): Promise { - const res = await portsApi.get(locationId, companyId); - return (res.data || res) as unknown as Location; + const q = new URLSearchParams({ company_id: String(companyId) }); + const res = await api.get(`${BASE_URL}/${locationId}?${q}`); + return (res.data ?? res) as Location; } export async function createLocation( data: LocationCreate, companyId: number ): Promise { - const res = await portsApi.create({ - port_code: data.location_code, - location_code: data.location_code, - description: null, - location_description: data.location_description || null, - port_type: PortType.ENTRY - }, companyId); - return (res.data || res) as unknown as Location; + const q = new URLSearchParams({ company_id: String(companyId) }); + const res = await api.post(`${BASE_URL}/?${q}`, data); + return (res.data ?? res) as Location; } export async function updateLocation( @@ -66,15 +86,15 @@ export async function updateLocation( data: LocationUpdate, companyId: number ): Promise { - const res = await portsApi.update(locationId, { - location_description: data.location_description - }, companyId); - return (res.data || res) as unknown as Location; + const q = new URLSearchParams({ company_id: String(companyId) }); + const res = await api.put(`${BASE_URL}/${locationId}/?${q}`, data); + return (res.data ?? res) as Location; } export async function deleteLocation( locationId: number, companyId: number ): Promise { - await portsApi.delete(locationId, companyId); -} \ No newline at end of file + const q = new URLSearchParams({ company_id: String(companyId) }); + await api.delete(`${BASE_URL}/${locationId}?${q}`); +} diff --git a/frontend/src/lib/components/dashboard/general_catalogs/locations/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/general_catalogs/locations/create-edit-dialog.svelte index 794c7350..52197404 100644 --- a/frontend/src/lib/components/dashboard/general_catalogs/locations/create-edit-dialog.svelte +++ b/frontend/src/lib/components/dashboard/general_catalogs/locations/create-edit-dialog.svelte @@ -6,7 +6,8 @@ import { createLocation, updateLocation, - type Location + type Location, + type LocationSystem } from '$lib/api/dashboard/a76/general_catalogs/locations'; import { companyStore } from '$lib/stores/company.svelte'; import { obtenerAtajosFormularioLocalidades } from '$lib/config/shortcuts/dashboard/general_catalogs/locations/edit'; @@ -21,14 +22,18 @@ onSuccess?: () => void; } = $props(); - // Atajos - const isEdit = $derived(!!item); const title = $derived(isEdit ? 'Editar Ubicación' : 'Nueva Ubicación'); + const systemOptions: { value: LocationSystem; label: string }[] = [ + { value: 'fixed_asset', label: 'Activo fijo (FA)' }, + { value: 'inventory', label: 'Inventario' } + ]; + let formData = $state({ - location_code: '', - location_description: '' + clave_localizacion: '', + localizacion: '', + system: 'fixed_asset' as LocationSystem }); let loading = $state(false); @@ -38,11 +43,16 @@ if (open) { if (item) { formData = { - location_code: item.location_code || '', - location_description: item.location_description || '' + clave_localizacion: item.clave_localizacion ?? '', + localizacion: item.localizacion ?? '', + system: item.system ?? 'fixed_asset' }; } else { - formData = { location_code: '', location_description: '' }; + formData = { + clave_localizacion: '', + localizacion: '', + system: 'fixed_asset' + }; } error = null; } @@ -55,20 +65,31 @@ const companyId = companyStore.activeCompany?.id; if (!companyId) throw new Error('No hay una compañía seleccionada'); - if (!formData.location_code.trim()) throw new Error('El código es requerido'); + if (!formData.clave_localizacion.trim()) throw new Error('La clave es requerida'); const basePayload = { - location_description: formData.location_description?.trim() || null + localizacion: formData.localizacion?.trim() || null }; if (isEdit && item) { - await updateLocation(item.id, basePayload, companyId); + await updateLocation( + item.id, + { + ...basePayload, + clave_localizacion: formData.clave_localizacion.trim(), + system: formData.system + }, + companyId + ); } else { - const createPayload = { - location_code: formData.location_code.trim(), - ...basePayload - }; - await createLocation(createPayload, companyId); + await createLocation( + { + clave_localizacion: formData.clave_localizacion.trim(), + ...basePayload, + system: formData.system + }, + companyId + ); } open = false; @@ -107,12 +128,12 @@
- +
@@ -120,16 +141,32 @@
- +
+ +
+ +
+ +
+
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/InvoiceSelectorModal.svelte b/frontend/src/lib/components/dashboard/invoices/edit/InvoiceSelectorModal.svelte index 978f0a31..3b47096b 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/InvoiceSelectorModal.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/InvoiceSelectorModal.svelte @@ -10,10 +10,12 @@ let { open = $bindable(false), regimen = 'Temporal', + operationType = 'imp' as 'imp' | 'exp', onSelect }: { open: boolean; - regimen: string; + regimen?: string; + operationType?: 'imp' | 'exp'; onSelect: (invoice: Invoice) => void; } = $props(); @@ -25,15 +27,16 @@ if (!companyStore.activeCompany) return; loading = true; try { - let filters: any = { - operation_type: 'imp', - invoice_number: searchTerm + const filters: any = { + operation_type: operationType, + invoice_number: searchTerm || undefined }; - - if (regimen === 'Temporal' || regimen === 'TEMPORAL SCAF') { - filters.invoice_type = 'TEM'; - } else if (regimen === 'Definitiva' || regimen === 'DEFINITIVO SCAF') { - filters.invoice_type = 'DEF'; + if (operationType === 'imp' && regimen) { + if (regimen === 'Temporal' || regimen === 'TEMPORAL SCAF') { + filters.invoice_type = 'TEM'; + } else if (regimen === 'Definitiva' || regimen === 'DEFINITIVO SCAF') { + filters.invoice_type = 'DEF'; + } } const res = await invoicesApi.list(companyStore.activeCompany.id, 1, 50, filters); @@ -60,12 +63,18 @@ }); - + - Seleccionar Factura ({regimen}) + + {operationType === 'exp' ? 'Seleccionar Factura de Exportación' : `Seleccionar Factura (${regimen})`} + - Busca y selecciona una factura del catálogo de importación para el régimen {regimen}. + {#if operationType === 'exp'} + Busca y selecciona una factura del catálogo de exportación. + {:else} + Busca y selecciona una factura del catálogo de importación para el régimen {regimen}. + {/if} diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-sheet-fa.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-sheet-fa.svelte index e5230066..2ed9af1f 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-sheet-fa.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-sheet-fa.svelte @@ -1,13 +1,21 @@ + + + + + Catálogo de ubicaciones (maquinaria y equipo) + + + {#if showRegisterForm} + +
{ + e.preventDefault(); + handleRegisterSubmit(); + }} + > +
+ {#if formError} +
+ {formError} +
+ {/if} +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+ + +
+
+ {:else} + +
+
+ + +
+ +
+ +
+ {#if loading} +
+ +
+ {:else if error} +
+

{error}

+
+ {:else} +
+ + + + + + + + + {#each filtered as loc} + handleSelect(loc)} + > + + + + {/each} + {#if filtered.length === 0} + + + + {/if} + +
ClaveLocalización
{loc.clave_localizacion ?? '—'}{loc.localizacion ?? '—'}
+ No se encontraron resultados +
+
+ {/if} +
+ +
+ +
+ {/if} +
+
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-continuation.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-continuation.svelte index 238984b0..d071bc47 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-continuation.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-continuation.svelte @@ -8,6 +8,7 @@ import type { Item, LineDescriptions } from '$lib/api/dashboard/a76/items'; import type { InvoiceItemVisibility } from '$lib/config/invoice-item-visibility'; import PaymentMethodDialog from './payment-method-dialog.svelte'; + import LocationSelectorDialog from './location-selector-dialog.svelte'; let { lineItem = $bindable(), @@ -51,6 +52,7 @@ let paymentMethodDialogOpen = $state(false); let payment_method_description = $state(''); + let locationSelectorOpen = $state(false); // Load payment method description when payment_method exists $effect(() => { @@ -89,6 +91,10 @@ lineItem.payment_method = method.key; payment_method_description = method.description; } + + function handleLocationSelect(loc: { clave_localizacion: string; localizacion?: string | null }) { + descriptions.machinery_location = loc.localizacion ?? loc.clave_localizacion ?? ''; + }
@@ -224,15 +230,27 @@ {/if} {#if visibility.showContinuationLocation} - -
+ +
- +
- +
+
{/if} diff --git a/frontend/src/lib/components/dashboard/locations/columns.ts b/frontend/src/lib/components/dashboard/locations/columns.ts index 29212a6b..96e9897f 100644 --- a/frontend/src/lib/components/dashboard/locations/columns.ts +++ b/frontend/src/lib/components/dashboard/locations/columns.ts @@ -3,24 +3,35 @@ import type { Location } from '$lib/api/dashboard/a76/general_catalogs/locations import { renderComponent } from '$lib/components/ui/data-table'; import DataTableActions from './data-table-actions.svelte'; +const SYSTEM_LABELS: Record = { + fixed_asset: 'Activo fijo (FA)', + inventory: 'Inventario' +}; + export function createColumns(onSuccess?: () => void): ColumnDef[] { - return [ - { - accessorKey: 'location_code', - header: 'Código', - }, - { - accessorKey: 'location_description', - header: 'Descripción', - cell: ({ row }) => row.original.location_description || '—' - }, - { - id: 'actions', - header: 'Acciones', - cell: ({ row }) => renderComponent(DataTableActions, { - item: row.original, - onSuccess - }) - } - ]; + return [ + { + accessorKey: 'clave_localizacion', + header: 'Clave' + }, + { + accessorKey: 'localizacion', + header: 'Localización', + cell: ({ row }) => row.original.localizacion ?? '—' + }, + { + accessorKey: 'system', + header: 'Sistema', + cell: ({ row }) => SYSTEM_LABELS[row.original.system] ?? row.original.system + }, + { + id: 'actions', + header: 'Acciones', + cell: ({ row }) => + renderComponent(DataTableActions, { + item: row.original, + onSuccess + }) + } + ]; } diff --git a/frontend/src/lib/components/dashboard/locations/locations-catalog.svelte b/frontend/src/lib/components/dashboard/locations/locations-catalog.svelte new file mode 100644 index 00000000..11c64574 --- /dev/null +++ b/frontend/src/lib/components/dashboard/locations/locations-catalog.svelte @@ -0,0 +1,156 @@ + + +
+ {#if !compact} +
+
+

Ubicaciones

+

+ Clave y localización por sistema (FA / Inventario) +

+
+ +
+ {:else} +
+ +
+ {/if} + +
+
+ +
+
+ +
+ {#if showSystemFilter} +
+ +
+ {/if} +
+ +
+ {#if loading} +
+ Cargando... +
+ {:else} + + {/if} +
+ + +
diff --git a/frontend/src/lib/components/sidebar/modules.ts b/frontend/src/lib/components/sidebar/modules.ts index 0566199d..36c42eed 100644 --- a/frontend/src/lib/components/sidebar/modules.ts +++ b/frontend/src/lib/components/sidebar/modules.ts @@ -278,10 +278,6 @@ export function getSidebarData(): SidebarData { title: m["sidebar.general_catalogs.customs_warehouses"](), url: "/dashboard/reference_data/customs_warehouses", }, - { - title: m["sidebar.general_catalogs.locations"](), - url: "/dashboard/general_catalogs/locations", - }, { title: m["sidebar.general_catalogs.doda"](), url: "/dashboard/general_catalogs/doda", diff --git a/frontend/src/routes/dashboard/general_catalogs/locations/+page.server.ts b/frontend/src/routes/dashboard/general_catalogs/locations/+page.server.ts deleted file mode 100644 index bd4dc983..00000000 --- a/frontend/src/routes/dashboard/general_catalogs/locations/+page.server.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { PageServerLoad } from './$types'; -import { getAuthTokens, authenticatedFetch } from '$lib/server/api'; - -export const load: PageServerLoad = async ({ cookies, fetch, url, parent }) => { - const parentData = await parent(); - const { accessToken } = getAuthTokens(cookies); - - if (!accessToken) { - return { - error: 'No authenticated', - locations: { items: [], total: 0, page: 1, page_size: 10, pages: 0 } - }; - } - - const page = Number(url.searchParams.get('page')) || 1; - const pageSize = Number(url.searchParams.get('page_size')) || 50; - - const cookieCompanyId = cookies.get('active_company_id'); - const companyId = cookieCompanyId - ? parseInt(cookieCompanyId) - : parentData.companies?.[0]?.id; - - if (!companyId) { - return { - error: 'No company selected', - locations: { items: [], total: 0, page, page_size: pageSize, pages: 0 } - }; - } - - const filters: Record = {}; - const location_code = url.searchParams.get('location_code'); - const location_description = url.searchParams.get('location_description'); - - if (location_code) filters.location_code = location_code; - if (location_description) filters.location_description = location_description; - - try { - const queryParams = new URLSearchParams({ - page: page.toString(), - page_size: pageSize.toString(), - company_id: companyId.toString(), - ...filters - }); - - const response = await authenticatedFetch( - `v1/a76/ports/?${queryParams.toString()}`, - { method: 'GET', cache: 'no-store' }, - cookies, - fetch - ); - - if (!response.ok) { - return { - error: 'Failed to load', - locations: { items: [], total: 0, page, page_size: pageSize, pages: 0 } - }; - } - - const data = await response.json(); - - return { locations: data }; - } catch (error) { - console.error('Error loading locations:', error); - return { - error: 'Error loading', - locations: { items: [], total: 0, page: 1, page_size: pageSize, pages: 0 } - }; - } -}; diff --git a/frontend/src/routes/dashboard/general_catalogs/locations/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/locations/+page.svelte deleted file mode 100644 index 3d413e70..00000000 --- a/frontend/src/routes/dashboard/general_catalogs/locations/+page.svelte +++ /dev/null @@ -1,89 +0,0 @@ - - -
-
-
-

Ubicaciones

-

Catálogo de ubicaciones de puertos

-
- -
- -
-
- -
-
- -
-
- -
- -
- - -