From dbb828c92a0b68337fce87f1dec801c2793c0cb6 Mon Sep 17 00:00:00 2001 From: Kevin_Ramirez Date: Thu, 26 Feb 2026 12:04:31 -0600 Subject: [PATCH 01/11] recontruyendo CRUD en lugar de sitar --- .../fractions/us_tariff_fractions/routes.py | 40 ++---- .../fractions/us_tariff_fractions/service.py | 130 ++---------------- 2 files changed, 26 insertions(+), 144 deletions(-) diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py index d5552c7e..daabcd8a 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py @@ -17,10 +17,19 @@ from .dto import ( ) from .service import USTariffFractionService -# Create base router with generic CRUD routes - REMOVED strictly read-only from Sitar -# Writes are disabled at API level, but Service still supports fallback writes if needed internally +# Create router using TenantCRUDRoutes factory for basic CRUD operations +crud_router = TenantCRUDRoutes( + service=USTariffFractionService, + create_schema=USTariffFractionCreateDTO, + update_schema=USTariffFractionUpdateDTO, + response_schema=USTariffFractionResponseDTO, + prefix="/us-tariff-fractions", + tags=["a76 / general catalogs / us tariff fractions"], + resource_name="US Tariff Fraction", + enable_list=False, # We implement our custom list endpoint +) -router = APIRouter(prefix="/us-tariff-fractions", tags=["a76 / general catalogs / us tariff fractions"]) +router = crud_router.router # Custom list endpoint with search filter @router.get( @@ -44,8 +53,8 @@ async def list_us_tariff_fractions( if search: filters["search"] = search - # Updated to async call with Sitar integration - items, total = await USTariffFractionService.get_all( + # Updated to sync call + items, total = USTariffFractionService.get_all( db, tenant_id, company_id, skip, page_size, filters ) @@ -56,24 +65,3 @@ async def list_us_tariff_fractions( "page_size": page_size, "pages": (total + page_size - 1) // page_size, } - - -@router.get( - "/{us_tariff_fraction_id}", - response_model=USTariffFractionResponseDTO, - summary="Get US Tariff Fraction by ID", - description="Get a specific US tariff fraction by ID (Lookups in Local DB for legacy compatibility)", -) -async def get_us_tariff_fraction( - us_tariff_fraction_id: int, - 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) - - item = USTariffFractionService.get_by_id(db, tenant_id, company_id, us_tariff_fraction_id) - if not item: - from fastapi import HTTPException - raise HTTPException(status_code=404, detail="US Tariff fraction not found") - return USTariffFractionResponseDTO.model_validate(item) diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/service.py b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/service.py index 877dfcc5..42e8b956 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/service.py @@ -1,73 +1,21 @@ -""" -Service para fracciones arancelarias americanas -""" - from typing import List, Optional, Tuple, Dict, Any from sqlalchemy.orm import Session from sqlalchemy.exc import IntegrityError from fastapi import HTTPException -import zlib import logging -import re from decimal import Decimal from .models import USTariffFraction from .dto import USTariffFractionCreateDTO, USTariffFractionUpdateDTO -from api.v1.modules.sitar.fracciones_usa.service import FraccionesUSAService -from api.v1.modules.sitar.fracciones_usa.schemas import FraccionesUSAResponse logger = logging.getLogger(__name__) -class USTariffFractionMapper: - """Helper to map Sitar USA responses to Local domain objects""" - - @staticmethod - def to_domain(fraccion: FraccionesUSAResponse, tenant_id: int, company_id: int) -> USTariffFraction: - # Generate a deterministic numeric ID based on the unique code - # We use CRC32 to get a consistent integer implementation-independent - fake_id = zlib.crc32((fraccion.FRACCION_SIN_PUNTO or "").encode('utf-8')) - - # Parse numeric values safely - ad_valorem = None - if fraccion.TARIFA1: - try: - # Extract numbers from string like "5.2%" or similar if present - # Assuming TARIFA1 might be clean number or percentage string - clean_val = re.sub(r'[^\d.]', '', str(fraccion.TARIFA1)) - if clean_val: - ad_valorem = Decimal(clean_val) - except: - pass - - fixed_cost = None - if fraccion.ESPECIFICO: - try: - clean_val = re.sub(r'[^\d.]', '', str(fraccion.ESPECIFICO)) - if clean_val: - fixed_cost = Decimal(clean_val) - except: - pass - - return USTariffFraction( - id=fake_id, # Updated to use fake_id instead of sitar consecutive if needed, or consistent hash - tenant_id=tenant_id, - company_id=company_id, - code=fraccion.FRACCION_SIN_PUNTO or "", - prefix=None, # Not mapped from Sitar response currently - type_code=None, - ad_valorem=ad_valorem, - fixed_cost=fixed_cost, - unit_of_measure=fraccion.UNIDADCANTIDAD, - description=fraccion.DESCRIPCION - ) - - class USTariffFractionService: """Service para gestionar fracciones arancelarias americanas""" @staticmethod - async def get_all( + def get_all( db: Session, tenant_id: int, company_id: int, @@ -75,60 +23,7 @@ class USTariffFractionService: limit: int = 100, filters: Optional[Dict[str, Any]] = None, ) -> Tuple[List[USTariffFraction], int]: - """ - Obtiene todas las fracciones arancelarias americanas con filtros opcionales. - Estrategia: Sitar API -> Fallback Local DB - """ - - # 1. Try Sitar API - try: - sitar_service = FraccionesUSAService.get_instance() - - sitar_fraccion = None - has_filters = False - - if filters and filters.get("search"): - term = filters["search"] - # Sitar only filters by fraction code - if term.replace(".", "").isdigit(): - sitar_fraccion = term - has_filters = True - - sitar_items = await sitar_service.search( - fraccion=sitar_fraccion, - skip=skip, - limit=limit - ) - - # If Sitar returns empty list AND we didn't have specific filters, attempt fallback - if not sitar_items and not has_filters: - logger.warning("Sitar return empty list for USA broad query. Attempting fallback to local DB.") - return USTariffFractionService._get_all_local(db, tenant_id, company_id, skip, limit, filters) - - # Map items - items = [USTariffFractionMapper.to_domain(item, tenant_id, company_id) for item in sitar_items] - - # Estimate total - total = len(items) + skip - if len(items) == limit: - total += 1 - - return items, total - - except Exception as e: - logger.error(f"Error fetching USA Fractions from Sitar API, falling back to local DB: {e}") - return USTariffFractionService._get_all_local(db, tenant_id, company_id, skip, limit, filters) - - @staticmethod - def _get_all_local( - db: Session, - tenant_id: int, - company_id: int, - skip: int = 0, - limit: int = 100, - filters: Optional[Dict[str, Any]] = None, - ) -> Tuple[List[USTariffFraction], int]: - """Lógica original de consulta local""" + """Obtiene todas las fracciones locales con filtros opcionales.""" query = db.query(USTariffFraction).filter( USTariffFraction.tenant_id == tenant_id, USTariffFraction.company_id == company_id, @@ -150,13 +45,14 @@ class USTariffFractionService: return items, total + return items, total + @staticmethod def get_by_id( - db: Session, tenant_id: int, company_id: int, fraction_id: int + db: Session, fraction_id: int, tenant_id: int, company_id: int ) -> Optional[USTariffFraction]: """ - Obtiene por ID. - Legacy: Consulta Local DB. + Obtiene por ID local. """ return ( db.query(USTariffFraction) @@ -168,14 +64,12 @@ class USTariffFractionService: .first() ) - # WRITE OPERATIONS - DEPRECATED / LOCAL ONLY - @staticmethod def create( db: Session, + fraction_data: USTariffFractionCreateDTO, tenant_id: int, company_id: int, - fraction_data: USTariffFractionCreateDTO, ) -> USTariffFraction: try: db_fraction = USTariffFraction( @@ -198,13 +92,13 @@ class USTariffFractionService: @staticmethod def update( db: Session, - tenant_id: int, - company_id: int, fraction_id: int, + tenant_id: int, fraction_data: USTariffFractionUpdateDTO, + company_id: int, ) -> Optional[USTariffFraction]: db_fraction = USTariffFractionService.get_by_id( - db, tenant_id, company_id, fraction_id + db, fraction_id, tenant_id, company_id ) if not db_fraction: return None @@ -219,10 +113,10 @@ class USTariffFractionService: @staticmethod def delete( - db: Session, tenant_id: int, company_id: int, fraction_id: int + db: Session, fraction_id: int, tenant_id: int, company_id: int ) -> bool: db_fraction = USTariffFractionService.get_by_id( - db, tenant_id, company_id, fraction_id + db, fraction_id, tenant_id, company_id ) if not db_fraction: return False From 3723cfd50ec25e03bd9a13ef05f504d0c19e8009 Mon Sep 17 00:00:00 2001 From: Kevin_Ramirez Date: Thu, 26 Feb 2026 16:45:56 -0600 Subject: [PATCH 02/11] Sistema CRUD sin servicio sitar --- .../fractions/tariff_fractions/routes.py | 17 ++++--- .../fractions/tariff_fractions/service.py | 12 +++-- .../fractions/us_tariff_fractions/dto.py | 39 +++++++++++++++- .../fractions/us_tariff_fractions/routes.py | 1 + backend/test_debug.py | 44 +++++++++++++++++++ backend/test_user_info.py | 39 ++++++++++++++++ .../fractions/TariffFractionFormDialog.svelte | 26 +++++++---- .../goods/fractions/TariffFractionList.svelte | 2 +- 8 files changed, 157 insertions(+), 23 deletions(-) create mode 100644 backend/test_debug.py create mode 100644 backend/test_user_info.py diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/routes.py b/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/routes.py index 5f0f1600..21277c99 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/routes.py @@ -8,7 +8,7 @@ from fastapi import APIRouter, Depends, Query from sqlalchemy.orm import Session from core.database import get_core_db -from core.security import get_current_user +from core.security import get_current_user, get_tenant_from_token from .dto import ( TariffFractionCreateDTO, @@ -27,6 +27,7 @@ router = APIRouter(prefix="/tariff-fractions", tags=["a76 / general catalogs / t description="Get paginated list of Tariff Fractions with optional search filter (global catalog)", ) async def list_tariff_fractions( + company_id: int = Query(..., description="Company ID"), page: int = Query(1, ge=1, description="Page number"), page_size: int = Query(50, ge=1, le=10000, description="Page size"), search: Optional[str] = Query(None, description="Search in code, fraction, description, nico, or umt"), @@ -47,8 +48,9 @@ async def list_tariff_fractions( # Service.get_all calls Sitar (async) or DB (sync). # This should be fine. - tenant_id = current_user.get("tenant_id") - company_id = current_user.get("company_id") # Assuming user is context-aware or we use a default? + tenant_id = get_tenant_from_token(current_user) + if tenant_id is None: + tenant_id = current_user.get("tenant_id") # If using headers for selected company, it might be in current_user context if middleware sets it. items, total = await TariffFractionService.get_all( @@ -121,7 +123,7 @@ async def create_tariff_fraction( pass us_dto = USTariffFractionCreateDTO( - code=fraction_data.code, + code=fraction_data.fraction, # Store the punctuated fraction in the DB description=fraction_data.description, unit_of_measure=fraction_data.umt, ad_valorem=ad_valorem, @@ -131,7 +133,7 @@ async def create_tariff_fraction( fixed_cost=None ) - created = USTariffFractionService.create(db, tenant_id, company_id, us_dto) + created = USTariffFractionService.create(db, us_dto, tenant_id, company_id) return TariffFractionService.to_domain_usa_local(created) else: @@ -171,12 +173,13 @@ async def update_tariff_fraction( pass us_dto = USTariffFractionUpdateDTO( + code=fraction_data.fraction, description=fraction_data.description, unit_of_measure=fraction_data.umt, ad_valorem=ad_valorem ) - updated = USTariffFractionService.update(db, tenant_id, company_id, tariff_fraction_id, us_dto) + updated = USTariffFractionService.update(db, tariff_fraction_id, tenant_id, us_dto, company_id) if not updated: raise HTTPException(status_code=404, detail="US Tariff fraction not found") return TariffFractionService.to_domain_usa_local(updated) @@ -202,7 +205,7 @@ async def delete_tariff_fraction( if catalog == "american": from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.service import USTariffFractionService - success = USTariffFractionService.delete(db, tenant_id, company_id, tariff_fraction_id) + success = USTariffFractionService.delete(db, tariff_fraction_id, tenant_id, company_id) if not success: raise HTTPException(status_code=404, detail="US Tariff fraction not found") return {"ok": True} diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/service.py b/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/service.py index 7a00b364..65530eef 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/service.py @@ -96,10 +96,14 @@ class TariffFractionService: # US format: 1234.56.78.90. For now return as is or use helper if available. # item is USTariffFraction (imported inside method to avoid circular import if needed, or assumed available) + # Remove formatting (e.g. dots) for the 'code' property + code_str = str(item.code) + clean_code = code_str.replace(".", "").replace("-", "") + return TariffFraction( id=item.id, - code=item.code, - fraction=item.code, # TODO: Format if needed + code=clean_code, + fraction=code_str, description=item.description or "(Sin descripción)", nico=None, umt=item.unit_of_measure, @@ -133,11 +137,11 @@ class TariffFractionService: from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.service import USTariffFractionService # Use local service directly - usa_items, total = USTariffFractionService._get_all_local( + usa_items, total = USTariffFractionService.get_all( db, tenant_id, company_id, skip, limit, filters ) - items = [TariffFractionMapper.to_domain_usa_local(item) for item in usa_items] + items = [TariffFractionService.to_domain_usa_local(item) for item in usa_items] return items, total # USA CATALOG HANDLING (API - 'Fracciones US') diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/dto.py b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/dto.py index 1c943349..c316dc08 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/dto.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/dto.py @@ -3,9 +3,9 @@ DTOs para fracciones arancelarias americanas """ from datetime import datetime -from typing import Optional +from typing import Optional, Any -from pydantic import BaseModel, Field, ConfigDict +from pydantic import BaseModel, Field, ConfigDict, model_validator class USTariffFractionCreateDTO(BaseModel): @@ -23,6 +23,7 @@ class USTariffFractionCreateDTO(BaseModel): class USTariffFractionUpdateDTO(BaseModel): """DTO para actualizar fracción arancelaria americana""" + code: Optional[str] = Field(None, max_length=16) prefix: Optional[str] = Field(None, max_length=10) type_code: Optional[str] = Field(None, max_length=10) ad_valorem: Optional[float] = None @@ -38,6 +39,7 @@ class USTariffFractionResponseDTO(BaseModel): id: int code: str + fraction: Optional[str] = None prefix: Optional[str] = None type_code: Optional[str] = None ad_valorem: Optional[float] = None @@ -46,3 +48,36 @@ class USTariffFractionResponseDTO(BaseModel): description: Optional[str] = None created_at: datetime updated_at: datetime + + @model_validator(mode="before") + @classmethod + def format_code_and_fraction(cls, data: Any) -> Any: + # Check if data is an ORM model or dict + if hasattr(data, "code"): + raw_code = data.code + elif isinstance(data, dict): + raw_code = data.get("code") + else: + return data + + if raw_code: + code_str = str(raw_code) + # fraction keeps the original formatted string + fraction = code_str + # code strips dots and hyphens + code = code_str.replace(".", "").replace("-", "") + + if isinstance(data, dict): + data["code"] = code + data["fraction"] = fraction + else: + # If it's an ORM object, we can't easily modify the object's attribute + # cleanly without side effects for other things, so we convert it to dict + new_data = { + c.name: getattr(data, c.name) for c in data.__table__.columns + } + new_data["code"] = code + new_data["fraction"] = fraction + return new_data + + return data diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py index daabcd8a..f6521624 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py @@ -26,6 +26,7 @@ crud_router = TenantCRUDRoutes( prefix="/us-tariff-fractions", tags=["a76 / general catalogs / us tariff fractions"], resource_name="US Tariff Fraction", + id_name="id", enable_list=False, # We implement our custom list endpoint ) diff --git a/backend/test_debug.py b/backend/test_debug.py new file mode 100644 index 00000000..cfb342be --- /dev/null +++ b/backend/test_debug.py @@ -0,0 +1,44 @@ +import sys +import os +sys.path.append('/app') +sys.path.append('/home/josmar/dev/anexo76/backend') + +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +import asyncio +import logging + +# Disable logging to keep output clean +logging.basicConfig(level=logging.ERROR) + +from api.v1.modules.a76.general_catalogs.fractions.tariff_fractions.service import TariffFractionService +from api.v1.modules.a76.general_catalogs.fractions.tariff_fractions.dto import TariffFractionResponseDTO + +async def test(): + # Use the database URL from the environment or default + db_url = "postgresql://postgres:postgres@anexo76-postgres-a76:5432/anexo76_core" + engine = create_engine(db_url) + Session = sessionmaker(bind=engine) + db = Session() + + try: + print("Starting test for catalog='american'...") + items, total = await TariffFractionService.get_all( + db, skip=0, limit=10, filters=None, catalog="american", tenant_id=1, company_id=1 + ) + print(f"Service Success! Total: {total}") + + print("Validating items with TariffFractionResponseDTO...") + for item in items: + dto = TariffFractionResponseDTO.model_validate(item) + print(f"DTO: ID={dto.id}, Code={dto.code}, Fraction={dto.fraction}") + + except Exception as e: + print(f"Error caught: {type(e).__name__}: {e}") + import traceback + traceback.print_exc() + finally: + db.close() + +if __name__ == "__main__": + asyncio.run(test()) diff --git a/backend/test_user_info.py b/backend/test_user_info.py new file mode 100644 index 00000000..17f2cc2b --- /dev/null +++ b/backend/test_user_info.py @@ -0,0 +1,39 @@ +import sys +import os +sys.path.append('/app') +sys.path.append('/home/josmar/dev/anexo76/backend') + +from sqlalchemy import create_engine, text +from sqlalchemy.orm import sessionmaker + +def test(): + db_url = "postgresql://postgres:postgres@anexo76-postgres-a76:5432/anexo76_core" + engine = create_engine(db_url) + Session = sessionmaker(bind=engine) + db = Session() + + try: + print("Checking users and tenants...") + # Check users + users = db.execute(text("SELECT id, email, tenant_id FROM a76.users")).fetchall() + for u in users: + print(f"User ID: {u.id} | Email: {u.email} | Tenant ID: {u.tenant_id}") + + # Check companies + companies = db.execute(text("SELECT id, name, tenant_id FROM a76.companies")).fetchall() + for c in companies: + print(f"Company ID: {c.id} | Name: {c.name} | Tenant ID: {c.tenant_id}") + + # Check fractions + fractions = db.execute(text("SELECT id, code, tenant_id, company_id FROM a76.us_tariff_fractions")).fetchall() + print(f"Total US Fractions in DB: {len(fractions)}") + for f in fractions: + print(f"Fraction ID: {f.id} | Code: {f.code} | Tenant ID: {f.tenant_id} | Company ID: {f.company_id}") + + except Exception as e: + print(f"Error: {e}") + finally: + db.close() + +if __name__ == "__main__": + test() diff --git a/frontend/src/lib/components/dashboard/goods/fractions/TariffFractionFormDialog.svelte b/frontend/src/lib/components/dashboard/goods/fractions/TariffFractionFormDialog.svelte index 93eea293..d92d48e5 100644 --- a/frontend/src/lib/components/dashboard/goods/fractions/TariffFractionFormDialog.svelte +++ b/frontend/src/lib/components/dashboard/goods/fractions/TariffFractionFormDialog.svelte @@ -126,17 +126,25 @@
- - - {#if fraction} -

- El código no se puede modificar una vez creado. -

- {/if} + + +

+ {catalog === 'mex' + ? 'El código se genera automáticamente.' + : 'La clave se deriva de la fracción sin puntos.'} +

- - + +
diff --git a/frontend/src/lib/components/dashboard/goods/fractions/TariffFractionList.svelte b/frontend/src/lib/components/dashboard/goods/fractions/TariffFractionList.svelte index 15e75d87..ba3ea1cf 100644 --- a/frontend/src/lib/components/dashboard/goods/fractions/TariffFractionList.svelte +++ b/frontend/src/lib/components/dashboard/goods/fractions/TariffFractionList.svelte @@ -310,7 +310,7 @@ fraction={selectedFraction} {catalog} onSuccess={() => { - loadFractions(); + loadFractions(true); isFormDialogOpen = false; }} /> From 5e114bb564c9e552534c84400d5baaf344d7fe04 Mon Sep 17 00:00:00 2001 From: Kevin_Ramirez Date: Thu, 26 Feb 2026 16:52:48 -0600 Subject: [PATCH 03/11] Se agrego los efectos para caluclar la clave --- .../goods/fractions/TariffFractionFormDialog.svelte | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/src/lib/components/dashboard/goods/fractions/TariffFractionFormDialog.svelte b/frontend/src/lib/components/dashboard/goods/fractions/TariffFractionFormDialog.svelte index d92d48e5..9e0a6b62 100644 --- a/frontend/src/lib/components/dashboard/goods/fractions/TariffFractionFormDialog.svelte +++ b/frontend/src/lib/components/dashboard/goods/fractions/TariffFractionFormDialog.svelte @@ -66,6 +66,13 @@ } }); + // Sync code with fractionFormatted for US catalog + $effect(() => { + if (catalog === 'usa' || catalog === 'american') { + code = fractionFormatted.replace(/\./g, '').replace(/-/g, ''); + } + }); + async function handleSubmit() { const companyId = companyStore.activeCompany?.id; if (!companyId) return; @@ -126,9 +133,7 @@
- +

{catalog === 'mex' From dbffc95e67710b65340288b86b9d555f0c26ec69 Mon Sep 17 00:00:00 2001 From: Kevin_Ramirez Date: Thu, 26 Feb 2026 18:03:14 -0600 Subject: [PATCH 04/11] Correcion de yml y los script --- backend/core/celery_app.py | 2 +- docker-compose.yml | 3 --- scripts/backend-entrypoint.sh | 0 scripts/frontend-entrypoint.sh | 0 scripts/health-check.sh | 0 scripts/init_first_time.sh | 0 scripts/keycloak-entrypoint.sh | 0 scripts/postgres-app-entrypoint.sh | 0 scripts/postgres-keycloak-entrypoint.sh | 0 start.sh | 0 10 files changed, 1 insertion(+), 4 deletions(-) mode change 100644 => 100755 scripts/backend-entrypoint.sh mode change 100644 => 100755 scripts/frontend-entrypoint.sh mode change 100644 => 100755 scripts/health-check.sh mode change 100644 => 100755 scripts/init_first_time.sh mode change 100644 => 100755 scripts/keycloak-entrypoint.sh mode change 100644 => 100755 scripts/postgres-app-entrypoint.sh mode change 100644 => 100755 scripts/postgres-keycloak-entrypoint.sh mode change 100644 => 100755 start.sh diff --git a/backend/core/celery_app.py b/backend/core/celery_app.py index 35694fea..e5648d42 100644 --- a/backend/core/celery_app.py +++ b/backend/core/celery_app.py @@ -28,7 +28,7 @@ celery_app.conf.update( "api.v1.modules.a76.imports.tasks", "api.v1.modules.a76.reports.exportacion.transmission.MAINX30.task", "api.v1.modules.a76.reports.importacion.transmission.temporal.MAINX30.task", - "api.v1.modules.a76.reports.importacion.transmission.definitive.MAINX30.task" + "api.v1.modules.a76.reports.importacion.transmission.definitive.MAINX30.task", "api.v1.modules.core.help_center.tasks" ] # Ruta al módulo donde están las tareas ) diff --git a/docker-compose.yml b/docker-compose.yml index aeb6990b..5ae2e394 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -332,9 +332,6 @@ services: - backend_uploads:/app/uploads networks: - backend-net - volumes: - - ./backend:/app - valkey: image: valkey/valkey:7.2 diff --git a/scripts/backend-entrypoint.sh b/scripts/backend-entrypoint.sh old mode 100644 new mode 100755 diff --git a/scripts/frontend-entrypoint.sh b/scripts/frontend-entrypoint.sh old mode 100644 new mode 100755 diff --git a/scripts/health-check.sh b/scripts/health-check.sh old mode 100644 new mode 100755 diff --git a/scripts/init_first_time.sh b/scripts/init_first_time.sh old mode 100644 new mode 100755 diff --git a/scripts/keycloak-entrypoint.sh b/scripts/keycloak-entrypoint.sh old mode 100644 new mode 100755 diff --git a/scripts/postgres-app-entrypoint.sh b/scripts/postgres-app-entrypoint.sh old mode 100644 new mode 100755 diff --git a/scripts/postgres-keycloak-entrypoint.sh b/scripts/postgres-keycloak-entrypoint.sh old mode 100644 new mode 100755 diff --git a/start.sh b/start.sh old mode 100644 new mode 100755 From 18f63951acd7b5cc82a479304cbe0b3925c6d398 Mon Sep 17 00:00:00 2001 From: Kevin_Ramirez Date: Fri, 27 Feb 2026 09:19:27 -0600 Subject: [PATCH 05/11] Se corrigio el problema de las fracciones americanas y problemas al guardar --- backend/api/v1/modules/a76/classes/dto.py | 4 ++++ backend/api/v1/modules/a76/classes/service.py | 4 ++++ .../fractions/us_tariff_fractions/service.py | 6 +++--- frontend/src/lib/api.ts | 2 +- .../goods/classes/forms/FixedAssetClassForm.svelte | 2 +- .../dashboard/goods/fixed-asset-classes/+page.svelte | 8 ++++++++ 6 files changed, 21 insertions(+), 5 deletions(-) diff --git a/backend/api/v1/modules/a76/classes/dto.py b/backend/api/v1/modules/a76/classes/dto.py index 68054f2a..b8047e2f 100644 --- a/backend/api/v1/modules/a76/classes/dto.py +++ b/backend/api/v1/modules/a76/classes/dto.py @@ -221,6 +221,10 @@ class ClassWithFADataResponse(BaseModel): # FA-specific fields (embedded from a24.fa_classes) fa_class_id: Optional[int] = None + import_tariff_code: Optional[str] = None + import_tariff_type: Optional[str] = None + export_tariff_code: Optional[str] = None + export_tariff_type: Optional[str] = None depreciation_rate: Optional[Decimal] = None fda_code: Optional[str] = None eccn_code: Optional[str] = None diff --git a/backend/api/v1/modules/a76/classes/service.py b/backend/api/v1/modules/a76/classes/service.py index 4decee1c..904adddf 100644 --- a/backend/api/v1/modules/a76/classes/service.py +++ b/backend/api/v1/modules/a76/classes/service.py @@ -150,6 +150,10 @@ class ClassService: "updated_at": base_class.updated_at, # FA extension fields (None if no FA record exists) "fa_class_id": fa_class.id if fa_class else None, + "import_tariff_code": fa_class.import_tariff_code if fa_class else None, + "import_tariff_type": fa_class.import_tariff_type if fa_class else None, + "export_tariff_code": fa_class.export_tariff_code if fa_class else None, + "export_tariff_type": fa_class.export_tariff_type if fa_class else None, "depreciation_rate": fa_class.depreciation_rate if fa_class else None, "fda_code": fa_class.fda_code if fa_class else None, "eccn_code": fa_class.eccn_code if fa_class else None, diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/service.py b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/service.py index 877dfcc5..25894e11 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/service.py @@ -100,9 +100,9 @@ class USTariffFractionService: limit=limit ) - # If Sitar returns empty list AND we didn't have specific filters, attempt fallback - if not sitar_items and not has_filters: - logger.warning("Sitar return empty list for USA broad query. Attempting fallback to local DB.") + # If Sitar returns empty list, attempt fallback to local DB + if not sitar_items: + logger.info(f"Sitar returned no results for USA query (filters={has_filters}). Attempting fallback to local DB.") return USTariffFractionService._get_all_local(db, tenant_id, company_id, skip, limit, filters) # Map items diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index aed649d2..baa8f75c 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -270,7 +270,7 @@ async function fetchApi( } return { - error: data.message || data.detail || 'Error en la petición', + error: data.message || (typeof data.detail === 'string' ? data.detail : JSON.stringify(data.detail)) || 'Error en la petición', status: response.status }; } diff --git a/frontend/src/lib/components/dashboard/goods/classes/forms/FixedAssetClassForm.svelte b/frontend/src/lib/components/dashboard/goods/classes/forms/FixedAssetClassForm.svelte index fb406f32..d5578a0c 100644 --- a/frontend/src/lib/components/dashboard/goods/classes/forms/FixedAssetClassForm.svelte +++ b/frontend/src/lib/components/dashboard/goods/classes/forms/FixedAssetClassForm.svelte @@ -541,7 +541,7 @@ class="uppercase {validationErrors.class_code ? 'border-red-500 focus-visible:ring-red-500' : ''}" - maxlength={20} + maxlength={8} oninput={() => { if (validationErrors.class_code) { const errors = { ...validationErrors }; diff --git a/frontend/src/routes/dashboard/goods/fixed-asset-classes/+page.svelte b/frontend/src/routes/dashboard/goods/fixed-asset-classes/+page.svelte index 5a0af337..35ccccde 100644 --- a/frontend/src/routes/dashboard/goods/fixed-asset-classes/+page.svelte +++ b/frontend/src/routes/dashboard/goods/fixed-asset-classes/+page.svelte @@ -14,6 +14,10 @@ // Tipo extendido que combina A76Class y FAClass interface FixedAssetClassExtended extends A76Class { fa_class_id?: number; + import_tariff_code?: string | null; + import_tariff_type?: string | null; + export_tariff_code?: string | null; + export_tariff_type?: string | null; depreciation_rate?: number | null; fda_code?: string | null; eccn_code?: string | null; @@ -578,6 +582,10 @@ cleanData.depreciation_rate != null ? Number(cleanData.depreciation_rate) : null, + import_tariff_code: cleanData.import_tariff_code || null, + import_tariff_type: cleanData.import_tariff_type || null, + export_tariff_code: cleanData.export_tariff_code || null, + export_tariff_type: cleanData.export_tariff_type || null, fda_code: (cleanData.fda_key ?? cleanData.fda_code) || null, eccn_code: cleanData.eccn_code || null }, From 35a6f88211912a56720da5c1754577f7f26be6d1 Mon Sep 17 00:00:00 2001 From: AlexeerCT Date: Fri, 27 Feb 2026 11:07:16 -0600 Subject: [PATCH 06/11] Refactor US Tariff Fractions service to use async for get_all method and add USTariffFractionMapper for response mapping --- .../fractions/us_tariff_fractions/routes.py | 3 +- .../fractions/us_tariff_fractions/service.py | 37 ++++++++++++++++++- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py index f6521624..158bf765 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/routes.py @@ -54,8 +54,7 @@ async def list_us_tariff_fractions( if search: filters["search"] = search - # Updated to sync call - items, total = USTariffFractionService.get_all( + items, total = await USTariffFractionService.get_all( db, tenant_id, company_id, skip, page_size, filters ) diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/service.py b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/service.py index 1409b2f9..124f6460 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/service.py @@ -1,4 +1,5 @@ from typing import List, Optional, Tuple, Dict, Any +from datetime import datetime from sqlalchemy.orm import Session from sqlalchemy.exc import IntegrityError from fastapi import HTTPException @@ -7,15 +8,49 @@ from decimal import Decimal from .models import USTariffFraction from .dto import USTariffFractionCreateDTO, USTariffFractionUpdateDTO +from api.v1.modules.sitar.fracciones_usa.service import FraccionesUSAService logger = logging.getLogger(__name__) +class USTariffFractionMapper: + """Maps Sitar FraccionesUSAResponse objects to USTariffFraction domain objects""" + + @staticmethod + def to_domain( + item: Any, tenant_id: int, company_id: int + ) -> USTariffFraction: + """Convert a FraccionesUSAResponse to a USTariffFraction instance (not persisted)""" + code = item.FRACCION_CON_PUNTO or item.FRACCION_MOSTRAR or item.FRACCION_SIN_PUNTO or "" + ad_valorem: Optional[float] = None + if item.TARIFA1: + try: + ad_valorem = float(str(item.TARIFA1).replace("%", "").strip()) + except (ValueError, TypeError): + ad_valorem = None + + fraction = USTariffFraction() + fraction.id = item.CONSECUTIVO + fraction.tenant_id = tenant_id + fraction.company_id = company_id + fraction.code = code + fraction.prefix = item.FRACCION_SIN_PUNTO + fraction.type_code = str(item.NIVEL) if item.NIVEL is not None else None + fraction.ad_valorem = ad_valorem + fraction.fixed_cost = None + fraction.unit_of_measure = item.UNIDADCANTIDAD + fraction.description = item.DESCRIPCION + now = datetime.now() + fraction.created_at = now + fraction.updated_at = now + return fraction + + class USTariffFractionService: """Service para gestionar fracciones arancelarias americanas""" @staticmethod - def get_all( + async def get_all( db: Session, tenant_id: int, company_id: int, From 5367c9a687ed7550c03ec28d3af53cc90980c121 Mon Sep 17 00:00:00 2001 From: AlexeerCT Date: Fri, 27 Feb 2026 12:01:29 -0600 Subject: [PATCH 07/11] Refactor USTariffFractionService to use async in get_all method and clean up redundant return statement --- .../general_catalogs/fractions/tariff_fractions/service.py | 2 +- .../general_catalogs/fractions/us_tariff_fractions/service.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/service.py b/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/service.py index 65530eef..380cf508 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/tariff_fractions/service.py @@ -137,7 +137,7 @@ class TariffFractionService: from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.service import USTariffFractionService # Use local service directly - usa_items, total = USTariffFractionService.get_all( + usa_items, total = await USTariffFractionService.get_all( db, tenant_id, company_id, skip, limit, filters ) diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/service.py b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/service.py index 124f6460..e2fd10ee 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/service.py @@ -131,9 +131,7 @@ class USTariffFractionService: total = query.count() items = query.order_by(USTariffFraction.code).offset(skip).limit(limit).all() - return items, total - - return items, total + return items, total @staticmethod def get_by_id( From 68bada43fa567dadb8e9f1e80ea1a2c8e762264f Mon Sep 17 00:00:00 2001 From: AlexeerCT Date: Fri, 27 Feb 2026 12:54:12 -0600 Subject: [PATCH 08/11] Remove Sitar API fallback logic from get_all method in USTariffFractionService --- .../fractions/us_tariff_fractions/service.py | 73 +++++++++---------- 1 file changed, 36 insertions(+), 37 deletions(-) diff --git a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/service.py b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/service.py index e2fd10ee..661bf01b 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/fractions/us_tariff_fractions/service.py @@ -63,44 +63,43 @@ class USTariffFractionService: Estrategia: Sitar API -> Fallback Local DB """ - # 1. Try Sitar API - try: - sitar_service = FraccionesUSAService.get_instance() - - sitar_fraccion = None - has_filters = False - - if filters and filters.get("search"): - term = filters["search"] - # Sitar only filters by fraction code - if term.replace(".", "").isdigit(): - sitar_fraccion = term - has_filters = True - - sitar_items = await sitar_service.search( - fraccion=sitar_fraccion, - skip=skip, - limit=limit - ) - - # If Sitar returns empty list, attempt fallback to local DB - if not sitar_items: - logger.info(f"Sitar returned no results for USA query (filters={has_filters}). Attempting fallback to local DB.") - return USTariffFractionService._get_all_local(db, tenant_id, company_id, skip, limit, filters) + ## 1. Try Sitar API + #try: + # sitar_service = FraccionesUSAService.get_instance() + # + # sitar_fraccion = None + # has_filters = False + # + # if filters and filters.get("search"): + # term = filters["search"] + # # Sitar only filters by fraction code + # if term.replace(".", "").isdigit(): + # sitar_fraccion = term + # has_filters = True + # + # sitar_items = await sitar_service.search( + # fraccion=sitar_fraccion, + # skip=skip, + # limit=limit + # ) + # + # # If Sitar returns empty list, attempt fallback to local DB + # if not sitar_items: + # logger.info(f"Sitar returned no results for USA query (filters={has_filters}). Attempting fallback to local DB.") + # return USTariffFractionService._get_all_local(db, tenant_id, company_id, skip, limit, filters) + # + # # Map items + # items = [USTariffFractionMapper.to_domain(item, tenant_id, company_id) for item in sitar_items] + # + # # Estimate total + # total = len(items) + skip + # if len(items) == limit: + # total += 1 + # + # return items, total - # Map items - items = [USTariffFractionMapper.to_domain(item, tenant_id, company_id) for item in sitar_items] - - # Estimate total - total = len(items) + skip - if len(items) == limit: - total += 1 - - return items, total - - except Exception as e: - logger.error(f"Error fetching USA Fractions from Sitar API, falling back to local DB: {e}") - return USTariffFractionService._get_all_local(db, tenant_id, company_id, skip, limit, filters) + #except Exception as e: + return USTariffFractionService._get_all_local(db, tenant_id, company_id, skip, limit, filters) @staticmethod def _get_all_local( From dc4256ae7a943f6606ab3c556ccb2c6234abbf36 Mon Sep 17 00:00:00 2001 From: Galindo97 Date: Mon, 2 Mar 2026 16:47:07 -0700 Subject: [PATCH 09/11] feat: Implement CRUD operations for FDA catalog entries and their associated specifications, constituent elements, affirmation codes, and lot productions. --- .../a76/general_catalogs/fda_catalog/dto.py | 117 +- .../general_catalogs/fda_catalog/models.py | 127 +- .../general_catalogs/fda_catalog/routes.py | 201 ++++ .../general_catalogs/fda_catalog/service.py | 159 +++ backend/core/error_handlers.py | 2 +- frontend/messages/en.json | 3 +- frontend/messages/es.json | 3 +- .../src/lib/components/sidebar/modules.ts | 6 +- .../api-sveltekit/fda-catalog/+server.ts | 283 +++++ .../api-sveltekit/fda-catalog/[id]/+server.ts | 31 + .../[id]/affirmation-codes/+server.ts | 31 + .../[id]/constituent-elements/+server.ts | 31 + .../[id]/lot-productions/+server.ts | 31 + .../[id]/specifications/+server.ts | 31 + .../dashboard/goods/fda-codes/+page.svelte | 469 ++++++++ .../goods/fda-codes/edit/[id]/+page.svelte | 1071 +++++++++++++++++ 16 files changed, 2587 insertions(+), 9 deletions(-) create mode 100644 frontend/src/routes/api-sveltekit/fda-catalog/+server.ts create mode 100644 frontend/src/routes/api-sveltekit/fda-catalog/[id]/+server.ts create mode 100644 frontend/src/routes/api-sveltekit/fda-catalog/[id]/affirmation-codes/+server.ts create mode 100644 frontend/src/routes/api-sveltekit/fda-catalog/[id]/constituent-elements/+server.ts create mode 100644 frontend/src/routes/api-sveltekit/fda-catalog/[id]/lot-productions/+server.ts create mode 100644 frontend/src/routes/api-sveltekit/fda-catalog/[id]/specifications/+server.ts create mode 100644 frontend/src/routes/dashboard/goods/fda-codes/+page.svelte create mode 100644 frontend/src/routes/dashboard/goods/fda-codes/edit/[id]/+page.svelte diff --git a/backend/api/v1/modules/a76/general_catalogs/fda_catalog/dto.py b/backend/api/v1/modules/a76/general_catalogs/fda_catalog/dto.py index e07f048e..0735b6e3 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fda_catalog/dto.py +++ b/backend/api/v1/modules/a76/general_catalogs/fda_catalog/dto.py @@ -39,6 +39,7 @@ class FDACatalogUpdate(BaseModel): } + class FDACatalogResponse(BaseModel): """DTO para respuesta del catálogo FDA""" id: int @@ -51,9 +52,121 @@ class FDACatalogResponse(BaseModel): storage_status: Optional[str] warehouse_code: Optional[str] call_atl: Optional[str] - created_at: Optional[str] - updated_at: Optional[str] model_config = { "from_attributes": True } + + +# DTOs para FDA01 - Especificaciones +class FDASpecificationsBase(BaseModel): + prod_code: Optional[str] = None + commodity_desc: Optional[str] = None + brand_name: Optional[str] = None + disclaimer: Optional[str] = None + pgm_code: Optional[str] = None + proc_code: Optional[str] = None + intnd_use_code: Optional[str] = None + intnd_use_desc: Optional[str] = None + temp_qual: Optional[str] = None + temp_type: Optional[str] = None + temp_degrees: Optional[float] = None + temp_negative: Optional[float] = None + temp_location: Optional[str] = None + quantity_1: Optional[float] = None + qty_uom_1: Optional[str] = None + quantity_2: Optional[float] = None + qty_uom_2: Optional[str] = None + quantity_3: Optional[float] = None + qty_uom_3: Optional[str] = None + ctry_prod: Optional[str] = None + ctry_source: Optional[str] = None + ctry_growth: Optional[str] = None + ctry_refusal: Optional[str] = None + ctry_shipping: Optional[str] = None + manuf_key: Optional[str] = None + shipper_key: Optional[str] = None + ult_cons_key: Optional[str] = None + fda_imp_key: Optional[str] = None + pn_subm_key: Optional[str] = None + consol_key: Optional[str] = None + producer_key: Optional[str] = None + owner_key: Optional[str] = None + deli_party_key: Optional[str] = None + grower_key: Optional[str] = None + dev_ini_imp_key: Optional[str] = None + lacf_cont_1: Optional[str] = None + lacf_cont_2: Optional[str] = None + lacf_cont_3: Optional[str] = None + pn_transmitter_key: Optional[str] = None + +class FDASpecificationsCreate(FDASpecificationsBase): + pass + +class FDASpecificationsUpdate(FDASpecificationsBase): + pass + +class FDASpecificationsResponse(FDASpecificationsBase): + id: int + fda_catalog_id: int + model_config = {"from_attributes": True} + + +# DTOs para FDA04 - Elementos Constitutivos +class FDAConstituentElementsBase(BaseModel): + line: int + ele_name: str + ele_qty: Optional[float] = None + ele_qty_uom: Optional[str] = None + ele_pctg: Optional[float] = None + +class FDAConstituentElementsCreate(FDAConstituentElementsBase): + pass + +class FDAConstituentElementsUpdate(FDAConstituentElementsBase): + line: Optional[int] = None + ele_name: Optional[str] = None + +class FDAConstituentElementsResponse(FDAConstituentElementsBase): + id: int + fda_catalog_id: int + model_config = {"from_attributes": True} + + +# DTOs para FDA23 - Códigos de Afirmación +class FDAAffirmationCodesBase(BaseModel): + line: int + aoc_code: str + aoc_qual: Optional[str] = None + +class FDAAffirmationCodesCreate(FDAAffirmationCodesBase): + pass + +class FDAAffirmationCodesUpdate(FDAAffirmationCodesBase): + line: Optional[int] = None + aoc_code: Optional[str] = None + +class FDAAffirmationCodesResponse(FDAAffirmationCodesBase): + id: int + fda_catalog_id: int + model_config = {"from_attributes": True} + + +# DTOs para FDA25 - Lotes de Producción +class FDALotProductionBase(BaseModel): + line: int + lot_number: str + production_start_date: Optional[str] = None + production_end_date: Optional[str] = None + +class FDALotProductionCreate(FDALotProductionBase): + pass + +class FDALotProductionUpdate(FDALotProductionBase): + line: Optional[int] = None + lot_number: Optional[str] = None + +class FDALotProductionResponse(FDALotProductionBase): + id: int + fda_catalog_id: int + model_config = {"from_attributes": True} diff --git a/backend/api/v1/modules/a76/general_catalogs/fda_catalog/models.py b/backend/api/v1/modules/a76/general_catalogs/fda_catalog/models.py index 5455085e..f408df88 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fda_catalog/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/fda_catalog/models.py @@ -1,9 +1,9 @@ """ Modelo para el catálogo de claves FDA """ -from typing import Optional -from sqlalchemy import String, Integer, UniqueConstraint, PrimaryKeyConstraint -from sqlalchemy.orm import Mapped, mapped_column +from typing import Optional, List +from sqlalchemy import String, Integer, Numeric, Date, ForeignKey, UniqueConstraint, PrimaryKeyConstraint +from sqlalchemy.orm import Mapped, mapped_column, relationship from core.database import Base from api.v1.common.base_models import TenantScopedMixin, TimestampMixin @@ -27,3 +27,124 @@ class FDACatalog(Base, TenantScopedMixin, TimestampMixin): storage_status: Mapped[Optional[str]] = mapped_column(String(100)) warehouse_code: Mapped[Optional[str]] = mapped_column(String(20)) call_atl: Mapped[Optional[str]] = mapped_column(String(20)) + + # Relationships + specifications: Mapped[Optional["FDASpecifications"]] = relationship("FDASpecifications", back_populates="fda_catalog", cascade="all, delete-orphan", uselist=False) + constituent_elements: Mapped[List["FDAConstituentElements"]] = relationship("FDAConstituentElements", back_populates="fda_catalog", cascade="all, delete-orphan") + affirmation_codes: Mapped[List["FDAAffirmationCodes"]] = relationship("FDAAffirmationCodes", back_populates="fda_catalog", cascade="all, delete-orphan") + lot_productions: Mapped[List["FDALotProduction"]] = relationship("FDALotProduction", back_populates="fda_catalog", cascade="all, delete-orphan") + + +class FDASpecifications(Base, TenantScopedMixin, TimestampMixin): + """FDA01 - Especificaciones del Producto FDA""" + __tablename__ = "fda_specifications" + __table_args__ = ( + PrimaryKeyConstraint("id", name="fda_specifications_pkey"), + {'schema': 'a76'} + ) + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + fda_catalog_id: Mapped[int] = mapped_column(ForeignKey("a76.fda_catalog.id", ondelete="CASCADE"), nullable=False, index=True) + + prod_code: Mapped[Optional[str]] = mapped_column(String(50)) + commodity_desc: Mapped[Optional[str]] = mapped_column(String(200)) + brand_name: Mapped[Optional[str]] = mapped_column(String(100)) + disclaimer: Mapped[Optional[str]] = mapped_column(String(100)) + pgm_code: Mapped[Optional[str]] = mapped_column(String(50)) + proc_code: Mapped[Optional[str]] = mapped_column(String(50)) + intnd_use_code: Mapped[Optional[str]] = mapped_column(String(50)) + intnd_use_desc: Mapped[Optional[str]] = mapped_column(String(200)) + temp_qual: Mapped[Optional[str]] = mapped_column(String(50)) + temp_type: Mapped[Optional[str]] = mapped_column(String(50)) + temp_degrees: Mapped[Optional[float]] = mapped_column(Numeric(10, 2)) + temp_negative: Mapped[Optional[float]] = mapped_column(Numeric(10, 2)) + temp_location: Mapped[Optional[str]] = mapped_column(String(100)) + + quantity_1: Mapped[Optional[float]] = mapped_column(Numeric(15, 2)) + qty_uom_1: Mapped[Optional[str]] = mapped_column(String(20)) + quantity_2: Mapped[Optional[float]] = mapped_column(Numeric(15, 2)) + qty_uom_2: Mapped[Optional[str]] = mapped_column(String(20)) + quantity_3: Mapped[Optional[float]] = mapped_column(Numeric(15, 2)) + qty_uom_3: Mapped[Optional[str]] = mapped_column(String(20)) + + ctry_prod: Mapped[Optional[str]] = mapped_column(String(50)) + ctry_source: Mapped[Optional[str]] = mapped_column(String(50)) + ctry_growth: Mapped[Optional[str]] = mapped_column(String(50)) + ctry_refusal: Mapped[Optional[str]] = mapped_column(String(50)) + ctry_shipping: Mapped[Optional[str]] = mapped_column(String(50)) + + manuf_key: Mapped[Optional[str]] = mapped_column(String(50)) + shipper_key: Mapped[Optional[str]] = mapped_column(String(50)) + ult_cons_key: Mapped[Optional[str]] = mapped_column(String(50)) + fda_imp_key: Mapped[Optional[str]] = mapped_column(String(50)) + pn_subm_key: Mapped[Optional[str]] = mapped_column(String(50)) + consol_key: Mapped[Optional[str]] = mapped_column(String(50)) + producer_key: Mapped[Optional[str]] = mapped_column(String(50)) + owner_key: Mapped[Optional[str]] = mapped_column(String(50)) + deli_party_key: Mapped[Optional[str]] = mapped_column(String(50)) + grower_key: Mapped[Optional[str]] = mapped_column(String(50)) + dev_ini_imp_key: Mapped[Optional[str]] = mapped_column(String(50)) + + lacf_cont_1: Mapped[Optional[str]] = mapped_column(String(100)) + lacf_cont_2: Mapped[Optional[str]] = mapped_column(String(100)) + lacf_cont_3: Mapped[Optional[str]] = mapped_column(String(100)) + pn_transmitter_key: Mapped[Optional[str]] = mapped_column(String(50)) + + fda_catalog: Mapped["FDACatalog"] = relationship("FDACatalog", back_populates="specifications") + + +class FDAConstituentElements(Base, TenantScopedMixin, TimestampMixin): + """FDA04 - Constituent Elements""" + __tablename__ = "fda_constituent_elements" + __table_args__ = ( + PrimaryKeyConstraint("id", name="fda_constituent_elements_pkey"), + {'schema': 'a76'} + ) + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + fda_catalog_id: Mapped[int] = mapped_column(ForeignKey("a76.fda_catalog.id", ondelete="CASCADE"), nullable=False, index=True) + + line: Mapped[int] = mapped_column(Integer, nullable=False, default=1) + ele_name: Mapped[str] = mapped_column(String(200), nullable=False) + ele_qty: Mapped[Optional[float]] = mapped_column(Numeric(15, 2)) + ele_qty_uom: Mapped[Optional[str]] = mapped_column(String(20)) + ele_pctg: Mapped[Optional[float]] = mapped_column(Numeric(15, 2)) + + fda_catalog: Mapped["FDACatalog"] = relationship("FDACatalog", back_populates="constituent_elements") + + +class FDAAffirmationCodes(Base, TenantScopedMixin, TimestampMixin): + """FDA23 - Affirmation of Compliance""" + __tablename__ = "fda_affirmation_codes" + __table_args__ = ( + PrimaryKeyConstraint("id", name="fda_affirmation_codes_pkey"), + {'schema': 'a76'} + ) + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + fda_catalog_id: Mapped[int] = mapped_column(ForeignKey("a76.fda_catalog.id", ondelete="CASCADE"), nullable=False, index=True) + + line: Mapped[int] = mapped_column(Integer, nullable=False, default=1) + aoc_code: Mapped[str] = mapped_column(String(50), nullable=False) + aoc_qual: Mapped[Optional[str]] = mapped_column(String(100)) + + fda_catalog: Mapped["FDACatalog"] = relationship("FDACatalog", back_populates="affirmation_codes") + + +class FDALotProduction(Base, TenantScopedMixin, TimestampMixin): + """FDA25 - Lot and Production Dates""" + __tablename__ = "fda_lot_production" + __table_args__ = ( + PrimaryKeyConstraint("id", name="fda_lot_production_pkey"), + {'schema': 'a76'} + ) + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + fda_catalog_id: Mapped[int] = mapped_column(ForeignKey("a76.fda_catalog.id", ondelete="CASCADE"), nullable=False, index=True) + + line: Mapped[int] = mapped_column(Integer, nullable=False, default=1) + lot_number: Mapped[str] = mapped_column(String(100), nullable=False) + production_start_date: Mapped[Optional[str]] = mapped_column(String(50)) + production_end_date: Mapped[Optional[str]] = mapped_column(String(50)) + + fda_catalog: Mapped["FDACatalog"] = relationship("FDACatalog", back_populates="lot_productions") diff --git a/backend/api/v1/modules/a76/general_catalogs/fda_catalog/routes.py b/backend/api/v1/modules/a76/general_catalogs/fda_catalog/routes.py index c55fd860..f8d57080 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fda_catalog/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/fda_catalog/routes.py @@ -67,3 +67,204 @@ async def get_fda_catalog( "warehouse_code": entry.warehouse_code, "call_atl": entry.call_atl } + +from api.v1.modules.a76.general_catalogs.fda_catalog.dto import FDACatalogCreate, FDACatalogUpdate +from sqlalchemy.exc import IntegrityError + +@router.post("/", response_model=FDACatalogResponse, status_code=status.HTTP_201_CREATED) +async def create_fda_catalog( + data: FDACatalogCreate, + company_id: int = Query(..., description="Company ID"), + db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user) +): + """Crear nueva entrada en el catálogo FDA""" + tenant_id = validate_access_to_resource(db, company_id, current_user) + try: + return FDACatalogService.create(db, tenant_id, company_id, data) + except IntegrityError: + db.rollback() + raise HTTPException(status_code=400, detail="La Clave FDA ya existe o hay un conflicto de datos.") + + +@router.put("/{id}", response_model=FDACatalogResponse) +async def update_fda_catalog( + id: int, + data: FDACatalogUpdate, + company_id: int = Query(..., description="Company ID"), + db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user) +): + """Actualizar entrada del catálogo FDA""" + tenant_id = validate_access_to_resource(db, company_id, current_user) + + try: + entry = FDACatalogService.update(db, tenant_id, company_id, id, data) + if not entry: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Entrada no encontrada") + return entry + except IntegrityError: + db.rollback() + raise HTTPException(status_code=400, detail="Error de integridad de datos al actualizar la Clave FDA.") + + +@router.delete("/{id}") +async def delete_fda_catalog( + id: int, + company_id: int = Query(..., description="Company ID"), + db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user) +): + """Eliminar entrada del catálogo FDA""" + tenant_id = validate_access_to_resource(db, company_id, current_user) + success = FDACatalogService.delete(db, tenant_id, company_id, id) + if not success: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Entrada no encontrada") + return {"message": "Eliminado correctamente"} + +from typing import List +from api.v1.modules.a76.general_catalogs.fda_catalog.service import FDADetailsService +from api.v1.modules.a76.general_catalogs.fda_catalog.dto import ( + FDASpecificationsCreate, FDASpecificationsResponse, + FDAConstituentElementsCreate, FDAConstituentElementsUpdate, FDAConstituentElementsResponse, + FDAAffirmationCodesCreate, FDAAffirmationCodesUpdate, FDAAffirmationCodesResponse, + FDALotProductionCreate, FDALotProductionUpdate, FDALotProductionResponse +) + +# === FDA01: Especificaciones del Producto === +@router.get("/{id}/specifications", response_model=Optional[FDASpecificationsResponse]) +async def get_fda_specifications( + id: int, + 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) + catalog_entry = FDACatalogService.get_by_id(db, tenant_id, company_id, id) + if not catalog_entry: + raise HTTPException(status_code=404, detail="Catálogo FDA no encontrado") + + return FDADetailsService.get_specifications(db, tenant_id, company_id, id) + +@router.put("/{id}/specifications", response_model=FDASpecificationsResponse) +async def save_fda_specifications( + id: int, + data: FDASpecificationsCreate, + 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) + catalog_entry = FDACatalogService.get_by_id(db, tenant_id, company_id, id) + if not catalog_entry: + raise HTTPException(status_code=404, detail="Catálogo FDA no encontrado") + + return FDADetailsService.save_specifications(db, tenant_id, company_id, id, data.model_dump(exclude_unset=True)) + +# === FDA04: Elementos Constitutivos === +@router.get("/{id}/constituent-elements", response_model=List[FDAConstituentElementsResponse]) +async def list_fda_constituent_elements( + id: int, + 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) + return FDADetailsService.get_constituent_elements(db, tenant_id, company_id, id) + +@router.post("/{id}/constituent-elements", response_model=FDAConstituentElementsResponse) +async def create_fda_constituent_element( + id: int, + data: FDAConstituentElementsCreate, + 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) + return FDADetailsService.create_constituent_element(db, tenant_id, company_id, id, data.model_dump()) + +@router.delete("/{id}/constituent-elements/{element_id}") +async def delete_fda_constituent_element( + id: int, + element_id: int, + 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) + success = FDADetailsService.delete_constituent_element(db, tenant_id, company_id, element_id) + if not success: + raise HTTPException(status_code=404, detail="Elemento no encontrado") + return {"message": "Eliminado correctamente"} + +# === FDA23: Códigos de Afirmación === +@router.get("/{id}/affirmation-codes", response_model=List[FDAAffirmationCodesResponse]) +async def list_fda_affirmation_codes( + id: int, + 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) + return FDADetailsService.get_affirmation_codes(db, tenant_id, company_id, id) + +@router.post("/{id}/affirmation-codes", response_model=FDAAffirmationCodesResponse) +async def create_fda_affirmation_code( + id: int, + data: FDAAffirmationCodesCreate, + 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) + return FDADetailsService.create_affirmation_code(db, tenant_id, company_id, id, data.model_dump()) + +@router.delete("/{id}/affirmation-codes/{code_id}") +async def delete_fda_affirmation_code( + id: int, + code_id: int, + 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) + success = FDADetailsService.delete_affirmation_code(db, tenant_id, company_id, code_id) + if not success: + raise HTTPException(status_code=404, detail="Código no encontrado") + return {"message": "Eliminado correctamente"} + +# === FDA25: Lotes de Producción === +@router.get("/{id}/lot-productions", response_model=List[FDALotProductionResponse]) +async def list_fda_lot_productions( + id: int, + 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) + return FDADetailsService.get_lot_productions(db, tenant_id, company_id, id) + +@router.post("/{id}/lot-productions", response_model=FDALotProductionResponse) +async def create_fda_lot_production( + id: int, + data: FDALotProductionCreate, + 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) + return FDADetailsService.create_lot_production(db, tenant_id, company_id, id, data.model_dump()) + +@router.delete("/{id}/lot-productions/{lot_id}") +async def delete_fda_lot_production( + id: int, + lot_id: int, + 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) + success = FDADetailsService.delete_lot_production(db, tenant_id, company_id, lot_id) + if not success: + raise HTTPException(status_code=404, detail="Lote no encontrado") + return {"message": "Eliminado correctamente"} diff --git a/backend/api/v1/modules/a76/general_catalogs/fda_catalog/service.py b/backend/api/v1/modules/a76/general_catalogs/fda_catalog/service.py index 31ce3584..15921208 100644 --- a/backend/api/v1/modules/a76/general_catalogs/fda_catalog/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/fda_catalog/service.py @@ -106,3 +106,162 @@ class FDACatalogService: db.delete(entry) db.commit() return True + + +class FDADetailsService: + """Servicio CRUD para los detalles adicionales de FDA (Especificaciones, Lotes, etc)""" + + # FDA01 - Especificaciones + @staticmethod + def get_specifications(db: Session, tenant_id: int, company_id: int, fda_catalog_id: int): + from api.v1.modules.a76.general_catalogs.fda_catalog.models import FDASpecifications + return db.execute( + select(FDASpecifications).where( + (FDASpecifications.fda_catalog_id == fda_catalog_id) & + (FDASpecifications.tenant_id == tenant_id) & + (FDASpecifications.company_id == company_id) + ) + ).scalar_one_or_none() + + @staticmethod + def save_specifications(db: Session, tenant_id: int, company_id: int, fda_catalog_id: int, data: dict): + from api.v1.modules.a76.general_catalogs.fda_catalog.models import FDASpecifications + existing = FDADetailsService.get_specifications(db, tenant_id, company_id, fda_catalog_id) + if existing: + for field, value in data.items(): + setattr(existing, field, value) + entry = existing + else: + entry = FDASpecifications( + tenant_id=tenant_id, + company_id=company_id, + fda_catalog_id=fda_catalog_id, + **data + ) + db.add(entry) + db.commit() + db.refresh(entry) + return entry + + # FDA04 - Elementos Constitutivos + @staticmethod + def get_constituent_elements(db: Session, tenant_id: int, company_id: int, fda_catalog_id: int): + from api.v1.modules.a76.general_catalogs.fda_catalog.models import FDAConstituentElements + items = db.execute( + select(FDAConstituentElements).where( + (FDAConstituentElements.fda_catalog_id == fda_catalog_id) & + (FDAConstituentElements.tenant_id == tenant_id) & + (FDAConstituentElements.company_id == company_id) + ).order_by(FDAConstituentElements.line.asc()) + ).scalars().all() + return items + + @staticmethod + def create_constituent_element(db: Session, tenant_id: int, company_id: int, fda_catalog_id: int, data: dict): + from api.v1.modules.a76.general_catalogs.fda_catalog.models import FDAConstituentElements + entry = FDAConstituentElements( + tenant_id=tenant_id, + company_id=company_id, + fda_catalog_id=fda_catalog_id, + **data + ) + db.add(entry) + db.commit() + db.refresh(entry) + return entry + + @staticmethod + def delete_constituent_element(db: Session, tenant_id: int, company_id: int, element_id: int): + from api.v1.modules.a76.general_catalogs.fda_catalog.models import FDAConstituentElements + entry = db.execute(select(FDAConstituentElements).where( + (FDAConstituentElements.id == element_id) & + (FDAConstituentElements.tenant_id == tenant_id) & + (FDAConstituentElements.company_id == company_id) + )).scalar_one_or_none() + if entry: + db.delete(entry) + db.commit() + return True + return False + + # FDA23 - Códigos de Afirmación + @staticmethod + def get_affirmation_codes(db: Session, tenant_id: int, company_id: int, fda_catalog_id: int): + from api.v1.modules.a76.general_catalogs.fda_catalog.models import FDAAffirmationCodes + items = db.execute( + select(FDAAffirmationCodes).where( + (FDAAffirmationCodes.fda_catalog_id == fda_catalog_id) & + (FDAAffirmationCodes.tenant_id == tenant_id) & + (FDAAffirmationCodes.company_id == company_id) + ).order_by(FDAAffirmationCodes.line.asc()) + ).scalars().all() + return items + + @staticmethod + def create_affirmation_code(db: Session, tenant_id: int, company_id: int, fda_catalog_id: int, data: dict): + from api.v1.modules.a76.general_catalogs.fda_catalog.models import FDAAffirmationCodes + entry = FDAAffirmationCodes( + tenant_id=tenant_id, + company_id=company_id, + fda_catalog_id=fda_catalog_id, + **data + ) + db.add(entry) + db.commit() + db.refresh(entry) + return entry + + @staticmethod + def delete_affirmation_code(db: Session, tenant_id: int, company_id: int, code_id: int): + from api.v1.modules.a76.general_catalogs.fda_catalog.models import FDAAffirmationCodes + entry = db.execute(select(FDAAffirmationCodes).where( + (FDAAffirmationCodes.id == code_id) & + (FDAAffirmationCodes.tenant_id == tenant_id) & + (FDAAffirmationCodes.company_id == company_id) + )).scalar_one_or_none() + if entry: + db.delete(entry) + db.commit() + return True + return False + + # FDA25 - Lotes de Producción + @staticmethod + def get_lot_productions(db: Session, tenant_id: int, company_id: int, fda_catalog_id: int): + from api.v1.modules.a76.general_catalogs.fda_catalog.models import FDALotProduction + items = db.execute( + select(FDALotProduction).where( + (FDALotProduction.fda_catalog_id == fda_catalog_id) & + (FDALotProduction.tenant_id == tenant_id) & + (FDALotProduction.company_id == company_id) + ).order_by(FDALotProduction.line.asc()) + ).scalars().all() + return items + + @staticmethod + def create_lot_production(db: Session, tenant_id: int, company_id: int, fda_catalog_id: int, data: dict): + from api.v1.modules.a76.general_catalogs.fda_catalog.models import FDALotProduction + entry = FDALotProduction( + tenant_id=tenant_id, + company_id=company_id, + fda_catalog_id=fda_catalog_id, + **data + ) + db.add(entry) + db.commit() + db.refresh(entry) + return entry + + @staticmethod + def delete_lot_production(db: Session, tenant_id: int, company_id: int, lot_id: int): + from api.v1.modules.a76.general_catalogs.fda_catalog.models import FDALotProduction + entry = db.execute(select(FDALotProduction).where( + (FDALotProduction.id == lot_id) & + (FDALotProduction.tenant_id == tenant_id) & + (FDALotProduction.company_id == company_id) + )).scalar_one_or_none() + if entry: + db.delete(entry) + db.commit() + return True + return False diff --git a/backend/core/error_handlers.py b/backend/core/error_handlers.py index d038f4ab..d3ffc998 100644 --- a/backend/core/error_handlers.py +++ b/backend/core/error_handlers.py @@ -135,7 +135,7 @@ async def sqlalchemy_error_handler( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content={ "error": "DATABASE_ERROR", - "message": "Error en la operación de base de datos", + "message": f"Error en la operación de base de datos: {str(exc)}", "status_code": status.HTTP_500_INTERNAL_SERVER_ERROR, }, ) diff --git a/frontend/messages/en.json b/frontend/messages/en.json index fc56fcdf..51be78a8 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -76,7 +76,8 @@ "goods": { "title": "Goods", "classes": "Classes", - "parts": "Parts" + "parts": "Parts", + "fda_codes": "FDA Codes" }, "pedimentos": { "title": "Pedimentos", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 413726cd..3a253424 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -76,7 +76,8 @@ "goods": { "title": "Mercancías", "classes": "Clases", - "parts": "Partes" + "parts": "Partes", + "fda_codes": "Códigos F.D.A." }, "pedimentos": { "title": "Pedimentos", diff --git a/frontend/src/lib/components/sidebar/modules.ts b/frontend/src/lib/components/sidebar/modules.ts index 5ab973b2..b386422f 100644 --- a/frontend/src/lib/components/sidebar/modules.ts +++ b/frontend/src/lib/components/sidebar/modules.ts @@ -17,7 +17,7 @@ import { Ship, Truck, Users, -} from 'lucide-svelte'; +} from '@lucide/svelte'; import * as m from "$lib/paraglide/messages.js"; import { Title } from '../ui/alert'; @@ -366,6 +366,10 @@ export function getSidebarData(): SidebarData { { title: m["sidebar.goods.parts"](), url: "/dashboard/goods/parts", + }, + { + title: m["sidebar.goods.fda_codes"](), + url: "/dashboard/goods/fda-codes", } ], }, diff --git a/frontend/src/routes/api-sveltekit/fda-catalog/+server.ts b/frontend/src/routes/api-sveltekit/fda-catalog/+server.ts new file mode 100644 index 00000000..5b3df145 --- /dev/null +++ b/frontend/src/routes/api-sveltekit/fda-catalog/+server.ts @@ -0,0 +1,283 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { getAuthTokens, authenticatedFetch } from '$lib/server/api'; +import * as fs from 'fs'; + +// Helper to log to a file for debugging +const debugLog = (msg: string) => { + try { + const timestamp = new Date().toISOString(); + fs.appendFileSync('/tmp/fda_proxy_debug.log', `[${timestamp}] ${msg}\n`); + } catch (e) { } +}; + +// Precise cleaners to ensure only business data is sent to backend +const cleanFDA01 = (d: any) => { + if (!d) return null; + return { + prod_code: d.prod_code || null, + commodity_desc: d.commodity_desc || null, + brand_name: d.brand_name || null, + disclaimer: d.disclaimer || null, + pgm_code: d.pgm_code || null, + proc_code: d.proc_code || null, + intnd_use_code: d.intnd_use_code || null, + intnd_use_desc: d.intnd_use_desc || null, + temp_qual: d.temp_qual || null, + temp_type: d.temp_type || null, + temp_degrees: d.temp_degrees || null, + temp_negative: d.temp_negative || null, + temp_location: d.temp_location || null, + quantity_1: d.quantity_1 || null, + qty_uom_1: d.qty_uom_1 || null, + quantity_2: d.quantity_2 || null, + qty_uom_2: d.qty_uom_2 || null, + quantity_3: d.quantity_3 || null, + qty_uom_3: d.qty_uom_3 || null, + ctry_prod: d.ctry_prod || null, + ctry_source: d.ctry_source || null, + ctry_growth: d.ctry_growth || null, + ctry_refusal: d.ctry_refusal || null, + ctry_shipping: d.ctry_shipping || null, + manuf_key: d.manuf_key || null, + shipper_key: d.shipper_key || null, + ult_cons_key: d.ult_cons_key || null, + fda_imp_key: d.fda_imp_key || null, + pn_subm_key: d.pn_subm_key || null, + consol_key: d.consol_key || null, + producer_key: d.producer_key || null, + owner_key: d.owner_key || null, + deli_party_key: d.deli_party_key || null, + grower_key: d.grower_key || null, + dev_ini_imp_key: d.dev_ini_imp_key || null, + lacf_cont_1: d.lacf_cont_1 || null, + lacf_cont_2: d.lacf_cont_2 || null, + lacf_cont_3: d.lacf_cont_3 || null, + pn_transmitter_key: d.pn_transmitter_key || null + }; +}; + +const cleanFDA23 = (d: any) => ({ + line: typeof d.line === 'number' ? d.line : (parseInt(d.line) || 1), + aoc_code: d.aoc_code || '', + aoc_qual: d.aoc_qual || null +}); + +const cleanFDA04 = (d: any) => ({ + line: typeof d.line === 'number' ? d.line : (parseInt(d.line) || 1), + ele_name: d.ele_name || '', + ele_qty: (d.ele_qty === '' || d.ele_qty === null || d.ele_qty === undefined) ? null : parseFloat(d.ele_qty), + ele_qty_uom: d.ele_qty_uom || null, + ele_pctg: (d.ele_pctg === '' || d.ele_pctg === null || d.ele_pctg === undefined) ? null : parseFloat(d.ele_pctg) +}); + +const cleanFDA25 = (d: any) => ({ + line: typeof d.line === 'number' ? d.line : (parseInt(d.line) || 1), + lot_number: d.lot_number || '', + production_start_date: d.production_start_date || null, + production_end_date: d.production_end_date || null +}); + +async function saveNestedData(id: string, companyId: string, nested: any, cookies: any, fetch: any) { + debugLog(`saveNestedData started for id: ${id}`); + const { fda01, fda23_codes, fda04_elements, fda25_lots } = nested; + + // 1. FDA01 Specifications + if (fda01) { + debugLog(`Saving FDA01`); + const res = await authenticatedFetch( + `v1/a76/fda-catalog/${id}/specifications?company_id=${companyId}`, + { method: 'PUT', body: JSON.stringify(cleanFDA01(fda01)) }, + cookies, fetch + ); + if (!res.ok) { + const txt = await res.text(); + debugLog(`Error FDA01: ${res.status} - ${txt}`); + throw new Error(`Error en FDA01: ${res.status} - ${txt}`); + } + } + + // 2. FDA23 Affirmation Codes + debugLog(`Processing FDA23`); + const resExist23 = await authenticatedFetch(`v1/a76/fda-catalog/${id}/affirmation-codes?company_id=${companyId}`, { method: 'GET' }, cookies, fetch); + if (resExist23.ok) { + const items = await resExist23.json(); + debugLog(`Deleting ${items.length} existing FDA23 items`); + for (const item of items) { + await authenticatedFetch(`v1/a76/fda-catalog/${id}/affirmation-codes/${item.id}?company_id=${companyId}`, { method: 'DELETE' }, cookies, fetch); + } + } + if (fda23_codes && Array.isArray(fda23_codes)) { + for (const code of fda23_codes) { + if (!code.aoc_code) continue; + const res = await authenticatedFetch(`v1/a76/fda-catalog/${id}/affirmation-codes?company_id=${companyId}`, { method: 'POST', body: JSON.stringify(cleanFDA23(code)) }, cookies, fetch); + if (!res.ok) { + const txt = await res.text(); + debugLog(`Error FDA23: ${res.status} - ${txt}`); + throw new Error(`Error en FDA23: ${res.status} - ${txt}`); + } + } + } + + // 3. FDA04 Constituent Elements + debugLog(`Processing FDA04`); + const resExist04 = await authenticatedFetch(`v1/a76/fda-catalog/${id}/constituent-elements?company_id=${companyId}`, { method: 'GET' }, cookies, fetch); + if (resExist04.ok) { + const items = await resExist04.json(); + debugLog(`Deleting ${items.length} existing FDA04 items`); + for (const item of items) { + await authenticatedFetch(`v1/a76/fda-catalog/${id}/constituent-elements/${item.id}?company_id=${companyId}`, { method: 'DELETE' }, cookies, fetch); + } + } + if (fda04_elements && Array.isArray(fda04_elements)) { + for (const ele of fda04_elements) { + if (!ele.ele_name) continue; + debugLog(`Saving FDA04 item: ${ele.ele_name}`); + const res = await authenticatedFetch(`v1/a76/fda-catalog/${id}/constituent-elements?company_id=${companyId}`, { method: 'POST', body: JSON.stringify(cleanFDA04(ele)) }, cookies, fetch); + if (!res.ok) { + const txt = await res.text(); + debugLog(`Error FDA04: ${res.status} - ${txt}`); + throw new Error(`Error en FDA04: ${res.status} - ${txt}`); + } + } + } + + // 4. FDA25 Lot Productions + debugLog(`Processing FDA25`); + const resExist25 = await authenticatedFetch(`v1/a76/fda-catalog/${id}/lot-productions?company_id=${companyId}`, { method: 'GET' }, cookies, fetch); + if (resExist25.ok) { + const items = await resExist25.json(); + debugLog(`Deleting ${items.length} existing FDA25 items`); + for (const item of items) { + await authenticatedFetch(`v1/a76/fda-catalog/${id}/lot-productions/${item.id}?company_id=${companyId}`, { method: 'DELETE' }, cookies, fetch); + } + } + if (fda25_lots && Array.isArray(fda25_lots)) { + for (const lot of fda25_lots) { + if (!lot.lot_number) continue; + const res = await authenticatedFetch(`v1/a76/fda-catalog/${id}/lot-productions?company_id=${companyId}`, { method: 'POST', body: JSON.stringify(cleanFDA25(lot)) }, cookies, fetch); + if (!res.ok) { + const txt = await res.text(); + debugLog(`Error FDA25: ${res.status} - ${txt}`); + throw new Error(`Error en FDA25: ${res.status} - ${txt}`); + } + } + } + debugLog(`saveNestedData finished successfully`); +} + +export const GET: RequestHandler = async ({ url, cookies, fetch }) => { + const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id'); + const skip = url.searchParams.get('skip') || '0'; + const limit = url.searchParams.get('limit') || '50'; + const search = url.searchParams.get('search') || ''; + + if (!companyId) return json({ error: 'Compañía no seleccionada' }, { status: 400 }); + + try { + let apiUrl = `v1/a76/fda-catalog/?company_id=${companyId}&skip=${skip}&limit=${limit}`; + if (search) apiUrl += `&search=${encodeURIComponent(search)}`; + + const response = await authenticatedFetch(apiUrl, { method: 'GET' }, cookies, fetch); + if (!response.ok) return json({ error: 'Error al cargar registros FDA' }, { status: response.status }); + + const data = await response.json(); + return json(data); + } catch (error) { + return json({ error: 'Error de conexión' }, { status: 500 }); + } +}; + +export const POST: RequestHandler = async ({ request, cookies, fetch }) => { + debugLog(`POST started`); + const companyId = cookies.get('active_company_id'); + if (!companyId) return json({ error: 'Compañía no seleccionada' }, { status: 400 }); + + try { + const body = await request.json(); + const mainRecord = { + fda_key: body.fda_key, + description: body.description, + fda_code: body.fda_code || null, + requirements: body.requirements || null, + manufacturer_number: body.manufacturer_number || null, + country_of_production: body.country_of_production || null, + storage_status: body.storage_status || null, + warehouse_code: body.warehouse_code || null, + call_atl: body.call_atl || null + }; + + debugLog(`Creating main record`); + const res = await authenticatedFetch(`v1/a76/fda-catalog/?company_id=${companyId}`, { method: 'POST', body: JSON.stringify(mainRecord) }, cookies, fetch); + const data = await res.json(); + if (!res.ok) { + debugLog(`Error creating main record: ${res.status}`); + return json({ error: data.detail || 'Error al crear FDA' }, { status: res.status }); + } + + await saveNestedData(data.id.toString(), companyId, body, cookies, fetch); + debugLog(`POST finished successfully`); + return json(data); + } catch (error: any) { + debugLog(`POST Crash: ${error.message}`); + console.error('POST Error:', error); + return json({ error: error.message || 'Error al procesar el guardado' }, { status: 500 }); + } +}; + +export const PUT: RequestHandler = async ({ request, url, cookies, fetch }) => { + debugLog(`PUT started`); + const id = url.searchParams.get('id'); + const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id'); + + debugLog(`PUT params: id=${id}, companyId=${companyId}`); + if (!id || !companyId) return json({ error: 'ID o compañía no proporcionada' }, { status: 400 }); + + try { + const body = await request.json(); + const mainRecord = { + fda_key: body.fda_key, + description: body.description, + fda_code: body.fda_code || null, + requirements: body.requirements || null, + manufacturer_number: body.manufacturer_number || null, + country_of_production: body.country_of_production || null, + storage_status: body.storage_status || null, + warehouse_code: body.warehouse_code || null, + call_atl: body.call_atl || null + }; + + debugLog(`Updating main record id=${id}`); + const res = await authenticatedFetch(`v1/a76/fda-catalog/${id}?company_id=${companyId}`, { method: 'PUT', body: JSON.stringify(mainRecord) }, cookies, fetch); + const data = await res.json(); + if (!res.ok) { + debugLog(`Error updating main record: ${res.status}`); + return json({ error: data.detail || 'Error al actualizar FDA' }, { status: res.status }); + } + + await saveNestedData(id, companyId, body, cookies, fetch); + debugLog(`PUT finished successfully`); + return json(data); + } catch (error: any) { + debugLog(`PUT Crash: ${error.message}`); + console.error('PUT Error:', error); + return json({ error: error.message || 'Error al procesar la actualización' }, { status: 500 }); + } +}; + +export const DELETE: RequestHandler = async ({ url, cookies, fetch }) => { + const id = url.searchParams.get('id'); + const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id'); + if (!id || !companyId) return json({ error: 'ID o compañía no proporcionada' }, { status: 400 }); + + try { + const res = await authenticatedFetch(`v1/a76/fda-catalog/${id}?company_id=${companyId}`, { method: 'DELETE' }, cookies, fetch); + if (!res.ok) { + const data = await res.json(); + return json({ error: data.detail || 'Error al eliminar FDA' }, { status: res.status }); + } + return json({ success: true }); + } catch (error) { + return json({ error: 'Error de conexión' }, { status: 500 }); + } +}; diff --git a/frontend/src/routes/api-sveltekit/fda-catalog/[id]/+server.ts b/frontend/src/routes/api-sveltekit/fda-catalog/[id]/+server.ts new file mode 100644 index 00000000..1c904105 --- /dev/null +++ b/frontend/src/routes/api-sveltekit/fda-catalog/[id]/+server.ts @@ -0,0 +1,31 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { getAuthTokens, authenticatedFetch } from '$lib/server/api'; + +export const GET: RequestHandler = async ({ params, url, cookies, fetch }) => { + const { accessToken } = getAuthTokens(cookies); + if (!accessToken) return json({ error: 'No autorizado' }, { status: 401 }); + + const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id'); + const id = params.id; + + if (!companyId) return json({ error: 'Compañía no seleccionada' }, { status: 400 }); + + try { + const response = await authenticatedFetch( + `v1/a76/fda-catalog/${id}?company_id=${companyId}`, + { method: 'GET', cache: 'no-store' }, + cookies, fetch + ); + + if (!response.ok) { + return json({ error: 'Error al cargar registro FDA' }, { status: response.status }); + } + + const data = await response.json(); + return json(data); + } catch (error) { + console.error('Error in fda-catalog detail API:', error); + return json({ error: 'Error de conexión' }, { status: 500 }); + } +}; diff --git a/frontend/src/routes/api-sveltekit/fda-catalog/[id]/affirmation-codes/+server.ts b/frontend/src/routes/api-sveltekit/fda-catalog/[id]/affirmation-codes/+server.ts new file mode 100644 index 00000000..21e9777b --- /dev/null +++ b/frontend/src/routes/api-sveltekit/fda-catalog/[id]/affirmation-codes/+server.ts @@ -0,0 +1,31 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { getAuthTokens, authenticatedFetch } from '$lib/server/api'; + +export const GET: RequestHandler = async ({ params, url, cookies, fetch }) => { + const { accessToken } = getAuthTokens(cookies); + if (!accessToken) return json({ error: 'No autorizado' }, { status: 401 }); + + const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id'); + const id = params.id; + + if (!companyId) return json({ error: 'Compañía no seleccionada' }, { status: 400 }); + + try { + const response = await authenticatedFetch( + `v1/a76/fda-catalog/${id}/affirmation-codes?company_id=${companyId}`, + { method: 'GET', cache: 'no-store' }, + cookies, fetch + ); + + if (!response.ok) { + return json({ error: 'Error al cargar affirmation codes' }, { status: response.status }); + } + + const data = await response.json(); + return json(data); + } catch (error) { + console.error('Error in affirmation-codes API:', error); + return json({ error: 'Error de conexión' }, { status: 500 }); + } +}; diff --git a/frontend/src/routes/api-sveltekit/fda-catalog/[id]/constituent-elements/+server.ts b/frontend/src/routes/api-sveltekit/fda-catalog/[id]/constituent-elements/+server.ts new file mode 100644 index 00000000..b2944e68 --- /dev/null +++ b/frontend/src/routes/api-sveltekit/fda-catalog/[id]/constituent-elements/+server.ts @@ -0,0 +1,31 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { getAuthTokens, authenticatedFetch } from '$lib/server/api'; + +export const GET: RequestHandler = async ({ params, url, cookies, fetch }) => { + const { accessToken } = getAuthTokens(cookies); + if (!accessToken) return json({ error: 'No autorizado' }, { status: 401 }); + + const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id'); + const id = params.id; + + if (!companyId) return json({ error: 'Compañía no seleccionada' }, { status: 400 }); + + try { + const response = await authenticatedFetch( + `v1/a76/fda-catalog/${id}/constituent-elements?company_id=${companyId}`, + { method: 'GET', cache: 'no-store' }, + cookies, fetch + ); + + if (!response.ok) { + return json({ error: 'Error al cargar elementos constitutivos' }, { status: response.status }); + } + + const data = await response.json(); + return json(data); + } catch (error) { + console.error('Error in constituent-elements API:', error); + return json({ error: 'Error de conexión' }, { status: 500 }); + } +}; diff --git a/frontend/src/routes/api-sveltekit/fda-catalog/[id]/lot-productions/+server.ts b/frontend/src/routes/api-sveltekit/fda-catalog/[id]/lot-productions/+server.ts new file mode 100644 index 00000000..c9ed9e69 --- /dev/null +++ b/frontend/src/routes/api-sveltekit/fda-catalog/[id]/lot-productions/+server.ts @@ -0,0 +1,31 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { getAuthTokens, authenticatedFetch } from '$lib/server/api'; + +export const GET: RequestHandler = async ({ params, url, cookies, fetch }) => { + const { accessToken } = getAuthTokens(cookies); + if (!accessToken) return json({ error: 'No autorizado' }, { status: 401 }); + + const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id'); + const id = params.id; + + if (!companyId) return json({ error: 'Compañía no seleccionada' }, { status: 400 }); + + try { + const response = await authenticatedFetch( + `v1/a76/fda-catalog/${id}/lot-productions?company_id=${companyId}`, + { method: 'GET', cache: 'no-store' }, + cookies, fetch + ); + + if (!response.ok) { + return json({ error: 'Error al cargar lot productions' }, { status: response.status }); + } + + const data = await response.json(); + return json(data); + } catch (error) { + console.error('Error in lot-productions API:', error); + return json({ error: 'Error de conexión' }, { status: 500 }); + } +}; diff --git a/frontend/src/routes/api-sveltekit/fda-catalog/[id]/specifications/+server.ts b/frontend/src/routes/api-sveltekit/fda-catalog/[id]/specifications/+server.ts new file mode 100644 index 00000000..e80c4d6c --- /dev/null +++ b/frontend/src/routes/api-sveltekit/fda-catalog/[id]/specifications/+server.ts @@ -0,0 +1,31 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { getAuthTokens, authenticatedFetch } from '$lib/server/api'; + +export const GET: RequestHandler = async ({ params, url, cookies, fetch }) => { + const { accessToken } = getAuthTokens(cookies); + if (!accessToken) return json({ error: 'No autorizado' }, { status: 401 }); + + const companyId = url.searchParams.get('company_id') || cookies.get('active_company_id'); + const id = params.id; + + if (!companyId) return json({ error: 'Compañía no seleccionada' }, { status: 400 }); + + try { + const response = await authenticatedFetch( + `v1/a76/fda-catalog/${id}/specifications?company_id=${companyId}`, + { method: 'GET', cache: 'no-store' }, + cookies, fetch + ); + + if (!response.ok) { + return json({ error: 'Error al cargar especificaciones' }, { status: response.status }); + } + + const data = await response.json(); + return json(data); + } catch (error) { + console.error('Error in specifications API:', error); + return json({ error: 'Error de conexión' }, { status: 500 }); + } +}; diff --git a/frontend/src/routes/dashboard/goods/fda-codes/+page.svelte b/frontend/src/routes/dashboard/goods/fda-codes/+page.svelte new file mode 100644 index 00000000..831668c8 --- /dev/null +++ b/frontend/src/routes/dashboard/goods/fda-codes/+page.svelte @@ -0,0 +1,469 @@ + + +

+ +
+
+
+
+
+
+ +
+

Códigos F.D.A.

+
+

+ Gestión de códigos FDA para mercancías / FDA goods code management +

+
+
+ + +
+
+
+
+ + +
+ + +
+
+
+
+ +
+
+

Registros FDA

+

+ {filteredFdaList.length} de {fdaList.length} registros +

+
+
+ +
+ + +
+
+ + + {#if selectedRows.length > 0} +
+
+ + + {selectedRows.length} + {selectedRows.length === 1 ? 'registro seleccionado' : 'registros seleccionados'} + +
+ +
+ {/if} +
+ + +
+ + + + + 0 && + selectedRows.length === filteredFdaList.length} + indeterminate={selectedRows.length > 0 && + selectedRows.length < filteredFdaList.length} + onCheckedChange={toggleAllSelection} + aria-label="Seleccionar todos" + /> + + Clave FDA + Descripción + Código FDA + Fabricante + País + Estado + + + + {#if loading} + + +
+
+ +
+
+

Cargando registros...

+

Por favor espere

+
+
+
+
+ {:else if filteredFdaList.length === 0} + + + {#if searchQuery} +
+
+ +
+
+

No se encontraron resultados

+

Intenta con otra búsqueda

+
+ +
+ {:else} +
+
+ +
+
+

No hay registros FDA

+

+ No se encontraron registros para esta compañía +

+
+
+ {/if} +
+
+ {:else} + {#each filteredFdaList as fda, i (fda.id)} + (hoveredRow = fda.id)} + onmouseleave={() => (hoveredRow = null)} + onclick={() => toggleRowSelection(fda.id)} + > + e.stopPropagation()}> + toggleRowSelection(fda.id)} + aria-label="Seleccionar fila" + /> + + +
+ + {fda.fda_key} + +
+
+ +
+

+ {fda.description || '-'} +

+ {#if fda.requirements} +

+ {fda.requirements} +

+ {/if} +
+
+ + + {fda.fda_code || '-'} + + + +
+ + {fda.manufacturer_number || '-'} +
+
+ +
+ + {fda.country_of_production || '-'} +
+
+ + {#if fda.storage_status} + + {fda.storage_status} + + {:else} + - + {/if} + +
+ {/each} + {/if} +
+
+
+ + + {#if filteredFdaList.length > 0} +
+ + Mostrando {filteredFdaList.length} registros + + {#if searchQuery} + + Filtro: "{searchQuery}" + + + {/if} +
+ {/if} +
+
+ + +
+
+
+ +
+ {#if selectedRows.length > 0} +
+ + + {selectedRows.length} seleccionado(s) + +
+ {:else} +
+ + Selecciona registros para editar o eliminar +
+ {/if} +
+ + +
+ + + +
+
+
+
+
diff --git a/frontend/src/routes/dashboard/goods/fda-codes/edit/[id]/+page.svelte b/frontend/src/routes/dashboard/goods/fda-codes/edit/[id]/+page.svelte new file mode 100644 index 00000000..280b4f67 --- /dev/null +++ b/frontend/src/routes/dashboard/goods/fda-codes/edit/[id]/+page.svelte @@ -0,0 +1,1071 @@ + + + +
+
+

CÓDIGOS F.D.A. / FDA CODES

+

+ Gestión de códigos FDA para mercancías / FDA goods code management +

+
+ +
+ +
+
+ + e.key === 'Enter' && searchFdaCode()} + /> +
+ +
+ + (Packet Code) + {#if globalFdaId} + Registrado + {:else if globalFdaCode && !isLoadingFda && globalFdaId === null} + Nuevo registro + {/if} + +
+ + +
+ + +
+
+ + +

(Description)

+
+
+ +
+

+ Especificaciones del Producto FDA +

+ +
+
+ + +

(Product Code)

+
+ +
+ + +

(Cargo Storage Status)

+
+ +
+ + +

(Requirement)

+
+ +
+ + +

(Manufacture)

+
+ +
+ +
+ + +
+

(Country of Production)

+
+ +
+ + +

(Warehouse Code)

+
+ +
+ + +

(ATL Call)

+
+
+
+ +
+

+ Códigos de Afirmación (Affirmation Codes) +

+ +
+ {#each fda23Codes.slice(0, 5) as code, idx} +
+
+ + +
+
+ + +
+
+ {/each} +
+
+
+
+
+
+ + +
+ + +
+ +

Detalles FDA01

+
+ +
+
+ {#each [{ l: 'Prod Code', k: 'prod_code' }, { l: 'Comodity Desc', k: 'commodity_desc' }, { l: 'Brand Name', k: 'brand_name' }, { l: 'Disclaimer', k: 'disclaimer' }, { l: 'Pgm Code', k: 'pgm_code' }, { l: 'Proc Code', k: 'proc_code' }, { l: 'Intnd Use Code', k: 'intnd_use_code' }, { l: 'Intnd Use Desc', k: 'intnd_use_desc' }, { l: 'Temp Qual', k: 'temp_qual' }, { l: 'Temp Type', k: 'temp_type' }] as { l, k }} +
+ + +
+ {/each} + {#each [{ l: 'Temp Degrees', k: 'temp_degrees' }, { l: 'Temp Negative', k: 'temp_negative' }] as { l, k }} +
+ + +
+ {/each} +
+ + +
+ {#each [{ l: 'Quantity 1', k: 'quantity_1' }, { l: 'Quantity 2', k: 'quantity_2' }] as { l, k }} +
+ + +
+ {/each} +
+ + +
+
+ +
+
+ + +
+
+ + +
+ {#each [{ l: 'Qty Uom 3', k: 'qty_uom_3' }, { l: 'Ctry Prod', k: 'ctry_prod' }, { l: 'Ctry Source', k: 'ctry_source' }, { l: 'Ctry Growth', k: 'ctry_growth' }, { l: 'Ctry Refusal', k: 'ctry_refusal' }, { l: 'Ctry Shipping', k: 'ctry_shipping' }, { l: 'Manuf Key', k: 'manuf_key' }, { l: 'Shipper Key', k: 'shipper_key' }, { l: 'Ult Cons Key', k: 'ult_cons_key' }, { l: 'Fda Imp Key', k: 'fda_imp_key' }, { l: 'Pn Subm Key', k: 'pn_subm_key' }, { l: 'Consol Key', k: 'consol_key' }, { l: 'Producer Key', k: 'producer_key' }, { l: 'Owner Key', k: 'owner_key' }] as { l, k }} +
+ + +
+ {/each} +
+ +
+ {#each [{ l: 'Deli Party Key', k: 'deli_party_key' }, { l: 'Grower Key', k: 'grower_key' }, { l: 'Dev Ini Imp Key', k: 'dev_ini_imp_key' }, { l: 'Lacf Cont 1', k: 'lacf_cont_1' }, { l: 'Lacf Cont 2', k: 'lacf_cont_2' }, { l: 'Lacf Cont 3', k: 'lacf_cont_3' }, { l: 'PN Transmitter Key', k: 'pn_transmitter_key' }] as { l, k }} +
+ + +
+ {/each} +
+
+
+
+
+
+ + + +
+ +

Registros FDA04

+
+
+ + + + Línea + Ele Name + Ele Qty + Ele Qty UOM + Ele Pctg + Acciones + + + + {#each fda04Data as row, idx (idx)} + + {row.line} + {row.ele_name} + {row.ele_qty || '-'} + {row.ele_qty_uom || '-'} + {row.ele_pctg || '-'} + + + + + + {/each} + {#if fda04Data.length === 0} + + Sin registros + + {/if} + + +
+
+ + + + + + Constituent Elements + + Agrega o edita los elementos para la clave FDA. + + + +
+
+
+ +
{globalFdaCode || '---'}
+
+
+ + +
+
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + + + +
+
+
+
+
+ + + +
+ +

Registros FDA25

+
+
+ + + + Línea + Lot Number + Production Start Date + Production End Date + Acciones + + + + {#each fda25Data as row, idx (idx)} + + {row.line} + {row.lot_number} + {row.prod_start_date} + {row.prod_end_date} + + + + + + {/each} + {#if fda25Data.length === 0} + + Sin registros + + {/if} + + +
+
+ + + + + + Lot and Production Dates + + Agrega o edita la información de lotes y fechas de producción. + + + +
+
+
+ +
{globalFdaCode || '---'}
+
+
+ + +
+
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+
+ + + + + +
+
+
+
+
+ + + + +
+
+ +

Afirmación de Cumplimiento (FDA23)

+
+ +
+ + + + + Línea + Código (AOC Code) + Calificador (AOC Qual) + Acciones + + + + {#each fda23Codes as code, idx} + {#if code.aoc_code || code.aoc_qual} + + {idx + 1} + {code.aoc_code || '-'} + {code.aoc_qual || '-'} + +
+ + +
+
+
+ {/if} + {/each} +
+
+ + + + + + + {editIndexFda23 !== null ? 'Editar Código' : 'Nuevo Código'} de Afirmación + + Gestión de códigos de cumplimiento FDA23 + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + + +
+
+
+
+
+
+ +
+
+ + +
+ + General + FDA01 + FDA04 + FDA23 + FDA25 + +
+
+ + +
+
+
+
+ + { + formData.country_of_production = country.m3_key || country.mex_key || ''; + countryModalOpen = false; + }} +/> + + From 86fe2f72019aa953aa68d63b2e2781e7628148ee Mon Sep 17 00:00:00 2001 From: AlexeerCT Date: Tue, 3 Mar 2026 08:46:56 -0600 Subject: [PATCH 10/11] chore: Remove unused test files for debug and user info --- backend/test_debug.py | 44 --------------------------------------- backend/test_user_info.py | 39 ---------------------------------- 2 files changed, 83 deletions(-) delete mode 100644 backend/test_debug.py delete mode 100644 backend/test_user_info.py diff --git a/backend/test_debug.py b/backend/test_debug.py deleted file mode 100644 index cfb342be..00000000 --- a/backend/test_debug.py +++ /dev/null @@ -1,44 +0,0 @@ -import sys -import os -sys.path.append('/app') -sys.path.append('/home/josmar/dev/anexo76/backend') - -from sqlalchemy import create_engine -from sqlalchemy.orm import sessionmaker -import asyncio -import logging - -# Disable logging to keep output clean -logging.basicConfig(level=logging.ERROR) - -from api.v1.modules.a76.general_catalogs.fractions.tariff_fractions.service import TariffFractionService -from api.v1.modules.a76.general_catalogs.fractions.tariff_fractions.dto import TariffFractionResponseDTO - -async def test(): - # Use the database URL from the environment or default - db_url = "postgresql://postgres:postgres@anexo76-postgres-a76:5432/anexo76_core" - engine = create_engine(db_url) - Session = sessionmaker(bind=engine) - db = Session() - - try: - print("Starting test for catalog='american'...") - items, total = await TariffFractionService.get_all( - db, skip=0, limit=10, filters=None, catalog="american", tenant_id=1, company_id=1 - ) - print(f"Service Success! Total: {total}") - - print("Validating items with TariffFractionResponseDTO...") - for item in items: - dto = TariffFractionResponseDTO.model_validate(item) - print(f"DTO: ID={dto.id}, Code={dto.code}, Fraction={dto.fraction}") - - except Exception as e: - print(f"Error caught: {type(e).__name__}: {e}") - import traceback - traceback.print_exc() - finally: - db.close() - -if __name__ == "__main__": - asyncio.run(test()) diff --git a/backend/test_user_info.py b/backend/test_user_info.py deleted file mode 100644 index 17f2cc2b..00000000 --- a/backend/test_user_info.py +++ /dev/null @@ -1,39 +0,0 @@ -import sys -import os -sys.path.append('/app') -sys.path.append('/home/josmar/dev/anexo76/backend') - -from sqlalchemy import create_engine, text -from sqlalchemy.orm import sessionmaker - -def test(): - db_url = "postgresql://postgres:postgres@anexo76-postgres-a76:5432/anexo76_core" - engine = create_engine(db_url) - Session = sessionmaker(bind=engine) - db = Session() - - try: - print("Checking users and tenants...") - # Check users - users = db.execute(text("SELECT id, email, tenant_id FROM a76.users")).fetchall() - for u in users: - print(f"User ID: {u.id} | Email: {u.email} | Tenant ID: {u.tenant_id}") - - # Check companies - companies = db.execute(text("SELECT id, name, tenant_id FROM a76.companies")).fetchall() - for c in companies: - print(f"Company ID: {c.id} | Name: {c.name} | Tenant ID: {c.tenant_id}") - - # Check fractions - fractions = db.execute(text("SELECT id, code, tenant_id, company_id FROM a76.us_tariff_fractions")).fetchall() - print(f"Total US Fractions in DB: {len(fractions)}") - for f in fractions: - print(f"Fraction ID: {f.id} | Code: {f.code} | Tenant ID: {f.tenant_id} | Company ID: {f.company_id}") - - except Exception as e: - print(f"Error: {e}") - finally: - db.close() - -if __name__ == "__main__": - test() From 7ad519968a3df73f7fe5da6aa2be09eb1b3bd58f Mon Sep 17 00:00:00 2001 From: AlexeerCT Date: Tue, 3 Mar 2026 09:11:06 -0600 Subject: [PATCH 11/11] refactor: Remove create_transportation_tables function and related logic --- backend/main.py | 135 +----------------------------------------------- 1 file changed, 1 insertion(+), 134 deletions(-) diff --git a/backend/main.py b/backend/main.py index 8b468c74..c8fffaa3 100644 --- a/backend/main.py +++ b/backend/main.py @@ -150,146 +150,13 @@ async def validation_exception_handler(request: Request, exc: RequestValidationE def run_migrations(): subprocess.run(["alembic", "upgrade", "head"], check=True) - -def create_transportation_tables(): - """Crea las tablas de transporte directamente si no existen. - Se usa en lugar de una migración Alembic para evitar gestionar versiones. - """ - from sqlalchemy import text - from core.database import core_engine - - ddl_statements = [ - """ - CREATE TABLE IF NOT EXISTS a76.transporter ( - transporter_key VARCHAR(23) PRIMARY KEY, - name VARCHAR(256), - short_name VARCHAR(10), - responsible VARCHAR(100), - rfc VARCHAR(30), - streets VARCHAR(100), - postal_code VARCHAR(15), - city VARCHAR(30), - state VARCHAR(30), - country VARCHAR(3), - loader_code VARCHAR(9), - caat_code VARCHAR(49), - transport_code VARCHAR(8), - transport_interface_type VARCHAR(20), - ftp_server VARCHAR(200), - ftp_user VARCHAR(200), - ftp_password VARCHAR(100), - ftp_directory VARCHAR(1000), - filler_code VARCHAR(20), - tenant_id INTEGER NOT NULL REFERENCES core.tenants(id), - company_id INTEGER NOT NULL REFERENCES a76.company(id), - created_at TIMESTAMP NOT NULL DEFAULT now(), - updated_at TIMESTAMP NOT NULL DEFAULT now(), - deleted_at TIMESTAMP - ); - """, - """ - CREATE TABLE IF NOT EXISTS a76.trailer ( - trailer_number VARCHAR(20) PRIMARY KEY, - ace_trailer_number VARCHAR(10), - trailer_type_key VARCHAR(2), - seal VARCHAR(15), - entity_code VARCHAR(1), - plate_number VARCHAR(17), - state VARCHAR(30), - country VARCHAR(3), - container_key VARCHAR(3), - tenant_id INTEGER NOT NULL REFERENCES core.tenants(id), - company_id INTEGER NOT NULL REFERENCES a76.company(id), - created_at TIMESTAMP NOT NULL DEFAULT now(), - updated_at TIMESTAMP NOT NULL DEFAULT now(), - deleted_at TIMESTAMP - ); - """, - """ - CREATE TABLE IF NOT EXISTS a76.vehicle ( - vehicle_key VARCHAR(14) PRIMARY KEY, - ace_vehicle_key VARCHAR(10), - transporter_key VARCHAR(23), - transport_identifier VARCHAR(30), - transport_type VARCHAR(2), - entity_code VARCHAR(1), - transponder_number VARCHAR(16), - dot_number VARCHAR(8), - plate_number VARCHAR(17), - city VARCHAR(30), - state VARCHAR(30), - country VARCHAR(3), - seal VARCHAR(49), - insurance_company_name VARCHAR(30), - insurance_number VARCHAR(20), - insurance_amount NUMERIC(13, 2), - insurance_date INTEGER, - box_number VARCHAR(300), - brand VARCHAR(20), - year VARCHAR(4), - series VARCHAR(30), - description VARCHAR(100), - engine_number VARCHAR(50), - sct_permission VARCHAR(40), - color VARCHAR(20), - container_key VARCHAR(3), - tenant_id INTEGER NOT NULL REFERENCES core.tenants(id), - company_id INTEGER NOT NULL REFERENCES a76.company(id), - created_at TIMESTAMP NOT NULL DEFAULT now(), - updated_at TIMESTAMP NOT NULL DEFAULT now(), - deleted_at TIMESTAMP - ); - """, - ] - - with core_engine.connect() as conn: - for stmt in ddl_statements: - conn.execute(text(stmt)) - # Ampliar columnas que pudieron haberse creado con tamaño incorrecto - conn.execute(text(""" - DO $$ - BEGIN - -- Fix transporter_key si fue creada como VARCHAR(5) - IF EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_schema='a76' AND table_name='transporter' - AND column_name='transporter_key' - AND character_maximum_length < 23 - ) THEN - ALTER TABLE a76.transporter ALTER COLUMN transporter_key TYPE VARCHAR(23); - END IF; - -- Fix filler_code si fue creada como VARCHAR(4) - IF EXISTS ( - SELECT 1 FROM information_schema.columns - WHERE table_schema='a76' AND table_name='transporter' - AND column_name='filler_code' - AND character_maximum_length < 20 - ) THEN - ALTER TABLE a76.transporter ALTER COLUMN filler_code TYPE VARCHAR(20); - END IF; - - -- Eliminar constraint de trailer_type_key en trailer si existe - IF EXISTS ( - SELECT 1 FROM information_schema.table_constraints - WHERE constraint_name='trailer_trailer_type_key_fkey' - AND table_schema='a76' AND table_name='trailer' - ) THEN - ALTER TABLE a76.trailer DROP CONSTRAINT trailer_trailer_type_key_fkey; - END IF; - END$$; - """)) - conn.commit() - logger.info("Tablas de transporte verificadas/creadas correctamente.") - - # Inicializar la base de datos @app.on_event("startup") async def on_startup(): """Evento de inicio de la aplicación""" logger.info("Iniciando la aplicación Anexo76...") init_db() - run_migrations() - create_transportation_tables() + run_migrations() logger.info("Base de datos inicializada correctamente.")