diff --git a/backend/api/v1/modules/a24/inv/inv_aphis/inv_aphis_catalog/__init__.py b/backend/api/v1/modules/a24/inv/inv_aphis/inv_aphis_catalog/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/api/v1/modules/a24/inv/inv_aphis/inv_aphis_catalog/dto.py b/backend/api/v1/modules/a24/inv/inv_aphis/inv_aphis_catalog/dto.py new file mode 100644 index 00000000..7ece8ee1 --- /dev/null +++ b/backend/api/v1/modules/a24/inv/inv_aphis/inv_aphis_catalog/dto.py @@ -0,0 +1,57 @@ +from pydantic import BaseModel, ConfigDict, field_validator +from typing import Optional, List, Dict, Any +from datetime import date + + +class AphisCatalogDTO(BaseModel): + id: Optional[int] = None + + # --- Pestaña 1: General --- + program_code: Optional[str] = None + processing_code: Optional[str] = None + aphis_type: Optional[str] = None + disclaimer: Optional[str] = None + electronic_image: Optional[str] = None + confidential: Optional[str] = None + global_product_id: Optional[str] = None + intended_use_code: Optional[str] = None + intended_use_description: Optional[str] = None + item_type: Optional[str] = None + product_code: Optional[str] = None + product_code_2: Optional[str] = None + product_code_3: Optional[str] = None + scientific_genus_name: Optional[str] = None + scientific_species_name: Optional[str] = None + scientific_sub_species_name: Optional[str] = None + common_name_specific: Optional[str] = None + common_name_general: Optional[str] = None + signed_doc: Optional[str] = None + signed_doc_date: Optional[date] = None + signed_doc_id: Optional[str] = None + invoice_number: Optional[str] = None + quantity_1: Optional[str] = None + quantity_2: Optional[str] = None + quantity_3: Optional[str] = None + inspection: Optional[str] = None + inspection_date: Optional[date] = None + inspection_loc_date: Optional[date] = None + inspection_location: Optional[str] = None + country_production: Optional[str] = None + country_source: Optional[str] = None + + # --- Pestañas 2-7: Detalles (Listas de objetos) --- + characteristics: Optional[List[Dict[str, Any]]] = [] + pitems: Optional[List[Dict[str, Any]]] = [] + lpcos: Optional[List[Dict[str, Any]]] = [] + entities: Optional[List[Dict[str, Any]]] = [] + containers: Optional[List[Dict[str, Any]]] = [] + routing: Optional[List[Dict[str, Any]]] = [] + + model_config = ConfigDict(from_attributes=True) + + @field_validator("signed_doc_date", "inspection_date", "inspection_loc_date", mode="before") + @classmethod + def empty_to_none(cls, v): + if v == "": + return None + return v diff --git a/backend/api/v1/modules/a24/inv/inv_aphis/inv_aphis_catalog/models.py b/backend/api/v1/modules/a24/inv/inv_aphis/inv_aphis_catalog/models.py new file mode 100644 index 00000000..ef494b44 --- /dev/null +++ b/backend/api/v1/modules/a24/inv/inv_aphis/inv_aphis_catalog/models.py @@ -0,0 +1,61 @@ +from typing import Optional, List, Dict, Any +from datetime import date +from sqlalchemy import Integer, String, Date, PrimaryKeyConstraint, JSON +from sqlalchemy.orm import Mapped, mapped_column +from core.database import Base + + +class AphisCatalog(Base): + """ + Catálogo global de registros APHIS por empresa. + Soporta las 7 pestañas de información (General + 6 detalles via JSON). + """ + __tablename__ = "inv_aphis_catalog" + __table_args__ = ( + PrimaryKeyConstraint("id", name="inv_aphis_catalog_pkey"), + {"schema": "a24", "extend_existing": True}, + ) + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + + # --- Pestaña 1: General (Campos principales) --- + program_code: Mapped[Optional[str]] = mapped_column(String(10)) + processing_code: Mapped[Optional[str]] = mapped_column(String(10)) + aphis_type: Mapped[Optional[str]] = mapped_column(String(10)) + disclaimer: Mapped[Optional[str]] = mapped_column(String(10)) + electronic_image: Mapped[Optional[str]] = mapped_column(String(50)) + confidential: Mapped[Optional[str]] = mapped_column(String(1)) + global_product_id: Mapped[Optional[str]] = mapped_column(String(100)) + intended_use_code: Mapped[Optional[str]] = mapped_column(String(10)) + intended_use_description: Mapped[Optional[str]] = mapped_column(String(200)) + item_type: Mapped[Optional[str]] = mapped_column(String(20)) + product_code: Mapped[Optional[str]] = mapped_column(String(20)) + product_code_2: Mapped[Optional[str]] = mapped_column(String(20)) + product_code_3: Mapped[Optional[str]] = mapped_column(String(20)) + scientific_genus_name: Mapped[Optional[str]] = mapped_column(String(100)) + scientific_species_name: Mapped[Optional[str]] = mapped_column(String(100)) + scientific_sub_species_name: Mapped[Optional[str]] = mapped_column(String(100)) + common_name_specific: Mapped[Optional[str]] = mapped_column(String(200)) + common_name_general: Mapped[Optional[str]] = mapped_column(String(200)) + signed_doc: Mapped[Optional[str]] = mapped_column(String(100)) + signed_doc_date: Mapped[Optional[date]] = mapped_column(Date) + signed_doc_id: Mapped[Optional[str]] = mapped_column(String(50)) + invoice_number: Mapped[Optional[str]] = mapped_column(String(50)) + quantity_1: Mapped[Optional[str]] = mapped_column(String(50)) + quantity_2: Mapped[Optional[str]] = mapped_column(String(50)) + quantity_3: Mapped[Optional[str]] = mapped_column(String(50)) + inspection: Mapped[Optional[str]] = mapped_column(String(200)) + inspection_date: Mapped[Optional[date]] = mapped_column(Date) + inspection_loc_date: Mapped[Optional[date]] = mapped_column(Date) + inspection_location: Mapped[Optional[str]] = mapped_column(String(200)) + country_production: Mapped[Optional[str]] = mapped_column(String(3)) + country_source: Mapped[Optional[str]] = mapped_column(String(3)) + + # --- Pestañas 2-7: Detalles (Almacenados como JSON por flexibilidad) --- + characteristics: Mapped[Optional[List[Dict[str, Any]]]] = mapped_column(JSON, default=list) + pitems: Mapped[Optional[List[Dict[str, Any]]]] = mapped_column(JSON, default=list) + lpcos: Mapped[Optional[List[Dict[str, Any]]]] = mapped_column(JSON, default=list) + entities: Mapped[Optional[List[Dict[str, Any]]]] = mapped_column(JSON, default=list) + containers: Mapped[Optional[List[Dict[str, Any]]]] = mapped_column(JSON, default=list) + routing: Mapped[Optional[List[Dict[str, Any]]]] = mapped_column(JSON, default=list) diff --git a/backend/api/v1/modules/a24/inv/inv_aphis/inv_aphis_catalog/router.py b/backend/api/v1/modules/a24/inv/inv_aphis/inv_aphis_catalog/router.py new file mode 100644 index 00000000..3e5e21d5 --- /dev/null +++ b/backend/api/v1/modules/a24/inv/inv_aphis/inv_aphis_catalog/router.py @@ -0,0 +1,62 @@ +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from typing import List +from core.database import get_core_db +from .models import AphisCatalog +from .dto import AphisCatalogDTO + +router = APIRouter(prefix="/aphis-catalog", tags=["APHIS Catalog"]) + + +@router.get("/", response_model=List[AphisCatalogDTO]) +def list_aphis_catalog(company_id: int, db: Session = Depends(get_core_db)): + records = ( + db.query(AphisCatalog) + .filter(AphisCatalog.company_id == company_id) + .order_by(AphisCatalog.id) + .all() + ) + return records + + +@router.post("/", response_model=AphisCatalogDTO) +def create_aphis_catalog(data: AphisCatalogDTO, company_id: int, db: Session = Depends(get_core_db)): + payload = data.model_dump(exclude={"id"}) + record = AphisCatalog(**payload, company_id=company_id) + db.add(record) + db.commit() + db.refresh(record) + return record + + +@router.put("/{record_id}", response_model=AphisCatalogDTO) +def update_aphis_catalog( + record_id: int, data: AphisCatalogDTO, company_id: int, db: Session = Depends(get_core_db) +): + record = ( + db.query(AphisCatalog) + .filter(AphisCatalog.id == record_id, AphisCatalog.company_id == company_id) + .first() + ) + if not record: + raise HTTPException(status_code=404, detail="Registro no encontrado") + + for field, value in data.model_dump(exclude={"id"}).items(): + setattr(record, field, value) + + db.commit() + db.refresh(record) + return record + + +@router.delete("/{record_id}", status_code=204) +def delete_aphis_catalog(record_id: int, company_id: int, db: Session = Depends(get_core_db)): + record = ( + db.query(AphisCatalog) + .filter(AphisCatalog.id == record_id, AphisCatalog.company_id == company_id) + .first() + ) + if not record: + raise HTTPException(status_code=404, detail="Registro no encontrado") + db.delete(record) + db.commit() diff --git a/backend/api/v1/modules/a24/router.py b/backend/api/v1/modules/a24/router.py index f011e110..dca3877f 100644 --- a/backend/api/v1/modules/a24/router.py +++ b/backend/api/v1/modules/a24/router.py @@ -8,6 +8,10 @@ from fastapi import APIRouter from .fa.fa_classes.routes import router as fa_classes_router from .fa.fa_item_lines.routes import router as fa_item_lines_router from .inv.part_countries.routes import router as part_countries_router +from .inv.inv_aphis.inv_aphis_catalog.router import router as aphis_catalog_router + +# Importar modelo para que SQLAlchemy cree la tabla automáticamente +import api.v1.modules.a24.inv.inv_aphis.inv_aphis_catalog.models # noqa: F401 # Router principal de A24 @@ -21,3 +25,5 @@ router.include_router( # Registrar routers de INV (Inventory) router.include_router(part_countries_router, prefix="/a24", tags=["a24 / inv / part-countries"]) +router.include_router(aphis_catalog_router, prefix="/a24", tags=["a24 / inv / aphis-catalog"]) + diff --git a/frontend/src/lib/api/dashboard/a76/aphis-catalog.ts b/frontend/src/lib/api/dashboard/a76/aphis-catalog.ts new file mode 100644 index 00000000..900d7614 --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/aphis-catalog.ts @@ -0,0 +1,59 @@ +import { api } from '$lib/api'; + +export interface AphisCatalogRecord { + id?: number; + // --- Pestaña 1: General --- + program_code?: string; + processing_code?: string; + aphis_type?: string; + disclaimer?: string; + electronic_image?: string; + confidential?: string; + global_product_id?: string; + intended_use_code?: string; + intended_use_description?: string; + item_type?: string; + product_code?: string; + product_code_2?: string; + product_code_3?: string; + scientific_genus_name?: string; + scientific_species_name?: string; + scientific_sub_species_name?: string; + common_name_specific?: string; + common_name_general?: string; + signed_doc?: string; + signed_doc_date?: string; + signed_doc_id?: string; + invoice_number?: string; + quantity_1?: string; + quantity_2?: string; + quantity_3?: string; + inspection?: string; + inspection_date?: string; + inspection_loc_date?: string; + inspection_location?: string; + country_production?: string; + country_source?: string; + + // --- Pestañas 2-7: Detalles --- + characteristics?: any[]; + pitems?: any[]; + lpcos?: any[]; + entities?: any[]; + containers?: any[]; + routing?: any[]; +} + +export const aphisCatalogApi = { + list: (company_id: number) => + api.get(`/v1/a24/aphis-catalog/?company_id=${company_id}`), + + create: (data: AphisCatalogRecord, company_id: number) => + api.post(`/v1/a24/aphis-catalog/?company_id=${company_id}`, data), + + update: (id: number, data: AphisCatalogRecord, company_id: number) => + api.put(`/v1/a24/aphis-catalog/${id}?company_id=${company_id}`, data), + + delete: (id: number, company_id: number) => + api.delete(`/v1/a24/aphis-catalog/${id}?company_id=${company_id}`) +}; diff --git a/frontend/src/lib/components/dashboard/goods/parts/partForm.svelte b/frontend/src/lib/components/dashboard/goods/parts/partForm.svelte index 322c06c4..8bf4baff 100644 --- a/frontend/src/lib/components/dashboard/goods/parts/partForm.svelte +++ b/frontend/src/lib/components/dashboard/goods/parts/partForm.svelte @@ -1106,34 +1106,41 @@ {isEdit ? 'Editar' : 'Nueva'} + +
+ + + +
+ -

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

@@ -2690,28 +2697,26 @@ - {#if isEdit} -
-
-
- -
-
-

Datos de Extensión

-

Configura datos adicionales, APHIS y certificaciones.

-
+
+
+
+ +
+
+

Datos de Extensión

+

Configura datos adicionales, APHIS y certificaciones.

-
- {/if} + +
@@ -2979,7 +2984,7 @@
-
+

Permitir o denegar la descarga

diff --git a/frontend/src/routes/dashboard/goods/parts/extension/[[id]]/+page.svelte b/frontend/src/routes/dashboard/goods/parts/extension/[[id]]/+page.svelte index 4e2fb994..83e0d96e 100644 --- a/frontend/src/routes/dashboard/goods/parts/extension/[[id]]/+page.svelte +++ b/frontend/src/routes/dashboard/goods/parts/extension/[[id]]/+page.svelte @@ -44,6 +44,8 @@ import AgencyTariffCodeSelector from '$lib/components/dashboard/goods/parts/AgencyTariffCodeSelector.svelte'; import IdentifierSelector from '$lib/components/dashboard/goods/parts/IdentifierSelector.svelte'; import CartaPorteSelector from '$lib/components/dashboard/goods/parts/CartaPorteSelector.svelte'; + import ClientSelectorDialog from '$lib/components/dashboard/goods/modales/client-selector-dialog.svelte'; + import { aphisCatalogApi, type AphisCatalogRecord } from '$lib/api/dashboard/a76/aphis-catalog'; const idParam = $page.params.id; const isNew = $derived(idParam === 'new'); @@ -54,6 +56,10 @@ // Catálogo de clientes let clients = $state([]); + let isClientSearchOpen = $state(false); + + // Catálogo global de APHIS (independiente de las partes) + let aphisCatalogRecords = $state([]); // Estado del formulario de extensión let formData = $state({ @@ -97,11 +103,10 @@ use_rule_8: false, // Sustitutos (Pestaña Continuación) substitute_parts: [] as { substitute_part_number: string }[], - // Aphis (Sección en Continuación) + // Aphis - solo los dos inputs de referencia aphis_data: { input1: '', - input2: '', - table: [] as any[] + input2: '' } }); @@ -111,6 +116,7 @@ let clientFormData = $state({ part_number: '', client_id: '', + client_name: '', part_name: '' }); @@ -273,9 +279,13 @@ loading = true; try { - // Cargar catálogo de clientes primero - const clientsRes = await clientsProvidersApi.list(companyId, 1, 1000); + // Cargar catálogo de clientes y catálogo global APHIS en paralelo + const [clientsRes, aphisRes] = await Promise.all([ + clientsProvidersApi.list(companyId, 1, 1000), + aphisCatalogApi.list(companyId) + ]); clients = clientsRes.data?.items || []; + aphisCatalogRecords = (aphisRes as any).data || []; if (isNew) { loading = false; @@ -322,13 +332,8 @@ formData.part_identifiers = (part.inv_data as any).part_identifiers || []; formData.substitute_parts = (part.inv_data as any).substitute_parts || []; - formData.aphis_data = (part.inv_data as any).aphis_data || { input1: '', input2: '', table: [] }; - - // Si existen registros relacionales, usarlos para la tabla - const aphis_recs = (part.inv_data as any).aphis_records; - if (aphis_recs && aphis_recs.length > 0) { - formData.aphis_data.table = aphis_recs; - } + const aphisStored = (part.inv_data as any).aphis_data || {}; + formData.aphis_data = { input1: aphisStored.input1 || '', input2: aphisStored.input2 || '' }; } } } catch (e) { @@ -354,7 +359,6 @@ try { loading = true; - // Preparar update const updateData = { part_number: formData.part_number, description_spanish: formData.description_spanish, @@ -379,8 +383,8 @@ client_part_names: formData.client_part_names, part_identifiers: formData.part_identifiers, substitute_parts: formData.substitute_parts, - aphis_data: formData.aphis_data, - aphis_records: formData.aphis_data.table + // Guardamos solo los dos inputs de referencia APHIS (el catálogo es independiente) + aphis_data: formData.aphis_data } }; @@ -400,6 +404,7 @@ clientFormData = { part_number: formData.part_number || '', client_id: '', + client_name: '', part_name: '' }; isClientDialogOpen = true; @@ -411,6 +416,7 @@ clientFormData = { part_number: row.part_number || '', client_id: row.client_id.toString(), + client_name: row.client_name || '', part_name: row.part_name || '' }; isClientDialogOpen = true; @@ -423,10 +429,10 @@ } const selectedClient = clients.find(c => c.id.toString() === clientFormData.client_id); - const newRow = { + const newRow: { part_number: string, client_id: string, client_name: string, part_name: string } = { part_number: clientFormData.part_number, client_id: clientFormData.client_id, - client_name: selectedClient ? selectedClient.name : 'Desconocido', + client_name: clientFormData.client_name, part_name: clientFormData.part_name }; @@ -440,6 +446,12 @@ toast.success(editingClientIndex !== null ? 'Registro actualizado' : 'Registro agregado'); } + function handleClientSelect(client: ClientProvider) { + clientFormData.client_id = client.id.toString(); + clientFormData.client_name = client.name; + isClientSearchOpen = false; + } + function removeClientRow(index: number) { formData.client_part_names = formData.client_part_names.filter((_, i) => i !== index); toast.info('Registro eliminado'); @@ -513,7 +525,7 @@ function openAphisEdit(index: number) { editingAphisIndex = index; - const row = formData.aphis_data.table[index]; + const row = aphisCatalogRecords[index]; aphisFormData = { general: { program_code: row.program_code || '', @@ -549,7 +561,7 @@ country_source: row.country_source || '' }, characteristics: row.characteristics || [], - stype_pitems: row.stype_pitems || [], + stype_pitems: row.pitems || [], lpcos: row.lpcos || [], entities: row.entities || [], containers: row.containers || [], @@ -559,30 +571,57 @@ isAphisDetailDialogOpen = true; } - function saveAphisDetail() { - const consolidatedRow = { + async function saveAphisDetail() { + const companyId = companyStore.activeCompany?.id; + if (!companyId) return; + + // Consolidar todos los datos (General + 6 pestañas de detalle) + const payload: any = { ...aphisFormData.general, characteristics: aphisFormData.characteristics, - stype_pitems: aphisFormData.stype_pitems, + pitems: aphisFormData.stype_pitems, lpcos: aphisFormData.lpcos, entities: aphisFormData.entities, containers: aphisFormData.containers, routing: aphisFormData.routing }; - if (editingAphisIndex !== null) { - formData.aphis_data.table[editingAphisIndex] = consolidatedRow; - } else { - formData.aphis_data.table = [...formData.aphis_data.table, consolidatedRow]; - } + // Limpiar campos vacíos (convertir "" a null para evitar errores de validación en el backend) + Object.keys(payload).forEach(key => { + if (payload[key] === "") payload[key] = null; + }); - isAphisDetailDialogOpen = false; - toast.success(editingAphisIndex !== null ? 'Registro Aphis actualizado' : 'Registro Aphis agregado'); + try { + if (editingAphisIndex !== null) { + const existingId = aphisCatalogRecords[editingAphisIndex].id; + await aphisCatalogApi.update(existingId!, payload, companyId); + } else { + await aphisCatalogApi.create(payload, companyId); + } + // Recargar el catálogo completo para reflejar los datos reales del backend + const reloaded = await aphisCatalogApi.list(companyId); + aphisCatalogRecords = (reloaded as any).data || reloaded || []; + isAphisDetailDialogOpen = false; + toast.success(editingAphisIndex !== null ? 'Registro Aphis actualizado' : 'Registro Aphis agregado'); + } catch (e) { + console.error('Error guardando APHIS:', e); + toast.error('Error al guardar el registro Aphis'); + } } - function removeAphisRow(index: number) { - formData.aphis_data.table = formData.aphis_data.table.filter((_, i) => i !== index); - toast.info('Registro Aphis eliminado'); + + async function removeAphisRow(index: number) { + const companyId = companyStore.activeCompany?.id; + if (!companyId) return; + const record = aphisCatalogRecords[index]; + try { + if (record.id) await aphisCatalogApi.delete(record.id, companyId); + aphisCatalogRecords = aphisCatalogRecords.filter((_: AphisCatalogRecord, i: number) => i !== index); + toast.info('Registro Aphis eliminado'); + } catch (e) { + console.error('Error eliminando APHIS:', e); + toast.error('Error al eliminar el registro'); + } } // --- Funciones para Aphis Characteristic --- @@ -1168,15 +1207,20 @@
-
+
+
-
- +
+ + +
+
+ +
-
@@ -1203,17 +1247,20 @@
- - + +
+ isClientSearchOpen = true} + class="cursor-pointer bg-muted/30" + /> + +
@@ -1269,9 +1316,9 @@
- Gestión de Registros Aphis + Catálogo Aphis - Lista maestra de especificaciones Aphis para esta parte. + Gestiona tus registros Aphis. Al seleccionar uno, se usará su Program Code en el formulario.
@@ -1601,7 +1660,7 @@ {:else} - + No hay características asignadas a este registro. @@ -1610,7 +1669,7 @@
- +
Stype_Pitems @@ -1661,7 +1720,7 @@ {:else} - + No hay registros de stype asignados. @@ -1729,7 +1788,7 @@ {:else} - + No hay registros LPCO asignados. @@ -2184,4 +2243,5 @@ +