diff --git a/backend/alembic/versions/7937209f9718_seed_initial_data.py b/backend/alembic/versions/7937209f9718_seed_initial_data.py index f83e68bb..ddc5fcce 100644 --- a/backend/alembic/versions/7937209f9718_seed_initial_data.py +++ b/backend/alembic/versions/7937209f9718_seed_initial_data.py @@ -44,7 +44,7 @@ from api.v1.modules.public.reference_data.pedimento_codes.seed import ( from api.v1.modules.public.reference_data.pedimento_regimens.seed import ( seed as pedimento_regimens_seed, ) -from api.v1.modules.public.reference_data.sectors.seed import seed as sectors_seed +from api.v1.modules.a76.general_catalogs.sectors.seed import seed as sectors_seed from api.v1.modules.public.reference_data.states.seed import seed as states_seed from api.v1.modules.public.reference_data.transport_modes.seed import ( seed as transport_modes_seed, @@ -268,19 +268,8 @@ def upgrade() -> None: """ ) - values_sectors = ", ".join( - [ - f"('{key}', '{desc.replace(chr(39), chr(39)*2)}', '{authorized}')" - for key, desc, authorized in sectors_seed - ] - ) - op.execute( - f""" - INSERT INTO public.sectors (key, description, authorized) VALUES - {values_sectors} - ON CONFLICT (key) DO NOTHING; - """ - ) + # Sectors se siembran por compañía en _seed_company_data + # (a76.sectors requiere tenant_id/company_id — no aplica en seed global) values_tm = ", ".join( [ @@ -493,7 +482,7 @@ def downgrade() -> None: op.drop_table("transport_types", schema="public") op.drop_table("trailer_types", schema="public") op.drop_table("transport_modes", schema="public") - op.drop_table("sectors", schema="public") + op.drop_table("sectors", schema="a76") op.drop_table("payment_methods", schema="public") op.drop_table("material_types", schema="public") op.drop_table("invoice_types", schema="public") diff --git a/backend/api/v1/modules/a76/general_catalogs/company/service.py b/backend/api/v1/modules/a76/general_catalogs/company/service.py index 9f940c62..37158c12 100644 --- a/backend/api/v1/modules/a76/general_catalogs/company/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/company/service.py @@ -16,6 +16,7 @@ from ...audit_log.services.service import AuditService from ..units_of_measure.seed import seed as units_of_measure_seed from ..fractions.historical_tariff_fractions.seed import seed as historical_tariff_fractions_seed from ..fractions.warning_fractions.seed import seed as warning_fractions_seed +from ..sectors.seed import seed as sectors_seed from core.context import get_user_context from sqlalchemy import text @@ -774,6 +775,21 @@ class CompanyService: """)) db.execute(text("ALTER TABLE public.warning_fractions ENABLE TRIGGER ALL;")) + # 4. Sectors + values_sectors = ", ".join( + [ + f"({format_value(key)}, {format_value(description)}, {str(authorized).upper()}, {tenant_id}, {company_id})" + for key, description, authorized in sectors_seed + ] + ) + + if values_sectors: + db.execute(text(f""" + INSERT INTO a76.sectors (key, description, authorized, tenant_id, company_id) + VALUES {values_sectors} + ON CONFLICT (key, tenant_id, company_id) DO NOTHING; + """)) + def get_companies_by_tenant(self, tenant_id: int) -> List[Company]: """Get all companies for a tenant""" return ( diff --git a/backend/api/v1/modules/a76/general_catalogs/router.py b/backend/api/v1/modules/a76/general_catalogs/router.py index 291dde07..75975e40 100644 --- a/backend/api/v1/modules/a76/general_catalogs/router.py +++ b/backend/api/v1/modules/a76/general_catalogs/router.py @@ -26,6 +26,7 @@ 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 +from .sectors.routes import router as sectors_router router = APIRouter() @@ -56,3 +57,4 @@ router.include_router(error_catalogs_router) router.include_router(doda_router) router.include_router(prevalidators_router) router.include_router(electronic_notices_router) +router.include_router(sectors_router) diff --git a/backend/api/v1/modules/a76/general_catalogs/sectors/__init__.py b/backend/api/v1/modules/a76/general_catalogs/sectors/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/api/v1/modules/a76/general_catalogs/sectors/dto.py b/backend/api/v1/modules/a76/general_catalogs/sectors/dto.py new file mode 100644 index 00000000..557dce03 --- /dev/null +++ b/backend/api/v1/modules/a76/general_catalogs/sectors/dto.py @@ -0,0 +1,30 @@ +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field + + +class SectorBaseDTO(BaseModel): + key: str = Field(..., description="Clave del sector (ej: 'XIX', 'IIa')", max_length=8) + description: str = Field(..., description="Descripción del sector", max_length=150) + authorized: Optional[bool] = Field(False, description="True = autorizado para PROSEC") + + +class SectorCreateDTO(SectorBaseDTO): + pass + + +class SectorUpdateDTO(BaseModel): + key: Optional[str] = Field(None, max_length=8) + description: Optional[str] = Field(None, max_length=150) + authorized: Optional[bool] = None + + +class SectorResponseDTO(SectorBaseDTO): + id: int + company_id: int + tenant_id: int + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/a76/general_catalogs/sectors/models.py b/backend/api/v1/modules/a76/general_catalogs/sectors/models.py new file mode 100644 index 00000000..e210332f --- /dev/null +++ b/backend/api/v1/modules/a76/general_catalogs/sectors/models.py @@ -0,0 +1,23 @@ +from typing import Optional + +from api.v1.common.base_models import TenantScopedMixin, TimestampMixin +from core.database import Base +from sqlalchemy import Boolean, Integer, PrimaryKeyConstraint, String, UniqueConstraint +from sqlalchemy.orm import Mapped, mapped_column + + +class Sector(Base, TenantScopedMixin, TimestampMixin): + __tablename__ = "sectors" # GSectores + __table_args__ = ( + PrimaryKeyConstraint("id", name="sectors_pkey"), + UniqueConstraint("tenant_id", "company_id", "key", name="sectors_key_ukey"), + {"schema": "a76", "extend_existing": True}, + ) + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + key: Mapped[str] = mapped_column(String(8), nullable=False) + description: Mapped[str] = mapped_column(String(150), nullable=False) + authorized: Mapped[Optional[bool]] = mapped_column(Boolean, default=False, server_default="false") + + def __repr__(self): + return f"" diff --git a/backend/api/v1/modules/a76/general_catalogs/sectors/routes.py b/backend/api/v1/modules/a76/general_catalogs/sectors/routes.py new file mode 100644 index 00000000..83d09f12 --- /dev/null +++ b/backend/api/v1/modules/a76/general_catalogs/sectors/routes.py @@ -0,0 +1,23 @@ +""" +Routes for managing Sectors (GSectores) — a76 tenant-scoped catalog. +""" + +from api.v1.common.tenant_crud_routes import TenantCRUDRoutes + +from .dto import SectorCreateDTO, SectorResponseDTO, SectorUpdateDTO +from .service import SectorService + +router = TenantCRUDRoutes( + service=SectorService, + create_schema=SectorCreateDTO, + update_schema=SectorUpdateDTO, + response_schema=SectorResponseDTO, + prefix="/sectors", + tags=["a76 / sectors"], + resource_name="Sector", + id_name="sector_id", + enable_list=True, + enable_filters=True, + default_page_size=50, + max_page_size=100, +).router diff --git a/backend/api/v1/modules/a76/general_catalogs/sectors/seed.py b/backend/api/v1/modules/a76/general_catalogs/sectors/seed.py new file mode 100644 index 00000000..ee9bfd56 --- /dev/null +++ b/backend/api/v1/modules/a76/general_catalogs/sectors/seed.py @@ -0,0 +1,36 @@ +# (key, description, authorized) +seed = [ + ("I", "INDUSTRIA ELECTRICA", False), + ("II", "INDUSTRIA ELECTRONICA", False), + ("IIa", "PARA LOS BIENES A QUE SE REFIERE LA FRACCION II, INCISO a) o b), DE ARTICULO 4to DE ESTE DECRETO.", False), + ("IIb", "PARA LOS BIENES A QUE SE REFIERE LA FRACCION II, INCISO b), DE ARTICULO 4to DE ESTE DECRETO.", False), + ("III", "INDUSTRIA DEL MUEBLE", False), + ("IV", "INDUSTRIA DEL JUGUETE, JUEGOS DE RECREO Y ARTICULOS DEPORTIVOS", False), + ("IX", "INDUSTRIA DE MAQUINARIA AGRICOLA", False), + ("V", "INDUSTRIA DEL CALZADO", False), + ("VI", "INDUSTRIA MINERA Y METALURGICA", False), + ("VII", "INDUSTRIA DE BIENES DE CAPITAL", False), + ("VIII", "INDUSTRIA FOTOGRAFICA", False), + ("X", "INDUSTRIAS DIVERSAS", False), + ("XI", "INDUSTRIA QUIMICA", False), + ("XII", "INDUSTRIAS DE MANUFACTURAS DEL CAUCHO Y PLASTICOS", False), + ("XIII", "INDUSTRIA SIDERURGICA", False), + ("XIV", "INDUSTRIA DE PRODUCTOS FARMOQUIMICOS, MEDICAMENTOS Y EQUIPO MEDICO", False), + ("XIX", "INDUSTRIA AUTOMOTRIZ Y DE AUTOPARTES", False), + ("XIXa", "INDUSTRIA AUTOMOTRIZ Y DE AUTOPARTES", False), + ("XIXb", "INDUSTRIA AUTOMOTRIZ Y DE AUTOPARTES", False), + ("XV", "INDUSTRIA DEL TRANSPORTE, EXCEPTO EL SECTOR DE LA INDUSTRIA AUTOMOTRIZ Y DE AUTOPARTES", False), + ("XVa", "INDUSTRIA DEL TRANSPORTE, EXCEPTO EL SECTOR DE LA INDUSTRIA AUTOMOTRIZ Y DE AUTOPARTES.", False), + ("XVb", "INDUSTRIA DEL TRANSPORTE, EXCEPTO EL SECTOR DE LA INDUSTRIA AUTOMOTRIZ Y DE AUTOPARTES.", False), + ("XVI", "INDUSTRIA DEL PAPEL Y CARTON", False), + ("XVII", "INDUSTRIA DE LA MADERA", False), + ("XVIII", "INDUSTRIA DEL CUERO Y PIELES", False), + ("XX", "INDUSTRIA TEXTIL Y DE LA CONFECCION", False), + ("XXa", "INDUSTRIA TEXTIL Y DE LA CONFECCION", False), + ("XXb", "INDUSTRIA TEXTIL Y DE LA CONFECCION", False), + ("XXc", "INDUSTRIA TEXTIL Y DE LA CONFECCION", False), + ("XXd", "INDUSTRIA TEXTIL Y DE LA CONFECCION", False), + ("XXe", "INDUSTRIA TEXTIL Y DE LA CONFECCION", False), + ("XXI", "INDUSTRIA DE CHOCOLATES, DULCES Y SIMILARES", False), + ("XXII", "INDUSTRIA DEL CAFE", False), +] diff --git a/backend/api/v1/modules/a76/general_catalogs/sectors/service.py b/backend/api/v1/modules/a76/general_catalogs/sectors/service.py new file mode 100644 index 00000000..6d1ec4ee --- /dev/null +++ b/backend/api/v1/modules/a76/general_catalogs/sectors/service.py @@ -0,0 +1,143 @@ +""" +Service layer for Sectors (GSectores) — a76 tenant-scoped catalog. +""" + +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 . import dto, models + +logger = logging.getLogger(__name__) + + +class SectorService: + """Service for Sector CRUD operations with tenant support""" + + @staticmethod + def get_all( + db: Session, + tenant_id: int, + company_id: int, + skip: int = 0, + limit: int = 50, + filters: Optional[Dict[str, Any]] = None, + ) -> Tuple[List[models.Sector], int]: + """Get all sectors for a tenant/company with pagination""" + query = db.query(models.Sector).filter( + models.Sector.tenant_id == tenant_id, + models.Sector.company_id == company_id, + ) + + if filters: + if filters.get("key"): + query = query.filter( + models.Sector.key.ilike(f"%{filters['key']}%") + ) + if filters.get("description"): + query = query.filter( + models.Sector.description.ilike(f"%{filters['description']}%") + ) + + total = query.count() + sectors = query.order_by(models.Sector.key).offset(skip).limit(limit).all() + + return sectors, total + + @staticmethod + def get_by_id( + db: Session, sector_id: int, tenant_id: int, company_id: int + ) -> Optional[models.Sector]: + """Get sector by ID""" + return ( + db.query(models.Sector) + .filter( + models.Sector.id == sector_id, + models.Sector.tenant_id == tenant_id, + models.Sector.company_id == company_id, + ) + .first() + ) + + @staticmethod + def create( + db: Session, + sector_data: dto.SectorCreateDTO, + tenant_id: int, + company_id: int, + ) -> models.Sector: + """Create a new sector""" + new_sector = models.Sector( + **sector_data.model_dump(), tenant_id=tenant_id, company_id=company_id + ) + db.add(new_sector) + try: + db.commit() + db.refresh(new_sector) + return new_sector + except IntegrityError as e: + db.rollback() + logger.error(f"IntegrityError creating sector: {str(e)}") + raise HTTPException( + status_code=400, + detail="Ya existe un sector con esa clave para esta empresa.", + ) + + @staticmethod + def update( + db: Session, + sector_id: int, + tenant_id: int, + sector_data: dto.SectorUpdateDTO, + company_id: int, + ) -> Optional[models.Sector]: + """Update a sector""" + sector = SectorService.get_by_id(db, sector_id, tenant_id, company_id) + if not sector: + return None + + update_data = sector_data.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(sector, field, value) + + try: + db.commit() + db.refresh(sector) + return sector + except IntegrityError as e: + db.rollback() + logger.error(f"IntegrityError updating sector {sector_id}: {str(e)}") + raise HTTPException( + status_code=400, + detail="Ya existe un sector con esa clave para esta empresa.", + ) + + @staticmethod + def delete( + db: Session, sector_id: int, tenant_id: int, company_id: int + ) -> bool: + """Delete a sector""" + sector = SectorService.get_by_id(db, sector_id, tenant_id, company_id) + if not sector: + return False + + try: + db.delete(sector) + db.commit() + return True + except IntegrityError as e: + db.rollback() + logger.error(f"IntegrityError deleting sector {sector_id}: {str(e)}") + if "foreign key constraint" in str(e).lower(): + raise HTTPException( + status_code=400, + detail="No se puede eliminar el sector porque tiene registros relacionados.", + ) + raise HTTPException(status_code=400, detail="Error al eliminar el sector.") + except Exception as e: + db.rollback() + logger.error(f"Error deleting sector {sector_id}: {str(e)}") + raise HTTPException(status_code=500, detail="Error al eliminar el sector.") diff --git a/backend/api/v1/modules/a76/items/exports/validators/common.py b/backend/api/v1/modules/a76/items/exports/validators/common.py index 31ace4a5..d3d30993 100644 --- a/backend/api/v1/modules/a76/items/exports/validators/common.py +++ b/backend/api/v1/modules/a76/items/exports/validators/common.py @@ -16,7 +16,7 @@ from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMe from api.v1.modules.a76.general_catalogs.packages.models import Package from api.v1.modules.a24.fa.fa_item_lines.models import FaLineItem from api.v1.modules.public.reference_data.countries.models import Country -from api.v1.modules.public.reference_data.sectors.models import Sector +from api.v1.modules.a76.general_catalogs.sectors.models import Sector from api.v1.modules.public.reference_data.valuation_methods.models import ( ValuationMethod, ) @@ -314,7 +314,11 @@ def validate_common( ) elif fraction_type.strip().upper() == FractionType.PROSEC and sector: sector_db: Sector = ( - db.query(Sector).filter(Sector.key == sector).scalar() + db.query(Sector).filter( + Sector.key == sector, + Sector.tenant_id == tenant_id, + Sector.company_id == company_id, + ).scalar() ) if sector_db: errors.add_error( diff --git a/backend/api/v1/modules/a76/items/imports/validators/common.py b/backend/api/v1/modules/a76/items/imports/validators/common.py index ce5723e6..b1d0dc2f 100644 --- a/backend/api/v1/modules/a76/items/imports/validators/common.py +++ b/backend/api/v1/modules/a76/items/imports/validators/common.py @@ -15,7 +15,7 @@ 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.packages.models import Package from api.v1.modules.public.reference_data.countries.models import Country -from api.v1.modules.public.reference_data.sectors.models import Sector +from api.v1.modules.a76.general_catalogs.sectors.models import Sector from api.v1.modules.public.reference_data.valuation_methods.models import ( ValuationMethod, ) @@ -239,7 +239,11 @@ def validate_common( ) elif fraction_type.strip().upper() == FractionType.PROSEC and sector: sector_db: Sector = ( - db.query(Sector).filter(Sector.key == sector).scalar() + db.query(Sector).filter( + Sector.key == sector, + Sector.tenant_id == tenant_id, + Sector.company_id == company_id, + ).scalar() ) if sector_db: errors.add_error( diff --git a/backend/api/v1/modules/a76/layouts_csv/facturas/tasks.py b/backend/api/v1/modules/a76/layouts_csv/facturas/tasks.py index 3c52da6c..54095fdf 100644 --- a/backend/api/v1/modules/a76/layouts_csv/facturas/tasks.py +++ b/backend/api/v1/modules/a76/layouts_csv/facturas/tasks.py @@ -895,7 +895,7 @@ def _do_scan_file(job_id: str, model_target: str, config: Optional[str] = None, from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure from api.v1.modules.a76.general_catalogs.packages.models import Package from api.v1.modules.public.reference_data.countries.models import Country - from api.v1.modules.public.reference_data.sectors.models import Sector + from api.v1.modules.a76.general_catalogs.sectors.models import Sector from api.v1.modules.public.reference_data.valuation_methods.models import ValuationMethod from api.v1.modules.public.reference_data.payment_methods.models import PaymentMethod from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.models import USTariffFraction @@ -1018,7 +1018,11 @@ def _do_scan_file(job_id: str, model_target: str, config: Optional[str] = None, valid_fraction_ame.add((row[0] or "").strip()) authorized_sectors: Set[str] = set() - for row in session.query(Sector.key).filter(Sector.authorized == True).all(): + for row in session.query(Sector.key).filter( + Sector.authorized == True, + Sector.tenant_id == tenant_id, + Sector.company_id == company_id, + ).all(): if row[0]: authorized_sectors.add((row[0] or "").strip().upper()) @@ -1151,7 +1155,7 @@ def _do_scan_file(job_id: str, model_target: str, config: Optional[str] = None, from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure from api.v1.modules.a76.general_catalogs.packages.models import Package from api.v1.modules.public.reference_data.countries.models import Country - from api.v1.modules.public.reference_data.sectors.models import Sector + from api.v1.modules.a76.general_catalogs.sectors.models import Sector from api.v1.modules.public.reference_data.valuation_methods.models import ValuationMethod from api.v1.modules.public.reference_data.payment_methods.models import PaymentMethod from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.models import USTariffFraction @@ -1277,7 +1281,11 @@ def _do_scan_file(job_id: str, model_target: str, config: Optional[str] = None, valid_fraction_ame.add((row[0] or "").strip()) authorized_sectors: Set[str] = set() - for row in session.query(Sector.key).filter(Sector.authorized == True).all(): + for row in session.query(Sector.key).filter( + Sector.authorized == True, + Sector.tenant_id == tenant_id, + Sector.company_id == company_id, + ).all(): if row[0]: authorized_sectors.add((row[0] or "").strip().upper()) @@ -1679,7 +1687,7 @@ def _do_scan_file(job_id: str, model_target: str, config: Optional[str] = None, from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure from api.v1.modules.a76.general_catalogs.packages.models import Package from api.v1.modules.public.reference_data.countries.models import Country - from api.v1.modules.public.reference_data.sectors.models import Sector + from api.v1.modules.a76.general_catalogs.sectors.models import Sector from api.v1.modules.public.reference_data.valuation_methods.models import ValuationMethod from api.v1.modules.public.reference_data.payment_methods.models import PaymentMethod from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.models import USTariffFraction @@ -1804,7 +1812,11 @@ def _do_scan_file(job_id: str, model_target: str, config: Optional[str] = None, valid_fraction_ame.add((row[0] or "").strip()) authorized_sectors: Set[str] = set() - for row in session.query(Sector.key).filter(Sector.authorized == True).all(): + for row in session.query(Sector.key).filter( + Sector.authorized == True, + Sector.tenant_id == tenant_id, + Sector.company_id == company_id, + ).all(): if row[0]: authorized_sectors.add((row[0] or "").strip().upper()) diff --git a/backend/api/v1/modules/a76/layouts_csv/parts/common/fk_loader.py b/backend/api/v1/modules/a76/layouts_csv/parts/common/fk_loader.py index f173a0eb..04f33435 100644 --- a/backend/api/v1/modules/a76/layouts_csv/parts/common/fk_loader.py +++ b/backend/api/v1/modules/a76/layouts_csv/parts/common/fk_loader.py @@ -54,7 +54,7 @@ def load_parts_fk_sets( from api.v1.modules.public.reference_data.currency_types.models import CurrencyType from api.v1.modules.a76.general_catalogs.company.models import Company from api.v1.modules.public.reference_data.countries.models import Country - from api.v1.modules.public.reference_data.sectors.models import Sector + from api.v1.modules.a76.general_catalogs.sectors.models import Sector from api.v1.modules.a76.general_catalogs.fractions.tariff_fractions.models import TariffFraction from api.v1.modules.a76.general_catalogs.fractions.historical_tariff_fractions.models import ( HistoricalTariffFraction, @@ -102,7 +102,11 @@ def load_parts_fk_sets( for row in ( session.query(Sector.key) - .filter(Sector.authorized == True) + .filter( + Sector.authorized == True, + Sector.tenant_id == tenant_id, + Sector.company_id == company_id, + ) .all() ): if row[0]: diff --git a/backend/api/v1/modules/a76/layouts_csv/parts/validators/common.py b/backend/api/v1/modules/a76/layouts_csv/parts/validators/common.py index 39aa11ff..0b0f8411 100644 --- a/backend/api/v1/modules/a76/layouts_csv/parts/validators/common.py +++ b/backend/api/v1/modules/a76/layouts_csv/parts/validators/common.py @@ -320,12 +320,11 @@ def validate_row_sector( # Formato: solo A-Z/0-9 (sin espacios ni especiales), hasta 8 if sector: s = sector.strip().upper() - # Alinear con modelo A76: solo dígitos, máx 8 - if not (1 <= len(s) <= 8) or not s.isdigit(): + if not (1 <= len(s) <= 8) or not s.isalnum(): return { "line": line_num, "col": "SECTOR", - "msg": "Error: (Col. P) El Sector contiene caracteres no permitidos. Use solo números (máx. 8 dígitos).", + "msg": "Error: (Col. P) El Sector contiene caracteres no permitidos. Use solo letras y números sin espacios (máx. 8 caracteres).", } if pref == "PROSEC": if not sector: diff --git a/backend/api/v1/modules/a76/parts/dto.py b/backend/api/v1/modules/a76/parts/dto.py index 02445371..d069a856 100644 --- a/backend/api/v1/modules/a76/parts/dto.py +++ b/backend/api/v1/modules/a76/parts/dto.py @@ -6,7 +6,7 @@ from pydantic import BaseModel, Field, ConfigDict # --- SUB-DTO: DATOS ADUANALES (FaData) --- class FaDataDTO(BaseModel): origin_country: Optional[str] = Field(default=None, pattern=r"^[A-Z]{3}$") - sector: Optional[str] = Field(default=None, pattern=r"^[0-9]{1,8}$") + sector: Optional[str] = Field(default=None, pattern=r"^[A-Za-z0-9]{1,8}$") fraction_type: Optional[Literal["GENERAL", "PROSEC", "ALADI", "TLCS"]] = None model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/public/reference_data/router.py b/backend/api/v1/modules/public/reference_data/router.py index 6200d14a..fb4a322f 100644 --- a/backend/api/v1/modules/public/reference_data/router.py +++ b/backend/api/v1/modules/public/reference_data/router.py @@ -17,7 +17,6 @@ from .material_types.routes import router as material_types_router from .payment_methods.routes import router as payment_methods_router from .pedimento_codes.routes import router as pedimento_codes_router from .pedimento_regimens.routes import router as pedimento_regimens_router -from .sectors.routes import router as sectors_router from .states.routes import router as states_router from .trailer_types.routes import router as trailer_types_router from .transport_modes.routes import router as transport_modes_router @@ -81,11 +80,6 @@ router.include_router( prefix="/reference_data", tags=["public / reference_data / valuation_methods"], ) -router.include_router( - sectors_router, - prefix="/reference_data", - tags=["public / public / reference_data / sectors"], -) router.include_router( transport_modes_router, prefix="/reference_data", diff --git a/backend/api/v1/modules/public/reference_data/sectors/dto.py b/backend/api/v1/modules/public/reference_data/sectors/dto.py deleted file mode 100644 index e97e8a1d..00000000 --- a/backend/api/v1/modules/public/reference_data/sectors/dto.py +++ /dev/null @@ -1,9 +0,0 @@ -from pydantic import BaseModel, ConfigDict, Field - - -class SectorDTO(BaseModel): - key: str = Field(..., min_length=1, max_length=8) - description: str - authorized: bool - - model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/public/reference_data/sectors/models.py b/backend/api/v1/modules/public/reference_data/sectors/models.py deleted file mode 100644 index 6c581cf1..00000000 --- a/backend/api/v1/modules/public/reference_data/sectors/models.py +++ /dev/null @@ -1,23 +0,0 @@ -from core.database import Base -from sqlalchemy import Boolean, PrimaryKeyConstraint, SmallInteger, String -from sqlalchemy.orm import Mapped, mapped_column - - -class Sector(Base): - __tablename__ = "sectors" # GSectores - __table_args__ = ( - PrimaryKeyConstraint("key", name="sectors_pkey"), - {"schema": "public", "extend_existing": True}, # opcional - ) - - key: Mapped[str] = mapped_column( - String(8), nullable=False) # clave del sector - description: Mapped[str] = mapped_column( - String(150), nullable=False - ) # descripción oficial (en español) - authorized: Mapped[bool] = mapped_column( - Boolean - ) # True = autorizado, False = no autorizado - - def __repr__(self): - return f"" diff --git a/backend/api/v1/modules/public/reference_data/sectors/routes.py b/backend/api/v1/modules/public/reference_data/sectors/routes.py deleted file mode 100644 index da38ac7d..00000000 --- a/backend/api/v1/modules/public/reference_data/sectors/routes.py +++ /dev/null @@ -1,56 +0,0 @@ - -from typing import Any, Dict, Optional - -from core.database import get_core_db -from core.security import get_current_user -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy import or_ -from sqlalchemy.orm import Session - -from .dto import SectorDTO -from .models import Sector - -router = APIRouter(prefix="/sectors") - - -@router.get("/", response_model=Dict[str, Any]) -def list_sectors( - page: int = Query(1, ge=1, description="Número de página"), - page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), - search: Optional[str] = Query(None, description="Término de búsqueda"), - db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user), -): - skip = (page - 1) * page_size - query = db.query(Sector) - - if search: - search_filter = or_( - Sector.key.ilike(f"%{search}%"), - Sector.description.ilike(f"%{search}%") - ) - query = query.filter(search_filter) - - total = query.count() - # Add deterministic sort order - query = query.order_by(Sector.key) - items = query.offset(skip).limit(page_size).all() - - return { - "items": [SectorDTO.model_validate(obj) for obj in items], - "total": total, - "page": page, - "page_size": page_size, - } - - -@router.get("/{key}", response_model=SectorDTO) -def get_sector( - key: str, - db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user), -): - obj = db.query(Sector).filter(Sector.key == key).first() - if not obj: - raise HTTPException(status_code=404, detail="Not found") - return obj diff --git a/backend/api/v1/modules/public/reference_data/sectors/seed.py b/backend/api/v1/modules/public/reference_data/sectors/seed.py deleted file mode 100644 index 9eb39401..00000000 --- a/backend/api/v1/modules/public/reference_data/sectors/seed.py +++ /dev/null @@ -1,55 +0,0 @@ -seed = [ - ("I", "INDUSTRIA ELECTRICA", "0"), - ("II", "INDUSTRIA ELECTRONICA", "0"), - ( - "IIa", - "PARA LOS BIENES A QUE SE REFIERE LA FRACCION II, INCISO a) o b), DE ARTICULO 4to DE ESTE DECRETO.", - "0", - ), - ( - "IIb", - "PARA LOS BIENES A QUE SE REFIERE LA FRACCION II, INCISO b), DE ARTICULO 4to DE ESTE DECRETO.", - "0", - ), - ("III", "INDUSTRIA DEL MUEBLE", "0"), - ("IV", "INDUSTRIA DEL JUGUETE, JUEGOS DE RECREO Y ARTICULOS DEPORTIVOS", "0"), - ("IX", "INDUSTRIA DE MAQUINARIA AGRICOLA", "0"), - ("V", "INDUSTRIA DEL CALZADO", "0"), - ("VI", "INDUSTRIA MINERA Y METALURGICA", "0"), - ("VII", "INDUSTRIA DE BIENES DE CAPITAL", "0"), - ("VIII", "INDUSTRIA FOTOGRAFICA", "0"), - ("X", "INDUSTRIAS DIVERSAS", "0"), - ("XI", "INDUSTRIA QUIMICA", "0"), - ("XII", "INDUSTRIAS DE MANUFACTURAS DEL CAUCHO Y PLASTICOS", "0"), - ("XIII", "INDUSTRIA SIDERURGICA", "0"), - ("XIV", "INDUSTRIA DE PRODUCTOS FARMOQUIMICOS, MEDICAMENTOS Y EQUIPO MEDICO", "0"), - ("XIX", "INDUSTRIA AUTOMOTRIZ Y DE AUTOPARTES", "0"), - ("XIXa", "INDUSTRIA AUTOMOTRIZ Y DE AUTOPARTES", "0"), - ("XIXb", "INDUSTRIA AUTOMOTRIZ Y DE AUTOPARTES", "0"), - ( - "XV", - "INDUSTRIA DEL TRANSPORTE, EXCEPTO EL SECTOR DE LA INDUSTRIA AUTOMOTRIZ Y DE AUTOPARTES", - "0", - ), - ( - "XVa", - "INDUSTRIA DEL TRANSPORTE, EXCEPTO EL SECTOR DE LA INDUSTRIA AUTOMOTRIZ Y DE AUTOPARTES.", - "0", - ), - ( - "XVb", - "INDUSTRIA DEL TRANSPORTE, EXCEPTO EL SECTOR DE LA INDUSTRIA AUTOMOTRIZ Y DE AUTOPARTES.", - "0", - ), - ("XVI", "INDUSTRIA DEL PAPEL Y CARTON", "0"), - ("XVII", "INDUSTRIA DE LA MADERA", "0"), - ("XVIII", "INDUSTRIA DEL CUERO Y PIELES", "0"), - ("XX", "INDUSTRIA TEXTIL Y DE LA CONFECCION", "0"), - ("XXa", "INDUSTRIA TEXTIL Y DE LA CONFECCION", "0"), - ("XXb", "INDUSTRIA TEXTIL Y DE LA CONFECCION", "0"), - ("XXc", "INDUSTRIA TEXTIL Y DE LA CONFECCION", "0"), - ("XXd", "INDUSTRIA TEXTIL Y DE LA CONFECCION", "0"), - ("XXe", "INDUSTRIA TEXTIL Y DE LA CONFECCION", "0"), - ("XXI", "INDUSTRIA DE CHOCOLATES, DULCES Y SIMILARES", "0"), - ("XXII", "INDUSTRIA DEL CAFE", "0"), -] diff --git a/backend/api/v1/modules/public/reference_data/sectors/test_sectors.py b/backend/api/v1/modules/public/reference_data/sectors/test_sectors.py deleted file mode 100644 index 872ea295..00000000 --- a/backend/api/v1/modules/public/reference_data/sectors/test_sectors.py +++ /dev/null @@ -1,40 +0,0 @@ -import pytest -from api.v1.modules.public.reference_data.sectors.routes import router -from fastapi import FastAPI -from fastapi.testclient import TestClient - -app = FastAPI() -app.include_router(router) -client = TestClient(app) - - -@pytest.mark.usefixtures("client", "access_token") -def test_list_sectors(client, access_token): - headers = {"Authorization": f"Bearer {access_token}"} - response = client.get("/sectors/", headers=headers) - assert response.status_code == 200 - assert "items" in response.json() - assert "page" in response.json() - assert "page_size" in response.json() - - -@pytest.mark.usefixtures("client", "access_token") -def test_get_sector_not_found(client, access_token): - headers = {"Authorization": f"Bearer {access_token}"} - response = client.get("/sectors/invalid_key", headers=headers) - assert response.status_code == 404 - - -def test_create_sector_forbidden(): - response = client.post("/sectors/", json={"key": "TST", "description": "Test"}) - assert response.status_code in (403, 405, 404) - - -def test_update_sector_forbidden(): - response = client.put("/sectors/TST", json={"key": "TST", "description": "Test"}) - assert response.status_code in (403, 405, 404) - - -def test_delete_sector_forbidden(): - response = client.delete("/sectors/TST") - assert response.status_code in (403, 405, 404) diff --git a/backend/main.py b/backend/main.py index 08987b2e..b5c0f325 100644 --- a/backend/main.py +++ b/backend/main.py @@ -24,7 +24,7 @@ from api.v1.modules.public.reference_data.pedimento_regimens.models import Regim from api.v1.modules.public.reference_data.code_pedimento_regimens.models import ( CodePedimentoRegimen, ) -from api.v1.modules.public.reference_data.sectors.models import Sector +from api.v1.modules.a76.general_catalogs.sectors.models import Sector from api.v1.modules.public.reference_data.states.models import State from api.v1.modules.public.reference_data.transport_modes.models import TransportMode from api.v1.modules.public.reference_data.transport_types.models import TransportType @@ -221,7 +221,6 @@ from api.v1.modules.public.reference_data.pedimento_codes.models import Pediment from api.v1.modules.public.reference_data.pedimento_regimens.models import ( RegimenPedimento, ) -from api.v1.modules.public.reference_data.sectors.models import Sector from api.v1.modules.public.reference_data.states.models import State from api.v1.modules.public.reference_data.transport_modes.models import TransportMode from api.v1.modules.public.reference_data.transport_types.models import TransportType diff --git a/frontend/src/lib/api/dashboard/general_catalogs/sectors.ts b/frontend/src/lib/api/dashboard/general_catalogs/sectors.ts index b92acd2f..52fecd72 100644 --- a/frontend/src/lib/api/dashboard/general_catalogs/sectors.ts +++ b/frontend/src/lib/api/dashboard/general_catalogs/sectors.ts @@ -1,11 +1,14 @@ - import { api } from '$lib/api'; -import type { ApiResponse } from '$lib/api'; export interface Sector { + id: number; key: string; description: string; authorized: boolean; + company_id: number; + tenant_id: number; + created_at?: string; + updated_at?: string; } export interface SectorListResponse { @@ -18,18 +21,20 @@ export interface SectorListResponse { export async function getSectors( page = 1, pageSize = 50, + companyId: number, search?: string ): Promise { const params = new URLSearchParams({ page: page.toString(), - page_size: pageSize.toString() + page_size: pageSize.toString(), + company_id: companyId.toString() }); if (search) { - params.append('search', search); + params.append('key', search); } - const response = await api.get(`/v1/public/reference_data/sectors/?${params.toString()}`); + const response = await api.get(`/v1/a76/sectors/?${params.toString()}`); if (!response.data) throw new Error('Error fetching sectors'); return response.data; } diff --git a/frontend/src/lib/api/dashboard/reference_data/sectors.ts b/frontend/src/lib/api/dashboard/reference_data/sectors.ts index 409c9ef0..d36a26eb 100644 --- a/frontend/src/lib/api/dashboard/reference_data/sectors.ts +++ b/frontend/src/lib/api/dashboard/reference_data/sectors.ts @@ -1,13 +1,17 @@ /** - * API Client para Sectors - * Gestiona las operaciones CRUD para los sectores + * API Client para Sectors (a76 — tenant-scoped) */ import { api } from '$lib/api'; export interface Sector { + id: number; key: string; description: string; - authorized: number; + authorized: boolean; + company_id: number; + tenant_id: number; + created_at?: string; + updated_at?: string; } export interface SectorListResponse { @@ -20,60 +24,45 @@ export interface SectorListResponse { export interface CreateSectorData { key: string; description: string; - authorized: number; + authorized: boolean; } export interface UpdateSectorData { key?: string; description?: string; - authorized?: number; + authorized?: boolean; } -/** - * API para Sectors - */ export const sectorsApi = { - /** - * Lista todos los sectores con paginación - * @param page - Número de página (por defecto 1) - * @param pageSize - Tamaño de página (por defecto 50) - */ - list: (page = 1, pageSize = 50) => - api.get( - // CORREGIDO: Slash antes del '?' - `/v1/public/reference_data/sectors/?page=${page}&page_size=${pageSize}` - ), + list: (page = 1, pageSize = 50, companyId: number, search?: string) => { + const params = new URLSearchParams({ + page: page.toString(), + page_size: pageSize.toString(), + company_id: companyId.toString() + }); + if (search) params.append('key', search); + return api.get(`/v1/a76/sectors/?${params.toString()}`); + }, - /** - * Obtiene un sector por key - * @param key - Clave del sector - */ - get: (key: string) => - // CORREGIDO: Slash final - api.get(`/v1/public/reference_data/sectors/${key}/`), + get: (id: number, companyId: number) => + api.get(`/v1/a76/sectors/${id}/?company_id=${companyId}`), - /** - * Crea un nuevo sector - * @param data - Datos del sector a crear - */ - create: (data: CreateSectorData) => - // CORREGIDO: Slash final - api.post('/v1/public/reference_data/sectors/', data), + getByKey: (key: string, companyId: number) => { + const params = new URLSearchParams({ + page: '1', + page_size: '1', + company_id: companyId.toString(), + key + }); + return api.get(`/v1/a76/sectors/?${params.toString()}`); + }, - /** - * Actualiza un sector existente - * @param key - Clave del sector a actualizar - * @param data - Datos a actualizar - */ - update: (key: string, data: UpdateSectorData) => - // CORREGIDO: Slash final después de la variable - api.put(`/v1/public/reference_data/sectors/${key}/`, data), + create: (data: CreateSectorData, companyId: number) => + api.post(`/v1/a76/sectors/?company_id=${companyId}`, data), - /** - * Elimina un sector - * @param key - Clave del sector a eliminar - */ - delete: (key: string) => - // CORREGIDO: Slash final después de la variable - api.delete(`/v1/public/reference_data/sectors/${key}/`) -}; \ No newline at end of file + update: (id: number, data: UpdateSectorData, companyId: number) => + api.put(`/v1/a76/sectors/${id}/?company_id=${companyId}`, data), + + delete: (id: number, companyId: number) => + api.delete(`/v1/a76/sectors/${id}/?company_id=${companyId}`) +}; diff --git a/frontend/src/lib/components/dashboard/goods/parts/partForm.svelte b/frontend/src/lib/components/dashboard/goods/parts/partForm.svelte index 9c7876a2..2ce54185 100644 --- a/frontend/src/lib/components/dashboard/goods/parts/partForm.svelte +++ b/frontend/src/lib/components/dashboard/goods/parts/partForm.svelte @@ -1,3628 +1,3719 @@ - - -
-
-
-
- -

{title}

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

- {#if formType === 'both'} - - AMBOS - Gestión Inventario + Activo Fijo - - {:else if formType === 'fa'} - - SCAF - Gestión de Activo Fijo - - {:else} - - SCAI - Gestión de Inventario - - {/if} -

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