From 0b4c385aeb46a9a274f0d0ebd7e7361edbe5ecc9 Mon Sep 17 00:00:00 2001 From: Galindo97 Date: Fri, 2 Jan 2026 14:16:09 -0600 Subject: [PATCH] feat: Implement Document Types for Digitization module - Added backend API for managing document types related to digitization, including CRUD operations. - Created DTOs and models for DocumentTypeDigitization with validation using Pydantic. - Updated frontend API service to interact with the new document types API. - Refactored existing forms in the frontend to load and manage document types effectively. - Enhanced data loading and state management in various components related to pedimentos. --- .../v1/modules/a76/doc_types_dig/__init__.py | 1 + .../api/v1/modules/a76/doc_types_dig/dto.py | 32 ++ .../v1/modules/a76/doc_types_dig/models.py | 25 ++ .../v1/modules/a76/doc_types_dig/routes.py | 170 +++++++ .../models/pedimento_config_additional.py | 15 +- backend/api/v1/modules/a76/router.py | 2 + .../document_types_digitization.ts | 70 +++ .../accounts-compensation-tab-form.svelte | 7 +- .../edit/contributions-tab-form.svelte | 2 +- .../edit/digitization-tab-form.svelte | 418 +++++++++++++++--- .../edit/other-data-tab-form.svelte | 99 +---- .../package-transportation-tab-form.svelte | 76 +++- .../pedimentos/edit/[id]/+page.svelte | 201 +++++++-- 13 files changed, 890 insertions(+), 228 deletions(-) create mode 100644 backend/api/v1/modules/a76/doc_types_dig/__init__.py create mode 100644 backend/api/v1/modules/a76/doc_types_dig/dto.py create mode 100644 backend/api/v1/modules/a76/doc_types_dig/models.py create mode 100644 backend/api/v1/modules/a76/doc_types_dig/routes.py create mode 100644 frontend/src/lib/api/dashboard/refrence_data/document_types_digitization.ts diff --git a/backend/api/v1/modules/a76/doc_types_dig/__init__.py b/backend/api/v1/modules/a76/doc_types_dig/__init__.py new file mode 100644 index 00000000..e65cae83 --- /dev/null +++ b/backend/api/v1/modules/a76/doc_types_dig/__init__.py @@ -0,0 +1 @@ +# Document Types for Digitization module diff --git a/backend/api/v1/modules/a76/doc_types_dig/dto.py b/backend/api/v1/modules/a76/doc_types_dig/dto.py new file mode 100644 index 00000000..21ee57e2 --- /dev/null +++ b/backend/api/v1/modules/a76/doc_types_dig/dto.py @@ -0,0 +1,32 @@ +from pydantic import BaseModel, Field + + +class DocumentTypeDigitizationBase(BaseModel): + """Base schema for Document Type Digitization""" + + code: str = Field(..., max_length=10, description="Código del tipo de documento") + description: str = Field(..., description="Descripción del tipo de documento") + active: bool = Field(default=True, description="Indica si el tipo está activo") + + +class DocumentTypeDigitizationCreate(DocumentTypeDigitizationBase): + """Schema for creating a Document Type Digitization""" + + pass + + +class DocumentTypeDigitizationUpdate(BaseModel): + """Schema for updating a Document Type Digitization""" + + code: str | None = Field(None, max_length=10) + description: str | None = None + active: bool | None = None + + +class DocumentTypeDigitizationResponse(DocumentTypeDigitizationBase): + """Schema for Document Type Digitization response""" + + id: int + + class Config: + from_attributes = True diff --git a/backend/api/v1/modules/a76/doc_types_dig/models.py b/backend/api/v1/modules/a76/doc_types_dig/models.py new file mode 100644 index 00000000..db7636ce --- /dev/null +++ b/backend/api/v1/modules/a76/doc_types_dig/models.py @@ -0,0 +1,25 @@ +from api.v1.common.base_models import TenantScopedMixin, TimestampMixin +from core.database import Base +from sqlalchemy import Boolean, Integer, PrimaryKeyConstraint, String, Text, UniqueConstraint +from sqlalchemy.orm import Mapped, mapped_column + + +class DocumentTypeDigitization(Base, TenantScopedMixin, TimestampMixin): + """Tipos de documentos para digitalización de pedimentos""" + + __tablename__ = "document_types_digitization" + __table_args__ = ( + PrimaryKeyConstraint("id", name="document_types_digitization_pkey"), + UniqueConstraint( + "tenant_id", + "company_id", + "code", + name="document_types_digitization_code_key", + ), + {"schema": "a76"}, + ) + + id: Mapped[int] = mapped_column(Integer) + code: Mapped[str] = mapped_column(String(10), nullable=False, index=True) + description: Mapped[str] = mapped_column(Text, nullable=False) + active: Mapped[bool] = mapped_column(Boolean, default=True) diff --git a/backend/api/v1/modules/a76/doc_types_dig/routes.py b/backend/api/v1/modules/a76/doc_types_dig/routes.py new file mode 100644 index 00000000..a3ee51a7 --- /dev/null +++ b/backend/api/v1/modules/a76/doc_types_dig/routes.py @@ -0,0 +1,170 @@ +from typing import List + +from api.v1.modules.a76.doc_types_dig.dto import ( + DocumentTypeDigitizationCreate, + DocumentTypeDigitizationResponse, + DocumentTypeDigitizationUpdate, +) +from api.v1.modules.a76.doc_types_dig.models import DocumentTypeDigitization +from core.database import get_core_db +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy import select +from sqlalchemy.orm import Session + +router = APIRouter(prefix="/document-types-digitization", tags=["Document Types Digitization"]) + + +@router.get("", response_model=List[DocumentTypeDigitizationResponse]) +def get_all_document_types( + active_only: bool = True, + db: Session = Depends(get_core_db), +): + """ + Obtener todos los tipos de documentos para digitalización + + Args: + active_only: Si es True, solo devuelve los tipos activos + """ + query = select(DocumentTypeDigitization) + + if active_only: + query = query.where(DocumentTypeDigitization.active == True) + + query = query.order_by(DocumentTypeDigitization.code) + + result = db.execute(query) + document_types = result.scalars().all() + + return document_types + + +@router.get("/{document_type_id}", response_model=DocumentTypeDigitizationResponse) +def get_document_type( + document_type_id: int, + db: Session = Depends(get_core_db), +): + """Obtener un tipo de documento por ID""" + result = db.execute( + select(DocumentTypeDigitization).where(DocumentTypeDigitization.id == document_type_id) + ) + document_type = result.scalar_one_or_none() + + if not document_type: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Tipo de documento con ID {document_type_id} no encontrado" + ) + + return document_type + + +@router.get("/by-code/{code}", response_model=DocumentTypeDigitizationResponse) +def get_document_type_by_code( + code: str, + db: Session = Depends(get_core_db), +): + """Obtener un tipo de documento por código""" + result = db.execute( + select(DocumentTypeDigitization).where(DocumentTypeDigitization.code == code) + ) + document_type = result.scalar_one_or_none() + + if not document_type: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Tipo de documento con código {code} no encontrado" + ) + + return document_type + + +@router.post("", response_model=DocumentTypeDigitizationResponse, status_code=status.HTTP_201_CREATED) +def create_document_type( + document_type_data: DocumentTypeDigitizationCreate, + db: Session = Depends(get_core_db), +): + """Crear un nuevo tipo de documento""" + # Verificar si el código ya existe + result = db.execute( + select(DocumentTypeDigitization).where(DocumentTypeDigitization.code == document_type_data.code) + ) + existing = result.scalar_one_or_none() + + if existing: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Ya existe un tipo de documento con el código {document_type_data.code}" + ) + + new_document_type = DocumentTypeDigitization(**document_type_data.model_dump()) + db.add(new_document_type) + db.commit() + db.refresh(new_document_type) + + return new_document_type + + +@router.put("/{document_type_id}", response_model=DocumentTypeDigitizationResponse) +def update_document_type( + document_type_id: int, + document_type_data: DocumentTypeDigitizationUpdate, + db: Session = Depends(get_core_db), +): + """Actualizar un tipo de documento existente""" + result = db.execute( + select(DocumentTypeDigitization).where(DocumentTypeDigitization.id == document_type_id) + ) + document_type = result.scalar_one_or_none() + + if not document_type: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Tipo de documento con ID {document_type_id} no encontrado" + ) + + # Actualizar solo los campos proporcionados + update_data = document_type_data.model_dump(exclude_unset=True) + + # Verificar si el nuevo código ya existe (si se está actualizando) + if "code" in update_data and update_data["code"] != document_type.code: + result = db.execute( + select(DocumentTypeDigitization).where(DocumentTypeDigitization.code == update_data["code"]) + ) + existing = result.scalar_one_or_none() + if existing: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"Ya existe un tipo de documento con el código {update_data['code']}" + ) + + for field, value in update_data.items(): + setattr(document_type, field, value) + + db.commit() + db.refresh(document_type) + + return document_type + + +@router.delete("/{document_type_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_document_type( + document_type_id: int, + db: Session = Depends(get_core_db), +): + """Eliminar un tipo de documento (soft delete, marca como inactivo)""" + result = db.execute( + select(DocumentTypeDigitization).where(DocumentTypeDigitization.id == document_type_id) + ) + document_type = result.scalar_one_or_none() + + if not document_type: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"Tipo de documento con ID {document_type_id} no encontrado" + ) + + # Soft delete - solo marcar como inactivo + document_type.active = False + db.commit() + + return None diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_additional.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_additional.py index 22735ad6..28fff3a1 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_additional.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_additional.py @@ -3,11 +3,10 @@ from typing import TYPE_CHECKING from api.v1.common.base_models import TenantScopedMixin, TimestampMixin from core.database import Base from sqlalchemy import ( + Boolean, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, - SmallInteger, - String, UniqueConstraint, ) from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -38,12 +37,12 @@ class PedimentoConfigAdditional(Base, TenantScopedMixin, TimestampMixin): id: Mapped[int] = mapped_column(Integer) pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) - add_po_identifier: Mapped[int] = mapped_column(SmallInteger) - do_not_exempt_norms_complement_x: Mapped[int] = mapped_column(SmallInteger) - manual_pedimento_year: Mapped[str] = mapped_column(String(2)) - enable_import_invoice_recipient: Mapped[int] = mapped_column(SmallInteger) - send_502_validation_file_for_consolidated: Mapped[int] = mapped_column(SmallInteger) - add_remove_norms: Mapped[int] = mapped_column(SmallInteger) + add_po_identifier: Mapped[bool] = mapped_column(Boolean, default=False) + do_not_exempt_norms_complement_x: Mapped[bool] = mapped_column(Boolean, default=False) + manual_pedimento_year: Mapped[int] = mapped_column(Integer, nullable=True) + enable_import_invoice_recipient: Mapped[bool] = mapped_column(Boolean, default=False) + send_502_validation_file_for_consolidated: Mapped[bool] = mapped_column(Boolean, default=False) + add_remove_norms: Mapped[bool] = mapped_column(Boolean, default=False) pedimento: Mapped["Pedimentos"] = relationship( "Pedimentos", back_populates="pedimento_config_additional" diff --git a/backend/api/v1/modules/a76/router.py b/backend/api/v1/modules/a76/router.py index 8bfee681..a08d04d6 100644 --- a/backend/api/v1/modules/a76/router.py +++ b/backend/api/v1/modules/a76/router.py @@ -14,6 +14,7 @@ from .clients_and_providers import router as client_and_provider_router from .general_catalogs.company import router as company_router from .country_rule_oct.routes import router as country_rule_oct_router from .transportation.drivers.routes import router as drivers_router +from .doc_types_dig.routes import router as doc_types_dig_router from .general_catalogs.exchange_rate.routes import router as exchange_rate_router from .general_catalogs.identifiers.routes import router as identifiers_router from .fraction_rule_octave.routes import router as fraction_rule_octave_router @@ -73,6 +74,7 @@ router.include_router(trailers_router, prefix="/a76", tags=["a76 / trailers"]) router.include_router( customs_broker_router, prefix="/a76", tags=["a76 / customs_broker"] ) +router.include_router(doc_types_dig_router, prefix="/a76", tags=["a76 / document_types_digitization"]) router.include_router(drivers_router, prefix="/a76", tags=["a76 / drivers"]) router.include_router(transporters_router, prefix="/a76", tags=["a76 / transporters"]) diff --git a/frontend/src/lib/api/dashboard/refrence_data/document_types_digitization.ts b/frontend/src/lib/api/dashboard/refrence_data/document_types_digitization.ts new file mode 100644 index 00000000..4ddb1bc7 --- /dev/null +++ b/frontend/src/lib/api/dashboard/refrence_data/document_types_digitization.ts @@ -0,0 +1,70 @@ +import { api } from '$lib/api'; + +export interface DocumentTypeDigitization { + id: number; + code: string; + description: string; + active: boolean; +} + +export interface DocumentTypeDigitizationCreate { + code: string; + description: string; + active?: boolean; +} + +export interface DocumentTypeDigitizationUpdate { + code?: string; + description?: string; + active?: boolean; +} + +const BASE_URL = '/v1/a76/document-types-digitization'; + +/** + * API para Tipos de Documentos de Digitalización + */ +export const documentTypesDigitizationApi = { + /** + * Obtener todos los tipos de documentos para digitalización + */ + getAll: (activeOnly: boolean = true) => { + const url = `${BASE_URL}?active_only=${activeOnly}`; + return api.get(url); + }, + + /** + * Obtener un tipo de documento por ID + */ + getById: (id: number) => { + return api.get(`${BASE_URL}/${id}`); + }, + + /** + * Obtener un tipo de documento por código + */ + getByCode: (code: string) => { + return api.get(`${BASE_URL}/by-code/${code}`); + }, + + /** + * Crear un nuevo tipo de documento + */ + create: (data: DocumentTypeDigitizationCreate) => { + return api.post(BASE_URL, data); + }, + + /** + * Actualizar un tipo de documento existente + */ + update: (id: number, data: DocumentTypeDigitizationUpdate) => { + return api.put(`${BASE_URL}/${id}`, data); + }, + + /** + * Eliminar (soft delete) un tipo de documento + */ + delete: (id: number) => { + return api.delete(`${BASE_URL}/${id}`); + } +}; diff --git a/frontend/src/lib/components/dashboard/pedimentos/edit/accounts-compensation-tab-form.svelte b/frontend/src/lib/components/dashboard/pedimentos/edit/accounts-compensation-tab-form.svelte index 7008c275..f0b432e5 100644 --- a/frontend/src/lib/components/dashboard/pedimentos/edit/accounts-compensation-tab-form.svelte +++ b/frontend/src/lib/components/dashboard/pedimentos/edit/accounts-compensation-tab-form.svelte @@ -544,7 +544,12 @@ - Cuentas Aduaneras a nivel Pedimento + + {editingCuentaIndex !== null ? 'Cuentas Aduaneras a nivel Pedimento' : 'Insertando'} + + {#if editingCuentaIndex === null} +

Cuentas Aduaneras a nivel Pedimento

+ {/if}
diff --git a/frontend/src/lib/components/dashboard/pedimentos/edit/contributions-tab-form.svelte b/frontend/src/lib/components/dashboard/pedimentos/edit/contributions-tab-form.svelte index 90143061..41ebc60c 100644 --- a/frontend/src/lib/components/dashboard/pedimentos/edit/contributions-tab-form.svelte +++ b/frontend/src/lib/components/dashboard/pedimentos/edit/contributions-tab-form.svelte @@ -578,7 +578,7 @@ - {editingIndex !== null ? 'Editar Contribución' : 'Nueva Contribución'} + {editingIndex !== null ? 'Editar Contribución' : 'Insertando'} diff --git a/frontend/src/lib/components/dashboard/pedimentos/edit/digitization-tab-form.svelte b/frontend/src/lib/components/dashboard/pedimentos/edit/digitization-tab-form.svelte index 689ee413..992a7278 100644 --- a/frontend/src/lib/components/dashboard/pedimentos/edit/digitization-tab-form.svelte +++ b/frontend/src/lib/components/dashboard/pedimentos/edit/digitization-tab-form.svelte @@ -33,15 +33,16 @@ ChevronRight, ChevronsRight } from 'lucide-svelte'; + import { documentTypesDigitizationApi, type DocumentTypeDigitization } from '$lib/api/dashboard/refrence_data/document_types_digitization'; interface Digitalizacion { id?: number; linea: number; - clave: string; - documento: string; - e_document: string; - operacion: string; + tipo_documento: string; + ruta_archivo_pdf: string; observaciones: string; + e_document: string; + numero_operacion_vu: string; } let { @@ -57,6 +58,51 @@ // Estados para diálogo let isDialogOpen = $state(false); let editingIndex = $state(null); + let isTipoDocumentoDialogOpen = $state(false); + let isNuevoTipoDocumentoDialogOpen = $state(false); + + // Tipos de documentos disponibles (cargados desde el backend) + let tiposDocumentos = $state([]); + let isLoadingTiposDocumentos = $state(false); + let errorLoadingTiposDocumentos = $state(null); + + // Formulario para nuevo tipo de documento + let nuevoTipoDocumento = $state({ + code: '', + description: '', + active: true + }); + + // Cargar tipos de documentos desde el backend + async function cargarTiposDocumentos() { + try { + isLoadingTiposDocumentos = true; + errorLoadingTiposDocumentos = null; + const response = await documentTypesDigitizationApi.getAll(true); + tiposDocumentos = response.data || []; + } catch (error) { + console.error('Error al cargar tipos de documentos:', error); + errorLoadingTiposDocumentos = 'Error al cargar los tipos de documentos'; + // Si falla, usar array vacío para que el componente siga funcionando + tiposDocumentos = []; + } finally { + isLoadingTiposDocumentos = false; + } + } + + // Filtro de búsqueda para tipos de documentos + let tipoDocumentoBusqueda = $state(''); + + // Tipos de documentos filtrados + const tiposDocumentosFiltrados = $derived( + tipoDocumentoBusqueda.trim() === '' + ? tiposDocumentos + : tiposDocumentos.filter( + (td) => + td.code.toLowerCase().includes(tipoDocumentoBusqueda.toLowerCase()) || + td.description.toLowerCase().includes(tipoDocumentoBusqueda.toLowerCase()) + ) + ); // Paginación let currentPage = $state(0); @@ -64,13 +110,16 @@ let currentDigitalizacion = $state({ linea: 0, - clave: '', - documento: '', + tipo_documento: '', + ruta_archivo_pdf: '', + observaciones: '', e_document: '', - operacion: '', - observaciones: '' + numero_operacion_vu: '' }); + // Referencias para los inputs de archivo + let fileInputRef: HTMLInputElement; + const totalPages = $derived( Math.ceil((formData?.digitalizaciones?.length ?? 0) / pageSize) ); @@ -83,19 +132,23 @@ ); function openNewDigitalizacion() { + console.log('🟢 INICIO openNewDigitalizacion - isDialogOpen:', isDialogOpen); editingIndex = null; const nextLinea = formData?.digitalizaciones?.length ? Math.max(...formData.digitalizaciones.map((d) => d.linea)) + 1 : 1; currentDigitalizacion = { linea: nextLinea, - clave: '', - documento: '', + tipo_documento: '', + ruta_archivo_pdf: '', + observaciones: '', e_document: '', - operacion: '', - observaciones: '' + numero_operacion_vu: '' }; + console.log('🔵 Abriendo dialog de digitalización, isDialogOpen antes:', isDialogOpen); isDialogOpen = true; + console.log('🔵 isDialogOpen después:', isDialogOpen); + console.log('🟢 FIN openNewDigitalizacion'); } function openEditDigitalizacion(index: number) { @@ -157,6 +210,45 @@ alert('Cargar E-Document no implementado aún'); } + async function seleccionarRutaArchivo() { + try { + // Usar la File System Access API si está disponible + if ('showSaveFilePicker' in window) { + // @ts-ignore - API moderna del navegador + const handle = await window.showSaveFilePicker({ + suggestedName: 'documento.pdf', + types: [ + { + description: 'PDF Files', + accept: { 'application/pdf': ['.pdf'] } + } + ] + }); + + // Obtener la ruta (nombre del archivo seleccionado) + currentDigitalizacion.ruta_archivo_pdf = handle.name; + } else { + // Fallback para navegadores que no soportan la API + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.pdf'; + + input.onchange = (e: Event) => { + const target = e.target as HTMLInputElement; + if (target.files && target.files.length > 0) { + const file = target.files[0]; + currentDigitalizacion.ruta_archivo_pdf = file.name; + } + }; + + input.click(); + } + } catch (error) { + // Usuario canceló la selección + console.log('Selección de archivo cancelada'); + } + } + function goToFirstPage() { currentPage = 0; } @@ -172,6 +264,48 @@ function goToLastPage() { currentPage = totalPages - 1; } + + function abrirTiposDocumentos() { + tipoDocumentoBusqueda = ''; + // Cargar los tipos de documentos si aún no se han cargado + if (tiposDocumentos.length === 0 && !isLoadingTiposDocumentos) { + cargarTiposDocumentos(); + } + isTipoDocumentoDialogOpen = true; + } + + function seleccionarTipoDocumento(code: string) { + currentDigitalizacion.tipo_documento = code; + isTipoDocumentoDialogOpen = false; + } + + function abrirNuevoTipoDocumento() { + nuevoTipoDocumento = { + code: '', + description: '', + active: true + }; + isNuevoTipoDocumentoDialogOpen = true; + } + + async function guardarNuevoTipoDocumento() { + try { + const response = await documentTypesDigitizationApi.create(nuevoTipoDocumento); + if (response.data) { + // Agregar el nuevo tipo a la lista + tiposDocumentos = [...tiposDocumentos, response.data]; + // Seleccionar el nuevo tipo + currentDigitalizacion.tipo_documento = response.data.code; + // Cerrar ambos dialogs + isNuevoTipoDocumentoDialogOpen = false; + isTipoDocumentoDialogOpen = false; + } + } catch (error) { + console.error('Error al crear tipo de documento:', error); + alert('Error al crear el tipo de documento'); + } + } + @@ -182,11 +316,11 @@ Línea - Clave - Documento - E-Document - Operación - Observaciones + Tipo Documento + Ruta Archivo PDF + E-Document + Núm. Operación V.U + Observaciones Acciones @@ -207,10 +341,10 @@ {#each paginatedDigitalizaciones as digitalizacion, index} {digitalizacion.linea} - {digitalizacion.clave} - {digitalizacion.documento} + {digitalizacion.tipo_documento} + {digitalizacion.ruta_archivo_pdf} {digitalizacion.e_document} - {digitalizacion.operacion} + {digitalizacion.numero_operacion_vu} {digitalizacion.observaciones} @@ -338,76 +472,224 @@ - + - - {editingIndex !== null ? 'Editar Digitalización' : 'Nueva Digitalización'} - + Digitalización {currentDigitalizacion.linea}
-
-
- +
+ + +
+ +
+ +
-
-
- - +
- - + +
+ + +
- - -
- -
- - -
- -
- +