From 7e9c8397ef4df7d396cb1f504ceccb24601ba737 Mon Sep 17 00:00:00 2001
From: acazares
Date: Tue, 9 Dec 2025 16:00:46 -0600
Subject: [PATCH 01/15] feat: Implement invoice management module with CRUD
operations
- Added InvoiceHeader, InvoiceComplianceMx, InvoiceFinancials, InvoiceLogistics, InvoiceSalesDetails, and InvoiceCollections models.
- Created schemas for invoice operations including create, update, and response schemas.
- Developed services for handling business logic related to invoices, including retrieval, creation, updating, and deletion of invoices and their related data.
- Introduced routes for invoice management, enabling CRUD operations through a RESTful API.
- Integrated invoice routes into the main application router.
- Removed unused routers from the core module to streamline the API structure.
---
backend/api/v1/modules/a76/invoices/models.py | 169 ++++++++
backend/api/v1/modules/a76/invoices/routes.py | 283 ++++++++++++++
.../api/v1/modules/a76/invoices/schemas.py | 257 ++++++++++++
.../api/v1/modules/a76/invoices/services.py | 369 ++++++++++++++++++
backend/api/v1/modules/a76/router.py | 11 +-
backend/api/v1/modules/core/router.py | 12 +
backend/api/v1/router.py | 2 +
7 files changed, 1094 insertions(+), 9 deletions(-)
create mode 100644 backend/api/v1/modules/a76/invoices/models.py
create mode 100644 backend/api/v1/modules/a76/invoices/routes.py
create mode 100644 backend/api/v1/modules/a76/invoices/schemas.py
create mode 100644 backend/api/v1/modules/a76/invoices/services.py
create mode 100644 backend/api/v1/modules/core/router.py
diff --git a/backend/api/v1/modules/a76/invoices/models.py b/backend/api/v1/modules/a76/invoices/models.py
new file mode 100644
index 00000000..8c0feee9
--- /dev/null
+++ b/backend/api/v1/modules/a76/invoices/models.py
@@ -0,0 +1,169 @@
+from typing import Optional, List
+from sqlalchemy import BigInteger, Date, ForeignKey, Integer, Numeric, String, Text, TIMESTAMP
+from sqlalchemy.orm import Mapped, mapped_column, relationship
+from core.database import Base
+from datetime import datetime
+from ....common.base_models import TenantScopedMixin, TimestampMixin
+
+# --- 1. Invoice Header (invoice_header) ---
+class InvoiceHeader(Base, TenantScopedMixin, TimestampMixin):
+ __tablename__ = "invoice_header"
+ __tableargs__ = {'schema': 'a76'}
+
+ id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
+
+ # Identifiers
+ legacy_id: Mapped[Optional[int]] = mapped_column(Integer) # CONSECUTIVO
+ operation_type: Mapped[str] = mapped_column(String(20)) # TIPOMOVIMIENTO / Clasifica IMP/EXP/SM
+ invoice_number: Mapped[Optional[str]] = mapped_column(String(20)) # FACTURAIMPO/FACTURAEXPO/FACTURAREMISION
+ project_number: Mapped[Optional[str]] = mapped_column(String(14)) # NUMPROYECTO
+ purchase_order: Mapped[Optional[str]] = mapped_column(String(50)) # ORDENCOMPRA
+ related_doc_id: Mapped[Optional[int]] = mapped_column(Integer) # IDRELDOC (Para Rectificaciones)
+
+ # Dates
+ invoice_date: Mapped[Optional[datetime]] = mapped_column(Date) # FECHAFACTURA
+ capture_date: Mapped[datetime] = mapped_column(TIMESTAMP(timezone=False), default=datetime.now) # FECHACAPTURA + HORAACTUAL
+
+ # Status & Control
+ status: Mapped[Optional[str]] = mapped_column(String(5)) # ESTATUS
+ traffic_light_status: Mapped[Optional[str]] = mapped_column(String(50)) # SEMAFORO / SEMAFOROEXPO/IMPO
+ process_log: Mapped[Optional[str]] = mapped_column(String(300)) # COMOFUEPROCESADA **<-- AÑADIDO**
+ comments: Mapped[Optional[str]] = mapped_column(Text) # OBSERVACIONE + OBSERVACIONI + COMENTARIOSESTATUS
+
+ # Digital Archive Links
+ cfdi_uuid: Mapped[Optional[str]] = mapped_column(String(100)) # CFDIUUID
+ path_pdf: Mapped[Optional[str]] = mapped_column(String(500)) # CFDIPATHPDF **<-- AÑADIDO**
+ path_xml: Mapped[Optional[str]] = mapped_column(String(500)) # CFDIPATHXML **<-- AÑADIDO**
+
+ # Relationships (Para navegacion ORM)
+ compliance_mx: Mapped["InvoiceComplianceMx"] = relationship(back_populates="header", cascade="all, delete-orphan")
+ financials: Mapped["InvoiceFinancials"] = relationship(back_populates="header", cascade="all, delete-orphan")
+ details: Mapped[List["InvoiceSalesDetails"]] = relationship(back_populates="header", cascade="all, delete-orphan")
+ collections: Mapped[List["InvoiceCollections"]] = relationship(back_populates="header", cascade="all, delete-orphan")
+ logistics: Mapped[List["InvoiceLogistics"]] = relationship(back_populates="header", cascade="all, delete-orphan")
+
+
+# --- 2. Compliance MX (invoice_compliance_mx) ---
+class InvoiceComplianceMx(Base, TenantScopedMixin, TimestampMixin):
+ __tablename__ = "invoice_compliance_mx"
+ __tableargs__ = {'schema': 'a76'}
+
+ invoice_id: Mapped[int] = mapped_column(ForeignKey("invoice_header.id"), primary_key=True)
+
+ # Core Customs Data
+ pedimento: Mapped[Optional[str]] = mapped_column(String(19)) # PEDIMENTO/PEDIMENTOIMPO/EXPO
+ pedimento_code: Mapped[Optional[str]] = mapped_column(String(5)) # PEDIMENTOR1, K1
+ remesa: Mapped[Optional[int]] = mapped_column(Integer)
+ aduana: Mapped[Optional[str]] = mapped_column(String(5)) # ADUANA_CRUCE
+ customs_agent: Mapped[Optional[str]] = mapped_column(String(10)) # AADUANAL
+
+ # Flags & Specific Regimes
+ is_mixed: Mapped[Optional[str]] = mapped_column(String(2)) # ESMIXTO **<-- AÑADIDO**
+ waste_type: Mapped[Optional[str]] = mapped_column(String(1)) # TIPODESPERDICIO **<-- AÑADIDO**
+ appendix_17: Mapped[Optional[int]] = mapped_column(Integer) # APENDICE17
+
+ # VUCEM / Digital
+ edocument: Mapped[Optional[str]] = mapped_column(String(50)) # EDOCUMENT
+ electronic_signature: Mapped[Optional[str]] = mapped_column(String(999)) # FIRMAELECTRONICA
+ sem_id: Mapped[Optional[int]] = mapped_column(Integer) # SEM (de SFacEntradaSM/SFacSalidaSM)
+
+ # Relationship
+ header: Mapped["InvoiceHeader"] = relationship(back_populates="compliance_mx")
+
+
+# --- 3. Financials (invoice_financials) ---
+class InvoiceFinancials(Base, TenantScopedMixin, TimestampMixin):
+ __tablename__ = "invoice_financials"
+ __tableargs__ = {'schema': 'a76'}
+
+ invoice_id: Mapped[int] = mapped_column(ForeignKey("invoice_header.id"), primary_key=True)
+
+ currency: Mapped[Optional[str]] = mapped_column(String(3)) # CLAVEMONEDA
+ exchange_rate: Mapped[Optional[float]] = mapped_column(Numeric(13, 6)) # TIPOCAMBIO
+
+ # Merchandise Values
+ value_mn: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # VALORIMPOMN/EXPOMN
+ value_me: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0)
+ customs_value_mn: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # VALORADUANASMN **<-- AÑADIDO**
+
+ # Costs & Taxes
+ freight: Mapped[Optional[float]] = mapped_column(Numeric(19, 8), default=0) # FLETE
+ insurance: Mapped[Optional[float]] = mapped_column(Numeric(19, 8), default=0) # VALSEGUROS/SEGUROS
+ iva_mn: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # IVAEXPOMN / VALORIVAMN
+ iva_factor: Mapped[Optional[float]] = mapped_column(Numeric(17, 4)) # FACTORIVA
+
+ # Weights & Quantities
+ total_quantity: Mapped[Optional[float]] = mapped_column(Numeric(19, 8)) # CANTEXPO / CANTIMPO
+ gross_weight: Mapped[Optional[float]] = mapped_column(Numeric(19, 8)) # PESOBRUTO **<-- AÑADIDO**
+ net_weight: Mapped[Optional[float]] = mapped_column(Numeric(19, 8)) # PESONETO **<-- AÑADIDO**
+ bundle_count: Mapped[Optional[int]] = mapped_column(Integer) # CANTBULTOS
+
+ # Relationship
+ header: Mapped["InvoiceHeader"] = relationship(back_populates="financials")
+
+
+# --- 4. Logistics (invoice_logistics) ---
+class InvoiceLogistics(Base, TenantScopedMixin, TimestampMixin):
+ __tablename__ = "invoice_logistics"
+ __tableargs__ = {'schema': 'a76'}
+
+ logistics_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
+ invoice_id: Mapped[int] = mapped_column(ForeignKey("invoice_header.id"))
+
+ # Carrier Info
+ carrier_id: Mapped[Optional[str]] = mapped_column(String(10)) # TRANSPORTISTA
+ transport_mode: Mapped[Optional[str]] = mapped_column(String(15)) # TRANSPORTE
+ driver_name: Mapped[Optional[str]] = mapped_column(String(80)) # CONDUCTOR
+ is_rail: Mapped[Optional[str]] = mapped_column(String(2)) # ESFERROCARRIL
+ rail_id: Mapped[Optional[str]] = mapped_column(String(31)) # IDFERRORCARRIL
+
+ # Vehicle & Tracking
+ vehicle_num: Mapped[Optional[str]] = mapped_column(String(20)) # NUMVEHICULO / NUMTRAILER
+ license_plate: Mapped[Optional[str]] = mapped_column(String(20)) # NUMTRASPORTE
+ seal_number: Mapped[Optional[str]] = mapped_column(String(15)) # PRECINTO
+ guide_number: Mapped[Optional[str]] = mapped_column(String(20)) # NUMEROGUIA
+
+ # Logistics Dates
+ entry_exit_date: Mapped[Optional[datetime]] = mapped_column(Date) # FECHAENTRADA / FECHAENVIO
+
+ # Relationship
+ header: Mapped["InvoiceHeader"] = relationship(back_populates="logistics")
+
+
+# --- 5. Sales Order Details (invoice_sales_details) ---
+class InvoiceSalesDetails(Base, TenantScopedMixin, TimestampMixin):
+ __tablename__ = "invoice_sales_details"
+ __tableargs__ = {'schema': 'a76'}
+
+ detail_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
+ invoice_id: Mapped[int] = mapped_column(ForeignKey("invoice_header.id"))
+
+ line_number: Mapped[int] = mapped_column(Integer) # LINEA
+ sales_order: Mapped[Optional[str]] = mapped_column(String(20)) # ORDENVENTA
+
+ # Specific Custom Fields
+ colors_description: Mapped[Optional[str]] = mapped_column(String(49)) # COLORES
+ square_color_code: Mapped[Optional[str]] = mapped_column(String(1)) # COLORCUADRITO
+ line_bundles: Mapped[Optional[int]] = mapped_column(Integer) # CANTBULTOS (de la linea)
+
+ # Relationship
+ header: Mapped["InvoiceHeader"] = relationship(back_populates="details")
+
+
+# --- 6. Collections (invoice_collections) ---
+class InvoiceCollections(Base, TenantScopedMixin, TimestampMixin):
+ __tablename__ = "invoice_collections"
+ __tableargs__ = {'schema': 'a76'}
+
+ collection_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
+ invoice_id: Mapped[int] = mapped_column(ForeignKey("invoice_header.id"))
+
+ concept: Mapped[Optional[str]] = mapped_column(String(100)) # CONCEPTO
+ is_collected: Mapped[Optional[int]] = mapped_column(Integer) # COBRADO
+ collection_date: Mapped[Optional[datetime]] = mapped_column(Date) # FECHACOBRANZA
+ amount: Mapped[Optional[float]] = mapped_column(Numeric(23, 8)) # VALOR
+
+ collector_user: Mapped[Optional[str]] = mapped_column(String(20)) # FACTCOBRADOR
+
+ # Relationship
+ header: Mapped["InvoiceHeader"] = relationship(back_populates="collections")
\ No newline at end of file
diff --git a/backend/api/v1/modules/a76/invoices/routes.py b/backend/api/v1/modules/a76/invoices/routes.py
new file mode 100644
index 00000000..a2160a6b
--- /dev/null
+++ b/backend/api/v1/modules/a76/invoices/routes.py
@@ -0,0 +1,283 @@
+from typing import Dict, Any
+from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
+from core.database import get_core_db
+from core.security import get_current_user, validate_access_to_resource
+from fastapi import APIRouter, Depends, HTTPException, Query, Path
+from sqlalchemy.orm import Session
+
+from . import schemas, services
+
+# Create main router
+router = APIRouter()
+
+# Create CRUD routes for Invoice Header using TenantCRUDRoutes
+invoice_crud = TenantCRUDRoutes(
+ service=services.InvoiceService,
+ create_schema=schemas.InvoiceHeaderCreate,
+ update_schema=schemas.InvoiceHeaderUpdate,
+ response_schema=schemas.InvoiceHeaderResponse,
+ prefix="/invoices",
+ tags=[],
+ resource_name="Invoice",
+ id_name="invoice_id",
+ id_type=int,
+ enable_list=True, # Enable list endpoint with pagination
+ enable_filters=True, # Enable filters for status, operation_type, etc.
+ default_page_size=50,
+ max_page_size=200,
+)
+
+# Include the main CRUD routes
+router.include_router(invoice_crud.router)
+
+
+# Additional nested routes for child resources
+
+# --- Logistics Routes ---
+
+@router.get(
+ "/invoices/{invoice_id}/logistics",
+ response_model=list[schemas.InvoiceLogisticsResponse],
+ summary="Get all logistics for an invoice",
+)
+def get_invoice_logistics(
+ invoice_id: int = Path(..., description="Invoice ID"),
+ company_id: int = Query(..., description="Company ID"),
+ db: Session = Depends(get_core_db),
+ current_user: Dict[str, Any] = Depends(get_current_user),
+):
+ """Get all logistics entries for a specific invoice"""
+ tenant_id = validate_access_to_resource(db, company_id, current_user)
+
+ # Verify the invoice exists and belongs to the tenant/company
+ invoice = services.InvoiceService.get_by_id(
+ db, invoice_id, tenant_id, company_id)
+ if not invoice:
+ raise HTTPException(status_code=404, detail="Invoice not found")
+
+ logistics = services.InvoiceLogisticsService.get_all_by_invoice(
+ db, invoice_id)
+ return logistics
+
+
+@router.post(
+ "/invoices/{invoice_id}/logistics",
+ response_model=schemas.InvoiceLogisticsResponse,
+ status_code=201,
+ summary="Add logistics to an invoice",
+)
+def create_invoice_logistics(
+ invoice_id: int = Path(..., description="Invoice ID"),
+ logistics_data: schemas.InvoiceLogisticsCreate = ...,
+ company_id: int = Query(..., description="Company ID"),
+ db: Session = Depends(get_core_db),
+ current_user: Dict[str, Any] = Depends(get_current_user),
+):
+ """Add a new logistics entry to an invoice"""
+ tenant_id = validate_access_to_resource(db, company_id, current_user)
+
+ # Verify the invoice exists and belongs to the tenant/company
+ invoice = services.InvoiceService.get_by_id(
+ db, invoice_id, tenant_id, company_id)
+ if not invoice:
+ raise HTTPException(status_code=404, detail="Invoice not found")
+
+ logistics = services.InvoiceLogisticsService.create(
+ db, logistics_data, invoice_id, tenant_id, company_id)
+ return logistics
+
+
+@router.delete(
+ "/invoices/{invoice_id}/logistics/{logistics_id}",
+ status_code=204,
+ summary="Delete logistics from an invoice",
+)
+def delete_invoice_logistics(
+ invoice_id: int = Path(..., description="Invoice ID"),
+ logistics_id: int = Path(..., description="Logistics ID"),
+ company_id: int = Query(..., description="Company ID"),
+ db: Session = Depends(get_core_db),
+ current_user: Dict[str, Any] = Depends(get_current_user),
+):
+ """Delete a logistics entry from an invoice"""
+ tenant_id = validate_access_to_resource(db, company_id, current_user)
+
+ # Verify the invoice exists and belongs to the tenant/company
+ invoice = services.InvoiceService.get_by_id(
+ db, invoice_id, tenant_id, company_id)
+ if not invoice:
+ raise HTTPException(status_code=404, detail="Invoice not found")
+
+ success = services.InvoiceLogisticsService.delete(
+ db, logistics_id, invoice_id)
+ if not success:
+ raise HTTPException(
+ status_code=404, detail="Logistics entry not found")
+
+ return None
+
+
+# --- Sales Details Routes ---
+
+@router.get(
+ "/invoices/{invoice_id}/details",
+ response_model=list[schemas.InvoiceSalesDetailsResponse],
+ summary="Get all sales details for an invoice",
+)
+def get_invoice_details(
+ invoice_id: int = Path(..., description="Invoice ID"),
+ company_id: int = Query(..., description="Company ID"),
+ db: Session = Depends(get_core_db),
+ current_user: Dict[str, Any] = Depends(get_current_user),
+):
+ """Get all sales details for a specific invoice"""
+ tenant_id = validate_access_to_resource(db, company_id, current_user)
+
+ # Verify the invoice exists and belongs to the tenant/company
+ invoice = services.InvoiceService.get_by_id(
+ db, invoice_id, tenant_id, company_id)
+ if not invoice:
+ raise HTTPException(status_code=404, detail="Invoice not found")
+
+ details = services.InvoiceSalesDetailsService.get_all_by_invoice(
+ db, invoice_id)
+ return details
+
+
+@router.post(
+ "/invoices/{invoice_id}/details",
+ response_model=schemas.InvoiceSalesDetailsResponse,
+ status_code=201,
+ summary="Add sales detail to an invoice",
+)
+def create_invoice_detail(
+ invoice_id: int = Path(..., description="Invoice ID"),
+ detail_data: schemas.InvoiceSalesDetailsCreate = ...,
+ company_id: int = Query(..., description="Company ID"),
+ db: Session = Depends(get_core_db),
+ current_user: Dict[str, Any] = Depends(get_current_user),
+):
+ """Add a new sales detail to an invoice"""
+ tenant_id = validate_access_to_resource(db, company_id, current_user)
+
+ # Verify the invoice exists and belongs to the tenant/company
+ invoice = services.InvoiceService.get_by_id(
+ db, invoice_id, tenant_id, company_id)
+ if not invoice:
+ raise HTTPException(status_code=404, detail="Invoice not found")
+
+ detail = services.InvoiceSalesDetailsService.create(
+ db, detail_data, invoice_id, tenant_id, company_id)
+ return detail
+
+
+@router.delete(
+ "/invoices/{invoice_id}/details/{detail_id}",
+ status_code=204,
+ summary="Delete sales detail from an invoice",
+)
+def delete_invoice_detail(
+ invoice_id: int = Path(..., description="Invoice ID"),
+ detail_id: int = Path(..., description="Detail ID"),
+ company_id: int = Query(..., description="Company ID"),
+ db: Session = Depends(get_core_db),
+ current_user: Dict[str, Any] = Depends(get_current_user),
+):
+ """Delete a sales detail from an invoice"""
+ tenant_id = validate_access_to_resource(db, company_id, current_user)
+
+ # Verify the invoice exists and belongs to the tenant/company
+ invoice = services.InvoiceService.get_by_id(
+ db, invoice_id, tenant_id, company_id)
+ if not invoice:
+ raise HTTPException(status_code=404, detail="Invoice not found")
+
+ success = services.InvoiceSalesDetailsService.delete(
+ db, detail_id, invoice_id)
+ if not success:
+ raise HTTPException(status_code=404, detail="Sales detail not found")
+
+ return None
+
+
+# --- Collections Routes ---
+
+@router.get(
+ "/invoices/{invoice_id}/collections",
+ response_model=list[schemas.InvoiceCollectionsResponse],
+ summary="Get all collections for an invoice",
+)
+def get_invoice_collections(
+ invoice_id: int = Path(..., description="Invoice ID"),
+ company_id: int = Query(..., description="Company ID"),
+ db: Session = Depends(get_core_db),
+ current_user: Dict[str, Any] = Depends(get_current_user),
+):
+ """Get all collections for a specific invoice"""
+ tenant_id = validate_access_to_resource(db, company_id, current_user)
+
+ # Verify the invoice exists and belongs to the tenant/company
+ invoice = services.InvoiceService.get_by_id(
+ db, invoice_id, tenant_id, company_id)
+ if not invoice:
+ raise HTTPException(status_code=404, detail="Invoice not found")
+
+ collections = services.InvoiceCollectionsService.get_all_by_invoice(
+ db, invoice_id)
+ return collections
+
+
+@router.post(
+ "/invoices/{invoice_id}/collections",
+ response_model=schemas.InvoiceCollectionsResponse,
+ status_code=201,
+ summary="Add collection to an invoice",
+)
+def create_invoice_collection(
+ invoice_id: int = Path(..., description="Invoice ID"),
+ collection_data: schemas.InvoiceCollectionsCreate = ...,
+ company_id: int = Query(..., description="Company ID"),
+ db: Session = Depends(get_core_db),
+ current_user: Dict[str, Any] = Depends(get_current_user),
+):
+ """Add a new collection to an invoice"""
+ tenant_id = validate_access_to_resource(db, company_id, current_user)
+
+ # Verify the invoice exists and belongs to the tenant/company
+ invoice = services.InvoiceService.get_by_id(
+ db, invoice_id, tenant_id, company_id)
+ if not invoice:
+ raise HTTPException(status_code=404, detail="Invoice not found")
+
+ collection = services.InvoiceCollectionsService.create(
+ db, collection_data, invoice_id, tenant_id, company_id)
+ return collection
+
+
+@router.delete(
+ "/invoices/{invoice_id}/collections/{collection_id}",
+ status_code=204,
+ summary="Delete collection from an invoice",
+)
+def delete_invoice_collection(
+ invoice_id: int = Path(..., description="Invoice ID"),
+ collection_id: int = Path(..., description="Collection ID"),
+ company_id: int = Query(..., description="Company ID"),
+ db: Session = Depends(get_core_db),
+ current_user: Dict[str, Any] = Depends(get_current_user),
+):
+ """Delete a collection from an invoice"""
+ tenant_id = validate_access_to_resource(db, company_id, current_user)
+
+ # Verify the invoice exists and belongs to the tenant/company
+ invoice = services.InvoiceService.get_by_id(
+ db, invoice_id, tenant_id, company_id)
+ if not invoice:
+ raise HTTPException(status_code=404, detail="Invoice not found")
+
+ success = services.InvoiceCollectionsService.delete(
+ db, collection_id, invoice_id)
+ if not success:
+ raise HTTPException(status_code=404, detail="Collection not found")
+
+ return None
diff --git a/backend/api/v1/modules/a76/invoices/schemas.py b/backend/api/v1/modules/a76/invoices/schemas.py
new file mode 100644
index 00000000..a9b01b3b
--- /dev/null
+++ b/backend/api/v1/modules/a76/invoices/schemas.py
@@ -0,0 +1,257 @@
+from typing import Optional, List
+from datetime import datetime, date
+from decimal import Decimal
+from pydantic import BaseModel, Field
+
+
+# --- Base Schemas ---
+
+class InvoiceHeaderBase(BaseModel):
+ """Base fields for Invoice Header"""
+ legacy_id: Optional[int] = Field(
+ None, description="Legacy consecutive ID (CONSECUTIVO)")
+ operation_type: Optional[str] = Field(
+ None, max_length=20, description="Operation type: IMP/EXP/SM")
+ invoice_number: Optional[str] = Field(
+ None, max_length=20, description="Invoice number")
+ project_number: Optional[str] = Field(
+ None, max_length=14, description="Project number")
+ purchase_order: Optional[str] = Field(
+ None, max_length=50, description="Purchase order")
+ related_doc_id: Optional[int] = Field(
+ None, description="Related document ID for rectifications")
+ invoice_date: Optional[date] = Field(None, description="Invoice date")
+ status: Optional[str] = Field(None, max_length=5, description="Status")
+ traffic_light_status: Optional[str] = Field(
+ None, max_length=50, description="Traffic light status (SEMAFORO)")
+ process_log: Optional[str] = Field(
+ None, max_length=300, description="Processing log")
+ comments: Optional[str] = Field(
+ None, description="Comments and observations")
+ cfdi_uuid: Optional[str] = Field(
+ None, max_length=100, description="CFDI UUID")
+ path_pdf: Optional[str] = Field(
+ None, max_length=500, description="Path to PDF file")
+ path_xml: Optional[str] = Field(
+ None, max_length=500, description="Path to XML file")
+
+
+class InvoiceComplianceMxBase(BaseModel):
+ """Base fields for Compliance MX"""
+ pedimento: Optional[str] = Field(
+ None, max_length=19, description="Pedimento number")
+ pedimento_code: Optional[str] = Field(
+ None, max_length=5, description="Pedimento code (R1/K1)")
+ remesa: Optional[int] = Field(None, description="Remesa")
+ aduana: Optional[str] = Field(
+ None, max_length=5, description="Customs office")
+ customs_agent: Optional[str] = Field(
+ None, max_length=10, description="Customs agent")
+ is_mixed: Optional[str] = Field(
+ None, max_length=2, description="Is mixed operation")
+ waste_type: Optional[str] = Field(
+ None, max_length=1, description="Waste type")
+ appendix_17: Optional[int] = Field(None, description="Appendix 17")
+ edocument: Optional[str] = Field(
+ None, max_length=50, description="E-document")
+ electronic_signature: Optional[str] = Field(
+ None, max_length=999, description="Electronic signature")
+ sem_id: Optional[int] = Field(None, description="SEM ID")
+
+
+class InvoiceFinancialsBase(BaseModel):
+ """Base fields for Financials"""
+ currency: Optional[str] = Field(
+ None, max_length=3, description="Currency code")
+ exchange_rate: Optional[Decimal] = Field(None, description="Exchange rate")
+ value_mn: Optional[Decimal] = Field(None, description="Value in MXN")
+ value_me: Optional[Decimal] = Field(
+ None, description="Value in foreign currency")
+ customs_value_mn: Optional[Decimal] = Field(
+ None, description="Customs value in MXN")
+ freight: Optional[Decimal] = Field(None, description="Freight cost")
+ insurance: Optional[Decimal] = Field(None, description="Insurance cost")
+ iva_mn: Optional[Decimal] = Field(None, description="IVA in MXN")
+ iva_factor: Optional[Decimal] = Field(None, description="IVA factor")
+ total_quantity: Optional[Decimal] = Field(
+ None, description="Total quantity")
+ gross_weight: Optional[Decimal] = Field(None, description="Gross weight")
+ net_weight: Optional[Decimal] = Field(None, description="Net weight")
+ bundle_count: Optional[int] = Field(None, description="Bundle count")
+
+
+class InvoiceLogisticsBase(BaseModel):
+ """Base fields for Logistics"""
+ carrier_id: Optional[str] = Field(
+ None, max_length=10, description="Carrier ID")
+ transport_mode: Optional[str] = Field(
+ None, max_length=15, description="Transport mode")
+ driver_name: Optional[str] = Field(
+ None, max_length=80, description="Driver name")
+ is_rail: Optional[str] = Field(
+ None, max_length=2, description="Is rail transport")
+ rail_id: Optional[str] = Field(None, max_length=31, description="Rail ID")
+ vehicle_num: Optional[str] = Field(
+ None, max_length=20, description="Vehicle number")
+ license_plate: Optional[str] = Field(
+ None, max_length=20, description="License plate")
+ seal_number: Optional[str] = Field(
+ None, max_length=15, description="Seal number")
+ guide_number: Optional[str] = Field(
+ None, max_length=20, description="Guide number")
+ entry_exit_date: Optional[date] = Field(
+ None, description="Entry/Exit date")
+
+
+class InvoiceSalesDetailsBase(BaseModel):
+ """Base fields for Sales Details"""
+ line_number: int = Field(..., description="Line number")
+ sales_order: Optional[str] = Field(
+ None, max_length=20, description="Sales order")
+ colors_description: Optional[str] = Field(
+ None, max_length=49, description="Colors description")
+ square_color_code: Optional[str] = Field(
+ None, max_length=1, description="Square color code")
+ line_bundles: Optional[int] = Field(None, description="Line bundles count")
+
+
+class InvoiceCollectionsBase(BaseModel):
+ """Base fields for Collections"""
+ concept: Optional[str] = Field(None, max_length=100, description="Concept")
+ is_collected: Optional[int] = Field(None, description="Is collected flag")
+ collection_date: Optional[date] = Field(
+ None, description="Collection date")
+ amount: Optional[Decimal] = Field(None, description="Amount")
+ collector_user: Optional[str] = Field(
+ None, max_length=20, description="Collector user")
+
+
+# --- Create Schemas ---
+
+class InvoiceComplianceMxCreate(InvoiceComplianceMxBase):
+ """Schema for creating Compliance MX"""
+ pass
+
+
+class InvoiceFinancialsCreate(InvoiceFinancialsBase):
+ """Schema for creating Financials"""
+ pass
+
+
+class InvoiceLogisticsCreate(InvoiceLogisticsBase):
+ """Schema for creating Logistics"""
+ pass
+
+
+class InvoiceSalesDetailsCreate(InvoiceSalesDetailsBase):
+ """Schema for creating Sales Details"""
+ pass
+
+
+class InvoiceCollectionsCreate(InvoiceCollectionsBase):
+ """Schema for creating Collections"""
+ pass
+
+
+class InvoiceHeaderCreate(InvoiceHeaderBase):
+ """Schema for creating Invoice Header with nested relations"""
+ compliance_mx: Optional[InvoiceComplianceMxCreate] = None
+ financials: Optional[InvoiceFinancialsCreate] = None
+ logistics: Optional[List[InvoiceLogisticsCreate]] = None
+ details: Optional[List[InvoiceSalesDetailsCreate]] = None
+ collections: Optional[List[InvoiceCollectionsCreate]] = None
+
+
+# --- Update Schemas ---
+
+class InvoiceComplianceMxUpdate(InvoiceComplianceMxBase):
+ """Schema for updating Compliance MX"""
+ pass
+
+
+class InvoiceFinancialsUpdate(InvoiceFinancialsBase):
+ """Schema for updating Financials"""
+ pass
+
+
+class InvoiceLogisticsUpdate(InvoiceLogisticsBase):
+ """Schema for updating Logistics"""
+ pass
+
+
+class InvoiceSalesDetailsUpdate(InvoiceSalesDetailsBase):
+ """Schema for updating Sales Details"""
+ line_number: Optional[int] = None
+
+
+class InvoiceCollectionsUpdate(InvoiceCollectionsBase):
+ """Schema for updating Collections"""
+ pass
+
+
+class InvoiceHeaderUpdate(InvoiceHeaderBase):
+ """Schema for updating Invoice Header with nested relations"""
+ compliance_mx: Optional[InvoiceComplianceMxUpdate] = None
+ financials: Optional[InvoiceFinancialsUpdate] = None
+ logistics: Optional[List[InvoiceLogisticsUpdate]] = None
+ details: Optional[List[InvoiceSalesDetailsUpdate]] = None
+ collections: Optional[List[InvoiceCollectionsUpdate]] = None
+
+
+# --- Response Schemas ---
+
+class InvoiceComplianceMxResponse(InvoiceComplianceMxBase):
+ """Schema for Compliance MX response"""
+ invoice_id: int
+
+ class Config:
+ from_attributes = True
+
+
+class InvoiceFinancialsResponse(InvoiceFinancialsBase):
+ """Schema for Financials response"""
+ invoice_id: int
+
+ class Config:
+ from_attributes = True
+
+
+class InvoiceLogisticsResponse(InvoiceLogisticsBase):
+ """Schema for Logistics response"""
+ logistics_id: int
+ invoice_id: int
+
+ class Config:
+ from_attributes = True
+
+
+class InvoiceSalesDetailsResponse(InvoiceSalesDetailsBase):
+ """Schema for Sales Details response"""
+ detail_id: int
+ invoice_id: int
+
+ class Config:
+ from_attributes = True
+
+
+class InvoiceCollectionsResponse(InvoiceCollectionsBase):
+ """Schema for Collections response"""
+ collection_id: int
+ invoice_id: int
+
+ class Config:
+ from_attributes = True
+
+
+class InvoiceHeaderResponse(InvoiceHeaderBase):
+ """Schema for Invoice Header response with nested relations"""
+ id: int
+ capture_date: datetime
+ compliance_mx: Optional[InvoiceComplianceMxResponse] = None
+ financials: Optional[InvoiceFinancialsResponse] = None
+ logistics: List[InvoiceLogisticsResponse] = []
+ details: List[InvoiceSalesDetailsResponse] = []
+ collections: List[InvoiceCollectionsResponse] = []
+
+ class Config:
+ from_attributes = True
diff --git a/backend/api/v1/modules/a76/invoices/services.py b/backend/api/v1/modules/a76/invoices/services.py
new file mode 100644
index 00000000..c74fea63
--- /dev/null
+++ b/backend/api/v1/modules/a76/invoices/services.py
@@ -0,0 +1,369 @@
+from typing import Optional, List, Tuple
+from sqlalchemy.orm import Session
+from sqlalchemy import and_
+
+from . import models, schemas
+
+
+class InvoiceService:
+ """Service for Invoice Header operations"""
+
+ @staticmethod
+ def get_by_id(db: Session, invoice_id: int, tenant_id: int, company_id: int) -> Optional[models.InvoiceHeader]:
+ """Get an invoice by ID with tenant/company validation"""
+ return (
+ db.query(models.InvoiceHeader)
+ .filter(
+ models.InvoiceHeader.id == invoice_id,
+ models.InvoiceHeader.tenant_id == tenant_id,
+ models.InvoiceHeader.company_id == company_id,
+ )
+ .first()
+ )
+
+ @staticmethod
+ def get_all(
+ db: Session,
+ tenant_id: int,
+ company_id: int,
+ skip: int = 0,
+ limit: int = 100,
+ filters: Optional[dict] = None,
+ ) -> Tuple[List[models.InvoiceHeader], int]:
+ """Get all invoices for a tenant/company with pagination and optional filters"""
+ query = db.query(models.InvoiceHeader).filter(
+ models.InvoiceHeader.tenant_id == tenant_id,
+ models.InvoiceHeader.company_id == company_id,
+ )
+
+ # Apply filters if provided
+ if filters:
+ if filters.get("status"):
+ query = query.filter(
+ models.InvoiceHeader.status == filters["status"])
+ if filters.get("operation_type"):
+ query = query.filter(
+ models.InvoiceHeader.operation_type == filters["operation_type"])
+ if filters.get("invoice_number"):
+ query = query.filter(models.InvoiceHeader.invoice_number.ilike(
+ f"%{filters['invoice_number']}%"))
+ if filters.get("pedimento"):
+ query = query.join(models.InvoiceComplianceMx).filter(
+ models.InvoiceComplianceMx.pedimento.ilike(
+ f"%{filters['pedimento']}%")
+ )
+
+ total = query.count()
+ items = query.offset(skip).limit(limit).all()
+ return items, total
+
+ @staticmethod
+ def create(
+ db: Session,
+ invoice_data: schemas.InvoiceHeaderCreate,
+ tenant_id: int,
+ company_id: int
+ ) -> models.InvoiceHeader:
+ """Create a new invoice with all related data"""
+ # Extract nested data
+ compliance_data = invoice_data.compliance_mx
+ financials_data = invoice_data.financials
+ logistics_data = invoice_data.logistics or []
+ details_data = invoice_data.details or []
+ collections_data = invoice_data.collections or []
+
+ # Create main invoice header
+ invoice_dict = invoice_data.model_dump(
+ exclude={"compliance_mx", "financials",
+ "logistics", "details", "collections"}
+ )
+ invoice_dict["tenant_id"] = tenant_id
+ invoice_dict["company_id"] = company_id
+
+ new_invoice = models.InvoiceHeader(**invoice_dict)
+ db.add(new_invoice)
+ db.flush() # Flush to get the invoice ID
+
+ # Create compliance_mx if provided
+ if compliance_data:
+ compliance_dict = compliance_data.model_dump()
+ compliance_dict["invoice_id"] = new_invoice.id
+ compliance_dict["tenant_id"] = tenant_id
+ compliance_dict["company_id"] = company_id
+ new_compliance = models.InvoiceComplianceMx(**compliance_dict)
+ db.add(new_compliance)
+
+ # Create financials if provided
+ if financials_data:
+ financials_dict = financials_data.model_dump()
+ financials_dict["invoice_id"] = new_invoice.id
+ financials_dict["tenant_id"] = tenant_id
+ financials_dict["company_id"] = company_id
+ new_financials = models.InvoiceFinancials(**financials_dict)
+ db.add(new_financials)
+
+ # Create logistics entries
+ for logistics_item in logistics_data:
+ logistics_dict = logistics_item.model_dump()
+ logistics_dict["invoice_id"] = new_invoice.id
+ logistics_dict["tenant_id"] = tenant_id
+ logistics_dict["company_id"] = company_id
+ new_logistics = models.InvoiceLogistics(**logistics_dict)
+ db.add(new_logistics)
+
+ # Create sales details
+ for detail_item in details_data:
+ detail_dict = detail_item.model_dump()
+ detail_dict["invoice_id"] = new_invoice.id
+ detail_dict["tenant_id"] = tenant_id
+ detail_dict["company_id"] = company_id
+ new_detail = models.InvoiceSalesDetails(**detail_dict)
+ db.add(new_detail)
+
+ # Create collections
+ for collection_item in collections_data:
+ collection_dict = collection_item.model_dump()
+ collection_dict["invoice_id"] = new_invoice.id
+ collection_dict["tenant_id"] = tenant_id
+ collection_dict["company_id"] = company_id
+ new_collection = models.InvoiceCollections(**collection_dict)
+ db.add(new_collection)
+
+ db.commit()
+ db.refresh(new_invoice)
+ return new_invoice
+
+ @staticmethod
+ def update(
+ db: Session,
+ invoice_id: int,
+ tenant_id: int,
+ invoice_data: schemas.InvoiceHeaderUpdate,
+ company_id: int
+ ) -> Optional[models.InvoiceHeader]:
+ """Update an existing invoice and its related data"""
+ invoice = InvoiceService.get_by_id(
+ db, invoice_id, tenant_id, company_id)
+ if not invoice:
+ return None
+
+ # Update main invoice header fields
+ update_dict = invoice_data.model_dump(
+ exclude={"compliance_mx", "financials",
+ "logistics", "details", "collections"},
+ exclude_unset=True
+ )
+ for key, value in update_dict.items():
+ setattr(invoice, key, value)
+
+ # Update compliance_mx if provided
+ if invoice_data.compliance_mx is not None:
+ if invoice.compliance_mx:
+ for key, value in invoice_data.compliance_mx.model_dump(exclude_unset=True).items():
+ setattr(invoice.compliance_mx, key, value)
+ else:
+ compliance_dict = invoice_data.compliance_mx.model_dump()
+ compliance_dict["invoice_id"] = invoice.id
+ compliance_dict["tenant_id"] = tenant_id
+ compliance_dict["company_id"] = company_id
+ new_compliance = models.InvoiceComplianceMx(**compliance_dict)
+ db.add(new_compliance)
+
+ # Update financials if provided
+ if invoice_data.financials is not None:
+ if invoice.financials:
+ for key, value in invoice_data.financials.model_dump(exclude_unset=True).items():
+ setattr(invoice.financials, key, value)
+ else:
+ financials_dict = invoice_data.financials.model_dump()
+ financials_dict["invoice_id"] = invoice.id
+ financials_dict["tenant_id"] = tenant_id
+ financials_dict["company_id"] = company_id
+ new_financials = models.InvoiceFinancials(**financials_dict)
+ db.add(new_financials)
+
+ # Note: For logistics, details, and collections, we're not handling updates here
+ # as they are typically managed through separate endpoints for complex operations
+
+ db.commit()
+ db.refresh(invoice)
+ return invoice
+
+ @staticmethod
+ def delete(db: Session, invoice_id: int, tenant_id: int, company_id: int) -> bool:
+ """Delete an invoice and all related data (cascade delete)"""
+ invoice = InvoiceService.get_by_id(
+ db, invoice_id, tenant_id, company_id)
+ if invoice:
+ db.delete(invoice)
+ db.commit()
+ return True
+ return False
+
+
+class InvoiceLogisticsService:
+ """Service for Invoice Logistics operations"""
+
+ @staticmethod
+ def get_by_id(db: Session, logistics_id: int, invoice_id: int) -> Optional[models.InvoiceLogistics]:
+ """Get a logistics entry by ID"""
+ return (
+ db.query(models.InvoiceLogistics)
+ .filter(
+ models.InvoiceLogistics.logistics_id == logistics_id,
+ models.InvoiceLogistics.invoice_id == invoice_id,
+ )
+ .first()
+ )
+
+ @staticmethod
+ def get_all_by_invoice(db: Session, invoice_id: int) -> List[models.InvoiceLogistics]:
+ """Get all logistics entries for an invoice"""
+ return (
+ db.query(models.InvoiceLogistics)
+ .filter(models.InvoiceLogistics.invoice_id == invoice_id)
+ .all()
+ )
+
+ @staticmethod
+ def create(
+ db: Session,
+ logistics_data: schemas.InvoiceLogisticsCreate,
+ invoice_id: int,
+ tenant_id: int,
+ company_id: int
+ ) -> models.InvoiceLogistics:
+ """Create a new logistics entry"""
+ logistics_dict = logistics_data.model_dump()
+ logistics_dict["invoice_id"] = invoice_id
+ logistics_dict["tenant_id"] = tenant_id
+ logistics_dict["company_id"] = company_id
+
+ new_logistics = models.InvoiceLogistics(**logistics_dict)
+ db.add(new_logistics)
+ db.commit()
+ db.refresh(new_logistics)
+ return new_logistics
+
+ @staticmethod
+ def delete(db: Session, logistics_id: int, invoice_id: int) -> bool:
+ """Delete a logistics entry"""
+ logistics = InvoiceLogisticsService.get_by_id(
+ db, logistics_id, invoice_id)
+ if logistics:
+ db.delete(logistics)
+ db.commit()
+ return True
+ return False
+
+
+class InvoiceSalesDetailsService:
+ """Service for Invoice Sales Details operations"""
+
+ @staticmethod
+ def get_by_id(db: Session, detail_id: int, invoice_id: int) -> Optional[models.InvoiceSalesDetails]:
+ """Get a sales detail entry by ID"""
+ return (
+ db.query(models.InvoiceSalesDetails)
+ .filter(
+ models.InvoiceSalesDetails.detail_id == detail_id,
+ models.InvoiceSalesDetails.invoice_id == invoice_id,
+ )
+ .first()
+ )
+
+ @staticmethod
+ def get_all_by_invoice(db: Session, invoice_id: int) -> List[models.InvoiceSalesDetails]:
+ """Get all sales details for an invoice"""
+ return (
+ db.query(models.InvoiceSalesDetails)
+ .filter(models.InvoiceSalesDetails.invoice_id == invoice_id)
+ .all()
+ )
+
+ @staticmethod
+ def create(
+ db: Session,
+ detail_data: schemas.InvoiceSalesDetailsCreate,
+ invoice_id: int,
+ tenant_id: int,
+ company_id: int
+ ) -> models.InvoiceSalesDetails:
+ """Create a new sales detail entry"""
+ detail_dict = detail_data.model_dump()
+ detail_dict["invoice_id"] = invoice_id
+ detail_dict["tenant_id"] = tenant_id
+ detail_dict["company_id"] = company_id
+
+ new_detail = models.InvoiceSalesDetails(**detail_dict)
+ db.add(new_detail)
+ db.commit()
+ db.refresh(new_detail)
+ return new_detail
+
+ @staticmethod
+ def delete(db: Session, detail_id: int, invoice_id: int) -> bool:
+ """Delete a sales detail entry"""
+ detail = InvoiceSalesDetailsService.get_by_id(
+ db, detail_id, invoice_id)
+ if detail:
+ db.delete(detail)
+ db.commit()
+ return True
+ return False
+
+
+class InvoiceCollectionsService:
+ """Service for Invoice Collections operations"""
+
+ @staticmethod
+ def get_by_id(db: Session, collection_id: int, invoice_id: int) -> Optional[models.InvoiceCollections]:
+ """Get a collection entry by ID"""
+ return (
+ db.query(models.InvoiceCollections)
+ .filter(
+ models.InvoiceCollections.collection_id == collection_id,
+ models.InvoiceCollections.invoice_id == invoice_id,
+ )
+ .first()
+ )
+
+ @staticmethod
+ def get_all_by_invoice(db: Session, invoice_id: int) -> List[models.InvoiceCollections]:
+ """Get all collections for an invoice"""
+ return (
+ db.query(models.InvoiceCollections)
+ .filter(models.InvoiceCollections.invoice_id == invoice_id)
+ .all()
+ )
+
+ @staticmethod
+ def create(
+ db: Session,
+ collection_data: schemas.InvoiceCollectionsCreate,
+ invoice_id: int,
+ tenant_id: int,
+ company_id: int
+ ) -> models.InvoiceCollections:
+ """Create a new collection entry"""
+ collection_dict = collection_data.model_dump()
+ collection_dict["invoice_id"] = invoice_id
+ collection_dict["tenant_id"] = tenant_id
+ collection_dict["company_id"] = company_id
+
+ new_collection = models.InvoiceCollections(**collection_dict)
+ db.add(new_collection)
+ db.commit()
+ db.refresh(new_collection)
+ return new_collection
+
+ @staticmethod
+ def delete(db: Session, collection_id: int, invoice_id: int) -> bool:
+ """Delete a collection entry"""
+ collection = InvoiceCollectionsService.get_by_id(
+ db, collection_id, invoice_id)
+ if collection:
+ db.delete(collection)
+ db.commit()
+ return True
+ return False
diff --git a/backend/api/v1/modules/a76/router.py b/backend/api/v1/modules/a76/router.py
index aed3aa5a..8bfee681 100644
--- a/backend/api/v1/modules/a76/router.py
+++ b/backend/api/v1/modules/a76/router.py
@@ -8,7 +8,7 @@ from fastapi import APIRouter
from .customs_brokers.routes import router as customs_broker_router
# Importar routers de módulos
-from ..core.auth import router as auth_router
+from .invoices.routes import router as invoices_router
from .classes import router as classes_router
from .clients_and_providers import router as client_and_provider_router
from .general_catalogs.company import router as company_router
@@ -17,7 +17,6 @@ from .transportation.drivers.routes import router as drivers_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
-from ..core.licenses import router as licenses_router
from .general_catalogs.packages.routes import router as package_router
from .general_catalogs.ports.routes import router as ports_router
from .parts import router as parts_router
@@ -38,21 +37,15 @@ from .general_catalogs.error_catalogs.routes import router as error_catalogs_rou
from .general_catalogs.doda.routes import router as doda_router
from .general_catalogs.prevalidators.routes import router as prevalidators_router
from .general_catalogs.electronic_notices.routes import router as electronic_notices_router
-from ..core.tenants import router as tenants_router
from .transportation.trailers.routes import router as trailers_router
from .transportation.transporters.routes import router as transporters_router
-from ..core.user_tenant.routes import router as user_tenant_router
from .transportation.vehicles.routes import router as vehicles_router
# Router principal
router = APIRouter()
# Registrar módulos
-router.include_router(auth_router)
-router.include_router(tenants_router, prefix="/a76", tags=["a76 / tenants"])
-router.include_router(user_tenant_router, prefix="/a76",
- tags=["a76 / user-tenants"])
-router.include_router(licenses_router, prefix="/a76", tags=["a76 / licenses"])
+router.include_router(invoices_router, prefix="/a76", tags=["a76 / invoices"])
router.include_router(pedimentos_router, prefix="/a76")
router.include_router(
client_and_provider_router, prefix="/a76", tags=["a76 / clients_and_providers"]
diff --git a/backend/api/v1/modules/core/router.py b/backend/api/v1/modules/core/router.py
new file mode 100644
index 00000000..66b5b0d2
--- /dev/null
+++ b/backend/api/v1/modules/core/router.py
@@ -0,0 +1,12 @@
+from .auth.routes import router as auth_router
+from .licenses.routes import router as licenses_router
+from .tenants.routes import router as tenants_router
+from .user_tenant.routes import router as user_tenant_router
+from fastapi import APIRouter
+
+router = APIRouter()
+
+router.include_router(auth_router)
+router.include_router(tenants_router, prefix="/core", tags=["core / tenants"])
+router.include_router(user_tenant_router, prefix="/core", tags=["core / user-tenants"])
+router.include_router(licenses_router, prefix="/core", tags=["core / licenses"])
\ No newline at end of file
diff --git a/backend/api/v1/router.py b/backend/api/v1/router.py
index 5d7dc302..9fd2618e 100644
--- a/backend/api/v1/router.py
+++ b/backend/api/v1/router.py
@@ -6,6 +6,7 @@ Agrega todos los módulos de la aplicación
from fastapi import APIRouter
# Importar routers de módulos
+from .modules.core.router import router as core_router
from .modules.a76.router import router as a76_router
from .modules.public.router import router as public_router
@@ -13,6 +14,7 @@ from .modules.public.router import router as public_router
router = APIRouter()
# Registrar módulos
+router.include_router(core_router)
router.include_router(a76_router)
router.include_router(public_router)
From 6cab9552767712633873333e6a2db8c2bb366633 Mon Sep 17 00:00:00 2001
From: acazares
Date: Tue, 9 Dec 2025 18:02:24 -0600
Subject: [PATCH 02/15] feat: Enhance invoice management with new operation
types and schema updates
---
...31bf8cdae06_create_material_types_table.py | 1 +
backend/api/v1/modules/a76/invoices/models.py | 255 +++++++++++-------
.../api/v1/modules/a76/invoices/schemas.py | 39 +--
.../reference_data/invoice_types/models.py | 16 +-
.../reference_data/invoice_types/seed.py | 60 ++++-
5 files changed, 230 insertions(+), 141 deletions(-)
diff --git a/backend/alembic/versions/531bf8cdae06_create_material_types_table.py b/backend/alembic/versions/531bf8cdae06_create_material_types_table.py
index 2179bc81..bfdc01b1 100644
--- a/backend/alembic/versions/531bf8cdae06_create_material_types_table.py
+++ b/backend/alembic/versions/531bf8cdae06_create_material_types_table.py
@@ -81,6 +81,7 @@ def upgrade() -> None:
sa.Column("description", sa.String(length=50), nullable=False),
sa.Column("note", sa.String(length=500), nullable=False),
sa.Column("type", sa.String(length=15), nullable=False),
+ sa.Column("operation", sa.String(length=5), nullable=False),
sa.PrimaryKeyConstraint("key", name="invoice_types_pkey"),
schema="public",
)
diff --git a/backend/api/v1/modules/a76/invoices/models.py b/backend/api/v1/modules/a76/invoices/models.py
index 8c0feee9..bed62eaf 100644
--- a/backend/api/v1/modules/a76/invoices/models.py
+++ b/backend/api/v1/modules/a76/invoices/models.py
@@ -1,3 +1,4 @@
+from enum import Enum
from typing import Optional, List
from sqlalchemy import BigInteger, Date, ForeignKey, Integer, Numeric, String, Text, TIMESTAMP
from sqlalchemy.orm import Mapped, mapped_column, relationship
@@ -5,42 +6,68 @@ from core.database import Base
from datetime import datetime
from ....common.base_models import TenantScopedMixin, TimestampMixin
+
+class OperationType(str, Enum):
+ IMP = "imp" # Importación
+ EXP = "exp" # Exportación
+
# --- 1. Invoice Header (invoice_header) ---
+
+
class InvoiceHeader(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "invoice_header"
__tableargs__ = {'schema': 'a76'}
- id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
-
+ id: Mapped[int] = mapped_column(
+ BigInteger, primary_key=True, autoincrement=True)
+
# Identifiers
- legacy_id: Mapped[Optional[int]] = mapped_column(Integer) # CONSECUTIVO
- operation_type: Mapped[str] = mapped_column(String(20)) # TIPOMOVIMIENTO / Clasifica IMP/EXP/SM
- invoice_number: Mapped[Optional[str]] = mapped_column(String(20)) # FACTURAIMPO/FACTURAEXPO/FACTURAREMISION
- project_number: Mapped[Optional[str]] = mapped_column(String(14)) # NUMPROYECTO
- purchase_order: Mapped[Optional[str]] = mapped_column(String(50)) # ORDENCOMPRA
- related_doc_id: Mapped[Optional[int]] = mapped_column(Integer) # IDRELDOC (Para Rectificaciones)
-
+ operation_type: Mapped[OperationType] = mapped_column(
+ String(3)) # TIPOMOVIMIENTO / Clasifica IMP/EXP
+ invoice_type: Mapped[Optional[str]] = mapped_column(
+ ForeignKey("public.invoice_types.key")) # TIPOFACTURA
+ invoice_number: Mapped[Optional[str]] = mapped_column(
+ String(20)) # FACTURAIMPO/FACTURAEXPO/FACTURAREMISION
+ project_number: Mapped[Optional[str]] = mapped_column(
+ String(14)) # NUMPROYECTO
+ purchase_order: Mapped[Optional[str]] = mapped_column(
+ String(50)) # ORDENCOMPRA
+ related_doc_id: Mapped[Optional[int]] = mapped_column(
+ Integer) # IDRELDOC (Para Rectificaciones)
+
# Dates
- invoice_date: Mapped[Optional[datetime]] = mapped_column(Date) # FECHAFACTURA
- capture_date: Mapped[datetime] = mapped_column(TIMESTAMP(timezone=False), default=datetime.now) # FECHACAPTURA + HORAACTUAL
-
+ invoice_date: Mapped[Optional[datetime]
+ ] = mapped_column(Date) # FECHAFACTURA
+ capture_date: Mapped[datetime] = mapped_column(
+ TIMESTAMP(timezone=False), default=datetime.now) # FECHACAPTURA + HORAACTUAL
+
# Status & Control
- status: Mapped[Optional[str]] = mapped_column(String(5)) # ESTATUS
- traffic_light_status: Mapped[Optional[str]] = mapped_column(String(50)) # SEMAFORO / SEMAFOROEXPO/IMPO
- process_log: Mapped[Optional[str]] = mapped_column(String(300)) # COMOFUEPROCESADA **<-- AÑADIDO**
- comments: Mapped[Optional[str]] = mapped_column(Text) # OBSERVACIONE + OBSERVACIONI + COMENTARIOSESTATUS
-
+ status: Mapped[Optional[str]] = mapped_column(String(5)) # ESTATUS
+ traffic_light_status: Mapped[Optional[str]] = mapped_column(
+ String(50)) # SEMAFORO / SEMAFOROEXPO/IMPO
+ process_log: Mapped[Optional[str]] = mapped_column(
+ String(300)) # COMOFUEPROCESADA **<-- AÑADIDO**
+ # OBSERVACIONE + OBSERVACIONI + COMENTARIOSESTATUS
+ comments: Mapped[Optional[str]] = mapped_column(Text)
+
# Digital Archive Links
- cfdi_uuid: Mapped[Optional[str]] = mapped_column(String(100)) # CFDIUUID
- path_pdf: Mapped[Optional[str]] = mapped_column(String(500)) # CFDIPATHPDF **<-- AÑADIDO**
- path_xml: Mapped[Optional[str]] = mapped_column(String(500)) # CFDIPATHXML **<-- AÑADIDO**
-
+ cfdi_uuid: Mapped[Optional[str]] = mapped_column(String(100)) # CFDIUUID
+ path_pdf: Mapped[Optional[str]] = mapped_column(
+ String(500)) # CFDIPATHPDF **<-- AÑADIDO**
+ path_xml: Mapped[Optional[str]] = mapped_column(
+ String(500)) # CFDIPATHXML **<-- AÑADIDO**
+
# Relationships (Para navegacion ORM)
- compliance_mx: Mapped["InvoiceComplianceMx"] = relationship(back_populates="header", cascade="all, delete-orphan")
- financials: Mapped["InvoiceFinancials"] = relationship(back_populates="header", cascade="all, delete-orphan")
- details: Mapped[List["InvoiceSalesDetails"]] = relationship(back_populates="header", cascade="all, delete-orphan")
- collections: Mapped[List["InvoiceCollections"]] = relationship(back_populates="header", cascade="all, delete-orphan")
- logistics: Mapped[List["InvoiceLogistics"]] = relationship(back_populates="header", cascade="all, delete-orphan")
+ compliance_mx: Mapped["InvoiceComplianceMx"] = relationship(
+ back_populates="header", cascade="all, delete-orphan")
+ financials: Mapped["InvoiceFinancials"] = relationship(
+ back_populates="header", cascade="all, delete-orphan")
+ details: Mapped[List["InvoiceSalesDetails"]] = relationship(
+ back_populates="header", cascade="all, delete-orphan")
+ collections: Mapped[List["InvoiceCollections"]] = relationship(
+ back_populates="header", cascade="all, delete-orphan")
+ logistics: Mapped[List["InvoiceLogistics"]] = relationship(
+ back_populates="header", cascade="all, delete-orphan")
# --- 2. Compliance MX (invoice_compliance_mx) ---
@@ -48,27 +75,36 @@ class InvoiceComplianceMx(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "invoice_compliance_mx"
__tableargs__ = {'schema': 'a76'}
- invoice_id: Mapped[int] = mapped_column(ForeignKey("invoice_header.id"), primary_key=True)
-
+ invoice_id: Mapped[int] = mapped_column(
+ ForeignKey("invoice_header.id"), primary_key=True)
+
# Core Customs Data
- pedimento: Mapped[Optional[str]] = mapped_column(String(19)) # PEDIMENTO/PEDIMENTOIMPO/EXPO
- pedimento_code: Mapped[Optional[str]] = mapped_column(String(5)) # PEDIMENTOR1, K1
+ pedimento: Mapped[Optional[str]] = mapped_column(
+ String(19)) # PEDIMENTO/PEDIMENTOIMPO/EXPO
+ pedimento_code: Mapped[Optional[str]] = mapped_column(
+ String(5)) # PEDIMENTOR1, K1
remesa: Mapped[Optional[int]] = mapped_column(Integer)
- aduana: Mapped[Optional[str]] = mapped_column(String(5)) # ADUANA_CRUCE
- customs_agent: Mapped[Optional[str]] = mapped_column(String(10)) # AADUANAL
-
+ aduana: Mapped[Optional[str]] = mapped_column(String(5)) # ADUANA_CRUCE
+ customs_agent: Mapped[Optional[str]] = mapped_column(
+ String(10)) # AADUANAL
+
# Flags & Specific Regimes
- is_mixed: Mapped[Optional[str]] = mapped_column(String(2)) # ESMIXTO **<-- AÑADIDO**
- waste_type: Mapped[Optional[str]] = mapped_column(String(1)) # TIPODESPERDICIO **<-- AÑADIDO**
- appendix_17: Mapped[Optional[int]] = mapped_column(Integer) # APENDICE17
-
+ is_mixed: Mapped[Optional[str]] = mapped_column(
+ String(2)) # ESMIXTO **<-- AÑADIDO**
+ waste_type: Mapped[Optional[str]] = mapped_column(
+ String(1)) # TIPODESPERDICIO **<-- AÑADIDO**
+ appendix_17: Mapped[Optional[int]] = mapped_column(Integer) # APENDICE17
+
# VUCEM / Digital
- edocument: Mapped[Optional[str]] = mapped_column(String(50)) # EDOCUMENT
- electronic_signature: Mapped[Optional[str]] = mapped_column(String(999)) # FIRMAELECTRONICA
- sem_id: Mapped[Optional[int]] = mapped_column(Integer) # SEM (de SFacEntradaSM/SFacSalidaSM)
-
+ edocument: Mapped[Optional[str]] = mapped_column(String(50)) # EDOCUMENT
+ electronic_signature: Mapped[Optional[str]] = mapped_column(
+ String(999)) # FIRMAELECTRONICA
+ sem_id: Mapped[Optional[int]] = mapped_column(
+ Integer) # SEM (de SFacEntradaSM/SFacSalidaSM)
+
# Relationship
- header: Mapped["InvoiceHeader"] = relationship(back_populates="compliance_mx")
+ header: Mapped["InvoiceHeader"] = relationship(
+ back_populates="compliance_mx")
# --- 3. Financials (invoice_financials) ---
@@ -76,28 +112,40 @@ class InvoiceFinancials(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "invoice_financials"
__tableargs__ = {'schema': 'a76'}
- invoice_id: Mapped[int] = mapped_column(ForeignKey("invoice_header.id"), primary_key=True)
-
- currency: Mapped[Optional[str]] = mapped_column(String(3)) # CLAVEMONEDA
- exchange_rate: Mapped[Optional[float]] = mapped_column(Numeric(13, 6)) # TIPOCAMBIO
-
+ invoice_id: Mapped[int] = mapped_column(
+ ForeignKey("invoice_header.id"), primary_key=True)
+
+ currency: Mapped[Optional[str]] = mapped_column(String(3)) # CLAVEMONEDA
+ exchange_rate: Mapped[Optional[float]] = mapped_column(
+ Numeric(13, 6)) # TIPOCAMBIO
+
# Merchandise Values
- value_mn: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # VALORIMPOMN/EXPOMN
- value_me: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0)
- customs_value_mn: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # VALORADUANASMN **<-- AÑADIDO**
-
+ value_mn: Mapped[Optional[float]] = mapped_column(
+ Numeric(23, 8), default=0) # VALORIMPOMN/EXPOMN
+ value_me: Mapped[Optional[float]] = mapped_column(
+ Numeric(23, 8), default=0)
+ customs_value_mn: Mapped[Optional[float]] = mapped_column(
+ Numeric(23, 8), default=0) # VALORADUANASMN **<-- AÑADIDO**
+
# Costs & Taxes
- freight: Mapped[Optional[float]] = mapped_column(Numeric(19, 8), default=0) # FLETE
- insurance: Mapped[Optional[float]] = mapped_column(Numeric(19, 8), default=0) # VALSEGUROS/SEGUROS
- iva_mn: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # IVAEXPOMN / VALORIVAMN
- iva_factor: Mapped[Optional[float]] = mapped_column(Numeric(17, 4)) # FACTORIVA
-
+ freight: Mapped[Optional[float]] = mapped_column(
+ Numeric(19, 8), default=0) # FLETE
+ insurance: Mapped[Optional[float]] = mapped_column(
+ Numeric(19, 8), default=0) # VALSEGUROS/SEGUROS
+ iva_mn: Mapped[Optional[float]] = mapped_column(
+ Numeric(23, 8), default=0) # IVAEXPOMN / VALORIVAMN
+ iva_factor: Mapped[Optional[float]] = mapped_column(
+ Numeric(17, 4)) # FACTORIVA
+
# Weights & Quantities
- total_quantity: Mapped[Optional[float]] = mapped_column(Numeric(19, 8)) # CANTEXPO / CANTIMPO
- gross_weight: Mapped[Optional[float]] = mapped_column(Numeric(19, 8)) # PESOBRUTO **<-- AÑADIDO**
- net_weight: Mapped[Optional[float]] = mapped_column(Numeric(19, 8)) # PESONETO **<-- AÑADIDO**
- bundle_count: Mapped[Optional[int]] = mapped_column(Integer) # CANTBULTOS
-
+ total_quantity: Mapped[Optional[float]] = mapped_column(
+ Numeric(19, 8)) # CANTEXPO / CANTIMPO
+ gross_weight: Mapped[Optional[float]] = mapped_column(
+ Numeric(19, 8)) # PESOBRUTO **<-- AÑADIDO**
+ net_weight: Mapped[Optional[float]] = mapped_column(
+ Numeric(19, 8)) # PESONETO **<-- AÑADIDO**
+ bundle_count: Mapped[Optional[int]] = mapped_column(Integer) # CANTBULTOS
+
# Relationship
header: Mapped["InvoiceHeader"] = relationship(back_populates="financials")
@@ -107,25 +155,33 @@ class InvoiceLogistics(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "invoice_logistics"
__tableargs__ = {'schema': 'a76'}
- logistics_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
+ logistics_id: Mapped[int] = mapped_column(
+ BigInteger, primary_key=True, autoincrement=True)
invoice_id: Mapped[int] = mapped_column(ForeignKey("invoice_header.id"))
-
+
# Carrier Info
- carrier_id: Mapped[Optional[str]] = mapped_column(String(10)) # TRANSPORTISTA
- transport_mode: Mapped[Optional[str]] = mapped_column(String(15)) # TRANSPORTE
- driver_name: Mapped[Optional[str]] = mapped_column(String(80)) # CONDUCTOR
- is_rail: Mapped[Optional[str]] = mapped_column(String(2)) # ESFERROCARRIL
- rail_id: Mapped[Optional[str]] = mapped_column(String(31)) # IDFERRORCARRIL
-
+ carrier_id: Mapped[Optional[str]] = mapped_column(
+ String(10)) # TRANSPORTISTA
+ transport_mode: Mapped[Optional[str]] = mapped_column(
+ String(15)) # TRANSPORTE
+ driver_name: Mapped[Optional[str]] = mapped_column(String(80)) # CONDUCTOR
+ is_rail: Mapped[Optional[str]] = mapped_column(String(2)) # ESFERROCARRIL
+ rail_id: Mapped[Optional[str]] = mapped_column(
+ String(31)) # IDFERRORCARRIL
+
# Vehicle & Tracking
- vehicle_num: Mapped[Optional[str]] = mapped_column(String(20)) # NUMVEHICULO / NUMTRAILER
- license_plate: Mapped[Optional[str]] = mapped_column(String(20)) # NUMTRASPORTE
- seal_number: Mapped[Optional[str]] = mapped_column(String(15)) # PRECINTO
- guide_number: Mapped[Optional[str]] = mapped_column(String(20)) # NUMEROGUIA
-
+ vehicle_num: Mapped[Optional[str]] = mapped_column(
+ String(20)) # NUMVEHICULO / NUMTRAILER
+ license_plate: Mapped[Optional[str]] = mapped_column(
+ String(20)) # NUMTRASPORTE
+ seal_number: Mapped[Optional[str]] = mapped_column(String(15)) # PRECINTO
+ guide_number: Mapped[Optional[str]] = mapped_column(
+ String(20)) # NUMEROGUIA
+
# Logistics Dates
- entry_exit_date: Mapped[Optional[datetime]] = mapped_column(Date) # FECHAENTRADA / FECHAENVIO
-
+ entry_exit_date: Mapped[Optional[datetime]] = mapped_column(
+ Date) # FECHAENTRADA / FECHAENVIO
+
# Relationship
header: Mapped["InvoiceHeader"] = relationship(back_populates="logistics")
@@ -135,17 +191,22 @@ class InvoiceSalesDetails(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "invoice_sales_details"
__tableargs__ = {'schema': 'a76'}
- detail_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
+ detail_id: Mapped[int] = mapped_column(
+ BigInteger, primary_key=True, autoincrement=True)
invoice_id: Mapped[int] = mapped_column(ForeignKey("invoice_header.id"))
-
- line_number: Mapped[int] = mapped_column(Integer) # LINEA
- sales_order: Mapped[Optional[str]] = mapped_column(String(20)) # ORDENVENTA
-
+
+ line_number: Mapped[int] = mapped_column(Integer) # LINEA
+ sales_order: Mapped[Optional[str]] = mapped_column(
+ String(20)) # ORDENVENTA
+
# Specific Custom Fields
- colors_description: Mapped[Optional[str]] = mapped_column(String(49)) # COLORES
- square_color_code: Mapped[Optional[str]] = mapped_column(String(1)) # COLORCUADRITO
- line_bundles: Mapped[Optional[int]] = mapped_column(Integer) # CANTBULTOS (de la linea)
-
+ colors_description: Mapped[Optional[str]
+ ] = mapped_column(String(49)) # COLORES
+ square_color_code: Mapped[Optional[str]] = mapped_column(
+ String(1)) # COLORCUADRITO
+ line_bundles: Mapped[Optional[int]] = mapped_column(
+ Integer) # CANTBULTOS (de la linea)
+
# Relationship
header: Mapped["InvoiceHeader"] = relationship(back_populates="details")
@@ -155,15 +216,19 @@ class InvoiceCollections(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "invoice_collections"
__tableargs__ = {'schema': 'a76'}
- collection_id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
+ collection_id: Mapped[int] = mapped_column(
+ BigInteger, primary_key=True, autoincrement=True)
invoice_id: Mapped[int] = mapped_column(ForeignKey("invoice_header.id"))
-
- concept: Mapped[Optional[str]] = mapped_column(String(100)) # CONCEPTO
- is_collected: Mapped[Optional[int]] = mapped_column(Integer) # COBRADO
- collection_date: Mapped[Optional[datetime]] = mapped_column(Date) # FECHACOBRANZA
- amount: Mapped[Optional[float]] = mapped_column(Numeric(23, 8)) # VALOR
-
- collector_user: Mapped[Optional[str]] = mapped_column(String(20)) # FACTCOBRADOR
-
+
+ concept: Mapped[Optional[str]] = mapped_column(String(100)) # CONCEPTO
+ is_collected: Mapped[Optional[int]] = mapped_column(Integer) # COBRADO
+ collection_date: Mapped[Optional[datetime]
+ ] = mapped_column(Date) # FECHACOBRANZA
+ amount: Mapped[Optional[float]] = mapped_column(Numeric(23, 8)) # VALOR
+
+ collector_user: Mapped[Optional[str]] = mapped_column(
+ String(20)) # FACTCOBRADOR
+
# Relationship
- header: Mapped["InvoiceHeader"] = relationship(back_populates="collections")
\ No newline at end of file
+ header: Mapped["InvoiceHeader"] = relationship(
+ back_populates="collections")
diff --git a/backend/api/v1/modules/a76/invoices/schemas.py b/backend/api/v1/modules/a76/invoices/schemas.py
index a9b01b3b..70280e46 100644
--- a/backend/api/v1/modules/a76/invoices/schemas.py
+++ b/backend/api/v1/modules/a76/invoices/schemas.py
@@ -2,38 +2,27 @@ from typing import Optional, List
from datetime import datetime, date
from decimal import Decimal
from pydantic import BaseModel, Field
+from .models import OperationType
+
# --- Base Schemas ---
-
class InvoiceHeaderBase(BaseModel):
"""Base fields for Invoice Header"""
- legacy_id: Optional[int] = Field(
- None, description="Legacy consecutive ID (CONSECUTIVO)")
- operation_type: Optional[str] = Field(
- None, max_length=20, description="Operation type: IMP/EXP/SM")
- invoice_number: Optional[str] = Field(
- None, max_length=20, description="Invoice number")
- project_number: Optional[str] = Field(
- None, max_length=14, description="Project number")
- purchase_order: Optional[str] = Field(
- None, max_length=50, description="Purchase order")
- related_doc_id: Optional[int] = Field(
- None, description="Related document ID for rectifications")
+ operation_type: Optional[OperationType] = Field(None, max_length=20, description="Operation type: imp/exp")
+ invoice_type: Optional[str] = Field(None, max_length=5, description="Invoice type key")
+ invoice_number: Optional[str] = Field(None, max_length=20, description="Invoice number")
+ project_number: Optional[str] = Field(None, max_length=14, description="Project number")
+ purchase_order: Optional[str] = Field(None, max_length=50, description="Purchase order")
+ related_doc_id: Optional[int] = Field(None, description="Related document ID for rectifications")
invoice_date: Optional[date] = Field(None, description="Invoice date")
status: Optional[str] = Field(None, max_length=5, description="Status")
- traffic_light_status: Optional[str] = Field(
- None, max_length=50, description="Traffic light status (SEMAFORO)")
- process_log: Optional[str] = Field(
- None, max_length=300, description="Processing log")
- comments: Optional[str] = Field(
- None, description="Comments and observations")
- cfdi_uuid: Optional[str] = Field(
- None, max_length=100, description="CFDI UUID")
- path_pdf: Optional[str] = Field(
- None, max_length=500, description="Path to PDF file")
- path_xml: Optional[str] = Field(
- None, max_length=500, description="Path to XML file")
+ traffic_light_status: Optional[str] = Field(None, max_length=50, description="Traffic light status (SEMAFORO)")
+ process_log: Optional[str] = Field(None, max_length=300, description="Processing log")
+ comments: Optional[str] = Field(None, description="Comments and observations")
+ cfdi_uuid: Optional[str] = Field(None, max_length=100, description="CFDI UUID")
+ path_pdf: Optional[str] = Field(None, max_length=500, description="Path to PDF file")
+ path_xml: Optional[str] = Field(None, max_length=500, description="Path to XML file")
class InvoiceComplianceMxBase(BaseModel):
diff --git a/backend/api/v1/modules/public/reference_data/invoice_types/models.py b/backend/api/v1/modules/public/reference_data/invoice_types/models.py
index 8b3671f0..792cda91 100644
--- a/backend/api/v1/modules/public/reference_data/invoice_types/models.py
+++ b/backend/api/v1/modules/public/reference_data/invoice_types/models.py
@@ -10,15 +10,11 @@ class InvoiceType(Base):
{"schema": "public", "extend_existing": True}, # opcional
)
- key: Mapped[str] = mapped_column(
- String(5), nullable=False
- ) # clave del tipo de factura
- description: Mapped[str] = mapped_column(
- String(50), nullable=False
- ) # descripción oficial (en español)
- # observación o comentario adicional
- note: Mapped[str] = mapped_column(String(500))
- type: Mapped[str] = mapped_column(String(15)) # tipo
+ key: Mapped[str] = mapped_column(String(5), nullable=False) # clave del tipo de factura
+ description: Mapped[str] = mapped_column(String(50), nullable=False) # descripción oficial (en español)
+ note: Mapped[str] = mapped_column(String(500))# observación o comentario adicional
+ type: Mapped[str] = mapped_column(String(15))# tipo (MATERIAL, fixed asset, both)
+ operation: Mapped[str] = mapped_column(String(5)) # operación (imp, exp, both)
def __repr__(self):
- return f""
+ return f""
diff --git a/backend/api/v1/modules/public/reference_data/invoice_types/seed.py b/backend/api/v1/modules/public/reference_data/invoice_types/seed.py
index dd8f0fcb..235e726f 100644
--- a/backend/api/v1/modules/public/reference_data/invoice_types/seed.py
+++ b/backend/api/v1/modules/public/reference_data/invoice_types/seed.py
@@ -1,38 +1,76 @@
seed = [
- ("DONAC", "DONACION", "", "AMBOS"),
- ("EXDEF", "EXPORTACION DEFINITIVA", "", "MATERIAL"),
+ # === TIPOS DE IMPORTACION ===
+ (
+ "TEMP",
+ "IMPORTACION TEMPORAL",
+ "IMPORTACION TEMPORAL DE MATERIA PRIMA, COMPONENTES O MATERIALES PARA SER PROCESADOS Y POSTERIORMENTE EXPORTADOS.",
+ "material",
+ "imp",
+ ),
+ (
+ "DEF",
+ "IMPORTACION DEFINITIVA",
+ "IMPORTACION DEFINITIVA PARA NACIONALIZACION DE MERCANCIA QUE PERMANECE EN TERRITORIO NACIONAL.",
+ "material",
+ "imp",
+ ),
+ (
+ "MEX",
+ "COMPRAS MEXICANAS",
+ "IMPORTACION DE MERCANCIA NACIONAL ADQUIRIDA DE PROVEEDORES MEXICANOS PARA INCORPORAR A PROCESO PRODUCTIVO.",
+ "material",
+ "imp",
+ ),
+ (
+ "CR",
+ "CAMBIO DE REGIMEN",
+ "IMPORTACION POR CAMBIO DE REGIMEN DE MERCANCIA TEMPORAL QUE SE NACIONALIZA O RETORNA.",
+ "material",
+ "imp",
+ ),
+
+ # === TIPOS DE EXPORTACION ===
+ ("DONAC", "DONACION", "", "both", "exp"),
+ ("EXDEF", "EXPORTACION DEFINITIVA", "", "material", "exp"),
(
"MATDE",
"MATERIA PRIMA O MATERIAL DEVUELTO",
"ESTE PROCESO CONSISTE EN SOLO DESCARGAR LAS PARTES DADAS DE ALTA EN MATERIALES QUE SON RETORNADAS SIN NINGUNA MODIFICACION (A1)",
- "MATERIAL",
+ "material",
+ "exp",
),
(
"NODES",
"NO HACE DESCARGA",
"ESTE PROCESO DE ACTUALIZACION CONSISTE EN EXPORTAR UNA MERCANCIA Y NO DESCARGAR, POR LO TANTO NO EXISTE REPORTE DE DESCARGAS Y NO AFECTA SALDOS.",
- "AMBOS",
+ "both",
+ "exp",
),
(
"PTERM",
"PRODUCTO TERMINADO Y VIRTUALES",
"EL PRODUCTO TERMINADO Y VIRTUALES DESCARGARAN: 1) APARTIR DE LOS COMPONENTES DE CADA PRODUCTO TERMINADO REGISTRADO EN LAS PARTIDAS DE EXPORTACION. 2) POR PARTE, CON LAS OPCIONES DE PODER DESCARGAR POR SUSTITUTO Y POR CLASE EN CASO DE INSUFICIENCIAS DEL COMPONENTE.",
- "MATERIAL",
+ "material",
+ "exp",
),
(
"REPAR",
"REPARACION",
"PROCESO QUE CONSISTE EN DOS ETAPAS: 1) DESCARGA EL PRODUCTO DE REPARACION QUE SE IMPORTO PARA REPARA, 2) DESCARGA EL LISTADO DE COMPONENTES QUE SE AGREGO AL PRODUCTO DE REPARACION",
- "MATERIAL",
+ "material",
+ "exp",
),
- ("SCRAP", "SCRAP", "", "AMBOS"),
+ ("SCRAP", "SCRAP", "", "both", "exp"),
(
"VEMEX",
"VENTAS EN MEXICO",
"ESTE PROCESO CONSISTE EN LA VENTA EN EL MERCADO NACIONAL DE LOS PRODUCTOS.",
- "AMBOS",
+ "both",
+ "exp",
),
- ("VIRTU", "VIRTUALES", "", "MATERIAL"),
- ("AFIJO", "ACTIVO FIJO", "", "ACTIVO FIJO"),
- ("REEXP", "REEXPEDICION", "", "ACTIVO FIJO"),
+ ("VIRTU", "VIRTUALES", "", "material", "exp"),
+
+ # === ACTIVOS FIJOS (AMBAS OPERACIONES) ===
+ ("AFIJO", "ACTIVO FIJO", "", "fixed asset", "both"),
+ ("REEXP", "REEXPEDICION", "", "fixed asset", "both"),
]
From c63d543cb5a49a281ca6e0e54471f6573d22d299 Mon Sep 17 00:00:00 2001
From: acazares
Date: Thu, 11 Dec 2025 11:21:06 -0600
Subject: [PATCH 03/15] feat: Update invoice types seed data to include
operation types
---
.../7937209f9718_seed_initial_data.py | 6 ++--
.../reference_data/invoice_types/seed.py | 28 +++++++++----------
2 files changed, 17 insertions(+), 17 deletions(-)
diff --git a/backend/alembic/versions/7937209f9718_seed_initial_data.py b/backend/alembic/versions/7937209f9718_seed_initial_data.py
index 0ce75ac3..616f3fd5 100644
--- a/backend/alembic/versions/7937209f9718_seed_initial_data.py
+++ b/backend/alembic/versions/7937209f9718_seed_initial_data.py
@@ -192,13 +192,13 @@ def upgrade() -> None:
values_it = ", ".join(
[
- f"('{key}', '{desc.replace(chr(39), chr(39)*2)}', '{note.replace(chr(39), chr(39)*2)}', '{type.replace(chr(39), chr(39)*2)}')"
- for key, desc, note, type in invoice_types_seed
+ f"('{key}', '{desc.replace(chr(39), chr(39)*2)}', '{note.replace(chr(39), chr(39)*2)}', '{type.replace(chr(39), chr(39)*2)}'), '{operation.replace(chr(39), chr(39)*2)}')"
+ for key, desc, note, type, operation in invoice_types_seed
]
)
op.execute(
f"""
- INSERT INTO public.invoice_types (key, description, note, type) VALUES
+ INSERT INTO public.invoice_types (key, description, note, type, operation) VALUES
{values_it}
ON CONFLICT (key) DO NOTHING;
"""
diff --git a/backend/api/v1/modules/public/reference_data/invoice_types/seed.py b/backend/api/v1/modules/public/reference_data/invoice_types/seed.py
index 235e726f..f011769f 100644
--- a/backend/api/v1/modules/public/reference_data/invoice_types/seed.py
+++ b/backend/api/v1/modules/public/reference_data/invoice_types/seed.py
@@ -1,74 +1,74 @@
seed = [
# === TIPOS DE IMPORTACION ===
(
- "TEMP",
+ "TEM",
"IMPORTACION TEMPORAL",
"IMPORTACION TEMPORAL DE MATERIA PRIMA, COMPONENTES O MATERIALES PARA SER PROCESADOS Y POSTERIORMENTE EXPORTADOS.",
- "material",
+ "both",
"imp",
),
(
"DEF",
"IMPORTACION DEFINITIVA",
"IMPORTACION DEFINITIVA PARA NACIONALIZACION DE MERCANCIA QUE PERMANECE EN TERRITORIO NACIONAL.",
- "material",
+ "both",
"imp",
),
(
"MEX",
"COMPRAS MEXICANAS",
"IMPORTACION DE MERCANCIA NACIONAL ADQUIRIDA DE PROVEEDORES MEXICANOS PARA INCORPORAR A PROCESO PRODUCTIVO.",
- "material",
+ "both",
"imp",
),
(
"CR",
"CAMBIO DE REGIMEN",
"IMPORTACION POR CAMBIO DE REGIMEN DE MERCANCIA TEMPORAL QUE SE NACIONALIZA O RETORNA.",
- "material",
+ "both",
"imp",
),
# === TIPOS DE EXPORTACION ===
- ("DONAC", "DONACION", "", "both", "exp"),
- ("EXDEF", "EXPORTACION DEFINITIVA", "", "material", "exp"),
+ ("DONAC", "DONACION", "", "both", "both"),
+ ("EXDEF", "EXPORTACION DEFINITIVA", "", "material", "both"),
(
"MATDE",
"MATERIA PRIMA O MATERIAL DEVUELTO",
"ESTE PROCESO CONSISTE EN SOLO DESCARGAR LAS PARTES DADAS DE ALTA EN MATERIALES QUE SON RETORNADAS SIN NINGUNA MODIFICACION (A1)",
"material",
- "exp",
+ "both",
),
(
"NODES",
"NO HACE DESCARGA",
"ESTE PROCESO DE ACTUALIZACION CONSISTE EN EXPORTAR UNA MERCANCIA Y NO DESCARGAR, POR LO TANTO NO EXISTE REPORTE DE DESCARGAS Y NO AFECTA SALDOS.",
"both",
- "exp",
+ "both",
),
(
"PTERM",
"PRODUCTO TERMINADO Y VIRTUALES",
"EL PRODUCTO TERMINADO Y VIRTUALES DESCARGARAN: 1) APARTIR DE LOS COMPONENTES DE CADA PRODUCTO TERMINADO REGISTRADO EN LAS PARTIDAS DE EXPORTACION. 2) POR PARTE, CON LAS OPCIONES DE PODER DESCARGAR POR SUSTITUTO Y POR CLASE EN CASO DE INSUFICIENCIAS DEL COMPONENTE.",
"material",
- "exp",
+ "both",
),
(
"REPAR",
"REPARACION",
"PROCESO QUE CONSISTE EN DOS ETAPAS: 1) DESCARGA EL PRODUCTO DE REPARACION QUE SE IMPORTO PARA REPARA, 2) DESCARGA EL LISTADO DE COMPONENTES QUE SE AGREGO AL PRODUCTO DE REPARACION",
"material",
- "exp",
+ "both",
),
- ("SCRAP", "SCRAP", "", "both", "exp"),
+ ("SCRAP", "SCRAP", "", "both", "both"),
(
"VEMEX",
"VENTAS EN MEXICO",
"ESTE PROCESO CONSISTE EN LA VENTA EN EL MERCADO NACIONAL DE LOS PRODUCTOS.",
"both",
- "exp",
+ "both",
),
- ("VIRTU", "VIRTUALES", "", "material", "exp"),
+ ("VIRTU", "VIRTUALES", "", "material", "both"),
# === ACTIVOS FIJOS (AMBAS OPERACIONES) ===
("AFIJO", "ACTIVO FIJO", "", "fixed asset", "both"),
From 76b932571305073ac38d20f4e57f319568878bb7 Mon Sep 17 00:00:00 2001
From: acazares
Date: Thu, 11 Dec 2025 11:22:19 -0600
Subject: [PATCH 04/15] fix: Correct SQL values formatting in invoice types
seed data
---
backend/alembic/versions/7937209f9718_seed_initial_data.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backend/alembic/versions/7937209f9718_seed_initial_data.py b/backend/alembic/versions/7937209f9718_seed_initial_data.py
index 616f3fd5..864f6c72 100644
--- a/backend/alembic/versions/7937209f9718_seed_initial_data.py
+++ b/backend/alembic/versions/7937209f9718_seed_initial_data.py
@@ -192,7 +192,7 @@ def upgrade() -> None:
values_it = ", ".join(
[
- f"('{key}', '{desc.replace(chr(39), chr(39)*2)}', '{note.replace(chr(39), chr(39)*2)}', '{type.replace(chr(39), chr(39)*2)}'), '{operation.replace(chr(39), chr(39)*2)}')"
+ f"('{key}', '{desc.replace(chr(39), chr(39)*2)}', '{note.replace(chr(39), chr(39)*2)}', '{type.replace(chr(39), chr(39)*2)}', '{operation.replace(chr(39), chr(39)*2)}')"
for key, desc, note, type, operation in invoice_types_seed
]
)
From e5f6162ffb98b1bc591d95ba142bdf45f514ec37 Mon Sep 17 00:00:00 2001
From: acazares
Date: Thu, 11 Dec 2025 11:30:44 -0600
Subject: [PATCH 05/15] feat: Implement invoice management features including
data table, dialogs for viewing, editing, and deleting invoices, and
server-side loading of invoice data
---
backend/api/v1/modules/a76/invoices/models.py | 157 ++--
.../api/v1/modules/a76/invoices/schemas.py | 43 +-
frontend/messages/en.json | 12 +
frontend/messages/es.json | 12 +
.../src/lib/api/dashboard/a76/invoices.ts | 318 ++++++++
.../components/dashboard/invoices/columns.ts | 89 +++
.../invoices/create-edit-dialog.svelte | 677 ++++++++++++++++++
.../invoices/data-table-actions.svelte | 52 ++
.../dashboard/invoices/data-table.svelte | 131 ++++
.../dashboard/invoices/delete-dialog.svelte | 110 +++
.../dashboard/invoices/details-dialog.svelte | 435 +++++++++++
.../src/lib/components/sidebar/modules.ts | 40 ++
.../routes/dashboard/invoices/+page.server.ts | 104 +++
.../routes/dashboard/invoices/+page.svelte | 283 ++++++++
14 files changed, 2381 insertions(+), 82 deletions(-)
create mode 100644 frontend/src/lib/api/dashboard/a76/invoices.ts
create mode 100644 frontend/src/lib/components/dashboard/invoices/columns.ts
create mode 100644 frontend/src/lib/components/dashboard/invoices/create-edit-dialog.svelte
create mode 100644 frontend/src/lib/components/dashboard/invoices/data-table-actions.svelte
create mode 100644 frontend/src/lib/components/dashboard/invoices/data-table.svelte
create mode 100644 frontend/src/lib/components/dashboard/invoices/delete-dialog.svelte
create mode 100644 frontend/src/lib/components/dashboard/invoices/details-dialog.svelte
create mode 100644 frontend/src/routes/dashboard/invoices/+page.server.ts
create mode 100644 frontend/src/routes/dashboard/invoices/+page.svelte
diff --git a/backend/api/v1/modules/a76/invoices/models.py b/backend/api/v1/modules/a76/invoices/models.py
index bed62eaf..7f787def 100644
--- a/backend/api/v1/modules/a76/invoices/models.py
+++ b/backend/api/v1/modules/a76/invoices/models.py
@@ -1,6 +1,6 @@
from enum import Enum
from typing import Optional, List
-from sqlalchemy import BigInteger, Date, ForeignKey, Integer, Numeric, String, Text, TIMESTAMP
+from sqlalchemy import BigInteger, Boolean, Date, ForeignKey, Integer, Numeric, String, Text, TIMESTAMP
from sqlalchemy.orm import Mapped, mapped_column, relationship
from core.database import Base
from datetime import datetime
@@ -11,21 +11,35 @@ class OperationType(str, Enum):
IMP = "imp" # Importación
EXP = "exp" # Exportación
+
+class TransportType(str, Enum):
+ NONE = "none" # Ninguno
+ TRANSPORT = "transport" # Transporte
+ BOX = "box" # Caja
+ PLATES = "licence plates" # Placas
+ TRUCK = "truck" # Camión
+ VESSEL = "vessel" # Buque
+ BARGE = "rail barge" # Ferrobarcaza
+ CONTAINER = "container" # Contenedor
+ AIRPLANE = "airplane" # Avión
+ GONDOLA = "gondola" # Góndola
+ FLATBED = "flatbed" # Plataforma
+
# --- 1. Invoice Header (invoice_header) ---
-
-
class InvoiceHeader(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "invoice_header"
- __tableargs__ = {'schema': 'a76'}
+ __table_args__ = (
+ {"schema": "a76"},
+ )
id: Mapped[int] = mapped_column(
BigInteger, primary_key=True, autoincrement=True)
# Identifiers
operation_type: Mapped[OperationType] = mapped_column(
- String(3)) # TIPOMOVIMIENTO / Clasifica IMP/EXP
- invoice_type: Mapped[Optional[str]] = mapped_column(
- ForeignKey("public.invoice_types.key")) # TIPOFACTURA
+ String(3)) # TIPOMOVIMIENTO / Clasifica imp/exp
+ invoice_type: Mapped[Optional[str]] = mapped_column(ForeignKey(
+ "public.invoice_types.key")) # TIPOFACTURA (invoice_types)
invoice_number: Mapped[Optional[str]] = mapped_column(
String(20)) # FACTURAIMPO/FACTURAEXPO/FACTURAREMISION
project_number: Mapped[Optional[str]] = mapped_column(
@@ -42,20 +56,22 @@ class InvoiceHeader(Base, TenantScopedMixin, TimestampMixin):
TIMESTAMP(timezone=False), default=datetime.now) # FECHACAPTURA + HORAACTUAL
# Status & Control
- status: Mapped[Optional[str]] = mapped_column(String(5)) # ESTATUS
- traffic_light_status: Mapped[Optional[str]] = mapped_column(
- String(50)) # SEMAFORO / SEMAFOROEXPO/IMPO
- process_log: Mapped[Optional[str]] = mapped_column(
- String(300)) # COMOFUEPROCESADA **<-- AÑADIDO**
- # OBSERVACIONE + OBSERVACIONI + COMENTARIOSESTATUS
- comments: Mapped[Optional[str]] = mapped_column(Text)
+ is_updated: Mapped[Optional[str]] = mapped_column(Boolean) # ESTATUS
+ updated_date: Mapped[Optional[datetime]] = mapped_column(TIMESTAMP(timezone=False)) # FECHAACTUALIZACION
+ who_updated: Mapped[Optional[str]] = mapped_column(String(20)) # QUIENA ACTUALIZO
+
+ traffic_light_status: Mapped[Optional[str]] = mapped_column(String(50)) # SEMAFORO / SEMAFOROEXPO/IMPO
+ process_log: Mapped[Optional[str]] = mapped_column(String(300)) # COMOFUEPROCESADA
+
+ # Comments
+ observation_es: Mapped[Optional[str]] = mapped_column(Text) #OBSERVACIONE
+ observation_en: Mapped[Optional[str]] = mapped_column(Text) #OBSERVACIONI
+ comments_status: Mapped[Optional[str]] = mapped_column(Text) #COMENTARIOSESTATUS
# Digital Archive Links
cfdi_uuid: Mapped[Optional[str]] = mapped_column(String(100)) # CFDIUUID
- path_pdf: Mapped[Optional[str]] = mapped_column(
- String(500)) # CFDIPATHPDF **<-- AÑADIDO**
- path_xml: Mapped[Optional[str]] = mapped_column(
- String(500)) # CFDIPATHXML **<-- AÑADIDO**
+ path_pdf: Mapped[Optional[str]] = mapped_column(String(500)) # CFDIPATHPDF
+ path_xml: Mapped[Optional[str]] = mapped_column(String(500)) # CFDIPATHXML
# Relationships (Para navegacion ORM)
compliance_mx: Mapped["InvoiceComplianceMx"] = relationship(
@@ -73,10 +89,12 @@ class InvoiceHeader(Base, TenantScopedMixin, TimestampMixin):
# --- 2. Compliance MX (invoice_compliance_mx) ---
class InvoiceComplianceMx(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "invoice_compliance_mx"
- __tableargs__ = {'schema': 'a76'}
+ __table_args__ = (
+ {"schema": "a76"},
+ )
invoice_id: Mapped[int] = mapped_column(
- ForeignKey("invoice_header.id"), primary_key=True)
+ ForeignKey("a76.invoice_header.id"), primary_key=True)
# Core Customs Data
pedimento: Mapped[Optional[str]] = mapped_column(
@@ -84,23 +102,29 @@ class InvoiceComplianceMx(Base, TenantScopedMixin, TimestampMixin):
pedimento_code: Mapped[Optional[str]] = mapped_column(
String(5)) # PEDIMENTOR1, K1
remesa: Mapped[Optional[int]] = mapped_column(Integer)
- aduana: Mapped[Optional[str]] = mapped_column(String(5)) # ADUANA_CRUCE
- customs_agent: Mapped[Optional[str]] = mapped_column(
- String(10)) # AADUANAL
+ aduana: Mapped[Optional[str]] = mapped_column(ForeignKey("public.customs_sections.customs_code")) # ADUANA_CRUCE
+
+ # Clients & Providers
+ provider_header: Mapped[Optional[str]] = mapped_column(String(20)) # PROVEEDOREXPORTADOR
+ provider_id: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # PROVEEDOR
+ sold_to_header: Mapped[Optional[str]] = mapped_column(String(20)) # VENDIDOCONSIGNADO
+ sold_to_id: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # VENDIDOA
+ shipped_to_header: Mapped[Optional[str]] = mapped_column(String(20)) # ENVIADOTRANSFERIDO
+ shipped_to_id: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # ENVIADOA
+ shipped_by_header: Mapped[Optional[str]] = mapped_column(String(20)) # ENVIADOPORVENDIDOPOR
+ shipped_by_id: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # VENDIDOPOR
+
+ customs_broker_id: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.customs_brokers.id")) # AADUANAL
# Flags & Specific Regimes
- is_mixed: Mapped[Optional[str]] = mapped_column(
- String(2)) # ESMIXTO **<-- AÑADIDO**
- waste_type: Mapped[Optional[str]] = mapped_column(
- String(1)) # TIPODESPERDICIO **<-- AÑADIDO**
+ is_mixed: Mapped[Optional[bool]] = mapped_column(Boolean) # ESMIXTO
+ waste_type: Mapped[Optional[str]] = mapped_column(String(1)) # TIPODESPERDICIO
appendix_17: Mapped[Optional[int]] = mapped_column(Integer) # APENDICE17
# VUCEM / Digital
edocument: Mapped[Optional[str]] = mapped_column(String(50)) # EDOCUMENT
- electronic_signature: Mapped[Optional[str]] = mapped_column(
- String(999)) # FIRMAELECTRONICA
- sem_id: Mapped[Optional[int]] = mapped_column(
- Integer) # SEM (de SFacEntradaSM/SFacSalidaSM)
+ electronic_signature: Mapped[Optional[str]] = mapped_column(String(999)) # FIRMAELECTRONICA
+ sem_id: Mapped[Optional[int]] = mapped_column(Integer) # SEM (de SFacEntradaSM/SFacSalidaSM)
# Relationship
header: Mapped["InvoiceHeader"] = relationship(
@@ -110,40 +134,33 @@ class InvoiceComplianceMx(Base, TenantScopedMixin, TimestampMixin):
# --- 3. Financials (invoice_financials) ---
class InvoiceFinancials(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "invoice_financials"
- __tableargs__ = {'schema': 'a76'}
+ __table_args__ = (
+ {"schema": "a76"},
+ )
- invoice_id: Mapped[int] = mapped_column(
- ForeignKey("invoice_header.id"), primary_key=True)
+ id: Mapped[int] = mapped_column(
+ BigInteger, primary_key=True, autoincrement=True)
+ invoice_id: Mapped[int] = mapped_column(ForeignKey("a76.invoice_header.id"))
currency: Mapped[Optional[str]] = mapped_column(String(3)) # CLAVEMONEDA
- exchange_rate: Mapped[Optional[float]] = mapped_column(
- Numeric(13, 6)) # TIPOCAMBIO
+ currency_type: Mapped[Optional[str]] = mapped_column(ForeignKey("public.currency_types.code")) # TIPOCLAVEMONEDA
+ exchange_rate: Mapped[Optional[float]] = mapped_column(Numeric(13, 6)) # TIPOCAMBIO
# Merchandise Values
- value_mn: Mapped[Optional[float]] = mapped_column(
- Numeric(23, 8), default=0) # VALORIMPOMN/EXPOMN
- value_me: Mapped[Optional[float]] = mapped_column(
- Numeric(23, 8), default=0)
- customs_value_mn: Mapped[Optional[float]] = mapped_column(
- Numeric(23, 8), default=0) # VALORADUANASMN **<-- AÑADIDO**
+ value_mn: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # VALORIMPOMN/EXPOMN
+ value_me: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0)
+ customs_value_mn: Mapped[Optional[float]] = mapped_column( Numeric(23, 8), default=0) # VALORADUANASMN
# Costs & Taxes
- freight: Mapped[Optional[float]] = mapped_column(
- Numeric(19, 8), default=0) # FLETE
- insurance: Mapped[Optional[float]] = mapped_column(
- Numeric(19, 8), default=0) # VALSEGUROS/SEGUROS
- iva_mn: Mapped[Optional[float]] = mapped_column(
- Numeric(23, 8), default=0) # IVAEXPOMN / VALORIVAMN
- iva_factor: Mapped[Optional[float]] = mapped_column(
- Numeric(17, 4)) # FACTORIVA
+ freight: Mapped[Optional[float]] = mapped_column(Numeric(19, 8), default=0) # FLETE
+ insurance: Mapped[Optional[float]] = mapped_column(Numeric(19, 8), default=0) # VALSEGUROS/SEGUROS
+ iva_mn: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # IVAEXPOMN / VALORIVAMN
+ iva_factor: Mapped[Optional[float]] = mapped_column(Numeric(17, 4)) # FACTORIVA
# Weights & Quantities
- total_quantity: Mapped[Optional[float]] = mapped_column(
- Numeric(19, 8)) # CANTEXPO / CANTIMPO
- gross_weight: Mapped[Optional[float]] = mapped_column(
- Numeric(19, 8)) # PESOBRUTO **<-- AÑADIDO**
- net_weight: Mapped[Optional[float]] = mapped_column(
- Numeric(19, 8)) # PESONETO **<-- AÑADIDO**
+ total_quantity: Mapped[Optional[float]] = mapped_column(Numeric(19, 8)) # CANTEXPO / CANTIMPO
+ gross_weight: Mapped[Optional[float]] = mapped_column(Numeric(19, 8)) # PESOBRUTO
+ net_weight: Mapped[Optional[float]] = mapped_column(Numeric(19, 8)) # PESONETO
bundle_count: Mapped[Optional[int]] = mapped_column(Integer) # CANTBULTOS
# Relationship
@@ -153,17 +170,21 @@ class InvoiceFinancials(Base, TenantScopedMixin, TimestampMixin):
# --- 4. Logistics (invoice_logistics) ---
class InvoiceLogistics(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "invoice_logistics"
- __tableargs__ = {'schema': 'a76'}
+ __table_args__ = (
+ {"schema": "a76"},
+ )
- logistics_id: Mapped[int] = mapped_column(
+ id: Mapped[int] = mapped_column(
BigInteger, primary_key=True, autoincrement=True)
- invoice_id: Mapped[int] = mapped_column(ForeignKey("invoice_header.id"))
+ invoice_id: Mapped[int] = mapped_column(ForeignKey("a76.invoice_header.id"))
# Carrier Info
carrier_id: Mapped[Optional[str]] = mapped_column(
String(10)) # TRANSPORTISTA
+ transport_type: Mapped[TransportType] = mapped_column(
+ String(15), default="none") # TRANSPORTE
transport_mode: Mapped[Optional[str]] = mapped_column(
- String(15)) # TRANSPORTE
+ String(15)) # MODTRANS
driver_name: Mapped[Optional[str]] = mapped_column(String(80)) # CONDUCTOR
is_rail: Mapped[Optional[str]] = mapped_column(String(2)) # ESFERROCARRIL
rail_id: Mapped[Optional[str]] = mapped_column(
@@ -189,11 +210,13 @@ class InvoiceLogistics(Base, TenantScopedMixin, TimestampMixin):
# --- 5. Sales Order Details (invoice_sales_details) ---
class InvoiceSalesDetails(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "invoice_sales_details"
- __tableargs__ = {'schema': 'a76'}
+ __table_args__ = (
+ {"schema": "a76"},
+ )
- detail_id: Mapped[int] = mapped_column(
+ id: Mapped[int] = mapped_column(
BigInteger, primary_key=True, autoincrement=True)
- invoice_id: Mapped[int] = mapped_column(ForeignKey("invoice_header.id"))
+ invoice_id: Mapped[int] = mapped_column(ForeignKey("a76.invoice_header.id"))
line_number: Mapped[int] = mapped_column(Integer) # LINEA
sales_order: Mapped[Optional[str]] = mapped_column(
@@ -214,11 +237,13 @@ class InvoiceSalesDetails(Base, TenantScopedMixin, TimestampMixin):
# --- 6. Collections (invoice_collections) ---
class InvoiceCollections(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "invoice_collections"
- __tableargs__ = {'schema': 'a76'}
+ __table_args__ = (
+ {"schema": "a76"},
+ )
- collection_id: Mapped[int] = mapped_column(
+ id: Mapped[int] = mapped_column(
BigInteger, primary_key=True, autoincrement=True)
- invoice_id: Mapped[int] = mapped_column(ForeignKey("invoice_header.id"))
+ invoice_id: Mapped[int] = mapped_column(ForeignKey("a76.invoice_header.id"))
concept: Mapped[Optional[str]] = mapped_column(String(100)) # CONCEPTO
is_collected: Mapped[Optional[int]] = mapped_column(Integer) # COBRADO
diff --git a/backend/api/v1/modules/a76/invoices/schemas.py b/backend/api/v1/modules/a76/invoices/schemas.py
index 70280e46..8075cd94 100644
--- a/backend/api/v1/modules/a76/invoices/schemas.py
+++ b/backend/api/v1/modules/a76/invoices/schemas.py
@@ -5,24 +5,35 @@ from pydantic import BaseModel, Field
from .models import OperationType
-
# --- Base Schemas ---
class InvoiceHeaderBase(BaseModel):
"""Base fields for Invoice Header"""
- operation_type: Optional[OperationType] = Field(None, max_length=20, description="Operation type: imp/exp")
- invoice_type: Optional[str] = Field(None, max_length=5, description="Invoice type key")
- invoice_number: Optional[str] = Field(None, max_length=20, description="Invoice number")
- project_number: Optional[str] = Field(None, max_length=14, description="Project number")
- purchase_order: Optional[str] = Field(None, max_length=50, description="Purchase order")
- related_doc_id: Optional[int] = Field(None, description="Related document ID for rectifications")
+ operation_type: Optional[OperationType] = Field(
+ None, max_length=20, description="Operation type: imp/exp")
+ invoice_type: Optional[str] = Field(
+ None, max_length=5, description="Invoice type key")
+ invoice_number: Optional[str] = Field(
+ None, max_length=20, description="Invoice number")
+ project_number: Optional[str] = Field(
+ None, max_length=14, description="Project number")
+ purchase_order: Optional[str] = Field(
+ None, max_length=50, description="Purchase order")
+ related_doc_id: Optional[int] = Field(
+ None, description="Related document ID for rectifications")
invoice_date: Optional[date] = Field(None, description="Invoice date")
- status: Optional[str] = Field(None, max_length=5, description="Status")
- traffic_light_status: Optional[str] = Field(None, max_length=50, description="Traffic light status (SEMAFORO)")
- process_log: Optional[str] = Field(None, max_length=300, description="Processing log")
- comments: Optional[str] = Field(None, description="Comments and observations")
- cfdi_uuid: Optional[str] = Field(None, max_length=100, description="CFDI UUID")
- path_pdf: Optional[str] = Field(None, max_length=500, description="Path to PDF file")
- path_xml: Optional[str] = Field(None, max_length=500, description="Path to XML file")
+ is_updated: Optional[bool] = Field(None, description="Status")
+ traffic_light_status: Optional[str] = Field(
+ None, max_length=50, description="Traffic light status (SEMAFORO)")
+ process_log: Optional[str] = Field(
+ None, max_length=300, description="Processing log")
+ comments_status: Optional[str] = Field(
+ None, description="Comments and observations")
+ cfdi_uuid: Optional[str] = Field(
+ None, max_length=100, description="CFDI UUID")
+ path_pdf: Optional[str] = Field(
+ None, max_length=500, description="Path to PDF file")
+ path_xml: Optional[str] = Field(
+ None, max_length=500, description="Path to XML file")
class InvoiceComplianceMxBase(BaseModel):
@@ -36,8 +47,8 @@ class InvoiceComplianceMxBase(BaseModel):
None, max_length=5, description="Customs office")
customs_agent: Optional[str] = Field(
None, max_length=10, description="Customs agent")
- is_mixed: Optional[str] = Field(
- None, max_length=2, description="Is mixed operation")
+ is_mixed: Optional[bool] = Field(
+ None, description="Is mixed operation")
waste_type: Optional[str] = Field(
None, max_length=1, description="Waste type")
appendix_17: Optional[int] = Field(None, description="Appendix 17")
diff --git a/frontend/messages/en.json b/frontend/messages/en.json
index 2e0e9afd..86f3f4ce 100644
--- a/frontend/messages/en.json
+++ b/frontend/messages/en.json
@@ -72,6 +72,18 @@
"customs_sections": "Customs Sections",
"anexo_22_app_31": "Anexo 22 App 3"
},
+ "import_invoices":{
+ "title": "Import Invoices",
+ "temporary": "Temporary",
+ "definitive": "Definitive",
+ "mexican_purchases": "Mexican Purchases",
+ "regime_change": "Regime Change"
+ },
+ "export_invoices": {
+ "title": "Export Invoices",
+ "exportation": "Exportation",
+ "repair": "Repair"
+ },
"clients_and_providers": "Clients and Providers",
"customs_brokers": "Customs Brokers",
"nav_user": {
diff --git a/frontend/messages/es.json b/frontend/messages/es.json
index ae2806e5..af9b2e69 100644
--- a/frontend/messages/es.json
+++ b/frontend/messages/es.json
@@ -72,6 +72,18 @@
"customs_sections": "Secciones Aduaneras",
"anexo_22_app_31": "Anexo 22 App 3"
},
+ "import_invoices":{
+ "title": "Facturas de importación",
+ "temporary": "Temporal",
+ "definitive": "Definitiva",
+ "mexican_purchases": "Compras mexicanas",
+ "regime_change": "Cambio de régimen"
+ },
+ "export_invoices": {
+ "title": "Facturas de exportación",
+ "exportation": "Exportación",
+ "repair": "Reparación"
+ },
"clients_and_providers": "Clientes y Proveedores",
"customs_brokers": "Agentes Aduanales",
"nav_user": {
diff --git a/frontend/src/lib/api/dashboard/a76/invoices.ts b/frontend/src/lib/api/dashboard/a76/invoices.ts
new file mode 100644
index 00000000..99a23601
--- /dev/null
+++ b/frontend/src/lib/api/dashboard/a76/invoices.ts
@@ -0,0 +1,318 @@
+/**
+ * API Client para Facturas (Invoices)
+ * Gestiona las operaciones CRUD para facturas y sus relaciones
+ */
+import { api } from '$lib/api';
+
+export type OperationType = 'imp' | 'exp';
+export type TransportType = 'none' | 'transport' | 'box' | 'licence plates' | 'truck' | 'vessel' | 'rail barge' | 'container' | 'airplane' | 'gondola' | 'flatbed';
+
+// --- Interfaces ---
+
+export interface InvoiceComplianceMx {
+ invoice_id?: number;
+ pedimento?: string | null;
+ pedimento_code?: string | null;
+ remesa?: number | null;
+ aduana?: string | null;
+ provider_header?: string | null;
+ provider_id?: string | null;
+ sold_to_header?: string | null;
+ sold_to_id?: string | null;
+ shipped_to_header?: string | null;
+ shipped_to_id?: string | null;
+ shipped_by_header?: string | null;
+ shipped_by_id?: string | null;
+ customs_broker_id?: string | null;
+ is_mixed?: boolean | null;
+ waste_type?: string | null;
+ appendix_17?: number | null;
+ edocument?: string | null;
+ electronic_signature?: string | null;
+ sem_id?: number | null;
+}
+
+export interface InvoiceFinancials {
+ id?: number;
+ invoice_id?: number;
+ currency?: string | null;
+ currency_type?: string | null;
+ exchange_rate?: number | null;
+ value_mn?: number | null;
+ value_me?: number | null;
+ customs_value_mn?: number | null;
+ freight?: number | null;
+ insurance?: number | null;
+ iva_mn?: number | null;
+ iva_factor?: number | null;
+ total_quantity?: number | null;
+ gross_weight?: number | null;
+ net_weight?: number | null;
+ bundle_count?: number | null;
+}
+
+export interface InvoiceLogistics {
+ id?: number;
+ invoice_id?: number;
+ carrier_id?: string | null;
+ transport_type?: TransportType | null;
+ transport_mode?: string | null;
+ driver_name?: string | null;
+ is_rail?: string | null;
+ rail_id?: string | null;
+ vehicle_num?: string | null;
+ license_plate?: string | null;
+ seal_number?: string | null;
+ guide_number?: string | null;
+ entry_exit_date?: string | null;
+}
+
+export interface InvoiceSalesDetails {
+ id?: number;
+ invoice_id?: number;
+ line_number: number;
+ sales_order?: string | null;
+ colors_description?: string | null;
+ square_color_code?: string | null;
+ line_bundles?: number | null;
+}
+
+export interface InvoiceCollections {
+ id?: number;
+ invoice_id?: number;
+ concept?: string | null;
+ is_collected?: number | null;
+ collection_date?: string | null;
+ amount?: number | null;
+ collector_user?: string | null;
+}
+
+export interface Invoice {
+ id: number;
+ tenant_id: number;
+ company_id: number;
+ operation_type?: OperationType | null;
+ invoice_type?: string | null;
+ invoice_number?: string | null;
+ project_number?: string | null;
+ purchase_order?: string | null;
+ related_doc_id?: number | null;
+ invoice_date?: string | null;
+ capture_date: string;
+ is_updated?: boolean | null;
+ updated_date?: string | null;
+ who_updated?: string | null;
+ traffic_light_status?: string | null;
+ process_log?: string | null;
+ observation_es?: string | null;
+ observation_en?: string | null;
+ comments_status?: string | null;
+ cfdi_uuid?: string | null;
+ path_pdf?: string | null;
+ path_xml?: string | null;
+ compliance_mx?: InvoiceComplianceMx | null;
+ financials?: InvoiceFinancials | null;
+ logistics?: InvoiceLogistics[];
+ details?: InvoiceSalesDetails[];
+ collections?: InvoiceCollections[];
+}
+
+export interface InvoiceListResponse {
+ items: Invoice[];
+ total: number;
+ page: number;
+ page_size: number;
+}
+
+export interface CreateInvoiceData {
+ operation_type?: OperationType | null;
+ invoice_type?: string | null;
+ invoice_number?: string | null;
+ project_number?: string | null;
+ purchase_order?: string | null;
+ related_doc_id?: number | null;
+ invoice_date?: string | null;
+ traffic_light_status?: string | null;
+ process_log?: string | null;
+ observation_es?: string | null;
+ observation_en?: string | null;
+ comments_status?: string | null;
+ cfdi_uuid?: string | null;
+ path_pdf?: string | null;
+ path_xml?: string | null;
+ compliance_mx?: Omit | null;
+ financials?: Omit | null;
+ logistics?: Omit[] | null;
+ details?: Omit[] | null;
+ collections?: Omit[] | null;
+}
+
+export interface UpdateInvoiceData {
+ operation_type?: OperationType | null;
+ invoice_type?: string | null;
+ invoice_number?: string | null;
+ project_number?: string | null;
+ purchase_order?: string | null;
+ related_doc_id?: number | null;
+ invoice_date?: string | null;
+ traffic_light_status?: string | null;
+ process_log?: string | null;
+ observation_es?: string | null;
+ observation_en?: string | null;
+ comments_status?: string | null;
+ cfdi_uuid?: string | null;
+ path_pdf?: string | null;
+ path_xml?: string | null;
+ compliance_mx?: Partial | null;
+ financials?: Partial | null;
+ logistics?: Partial[] | null;
+ details?: Partial[] | null;
+ collections?: Partial[] | null;
+}
+
+/**
+ * API para Facturas
+ */
+export const invoicesApi = {
+ /**
+ * Lista todas las facturas con paginación
+ */
+ list: (companyId: number, page = 1, pageSize = 50, filters?: Record) => {
+ const params = new URLSearchParams({
+ company_id: companyId.toString(),
+ page: page.toString(),
+ page_size: pageSize.toString()
+ });
+
+ // Agregar filtros si existen
+ if (filters) {
+ Object.entries(filters).forEach(([key, value]) => {
+ if (value !== null && value !== undefined && value !== '') {
+ params.append(key, String(value));
+ }
+ });
+ }
+
+ return api.get(`/v1/a76/invoices?${params.toString()}`);
+ },
+
+ /**
+ * Obtiene una factura por ID
+ */
+ get: (invoiceId: number, companyId: number) => {
+ const params = new URLSearchParams({
+ company_id: companyId.toString()
+ });
+ return api.get(`/v1/a76/invoices/${invoiceId}?${params.toString()}`);
+ },
+
+ /**
+ * Crea una nueva factura
+ */
+ create: (companyId: number, data: CreateInvoiceData) => {
+ const params = new URLSearchParams({
+ company_id: companyId.toString()
+ });
+ return api.post(`/v1/a76/invoices?${params.toString()}`, data);
+ },
+
+ /**
+ * Actualiza una factura existente
+ */
+ update: (invoiceId: number, companyId: number, data: UpdateInvoiceData) => {
+ const params = new URLSearchParams({
+ company_id: companyId.toString()
+ });
+ return api.put(`/v1/a76/invoices/${invoiceId}?${params.toString()}`, data);
+ },
+
+ /**
+ * Elimina una factura
+ */
+ delete: (invoiceId: number, companyId: number) => {
+ const params = new URLSearchParams({
+ company_id: companyId.toString()
+ });
+ return api.delete(`/v1/a76/invoices/${invoiceId}?${params.toString()}`);
+ },
+
+ // --- Nested Resources ---
+
+ /**
+ * Logística de factura
+ */
+ logistics: {
+ list: (invoiceId: number, companyId: number) => {
+ const params = new URLSearchParams({
+ company_id: companyId.toString()
+ });
+ return api.get(`/v1/a76/invoices/${invoiceId}/logistics?${params.toString()}`);
+ },
+
+ create: (invoiceId: number, companyId: number, data: Omit) => {
+ const params = new URLSearchParams({
+ company_id: companyId.toString()
+ });
+ return api.post(`/v1/a76/invoices/${invoiceId}/logistics?${params.toString()}`, data);
+ },
+
+ delete: (invoiceId: number, logisticsId: number, companyId: number) => {
+ const params = new URLSearchParams({
+ company_id: companyId.toString()
+ });
+ return api.delete(`/v1/a76/invoices/${invoiceId}/logistics/${logisticsId}?${params.toString()}`);
+ }
+ },
+
+ /**
+ * Detalles de venta de factura
+ */
+ details: {
+ list: (invoiceId: number, companyId: number) => {
+ const params = new URLSearchParams({
+ company_id: companyId.toString()
+ });
+ return api.get(`/v1/a76/invoices/${invoiceId}/details?${params.toString()}`);
+ },
+
+ create: (invoiceId: number, companyId: number, data: Omit) => {
+ const params = new URLSearchParams({
+ company_id: companyId.toString()
+ });
+ return api.post(`/v1/a76/invoices/${invoiceId}/details?${params.toString()}`, data);
+ },
+
+ delete: (invoiceId: number, detailId: number, companyId: number) => {
+ const params = new URLSearchParams({
+ company_id: companyId.toString()
+ });
+ return api.delete(`/v1/a76/invoices/${invoiceId}/details/${detailId}?${params.toString()}`);
+ }
+ },
+
+ /**
+ * Cobranzas de factura
+ */
+ collections: {
+ list: (invoiceId: number, companyId: number) => {
+ const params = new URLSearchParams({
+ company_id: companyId.toString()
+ });
+ return api.get(`/v1/a76/invoices/${invoiceId}/collections?${params.toString()}`);
+ },
+
+ create: (invoiceId: number, companyId: number, data: Omit) => {
+ const params = new URLSearchParams({
+ company_id: companyId.toString()
+ });
+ return api.post(`/v1/a76/invoices/${invoiceId}/collections?${params.toString()}`, data);
+ },
+
+ delete: (invoiceId: number, collectionId: number, companyId: number) => {
+ const params = new URLSearchParams({
+ company_id: companyId.toString()
+ });
+ return api.delete(`/v1/a76/invoices/${invoiceId}/collections/${collectionId}?${params.toString()}`);
+ }
+ }
+};
diff --git a/frontend/src/lib/components/dashboard/invoices/columns.ts b/frontend/src/lib/components/dashboard/invoices/columns.ts
new file mode 100644
index 00000000..d7f03fc5
--- /dev/null
+++ b/frontend/src/lib/components/dashboard/invoices/columns.ts
@@ -0,0 +1,89 @@
+/**
+ * Definición de columnas para la tabla de facturas
+ */
+import type { Invoice } from '$lib/api/dashboard/a76/invoices';
+import DataTableActions from './data-table-actions.svelte';
+
+export function createColumns() {
+ return [
+ {
+ accessorKey: 'id',
+ header: 'ID',
+ cell: (info: any) => info.getValue(),
+ enableSorting: true
+ },
+ {
+ accessorKey: 'operation_type',
+ header: 'Tipo',
+ cell: (info: any) => {
+ const type = info.getValue();
+ return type === 'imp' ? 'Importación' : type === 'exp' ? 'Exportación' : '-';
+ }
+ },
+ {
+ accessorKey: 'invoice_number',
+ header: 'Número de Factura',
+ cell: (info: any) => info.getValue() || '-'
+ },
+ {
+ accessorKey: 'invoice_type',
+ header: 'Tipo Factura',
+ cell: (info: any) => info.getValue() || '-'
+ },
+ {
+ accessorKey: 'project_number',
+ header: 'Proyecto',
+ cell: (info: any) => info.getValue() || '-'
+ },
+ {
+ accessorKey: 'compliance_mx.pedimento',
+ header: 'Pedimento',
+ cell: (info: any) => {
+ const row = info.row.original;
+ return row.compliance_mx?.pedimento || '-';
+ }
+ },
+ {
+ accessorKey: 'invoice_date',
+ header: 'Fecha Factura',
+ cell: (info: any) => {
+ const date = info.getValue();
+ if (!date) return '-';
+ return new Date(date).toLocaleDateString('es-MX');
+ }
+ },
+ {
+ accessorKey: 'financials.value_mn',
+ header: 'Valor MN',
+ cell: (info: any) => {
+ const row = info.row.original;
+ const value = row.financials?.value_mn;
+ if (value === null || value === undefined) return '-';
+ return new Intl.NumberFormat('es-MX', {
+ style: 'currency',
+ currency: 'MXN'
+ }).format(value);
+ }
+ },
+ {
+ accessorKey: 'traffic_light_status',
+ header: 'Semáforo',
+ cell: (info: any) => info.getValue() || '-'
+ },
+ {
+ accessorKey: 'capture_date',
+ header: 'Fecha Captura',
+ cell: (info: any) => {
+ const date = info.getValue();
+ if (!date) return '-';
+ return new Date(date).toLocaleDateString('es-MX');
+ }
+ },
+ {
+ id: 'actions',
+ header: 'Acciones',
+ cell: (info: any) => DataTableActions,
+ enableSorting: false
+ }
+ ];
+}
diff --git a/frontend/src/lib/components/dashboard/invoices/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/invoices/create-edit-dialog.svelte
new file mode 100644
index 00000000..6b523b0c
--- /dev/null
+++ b/frontend/src/lib/components/dashboard/invoices/create-edit-dialog.svelte
@@ -0,0 +1,677 @@
+
+
+
+
+
+
+ {isEditing ? "Editar Factura" : "Nueva Factura"}
+
+
+ {isEditing
+ ? "Modifica los datos de la factura"
+ : "Ingresa los datos de la nueva factura"}
+
+
+
+
+
+
diff --git a/frontend/src/lib/components/dashboard/invoices/data-table-actions.svelte b/frontend/src/lib/components/dashboard/invoices/data-table-actions.svelte
new file mode 100644
index 00000000..13367343
--- /dev/null
+++ b/frontend/src/lib/components/dashboard/invoices/data-table-actions.svelte
@@ -0,0 +1,52 @@
+
+
+
+
+ {#snippet child({ props })}
+
+ {/snippet}
+
+
+ Acciones
+
+
+
+ Ver Detalles
+
+
+
+ Editar
+
+
+
+
+ Eliminar
+
+
+
diff --git a/frontend/src/lib/components/dashboard/invoices/data-table.svelte b/frontend/src/lib/components/dashboard/invoices/data-table.svelte
new file mode 100644
index 00000000..14c41b7e
--- /dev/null
+++ b/frontend/src/lib/components/dashboard/invoices/data-table.svelte
@@ -0,0 +1,131 @@
+
+
+
+
+
+
+ {#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
+
+ {#each headerGroup.headers as header (header.id)}
+
+ {#if !header.isPlaceholder}
+
+ {/if}
+
+ {/each}
+
+ {/each}
+
+
+ {#each table.getRowModel().rows as row (row.id)}
+
+ {#each row.getVisibleCells() as cell (cell.id)}
+
+ {#if cell.column.id === 'actions'}
+ {@const cellDef = cell.column.columnDef.cell}
+ {#if cellDef && typeof cellDef === 'function'}
+ {@const Component = cellDef(cell.getContext())}
+
+ {/if}
+ {:else}
+
+ {/if}
+
+ {/each}
+
+ {:else}
+
+
+ No hay resultados.
+
+
+ {/each}
+
+
+ {#if hasMore}
+
+
+
+ {#if loading}
+
+ {:else}
+
+ Desplázate para cargar más
+
+ {/if}
+
+
+
+ {/if}
+
+
+
+
diff --git a/frontend/src/lib/components/dashboard/invoices/delete-dialog.svelte b/frontend/src/lib/components/dashboard/invoices/delete-dialog.svelte
new file mode 100644
index 00000000..f33d7937
--- /dev/null
+++ b/frontend/src/lib/components/dashboard/invoices/delete-dialog.svelte
@@ -0,0 +1,110 @@
+
+
+
+
+
+ ¿Estás seguro?
+
+ Esta acción no se puede deshacer. Se eliminará permanentemente esta factura:
+ {#if item}
+
+
+ ID:
+ {item.id}
+
+
+ Número de Factura:
+ {item.invoice_number || 'N/A'}
+
+
+ Tipo:
+
+ {item.operation_type === 'imp' ? 'Importación' :
+ item.operation_type === 'exp' ? 'Exportación' : 'N/A'}
+
+
+
+ Proyecto:
+ {item.project_number || 'N/A'}
+
+
+ Pedimento:
+ {item.compliance_mx?.pedimento || 'N/A'}
+
+
+ {/if}
+ {#if error}
+
+ {error}
+
+ {/if}
+
+
+
+ Cancelar
+
+ {#if loading}
+
+ {/if}
+ Eliminar
+
+
+
+
diff --git a/frontend/src/lib/components/dashboard/invoices/details-dialog.svelte b/frontend/src/lib/components/dashboard/invoices/details-dialog.svelte
new file mode 100644
index 00000000..e517faa3
--- /dev/null
+++ b/frontend/src/lib/components/dashboard/invoices/details-dialog.svelte
@@ -0,0 +1,435 @@
+
+
+ (open = v)}>
+
+
+ Detalles de Factura #{invoice?.id}
+
+ Información completa de la factura
+
+
+
+ {#if invoice}
+
+
+ General
+ Cumplimiento
+ Financieros
+ Logística
+ Detalles
+
+
+
+
+
+
+
Tipo de Operación
+
+ {#if invoice.operation_type === 'imp'}
+ Importación
+ {:else if invoice.operation_type === 'exp'}
+ Exportación
+ {:else}
+ -
+ {/if}
+
+
+
+
+
Número de Factura
+
{invoice.invoice_number || '-'}
+
+
+
+
Tipo de Factura
+
{invoice.invoice_type || '-'}
+
+
+
+
Número de Proyecto
+
{invoice.project_number || '-'}
+
+
+
+
Orden de Compra
+
{invoice.purchase_order || '-'}
+
+
+
+
Fecha de Factura
+
{formatDate(invoice.invoice_date)}
+
+
+
+
Fecha de Captura
+
{formatDate(invoice.capture_date)}
+
+
+
+
Semáforo
+
{invoice.traffic_light_status || '-'}
+
+
+
+
CFDI UUID
+
{invoice.cfdi_uuid || '-'}
+
+
Actualizado
+
{invoice.is_updated ? 'Sí' : 'No'}
+
+
+
+
Observaciones (ES)
+
{invoice.observation_es || '-'}
+
+
+
+
Observaciones (EN)
+
{invoice.observation_en || '-'}
+
+
+
+
Log de Proceso
+
{invoice.process_log || '-'}
+
+
+
+
+
+
+ {#if invoice.compliance_mx}
+
+
+
Pedimento
+
{invoice.compliance_mx.pedimento || '-'}
+
+
+
+
Código de Pedimento
+
{invoice.compliance_mx.pedimento_code || '-'}
+
+
+
+
Remesa
+
{invoice.compliance_mx.remesa || '-'}
+
+
+
+
Aduana
+
{invoice.compliance_mx.aduana || '-'}
+
+
+
+
Agente Aduanal ID
+
{invoice.compliance_mx.customs_broker_id || '-'}
+
+
+
+
Proveedor
+
{invoice.compliance_mx.provider_id || '-'}
+
+
+
+
Vendido A
+
{invoice.compliance_mx.sold_to_id || '-'}
+
+
+
+
Enviado A
+
{invoice.compliance_mx.shipped_to_id || '-'}
+
+
+
+
Enviado Por
+
{invoice.compliance_mx.shipped_by_id || '-'}
+
+
+
+
Operación Mixta
+
{invoice.compliance_mx.is_mixed ? 'Sí' : 'No'}
+
+
+
+
Tipo de Desperdicio
+
{invoice.compliance_mx.waste_type || '-'}
+
+
+
+
Apéndice 17
+
{invoice.compliance_mx.appendix_17 || '-'}
+
+
+
+
E-Document
+
{invoice.compliance_mx.edocument || '-'}
+
+
+
+
Firma Electrónica
+
{invoice.compliance_mx.electronic_signature || '-'}
+
+
+ {:else}
+ No hay información de cumplimiento disponible.
+ {/if}
+
+
+
+
+ {#if invoice.financials}
+
+
+
Moneda
+
{invoice.financials.currency || '-'}
+
+
+
+
Tipo de Cambio
+
{formatNumber(invoice.financials.exchange_rate)}
+
+
+
+
Valor MN
+
{formatCurrency(invoice.financials.value_mn)}
+
+
+
+
Valor ME
+
{formatNumber(invoice.financials.value_me)}
+
+
+
+
Valor Aduana MN
+
{formatCurrency(invoice.financials.customs_value_mn)}
+
+
+
+
Flete
+
{formatCurrency(invoice.financials.freight)}
+
+
+
+
Seguro
+
{formatCurrency(invoice.financials.insurance)}
+
+
+
+
IVA MN
+
{formatCurrency(invoice.financials.iva_mn)}
+
+
+
+
Factor IVA
+
{formatNumber(invoice.financials.iva_factor)}
+
+
+
+
Cantidad Total
+
{formatNumber(invoice.financials.total_quantity)}
+
+
+
+
Peso Bruto
+
{formatNumber(invoice.financials.gross_weight)}
+
+
+
+
Peso Neto
+
{formatNumber(invoice.financials.net_weight)}
+
+
+
+
Número de Bultos
+
{invoice.financials.bundle_count || '-'}
+
+
+ {:else}
+ No hay información financiera disponible.
+ {/if}
+
+
+
+
+ {#if invoice.logistics && invoice.logistics.length > 0}
+
+ {#each invoice.logistics as logistics, index}
+
+
Logística #{index + 1}
+
+
+
Transportista
+
{logistics.carrier_id || '-'}
+
+
+
+
Tipo de Transporte
+
{logistics.transport_type || '-'}
+
+
+
+
Modo de Transporte
+
{logistics.transport_mode || '-'}
+
+
+
+
Conductor
+
{logistics.driver_name || '-'}
+
+
+
+
Número de Vehículo
+
{logistics.vehicle_num || '-'}
+
+
+
+
Placa
+
{logistics.license_plate || '-'}
+
+
+
+
Número de Sello
+
{logistics.seal_number || '-'}
+
+
+
+
Guía
+
{logistics.guide_number || '-'}
+
+
+
+
Fecha Entrada/Salida
+
{formatDate(logistics.entry_exit_date)}
+
+
+
+ {/each}
+
+ {:else}
+ No hay información de logística disponible.
+ {/if}
+
+
+
+
+ {#if invoice.details && invoice.details.length > 0}
+
+
+
Detalles de Venta
+
+ {#each invoice.details as detail}
+
+
+
+
Línea
+
{detail.line_number}
+
+
+
+
Orden de Venta
+
{detail.sales_order || '-'}
+
+
+
+
Descripción de Colores
+
{detail.colors_description || '-'}
+
+
+
+
Código de Color
+
{detail.square_color_code || '-'}
+
+
+
+
Bultos
+
{detail.line_bundles || '-'}
+
+
+
+ {/each}
+
+
+
+ {#if invoice.collections && invoice.collections.length > 0}
+
+
Cobranzas
+
+ {#each invoice.collections as collection}
+
+
+
+
Concepto
+
{collection.concept || '-'}
+
+
+
+
Monto
+
{formatCurrency(collection.amount)}
+
+
+
+
Fecha de Cobranza
+
{formatDate(collection.collection_date)}
+
+
+
+
Cobrado
+
{collection.is_collected ? 'Sí' : 'No'}
+
+
+
+
Cobrador
+
{collection.collector_user || '-'}
+
+
+
+ {/each}
+
+
+ {/if}
+
+ {:else}
+ No hay detalles de venta o cobranzas disponibles.
+ {/if}
+
+
+ {/if}
+
+
+
+
+
+
diff --git a/frontend/src/lib/components/sidebar/modules.ts b/frontend/src/lib/components/sidebar/modules.ts
index 56c7e58d..60662981 100644
--- a/frontend/src/lib/components/sidebar/modules.ts
+++ b/frontend/src/lib/components/sidebar/modules.ts
@@ -1,4 +1,6 @@
import {
+ ArrowDownToLine,
+ ArrowUpFromLine,
BadgeCheck,
ChartPie,
Database,
@@ -314,6 +316,44 @@ export function getSidebarData(): SidebarData {
},
],
},
+ {
+ title: m["sidebar.import_invoices.title"](),
+ url: "#",
+ icon: ArrowDownToLine,
+ items: [
+ {
+ title: m["sidebar.import_invoices.temporary"](),
+ url: "/dashboard/invoices",
+ },
+ {
+ title: m["sidebar.import_invoices.definitive"](),
+ url: "/dashboard/invoices",
+ },
+ {
+ title: m["sidebar.import_invoices.mexican_purchases"](),
+ url: "/dashboard/invoices",
+ },
+ {
+ title: m["sidebar.import_invoices.regime_change"](),
+ url: "/dashboard/invoices",
+ }
+ ],
+ },
+ {
+ title: m["sidebar.export_invoices.title"](),
+ url: "#",
+ icon: ArrowUpFromLine,
+ items: [
+ {
+ title: m["sidebar.export_invoices.exportation"](),
+ url: "/dashboard/invoices",
+ },
+ {
+ title: m["sidebar.export_invoices.repair"](),
+ url: "/dashboard/invoices",
+ },
+ ],
+ },
{
title: m["sidebar.clients_and_providers"](),
url: "/dashboard/clients_and_providers",
diff --git a/frontend/src/routes/dashboard/invoices/+page.server.ts b/frontend/src/routes/dashboard/invoices/+page.server.ts
new file mode 100644
index 00000000..8b9ed8bb
--- /dev/null
+++ b/frontend/src/routes/dashboard/invoices/+page.server.ts
@@ -0,0 +1,104 @@
+import type { PageServerLoad } from './$types';
+import { redirect } from '@sveltejs/kit';
+import {
+ getAuthTokens,
+ authenticatedFetch
+} from '$lib/server/api';
+
+export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
+ // Esperar a que el layout padre valide/refresque el token
+ const parentData = await parent();
+
+ // Verificar autenticación
+ const { accessToken } = getAuthTokens(cookies);
+
+ if (!accessToken) {
+ throw redirect(302, '/login');
+ }
+
+ try {
+ // Obtener company_id de múltiples fuentes (en orden de prioridad):
+ // 1. URL query param (permite cambiar vía navegación)
+ // 2. Cookie active_company_id (setted por el team-switcher)
+ // 3. Primera compañía del usuario (fallback)
+ const companyIdParam = url.searchParams.get('company_id');
+ const cookieCompanyId = cookies.get('active_company_id');
+
+ const companyId = companyIdParam
+ ? parseInt(companyIdParam)
+ : cookieCompanyId
+ ? parseInt(cookieCompanyId)
+ : parentData.companies?.[0]?.id;
+
+ // Si aún no hay companyId, mostrar error
+ if (!companyId) {
+ return {
+ items: [],
+ total: 0,
+ page: 1,
+ page_size: 50,
+ error: 'No se encontró una compañía seleccionada',
+ companies: parentData.companies || []
+ };
+ }
+
+ // Obtener filtro de tipo de operación
+ const operationType = url.searchParams.get('operation_type');
+
+ // Construir parámetros de consulta
+ const params = new URLSearchParams({
+ company_id: companyId.toString(),
+ page: '1',
+ page_size: '50'
+ });
+
+ // Agregar filtro de tipo si existe y no es 'all'
+ if (operationType && operationType !== 'all') {
+ params.append('operation_type', operationType);
+ }
+
+ // Usar authenticatedFetch para manejar automáticamente el refresh de tokens
+ const response = await authenticatedFetch(
+ `v1/a76/invoices?${params.toString()}`,
+ {},
+ cookies,
+ fetch,
+ '/login'
+ );
+
+ if (!response.ok) {
+ return {
+ items: [],
+ total: 0,
+ page: 1,
+ page_size: 50,
+ error: 'Error al cargar facturas',
+ companies: parentData.companies || [],
+ currentCompanyId: companyId,
+ operationType: operationType || 'all'
+ };
+ }
+
+ const data = await response.json();
+
+ return {
+ items: data.items || [],
+ total: data.total || 0,
+ page: data.page || 1,
+ page_size: data.page_size || 50,
+ companies: parentData.companies || [],
+ currentCompanyId: companyId,
+ operationType: operationType || 'all'
+ };
+ } catch (error) {
+ console.error('Error loading invoices:', error);
+ return {
+ items: [],
+ total: 0,
+ page: 1,
+ page_size: 50,
+ error: 'Error al cargar facturas',
+ companies: parentData.companies || []
+ };
+ }
+};
diff --git a/frontend/src/routes/dashboard/invoices/+page.svelte b/frontend/src/routes/dashboard/invoices/+page.svelte
new file mode 100644
index 00000000..c9fee531
--- /dev/null
+++ b/frontend/src/routes/dashboard/invoices/+page.svelte
@@ -0,0 +1,283 @@
+
+
+
+
+
+
+
Facturas
+
+ Gestiona las facturas de importación y exportación
+
+
+
+
+
+
+ {#if error}
+
+
+ Error
+ {error}
+
+
+ {/if}
+
+
+
+
+
+
+ Listado de Facturas
+
+ Mostrando {allItems.length} de {totalItems} registros
+ {#if companyStore.activeCompany}
+ - Compañía: {companyStore.activeCompany.name}
+ {/if}
+
+
+
+
+
+ {selectedType === 'all' ? 'Todas' : selectedType === 'imp' ? 'Importación' : 'Exportación'}
+
+
+ Todas
+ Importación
+ Exportación
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
From a07aeb7b12370af6020d63663ec3fcebce9adbbb Mon Sep 17 00:00:00 2001
From: acazares
Date: Fri, 12 Dec 2025 08:44:09 -0600
Subject: [PATCH 06/15] feat: Enhance invoice management with operation and
invoice type filters, update dialog defaults, and modify routes for improved
functionality
---
backend/api/v1/common/tenant_crud_routes.py | 74 ++++++++-----
.../api/v1/modules/a76/invoices/services.py | 7 ++
.../reference_data/invoice_types/dto.py | 1 +
.../reference_data/invoice_types/routes.py | 25 +++--
.../reference_data/invoice_types/seed.py | 22 ++--
.../dashboard/refrence_data/invoice_types.ts | 18 +++-
frontend/src/lib/assets/favicon.svg | 14 ++-
.../invoices/create-edit-dialog.svelte | 8 +-
.../src/lib/components/sidebar/modules.ts | 12 +--
.../routes/dashboard/invoices/+page.server.ts | 16 ++-
.../routes/dashboard/invoices/+page.svelte | 101 +++++++++++++++++-
11 files changed, 238 insertions(+), 60 deletions(-)
diff --git a/backend/api/v1/common/tenant_crud_routes.py b/backend/api/v1/common/tenant_crud_routes.py
index 51a99305..9a3c65ae 100644
--- a/backend/api/v1/common/tenant_crud_routes.py
+++ b/backend/api/v1/common/tenant_crud_routes.py
@@ -15,7 +15,8 @@ ServiceType = TypeVar("ServiceType")
class TenantCRUDRoutes(
- Generic[CreateSchemaType, UpdateSchemaType, ResponseSchemaType, ServiceType]
+ Generic[CreateSchemaType, UpdateSchemaType,
+ ResponseSchemaType, ServiceType]
):
"""
Generic CRUD routes factory for tenant-scoped resources
@@ -74,7 +75,8 @@ class TenantCRUDRoutes(
prefix: str,
tags: list[str],
resource_name: str = "Resource",
- id_name: Optional[str] = None, # For parent resources (e.g., "pedimento_id")
+ # For parent resources (e.g., "pedimento_id")
+ id_name: Optional[str] = None,
id_type: Type = int, # Type of the ID (int, str, etc.)
parent_id_name: Optional[
str
@@ -128,9 +130,15 @@ class TenantCRUDRoutes(
le=self.max_page_size,
description="Page size",
),
- status: Optional[str] = Query(None, description="Filter by status"),
+ status: Optional[str] = Query(
+ None, description="Filter by status"),
+ operation_type: Optional[str] = Query(
+ None, description="Filter by operation type"),
+ invoice_type: Optional[str] = Query(
+ None, description="Filter by invoice type"),
db: Session = Depends(self.db_dependency),
- current_user: Dict[str, Any] = Depends(self.auth_dependency),
+ current_user: Dict[str, Any] = Depends(
+ self.auth_dependency),
):
tenant_id = validate_access_to_resource(
db, company_id, current_user
@@ -140,6 +148,10 @@ class TenantCRUDRoutes(
filters = {}
if status:
filters["status"] = status
+ if operation_type:
+ filters["operation_type"] = operation_type
+ if invoice_type:
+ filters["invoice_type"] = invoice_type
items, total = self.service.get_all(
db, tenant_id, company_id, skip, page_size, filters
@@ -172,7 +184,8 @@ class TenantCRUDRoutes(
description="Page size",
),
db: Session = Depends(self.db_dependency),
- current_user: Dict[str, Any] = Depends(self.auth_dependency),
+ current_user: Dict[str, Any] = Depends(
+ self.auth_dependency),
):
tenant_id = validate_access_to_resource(
db, company_id, current_user
@@ -211,7 +224,8 @@ class TenantCRUDRoutes(
**path_params,
):
- tenant_id = validate_access_to_resource(db, company_id, current_user)
+ tenant_id = validate_access_to_resource(
+ db, company_id, current_user)
parent_id = path_params.get(self.parent_id_name)
# Try method with 4 params (pedimento_id, tenant_id, company_id)
@@ -225,7 +239,8 @@ class TenantCRUDRoutes(
db, parent_id, tenant_id, company_id
)
else:
- resource = self.service.get(db, parent_id, tenant_id, company_id)
+ resource = self.service.get(
+ db, parent_id, tenant_id, company_id)
if not resource:
raise HTTPException(
@@ -249,7 +264,8 @@ class TenantCRUDRoutes(
db: Session = Depends(self.db_dependency),
current_user: Dict[str, Any] = Depends(self.auth_dependency),
):
- tenant_id = validate_access_to_resource(db, company_id, current_user)
+ tenant_id = validate_access_to_resource(
+ db, company_id, current_user)
resource = self.service.get_by_id(
db, resource_id, tenant_id, company_id
@@ -264,10 +280,10 @@ class TenantCRUDRoutes(
# POST route
if self.parent_id_name:
# Child resource - needs parent_id from path
-
+
# Create a closure to capture the schema type
create_schema = self.create_schema
-
+
@self.router.post(
"/",
response_model=self.response_schema,
@@ -281,17 +297,18 @@ class TenantCRUDRoutes(
db: Session = Depends(self.db_dependency),
current_user: Dict[str, Any] = Depends(self.auth_dependency),
):
- tenant_id = validate_access_to_resource(db, company_id, current_user)
-
+ tenant_id = validate_access_to_resource(
+ db, company_id, current_user)
+
# For child resources, parent_id validation would go here
resource = self.service.create(db, data, tenant_id, company_id)
return resource
else:
# Parent resource - no parent_id needed
-
+
# Create a closure to capture the schema type
create_schema = self.create_schema
-
+
@self.router.post(
"/",
response_model=self.response_schema,
@@ -305,7 +322,8 @@ class TenantCRUDRoutes(
db: Session = Depends(self.db_dependency),
current_user: Dict[str, Any] = Depends(self.auth_dependency),
):
- tenant_id = validate_access_to_resource(db, company_id, current_user)
+ tenant_id = validate_access_to_resource(
+ db, company_id, current_user)
resource = self.service.create(db, data, tenant_id, company_id)
return resource
@@ -314,10 +332,10 @@ class TenantCRUDRoutes(
# For child resources: PUT / (parent_id comes from path)
if self.parent_id_name:
# Child resource
-
+
# Create a closure to capture the schema type
update_schema = self.update_schema
-
+
@self.router.put(
"/",
response_model=self.response_schema,
@@ -331,7 +349,8 @@ class TenantCRUDRoutes(
current_user: Dict[str, Any] = Depends(self.auth_dependency),
**path_params,
):
- tenant_id = validate_access_to_resource(db, company_id, current_user)
+ tenant_id = validate_access_to_resource(
+ db, company_id, current_user)
parent_id = path_params.get(self.parent_id_name)
resource = self.service.update(
@@ -346,10 +365,10 @@ class TenantCRUDRoutes(
else:
# Parent resource
-
+
# Create a closure to capture the schema type
update_schema = self.update_schema
-
+
@self.router.put(
f"/{{{self.id_name}}}",
response_model=self.response_schema,
@@ -366,7 +385,8 @@ class TenantCRUDRoutes(
current_user: Dict[str, Any] = Depends(self.auth_dependency),
):
f"""Update {self.resource_name}"""
- tenant_id = validate_access_to_resource(db, company_id, current_user)
+ tenant_id = validate_access_to_resource(
+ db, company_id, current_user)
resource = self.service.update(
db, resource_id, tenant_id, data, company_id
@@ -395,10 +415,12 @@ class TenantCRUDRoutes(
current_user: Dict[str, Any] = Depends(self.auth_dependency),
**path_params,
):
- tenant_id = validate_access_to_resource(db, company_id, current_user)
+ tenant_id = validate_access_to_resource(
+ db, company_id, current_user)
parent_id = path_params.get(self.parent_id_name)
- success = self.service.delete(db, parent_id, tenant_id, company_id)
+ success = self.service.delete(
+ db, parent_id, tenant_id, company_id)
if not success:
raise HTTPException(
@@ -422,9 +444,11 @@ class TenantCRUDRoutes(
db: Session = Depends(self.db_dependency),
current_user: Dict[str, Any] = Depends(self.auth_dependency),
):
- tenant_id = validate_access_to_resource(db, company_id, current_user)
+ tenant_id = validate_access_to_resource(
+ db, company_id, current_user)
- success = self.service.delete(db, resource_id, tenant_id, company_id)
+ success = self.service.delete(
+ db, resource_id, tenant_id, company_id)
if not success:
raise HTTPException(
diff --git a/backend/api/v1/modules/a76/invoices/services.py b/backend/api/v1/modules/a76/invoices/services.py
index c74fea63..ed8f9c38 100644
--- a/backend/api/v1/modules/a76/invoices/services.py
+++ b/backend/api/v1/modules/a76/invoices/services.py
@@ -44,6 +44,9 @@ class InvoiceService:
if filters.get("operation_type"):
query = query.filter(
models.InvoiceHeader.operation_type == filters["operation_type"])
+ if filters.get("invoice_type"):
+ query = query.filter(
+ models.InvoiceHeader.invoice_type == filters["invoice_type"])
if filters.get("invoice_number"):
query = query.filter(models.InvoiceHeader.invoice_number.ilike(
f"%{filters['invoice_number']}%"))
@@ -53,6 +56,10 @@ class InvoiceService:
f"%{filters['pedimento']}%")
)
+ if not filters.get("invoice_type") and filters.get("operation_type") == "exp":
+ query = query.filter(
+ models.InvoiceHeader.operation_type != "REPAR")
+
total = query.count()
items = query.offset(skip).limit(limit).all()
return items, total
diff --git a/backend/api/v1/modules/public/reference_data/invoice_types/dto.py b/backend/api/v1/modules/public/reference_data/invoice_types/dto.py
index 7e84a068..0488a2a2 100644
--- a/backend/api/v1/modules/public/reference_data/invoice_types/dto.py
+++ b/backend/api/v1/modules/public/reference_data/invoice_types/dto.py
@@ -8,5 +8,6 @@ class InvoiceTypeDTO(BaseModel):
description: str
note: Optional[str] = None
type: Optional[str] = None
+ operation: Optional[str] = None
model_config = ConfigDict(from_attributes=True)
diff --git a/backend/api/v1/modules/public/reference_data/invoice_types/routes.py b/backend/api/v1/modules/public/reference_data/invoice_types/routes.py
index 936d7fb3..e0ce0866 100644
--- a/backend/api/v1/modules/public/reference_data/invoice_types/routes.py
+++ b/backend/api/v1/modules/public/reference_data/invoice_types/routes.py
@@ -1,4 +1,4 @@
-from typing import Any, Dict
+from typing import Any, Dict, Optional
from core.database import get_core_db
from core.security import get_current_user, has_role
@@ -11,17 +11,28 @@ from .models import InvoiceType
router = APIRouter(prefix="/invoice-types")
-@router.get("/", response_model=Dict[str, Any])
+@router.get("/", response_model=dict)
def list_invoice_types(
- page: int = Query(1, ge=1, description="Número de página"),
- page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"),
+ page: int = Query(1, ge=1),
+ page_size: int = Query(50, ge=1, le=100),
+ type: Optional[str] = Query(None, description="Filter by type"),
+ operation: Optional[str] = Query(None, description="Filter by operation type (imp, exp, both)"),
db: Session = Depends(get_core_db),
- current_user: dict = Depends(get_current_user),
):
- skip = (page - 1) * page_size
query = db.query(InvoiceType)
- items = query.offset(skip).limit(page_size).all()
+
+ # Filter by operation if provided
+ if operation:
+ query = query.filter(
+ (InvoiceType.operation == operation) | (
+ InvoiceType.operation == "both")
+ )
+
+ if type == "imp" and operation == "CR":
+ query = query.filter(InvoiceType.operation != "exp")
+
total = query.count()
+ items = query.offset((page - 1) * page_size).limit(page_size).all()
return {
"items": [InvoiceTypeDTO.model_validate(obj) for obj in items],
"total": total,
diff --git a/backend/api/v1/modules/public/reference_data/invoice_types/seed.py b/backend/api/v1/modules/public/reference_data/invoice_types/seed.py
index f011769f..151163dd 100644
--- a/backend/api/v1/modules/public/reference_data/invoice_types/seed.py
+++ b/backend/api/v1/modules/public/reference_data/invoice_types/seed.py
@@ -30,47 +30,47 @@ seed = [
),
# === TIPOS DE EXPORTACION ===
- ("DONAC", "DONACION", "", "both", "both"),
- ("EXDEF", "EXPORTACION DEFINITIVA", "", "material", "both"),
+ ("DONAC", "DONACION", "", "both", "exp"),
+ ("EXDEF", "EXPORTACION DEFINITIVA", "", "material", "exp"),
(
"MATDE",
"MATERIA PRIMA O MATERIAL DEVUELTO",
"ESTE PROCESO CONSISTE EN SOLO DESCARGAR LAS PARTES DADAS DE ALTA EN MATERIALES QUE SON RETORNADAS SIN NINGUNA MODIFICACION (A1)",
"material",
- "both",
+ "exp",
),
(
"NODES",
"NO HACE DESCARGA",
"ESTE PROCESO DE ACTUALIZACION CONSISTE EN EXPORTAR UNA MERCANCIA Y NO DESCARGAR, POR LO TANTO NO EXISTE REPORTE DE DESCARGAS Y NO AFECTA SALDOS.",
"both",
- "both",
+ "exp",
),
(
"PTERM",
"PRODUCTO TERMINADO Y VIRTUALES",
"EL PRODUCTO TERMINADO Y VIRTUALES DESCARGARAN: 1) APARTIR DE LOS COMPONENTES DE CADA PRODUCTO TERMINADO REGISTRADO EN LAS PARTIDAS DE EXPORTACION. 2) POR PARTE, CON LAS OPCIONES DE PODER DESCARGAR POR SUSTITUTO Y POR CLASE EN CASO DE INSUFICIENCIAS DEL COMPONENTE.",
"material",
- "both",
+ "exp",
),
(
"REPAR",
"REPARACION",
"PROCESO QUE CONSISTE EN DOS ETAPAS: 1) DESCARGA EL PRODUCTO DE REPARACION QUE SE IMPORTO PARA REPARA, 2) DESCARGA EL LISTADO DE COMPONENTES QUE SE AGREGO AL PRODUCTO DE REPARACION",
"material",
- "both",
+ "exp",
),
- ("SCRAP", "SCRAP", "", "both", "both"),
+ ("SCRAP", "SCRAP", "", "both", "exp"),
(
"VEMEX",
"VENTAS EN MEXICO",
"ESTE PROCESO CONSISTE EN LA VENTA EN EL MERCADO NACIONAL DE LOS PRODUCTOS.",
"both",
- "both",
+ "exp",
),
- ("VIRTU", "VIRTUALES", "", "material", "both"),
+ ("VIRTU", "VIRTUALES", "", "material", "exp"),
# === ACTIVOS FIJOS (AMBAS OPERACIONES) ===
- ("AFIJO", "ACTIVO FIJO", "", "fixed asset", "both"),
- ("REEXP", "REEXPEDICION", "", "fixed asset", "both"),
+ ("AFIJO", "ACTIVO FIJO", "", "fixed asset", "exp"),
+ ("REEXP", "REEXPEDICION", "", "fixed asset", "exp"),
]
diff --git a/frontend/src/lib/api/dashboard/refrence_data/invoice_types.ts b/frontend/src/lib/api/dashboard/refrence_data/invoice_types.ts
index 8b132732..67bdae54 100644
--- a/frontend/src/lib/api/dashboard/refrence_data/invoice_types.ts
+++ b/frontend/src/lib/api/dashboard/refrence_data/invoice_types.ts
@@ -9,6 +9,7 @@ export interface InvoiceType {
description: string;
note?: string;
type?: string;
+ operation?: string;
}
export interface InvoiceTypeListResponse {
@@ -40,11 +41,20 @@ export const invoiceTypesApi = {
* Lista todos los tipos de factura con paginación
* @param page - Número de página (por defecto 1)
* @param pageSize - Tamaño de página (por defecto 50)
+ * @param operation - Filtrar por tipo de operación (imp, exp)
*/
- list: (page = 1, pageSize = 50) =>
- api.get(
- `/v1/public/refrence_data/invoice-types?page=${page}&page_size=${pageSize}`
- ),
+ list: (page = 1, pageSize = 50, operation?: string) => {
+ const params = new URLSearchParams({
+ page: page.toString(),
+ page_size: pageSize.toString()
+ });
+ if (operation) {
+ params.append('operation', operation);
+ }
+ return api.get(
+ `/v1/public/refrence_data/invoice-types?${params.toString()}`
+ );
+ },
/**
* Obtiene un tipo de factura por key
diff --git a/frontend/src/lib/assets/favicon.svg b/frontend/src/lib/assets/favicon.svg
index cc5dc66a..1e26f0a3 100644
--- a/frontend/src/lib/assets/favicon.svg
+++ b/frontend/src/lib/assets/favicon.svg
@@ -1 +1,13 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/frontend/src/lib/components/dashboard/invoices/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/invoices/create-edit-dialog.svelte
index 6b523b0c..94004867 100644
--- a/frontend/src/lib/components/dashboard/invoices/create-edit-dialog.svelte
+++ b/frontend/src/lib/components/dashboard/invoices/create-edit-dialog.svelte
@@ -12,10 +12,14 @@
let {
open = $bindable(false),
item = $bindable(null),
+ defaultOperationType,
+ defaultInvoiceType,
onSuccess
}: {
open: boolean;
item?: Invoice | null;
+ defaultOperationType?: 'imp' | 'exp';
+ defaultInvoiceType?: string;
onSuccess?: () => void;
} = $props();
@@ -122,8 +126,8 @@
function resetForm() {
formData = {
- operation_type: "imp",
- invoice_type: "",
+ operation_type: defaultOperationType || "imp",
+ invoice_type: defaultInvoiceType || "",
invoice_number: "",
project_number: "",
purchase_order: "",
diff --git a/frontend/src/lib/components/sidebar/modules.ts b/frontend/src/lib/components/sidebar/modules.ts
index 60662981..cbc38c14 100644
--- a/frontend/src/lib/components/sidebar/modules.ts
+++ b/frontend/src/lib/components/sidebar/modules.ts
@@ -323,19 +323,19 @@ export function getSidebarData(): SidebarData {
items: [
{
title: m["sidebar.import_invoices.temporary"](),
- url: "/dashboard/invoices",
+ url: "/dashboard/invoices?operation_type=imp&invoice_type=TEM",
},
{
title: m["sidebar.import_invoices.definitive"](),
- url: "/dashboard/invoices",
+ url: "/dashboard/invoices?operation_type=imp&invoice_type=DEF",
},
{
title: m["sidebar.import_invoices.mexican_purchases"](),
- url: "/dashboard/invoices",
+ url: "/dashboard/invoices?operation_type=imp&invoice_type=MEX",
},
{
title: m["sidebar.import_invoices.regime_change"](),
- url: "/dashboard/invoices",
+ url: "/dashboard/invoices?operation_type=imp&invoice_type=CR",
}
],
},
@@ -346,11 +346,11 @@ export function getSidebarData(): SidebarData {
items: [
{
title: m["sidebar.export_invoices.exportation"](),
- url: "/dashboard/invoices",
+ url: "/dashboard/invoices?operation_type=exp",
},
{
title: m["sidebar.export_invoices.repair"](),
- url: "/dashboard/invoices",
+ url: "/dashboard/invoices?operation_type=exp&invoice_type=REPAR",
},
],
},
diff --git a/frontend/src/routes/dashboard/invoices/+page.server.ts b/frontend/src/routes/dashboard/invoices/+page.server.ts
index 8b9ed8bb..4e3b8795 100644
--- a/frontend/src/routes/dashboard/invoices/+page.server.ts
+++ b/frontend/src/routes/dashboard/invoices/+page.server.ts
@@ -44,6 +44,7 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
// Obtener filtro de tipo de operación
const operationType = url.searchParams.get('operation_type');
+ const invoiceType = url.searchParams.get('invoice_type');
// Construir parámetros de consulta
const params = new URLSearchParams({
@@ -57,6 +58,11 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
params.append('operation_type', operationType);
}
+ // Agregar filtro de invoice_type si existe
+ if (invoiceType) {
+ params.append('invoice_type', invoiceType);
+ }
+
// Usar authenticatedFetch para manejar automáticamente el refresh de tokens
const response = await authenticatedFetch(
`v1/a76/invoices?${params.toString()}`,
@@ -75,7 +81,8 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
error: 'Error al cargar facturas',
companies: parentData.companies || [],
currentCompanyId: companyId,
- operationType: operationType || 'all'
+ operationType: operationType || 'all',
+ invoiceType: invoiceType || null
};
}
@@ -88,7 +95,8 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
page_size: data.page_size || 50,
companies: parentData.companies || [],
currentCompanyId: companyId,
- operationType: operationType || 'all'
+ operationType: operationType || 'all',
+ invoiceType: invoiceType || null
};
} catch (error) {
console.error('Error loading invoices:', error);
@@ -98,7 +106,9 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
page: 1,
page_size: 50,
error: 'Error al cargar facturas',
- companies: parentData.companies || []
+ companies: parentData.companies || [],
+ operationType: 'all',
+ invoiceType: null
};
}
};
diff --git a/frontend/src/routes/dashboard/invoices/+page.svelte b/frontend/src/routes/dashboard/invoices/+page.svelte
index c9fee531..0a837d10 100644
--- a/frontend/src/routes/dashboard/invoices/+page.svelte
+++ b/frontend/src/routes/dashboard/invoices/+page.svelte
@@ -1,6 +1,7 @@
@@ -35,18 +37,21 @@
Acciones
+
Ver Detalles
+
Editar
+
Eliminar
-
+
\ No newline at end of file
diff --git a/frontend/src/lib/components/sidebar/modules.ts b/frontend/src/lib/components/sidebar/modules.ts
index cbc38c14..328f53dc 100644
--- a/frontend/src/lib/components/sidebar/modules.ts
+++ b/frontend/src/lib/components/sidebar/modules.ts
@@ -323,19 +323,23 @@ export function getSidebarData(): SidebarData {
items: [
{
title: m["sidebar.import_invoices.temporary"](),
- url: "/dashboard/invoices?operation_type=imp&invoice_type=TEM",
+ //url: "/dashboard/invoices?operation_type=imp&invoice_type=TEM"
+ url: "/dashboard/invoices/importacion/temporal",
},
{
title: m["sidebar.import_invoices.definitive"](),
- url: "/dashboard/invoices?operation_type=imp&invoice_type=DEF",
+ //url: "/dashboard/invoices?operation_type=imp&invoice_type=DEF",
+ url: "/dashboard/invoices/importacion/definitiva",
},
{
title: m["sidebar.import_invoices.mexican_purchases"](),
- url: "/dashboard/invoices?operation_type=imp&invoice_type=MEX",
+ //url: "/dashboard/invoices?operation_type=imp&invoice_type=MEX",
+ url: "/dashboard/invoices/importacion/compras_mexicanas",
},
{
title: m["sidebar.import_invoices.regime_change"](),
- url: "/dashboard/invoices?operation_type=imp&invoice_type=CR",
+ //url: "/dashboard/invoices?operation_type=imp&invoice_type=CR",
+ url: "/dashboard/invoices/importacion/cambio_regimen",
}
],
},
@@ -346,11 +350,13 @@ export function getSidebarData(): SidebarData {
items: [
{
title: m["sidebar.export_invoices.exportation"](),
- url: "/dashboard/invoices?operation_type=exp",
+ //url: "/dashboard/invoices?operation_type=exp",
+ url: "/dashboard/invoices/exportacion/exportacion",
},
{
title: m["sidebar.export_invoices.repair"](),
- url: "/dashboard/invoices?operation_type=exp&invoice_type=REPAR",
+ //url: "/dashboard/invoices?operation_type=exp&invoice_type=REPAR",
+ url: "/dashboard/invoices/exportacion/reparacion",
},
],
},
diff --git a/frontend/src/routes/dashboard/invoices/+page.server.ts b/frontend/src/routes/dashboard/invoices/exportacion/exportacion/+page.server.ts
similarity index 94%
rename from frontend/src/routes/dashboard/invoices/+page.server.ts
rename to frontend/src/routes/dashboard/invoices/exportacion/exportacion/+page.server.ts
index 4e3b8795..54f3eb0b 100644
--- a/frontend/src/routes/dashboard/invoices/+page.server.ts
+++ b/frontend/src/routes/dashboard/invoices/exportacion/exportacion/+page.server.ts
@@ -1,4 +1,4 @@
-import type { PageServerLoad } from './$types';
+import type { PageServerLoad } from '../$types';
import { redirect } from '@sveltejs/kit';
import {
getAuthTokens,
@@ -43,8 +43,8 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
}
// Obtener filtro de tipo de operación
- const operationType = url.searchParams.get('operation_type');
- const invoiceType = url.searchParams.get('invoice_type');
+ const operationType = 'exp'
+ const invoiceType = 'exp'
// Construir parámetros de consulta
const params = new URLSearchParams({
diff --git a/frontend/src/routes/dashboard/invoices/+page.svelte b/frontend/src/routes/dashboard/invoices/exportacion/exportacion/+page.svelte
similarity index 100%
rename from frontend/src/routes/dashboard/invoices/+page.svelte
rename to frontend/src/routes/dashboard/invoices/exportacion/exportacion/+page.svelte
diff --git a/frontend/src/routes/dashboard/invoices/exportacion/reparacion/+page.server.ts b/frontend/src/routes/dashboard/invoices/exportacion/reparacion/+page.server.ts
new file mode 100644
index 00000000..4feb09c2
--- /dev/null
+++ b/frontend/src/routes/dashboard/invoices/exportacion/reparacion/+page.server.ts
@@ -0,0 +1,114 @@
+import type { PageServerLoad } from '../$types';
+import { redirect } from '@sveltejs/kit';
+import {
+ getAuthTokens,
+ authenticatedFetch
+} from '$lib/server/api';
+
+export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
+ // Esperar a que el layout padre valide/refresque el token
+ const parentData = await parent();
+
+ // Verificar autenticación
+ const { accessToken } = getAuthTokens(cookies);
+
+ if (!accessToken) {
+ throw redirect(302, '/login');
+ }
+
+ try {
+ // Obtener company_id de múltiples fuentes (en orden de prioridad):
+ // 1. URL query param (permite cambiar vía navegación)
+ // 2. Cookie active_company_id (setted por el team-switcher)
+ // 3. Primera compañía del usuario (fallback)
+ const companyIdParam = url.searchParams.get('company_id');
+ const cookieCompanyId = cookies.get('active_company_id');
+
+ const companyId = companyIdParam
+ ? parseInt(companyIdParam)
+ : cookieCompanyId
+ ? parseInt(cookieCompanyId)
+ : parentData.companies?.[0]?.id;
+
+ // Si aún no hay companyId, mostrar error
+ if (!companyId) {
+ return {
+ items: [],
+ total: 0,
+ page: 1,
+ page_size: 50,
+ error: 'No se encontró una compañía seleccionada',
+ companies: parentData.companies || []
+ };
+ }
+
+ // Obtener filtro de tipo de operación
+ const operationType = 'exp'
+ const invoiceType = 'REPAR'
+
+ // Construir parámetros de consulta
+ const params = new URLSearchParams({
+ company_id: companyId.toString(),
+ page: '1',
+ page_size: '50'
+ });
+
+ // Agregar filtro de tipo si existe y no es 'all'
+ if (operationType && operationType !== 'all') {
+ params.append('operation_type', operationType);
+ }
+
+ // Agregar filtro de invoice_type si existe
+ if (invoiceType) {
+ params.append('invoice_type', invoiceType);
+ }
+
+ // Usar authenticatedFetch para manejar automáticamente el refresh de tokens
+ const response = await authenticatedFetch(
+ `v1/a76/invoices?${params.toString()}`,
+ {},
+ cookies,
+ fetch,
+ '/login'
+ );
+
+ if (!response.ok) {
+ return {
+ items: [],
+ total: 0,
+ page: 1,
+ page_size: 50,
+ error: 'Error al cargar facturas',
+ companies: parentData.companies || [],
+ currentCompanyId: companyId,
+ operationType: operationType || 'all',
+ invoiceType: invoiceType || null
+ };
+ }
+
+ const data = await response.json();
+
+ return {
+ items: data.items || [],
+ total: data.total || 0,
+ page: data.page || 1,
+ page_size: data.page_size || 50,
+ companies: parentData.companies || [],
+ currentCompanyId: companyId,
+ operationType: operationType || 'all',
+ invoiceType: invoiceType || null
+ };
+ } catch (error) {
+ console.error('Error loading invoices:', error);
+ return {
+ items: [],
+ total: 0,
+ page: 1,
+ page_size: 50,
+ error: 'Error al cargar facturas',
+ companies: parentData.companies || [],
+ operationType: 'all',
+ invoiceType: null
+ };
+ }
+};
diff --git a/frontend/src/routes/dashboard/invoices/exportacion/reparacion/+page.svelte b/frontend/src/routes/dashboard/invoices/exportacion/reparacion/+page.svelte
new file mode 100644
index 00000000..0a837d10
--- /dev/null
+++ b/frontend/src/routes/dashboard/invoices/exportacion/reparacion/+page.svelte
@@ -0,0 +1,382 @@
+
+
+
+
+
+
+
Facturas
+
+ Gestiona las facturas de importación y exportación
+
+
+
+
+
+
+ {#if error}
+
+
+ Error
+ {error}
+
+
+ {/if}
+
+
+
+
+
+
+ Listado de Facturas
+
+ Mostrando {allItems.length} de {totalItems} registros
+ {#if companyStore.activeCompany}
+ - Compañía: {companyStore.activeCompany.name}
+ {/if}
+
+
+
+
+
+ {selectedType === 'all' ? 'Todas' : selectedType === 'imp' ? 'Importación' : 'Exportación'}
+
+
+ Todas
+ Importación
+ Exportación
+
+
+
+ {#if selectedType !== 'all' && availableInvoiceTypes.length > 0}
+
+
+
+ {#if loadingInvoiceTypes}
+ Cargando...
+ {:else if selectedInvoiceType}
+ {(() => {
+ const found = availableInvoiceTypes.find(t => t.key === selectedInvoiceType);
+ return found ? `${found.key} - ${found.description}` : selectedInvoiceType;
+ })()}
+ {:else}
+ Todos los tipos
+ {/if}
+
+
+
+ Todos los tipos
+ {#each availableInvoiceTypes as invType}
+
+
+ {invType.key} - {invType.description}
+
+
+ {/each}
+
+
+ {/if}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/routes/dashboard/invoices/importacion/cambio_regimen/+page.server.ts b/frontend/src/routes/dashboard/invoices/importacion/cambio_regimen/+page.server.ts
new file mode 100644
index 00000000..fb3159d9
--- /dev/null
+++ b/frontend/src/routes/dashboard/invoices/importacion/cambio_regimen/+page.server.ts
@@ -0,0 +1,114 @@
+import type { PageServerLoad } from '../../$types';
+import { redirect } from '@sveltejs/kit';
+import {
+ getAuthTokens,
+ authenticatedFetch
+} from '$lib/server/api';
+
+export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
+ // Esperar a que el layout padre valide/refresque el token
+ const parentData = await parent();
+
+ // Verificar autenticación
+ const { accessToken } = getAuthTokens(cookies);
+
+ if (!accessToken) {
+ throw redirect(302, '/login');
+ }
+
+ try {
+ // Obtener company_id de múltiples fuentes (en orden de prioridad):
+ // 1. URL query param (permite cambiar vía navegación)
+ // 2. Cookie active_company_id (setted por el team-switcher)
+ // 3. Primera compañía del usuario (fallback)
+ const companyIdParam = url.searchParams.get('company_id');
+ const cookieCompanyId = cookies.get('active_company_id');
+
+ const companyId = companyIdParam
+ ? parseInt(companyIdParam)
+ : cookieCompanyId
+ ? parseInt(cookieCompanyId)
+ : parentData.companies?.[0]?.id;
+
+ // Si aún no hay companyId, mostrar error
+ if (!companyId) {
+ return {
+ items: [],
+ total: 0,
+ page: 1,
+ page_size: 50,
+ error: 'No se encontró una compañía seleccionada',
+ companies: parentData.companies || []
+ };
+ }
+
+ // Obtener filtro de tipo de operación
+ const operationType = 'imp'
+ const invoiceType = 'CR'
+
+ // Construir parámetros de consulta
+ const params = new URLSearchParams({
+ company_id: companyId.toString(),
+ page: '1',
+ page_size: '50'
+ });
+
+ // Agregar filtro de tipo si existe y no es 'all'
+ if (operationType && operationType !== 'all') {
+ params.append('operation_type', operationType);
+ }
+
+ // Agregar filtro de invoice_type si existe
+ if (invoiceType) {
+ params.append('invoice_type', invoiceType);
+ }
+
+ // Usar authenticatedFetch para manejar automáticamente el refresh de tokens
+ const response = await authenticatedFetch(
+ `v1/a76/invoices?${params.toString()}`,
+ {},
+ cookies,
+ fetch,
+ '/login'
+ );
+
+ if (!response.ok) {
+ return {
+ items: [],
+ total: 0,
+ page: 1,
+ page_size: 50,
+ error: 'Error al cargar facturas',
+ companies: parentData.companies || [],
+ currentCompanyId: companyId,
+ operationType: operationType || 'all',
+ invoiceType: invoiceType || null
+ };
+ }
+
+ const data = await response.json();
+
+ return {
+ items: data.items || [],
+ total: data.total || 0,
+ page: data.page || 1,
+ page_size: data.page_size || 50,
+ companies: parentData.companies || [],
+ currentCompanyId: companyId,
+ operationType: operationType || 'all',
+ invoiceType: invoiceType || null
+ };
+ } catch (error) {
+ console.error('Error loading invoices:', error);
+ return {
+ items: [],
+ total: 0,
+ page: 1,
+ page_size: 50,
+ error: 'Error al cargar facturas',
+ companies: parentData.companies || [],
+ operationType: 'all',
+ invoiceType: null
+ };
+ }
+};
diff --git a/frontend/src/routes/dashboard/invoices/importacion/cambio_regimen/+page.svelte b/frontend/src/routes/dashboard/invoices/importacion/cambio_regimen/+page.svelte
new file mode 100644
index 00000000..c0d0b0cf
--- /dev/null
+++ b/frontend/src/routes/dashboard/invoices/importacion/cambio_regimen/+page.svelte
@@ -0,0 +1,382 @@
+
+
+
+
+
+
+
Facturas
+
+ Gestiona las facturas de importación y exportación
+
+
+
+
+
+
+ {#if error}
+
+
+ Error
+ {error}
+
+
+ {/if}
+
+
+
+
+
+
+ Listado de Facturas
+
+ Mostrando {allItems.length} de {totalItems} registros
+ {#if companyStore.activeCompany}
+ - Compañía: {companyStore.activeCompany.name}
+ {/if}
+
+
+
+
+
+ {selectedType === 'all' ? 'Todas' : selectedType === 'imp' ? 'Importación' : 'Exportación'}
+
+
+ Todas
+ Importación
+ Exportación
+
+
+
+ {#if selectedType !== 'all' && availableInvoiceTypes.length > 0}
+
+
+
+ {#if loadingInvoiceTypes}
+ Cargando...
+ {:else if selectedInvoiceType}
+ {(() => {
+ const found = availableInvoiceTypes.find(t => t.key === selectedInvoiceType);
+ return found ? `${found.key} - ${found.description}` : selectedInvoiceType;
+ })()}
+ {:else}
+ Todos los tipos
+ {/if}
+
+
+
+ Todos los tipos
+ {#each availableInvoiceTypes as invType}
+
+
+ {invType.key} - {invType.description}
+
+
+ {/each}
+
+
+ {/if}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/routes/dashboard/invoices/importacion/cambio_regimen/new/+page.svelte b/frontend/src/routes/dashboard/invoices/importacion/cambio_regimen/new/+page.svelte
new file mode 100644
index 00000000..76f88411
--- /dev/null
+++ b/frontend/src/routes/dashboard/invoices/importacion/cambio_regimen/new/+page.svelte
@@ -0,0 +1,320 @@
+
+
+
+
+
+
+
+
Nueva Factura Cambio Regimen
+
Ingresa los datos para registrar la importación.
+
+
+
+ {#if error}
+
+ ⚠️ {error}
+
+ {/if}
+
+
+
\ No newline at end of file
diff --git a/frontend/src/routes/dashboard/invoices/importacion/compras_mexicanas/+page.server.ts b/frontend/src/routes/dashboard/invoices/importacion/compras_mexicanas/+page.server.ts
new file mode 100644
index 00000000..a8794bcf
--- /dev/null
+++ b/frontend/src/routes/dashboard/invoices/importacion/compras_mexicanas/+page.server.ts
@@ -0,0 +1,114 @@
+import type { PageServerLoad } from '../../$types';
+import { redirect } from '@sveltejs/kit';
+import {
+ getAuthTokens,
+ authenticatedFetch
+} from '$lib/server/api';
+
+export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
+ // Esperar a que el layout padre valide/refresque el token
+ const parentData = await parent();
+
+ // Verificar autenticación
+ const { accessToken } = getAuthTokens(cookies);
+
+ if (!accessToken) {
+ throw redirect(302, '/login');
+ }
+
+ try {
+ // Obtener company_id de múltiples fuentes (en orden de prioridad):
+ // 1. URL query param (permite cambiar vía navegación)
+ // 2. Cookie active_company_id (setted por el team-switcher)
+ // 3. Primera compañía del usuario (fallback)
+ const companyIdParam = url.searchParams.get('company_id');
+ const cookieCompanyId = cookies.get('active_company_id');
+
+ const companyId = companyIdParam
+ ? parseInt(companyIdParam)
+ : cookieCompanyId
+ ? parseInt(cookieCompanyId)
+ : parentData.companies?.[0]?.id;
+
+ // Si aún no hay companyId, mostrar error
+ if (!companyId) {
+ return {
+ items: [],
+ total: 0,
+ page: 1,
+ page_size: 50,
+ error: 'No se encontró una compañía seleccionada',
+ companies: parentData.companies || []
+ };
+ }
+
+ // Obtener filtro de tipo de operación
+ const operationType = 'imp'
+ const invoiceType = 'MEX'
+
+ // Construir parámetros de consulta
+ const params = new URLSearchParams({
+ company_id: companyId.toString(),
+ page: '1',
+ page_size: '50'
+ });
+
+ // Agregar filtro de tipo si existe y no es 'all'
+ if (operationType && operationType !== 'all') {
+ params.append('operation_type', operationType);
+ }
+
+ // Agregar filtro de invoice_type si existe
+ if (invoiceType) {
+ params.append('invoice_type', invoiceType);
+ }
+
+ // Usar authenticatedFetch para manejar automáticamente el refresh de tokens
+ const response = await authenticatedFetch(
+ `v1/a76/invoices?${params.toString()}`,
+ {},
+ cookies,
+ fetch,
+ '/login'
+ );
+
+ if (!response.ok) {
+ return {
+ items: [],
+ total: 0,
+ page: 1,
+ page_size: 50,
+ error: 'Error al cargar facturas',
+ companies: parentData.companies || [],
+ currentCompanyId: companyId,
+ operationType: operationType || 'all',
+ invoiceType: invoiceType || null
+ };
+ }
+
+ const data = await response.json();
+
+ return {
+ items: data.items || [],
+ total: data.total || 0,
+ page: data.page || 1,
+ page_size: data.page_size || 50,
+ companies: parentData.companies || [],
+ currentCompanyId: companyId,
+ operationType: operationType || 'all',
+ invoiceType: invoiceType || null
+ };
+ } catch (error) {
+ console.error('Error loading invoices:', error);
+ return {
+ items: [],
+ total: 0,
+ page: 1,
+ page_size: 50,
+ error: 'Error al cargar facturas',
+ companies: parentData.companies || [],
+ operationType: 'all',
+ invoiceType: null
+ };
+ }
+};
diff --git a/frontend/src/routes/dashboard/invoices/importacion/compras_mexicanas/+page.svelte b/frontend/src/routes/dashboard/invoices/importacion/compras_mexicanas/+page.svelte
new file mode 100644
index 00000000..87cdb207
--- /dev/null
+++ b/frontend/src/routes/dashboard/invoices/importacion/compras_mexicanas/+page.svelte
@@ -0,0 +1,382 @@
+
+
+
+
+
+
+
Facturas
+
+ Gestiona las facturas de importación y exportación
+
+
+
+
+
+
+ {#if error}
+
+
+ Error
+ {error}
+
+
+ {/if}
+
+
+
+
+
+
+ Listado de Facturas
+
+ Mostrando {allItems.length} de {totalItems} registros
+ {#if companyStore.activeCompany}
+ - Compañía: {companyStore.activeCompany.name}
+ {/if}
+
+
+
+
+
+ {selectedType === 'all' ? 'Todas' : selectedType === 'imp' ? 'Importación' : 'Exportación'}
+
+
+ Todas
+ Importación
+ Exportación
+
+
+
+ {#if selectedType !== 'all' && availableInvoiceTypes.length > 0}
+
+
+
+ {#if loadingInvoiceTypes}
+ Cargando...
+ {:else if selectedInvoiceType}
+ {(() => {
+ const found = availableInvoiceTypes.find(t => t.key === selectedInvoiceType);
+ return found ? `${found.key} - ${found.description}` : selectedInvoiceType;
+ })()}
+ {:else}
+ Todos los tipos
+ {/if}
+
+
+
+ Todos los tipos
+ {#each availableInvoiceTypes as invType}
+
+
+ {invType.key} - {invType.description}
+
+
+ {/each}
+
+
+ {/if}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/routes/dashboard/invoices/importacion/compras_mexicanas/new/+page.svelte b/frontend/src/routes/dashboard/invoices/importacion/compras_mexicanas/new/+page.svelte
new file mode 100644
index 00000000..488312d0
--- /dev/null
+++ b/frontend/src/routes/dashboard/invoices/importacion/compras_mexicanas/new/+page.svelte
@@ -0,0 +1,320 @@
+
+
+
+
+
+
+
+
Nueva Factura
+
Ingresa los datos para registrar la importación.
+
+
+
+ {#if error}
+
+ ⚠️ {error}
+
+ {/if}
+
+
+
\ No newline at end of file
diff --git a/frontend/src/routes/dashboard/invoices/importacion/definitiva/+page.server.ts b/frontend/src/routes/dashboard/invoices/importacion/definitiva/+page.server.ts
new file mode 100644
index 00000000..d00d94fe
--- /dev/null
+++ b/frontend/src/routes/dashboard/invoices/importacion/definitiva/+page.server.ts
@@ -0,0 +1,114 @@
+import type { PageServerLoad } from '../../$types';
+import { redirect } from '@sveltejs/kit';
+import {
+ getAuthTokens,
+ authenticatedFetch
+} from '$lib/server/api';
+
+export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
+ // Esperar a que el layout padre valide/refresque el token
+ const parentData = await parent();
+
+ // Verificar autenticación
+ const { accessToken } = getAuthTokens(cookies);
+
+ if (!accessToken) {
+ throw redirect(302, '/login');
+ }
+
+ try {
+ // Obtener company_id de múltiples fuentes (en orden de prioridad):
+ // 1. URL query param (permite cambiar vía navegación)
+ // 2. Cookie active_company_id (setted por el team-switcher)
+ // 3. Primera compañía del usuario (fallback)
+ const companyIdParam = url.searchParams.get('company_id');
+ const cookieCompanyId = cookies.get('active_company_id');
+
+ const companyId = companyIdParam
+ ? parseInt(companyIdParam)
+ : cookieCompanyId
+ ? parseInt(cookieCompanyId)
+ : parentData.companies?.[0]?.id;
+
+ // Si aún no hay companyId, mostrar error
+ if (!companyId) {
+ return {
+ items: [],
+ total: 0,
+ page: 1,
+ page_size: 50,
+ error: 'No se encontró una compañía seleccionada',
+ companies: parentData.companies || []
+ };
+ }
+
+ // Obtener filtro de tipo de operación
+ const operationType = 'imp'
+ const invoiceType = 'DEF'
+
+ // Construir parámetros de consulta
+ const params = new URLSearchParams({
+ company_id: companyId.toString(),
+ page: '1',
+ page_size: '50'
+ });
+
+ // Agregar filtro de tipo si existe y no es 'all'
+ if (operationType && operationType !== 'all') {
+ params.append('operation_type', operationType);
+ }
+
+ // Agregar filtro de invoice_type si existe
+ if (invoiceType) {
+ params.append('invoice_type', invoiceType);
+ }
+
+ // Usar authenticatedFetch para manejar automáticamente el refresh de tokens
+ const response = await authenticatedFetch(
+ `v1/a76/invoices?${params.toString()}`,
+ {},
+ cookies,
+ fetch,
+ '/login'
+ );
+
+ if (!response.ok) {
+ return {
+ items: [],
+ total: 0,
+ page: 1,
+ page_size: 50,
+ error: 'Error al cargar facturas',
+ companies: parentData.companies || [],
+ currentCompanyId: companyId,
+ operationType: operationType || 'all',
+ invoiceType: invoiceType || null
+ };
+ }
+
+ const data = await response.json();
+
+ return {
+ items: data.items || [],
+ total: data.total || 0,
+ page: data.page || 1,
+ page_size: data.page_size || 50,
+ companies: parentData.companies || [],
+ currentCompanyId: companyId,
+ operationType: operationType || 'all',
+ invoiceType: invoiceType || null
+ };
+ } catch (error) {
+ console.error('Error loading invoices:', error);
+ return {
+ items: [],
+ total: 0,
+ page: 1,
+ page_size: 50,
+ error: 'Error al cargar facturas',
+ companies: parentData.companies || [],
+ operationType: 'all',
+ invoiceType: null
+ };
+ }
+};
diff --git a/frontend/src/routes/dashboard/invoices/importacion/definitiva/+page.svelte b/frontend/src/routes/dashboard/invoices/importacion/definitiva/+page.svelte
new file mode 100644
index 00000000..17f52848
--- /dev/null
+++ b/frontend/src/routes/dashboard/invoices/importacion/definitiva/+page.svelte
@@ -0,0 +1,390 @@
+
+
+
+
+
+
+
Facturas
+
+ Gestiona las facturas de importación y exportación
+
+
+
+
+
+
+ {#if error}
+
+
+ Error
+ {error}
+
+
+ {/if}
+
+
+
+
+
+
+ Listado de Facturas
+
+ Mostrando {allItems.length} de {totalItems} registros
+ {#if companyStore.activeCompany}
+ - Compañía: {companyStore.activeCompany.name}
+ {/if}
+
+
+
+
+
+ {selectedType === 'all' ? 'Todas' : selectedType === 'imp' ? 'Importación' : 'Exportación'}
+
+
+ Todas
+ Importación
+ Exportación
+
+
+
+ {#if selectedType !== 'all' && availableInvoiceTypes.length > 0}
+
+
+
+ {#if loadingInvoiceTypes}
+ Cargando...
+ {:else if selectedInvoiceType}
+ {(() => {
+ const found = availableInvoiceTypes.find(t => t.key === selectedInvoiceType);
+ return found ? `${found.key} - ${found.description}` : selectedInvoiceType;
+ })()}
+ {:else}
+ Todos los tipos
+ {/if}
+
+
+
+ Todos los tipos
+ {#each availableInvoiceTypes as invType}
+
+
+ {invType.key} - {invType.description}
+
+
+ {/each}
+
+
+ {/if}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/routes/dashboard/invoices/importacion/definitiva/new/+page.svelte b/frontend/src/routes/dashboard/invoices/importacion/definitiva/new/+page.svelte
new file mode 100644
index 00000000..57e4fc64
--- /dev/null
+++ b/frontend/src/routes/dashboard/invoices/importacion/definitiva/new/+page.svelte
@@ -0,0 +1,320 @@
+
+
+
+
+
+
+
+
Nueva Factura Definitiva
+
Ingresa los datos para registrar la importación.
+
+
+
+ {#if error}
+
+ ⚠️ {error}
+
+ {/if}
+
+
+
\ No newline at end of file
diff --git a/frontend/src/routes/dashboard/invoices/importacion/temporal/+page.server.ts b/frontend/src/routes/dashboard/invoices/importacion/temporal/+page.server.ts
new file mode 100644
index 00000000..83708beb
--- /dev/null
+++ b/frontend/src/routes/dashboard/invoices/importacion/temporal/+page.server.ts
@@ -0,0 +1,114 @@
+import type { PageServerLoad } from '../$types';
+import { redirect } from '@sveltejs/kit';
+import {
+ getAuthTokens,
+ authenticatedFetch
+} from '$lib/server/api';
+
+export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
+ // Esperar a que el layout padre valide/refresque el token
+ const parentData = await parent();
+
+ // Verificar autenticación
+ const { accessToken } = getAuthTokens(cookies);
+
+ if (!accessToken) {
+ throw redirect(302, '/login');
+ }
+
+ try {
+ // Obtener company_id de múltiples fuentes (en orden de prioridad):
+ // 1. URL query param (permite cambiar vía navegación)
+ // 2. Cookie active_company_id (setted por el team-switcher)
+ // 3. Primera compañía del usuario (fallback)
+ const companyIdParam = url.searchParams.get('company_id');
+ const cookieCompanyId = cookies.get('active_company_id');
+
+ const companyId = companyIdParam
+ ? parseInt(companyIdParam)
+ : cookieCompanyId
+ ? parseInt(cookieCompanyId)
+ : parentData.companies?.[0]?.id;
+
+ // Si aún no hay companyId, mostrar error
+ if (!companyId) {
+ return {
+ items: [],
+ total: 0,
+ page: 1,
+ page_size: 50,
+ error: 'No se encontró una compañía seleccionada',
+ companies: parentData.companies || []
+ };
+ }
+
+ // Obtener filtro de tipo de operación
+ const operationType = 'imp'
+ const invoiceType = 'TEM'
+
+ // Construir parámetros de consulta
+ const params = new URLSearchParams({
+ company_id: companyId.toString(),
+ page: '1',
+ page_size: '50'
+ });
+
+ // Agregar filtro de tipo si existe y no es 'all'
+ if (operationType && operationType !== 'all') {
+ params.append('operation_type', operationType);
+ }
+
+ // Agregar filtro de invoice_type si existe
+ if (invoiceType) {
+ params.append('invoice_type', invoiceType);
+ }
+
+ // Usar authenticatedFetch para manejar automáticamente el refresh de tokens
+ const response = await authenticatedFetch(
+ `v1/a76/invoices?${params.toString()}`,
+ {},
+ cookies,
+ fetch,
+ '/login'
+ );
+
+ if (!response.ok) {
+ return {
+ items: [],
+ total: 0,
+ page: 1,
+ page_size: 50,
+ error: 'Error al cargar facturas',
+ companies: parentData.companies || [],
+ currentCompanyId: companyId,
+ operationType: operationType || 'all',
+ invoiceType: invoiceType || null
+ };
+ }
+
+ const data = await response.json();
+
+ return {
+ items: data.items || [],
+ total: data.total || 0,
+ page: data.page || 1,
+ page_size: data.page_size || 50,
+ companies: parentData.companies || [],
+ currentCompanyId: companyId,
+ operationType: operationType || 'all',
+ invoiceType: invoiceType || null
+ };
+ } catch (error) {
+ console.error('Error loading invoices:', error);
+ return {
+ items: [],
+ total: 0,
+ page: 1,
+ page_size: 50,
+ error: 'Error al cargar facturas',
+ companies: parentData.companies || [],
+ operationType: 'all',
+ invoiceType: null
+ };
+ }
+};
diff --git a/frontend/src/routes/dashboard/invoices/importacion/temporal/+page.svelte b/frontend/src/routes/dashboard/invoices/importacion/temporal/+page.svelte
new file mode 100644
index 00000000..d9a1ca87
--- /dev/null
+++ b/frontend/src/routes/dashboard/invoices/importacion/temporal/+page.svelte
@@ -0,0 +1,382 @@
+
+
+
+
+
+
+
Facturas
+
+ Gestiona las facturas de importación y exportación
+
+
+
+
+
+
+ {#if error}
+
+
+ Error
+ {error}
+
+
+ {/if}
+
+
+
+
+
+
+ Listado de Facturas
+
+ Mostrando {allItems.length} de {totalItems} registros
+ {#if companyStore.activeCompany}
+ - Compañía: {companyStore.activeCompany.name}
+ {/if}
+
+
+
+
+
+ {selectedType === 'all' ? 'Todas' : selectedType === 'imp' ? 'Importación' : 'Exportación'}
+
+
+ Todas
+ Importación
+ Exportación
+
+
+
+ {#if selectedType !== 'all' && availableInvoiceTypes.length > 0}
+
+
+
+ {#if loadingInvoiceTypes}
+ Cargando...
+ {:else if selectedInvoiceType}
+ {(() => {
+ const found = availableInvoiceTypes.find(t => t.key === selectedInvoiceType);
+ return found ? `${found.key} - ${found.description}` : selectedInvoiceType;
+ })()}
+ {:else}
+ Todos los tipos
+ {/if}
+
+
+
+ Todos los tipos
+ {#each availableInvoiceTypes as invType}
+
+
+ {invType.key} - {invType.description}
+
+
+ {/each}
+
+
+ {/if}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/routes/dashboard/invoices/importacion/temporal/new/+page.svelte b/frontend/src/routes/dashboard/invoices/importacion/temporal/new/+page.svelte
new file mode 100644
index 00000000..fae289e8
--- /dev/null
+++ b/frontend/src/routes/dashboard/invoices/importacion/temporal/new/+page.svelte
@@ -0,0 +1,320 @@
+
+
+
+
+
+
+
+
Nueva Factura Temporal
+
Ingresa los datos para registrar la importación.
+
+
+
+ {#if error}
+
+ ⚠️ {error}
+
+ {/if}
+
+
+
\ No newline at end of file
From d1caf0b9964857786fb0bf15186cbcf1b581b083 Mon Sep 17 00:00:00 2001
From: "Kevin A. Rosales Marquez"
Date: Tue, 16 Dec 2025 08:29:39 -0600
Subject: [PATCH 08/15] Creacion de rutas para nuevas facturas, asi como nuevas
rutinas para el filtro de datos en facturas de exportacion
---
.../api/v1/modules/a76/invoices/services.py | 348 ++---
estructura.txt | 1190 +++++++++++++++++
.../invoices/create-edit-dialog.svelte | 2 +-
.../exportacion/exportacion/+page.server.ts | 21 +-
.../exportacion/exportacion/+page.svelte | 2 +-
.../exportacion/exportacion/new/+page.svelte | 401 ++++++
.../exportacion/reparacion/+page.svelte | 5 +-
.../exportacion/reparacion/new/+page.svelte | 325 +++++
.../cambio_regimen/new/+page.svelte | 39 +-
.../compras_mexicanas/new/+page.svelte | 47 +-
.../importacion/definitiva/new/+page.svelte | 41 +-
.../importacion/temporal/new/+page.svelte | 43 +-
12 files changed, 2139 insertions(+), 325 deletions(-)
create mode 100644 estructura.txt
create mode 100644 frontend/src/routes/dashboard/invoices/exportacion/exportacion/new/+page.svelte
create mode 100644 frontend/src/routes/dashboard/invoices/exportacion/reparacion/new/+page.svelte
diff --git a/backend/api/v1/modules/a76/invoices/services.py b/backend/api/v1/modules/a76/invoices/services.py
index ed8f9c38..478bdae8 100644
--- a/backend/api/v1/modules/a76/invoices/services.py
+++ b/backend/api/v1/modules/a76/invoices/services.py
@@ -1,10 +1,10 @@
+import traceback
from typing import Optional, List, Tuple
from sqlalchemy.orm import Session
from sqlalchemy import and_
from . import models, schemas
-
class InvoiceService:
"""Service for Invoice Header operations"""
@@ -55,7 +55,6 @@ class InvoiceService:
models.InvoiceComplianceMx.pedimento.ilike(
f"%{filters['pedimento']}%")
)
-
if not filters.get("invoice_type") and filters.get("operation_type") == "exp":
query = query.filter(
models.InvoiceHeader.operation_type != "REPAR")
@@ -72,73 +71,118 @@ class InvoiceService:
company_id: int
) -> models.InvoiceHeader:
"""Create a new invoice with all related data"""
- # Extract nested data
- compliance_data = invoice_data.compliance_mx
- financials_data = invoice_data.financials
- logistics_data = invoice_data.logistics or []
- details_data = invoice_data.details or []
- collections_data = invoice_data.collections or []
+
+
+ def clean_dict(data_dict: dict) -> dict:
+ cleaned = {}
+ for key, value in data_dict.items():
+
+ if key == 'customs_agent':
+ key = 'customs_broker_id'
+ elif key == 'provider':
+ key = 'provider_id'
+
+
+ if isinstance(value, str) and not value.strip():
+ cleaned[key] = None
+
+ elif value == 0 and (key.endswith('_id') or key == 'remesa'):
+ cleaned[key] = None
+ else:
+ cleaned[key] = value
+ return cleaned
+
- # Create main invoice header
- invoice_dict = invoice_data.model_dump(
- exclude={"compliance_mx", "financials",
- "logistics", "details", "collections"}
- )
- invoice_dict["tenant_id"] = tenant_id
- invoice_dict["company_id"] = company_id
+ try:
+ # Extract nested data
+ compliance_data = invoice_data.compliance_mx
+ financials_data = invoice_data.financials
+ logistics_data = invoice_data.logistics or []
+ details_data = invoice_data.details or []
+ collections_data = invoice_data.collections or []
- new_invoice = models.InvoiceHeader(**invoice_dict)
- db.add(new_invoice)
- db.flush() # Flush to get the invoice ID
+ # Create main invoice header
+ raw_invoice_dict = invoice_data.model_dump(
+ exclude={"compliance_mx", "financials",
+ "logistics", "details", "collections"}
+ )
+ invoice_dict = clean_dict(raw_invoice_dict)
+ invoice_dict["tenant_id"] = tenant_id
+ invoice_dict["company_id"] = company_id
- # Create compliance_mx if provided
- if compliance_data:
- compliance_dict = compliance_data.model_dump()
- compliance_dict["invoice_id"] = new_invoice.id
- compliance_dict["tenant_id"] = tenant_id
- compliance_dict["company_id"] = company_id
- new_compliance = models.InvoiceComplianceMx(**compliance_dict)
- db.add(new_compliance)
+ new_invoice = models.InvoiceHeader(**invoice_dict)
+ db.add(new_invoice)
+ db.flush() # Flush to get the invoice ID
- # Create financials if provided
- if financials_data:
- financials_dict = financials_data.model_dump()
- financials_dict["invoice_id"] = new_invoice.id
- financials_dict["tenant_id"] = tenant_id
- financials_dict["company_id"] = company_id
- new_financials = models.InvoiceFinancials(**financials_dict)
- db.add(new_financials)
+ # Create compliance_mx if provided
+ if compliance_data:
+ raw_comp_dict = compliance_data.model_dump()
+ # Pasamos los datos por la lavadora para arreglar pedimento, aduana, etc.
+ compliance_dict = clean_dict(raw_comp_dict)
+
+ compliance_dict["invoice_id"] = new_invoice.id
+ compliance_dict["tenant_id"] = tenant_id
+ compliance_dict["company_id"] = company_id
+
+ new_compliance = models.InvoiceComplianceMx(**compliance_dict)
+ db.add(new_compliance)
- # Create logistics entries
- for logistics_item in logistics_data:
- logistics_dict = logistics_item.model_dump()
- logistics_dict["invoice_id"] = new_invoice.id
- logistics_dict["tenant_id"] = tenant_id
- logistics_dict["company_id"] = company_id
- new_logistics = models.InvoiceLogistics(**logistics_dict)
- db.add(new_logistics)
+ # Create financials if provided
+ if financials_data:
+ raw_fin_dict = financials_data.model_dump()
+ financials_dict = clean_dict(raw_fin_dict)
+
+ financials_dict["invoice_id"] = new_invoice.id
+ financials_dict["tenant_id"] = tenant_id
+ financials_dict["company_id"] = company_id
+
+ new_financials = models.InvoiceFinancials(**financials_dict)
+ db.add(new_financials)
- # Create sales details
- for detail_item in details_data:
- detail_dict = detail_item.model_dump()
- detail_dict["invoice_id"] = new_invoice.id
- detail_dict["tenant_id"] = tenant_id
- detail_dict["company_id"] = company_id
- new_detail = models.InvoiceSalesDetails(**detail_dict)
- db.add(new_detail)
+ # Create logistics entries
+ for logistics_item in logistics_data:
+ raw_log_dict = logistics_item.model_dump()
+ logistics_dict = clean_dict(raw_log_dict)
+
+ logistics_dict["invoice_id"] = new_invoice.id
+ logistics_dict["tenant_id"] = tenant_id
+ logistics_dict["company_id"] = company_id
+ new_logistics = models.InvoiceLogistics(**logistics_dict)
+ db.add(new_logistics)
- # Create collections
- for collection_item in collections_data:
- collection_dict = collection_item.model_dump()
- collection_dict["invoice_id"] = new_invoice.id
- collection_dict["tenant_id"] = tenant_id
- collection_dict["company_id"] = company_id
- new_collection = models.InvoiceCollections(**collection_dict)
- db.add(new_collection)
+ # Create sales details
+ for detail_item in details_data:
+ raw_det_dict = detail_item.model_dump()
+ detail_dict = clean_dict(raw_det_dict)
+
+ detail_dict["invoice_id"] = new_invoice.id
+ detail_dict["tenant_id"] = tenant_id
+ detail_dict["company_id"] = company_id
+ new_detail = models.InvoiceSalesDetails(**detail_dict)
+ db.add(new_detail)
- db.commit()
- db.refresh(new_invoice)
- return new_invoice
+ # Create collections
+ for collection_item in collections_data:
+ raw_col_dict = collection_item.model_dump()
+ collection_dict = clean_dict(raw_col_dict)
+
+ collection_dict["invoice_id"] = new_invoice.id
+ collection_dict["tenant_id"] = tenant_id
+ collection_dict["company_id"] = company_id
+ new_collection = models.InvoiceCollections(**collection_dict)
+ db.add(new_collection)
+
+ db.commit()
+ db.refresh(new_invoice)
+ return new_invoice
+
+ except Exception as e:
+ db.rollback()
+ print("\n\n🔥 ERROR AL GUARDAR FACTURA 🔥")
+ print(f"Error: {str(e)}")
+ traceback.print_exc() # Esto imprime el error real en la consola
+ print("--------------------------------\n")
+ raise e
@staticmethod
def update(
@@ -148,7 +192,8 @@ class InvoiceService:
invoice_data: schemas.InvoiceHeaderUpdate,
company_id: int
) -> Optional[models.InvoiceHeader]:
- """Update an existing invoice and its related data"""
+ # ... (El resto de tu código update se queda igual) ...
+ # (Te recomiendo implementar clean_dict aquí también si tienes problemas al editar)
invoice = InvoiceService.get_by_id(
db, invoice_id, tenant_id, company_id)
if not invoice:
@@ -167,9 +212,14 @@ class InvoiceService:
if invoice_data.compliance_mx is not None:
if invoice.compliance_mx:
for key, value in invoice_data.compliance_mx.model_dump(exclude_unset=True).items():
+ # Parche rápido para update
+ if value == "": value = None
setattr(invoice.compliance_mx, key, value)
else:
compliance_dict = invoice_data.compliance_mx.model_dump()
+ # Aplicar limpieza manual si es necesario
+ if 'customs_agent' in compliance_dict: compliance_dict['customs_broker_id'] = compliance_dict.pop('customs_agent')
+
compliance_dict["invoice_id"] = invoice.id
compliance_dict["tenant_id"] = tenant_id
compliance_dict["company_id"] = company_id
@@ -180,6 +230,7 @@ class InvoiceService:
if invoice_data.financials is not None:
if invoice.financials:
for key, value in invoice_data.financials.model_dump(exclude_unset=True).items():
+ if value == "": value = None
setattr(invoice.financials, key, value)
else:
financials_dict = invoice_data.financials.model_dump()
@@ -189,9 +240,6 @@ class InvoiceService:
new_financials = models.InvoiceFinancials(**financials_dict)
db.add(new_financials)
- # Note: For logistics, details, and collections, we're not handling updates here
- # as they are typically managed through separate endpoints for complex operations
-
db.commit()
db.refresh(invoice)
return invoice
@@ -205,172 +253,4 @@ class InvoiceService:
db.delete(invoice)
db.commit()
return True
- return False
-
-
-class InvoiceLogisticsService:
- """Service for Invoice Logistics operations"""
-
- @staticmethod
- def get_by_id(db: Session, logistics_id: int, invoice_id: int) -> Optional[models.InvoiceLogistics]:
- """Get a logistics entry by ID"""
- return (
- db.query(models.InvoiceLogistics)
- .filter(
- models.InvoiceLogistics.logistics_id == logistics_id,
- models.InvoiceLogistics.invoice_id == invoice_id,
- )
- .first()
- )
-
- @staticmethod
- def get_all_by_invoice(db: Session, invoice_id: int) -> List[models.InvoiceLogistics]:
- """Get all logistics entries for an invoice"""
- return (
- db.query(models.InvoiceLogistics)
- .filter(models.InvoiceLogistics.invoice_id == invoice_id)
- .all()
- )
-
- @staticmethod
- def create(
- db: Session,
- logistics_data: schemas.InvoiceLogisticsCreate,
- invoice_id: int,
- tenant_id: int,
- company_id: int
- ) -> models.InvoiceLogistics:
- """Create a new logistics entry"""
- logistics_dict = logistics_data.model_dump()
- logistics_dict["invoice_id"] = invoice_id
- logistics_dict["tenant_id"] = tenant_id
- logistics_dict["company_id"] = company_id
-
- new_logistics = models.InvoiceLogistics(**logistics_dict)
- db.add(new_logistics)
- db.commit()
- db.refresh(new_logistics)
- return new_logistics
-
- @staticmethod
- def delete(db: Session, logistics_id: int, invoice_id: int) -> bool:
- """Delete a logistics entry"""
- logistics = InvoiceLogisticsService.get_by_id(
- db, logistics_id, invoice_id)
- if logistics:
- db.delete(logistics)
- db.commit()
- return True
- return False
-
-
-class InvoiceSalesDetailsService:
- """Service for Invoice Sales Details operations"""
-
- @staticmethod
- def get_by_id(db: Session, detail_id: int, invoice_id: int) -> Optional[models.InvoiceSalesDetails]:
- """Get a sales detail entry by ID"""
- return (
- db.query(models.InvoiceSalesDetails)
- .filter(
- models.InvoiceSalesDetails.detail_id == detail_id,
- models.InvoiceSalesDetails.invoice_id == invoice_id,
- )
- .first()
- )
-
- @staticmethod
- def get_all_by_invoice(db: Session, invoice_id: int) -> List[models.InvoiceSalesDetails]:
- """Get all sales details for an invoice"""
- return (
- db.query(models.InvoiceSalesDetails)
- .filter(models.InvoiceSalesDetails.invoice_id == invoice_id)
- .all()
- )
-
- @staticmethod
- def create(
- db: Session,
- detail_data: schemas.InvoiceSalesDetailsCreate,
- invoice_id: int,
- tenant_id: int,
- company_id: int
- ) -> models.InvoiceSalesDetails:
- """Create a new sales detail entry"""
- detail_dict = detail_data.model_dump()
- detail_dict["invoice_id"] = invoice_id
- detail_dict["tenant_id"] = tenant_id
- detail_dict["company_id"] = company_id
-
- new_detail = models.InvoiceSalesDetails(**detail_dict)
- db.add(new_detail)
- db.commit()
- db.refresh(new_detail)
- return new_detail
-
- @staticmethod
- def delete(db: Session, detail_id: int, invoice_id: int) -> bool:
- """Delete a sales detail entry"""
- detail = InvoiceSalesDetailsService.get_by_id(
- db, detail_id, invoice_id)
- if detail:
- db.delete(detail)
- db.commit()
- return True
- return False
-
-
-class InvoiceCollectionsService:
- """Service for Invoice Collections operations"""
-
- @staticmethod
- def get_by_id(db: Session, collection_id: int, invoice_id: int) -> Optional[models.InvoiceCollections]:
- """Get a collection entry by ID"""
- return (
- db.query(models.InvoiceCollections)
- .filter(
- models.InvoiceCollections.collection_id == collection_id,
- models.InvoiceCollections.invoice_id == invoice_id,
- )
- .first()
- )
-
- @staticmethod
- def get_all_by_invoice(db: Session, invoice_id: int) -> List[models.InvoiceCollections]:
- """Get all collections for an invoice"""
- return (
- db.query(models.InvoiceCollections)
- .filter(models.InvoiceCollections.invoice_id == invoice_id)
- .all()
- )
-
- @staticmethod
- def create(
- db: Session,
- collection_data: schemas.InvoiceCollectionsCreate,
- invoice_id: int,
- tenant_id: int,
- company_id: int
- ) -> models.InvoiceCollections:
- """Create a new collection entry"""
- collection_dict = collection_data.model_dump()
- collection_dict["invoice_id"] = invoice_id
- collection_dict["tenant_id"] = tenant_id
- collection_dict["company_id"] = company_id
-
- new_collection = models.InvoiceCollections(**collection_dict)
- db.add(new_collection)
- db.commit()
- db.refresh(new_collection)
- return new_collection
-
- @staticmethod
- def delete(db: Session, collection_id: int, invoice_id: int) -> bool:
- """Delete a collection entry"""
- collection = InvoiceCollectionsService.get_by_id(
- db, collection_id, invoice_id)
- if collection:
- db.delete(collection)
- db.commit()
- return True
- return False
+ return False
\ No newline at end of file
diff --git a/estructura.txt b/estructura.txt
new file mode 100644
index 00000000..aa2beccb
--- /dev/null
+++ b/estructura.txt
@@ -0,0 +1,1190 @@
+.
+├── azure.crt
+├── backend
+│ ├── alembic
+│ │ ├── env.py
+│ │ ├── README
+│ │ ├── script.py.mako
+│ │ └── versions
+│ │ ├── 531bf8cdae06_create_material_types_table.py
+│ │ └── 7937209f9718_seed_initial_data.py
+│ ├── alembic.ini
+│ ├── api
+│ │ └── v1
+│ │ ├── common
+│ │ │ ├── base_models.py
+│ │ │ ├── crud_routes.py
+│ │ │ ├── dto_mixins.py
+│ │ │ └── tenant_crud_routes.py
+│ │ ├── modules
+│ │ │ ├── a24
+│ │ │ │ ├── fa
+│ │ │ │ │ └── fa_classes
+│ │ │ │ │ └── models.py
+│ │ │ │ └── inv
+│ │ │ │ ├── inv_classes
+│ │ │ │ │ └── models.py
+│ │ │ │ └── location
+│ │ │ │ ├── dto.py
+│ │ │ │ ├── __init__.py
+│ │ │ │ ├── models.py
+│ │ │ │ ├── routes.py
+│ │ │ │ └── service.py
+│ │ │ ├── a76
+│ │ │ │ ├── classes
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── __init__.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ ├── service.py
+│ │ │ │ │ └── test_classes.py
+│ │ │ │ ├── clients_and_providers
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── __init__.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ ├── service.py
+│ │ │ │ │ └── test_client_and_provider.py
+│ │ │ │ ├── country_rule_oct
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ ├── services.py
+│ │ │ │ │ └── test_country_rule_oct.py
+│ │ │ │ ├── customs_brokers
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ └── services.py
+│ │ │ │ ├── fraction_rule_octave
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ ├── services.py
+│ │ │ │ │ └── test_fraction_rule_octave.py
+│ │ │ │ ├── general_catalogs
+│ │ │ │ │ ├── classification_concepts
+│ │ │ │ │ │ ├── dto.py
+│ │ │ │ │ │ ├── __init__.py
+│ │ │ │ │ │ ├── models.py
+│ │ │ │ │ │ ├── routes.py
+│ │ │ │ │ │ └── service.py
+│ │ │ │ │ ├── company
+│ │ │ │ │ │ ├── dto.py
+│ │ │ │ │ │ ├── __init__.py
+│ │ │ │ │ │ ├── models.py
+│ │ │ │ │ │ ├── routes.py
+│ │ │ │ │ │ ├── service.py
+│ │ │ │ │ │ └── test_company.py
+│ │ │ │ │ ├── concepts
+│ │ │ │ │ │ ├── dto.py
+│ │ │ │ │ │ ├── __init__.py
+│ │ │ │ │ │ ├── models.py
+│ │ │ │ │ │ ├── routes.py
+│ │ │ │ │ │ └── service.py
+│ │ │ │ │ ├── customs_broker_concepts
+│ │ │ │ │ │ ├── dto.py
+│ │ │ │ │ │ ├── __init__.py
+│ │ │ │ │ │ ├── models.py
+│ │ │ │ │ │ ├── routes.py
+│ │ │ │ │ │ └── service.py
+│ │ │ │ │ ├── doda
+│ │ │ │ │ │ ├── dto.py
+│ │ │ │ │ │ ├── __init__.py
+│ │ │ │ │ │ ├── models.py
+│ │ │ │ │ │ ├── routes.py
+│ │ │ │ │ │ └── service.py
+│ │ │ │ │ ├── electronic_notices
+│ │ │ │ │ │ ├── dto.py
+│ │ │ │ │ │ ├── __init__.py
+│ │ │ │ │ │ ├── models.py
+│ │ │ │ │ │ ├── routes.py
+│ │ │ │ │ │ └── service.py
+│ │ │ │ │ ├── equivalencies
+│ │ │ │ │ │ ├── dto.py
+│ │ │ │ │ │ ├── __init__.py
+│ │ │ │ │ │ ├── models.py
+│ │ │ │ │ │ ├── routes.py
+│ │ │ │ │ │ └── service.py
+│ │ │ │ │ ├── error_catalogs
+│ │ │ │ │ │ ├── dto.py
+│ │ │ │ │ │ ├── __init__.py
+│ │ │ │ │ │ ├── models.py
+│ │ │ │ │ │ ├── routes.py
+│ │ │ │ │ │ └── service.py
+│ │ │ │ │ ├── exchange_rate
+│ │ │ │ │ │ ├── dto.py
+│ │ │ │ │ │ ├── models.py
+│ │ │ │ │ │ ├── routes.py
+│ │ │ │ │ │ ├── services.py
+│ │ │ │ │ │ └── test_exchange_rate.py
+│ │ │ │ │ ├── identifiers
+│ │ │ │ │ │ ├── dto.py
+│ │ │ │ │ │ ├── __init__.py
+│ │ │ │ │ │ ├── models.py
+│ │ │ │ │ │ ├── routes.py
+│ │ │ │ │ │ └── service.py
+│ │ │ │ │ ├── inpc
+│ │ │ │ │ │ ├── dto.py
+│ │ │ │ │ │ ├── __init__.py
+│ │ │ │ │ │ ├── models.py
+│ │ │ │ │ │ ├── routes.py
+│ │ │ │ │ │ └── service.py
+│ │ │ │ │ ├── legends
+│ │ │ │ │ │ ├── dto.py
+│ │ │ │ │ │ ├── __init__.py
+│ │ │ │ │ │ ├── models.py
+│ │ │ │ │ │ ├── routes.py
+│ │ │ │ │ │ └── service.py
+│ │ │ │ │ ├── multi_currency_types
+│ │ │ │ │ │ ├── dto.py
+│ │ │ │ │ │ ├── __init__.py
+│ │ │ │ │ │ ├── models.py
+│ │ │ │ │ │ ├── routes.py
+│ │ │ │ │ │ └── service.py
+│ │ │ │ │ ├── packages
+│ │ │ │ │ │ ├── dto.py
+│ │ │ │ │ │ ├── models.py
+│ │ │ │ │ │ ├── routes.py
+│ │ │ │ │ │ ├── services.py
+│ │ │ │ │ │ └── test_package.py
+│ │ │ │ │ ├── ports
+│ │ │ │ │ │ ├── dto.py
+│ │ │ │ │ │ ├── __init__.py
+│ │ │ │ │ │ ├── models.py
+│ │ │ │ │ │ ├── routes.py
+│ │ │ │ │ │ └── service.py
+│ │ │ │ │ ├── prevalidators
+│ │ │ │ │ │ ├── dto.py
+│ │ │ │ │ │ ├── __init__.py
+│ │ │ │ │ │ ├── models.py
+│ │ │ │ │ │ ├── routes.py
+│ │ │ │ │ │ └── service.py
+│ │ │ │ │ ├── seal
+│ │ │ │ │ │ ├── dto.py
+│ │ │ │ │ │ ├── models.py
+│ │ │ │ │ │ ├── routes.py
+│ │ │ │ │ │ ├── services.py
+│ │ │ │ │ │ └── test_seal.py
+│ │ │ │ │ ├── signatures
+│ │ │ │ │ │ ├── dto.py
+│ │ │ │ │ │ ├── __init__.py
+│ │ │ │ │ │ ├── models.py
+│ │ │ │ │ │ ├── routes.py
+│ │ │ │ │ │ └── service.py
+│ │ │ │ │ ├── unit_conversions
+│ │ │ │ │ │ ├── dto.py
+│ │ │ │ │ │ ├── __init__.py
+│ │ │ │ │ │ ├── models.py
+│ │ │ │ │ │ ├── routes.py
+│ │ │ │ │ │ └── service.py
+│ │ │ │ │ └── units_of_measure
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── __init__.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ └── service.py
+│ │ │ │ ├── invoices
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ ├── schemas.py
+│ │ │ │ │ └── services.py
+│ │ │ │ ├── parts
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── __init__.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ ├── service.py
+│ │ │ │ │ └── test_parts.py
+│ │ │ │ ├── pedmientos
+│ │ │ │ │ ├── dtos
+│ │ │ │ │ │ ├── pedimento_config_additional.py
+│ │ │ │ │ │ ├── pedimento_config_calculations.py
+│ │ │ │ │ │ ├── pedimento_config_parameters.py
+│ │ │ │ │ │ ├── pedimento_config_surcharges.py
+│ │ │ │ │ │ ├── pedimento_config_update_rectification.py
+│ │ │ │ │ │ ├── pedimento_config_updates.py
+│ │ │ │ │ │ ├── pedimento_customs_offices.py
+│ │ │ │ │ │ ├── pedimento_dates.py
+│ │ │ │ │ │ ├── pedimento_decrementables.py
+│ │ │ │ │ │ ├── pedimento_incrementables.py
+│ │ │ │ │ │ ├── pedimento_indexes.py
+│ │ │ │ │ │ ├── pedimento_payments.py
+│ │ │ │ │ │ ├── pedimento_rectification_destination.py
+│ │ │ │ │ │ ├── pedimento_rectification_origin.py
+│ │ │ │ │ │ ├── pedimentos.py
+│ │ │ │ │ │ ├── pedimento_transport_means.py
+│ │ │ │ │ │ └── pedimento_validation.py
+│ │ │ │ │ ├── models
+│ │ │ │ │ │ ├── pedimento_config_additional.py
+│ │ │ │ │ │ ├── pedimento_config_calculations.py
+│ │ │ │ │ │ ├── pedimento_config_parameters.py
+│ │ │ │ │ │ ├── pedimento_config_surcharges.py
+│ │ │ │ │ │ ├── pedimento_config_update_rectification.py
+│ │ │ │ │ │ ├── pedimento_config_updates.py
+│ │ │ │ │ │ ├── pedimento_customs_offices.py
+│ │ │ │ │ │ ├── pedimento_dates.py
+│ │ │ │ │ │ ├── pedimento_decrementables.py
+│ │ │ │ │ │ ├── pedimento_incrementables.py
+│ │ │ │ │ │ ├── pedimento_indexes.py
+│ │ │ │ │ │ ├── pedimento_payments.py
+│ │ │ │ │ │ ├── pedimento_rectification_destination.py
+│ │ │ │ │ │ ├── pedimento_rectification_origin.py
+│ │ │ │ │ │ ├── pedimentos.py
+│ │ │ │ │ │ ├── pedimento_transport_means.py
+│ │ │ │ │ │ └── pedimento_validation.py
+│ │ │ │ │ ├── router.py
+│ │ │ │ │ ├── routes
+│ │ │ │ │ │ ├── pedimento_config_additional.py
+│ │ │ │ │ │ ├── pedimento_config_calculations.py
+│ │ │ │ │ │ ├── pedimento_config_parameters.py
+│ │ │ │ │ │ ├── pedimento_config_surcharges.py
+│ │ │ │ │ │ ├── pedimento_config_update_rectification.py
+│ │ │ │ │ │ ├── pedimento_config_updates.py
+│ │ │ │ │ │ ├── pedimento_customs_offices.py
+│ │ │ │ │ │ ├── pedimento_dates.py
+│ │ │ │ │ │ ├── pedimento_decrementables.py
+│ │ │ │ │ │ ├── pedimento_incrementables.py
+│ │ │ │ │ │ ├── pedimento_indexes.py
+│ │ │ │ │ │ ├── pedimento_payments.py
+│ │ │ │ │ │ ├── pedimento_rectification_destination.py
+│ │ │ │ │ │ ├── pedimento_rectification_origin.py
+│ │ │ │ │ │ ├── pedimentos.py
+│ │ │ │ │ │ ├── pedimento_transport_means.py
+│ │ │ │ │ │ └── pedimento_validation.py
+│ │ │ │ │ └── services
+│ │ │ │ │ ├── pedimento_config_additional.py
+│ │ │ │ │ ├── pedimento_config_calculations.py
+│ │ │ │ │ ├── pedimento_config_parameters.py
+│ │ │ │ │ ├── pedimento_config_surcharges.py
+│ │ │ │ │ ├── pedimento_config_update_rectification.py
+│ │ │ │ │ ├── pedimento_config_updates.py
+│ │ │ │ │ ├── pedimento_customs_offices.py
+│ │ │ │ │ ├── pedimento_dates.py
+│ │ │ │ │ ├── pedimento_decrementables.py
+│ │ │ │ │ ├── pedimento_incrementables.py
+│ │ │ │ │ ├── pedimento_indexes.py
+│ │ │ │ │ ├── pedimento_payments.py
+│ │ │ │ │ ├── pedimento_rectification_destination.py
+│ │ │ │ │ ├── pedimento_rectification_origin.py
+│ │ │ │ │ ├── pedimentos.py
+│ │ │ │ │ ├── pedimento_transport_means.py
+│ │ │ │ │ └── pedimento_validation.py
+│ │ │ │ ├── permission_rule_oct
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ ├── services.py
+│ │ │ │ │ └── test_permission_rule_oct.py
+│ │ │ │ ├── router.py
+│ │ │ │ └── transportation
+│ │ │ │ ├── drivers
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ └── services.py
+│ │ │ │ ├── trailers
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ └── services.py
+│ │ │ │ ├── transporters
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ └── services.py
+│ │ │ │ └── vehicles
+│ │ │ │ ├── dto.py
+│ │ │ │ ├── models.py
+│ │ │ │ ├── routes.py
+│ │ │ │ └── services.py
+│ │ │ ├── core
+│ │ │ │ ├── auth
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── __init__.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ └── service.py
+│ │ │ │ ├── licenses
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── __init__.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ └── service.py
+│ │ │ │ ├── router.py
+│ │ │ │ ├── tenants
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── __init__.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ └── service.py
+│ │ │ │ └── user_tenant
+│ │ │ │ ├── dto.py
+│ │ │ │ ├── models.py
+│ │ │ │ ├── routes.py
+│ │ │ │ └── service.py
+│ │ │ └── public
+│ │ │ ├── __init__.py
+│ │ │ ├── reference_data
+│ │ │ │ ├── code_pedimento_regimens
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ ├── seed.py
+│ │ │ │ │ └── test_code_pedimento_regimens.py
+│ │ │ │ ├── conftest.py
+│ │ │ │ ├── containers
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ ├── seed.py
+│ │ │ │ │ └── test_containers.py
+│ │ │ │ ├── countries
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ ├── seed.py
+│ │ │ │ │ └── test_countries.py
+│ │ │ │ ├── currency_types
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ ├── seed.py
+│ │ │ │ │ └── test_currency_types.py
+│ │ │ │ ├── customs_sections
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ ├── seed.py
+│ │ │ │ │ └── test_customs_sections.py
+│ │ │ │ ├── customs_warehouses
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ ├── seed.py
+│ │ │ │ │ └── test_customs_warehouses.py
+│ │ │ │ ├── incoterms
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ ├── seed.py
+│ │ │ │ │ └── test_incoterms.py
+│ │ │ │ ├── invoice_types
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ ├── seed.py
+│ │ │ │ │ └── test_invoice_types.py
+│ │ │ │ ├── material_types
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ ├── seed.py
+│ │ │ │ │ └── test_material_types.py
+│ │ │ │ ├── payment_methods
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ ├── seed.py
+│ │ │ │ │ └── test_payment_methods.py
+│ │ │ │ ├── pedimento_codes
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ ├── seed.py
+│ │ │ │ │ └── test_pedimento_codes.py
+│ │ │ │ ├── pedimento_regimens
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ ├── seed.py
+│ │ │ │ │ └── test_pedimento_regimens.py
+│ │ │ │ ├── router.py
+│ │ │ │ ├── sectors
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ ├── seed.py
+│ │ │ │ │ └── test_sectors.py
+│ │ │ │ ├── states
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ ├── seed.py
+│ │ │ │ │ └── test_states.py
+│ │ │ │ ├── trailer_types
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ └── services.py
+│ │ │ │ ├── transport_modes
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ ├── seed.py
+│ │ │ │ │ └── test_transport_modes.py
+│ │ │ │ ├── transport_types
+│ │ │ │ │ ├── dto.py
+│ │ │ │ │ ├── models.py
+│ │ │ │ │ ├── routes.py
+│ │ │ │ │ ├── seed.py
+│ │ │ │ │ └── test_transport_types.py
+│ │ │ │ └── valuation_methods
+│ │ │ │ ├── dto.py
+│ │ │ │ ├── models.py
+│ │ │ │ ├── routes.py
+│ │ │ │ ├── seed.py
+│ │ │ │ └── test_valuation_methods.py
+│ │ │ └── router.py
+│ │ └── router.py
+│ ├── core
+│ │ ├── config.py
+│ │ ├── database.py
+│ │ ├── __init__.py
+│ │ ├── middleware.py
+│ │ └── security.py
+│ ├── Dockerfile
+│ ├── main.py
+│ ├── __pycache__
+│ └── requirements.txt
+├── docker-compose.yml
+├── docs
+│ ├── a76.json
+│ ├── ARCHITECTURE.md
+│ ├── KEYCLOAK_SETUP.md
+│ ├── MICROSOFT_SSO_SETUP.md
+│ └── VERIFICAR_MICROSOFT_CONFIG.md
+├── estructura.txt
+├── frontend
+│ ├── components.json
+│ ├── Dockerfile
+│ ├── e2e
+│ │ └── demo.test.ts
+│ ├── eslint.config.js
+│ ├── messages
+│ │ ├── en.json
+│ │ └── es.json
+│ ├── node_modules
+│ ├── package.json
+│ ├── playwright.config.ts
+│ ├── pnpm-lock.yaml
+│ ├── pnpm-workspace.yaml
+│ ├── project.inlang
+│ │ ├── cache
+│ │ │ └── plugins
+│ │ │ ├── 2sy648wh9sugi
+│ │ │ └── ygx0uiahq6uw
+│ │ ├── project_id
+│ │ └── settings.json
+│ ├── README.md
+│ ├── src
+│ │ ├── app.css
+│ │ ├── app.d.ts
+│ │ ├── app.html
+│ │ ├── demo.spec.ts
+│ │ ├── hooks.server.ts
+│ │ ├── hooks.ts
+│ │ ├── lib
+│ │ │ ├── api
+│ │ │ │ └── dashboard
+│ │ │ │ ├── a76
+│ │ │ │ │ ├── classes.ts
+│ │ │ │ │ ├── clients-providers.ts
+│ │ │ │ │ ├── customs-brokers.ts
+│ │ │ │ │ ├── general_catalogs
+│ │ │ │ │ │ ├── classification-concepts.ts
+│ │ │ │ │ │ ├── company.ts
+│ │ │ │ │ │ ├── concepts.ts
+│ │ │ │ │ │ ├── customs-broker-concepts.ts
+│ │ │ │ │ │ ├── doda.ts
+│ │ │ │ │ │ ├── electronic-notices.ts
+│ │ │ │ │ │ ├── equivalencies.ts
+│ │ │ │ │ │ ├── error-catalogs.ts
+│ │ │ │ │ │ ├── exchange-rate.ts
+│ │ │ │ │ │ ├── identifiers.ts
+│ │ │ │ │ │ ├── index.ts
+│ │ │ │ │ │ ├── inpc.ts
+│ │ │ │ │ │ ├── legends.ts
+│ │ │ │ │ │ ├── locations.ts
+│ │ │ │ │ │ ├── multi-currency-types.ts
+│ │ │ │ │ │ ├── packages.ts
+│ │ │ │ │ │ ├── ports.ts
+│ │ │ │ │ │ ├── prevalidators.ts
+│ │ │ │ │ │ ├── seal.ts
+│ │ │ │ │ │ ├── signatures.ts
+│ │ │ │ │ │ ├── um-ace.ts
+│ │ │ │ │ │ ├── um-customs-ame.ts
+│ │ │ │ │ │ ├── um-customs-mex.ts
+│ │ │ │ │ │ ├── um-oma.ts
+│ │ │ │ │ │ ├── unit-conversions.ts
+│ │ │ │ │ │ ├── unit-measures.ts
+│ │ │ │ │ │ └── units-of-measure.ts
+│ │ │ │ │ ├── index.ts
+│ │ │ │ │ ├── invoices.ts
+│ │ │ │ │ ├── pedimento-dates.ts
+│ │ │ │ │ ├── pedimento-payments.ts
+│ │ │ │ │ ├── pedimentos.ts
+│ │ │ │ │ ├── pedimento-transport.ts
+│ │ │ │ │ └── pedimento-validation.ts
+│ │ │ │ └── refrence_data
+│ │ │ │ ├── code_pedimento_regimens.ts
+│ │ │ │ ├── containers.ts
+│ │ │ │ ├── countries.ts
+│ │ │ │ ├── currency_types.ts
+│ │ │ │ ├── customs_sections.ts
+│ │ │ │ ├── customs_warehouses.ts
+│ │ │ │ ├── incoterms.ts
+│ │ │ │ ├── invoice_types.ts
+│ │ │ │ ├── material_types.ts
+│ │ │ │ ├── payment_methods.ts
+│ │ │ │ ├── pedimento_codes.ts
+│ │ │ │ ├── pedimento_regimens.ts
+│ │ │ │ ├── sectors.ts
+│ │ │ │ ├── states.ts
+│ │ │ │ ├── transport_modes.ts
+│ │ │ │ ├── transport_types.ts
+│ │ │ │ └── valuation_methods.ts
+│ │ │ ├── api.ts
+│ │ │ ├── assets
+│ │ │ │ └── favicon.svg
+│ │ │ ├── auth.ts
+│ │ │ ├── components
+│ │ │ │ ├── dashboard
+│ │ │ │ │ ├── classes
+│ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ ├── data-table-actions.svelte
+│ │ │ │ │ │ └── data-table.svelte
+│ │ │ │ │ ├── clients_and_providers
+│ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ ├── data-table-actions.svelte
+│ │ │ │ │ │ ├── data-table.svelte
+│ │ │ │ │ │ ├── delete-dialog.svelte
+│ │ │ │ │ │ └── details-dialog.svelte
+│ │ │ │ │ ├── company
+│ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ └── data-table-actions.svelte
+│ │ │ │ │ ├── customs_brokers
+│ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ ├── create-dialog.svelte
+│ │ │ │ │ │ ├── data-table-actions.svelte
+│ │ │ │ │ │ ├── data-table.svelte
+│ │ │ │ │ │ ├── delete-dialog.svelte
+│ │ │ │ │ │ ├── details-dialog.svelte
+│ │ │ │ │ │ └── edit-dialog.svelte
+│ │ │ │ │ ├── exchange-rate
+│ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ ├── data-table-actions.svelte
+│ │ │ │ │ │ └── data-table.svelte
+│ │ │ │ │ ├── general_catalogs
+│ │ │ │ │ │ └── simple-data-table.svelte
+│ │ │ │ │ ├── identifiers
+│ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ ├── data-table-actions.svelte
+│ │ │ │ │ │ └── data-table.svelte
+│ │ │ │ │ ├── invoices
+│ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ ├── data-table-actions.svelte
+│ │ │ │ │ │ ├── data-table.svelte
+│ │ │ │ │ │ ├── delete-dialog.svelte
+│ │ │ │ │ │ └── details-dialog.svelte
+│ │ │ │ │ ├── locations
+│ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ └── data-table.svelte
+│ │ │ │ │ ├── packages
+│ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ ├── data-table-actions.svelte
+│ │ │ │ │ │ └── data-table.svelte
+│ │ │ │ │ ├── pedimentos
+│ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ ├── data-table-actions.svelte
+│ │ │ │ │ │ ├── data-table.svelte
+│ │ │ │ │ │ └── edit
+│ │ │ │ │ │ ├── dates-tab-form.svelte
+│ │ │ │ │ │ ├── general-tab-form.svelte
+│ │ │ │ │ │ ├── payments-tab-form.svelte
+│ │ │ │ │ │ ├── transport-tab-form.svelte
+│ │ │ │ │ │ └── validation-tab-form.svelte
+│ │ │ │ │ ├── ports
+│ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ └── data-table-actions.svelte
+│ │ │ │ │ ├── reference_data
+│ │ │ │ │ │ ├── code_pedimento_regimens
+│ │ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ │ ├── data-table-actions.svelte
+│ │ │ │ │ │ │ ├── data-table.svelte
+│ │ │ │ │ │ │ ├── delete-dialog.svelte
+│ │ │ │ │ │ │ └── details-dialog.svelte
+│ │ │ │ │ │ ├── containers
+│ │ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ │ ├── data-table-actions.svelte
+│ │ │ │ │ │ │ ├── data-table.svelte
+│ │ │ │ │ │ │ ├── delete-dialog.svelte
+│ │ │ │ │ │ │ └── details-dialog.svelte
+│ │ │ │ │ │ ├── countries
+│ │ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ │ ├── data-table-actions.svelte
+│ │ │ │ │ │ │ ├── data-table.svelte
+│ │ │ │ │ │ │ ├── delete-dialog.svelte
+│ │ │ │ │ │ │ └── details-dialog.svelte
+│ │ │ │ │ │ ├── currency_types
+│ │ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ │ ├── data-table-actions.svelte
+│ │ │ │ │ │ │ ├── data-table.svelte
+│ │ │ │ │ │ │ ├── delete-dialog.svelte
+│ │ │ │ │ │ │ └── details-dialog.svelte
+│ │ │ │ │ │ ├── customs_sections
+│ │ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ │ ├── data-table-actions.svelte
+│ │ │ │ │ │ │ ├── data-table.svelte
+│ │ │ │ │ │ │ ├── delete-dialog.svelte
+│ │ │ │ │ │ │ └── details-dialog.svelte
+│ │ │ │ │ │ ├── customs_warehouses
+│ │ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ │ ├── data-table-actions.svelte
+│ │ │ │ │ │ │ ├── data-table.svelte
+│ │ │ │ │ │ │ ├── delete-dialog.svelte
+│ │ │ │ │ │ │ └── details-dialog.svelte
+│ │ │ │ │ │ ├── incoterms
+│ │ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ │ ├── data-table-actions.svelte
+│ │ │ │ │ │ │ ├── data-table.svelte
+│ │ │ │ │ │ │ ├── delete-dialog.svelte
+│ │ │ │ │ │ │ └── details-dialog.svelte
+│ │ │ │ │ │ ├── invoice_types
+│ │ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ │ ├── data-table-actions.svelte
+│ │ │ │ │ │ │ ├── data-table.svelte
+│ │ │ │ │ │ │ ├── delete-dialog.svelte
+│ │ │ │ │ │ │ └── details-dialog.svelte
+│ │ │ │ │ │ ├── material_types
+│ │ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ │ ├── data-table-actions.svelte
+│ │ │ │ │ │ │ ├── data-table.svelte
+│ │ │ │ │ │ │ ├── delete-dialog.svelte
+│ │ │ │ │ │ │ └── details-dialog.svelte
+│ │ │ │ │ │ ├── payment_methods
+│ │ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ │ ├── data-table-actions.svelte
+│ │ │ │ │ │ │ ├── data-table.svelte
+│ │ │ │ │ │ │ ├── delete-dialog.svelte
+│ │ │ │ │ │ │ └── details-dialog.svelte
+│ │ │ │ │ │ ├── pedimento_codes
+│ │ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ │ ├── data-table-actions.svelte
+│ │ │ │ │ │ │ ├── data-table.svelte
+│ │ │ │ │ │ │ ├── delete-dialog.svelte
+│ │ │ │ │ │ │ └── details-dialog.svelte
+│ │ │ │ │ │ ├── pedimento_regimens
+│ │ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ │ ├── data-table-actions.svelte
+│ │ │ │ │ │ │ ├── data-table.svelte
+│ │ │ │ │ │ │ ├── delete-dialog.svelte
+│ │ │ │ │ │ │ └── details-dialog.svelte
+│ │ │ │ │ │ ├── sectors
+│ │ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ │ ├── data-table-actions.svelte
+│ │ │ │ │ │ │ ├── data-table.svelte
+│ │ │ │ │ │ │ ├── delete-dialog.svelte
+│ │ │ │ │ │ │ └── details-dialog.svelte
+│ │ │ │ │ │ ├── states
+│ │ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ │ ├── data-table-actions.svelte
+│ │ │ │ │ │ │ ├── data-table.svelte
+│ │ │ │ │ │ │ ├── delete-dialog.svelte
+│ │ │ │ │ │ │ └── details-dialog.svelte
+│ │ │ │ │ │ ├── transport_modes
+│ │ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ │ ├── data-table-actions.svelte
+│ │ │ │ │ │ │ ├── data-table.svelte
+│ │ │ │ │ │ │ ├── delete-dialog.svelte
+│ │ │ │ │ │ │ └── details-dialog.svelte
+│ │ │ │ │ │ ├── transport_types
+│ │ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ │ ├── data-table-actions.svelte
+│ │ │ │ │ │ │ ├── data-table.svelte
+│ │ │ │ │ │ │ ├── delete-dialog.svelte
+│ │ │ │ │ │ │ └── details-dialog.svelte
+│ │ │ │ │ │ └── valuation_methods
+│ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ ├── data-table-actions.svelte
+│ │ │ │ │ │ ├── data-table.svelte
+│ │ │ │ │ │ ├── delete-dialog.svelte
+│ │ │ │ │ │ └── details-dialog.svelte
+│ │ │ │ │ ├── seal
+│ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ ├── data-table-actions.svelte
+│ │ │ │ │ │ ├── data-table.svelte
+│ │ │ │ │ │ └── index.ts
+│ │ │ │ │ └── units_of_measure
+│ │ │ │ │ ├── ace
+│ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ ├── data-table-actions.svelte
+│ │ │ │ │ │ └── data-table.svelte
+│ │ │ │ │ ├── american
+│ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ └── data-table-actions.svelte
+│ │ │ │ │ ├── customs
+│ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ ├── data-table-actions.svelte
+│ │ │ │ │ │ └── data-table.svelte
+│ │ │ │ │ ├── general
+│ │ │ │ │ │ ├── columns.ts
+│ │ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ │ ├── data-table-actions.svelte
+│ │ │ │ │ │ └── data-table.svelte
+│ │ │ │ │ └── oma
+│ │ │ │ │ ├── columns.ts
+│ │ │ │ │ ├── create-edit-dialog.svelte
+│ │ │ │ │ └── data-table-actions.svelte
+│ │ │ │ ├── login-form.svelte
+│ │ │ │ ├── sidebar
+│ │ │ │ │ ├── app-sidebar.svelte
+│ │ │ │ │ ├── modules.ts
+│ │ │ │ │ ├── nav-main.svelte
+│ │ │ │ │ ├── nav-projects.svelte
+│ │ │ │ │ ├── nav-user.svelte
+│ │ │ │ │ └── team-switcher.svelte
+│ │ │ │ └── ui
+│ │ │ │ ├── alert
+│ │ │ │ │ ├── alert-description.svelte
+│ │ │ │ │ ├── alert.svelte
+│ │ │ │ │ ├── alert-title.svelte
+│ │ │ │ │ └── index.ts
+│ │ │ │ ├── alert-dialog
+│ │ │ │ │ ├── alert-dialog-action.svelte
+│ │ │ │ │ ├── alert-dialog-cancel.svelte
+│ │ │ │ │ ├── alert-dialog-content.svelte
+│ │ │ │ │ ├── alert-dialog-description.svelte
+│ │ │ │ │ ├── alert-dialog-footer.svelte
+│ │ │ │ │ ├── alert-dialog-header.svelte
+│ │ │ │ │ ├── alert-dialog-overlay.svelte
+│ │ │ │ │ ├── alert-dialog-title.svelte
+│ │ │ │ │ ├── alert-dialog-trigger.svelte
+│ │ │ │ │ └── index.ts
+│ │ │ │ ├── avatar
+│ │ │ │ │ ├── avatar-fallback.svelte
+│ │ │ │ │ ├── avatar-image.svelte
+│ │ │ │ │ ├── avatar.svelte
+│ │ │ │ │ └── index.ts
+│ │ │ │ ├── badge
+│ │ │ │ │ ├── badge.svelte
+│ │ │ │ │ └── index.ts
+│ │ │ │ ├── breadcrumb
+│ │ │ │ │ ├── breadcrumb-ellipsis.svelte
+│ │ │ │ │ ├── breadcrumb-item.svelte
+│ │ │ │ │ ├── breadcrumb-link.svelte
+│ │ │ │ │ ├── breadcrumb-list.svelte
+│ │ │ │ │ ├── breadcrumb-page.svelte
+│ │ │ │ │ ├── breadcrumb-separator.svelte
+│ │ │ │ │ ├── breadcrumb.svelte
+│ │ │ │ │ └── index.ts
+│ │ │ │ ├── button
+│ │ │ │ │ ├── button.svelte
+│ │ │ │ │ └── index.ts
+│ │ │ │ ├── card
+│ │ │ │ │ ├── card-action.svelte
+│ │ │ │ │ ├── card-content.svelte
+│ │ │ │ │ ├── card-description.svelte
+│ │ │ │ │ ├── card-footer.svelte
+│ │ │ │ │ ├── card-header.svelte
+│ │ │ │ │ ├── card.svelte
+│ │ │ │ │ ├── card-title.svelte
+│ │ │ │ │ └── index.ts
+│ │ │ │ ├── collapsible
+│ │ │ │ │ ├── collapsible-content.svelte
+│ │ │ │ │ ├── collapsible.svelte
+│ │ │ │ │ ├── collapsible-trigger.svelte
+│ │ │ │ │ └── index.ts
+│ │ │ │ ├── data-table
+│ │ │ │ │ ├── data-table.svelte.ts
+│ │ │ │ │ ├── flex-render.svelte
+│ │ │ │ │ ├── index.ts
+│ │ │ │ │ └── render-helpers.ts
+│ │ │ │ ├── dialog
+│ │ │ │ │ ├── dialog-close.svelte
+│ │ │ │ │ ├── dialog-content.svelte
+│ │ │ │ │ ├── dialog-description.svelte
+│ │ │ │ │ ├── dialog-footer.svelte
+│ │ │ │ │ ├── dialog-header.svelte
+│ │ │ │ │ ├── dialog-overlay.svelte
+│ │ │ │ │ ├── dialog-title.svelte
+│ │ │ │ │ ├── dialog-trigger.svelte
+│ │ │ │ │ └── index.ts
+│ │ │ │ ├── dropdown-menu
+│ │ │ │ │ ├── dropdown-menu-checkbox-item.svelte
+│ │ │ │ │ ├── dropdown-menu-content.svelte
+│ │ │ │ │ ├── dropdown-menu-group-heading.svelte
+│ │ │ │ │ ├── dropdown-menu-group.svelte
+│ │ │ │ │ ├── dropdown-menu-item.svelte
+│ │ │ │ │ ├── dropdown-menu-label.svelte
+│ │ │ │ │ ├── dropdown-menu-radio-group.svelte
+│ │ │ │ │ ├── dropdown-menu-radio-item.svelte
+│ │ │ │ │ ├── dropdown-menu-separator.svelte
+│ │ │ │ │ ├── dropdown-menu-shortcut.svelte
+│ │ │ │ │ ├── dropdown-menu-sub-content.svelte
+│ │ │ │ │ ├── dropdown-menu-sub-trigger.svelte
+│ │ │ │ │ ├── dropdown-menu-trigger.svelte
+│ │ │ │ │ └── index.ts
+│ │ │ │ ├── field
+│ │ │ │ │ ├── field-content.svelte
+│ │ │ │ │ ├── field-description.svelte
+│ │ │ │ │ ├── field-error.svelte
+│ │ │ │ │ ├── field-group.svelte
+│ │ │ │ │ ├── field-label.svelte
+│ │ │ │ │ ├── field-legend.svelte
+│ │ │ │ │ ├── field-separator.svelte
+│ │ │ │ │ ├── field-set.svelte
+│ │ │ │ │ ├── field.svelte
+│ │ │ │ │ ├── field-title.svelte
+│ │ │ │ │ └── index.ts
+│ │ │ │ ├── input
+│ │ │ │ │ ├── index.ts
+│ │ │ │ │ └── input.svelte
+│ │ │ │ ├── label
+│ │ │ │ │ ├── index.ts
+│ │ │ │ │ └── label.svelte
+│ │ │ │ ├── select
+│ │ │ │ │ ├── index.ts
+│ │ │ │ │ ├── select-content.svelte
+│ │ │ │ │ ├── select-group-heading.svelte
+│ │ │ │ │ ├── select-group.svelte
+│ │ │ │ │ ├── select-item.svelte
+│ │ │ │ │ ├── select-label.svelte
+│ │ │ │ │ ├── select-scroll-down-button.svelte
+│ │ │ │ │ ├── select-scroll-up-button.svelte
+│ │ │ │ │ ├── select-separator.svelte
+│ │ │ │ │ └── select-trigger.svelte
+│ │ │ │ ├── separator
+│ │ │ │ │ ├── index.ts
+│ │ │ │ │ └── separator.svelte
+│ │ │ │ ├── sheet
+│ │ │ │ │ ├── index.ts
+│ │ │ │ │ ├── sheet-close.svelte
+│ │ │ │ │ ├── sheet-content.svelte
+│ │ │ │ │ ├── sheet-description.svelte
+│ │ │ │ │ ├── sheet-footer.svelte
+│ │ │ │ │ ├── sheet-header.svelte
+│ │ │ │ │ ├── sheet-overlay.svelte
+│ │ │ │ │ ├── sheet-title.svelte
+│ │ │ │ │ └── sheet-trigger.svelte
+│ │ │ │ ├── sidebar
+│ │ │ │ │ ├── constants.ts
+│ │ │ │ │ ├── context.svelte.ts
+│ │ │ │ │ ├── index.ts
+│ │ │ │ │ ├── sidebar-content.svelte
+│ │ │ │ │ ├── sidebar-footer.svelte
+│ │ │ │ │ ├── sidebar-group-action.svelte
+│ │ │ │ │ ├── sidebar-group-content.svelte
+│ │ │ │ │ ├── sidebar-group-label.svelte
+│ │ │ │ │ ├── sidebar-group.svelte
+│ │ │ │ │ ├── sidebar-header.svelte
+│ │ │ │ │ ├── sidebar-input.svelte
+│ │ │ │ │ ├── sidebar-inset.svelte
+│ │ │ │ │ ├── sidebar-menu-action.svelte
+│ │ │ │ │ ├── sidebar-menu-badge.svelte
+│ │ │ │ │ ├── sidebar-menu-button.svelte
+│ │ │ │ │ ├── sidebar-menu-item.svelte
+│ │ │ │ │ ├── sidebar-menu-skeleton.svelte
+│ │ │ │ │ ├── sidebar-menu-sub-button.svelte
+│ │ │ │ │ ├── sidebar-menu-sub-item.svelte
+│ │ │ │ │ ├── sidebar-menu-sub.svelte
+│ │ │ │ │ ├── sidebar-menu.svelte
+│ │ │ │ │ ├── sidebar-provider.svelte
+│ │ │ │ │ ├── sidebar-rail.svelte
+│ │ │ │ │ ├── sidebar-separator.svelte
+│ │ │ │ │ ├── sidebar.svelte
+│ │ │ │ │ └── sidebar-trigger.svelte
+│ │ │ │ ├── skeleton
+│ │ │ │ │ ├── index.ts
+│ │ │ │ │ └── skeleton.svelte
+│ │ │ │ ├── switch
+│ │ │ │ │ ├── index.ts
+│ │ │ │ │ └── switch.svelte
+│ │ │ │ ├── table
+│ │ │ │ │ ├── index.ts
+│ │ │ │ │ ├── table-body.svelte
+│ │ │ │ │ ├── table-caption.svelte
+│ │ │ │ │ ├── table-cell.svelte
+│ │ │ │ │ ├── table-footer.svelte
+│ │ │ │ │ ├── table-header.svelte
+│ │ │ │ │ ├── table-head.svelte
+│ │ │ │ │ ├── table-row.svelte
+│ │ │ │ │ └── table.svelte
+│ │ │ │ ├── tabs
+│ │ │ │ │ ├── index.ts
+│ │ │ │ │ ├── tabs-content.svelte
+│ │ │ │ │ ├── tabs-list.svelte
+│ │ │ │ │ ├── tabs.svelte
+│ │ │ │ │ └── tabs-trigger.svelte
+│ │ │ │ ├── textarea
+│ │ │ │ │ ├── index.ts
+│ │ │ │ │ └── textarea.svelte
+│ │ │ │ └── tooltip
+│ │ │ │ ├── index.ts
+│ │ │ │ ├── tooltip-content.svelte
+│ │ │ │ └── tooltip-trigger.svelte
+│ │ │ ├── hooks
+│ │ │ │ └── is-mobile.svelte.ts
+│ │ │ ├── paraglide
+│ │ │ │ ├── messages
+│ │ │ │ │ ├── en.js
+│ │ │ │ │ ├── es.js
+│ │ │ │ │ └── _index.js
+│ │ │ │ ├── messages.js
+│ │ │ │ ├── registry.js
+│ │ │ │ ├── runtime.js
+│ │ │ │ └── server.js
+│ │ │ ├── server
+│ │ │ │ └── api.ts
+│ │ │ ├── sso.ts
+│ │ │ ├── stores
+│ │ │ │ └── company.svelte.ts
+│ │ │ └── utils.ts
+│ │ └── routes
+│ │ ├── api
+│ │ │ └── company
+│ │ │ └── my-companies
+│ │ │ └── +server.ts
+│ │ ├── auth
+│ │ │ └── callback
+│ │ │ ├── +page.server.ts
+│ │ │ └── +page.svelte
+│ │ ├── dashboard
+│ │ │ ├── classes
+│ │ │ │ └── +page.svelte
+│ │ │ ├── clients_and_providers
+│ │ │ │ ├── +page.server.ts
+│ │ │ │ └── +page.svelte
+│ │ │ ├── customs_brokers
+│ │ │ │ ├── +page.server.ts
+│ │ │ │ └── +page.svelte
+│ │ │ ├── general_catalogs
+│ │ │ │ ├── classification_concepts
+│ │ │ │ │ ├── +page.server.ts
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ ├── company_information
+│ │ │ │ │ ├── +page.server.ts
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ ├── concepts
+│ │ │ │ │ ├── +page.server.ts
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ ├── customs_broker_concepts
+│ │ │ │ │ ├── +page.server.ts
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ ├── doda
+│ │ │ │ │ ├── +page.server.ts
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ ├── electronic_notices
+│ │ │ │ │ ├── +page.server.ts
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ ├── equivalencies
+│ │ │ │ │ ├── +page.server.ts
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ ├── error_catalogs
+│ │ │ │ │ ├── +page.server.ts
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ ├── exchange-rate
+│ │ │ │ │ ├── +page.server.ts
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ ├── identifiers
+│ │ │ │ │ ├── +page.server.ts
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ ├── inpc
+│ │ │ │ │ ├── +page.server.ts
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ ├── legends
+│ │ │ │ │ ├── +page.server.ts
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ ├── locations
+│ │ │ │ │ ├── +page.server.ts
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ ├── multi_currency_types
+│ │ │ │ │ ├── +page.server.ts
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ ├── packages
+│ │ │ │ │ ├── +page.server.ts
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ ├── +page.svelte
+│ │ │ │ ├── ports
+│ │ │ │ │ ├── +page.server.ts
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ ├── prevalidators
+│ │ │ │ │ ├── +page.server.ts
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ ├── seal
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ ├── signatures
+│ │ │ │ │ ├── +page.server.ts
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ ├── unit_conversions
+│ │ │ │ │ ├── +page.server.ts
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ └── units_of_measure
+│ │ │ │ ├── ace
+│ │ │ │ │ ├── +page.server.ts
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ ├── american
+│ │ │ │ │ ├── +page.server.ts
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ ├── customs
+│ │ │ │ │ ├── +page.server.ts
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ ├── general
+│ │ │ │ │ ├── +page.server.ts
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ └── oma
+│ │ │ │ ├── +page.server.ts
+│ │ │ │ └── +page.svelte
+│ │ │ ├── invoices
+│ │ │ │ ├── exportacion
+│ │ │ │ │ ├── exportacion
+│ │ │ │ │ │ ├── +page.server.ts
+│ │ │ │ │ │ └── +page.svelte
+│ │ │ │ │ └── reparacion
+│ │ │ │ │ ├── +page.server.ts
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ └── importacion
+│ │ │ │ ├── cambio_regimen
+│ │ │ │ │ ├── new
+│ │ │ │ │ │ └── +page.svelte
+│ │ │ │ │ ├── +page.server.ts
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ ├── compras_mexicanas
+│ │ │ │ │ ├── new
+│ │ │ │ │ │ └── +page.svelte
+│ │ │ │ │ ├── +page.server.ts
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ ├── definitiva
+│ │ │ │ │ ├── new
+│ │ │ │ │ │ └── +page.svelte
+│ │ │ │ │ ├── +page.server.ts
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ └── temporal
+│ │ │ │ ├── new
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ ├── +page.server.ts
+│ │ │ │ └── +page.svelte
+│ │ │ ├── +layout.server.ts
+│ │ │ ├── +layout.svelte
+│ │ │ ├── +layout.ts
+│ │ │ ├── +page.svelte
+│ │ │ ├── pedimentos
+│ │ │ │ ├── edit
+│ │ │ │ │ └── [id]
+│ │ │ │ │ ├── +page.server.ts
+│ │ │ │ │ └── +page.svelte
+│ │ │ │ ├── +page.server.ts
+│ │ │ │ └── +page.svelte
+│ │ │ └── reference_data
+│ │ │ ├── code_pedimento_regimens
+│ │ │ │ ├── +page.server.ts
+│ │ │ │ └── +page.svelte
+│ │ │ ├── containers
+│ │ │ │ ├── +page.server.ts
+│ │ │ │ └── +page.svelte
+│ │ │ ├── countries
+│ │ │ │ ├── +page.server.ts
+│ │ │ │ └── +page.svelte
+│ │ │ ├── currency_types
+│ │ │ │ ├── +page.server.ts
+│ │ │ │ └── +page.svelte
+│ │ │ ├── customs_sections
+│ │ │ │ ├── +page.server.ts
+│ │ │ │ └── +page.svelte
+│ │ │ ├── customs_warehouses
+│ │ │ │ ├── +page.server.ts
+│ │ │ │ └── +page.svelte
+│ │ │ ├── incoterms
+│ │ │ │ ├── +page.server.ts
+│ │ │ │ └── +page.svelte
+│ │ │ ├── invoice_types
+│ │ │ │ ├── +page.server.ts
+│ │ │ │ └── +page.svelte
+│ │ │ ├── material_types
+│ │ │ │ ├── +page.server.ts
+│ │ │ │ └── +page.svelte
+│ │ │ ├── +page.svelte
+│ │ │ ├── payment_methods
+│ │ │ │ ├── +page.server.ts
+│ │ │ │ └── +page.svelte
+│ │ │ ├── pedimento_codes
+│ │ │ │ ├── +page.server.ts
+│ │ │ │ └── +page.svelte
+│ │ │ ├── pedimento_regimens
+│ │ │ │ ├── +page.server.ts
+│ │ │ │ └── +page.svelte
+│ │ │ ├── sectors
+│ │ │ │ ├── +page.server.ts
+│ │ │ │ └── +page.svelte
+│ │ │ ├── states
+│ │ │ │ ├── +page.server.ts
+│ │ │ │ └── +page.svelte
+│ │ │ ├── transport_modes
+│ │ │ │ ├── +page.server.ts
+│ │ │ │ └── +page.svelte
+│ │ │ ├── transport_types
+│ │ │ │ ├── +page.server.ts
+│ │ │ │ └── +page.svelte
+│ │ │ └── valuation_methods
+│ │ │ ├── +page.server.ts
+│ │ │ └── +page.svelte
+│ │ ├── demo
+│ │ │ ├── +page.svelte
+│ │ │ └── paraglide
+│ │ │ └── +page.svelte
+│ │ ├── +layout.svelte
+│ │ ├── login
+│ │ │ ├── +page.server.ts
+│ │ │ └── +page.svelte
+│ │ ├── logout
+│ │ │ └── +server.ts
+│ │ ├── +page.server.ts
+│ │ ├── +page.svelte
+│ │ ├── page.svelte.spec.ts
+│ │ └── register
+│ │ └── +page.svelte
+│ ├── svelte.config.js
+│ ├── tsconfig.json
+│ ├── vite.config.ts
+│ └── vitest-setup-client.ts
+├── pnpm-lock.yaml
+├── README.md
+├── scripts
+│ ├── backend-entrypoint.sh
+│ ├── frontend-entrypoint.sh
+│ ├── health-check.sh
+│ ├── init_first_time.sh
+│ ├── keycloak-entrypoint.sh
+│ ├── postgres-app-entrypoint.sh
+│ └── postgres-keycloak-entrypoint.sh
+└── start.sh
+
+245 directories, 943 files
diff --git a/frontend/src/lib/components/dashboard/invoices/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/invoices/create-edit-dialog.svelte
index 94004867..4e0eef19 100644
--- a/frontend/src/lib/components/dashboard/invoices/create-edit-dialog.svelte
+++ b/frontend/src/lib/components/dashboard/invoices/create-edit-dialog.svelte
@@ -315,7 +315,7 @@
-
+
{isEditing ? "Editar Factura" : "Nueva Factura"}
diff --git a/frontend/src/routes/dashboard/invoices/exportacion/exportacion/+page.server.ts b/frontend/src/routes/dashboard/invoices/exportacion/exportacion/+page.server.ts
index 54f3eb0b..f72744df 100644
--- a/frontend/src/routes/dashboard/invoices/exportacion/exportacion/+page.server.ts
+++ b/frontend/src/routes/dashboard/invoices/exportacion/exportacion/+page.server.ts
@@ -42,9 +42,9 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
};
}
- // Obtener filtro de tipo de operación
- const operationType = 'exp'
- const invoiceType = 'exp'
+ // Obtener filtros de la URL
+ const operationType = 'exp';
+ const invoiceType = url.searchParams.get('invoice_type') || '';
// Construir parámetros de consulta
const params = new URLSearchParams({
@@ -53,16 +53,15 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
page_size: '50'
});
- // Agregar filtro de tipo si existe y no es 'all'
+ // Agregar filtro de tipo de operación si existe y no es 'all'
if (operationType && operationType !== 'all') {
params.append('operation_type', operationType);
}
-
- // Agregar filtro de invoice_type si existe
- if (invoiceType) {
+ // Agregar filtro de invoice_type si existe y no está vacío
+ if (invoiceType && invoiceType !== '') {
params.append('invoice_type', invoiceType);
}
-
+
// Usar authenticatedFetch para manejar automáticamente el refresh de tokens
const response = await authenticatedFetch(
`v1/a76/invoices?${params.toString()}`,
@@ -81,7 +80,7 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
error: 'Error al cargar facturas',
companies: parentData.companies || [],
currentCompanyId: companyId,
- operationType: operationType || 'all',
+ operationType: operationType || 'exp',
invoiceType: invoiceType || null
};
}
@@ -95,7 +94,7 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
page_size: data.page_size || 50,
companies: parentData.companies || [],
currentCompanyId: companyId,
- operationType: operationType || 'all',
+ operationType: operationType || 'exp',
invoiceType: invoiceType || null
};
} catch (error) {
@@ -107,7 +106,7 @@ export const load: PageServerLoad = async ({ fetch, cookies, url, parent }) => {
page_size: 50,
error: 'Error al cargar facturas',
companies: parentData.companies || [],
- operationType: 'all',
+ operationType: 'exp',
invoiceType: null
};
}
diff --git a/frontend/src/routes/dashboard/invoices/exportacion/exportacion/+page.svelte b/frontend/src/routes/dashboard/invoices/exportacion/exportacion/+page.svelte
index 0a837d10..3697bf25 100644
--- a/frontend/src/routes/dashboard/invoices/exportacion/exportacion/+page.svelte
+++ b/frontend/src/routes/dashboard/invoices/exportacion/exportacion/+page.svelte
@@ -267,7 +267,7 @@
Gestiona las facturas de importación y exportación
-