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"} + + + +
+ + + General + Cumplimiento + Financieros + + + + +
+
+ + { + if (v) formData.operation_type = v as "imp" | "exp"; + }} + > + + {formData.operation_type === 'imp' ? 'Importación' : formData.operation_type === 'exp' ? 'Exportación' : 'Seleccionar tipo'} + + + Importación + Exportación + + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+
+ + + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+ + {#if error} +
+ {error} +
+ {/if} + + + + + +
+
+
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} +
+
+ Cargando más... +
+ {: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 @@ -svelte-logo \ 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} + +
+ + + + + + General + Cumplimiento + Financieros + + + +
+ +
+ +
Importación
+
+ +
+ +
CAMBIO REGIMEN (CR)
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ +
+ +
+ + + + + +
+
+
\ 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} + +
+ + + + + + General + Cumplimiento + Financieros + + + +
+ +
+ +
Importación
+
+ +
+ +
COMPRAS MEXICANAS (MEX)
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ +
+ +
+ + + + + +
+
+
\ 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} + +
+ + + + + + General + Cumplimiento + Financieros + + + +
+ +
+ +
Importación
+
+ +
+ +
Definitiva (DEF)
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ +
+ +
+ + + + + +
+
+
\ 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} + +
+ + + + + + General + Cumplimiento + Financieros + + + +
+ +
+ +
Importación
+
+ +
+ +
Temporal (TEM)
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ +
+ +
+ + + + + +
+
+
\ 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

- diff --git a/frontend/src/routes/dashboard/invoices/exportacion/exportacion/new/+page.svelte b/frontend/src/routes/dashboard/invoices/exportacion/exportacion/new/+page.svelte new file mode 100644 index 00000000..1feb1929 --- /dev/null +++ b/frontend/src/routes/dashboard/invoices/exportacion/exportacion/new/+page.svelte @@ -0,0 +1,401 @@ + + +
+ +
+ +
+

Nueva Factura Exportación

+

Ingresa los datos para registrar la operación.

+
+
+ + {#if error} +
+ ⚠️ {error} +
+ {/if} + +
+ + + + + + +
+ +
+ +
Exportación
+
+ +
+ + formData.invoice_type = v} + > + + {#if formData.invoice_type} + {INVOICE_TYPES_OPTIONS.find(t => t.value === formData.invoice_type)?.label} + {:else} + Seleccionar tipo + {/if} + + + + {#each INVOICE_TYPES_OPTIONS as type} + + {type.label} + + {/each} + + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + + General + Cumplimiento + Financieros + + +
+ +
+ +
+ +
+
+ + +
+
+ +
+
\ No newline at end of file diff --git a/frontend/src/routes/dashboard/invoices/exportacion/reparacion/+page.svelte b/frontend/src/routes/dashboard/invoices/exportacion/reparacion/+page.svelte index 0a837d10..5921b9fa 100644 --- a/frontend/src/routes/dashboard/invoices/exportacion/reparacion/+page.svelte +++ b/frontend/src/routes/dashboard/invoices/exportacion/reparacion/+page.svelte @@ -231,8 +231,7 @@ } function handleCreateClick() { - selectedInvoice = null; - showCreateDialog = true; + goto('/dashboard/invoices/importacion/reparacion/new'); } function handleView(invoice: Invoice) { @@ -267,7 +266,7 @@ Gestiona las facturas de importación y exportación

- diff --git a/frontend/src/routes/dashboard/invoices/exportacion/reparacion/new/+page.svelte b/frontend/src/routes/dashboard/invoices/exportacion/reparacion/new/+page.svelte new file mode 100644 index 00000000..5ce4885e --- /dev/null +++ b/frontend/src/routes/dashboard/invoices/exportacion/reparacion/new/+page.svelte @@ -0,0 +1,325 @@ + + +
+ +
+ +
+

Nueva Factura Reparacion

+

Ingresa los datos para registrar la exportación.

+
+
+ + {#if error} +
+ {error} +
+ {/if} + +
+ + + + + + +
+ +
+ +
Importación
+
+ +
+ +
REPARACION (REPAR)
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+
+ + +
+ +
+ + +
+
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + + General + Cumplimiento + Financieros + + +
+ +
+ +
+ +
+
+ + +
+
+ +
+
\ No newline at end of file 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 index 76f88411..75178fdd 100644 --- a/frontend/src/routes/dashboard/invoices/importacion/cambio_regimen/new/+page.svelte +++ b/frontend/src/routes/dashboard/invoices/importacion/cambio_regimen/new/+page.svelte @@ -3,9 +3,8 @@ import { Button } from "$lib/components/ui/button"; import { Input } from "$lib/components/ui/input"; import { Label } from "$lib/components/ui/label"; - import * as Select from "$lib/components/ui/select"; import * as Tabs from "$lib/components/ui/tabs"; - import * as Card from "$lib/components/ui/card"; // Usamos Card para enmarcar + import * as Card from "$lib/components/ui/card"; import { invoicesApi, type CreateInvoiceData } from "$lib/api/dashboard/a76/invoices"; import { companyStore } from "$lib/stores/company.svelte"; import { LoaderCircle, ArrowLeft, Save } from 'lucide-svelte'; @@ -67,8 +66,9 @@ async function handleSubmit(e: Event) { e.preventDefault(); - if (!companyStore.activeCompany) { - error = "No hay compañía seleccionada"; + // VALIDACIÓN DE SEGURIDAD (Por si se refresca la página) + if (!companyStore.activeCompany?.id) { + error = "Error: No se detecta la compañía activa. Por favor, vuelve al dashboard y selecciona una compañía."; return; } @@ -76,7 +76,7 @@ error = null; try { - // Construimos el Payload + // Construimos el Payload (LÓGICA ORIGINAL QUE FUNCIONA) const payload: CreateInvoiceData = { ...formData, // Aseguramos que se envíen los fijos @@ -108,7 +108,7 @@ } -
+
@@ -314,7 +318,8 @@ Guardar Factura {/if} - - +
+
+
\ No newline at end of file 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 index 488312d0..ae160c91 100644 --- a/frontend/src/routes/dashboard/invoices/importacion/compras_mexicanas/new/+page.svelte +++ b/frontend/src/routes/dashboard/invoices/importacion/compras_mexicanas/new/+page.svelte @@ -3,14 +3,13 @@ import { Button } from "$lib/components/ui/button"; import { Input } from "$lib/components/ui/input"; import { Label } from "$lib/components/ui/label"; - import * as Select from "$lib/components/ui/select"; import * as Tabs from "$lib/components/ui/tabs"; - import * as Card from "$lib/components/ui/card"; // Usamos Card para enmarcar + import * as Card from "$lib/components/ui/card"; import { invoicesApi, type CreateInvoiceData } from "$lib/api/dashboard/a76/invoices"; import { companyStore } from "$lib/stores/company.svelte"; import { LoaderCircle, ArrowLeft, Save } from 'lucide-svelte'; - // DATOS FIJOS PARA ESTA CARPETA (COMPRAS MEXICANAS) + // DATOS FIJOS PARA ESTA CARPETA (DEFINITIVA) const FIXED_OP_TYPE = "imp"; const FIXED_INV_TYPE = "MEX"; @@ -67,8 +66,9 @@ async function handleSubmit(e: Event) { e.preventDefault(); - if (!companyStore.activeCompany) { - error = "No hay compañía seleccionada"; + // VALIDACIÓN DE SEGURIDAD (Por si se refresca la página) + if (!companyStore.activeCompany?.id) { + error = "Error: No se detecta la compañía activa. Por favor, vuelve al dashboard y selecciona una compañía."; return; } @@ -76,7 +76,7 @@ error = null; try { - // Construimos el Payload + // Construimos el Payload (LÓGICA ORIGINAL QUE FUNCIONA) const payload: CreateInvoiceData = { ...formData, // Aseguramos que se envíen los fijos @@ -108,21 +108,21 @@ } -
+
-

Nueva Factura

+

Nueva Factura Compras Mexicanas

Ingresa los datos para registrar la importación.

{#if error}
- ⚠️ {error} + {error}
{/if} @@ -131,14 +131,9 @@ - - General - Cumplimiento - Financieros - -
+
@@ -195,7 +190,7 @@ -
+
@@ -239,7 +234,7 @@ -
+
@@ -297,12 +292,21 @@
+ + General + Cumplimiento + Financieros + + - - - - +
+
+
\ No newline at end of file diff --git a/frontend/src/routes/dashboard/invoices/importacion/definitiva/new/+page.svelte b/frontend/src/routes/dashboard/invoices/importacion/definitiva/new/+page.svelte index 57e4fc64..e674b384 100644 --- a/frontend/src/routes/dashboard/invoices/importacion/definitiva/new/+page.svelte +++ b/frontend/src/routes/dashboard/invoices/importacion/definitiva/new/+page.svelte @@ -3,9 +3,8 @@ import { Button } from "$lib/components/ui/button"; import { Input } from "$lib/components/ui/input"; import { Label } from "$lib/components/ui/label"; - import * as Select from "$lib/components/ui/select"; import * as Tabs from "$lib/components/ui/tabs"; - import * as Card from "$lib/components/ui/card"; // Usamos Card para enmarcar + import * as Card from "$lib/components/ui/card"; import { invoicesApi, type CreateInvoiceData } from "$lib/api/dashboard/a76/invoices"; import { companyStore } from "$lib/stores/company.svelte"; import { LoaderCircle, ArrowLeft, Save } from 'lucide-svelte'; @@ -67,8 +66,9 @@ async function handleSubmit(e: Event) { e.preventDefault(); - if (!companyStore.activeCompany) { - error = "No hay compañía seleccionada"; + // VALIDACIÓN DE SEGURIDAD (Por si se refresca la página) + if (!companyStore.activeCompany?.id) { + error = "Error: No se detecta la compañía activa. Por favor, vuelve al dashboard y selecciona una compañía."; return; } @@ -76,7 +76,7 @@ error = null; try { - // Construimos el Payload + // Construimos el Payload (LÓGICA ORIGINAL QUE FUNCIONA) const payload: CreateInvoiceData = { ...formData, // Aseguramos que se envíen los fijos @@ -108,7 +108,7 @@ } -
+
@@ -314,7 +318,8 @@ Guardar Factura {/if} - - +
+
+
\ No newline at end of file diff --git a/frontend/src/routes/dashboard/invoices/importacion/temporal/new/+page.svelte b/frontend/src/routes/dashboard/invoices/importacion/temporal/new/+page.svelte index fae289e8..873515b6 100644 --- a/frontend/src/routes/dashboard/invoices/importacion/temporal/new/+page.svelte +++ b/frontend/src/routes/dashboard/invoices/importacion/temporal/new/+page.svelte @@ -3,14 +3,13 @@ import { Button } from "$lib/components/ui/button"; import { Input } from "$lib/components/ui/input"; import { Label } from "$lib/components/ui/label"; - import * as Select from "$lib/components/ui/select"; import * as Tabs from "$lib/components/ui/tabs"; - import * as Card from "$lib/components/ui/card"; // Usamos Card para enmarcar + import * as Card from "$lib/components/ui/card"; import { invoicesApi, type CreateInvoiceData } from "$lib/api/dashboard/a76/invoices"; import { companyStore } from "$lib/stores/company.svelte"; import { LoaderCircle, ArrowLeft, Save } from 'lucide-svelte'; - // DATOS FIJOS PARA ESTA CARPETA (Temporal Importación) + // DATOS FIJOS PARA ESTA CARPETA (TEMINITIVA) const FIXED_OP_TYPE = "imp"; const FIXED_INV_TYPE = "TEM"; @@ -67,8 +66,9 @@ async function handleSubmit(e: Event) { e.preventDefault(); - if (!companyStore.activeCompany) { - error = "No hay compañía seleccionada"; + // VALIDACIÓN DE SEGURIDAD (Por si se refresca la página) + if (!companyStore.activeCompany?.id) { + error = "Error: No se detecta la compañía activa. Por favor, vuelve al dashboard y selecciona una compañía."; return; } @@ -76,7 +76,7 @@ error = null; try { - // Construimos el Payload + // Construimos el Payload (LÓGICA ORIGINAL QUE FUNCIONA) const payload: CreateInvoiceData = { ...formData, // Aseguramos que se envíen los fijos @@ -108,7 +108,7 @@ } -
+
@@ -314,7 +318,8 @@ Guardar Factura {/if} - - +
+
+
\ No newline at end of file From 827c5319bb61ee0dcd1b9f95e18f6c87f2bca4db Mon Sep 17 00:00:00 2001 From: "Kevin A. Rosales Marquez" Date: Tue, 16 Dec 2025 15:40:17 -0600 Subject: [PATCH 09/15] Se agegaron rutas para clientes/proveedores asi como a las agencias aduanales --- .../clients_and_providers/+page.svelte | 2 +- .../clients_and_providers/new/+page.svelte | 438 ++++++++++++++++++ .../dashboard/customs_brokers/+page.svelte | 2 +- .../customs_brokers/new/+page.svelte | 395 ++++++++++++++++ 4 files changed, 835 insertions(+), 2 deletions(-) create mode 100644 frontend/src/routes/dashboard/clients_and_providers/new/+page.svelte create mode 100644 frontend/src/routes/dashboard/customs_brokers/new/+page.svelte diff --git a/frontend/src/routes/dashboard/clients_and_providers/+page.svelte b/frontend/src/routes/dashboard/clients_and_providers/+page.svelte index 2d25421a..ca02a3f0 100644 --- a/frontend/src/routes/dashboard/clients_and_providers/+page.svelte +++ b/frontend/src/routes/dashboard/clients_and_providers/+page.svelte @@ -195,7 +195,7 @@ Gestiona el catálogo de clientes y proveedores de tu empresa

- diff --git a/frontend/src/routes/dashboard/clients_and_providers/new/+page.svelte b/frontend/src/routes/dashboard/clients_and_providers/new/+page.svelte new file mode 100644 index 00000000..2888f15a --- /dev/null +++ b/frontend/src/routes/dashboard/clients_and_providers/new/+page.svelte @@ -0,0 +1,438 @@ + + +
+ +
+ +
+

Nuevo Cliente/Proveedor

+

+ Completa los datos para crear un nuevo cliente o proveedor +

+
+
+ + + + + Información del Cliente/Proveedor + + Todos los campos marcados con * son obligatorios + + + +
+ {#if error} +
+ {error} +
+ {/if} + + +
+

Información Básica

+ +
+
+ + +
+ +
+ + (formData.client_or_provider = value || "client")}> + + + + + Cliente + Proveedor + Ambos + + +
+
+ +
+ + +
+ +
+
+ + +
+ +
+ + +
+
+
+ + +
+

Información Fiscal

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

Dirección

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

Programas

+ +
+
+ + +
+ +
+ + +
+
+
+
+
+
+
+ + +
+ + + +
+ + +
diff --git a/frontend/src/routes/dashboard/customs_brokers/+page.svelte b/frontend/src/routes/dashboard/customs_brokers/+page.svelte index fe9682fd..f90963cd 100644 --- a/frontend/src/routes/dashboard/customs_brokers/+page.svelte +++ b/frontend/src/routes/dashboard/customs_brokers/+page.svelte @@ -175,7 +175,7 @@ Gestiona el catálogo de agentes aduanales

- diff --git a/frontend/src/routes/dashboard/customs_brokers/new/+page.svelte b/frontend/src/routes/dashboard/customs_brokers/new/+page.svelte new file mode 100644 index 00000000..8777c854 --- /dev/null +++ b/frontend/src/routes/dashboard/customs_brokers/new/+page.svelte @@ -0,0 +1,395 @@ + + +
+ +
+ +
+

Nuevo Agente Aduanal

+

+ Completa los datos para crear un nuevo agente aduanal +

+
+
+ + + + + Información del Agente Aduanal + + Todos los campos marcados con * son obligatorios + + + +
+ {#if error} +
+ {error} +
+ {/if} + + +
+

Información Básica

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

Información de Contacto

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

Dirección

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

Información Fiscal

+ +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+
+ +
+ + + +
+
+
+
+
From 153fca9ad7a7374c21bb9db2c018f7e82dab1a96 Mon Sep 17 00:00:00 2001 From: Kevin_Ramirez Date: Fri, 19 Dec 2025 17:25:12 -0600 Subject: [PATCH 10/15] Se agrego un modal provicional para el fomrulario y creacion de los registros en la base de datos. Aun faltan 3 catalogos para la integracion con la base de datos --- .../classification_concepts/models.py | 1 + .../a76/general_catalogs/company/routes.py | 32 +- .../a76/general_catalogs/company/service.py | 31 +- .../a76/general_catalogs/concepts/dto.py | 4 +- .../a76/general_catalogs/concepts/models.py | 14 + .../customs_broker_concepts/dto.py | 3 +- .../customs_broker_concepts/models.py | 8 +- .../customs_broker_concepts/routes.py | 2 +- .../general_catalogs/equivalencies/models.py | 4 + .../general_catalogs/error_catalogs/models.py | 8 +- .../general_catalogs/exchange_rate/models.py | 3 + .../a76/general_catalogs/identifiers/dto.py | 17 +- .../general_catalogs/identifiers/models.py | 4 + .../modules/a76/general_catalogs/inpc/dto.py | 6 +- .../a76/general_catalogs/inpc/models.py | 3 + .../a76/general_catalogs/inpc/routes.py | 14 +- .../a76/general_catalogs/legends/dto.py | 2 + .../a76/general_catalogs/legends/models.py | 2 + .../a76/general_catalogs/legends/routes.py | 2 +- .../multi_currency_types/dto.py | 4 +- .../multi_currency_types/models.py | 5 + .../a76/general_catalogs/packages/models.py | 2 +- .../a76/general_catalogs/signatures/dto.py | 3 + .../a76/general_catalogs/signatures/models.py | 1 + .../general_catalogs/unit_conversions/dto.py | 18 +- .../unit_conversions/models.py | 3 + .../units_of_measure/models.py | 2 +- .../classification-concepts.ts | 83 +++-- .../dashboard/a76/general_catalogs/company.ts | 164 ++++----- .../a76/general_catalogs/concepts.ts | 118 ++++--- .../customs-broker-concepts.ts | 100 +++--- .../a76/general_catalogs/error-catalogs.ts | 143 ++++++-- .../a76/general_catalogs/identifiers.ts | 34 +- .../dashboard/a76/general_catalogs/inpc.ts | 91 ++--- .../dashboard/a76/general_catalogs/legends.ts | 85 ++--- .../a76/general_catalogs/locations.ts | 99 +++--- .../general_catalogs/multi-currency-types.ts | 90 +++-- .../a76/general_catalogs/packages.ts | 77 +++-- .../a76/general_catalogs/signatures.ts | 93 +++--- .../a76/general_catalogs/unit-conversions.ts | 90 ++--- .../src/lib/api/dashboard/a76/invoices.ts | 2 +- .../company/create-edit-dialog.svelte | 127 ------- .../exchange-rate/create-edit-dialog.svelte | 313 ++++++++++-------- .../classification/create-edit-dialog.svelte | 115 +++++++ .../{ => general_catalogs}/company/columns.ts | 0 .../company/create-edit-dialog.svelte | 296 +++++++++++++++++ .../company/data-table-actions.svelte | 0 .../concepts/create-edit-dialog.svelte | 213 ++++++++++++ .../create-edite-dialoge.svelte | 165 +++++++++ .../create-edite-dialoge.svelte | 141 ++++++++ .../inpc/create-edite-dialoge.svelte | 166 ++++++++++ .../legend/create-edite-dialoge.svelte | 149 +++++++++ .../locations/create-edite-dialog.svelte | 152 +++++++++ .../create-edit-dialog.svelte | 196 +++++++++++ .../signatures/create-edit-dialog.svelte | 163 +++++++++ .../create-edite-dialoge.svelte | 164 +++++++++ .../identifiers/create-edit-dialog.svelte | 267 ++++++++------- .../packages/create-edit-dialog.svelte | 6 +- .../dashboard/packages/data-table.svelte | 169 +++++----- .../classification_concepts/+page.svelte | 6 + .../company_information/+page.svelte | 4 +- .../general_catalogs/concepts/+page.svelte | 6 + .../customs_broker_concepts/+page.svelte | 6 + .../error_catalogs/+page.svelte | 9 + .../exchange-rate/+page.svelte | 14 +- .../general_catalogs/inpc/+page.svelte | 6 + .../general_catalogs/legends/+page.svelte | 6 + .../general_catalogs/locations/+page.svelte | 88 ++--- .../multi_currency_types/+page.svelte | 7 + .../general_catalogs/signatures/+page.svelte | 9 +- .../unit_conversions/+page.svelte | 7 + 71 files changed, 3316 insertions(+), 1121 deletions(-) delete mode 100644 frontend/src/lib/components/dashboard/company/create-edit-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/general_catalogs/classification/create-edit-dialog.svelte rename frontend/src/lib/components/dashboard/{ => general_catalogs}/company/columns.ts (100%) create mode 100644 frontend/src/lib/components/dashboard/general_catalogs/company/create-edit-dialog.svelte rename frontend/src/lib/components/dashboard/{ => general_catalogs}/company/data-table-actions.svelte (100%) create mode 100644 frontend/src/lib/components/dashboard/general_catalogs/concepts/create-edit-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/general_catalogs/customs_broker_concepts/create-edite-dialoge.svelte create mode 100644 frontend/src/lib/components/dashboard/general_catalogs/error_catalogs/create-edite-dialoge.svelte create mode 100644 frontend/src/lib/components/dashboard/general_catalogs/inpc/create-edite-dialoge.svelte create mode 100644 frontend/src/lib/components/dashboard/general_catalogs/legend/create-edite-dialoge.svelte create mode 100644 frontend/src/lib/components/dashboard/general_catalogs/locations/create-edite-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/general_catalogs/multi_currency_types/create-edit-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/general_catalogs/signatures/create-edit-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/general_catalogs/unit-conversion/create-edite-dialoge.svelte diff --git a/backend/api/v1/modules/a76/general_catalogs/classification_concepts/models.py b/backend/api/v1/modules/a76/general_catalogs/classification_concepts/models.py index 9789731e..129d05fc 100644 --- a/backend/api/v1/modules/a76/general_catalogs/classification_concepts/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/classification_concepts/models.py @@ -14,5 +14,6 @@ class ClassificationConcept(Base, TenantScopedMixin, TimestampMixin): id: Mapped[int] = mapped_column( Integer, primary_key=True, autoincrement=True) + classification: Mapped[str] = mapped_column( String(30), nullable=False) # CLASIFICACION diff --git a/backend/api/v1/modules/a76/general_catalogs/company/routes.py b/backend/api/v1/modules/a76/general_catalogs/company/routes.py index 0095bc4d..8a948f6b 100644 --- a/backend/api/v1/modules/a76/general_catalogs/company/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/company/routes.py @@ -2,7 +2,7 @@ Rutas para gestión de empresa """ -from typing import List +from typing import List, Optional from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session @@ -17,7 +17,29 @@ from .service import CompanyService # Main router that includes base CRUD router = APIRouter(prefix="/company") -# Custom endpoints +@router.post( + "", # Se suma al prefix, queda POST /api/v1/a76/company + response_model=CompanyResponseDTO, + status_code=status.HTTP_201_CREATED, + summary="Create a new company", +) +async def create_company( + data: CompanyCreateDTO, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): + + tenant_id = current_user.get("tenant_id") + if not tenant_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Tenant ID not found in user data", + ) + + service = CompanyService(db) + return service.create_company_manually(data, tenant_id=tenant_id) + + @router.get( "/my-companies", response_model=List[CompanyResponseDTO], @@ -166,11 +188,10 @@ async def get_program_info( "prosec": company.prosec, "prosec_authorization": company.prosec_authorization, } - -# Base CRUD routes using TenantCRUDRoutes + base_router = TenantCRUDRoutes( service=CompanyService, - create_schema=CompanyCreateDTO, + create_schema=None, update_schema=CompanyUpdateDTO, response_schema=CompanyResponseDTO, prefix="", @@ -179,4 +200,5 @@ base_router = TenantCRUDRoutes( enable_list=True, enable_filters=True, ).router + router.include_router(base_router) \ No newline at end of file diff --git a/backend/api/v1/modules/a76/general_catalogs/company/service.py b/backend/api/v1/modules/a76/general_catalogs/company/service.py index a42250a5..f3c03827 100644 --- a/backend/api/v1/modules/a76/general_catalogs/company/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/company/service.py @@ -64,6 +64,7 @@ class CompanyService: .first() ) + # ESTE ES EL MÉTODO VIEJO QUE CAUSABA PROBLEMAS (Lo dejamos por si acaso) @staticmethod def create( db: Session, @@ -71,7 +72,7 @@ class CompanyService: tenant_id: int, company_id: int, ) -> Company: - """Create a new company""" + """Create a new company (MÉTODO GENÉRICO - NO USAR PARA CREACIÓN MANUAL)""" try: db_company = Company( **company_data.model_dump(exclude_unset=True), @@ -159,3 +160,31 @@ class CompanyService: .first() is not None ) + + def create_company_manually(self, data: CompanyCreateDTO, tenant_id: int) -> Company: + + try: + # 1. Preparar datos + obj_data = data.model_dump(exclude_unset=True) + + # 2. Crear objeto SQLAlchemy + db_obj = Company(**obj_data, tenant_id=tenant_id) + + # 3. Guardar + self.db.add(db_obj) + self.db.commit() + self.db.refresh(db_obj) + + return db_obj + + except IntegrityError as e: + self.db.rollback() + logger.error(f"IntegrityError creating company manually: {str(e)}") + raise HTTPException( + status_code=400, + detail="Error de integridad: Es posible que esta empresa ya exista.", + ) + except Exception as e: + self.db.rollback() + logger.error(f"Error creating company manually: {str(e)}") + raise HTTPException(status_code=500, detail=f"Error creando empresa: {str(e)}") \ No newline at end of file diff --git a/backend/api/v1/modules/a76/general_catalogs/concepts/dto.py b/backend/api/v1/modules/a76/general_catalogs/concepts/dto.py index e4b391ab..e8c030e0 100644 --- a/backend/api/v1/modules/a76/general_catalogs/concepts/dto.py +++ b/backend/api/v1/modules/a76/general_catalogs/concepts/dto.py @@ -4,6 +4,7 @@ from pydantic import BaseModel, Field, ConfigDict class ConceptBase(BaseModel): code: str = Field(..., max_length=15, description="Concept Code (CLAVE)") + company_id: int = Field(..., description="Company ID") description: Optional[str] = Field( None, max_length=120, description="Description") description_en: Optional[str] = Field( @@ -18,7 +19,6 @@ class ConceptBase(BaseModel): section: Optional[int] = Field(None, description="Section") classification: Optional[str] = Field( None, max_length=30, description="Classification") - company_id: int = Field(..., description="Company ID") class ConceptCreate(ConceptBase): @@ -32,7 +32,7 @@ class ConceptUpdate(BaseModel): detailed_description: Optional[str] = Field(None, max_length=1000) priority: Optional[int] = None priority_ame: Optional[int] = None - first_total: Optional[bool] = None + first_total: Optional[bool] = None type: Optional[str] = Field(None, max_length=9) is_printed: Optional[bool] = None section: Optional[int] = None diff --git a/backend/api/v1/modules/a76/general_catalogs/concepts/models.py b/backend/api/v1/modules/a76/general_catalogs/concepts/models.py index 8c13fc34..d3824d1d 100644 --- a/backend/api/v1/modules/a76/general_catalogs/concepts/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/concepts/models.py @@ -18,25 +18,39 @@ class Concept(Base, TenantScopedMixin, TimestampMixin): id: Mapped[int] = mapped_column( Integer, primary_key=True, autoincrement=True) + + company_id: Mapped[int] = mapped_column( + Integer, nullable=False) # IDEMPRESA + code: Mapped[str] = mapped_column(String(15), nullable=False) # CLAVE + description: Mapped[Optional[str]] = mapped_column( String(120), nullable=True) # DESCRIPCION + description_en: Mapped[Optional[str]] = mapped_column( String(120), nullable=True) # DESCRIPCIONINGLES + detailed_description: Mapped[Optional[str]] = mapped_column( String(1000), nullable=True) # DESCRIPCIONDETALLADA + priority: Mapped[Optional[int]] = mapped_column( Integer, nullable=True) # PRIORIDAD + priority_ame: Mapped[Optional[int]] = mapped_column( Integer, nullable=True) # PRIORIDADAME + first_total: Mapped[Optional[bool]] = mapped_column( Boolean, nullable=True) # PRIMERTOTAL + type: Mapped[Optional[str]] = mapped_column( String(9), nullable=True) # TIPO + is_printed: Mapped[Optional[bool]] = mapped_column( Boolean, nullable=True) # SEIMPRIME + section: Mapped[Optional[int]] = mapped_column( Integer, nullable=True) # SECCION + classification: Mapped[Optional[str]] = mapped_column(String(30), ForeignKey( "a76.classification_concepts.classification"), nullable=True) # CLASIFICACION diff --git a/backend/api/v1/modules/a76/general_catalogs/customs_broker_concepts/dto.py b/backend/api/v1/modules/a76/general_catalogs/customs_broker_concepts/dto.py index 470de77a..30416dea 100644 --- a/backend/api/v1/modules/a76/general_catalogs/customs_broker_concepts/dto.py +++ b/backend/api/v1/modules/a76/general_catalogs/customs_broker_concepts/dto.py @@ -4,8 +4,7 @@ from pydantic import BaseModel, Field, ConfigDict class CustomsBrokerConceptBase(BaseModel): - broker_key: str = Field(..., max_length=5, - description="Customs Broker Key (CLAVEAA)") + broker_key: str = Field(..., max_length=5, description="Customs Broker Key (CLAVEAA)") concept: str = Field(..., max_length=15, description="Concept") amount: Optional[Decimal] = Field(None, description="Amount") priority: Optional[int] = Field(None, description="Priority") diff --git a/backend/api/v1/modules/a76/general_catalogs/customs_broker_concepts/models.py b/backend/api/v1/modules/a76/general_catalogs/customs_broker_concepts/models.py index f55c97c4..301e8d48 100644 --- a/backend/api/v1/modules/a76/general_catalogs/customs_broker_concepts/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/customs_broker_concepts/models.py @@ -9,17 +9,23 @@ from core.database import Base class CustomsBrokerConcept(Base, TenantScopedMixin, TimestampMixin): __tablename__ = "customs_broker_concepts" __table_args__ = ( - UniqueConstraint("broker_key", "concept", name="uq_broker_concept"), + UniqueConstraint("broker_key", "concept", "company_id", name="uq_broker_concept"), {"schema": "a76"} ) id: Mapped[int] = mapped_column( Integer, primary_key=True, autoincrement=True) + + company_id: Mapped[int] = mapped_column(Integer, nullable=False) + broker_key: Mapped[str] = mapped_column( String(5), nullable=False) # CLAVEAA + concept: Mapped[str] = mapped_column( String(15), nullable=False) # CONCEPTO + amount: Mapped[Optional[Decimal]] = mapped_column( Numeric(11, 2), nullable=True) # IMPORTE + priority: Mapped[Optional[int]] = mapped_column( Integer, nullable=True) # PRIORIDAD diff --git a/backend/api/v1/modules/a76/general_catalogs/customs_broker_concepts/routes.py b/backend/api/v1/modules/a76/general_catalogs/customs_broker_concepts/routes.py index eeb5bfab..c8ec83ad 100644 --- a/backend/api/v1/modules/a76/general_catalogs/customs_broker_concepts/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/customs_broker_concepts/routes.py @@ -7,7 +7,7 @@ router = TenantCRUDRoutes( create_schema=CustomsBrokerConceptCreate, update_schema=CustomsBrokerConceptUpdate, response_schema=CustomsBrokerConceptResponse, - prefix="/customs-broker-concepts", + prefix="/customs-broker-concepts", tags=["a76.general_catalogs.customs_broker_concepts"], resource_name="Customs Broker Concept", enable_list=True, diff --git a/backend/api/v1/modules/a76/general_catalogs/equivalencies/models.py b/backend/api/v1/modules/a76/general_catalogs/equivalencies/models.py index 7bcc724a..1d4b3d7c 100644 --- a/backend/api/v1/modules/a76/general_catalogs/equivalencies/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/equivalencies/models.py @@ -39,11 +39,15 @@ class EquivalencyItem(Base, TenantScopedMixin, TimestampMixin): id: Mapped[int] = mapped_column( Integer, primary_key=True, autoincrement=True) + equivalency_id: Mapped[int] = mapped_column( Integer, ForeignKey("a76.equivalencies.id"), nullable=False) + original_field: Mapped[str] = mapped_column( String(100), nullable=False) # Relation to Unit of Measure + external_field: Mapped[str] = mapped_column(String(100), nullable=False) equivalency: Mapped["Equivalency"] = relationship(back_populates="items") + unit_of_measure: Mapped["UnitOfMeasure"] = relationship() diff --git a/backend/api/v1/modules/a76/general_catalogs/error_catalogs/models.py b/backend/api/v1/modules/a76/general_catalogs/error_catalogs/models.py index af8d053d..147770c3 100644 --- a/backend/api/v1/modules/a76/general_catalogs/error_catalogs/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/error_catalogs/models.py @@ -24,7 +24,8 @@ class ErrorClassification(Base, TenantScopedMixin, TimestampMixin): # Primary key id: Mapped[int] = mapped_column( - Integer, primary_key=True, autoincrement=True) + Integer, primary_key=True, autoincrement=True + ) # Classification code (unique) code: Mapped[str] = mapped_column(String(100), nullable=False, unique=True) @@ -60,7 +61,8 @@ class ErrorCatalog(Base, TenantScopedMixin, TimestampMixin): # Primary key id: Mapped[int] = mapped_column( - Integer, primary_key=True, autoincrement=True) + Integer, primary_key=True, autoincrement=True + ) # Error code (unique) code: Mapped[str] = mapped_column(String(15), nullable=False, unique=True) @@ -77,4 +79,4 @@ class ErrorCatalog(Base, TenantScopedMixin, TimestampMixin): ) def __repr__(self): - return f"" + return f"" \ No newline at end of file diff --git a/backend/api/v1/modules/a76/general_catalogs/exchange_rate/models.py b/backend/api/v1/modules/a76/general_catalogs/exchange_rate/models.py index 5e9d95b4..92f8e4f7 100644 --- a/backend/api/v1/modules/a76/general_catalogs/exchange_rate/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/exchange_rate/models.py @@ -30,6 +30,9 @@ class ExchangeRate(Base, TenantScopedMixin, TimestampMixin): id: Mapped[int] = mapped_column(Integer, primary_key=True) date: Mapped[datetime] = mapped_column(DateTime) + value: Mapped[Optional[Decimal]] = mapped_column(DECIMAL(13, 6)) + local_currency: Mapped[Optional[str]] = mapped_column(String(7)) + foreign_currency: Mapped[Optional[str]] = mapped_column(String(7)) diff --git a/backend/api/v1/modules/a76/general_catalogs/identifiers/dto.py b/backend/api/v1/modules/a76/general_catalogs/identifiers/dto.py index 718da777..9432a023 100644 --- a/backend/api/v1/modules/a76/general_catalogs/identifiers/dto.py +++ b/backend/api/v1/modules/a76/general_catalogs/identifiers/dto.py @@ -1,9 +1,6 @@ from typing import Optional from pydantic import BaseModel, Field, ConfigDict -# Identifier DTOs - - class IdentifierBase(BaseModel): code: str = Field(..., max_length=2, description="Identifier Code (CLAVE)") description: Optional[str] = Field( @@ -11,28 +8,23 @@ class IdentifierBase(BaseModel): level: Optional[str] = Field(None, max_length=1, description="Level") complement: Optional[str] = Field( None, max_length=5000, description="Complement") - company_id: int = Field(..., description="Company ID") - class IdentifierCreate(IdentifierBase): pass - class IdentifierUpdate(BaseModel): code: Optional[str] = Field(None, max_length=2) description: Optional[str] = Field(None, max_length=1000) level: Optional[str] = Field(None, max_length=1) complement: Optional[str] = Field(None, max_length=5000) - class IdentifierResponse(IdentifierBase): id: int tenant_id: int + company_id: int model_config = ConfigDict(from_attributes=True) -# Identifier Detail DTOs - class IdentifierDetailBase(BaseModel): invoice_consecutive: Optional[int] = Field( @@ -47,13 +39,10 @@ class IdentifierDetailBase(BaseModel): None, max_length=51, description="Complement 2") complement3: Optional[str] = Field( None, max_length=50, description="Complement 3") - company_id: int = Field(..., description="Company ID") - class IdentifierDetailCreate(IdentifierDetailBase): pass - class IdentifierDetailUpdate(BaseModel): invoice_consecutive: Optional[int] = None part_line: Optional[int] = None @@ -63,9 +52,9 @@ class IdentifierDetailUpdate(BaseModel): complement2: Optional[str] = Field(None, max_length=51) complement3: Optional[str] = Field(None, max_length=50) - class IdentifierDetailResponse(IdentifierDetailBase): id: int tenant_id: int + company_id: int - model_config = ConfigDict(from_attributes=True) + model_config = ConfigDict(from_attributes=True) \ No newline at end of file diff --git a/backend/api/v1/modules/a76/general_catalogs/identifiers/models.py b/backend/api/v1/modules/a76/general_catalogs/identifiers/models.py index 27174064..daabe759 100644 --- a/backend/api/v1/modules/a76/general_catalogs/identifiers/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/identifiers/models.py @@ -14,11 +14,15 @@ class Identifier(Base, TenantScopedMixin, TimestampMixin): id: Mapped[int] = mapped_column( Integer, primary_key=True, autoincrement=True) + code: Mapped[str] = mapped_column(String(2), nullable=False) # CLAVE + description: Mapped[Optional[str]] = mapped_column( String(1000), nullable=True) # DESCRIPCION + level: Mapped[Optional[str]] = mapped_column( String(1), nullable=True) # NIVEL + complement: Mapped[Optional[str]] = mapped_column( String(5000), nullable=True) # COMPLEMENTO diff --git a/backend/api/v1/modules/a76/general_catalogs/inpc/dto.py b/backend/api/v1/modules/a76/general_catalogs/inpc/dto.py index 7718a927..72afc5c6 100644 --- a/backend/api/v1/modules/a76/general_catalogs/inpc/dto.py +++ b/backend/api/v1/modules/a76/general_catalogs/inpc/dto.py @@ -2,7 +2,6 @@ from typing import Optional from decimal import Decimal from pydantic import BaseModel, Field, ConfigDict - class INPCBase(BaseModel): year: str = Field(..., max_length=4, description="Year (YYYY)") month: str = Field(..., max_length=2, description="Month (MM)") @@ -12,7 +11,6 @@ class INPCBase(BaseModel): class INPCCreate(INPCBase): pass - class INPCUpdate(BaseModel): year: Optional[str] = Field(None, max_length=4) month: Optional[str] = Field(None, max_length=2) @@ -21,5 +19,7 @@ class INPCUpdate(BaseModel): class INPCResponse(INPCBase): id: int + tenant_id: int + company_id: int - model_config = ConfigDict(from_attributes=True) + model_config = ConfigDict(from_attributes=True) \ No newline at end of file diff --git a/backend/api/v1/modules/a76/general_catalogs/inpc/models.py b/backend/api/v1/modules/a76/general_catalogs/inpc/models.py index 7a41d26b..65e79049 100644 --- a/backend/api/v1/modules/a76/general_catalogs/inpc/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/inpc/models.py @@ -16,7 +16,10 @@ class INPC(Base, TenantScopedMixin, TimestampMixin): id: Mapped[int] = mapped_column( Integer, primary_key=True, autoincrement=True) + year: Mapped[str] = mapped_column(String(4), nullable=False) # ANIO + month: Mapped[str] = mapped_column(String(2), nullable=False) # MES + value: Mapped[Optional[Decimal]] = mapped_column( Numeric(19, 8), nullable=True) # VALOR diff --git a/backend/api/v1/modules/a76/general_catalogs/inpc/routes.py b/backend/api/v1/modules/a76/general_catalogs/inpc/routes.py index dd2b6cdb..a91b276d 100644 --- a/backend/api/v1/modules/a76/general_catalogs/inpc/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/inpc/routes.py @@ -1,20 +1,16 @@ -from fastapi import APIRouter from api.v1.common.tenant_crud_routes import TenantCRUDRoutes from .models import INPC from .dto import INPCCreate, INPCResponse, INPCUpdate from .service import INPCService -router = APIRouter(prefix="/inpc", tags=["a76.general_catalogs.inpc"]) - -inpc_crud = TenantCRUDRoutes( +# Usamos TenantCRUDRoutes directamente +router = TenantCRUDRoutes( service=INPCService, create_schema=INPCCreate, update_schema=INPCUpdate, response_schema=INPCResponse, - prefix="", - tags=["INPC"], + prefix="/inpc", + tags=["a76.general_catalogs.inpc"], resource_name="INPC", enable_list=True, -) - -router.include_router(inpc_crud.router) +).router \ No newline at end of file diff --git a/backend/api/v1/modules/a76/general_catalogs/legends/dto.py b/backend/api/v1/modules/a76/general_catalogs/legends/dto.py index c3900566..2f3b9570 100644 --- a/backend/api/v1/modules/a76/general_catalogs/legends/dto.py +++ b/backend/api/v1/modules/a76/general_catalogs/legends/dto.py @@ -21,3 +21,5 @@ class LegendResponse(LegendBase): id: int model_config = ConfigDict(from_attributes=True) + tenant_id : int + company_id : int diff --git a/backend/api/v1/modules/a76/general_catalogs/legends/models.py b/backend/api/v1/modules/a76/general_catalogs/legends/models.py index eaea60a1..f6ea4591 100644 --- a/backend/api/v1/modules/a76/general_catalogs/legends/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/legends/models.py @@ -15,6 +15,8 @@ class Legend(Base, TenantScopedMixin, TimestampMixin): id: Mapped[int] = mapped_column( Integer, primary_key=True, autoincrement=True) + code: Mapped[int] = mapped_column(Integer, nullable=False) # CLAVELEY + description: Mapped[Optional[str]] = mapped_column( String(2000), nullable=True) # DESCLEYENDA diff --git a/backend/api/v1/modules/a76/general_catalogs/legends/routes.py b/backend/api/v1/modules/a76/general_catalogs/legends/routes.py index e053c4d5..92f4a64b 100644 --- a/backend/api/v1/modules/a76/general_catalogs/legends/routes.py +++ b/backend/api/v1/modules/a76/general_catalogs/legends/routes.py @@ -11,7 +11,7 @@ legend_crud = TenantCRUDRoutes( create_schema=LegendCreate, update_schema=LegendUpdate, response_schema=LegendResponse, - prefix="", + prefix="/legends", tags=["Legends"], resource_name="Legend", enable_list=True, diff --git a/backend/api/v1/modules/a76/general_catalogs/multi_currency_types/dto.py b/backend/api/v1/modules/a76/general_catalogs/multi_currency_types/dto.py index 6bbdfe9c..b76b798f 100644 --- a/backend/api/v1/modules/a76/general_catalogs/multi_currency_types/dto.py +++ b/backend/api/v1/modules/a76/general_catalogs/multi_currency_types/dto.py @@ -27,5 +27,7 @@ class MultiCurrencyTypeUpdate(BaseModel): class MultiCurrencyTypeResponse(MultiCurrencyTypeBase): id: int - + company_id: int + tenant_id: int + model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/a76/general_catalogs/multi_currency_types/models.py b/backend/api/v1/modules/a76/general_catalogs/multi_currency_types/models.py index 91751a9a..e119b8b8 100644 --- a/backend/api/v1/modules/a76/general_catalogs/multi_currency_types/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/multi_currency_types/models.py @@ -18,13 +18,18 @@ class MultiCurrencyType(Base, TenantScopedMixin, TimestampMixin): id: Mapped[int] = mapped_column( Integer, primary_key=True, autoincrement=True) + currency_type_code: Mapped[str] = mapped_column( String(3), ForeignKey("public.currency_types.code"), nullable=False) + country_key: Mapped[Optional[str]] = mapped_column( String(3), ForeignKey("public.countries.m3_key"), nullable=True) + conversion_factor: Mapped[Optional[Decimal]] = mapped_column( Numeric(13, 6), nullable=True) + publication_date: Mapped[int] = mapped_column(Integer, nullable=False) currency_type: Mapped["CurrencyType"] = relationship() + country: Mapped[Optional["Country"]] = relationship() diff --git a/backend/api/v1/modules/a76/general_catalogs/packages/models.py b/backend/api/v1/modules/a76/general_catalogs/packages/models.py index 98e560d3..f191fa94 100644 --- a/backend/api/v1/modules/a76/general_catalogs/packages/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/packages/models.py @@ -31,4 +31,4 @@ class Package(Base, TenantScopedMixin, TimestampMixin): plurals: Mapped[Optional[str]] = mapped_column(String(4)) plural_in: Mapped[Optional[str]] = mapped_column(String(4)) code_ace: Mapped[Optional[str]] = mapped_column(String(4)) - code_aamex: Mapped[Optional[str]] = mapped_column(String(9)) + code_aamex: Mapped[Optional[str]] = mapped_column(String(9)) \ No newline at end of file diff --git a/backend/api/v1/modules/a76/general_catalogs/signatures/dto.py b/backend/api/v1/modules/a76/general_catalogs/signatures/dto.py index 4524f91c..aeee8ca3 100644 --- a/backend/api/v1/modules/a76/general_catalogs/signatures/dto.py +++ b/backend/api/v1/modules/a76/general_catalogs/signatures/dto.py @@ -11,6 +11,7 @@ class SignatureCreateDTO(BaseModel): """DTO para crear una firma""" code: str = Field(..., max_length=10, description="Signature code") + signature: Optional[str] = Field( None, max_length=1000, description="Signature") photo_path: Optional[str] = Field( @@ -39,6 +40,8 @@ class SignatureResponseDTO(BaseModel): code: str signature: Optional[str] = None photo_path: Optional[str] = None + intenant_id: int + company_id: int class Config: from_attributes = True diff --git a/backend/api/v1/modules/a76/general_catalogs/signatures/models.py b/backend/api/v1/modules/a76/general_catalogs/signatures/models.py index e3d04b63..0885a276 100644 --- a/backend/api/v1/modules/a76/general_catalogs/signatures/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/signatures/models.py @@ -32,6 +32,7 @@ class Signature(Base, TenantScopedMixin, TimestampMixin): # Signature information signature: Mapped[Optional[str]] = mapped_column(String(1000)) + photo_path: Mapped[Optional[str]] = mapped_column(String(1000)) def __repr__(self): diff --git a/backend/api/v1/modules/a76/general_catalogs/unit_conversions/dto.py b/backend/api/v1/modules/a76/general_catalogs/unit_conversions/dto.py index 85fd2ad9..0bb41aed 100644 --- a/backend/api/v1/modules/a76/general_catalogs/unit_conversions/dto.py +++ b/backend/api/v1/modules/a76/general_catalogs/unit_conversions/dto.py @@ -2,27 +2,23 @@ from typing import Optional from decimal import Decimal from pydantic import BaseModel, Field, ConfigDict - class UnitConversionBase(BaseModel): - from_unit_code: str = Field(..., max_length=5, - description="Source Unit Code") - to_unit_code: str = Field(..., max_length=5, - description="Target Unit Code") - conversion_factor: Optional[Decimal] = Field( - None, description="Conversion Factor") - + # 👇 OJO: Son códigos (Strings), no IDs + from_unit_code: str = Field(..., max_length=5, description="Source Unit Code") + to_unit_code: str = Field(..., max_length=5, description="Target Unit Code") + conversion_factor: Optional[Decimal] = Field(None, description="Conversion Factor") class UnitConversionCreate(UnitConversionBase): pass - class UnitConversionUpdate(BaseModel): from_unit_code: Optional[str] = Field(None, max_length=5) to_unit_code: Optional[str] = Field(None, max_length=5) conversion_factor: Optional[Decimal] = None - class UnitConversionResponse(UnitConversionBase): id: int + tenant_id: int + company_id: int - model_config = ConfigDict(from_attributes=True) + model_config = ConfigDict(from_attributes=True) \ No newline at end of file diff --git a/backend/api/v1/modules/a76/general_catalogs/unit_conversions/models.py b/backend/api/v1/modules/a76/general_catalogs/unit_conversions/models.py index 65122a4c..26893749 100644 --- a/backend/api/v1/modules/a76/general_catalogs/unit_conversions/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/unit_conversions/models.py @@ -27,8 +27,11 @@ class UnitConversion(Base, TenantScopedMixin, TimestampMixin): id: Mapped[int] = mapped_column( Integer, primary_key=True, autoincrement=True) + from_unit_code: Mapped[str] = mapped_column(String(5), nullable=False) + to_unit_code: Mapped[str] = mapped_column(String(5), nullable=False) + conversion_factor: Mapped[Optional[Decimal]] = mapped_column( Numeric(13, 6), nullable=True) diff --git a/backend/api/v1/modules/a76/general_catalogs/units_of_measure/models.py b/backend/api/v1/modules/a76/general_catalogs/units_of_measure/models.py index 04cae220..ffd359db 100644 --- a/backend/api/v1/modules/a76/general_catalogs/units_of_measure/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/units_of_measure/models.py @@ -181,4 +181,4 @@ class UnitOfMeasureGeneral(Base, TenantScopedMixin, TimestampMixin): String(4), nullable=True) # CLAVEACE customs_unit: Mapped[Optional["UnitOfMeasureCustoms"]] = relationship() - ace_unit: Mapped[Optional["UnitOfMeasureACE"]] = relationship() + ace_unit: Mapped[Optional["UnitOfMeasureACE"]] = relationship(overlaps="customs_unit") diff --git a/frontend/src/lib/api/dashboard/a76/general_catalogs/classification-concepts.ts b/frontend/src/lib/api/dashboard/a76/general_catalogs/classification-concepts.ts index 6179c188..984cc4c0 100644 --- a/frontend/src/lib/api/dashboard/a76/general_catalogs/classification-concepts.ts +++ b/frontend/src/lib/api/dashboard/a76/general_catalogs/classification-concepts.ts @@ -2,60 +2,71 @@ import { api } from '$lib/api'; import type { ApiResponse } from '$lib/api'; export interface ClassificationConcept { - id: number; - classification: string; - description?: string; - tenant_id: string; - company_id?: string; - created_at: string; - updated_at?: string; + id: number; + classification: string; + tenant_id: number; + company_id: number; + created_at: string; + updated_at?: string; } export interface ClassificationConceptCreate { - classification: string; - description?: string; + classification: string; } export interface ClassificationConceptUpdate extends Partial {} export interface ClassificationConceptListResponse { - items: ClassificationConcept[]; - total: number; - page: number; - page_size: number; - pages: number; + items: ClassificationConcept[]; + total: number; + page: number; + page_size: number; + pages: number; } export async function getClassificationConcepts( - page: number = 1, - pageSize: number = 50, - filters: Record = {} + page: number = 1, + pageSize: number = 50, + companyId: number, + filters: Record = {} ): Promise> { - const params = new URLSearchParams({ - page: page.toString(), - page_size: pageSize.toString(), - ...filters - }); + const params = new URLSearchParams({ + page: page.toString(), + page_size: pageSize.toString(), + company_id: companyId.toString(), + ...filters + }); - const response = await api.get(`/a76/classification_concepts?${params.toString()}`); - return response.data; + + const response = await api.get(`/v1/a76/classification-concepts/?${params.toString()}`); + return response.data; } -export async function getClassificationConcept(id: number): Promise { - const response = await api.get(`/a76/classification_concepts/${id}`); - return response.data; +export async function getClassificationConcept(id: number, companyId: number): Promise { + const response = await api.get(`/v1/a76/classification-concepts/${id}/?company_id=${companyId}`); + return response.data; } -export async function createClassificationConcept(data: ClassificationConceptCreate): Promise { - const response = await api.post('/a76/classification_concepts', data); - return response.data; +export async function createClassificationConcept( + data: ClassificationConceptCreate, + companyId: number +): Promise { + // 👇 AQUÍ ESTABA EL ERROR GRAVE + const response = await api.post(`/v1/a76/classification-concepts/?company_id=${companyId}`, data); + return response.data; } -export async function updateClassificationConcept(id: number, data: ClassificationConceptUpdate): Promise { - const response = await api.patch(`/a76/classification_concepts/${id}`, data); - return response.data; + +export async function updateClassificationConcept( + id: number, + data: ClassificationConceptUpdate, + companyId: number // <-- Faltaba esto +): Promise { + const response = await api.put(`/v1/a76/classification-concepts/${id}/?company_id=${companyId}`, data); + return response.data; } -export async function deleteClassificationConcept(id: number): Promise { - await api.delete(`/a76/classification_concepts/${id}`); -} + +export async function deleteClassificationConcept(id: number, companyId: number): Promise { + await api.delete(`/v1/a76/classification-concepts/${id}/?company_id=${companyId}`); +} \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/a76/general_catalogs/company.ts b/frontend/src/lib/api/dashboard/a76/general_catalogs/company.ts index 3806c386..05f4682f 100644 --- a/frontend/src/lib/api/dashboard/a76/general_catalogs/company.ts +++ b/frontend/src/lib/api/dashboard/a76/general_catalogs/company.ts @@ -1,91 +1,91 @@ -import { api } from '$lib/api'; -import type { ApiResponse } from '$lib/api'; + import { api } from '$lib/api'; + import type { ApiResponse } from '$lib/api'; -export interface Company { - id: number; - tenant_id: number; - name: string | null; - rfc: string | null; - main_activity: string | null; - program: string | null; - program_number: string | null; - prosec: number | null; - prosec_authorization: string | null; - manufacturer_id: string | null; - broker_company: string | null; - responsible: string | null; - responsible_name: string | null; - responsible_last_name: string | null; - responsible_mother_last_name: string | null; - created_at: string | null; - updated_at: string | null; -} + export interface Company { + id: number; + tenant_id: number; + name: string | null; + rfc: string | null; + main_activity: string | null; + program: string | null; + program_number: string | null; + prosec: number | null; + prosec_authorization: string | null; + manufacturer_id: string | null; + broker_company: string | null; + responsible: string | null; + responsible_name: string | null; + responsible_last_name: string | null; + responsible_mother_last_name: string | null; + created_at: string | null; + updated_at: string | null; + } -export interface CompanyCreate { - name?: string | null; - rfc?: string | null; - main_activity?: string | null; - program?: string | null; - program_number?: string | null; - prosec?: number | null; - prosec_authorization?: string | null; - manufacturer_id?: string | null; - broker_company?: string | null; - responsible?: string | null; - responsible_name?: string | null; - responsible_last_name?: string | null; - responsible_mother_last_name?: string | null; -} + export interface CompanyCreate { + name?: string | null; + rfc?: string | null; + main_activity?: string | null; + program?: string | null; + program_number?: string | null; + prosec?: number | null; + prosec_authorization?: string | null; + manufacturer_id?: string | null; + broker_company?: string | null; + responsible?: string | null; + responsible_name?: string | null; + responsible_last_name?: string | null; + responsible_mother_last_name?: string | null; + } -export interface CompanyUpdate { - name?: string | null; - rfc?: string | null; - main_activity?: string | null; - program?: string | null; - program_number?: string | null; - prosec?: number | null; - prosec_authorization?: string | null; - manufacturer_id?: string | null; - broker_company?: string | null; - responsible?: string | null; - responsible_name?: string | null; - responsible_last_name?: string | null; - responsible_mother_last_name?: string | null; -} + export interface CompanyUpdate { + name?: string | null; + rfc?: string | null; + main_activity?: string | null; + program?: string | null; + program_number?: string | null; + prosec?: number | null; + prosec_authorization?: string | null; + manufacturer_id?: string | null; + broker_company?: string | null; + responsible?: string | null; + responsible_name?: string | null; + responsible_last_name?: string | null; + responsible_mother_last_name?: string | null; + } -export interface CompanyListResponse { - items: Company[]; - total: number; - page: number; - page_size: number; - pages: number; -} + export interface CompanyListResponse { + items: Company[]; + total: number; + page: number; + page_size: number; + pages: number; + } -export async function getCompanies( - page = 1, - pageSize = 50, - filters: Record = {} -): Promise> { - const queryParams = new URLSearchParams({ - page: page.toString(), - page_size: pageSize.toString(), - ...filters - }); - return await api.get(`/a76/company?${queryParams.toString()}`); -} + export async function getCompanies( + page = 1, + pageSize = 50, + filters: Record = {} + ): Promise> { + const queryParams = new URLSearchParams({ + page: page.toString(), + page_size: pageSize.toString(), + ...filters + }); + return await api.get(`/a76/company?${queryParams.toString()}`); + } -export async function getCompany(id: number): Promise> { - return await api.get(`/a76/company/${id}`); -} + export async function getCompany(id: number): Promise> { + return await api.get(`/a76/company/${id}`); + } -export async function createCompany(data: CompanyCreate): Promise> { - return await api.post(`/a76/company`, data); -} + export async function createCompany(data: CompanyCreate): Promise> { + return await api.post(`/v1/a76/company`, data); + } -export async function updateCompany(id: number, data: CompanyUpdate): Promise> { - return await api.put(`/a76/company/${id}`, data); -} + export async function updateCompany(id: number, data: CompanyUpdate): Promise> { + return await api.put(`/a76/company/${id}`, data); + } -export async function deleteCompany(id: number): Promise> { - return await api.delete(`/a76/company/${id}`); -} + export async function deleteCompany(id: number): Promise> { + return await api.delete(`/a76/company/${id}`); + } diff --git a/frontend/src/lib/api/dashboard/a76/general_catalogs/concepts.ts b/frontend/src/lib/api/dashboard/a76/general_catalogs/concepts.ts index aa723d72..2371e1c4 100644 --- a/frontend/src/lib/api/dashboard/a76/general_catalogs/concepts.ts +++ b/frontend/src/lib/api/dashboard/a76/general_catalogs/concepts.ts @@ -2,78 +2,88 @@ import { api } from '$lib/api'; import type { ApiResponse } from '$lib/api'; export interface Concept { - id: number; - code: string; - description?: string; - description_en?: string; - detailed_description?: string; - priority?: number; - priority_ame?: number; - first_total?: boolean; - type?: string; - is_printed?: boolean; - section?: number; - classification?: string; - tenant_id: string; - company_id?: string; - created_at: string; - updated_at?: string; + id: number; + code: string; + description?: string; + description_en?: string; + detailed_description?: string; + priority?: number; + priority_ame?: number; + first_total?: boolean; + type?: string; + is_printed?: boolean; + section?: number; + classification?: string; + tenant_id: string; + company_id?: string; + created_at: string; + updated_at?: string; } export interface ConceptCreate { - code: string; - description?: string; - description_en?: string; - detailed_description?: string; - priority?: number; - priority_ame?: number; - first_total?: boolean; - type?: string; - is_printed?: boolean; - section?: number; - classification?: string; + code: string; + description?: string; + description_en?: string; + detailed_description?: string; + priority?: number; + priority_ame?: number; + first_total?: boolean; + type?: string; + is_printed?: boolean; + section?: number; + classification?: string; } export interface ConceptUpdate extends Partial {} export interface ConceptListResponse { - items: Concept[]; - total: number; - page: number; - page_size: number; - pages: number; + items: Concept[]; + total: number; + page: number; + page_size: number; + pages: number; } export async function getConcepts( - page: number = 1, - pageSize: number = 50, - filters: Record = {} + page: number = 1, + pageSize: number = 50, + companyId: number, + filters: Record = {}, ): Promise> { - const params = new URLSearchParams({ - page: page.toString(), - page_size: pageSize.toString(), - ...filters - }); + const params = new URLSearchParams({ + page: page.toString(), + page_size: pageSize.toString(), + company_id: companyId.toString(), + ...filters + }); - const response = await api.get(`/a76/concepts?${params.toString()}`); - return response.data; + const response = await api.get(`/v1/a76/concepts/?${params.toString()}`); + return response.data; } -export async function getConcept(id: number): Promise { - const response = await api.get(`/a76/concepts/${id}`); - return response.data; + +export async function getConcept(id: number, companyId: number): Promise { + + const response = await api.get(`/v1/a76/concepts/${id}/?company_id=${companyId}`); + return response.data; } -export async function createConcept(data: ConceptCreate): Promise { - const response = await api.post('/a76/concepts', data); - return response.data; + +export async function createConcept(data: ConceptCreate, companyId: number): Promise { + + const response = await api.post(`/v1/a76/concepts/?company_id=${companyId}`, data); + return response.data; } -export async function updateConcept(id: number, data: ConceptUpdate): Promise { - const response = await api.patch(`/a76/concepts/${id}`, data); - return response.data; + +export async function updateConcept(id: number, data: ConceptUpdate, companyId: number): Promise { + + const response = await api.put(`/v1/a76/concepts/${id}/?company_id=${companyId}`, data); + return response.data; } -export async function deleteConcept(id: number): Promise { - await api.delete(`/a76/concepts/${id}`); -} + +export async function deleteConcept(id: number, companyId: number): Promise { + + await api.delete(`/v1/a76/concepts/${id}/?company_id=${companyId}`); +} \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/a76/general_catalogs/customs-broker-concepts.ts b/frontend/src/lib/api/dashboard/a76/general_catalogs/customs-broker-concepts.ts index 5e296fb8..3e40b598 100644 --- a/frontend/src/lib/api/dashboard/a76/general_catalogs/customs-broker-concepts.ts +++ b/frontend/src/lib/api/dashboard/a76/general_catalogs/customs-broker-concepts.ts @@ -2,76 +2,68 @@ import { api } from '$lib/api'; import type { ApiResponse } from '$lib/api'; export interface CustomsBrokerConcept { - id: number; - code: string; - description?: string; - description_en?: string; - detailed_description?: string; - priority?: number; - first_total?: boolean; - type?: string; - is_printed?: boolean; - section?: number; - classification?: string; - tenant_id: string; - company_id?: string; - created_at: string; - updated_at?: string; + id: number; + broker_key: string; + concept: string; + amount?: number; + priority?: number; } export interface CustomsBrokerConceptCreate { - code: string; - description?: string; - description_en?: string; - detailed_description?: string; - priority?: number; - first_total?: boolean; - type?: string; - is_printed?: boolean; - section?: number; - classification?: string; + broker_key: string; + concept?: string; + amount?: number; + priority?: number; } export interface CustomsBrokerConceptUpdate extends Partial {} export interface CustomsBrokerConceptListResponse { - items: CustomsBrokerConcept[]; - total: number; - page: number; - page_size: number; - pages: number; + items: CustomsBrokerConcept[]; + total: number; + page: number; + page_size: number; + pages: number; } + export async function getCustomsBrokerConcepts( - page: number = 1, - pageSize: number = 50, - filters: Record = {} + page: number = 1, + pageSize: number = 50, + companyId: number, // <-- Nuevo + filters: Record = {} ): Promise> { - const params = new URLSearchParams({ - page: page.toString(), - page_size: pageSize.toString(), - ...filters - }); + const params = new URLSearchParams({ + page: page.toString(), + page_size: pageSize.toString(), + company_id: companyId.toString(), + ...filters + }); - const response = await api.get(`/a76/customs_broker_concepts?${params.toString()}`); - return response.data; + + const response = await api.get(`/v1/a76/customs-broker-concepts/?${params.toString()}`); + return response.data; } -export async function getCustomsBrokerConcept(id: number): Promise { - const response = await api.get(`/a76/customs_broker_concepts/${id}`); - return response.data; +export async function createCustomsBrokerConcept( + data: CustomsBrokerConceptCreate, + companyId: number +): Promise { + + const response = await api.post(`/v1/a76/customs-broker-concepts/?company_id=${companyId}`, data); + return response.data; } -export async function createCustomsBrokerConcept(data: CustomsBrokerConceptCreate): Promise { - const response = await api.post('/a76/customs_broker_concepts', data); - return response.data; +export async function updateCustomsBrokerConcept( + id: number, + data: CustomsBrokerConceptUpdate, + companyId: number +): Promise { + + const response = await api.put(`/v1/a76/customs-broker-concepts/${id}/?company_id=${companyId}`, data); + return response.data; } -export async function updateCustomsBrokerConcept(id: number, data: CustomsBrokerConceptUpdate): Promise { - const response = await api.patch(`/a76/customs_broker_concepts/${id}`, data); - return response.data; -} - -export async function deleteCustomsBrokerConcept(id: number): Promise { - await api.delete(`/a76/customs_broker_concepts/${id}`); -} +export async function deleteCustomsBrokerConcept(id: number, companyId: number): Promise { + await api.delete(`/v1/a76/customs-broker-concepts/${id}/?company_id=${companyId}`); +} \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/a76/general_catalogs/error-catalogs.ts b/frontend/src/lib/api/dashboard/a76/general_catalogs/error-catalogs.ts index 994caa7f..b5dc471d 100644 --- a/frontend/src/lib/api/dashboard/a76/general_catalogs/error-catalogs.ts +++ b/frontend/src/lib/api/dashboard/a76/general_catalogs/error-catalogs.ts @@ -1,61 +1,134 @@ import { api } from '$lib/api'; import type { ApiResponse } from '$lib/api'; +// ========================================== +// ERROR CLASSIFICATION +// ========================================== + +export interface ErrorClassification { + id: number; + code: string; + level?: string; + errors?: ErrorCatalog[]; + tenant_id?: string; + created_at?: string; + updated_at?: string; +} + +export interface ErrorClassificationCreate { + code: string; + level?: string; +} + +export interface ErrorClassificationUpdate { + level?: string; +} + +export interface ErrorClassificationListResponse { + items: ErrorClassification[]; + total: number; + page: number; + page_size: number; + pages: number; +} + + export interface ErrorCatalog { - id: number; - code: string; - description?: string; - tenant_id: string; - company_id?: string; - created_at: string; - updated_at?: string; + id: number; + code: string; + description?: string; + classification_id?: number; + classification?: ErrorClassification; + tenant_id?: string; + created_at?: string; + updated_at?: string; } export interface ErrorCatalogCreate { - code: string; - description?: string; + code: string; + description?: string; + classification_id?: number; } -export interface ErrorCatalogUpdate extends Partial {} +export interface ErrorCatalogUpdate { + description?: string; + classification_id?: number; +} export interface ErrorCatalogListResponse { - items: ErrorCatalog[]; - total: number; - page: number; - page_size: number; - pages: number; + items: ErrorCatalog[]; + total: number; + page: number; + page_size: number; + pages: number; } -export async function getErrorCatalogs( - page: number = 1, - pageSize: number = 50, - filters: Record = {} -): Promise> { - const params = new URLSearchParams({ - page: page.toString(), - page_size: pageSize.toString(), - ...filters - }); +export async function getErrorClassifications( + page: number = 1, + pageSize: number = 50, + filters: Record = {} +): Promise> { + const params = new URLSearchParams({ + page: page.toString(), + page_size: pageSize.toString(), + ...filters + }); - const response = await api.get(`/a76/error_catalogs?${params.toString()}`); - return response.data; + const response = await api.get(`/a76/error_classifications?${params.toString()}`); + return response.data; +} + +export async function getErrorClassification(id: number): Promise { + const response = await api.get(`/a76/error_classifications/${id}`); + return response.data; +} + +export async function createErrorClassification(data: ErrorClassificationCreate): Promise { + const response = await api.post('/a76/error_classifications', data); + return response.data; +} + +export async function updateErrorClassification(id: number, data: ErrorClassificationUpdate): Promise { + const response = await api.patch(`/a76/error_classifications/${id}`, data); + return response.data; +} + +export async function deleteErrorClassification(id: number): Promise { + await api.delete(`/a76/error_classifications/${id}`); +} + +// --- Catalogs --- + +export async function getErrorCatalogs( + page: number = 1, + pageSize: number = 50, + filters: Record = {} +): Promise> { + const params = new URLSearchParams({ + page: page.toString(), + page_size: pageSize.toString(), + ...filters + }); + + const response = await api.get(`/a76/error_catalogs?${params.toString()}`); + return response.data; } export async function getErrorCatalog(id: number): Promise { - const response = await api.get(`/a76/error_catalogs/${id}`); - return response.data; + const response = await api.get(`/a76/error_catalogs/${id}`); + return response.data; } export async function createErrorCatalog(data: ErrorCatalogCreate): Promise { - const response = await api.post('/a76/error_catalogs', data); - return response.data; + const response = await api.post('/a76/error_catalogs', data); + return response.data; } export async function updateErrorCatalog(id: number, data: ErrorCatalogUpdate): Promise { - const response = await api.patch(`/a76/error_catalogs/${id}`, data); - return response.data; + const response = await api.patch(`/a76/error_catalogs/${id}`, data); + return response.data; } export async function deleteErrorCatalog(id: number): Promise { - await api.delete(`/a76/error_catalogs/${id}`); -} + await api.delete(`/a76/error_catalogs/${id}`); +} \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/a76/general_catalogs/identifiers.ts b/frontend/src/lib/api/dashboard/a76/general_catalogs/identifiers.ts index 5a2ca8d3..98660069 100644 --- a/frontend/src/lib/api/dashboard/a76/general_catalogs/identifiers.ts +++ b/frontend/src/lib/api/dashboard/a76/general_catalogs/identifiers.ts @@ -7,8 +7,8 @@ export interface Identifier { description: string | null; level: string | null; complement: string | null; - company_id: number; - tenant_id: number; + company_id: number; + tenant_id: number; created_at: string | null; updated_at: string | null; } @@ -18,7 +18,6 @@ export interface IdentifierCreate { description?: string | null; level?: string | null; complement?: string | null; - company_id: number; } export interface IdentifierUpdate { @@ -39,24 +38,37 @@ export interface IdentifierListResponse { export async function getIdentifiers( page = 1, pageSize = 50, + companyId: number, filters: Record = {} ): Promise> { const queryParams = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), + company_id: companyId.toString(), ...filters }); - return await api.get(`/a76/identifiers?${queryParams.toString()}`); + + return await api.get(`/v1/a76/identifiers/?${queryParams.toString()}`); } -export async function createIdentifier(data: IdentifierCreate): Promise> { - return await api.post('/a76/identifiers', data); +export async function createIdentifier( + data: IdentifierCreate, + companyId: number +): Promise> { + return await api.post(`/v1/a76/identifiers/?company_id=${companyId}`, data); } -export async function updateIdentifier(id: number, data: IdentifierUpdate): Promise> { - return await api.put(`/a76/identifiers/${id}`, data); +export async function updateIdentifier( + id: number, + data: IdentifierUpdate, + companyId: number +): Promise> { + return await api.put(`/v1/a76/identifiers/${id}/?company_id=${companyId}`, data); } -export async function deleteIdentifier(id: number): Promise> { - return await api.delete(`/a76/identifiers/${id}`); -} +export async function deleteIdentifier( + id: number, + companyId: number +): Promise> { + return await api.delete(`/v1/a76/identifiers/${id}/?company_id=${companyId}`); +} \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/a76/general_catalogs/inpc.ts b/frontend/src/lib/api/dashboard/a76/general_catalogs/inpc.ts index 95f5d8c4..1abb0359 100644 --- a/frontend/src/lib/api/dashboard/a76/general_catalogs/inpc.ts +++ b/frontend/src/lib/api/dashboard/a76/general_catalogs/inpc.ts @@ -2,62 +2,77 @@ import { api } from '$lib/api'; import type { ApiResponse } from '$lib/api'; export interface INPC { - id: number; - year: string; - month: string; - value?: number; - tenant_id: string; - company_id?: string; - created_at: string; - updated_at?: string; + id: number; + year: string; + month: string; + value?: number; + tenant_id: number; + company_id: number; + created_at: string; + updated_at?: string; } export interface INPCCreate { - year: string; - month: string; - value?: number; + year: string; + month: string; + value?: number; + } export interface INPCUpdate extends Partial {} export interface INPCListResponse { - items: INPC[]; - total: number; - page: number; - page_size: number; - pages: number; + items: INPC[]; + total: number; + page: number; + page_size: number; + pages: number; } + export async function getINPCs( - page: number = 1, - pageSize: number = 50, - filters: Record = {} + page: number = 1, + pageSize: number = 50, + companyId: number, + filters: Record = {} ): Promise> { - const params = new URLSearchParams({ - page: page.toString(), - page_size: pageSize.toString(), - ...filters - }); + const params = new URLSearchParams({ + page: page.toString(), + page_size: pageSize.toString(), + company_id: companyId.toString(), + ...filters + }); - const response = await api.get(`/a76/inpc?${params.toString()}`); - return response.data; + const response = await api.get(`/v1/a76/inpc/?${params.toString()}`); + return response.data; } -export async function getINPC(id: number): Promise { - const response = await api.get(`/a76/inpc/${id}`); - return response.data; + +export async function getINPC(id: number, companyId: number): Promise { + const response = await api.get(`/v1/a76/inpc/${id}/?company_id=${companyId}`); + return response.data; } -export async function createINPC(data: INPCCreate): Promise { - const response = await api.post('/a76/inpc', data); - return response.data; + +export async function createINPC( + data: INPCCreate, + companyId: number +): Promise { + const response = await api.post(`/v1/a76/inpc/?company_id=${companyId}`, data); + return response.data; } -export async function updateINPC(id: number, data: INPCUpdate): Promise { - const response = await api.patch(`/a76/inpc/${id}`, data); - return response.data; + +export async function updateINPC( + id: number, + data: INPCUpdate, + companyId: number +): Promise { + + const response = await api.put(`/v1/a76/inpc/${id}/?company_id=${companyId}`, data); + return response.data; } -export async function deleteINPC(id: number): Promise { - await api.delete(`/a76/inpc/${id}`); -} +export async function deleteINPC(id: number, companyId: number): Promise { + await api.delete(`/v1/a76/inpc/${id}/?company_id=${companyId}`); +} \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/a76/general_catalogs/legends.ts b/frontend/src/lib/api/dashboard/a76/general_catalogs/legends.ts index d04317a6..989f8d37 100644 --- a/frontend/src/lib/api/dashboard/a76/general_catalogs/legends.ts +++ b/frontend/src/lib/api/dashboard/a76/general_catalogs/legends.ts @@ -2,60 +2,71 @@ import { api } from '$lib/api'; import type { ApiResponse } from '$lib/api'; export interface Legend { - id: number; - code: string; - description?: string; - tenant_id: string; - company_id?: string; - created_at: string; - updated_at?: string; + id: number; + code: number; + description?: string; + tenant_id: number; + company_id: number; + created_at: string; + updated_at?: string; } export interface LegendCreate { - code: string; - description?: string; + code: number; + description?: string; } export interface LegendUpdate extends Partial {} export interface LegendListResponse { - items: Legend[]; - total: number; - page: number; - page_size: number; - pages: number; + items: Legend[]; + total: number; + page: number; + page_size: number; + pages: number; } export async function getLegends( - page: number = 1, - pageSize: number = 50, - filters: Record = {} + page: number = 1, + pageSize: number = 50, + companyId: number, + filters: Record = {} ): Promise> { - const params = new URLSearchParams({ - page: page.toString(), - page_size: pageSize.toString(), - ...filters - }); - - const response = await api.get(`/a76/legends?${params.toString()}`); - return response.data; + const params = new URLSearchParams({ + page: page.toString(), + page_size: pageSize.toString(), + company_id: companyId.toString(), + ...filters + }); + const response = await api.get(`/v1/a76/legends/?${params.toString()}`); + return response.data; } -export async function getLegend(id: number): Promise { - const response = await api.get(`/a76/legends/${id}`); - return response.data; +export async function getLegend(id: number, companyId: number): Promise { + const response = await api.get(`/v1/a76/legends/${id}/?company_id=${companyId}`); + return response.data; } -export async function createLegend(data: LegendCreate): Promise { - const response = await api.post('/a76/legends', data); - return response.data; + +export async function createLegend( + data: LegendCreate, + companyId: number +): Promise { + + const response = await api.post(`/v1/a76/legends/?company_id=${companyId}`, data); + return response.data; } -export async function updateLegend(id: number, data: LegendUpdate): Promise { - const response = await api.patch(`/a76/legends/${id}`, data); - return response.data; +export async function updateLegend( + id: number, + data: LegendUpdate, + companyId: number +): Promise { + + const response = await api.put(`/v1/a76/legends/${id}/?company_id=${companyId}`, data); + return response.data; } -export async function deleteLegend(id: number): Promise { - await api.delete(`/a76/legends/${id}`); -} +export async function deleteLegend(id: number, companyId: number): Promise { + await api.delete(`/v1/a76/legends/${id}/?company_id=${companyId}`); +} \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/a76/general_catalogs/locations.ts b/frontend/src/lib/api/dashboard/a76/general_catalogs/locations.ts index 888e6f98..25426fa8 100644 --- a/frontend/src/lib/api/dashboard/a76/general_catalogs/locations.ts +++ b/frontend/src/lib/api/dashboard/a76/general_catalogs/locations.ts @@ -1,52 +1,69 @@ -/** - * API Client para Locations - Ubicaciones relacionadas con puertos - * Basado en los campos location_code y location_description del módulo de puertos - */ import { api } from '$lib/api'; import type { ApiResponse } from '$lib/api'; export interface Location { - location_code: string; - location_description: string | null; + id: number; + code: string; + description?: string; + + + tenant_id?: string; + created_at?: string; + updated_at?: string; } -/** - * Nota: Las ubicaciones están integradas en el módulo de puertos. - * Este archivo proporciona tipos para trabajar con ubicaciones, - * pero las operaciones se realizan a través del módulo de puertos. - * - * Ver: /a76/ports para operaciones relacionadas con ubicaciones - */ -/** - * Obtiene ubicaciones únicas de los puertos - * Esta función extrae las ubicaciones únicas de la lista de puertos - */ -export async function getLocationsFromPorts(): Promise> { - const portsResponse = await api.get('/a76/ports?page_size=1000'); - - if (portsResponse.error || !portsResponse.data) { - return { - error: portsResponse.error || 'Error al obtener puertos', - status: portsResponse.status - }; - } +export interface LocationCreate { + code: string; + description?: string; +} - // Extraer ubicaciones únicas - const locationMap = new Map(); - const ports = portsResponse.data.items || []; - - ports.forEach((port: any) => { - if (port.location_code && !locationMap.has(port.location_code)) { - locationMap.set(port.location_code, { - location_code: port.location_code, - location_description: port.location_description - }); - } + +export interface LocationUpdate { + code?: string; + description?: string; +} + +export interface LocationListResponse { + items: Location[]; + total: number; + page: number; + page_size: number; + pages: number; +} + + + +export async function getLocations( + page: number = 1, + pageSize: number = 50, + filters: Record = {} +): Promise> { + const params = new URLSearchParams({ + page: page.toString(), + page_size: pageSize.toString(), + ...filters }); - return { - data: Array.from(locationMap.values()), - status: 200 - }; + const response = await api.get(`/a24/locations?${params.toString()}`); + return response.data; } + +export async function getLocation(id: number): Promise { + const response = await api.get(`/a24/locations/${id}`); + return response.data; +} + +export async function createLocation(data: LocationCreate): Promise { + const response = await api.post('/a24/locations', data); + return response.data; +} + +export async function updateLocation(id: number, data: LocationUpdate): Promise { + const response = await api.patch(`/a24/locations/${id}`, data); + return response.data; +} + +export async function deleteLocation(id: number): Promise { + await api.delete(`/a24/locations/${id}`); +} \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/a76/general_catalogs/multi-currency-types.ts b/frontend/src/lib/api/dashboard/a76/general_catalogs/multi-currency-types.ts index d944b8c7..630a1185 100644 --- a/frontend/src/lib/api/dashboard/a76/general_catalogs/multi-currency-types.ts +++ b/frontend/src/lib/api/dashboard/a76/general_catalogs/multi-currency-types.ts @@ -2,60 +2,78 @@ import { api } from '$lib/api'; import type { ApiResponse } from '$lib/api'; export interface MultiCurrencyType { - id: number; - key: string; - description?: string; - tenant_id: string; - company_id?: string; - created_at: string; - updated_at?: string; + id: number; + + currency_type_code: string; + country_key: string | null; + conversion_factor: number | null; + publication_date: number; + tenant_id: number; + company_id: number; + created_at: string; + updated_at?: string; } export interface MultiCurrencyTypeCreate { - key: string; - description?: string; + currency_type_code: string; + country_key?: string | null; + conversion_factor?: number | null; + publication_date: number; } export interface MultiCurrencyTypeUpdate extends Partial {} export interface MultiCurrencyTypeListResponse { - items: MultiCurrencyType[]; - total: number; - page: number; - page_size: number; - pages: number; + items: MultiCurrencyType[]; + total: number; + page: number; + page_size: number; + pages: number; } export async function getMultiCurrencyTypes( - page: number = 1, - pageSize: number = 50, - filters: Record = {} + page: number = 1, + pageSize: number = 50, + companyId: number, + filters: Record = {} ): Promise> { - const params = new URLSearchParams({ - page: page.toString(), - page_size: pageSize.toString(), - ...filters - }); + const params = new URLSearchParams({ + page: page.toString(), + page_size: pageSize.toString(), + company_id: companyId.toString(), + ...filters + }); - const response = await api.get(`/a76/multi_currency_types?${params.toString()}`); - return response.data; + // Agregamos /v1 y el path correcto + const response = await api.get(`/v1/a76/multi_currency_types/?${params.toString()}`); + return response.data; } -export async function getMultiCurrencyType(id: number): Promise { - const response = await api.get(`/a76/multi_currency_types/${id}`); - return response.data; + +export async function getMultiCurrencyType(id: number, companyId: number): Promise { + const response = await api.get(`/v1/a76/multi_currency_types/${id}/?company_id=${companyId}`); + return response.data; } -export async function createMultiCurrencyType(data: MultiCurrencyTypeCreate): Promise { - const response = await api.post('/a76/multi_currency_types', data); - return response.data; +export async function createMultiCurrencyType( + data: MultiCurrencyTypeCreate, + companyId: number +): Promise { + const response = await api.post(`/v1/a76/multi_currency_types/?company_id=${companyId}`, data); + return response.data; } -export async function updateMultiCurrencyType(id: number, data: MultiCurrencyTypeUpdate): Promise { - const response = await api.patch(`/a76/multi_currency_types/${id}`, data); - return response.data; + +export async function updateMultiCurrencyType( + id: number, + data: MultiCurrencyTypeUpdate, + companyId: number +): Promise { + const response = await api.put(`/v1/a76/multi_currency_types/${id}/?company_id=${companyId}`, data); + return response.data; } -export async function deleteMultiCurrencyType(id: number): Promise { - await api.delete(`/a76/multi_currency_types/${id}`); -} + +export async function deleteMultiCurrencyType(id: number, companyId: number): Promise { + await api.delete(`/v1/a76/multi_currency_types/${id}/?company_id=${companyId}`); +} \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/a76/general_catalogs/packages.ts b/frontend/src/lib/api/dashboard/a76/general_catalogs/packages.ts index 7af320b2..49e620ca 100644 --- a/frontend/src/lib/api/dashboard/a76/general_catalogs/packages.ts +++ b/frontend/src/lib/api/dashboard/a76/general_catalogs/packages.ts @@ -4,17 +4,17 @@ import type { ApiResponse } from '$lib/api'; export interface Package { id: number; tenant_id: number; - company_id: number; - key: string; - description_es: string | null; - description_en: string | null; - weight_unit: number | null; - plurals: string | null; - plural_in: string | null; - code_ace: string | null; - code_aamex: string | null; - created_at: string | null; - updated_at: string | null; + company_id: number; + key: string; + description_es: string | null; + description_en: string | null; + weight_unit: number | null; + plurals: string | null; + plural_in: string | null; + code_ace: string | null; + code_aamex: string | null; + created_at: string; + updated_at?: string; } export interface PackageCreate { @@ -26,19 +26,9 @@ export interface PackageCreate { plural_in?: string | null; code_ace?: string | null; code_aamex?: string | null; - company_id: number; } -export interface PackageUpdate { - key?: string; - description_es?: string | null; - description_en?: string | null; - weight_unit?: number | null; - plurals?: string | null; - plural_in?: string | null; - code_ace?: string | null; - code_aamex?: string | null; -} +export interface PackageUpdate extends Partial {} export interface PackageListResponse { items: Package[]; @@ -48,31 +38,48 @@ export interface PackageListResponse { pages: number; } + export async function getPackages( - page = 1, - pageSize = 50, + page: number = 1, + pageSize: number = 50, + companyId: number, // Obligatorio por el Mixin filters: Record = {} ): Promise> { - const queryParams = new URLSearchParams({ + const params = new URLSearchParams({ page: page.toString(), page_size: pageSize.toString(), + company_id: companyId.toString(), ...filters }); - return await api.get(`/a76/packages?${queryParams.toString()}`); + + const response = await api.get(`/v1/a76/packages/?${params.toString()}`); + return response.data; } -export async function getPackage(id: number): Promise> { - return await api.get(`/a76/packages/${id}`); + +export async function getPackage(id: number, companyId: number): Promise { + const response = await api.get(`/v1/a76/packages/${id}/?company_id=${companyId}`); + return response.data; } -export async function createPackage(data: PackageCreate): Promise> { - return await api.post(`/a76/packages`, data); +export async function createPackage( + data: PackageCreate, + companyId: number +): Promise { + const response = await api.post(`/v1/a76/packages/?company_id=${companyId}`, data); + return response.data; } -export async function updatePackage(id: number, data: PackageUpdate): Promise> { - return await api.put(`/a76/packages/${id}`, data); + +export async function updatePackage( + id: number, + data: PackageUpdate, + companyId: number +): Promise { + const response = await api.put(`/v1/a76/packages/${id}/?company_id=${companyId}`, data); + return response.data; } -export async function deletePackage(id: number): Promise> { - return await api.delete(`/a76/packages/${id}`); -} +export async function deletePackage(id: number, companyId: number): Promise { + await api.delete(`/v1/a76/packages/${id}/?company_id=${companyId}`); +} \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/a76/general_catalogs/signatures.ts b/frontend/src/lib/api/dashboard/a76/general_catalogs/signatures.ts index 60073763..1d22184f 100644 --- a/frontend/src/lib/api/dashboard/a76/general_catalogs/signatures.ts +++ b/frontend/src/lib/api/dashboard/a76/general_catalogs/signatures.ts @@ -2,62 +2,79 @@ import { api } from '$lib/api'; import type { ApiResponse } from '$lib/api'; export interface Signature { - id: number; - name: string; - position?: string; - certificate?: string; - tenant_id: string; - company_id?: string; - created_at: string; - updated_at?: string; + id: number; + + code: string; + signature: string | null; + photo_path: string | null; + + + tenant_id: number; + company_id: number; + created_at: string; + updated_at?: string; } export interface SignatureCreate { - name: string; - position?: string; - certificate?: string; + code: string; + signature?: string | null; + photo_path?: string | null; + } export interface SignatureUpdate extends Partial {} export interface SignatureListResponse { - items: Signature[]; - total: number; - page: number; - page_size: number; - pages: number; + items: Signature[]; + total: number; + page: number; + page_size: number; + pages: number; } + export async function getSignatures( - page: number = 1, - pageSize: number = 50, - filters: Record = {} + page: number = 1, + pageSize: number = 50, + companyId: number, + filters: Record = {} ): Promise> { - const params = new URLSearchParams({ - page: page.toString(), - page_size: pageSize.toString(), - ...filters - }); + const params = new URLSearchParams({ + page: page.toString(), + page_size: pageSize.toString(), + company_id: companyId.toString(), + ...filters + }); - const response = await api.get(`/a76/signatures?${params.toString()}`); - return response.data; + + const response = await api.get(`/v1/a76/signatures/?${params.toString()}`); + return response.data; } -export async function getSignature(id: number): Promise { - const response = await api.get(`/a76/signatures/${id}`); - return response.data; + +export async function getSignature(id: number, companyId: number): Promise { + const response = await api.get(`/v1/a76/signatures/${id}/?company_id=${companyId}`); + return response.data; } -export async function createSignature(data: SignatureCreate): Promise { - const response = await api.post('/a76/signatures', data); - return response.data; + +export async function createSignature( + data: SignatureCreate, + companyId: number +): Promise { + const response = await api.post(`/v1/a76/signatures/?company_id=${companyId}`, data); + return response.data; } -export async function updateSignature(id: number, data: SignatureUpdate): Promise { - const response = await api.patch(`/a76/signatures/${id}`, data); - return response.data; +export async function updateSignature( + id: number, + data: SignatureUpdate, + companyId: number +): Promise { + const response = await api.put(`/v1/a76/signatures/${id}/?company_id=${companyId}`, data); + return response.data; } -export async function deleteSignature(id: number): Promise { - await api.delete(`/a76/signatures/${id}`); -} +export async function deleteSignature(id: number, companyId: number): Promise { + await api.delete(`/v1/a76/signatures/${id}/?company_id=${companyId}`); +} \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/a76/general_catalogs/unit-conversions.ts b/frontend/src/lib/api/dashboard/a76/general_catalogs/unit-conversions.ts index 1494fd76..610f707d 100644 --- a/frontend/src/lib/api/dashboard/a76/general_catalogs/unit-conversions.ts +++ b/frontend/src/lib/api/dashboard/a76/general_catalogs/unit-conversions.ts @@ -2,62 +2,76 @@ import { api } from '$lib/api'; import type { ApiResponse } from '$lib/api'; export interface UnitConversion { - id: number; - from_unit_id: number; - to_unit_id: number; - conversion_factor: number; - tenant_id: string; - company_id?: string; - created_at: string; - updated_at?: string; + id: number; + from_unit_code: string; + to_unit_code: string; + conversion_factor: number; + tenant_id: number; // number + company_id: number; // number + created_at: string; + updated_at?: string; } export interface UnitConversionCreate { - from_unit_id: number; - to_unit_id: number; - conversion_factor: number; + from_unit_code: string; // Ej: "KGM" + to_unit_code: string; // Ej: "LBR" + conversion_factor: number; + // company_id va en la URL } export interface UnitConversionUpdate extends Partial {} export interface UnitConversionListResponse { - items: UnitConversion[]; - total: number; - page: number; - page_size: number; - pages: number; + items: UnitConversion[]; + total: number; + page: number; + page_size: number; + pages: number; } + export async function getUnitConversions( - page: number = 1, - pageSize: number = 50, - filters: Record = {} + page: number = 1, + pageSize: number = 50, + companyId: number, // 👈 Obligatorio + filters: Record = {} ): Promise> { - const params = new URLSearchParams({ - page: page.toString(), - page_size: pageSize.toString(), - ...filters - }); + const params = new URLSearchParams({ + page: page.toString(), + page_size: pageSize.toString(), + company_id: companyId.toString(), + ...filters + }); - const response = await api.get(`/a76/unit_conversions?${params.toString()}`); - return response.data; + // Agregamos /v1 y prefijo. + // NOTA: Revisa si en tu router definiste "unit_conversions" o "unit-conversions" + const response = await api.get(`/v1/a76/unit-conversions/?${params.toString()}`); + return response.data; } -export async function getUnitConversion(id: number): Promise { - const response = await api.get(`/a76/unit_conversions/${id}`); - return response.data; +export async function getUnitConversion(id: number, companyId: number): Promise { + const response = await api.get(`/v1/a76/unit-conversions/${id}/?company_id=${companyId}`); + return response.data; } -export async function createUnitConversion(data: UnitConversionCreate): Promise { - const response = await api.post('/a76/unit_conversions', data); - return response.data; +export async function createUnitConversion( + data: UnitConversionCreate, + companyId: number +): Promise { + const response = await api.post(`/v1/a76/unit-conversions/?company_id=${companyId}`, data); + return response.data; } -export async function updateUnitConversion(id: number, data: UnitConversionUpdate): Promise { - const response = await api.patch(`/a76/unit_conversions/${id}`, data); - return response.data; + +export async function updateUnitConversion( + id: number, + data: UnitConversionUpdate, + companyId: number +): Promise { + const response = await api.put(`/v1/a76/unit-conversions/${id}/?company_id=${companyId}`, data); + return response.data; } -export async function deleteUnitConversion(id: number): Promise { - await api.delete(`/a76/unit_conversions/${id}`); -} +export async function deleteUnitConversion(id: number, companyId: number): Promise { + await api.delete(`/v1/a76/unit_conversions/${id}/?company_id=${companyId}`); +} \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/a76/invoices.ts b/frontend/src/lib/api/dashboard/a76/invoices.ts index 99a23601..461b8346 100644 --- a/frontend/src/lib/api/dashboard/a76/invoices.ts +++ b/frontend/src/lib/api/dashboard/a76/invoices.ts @@ -124,7 +124,7 @@ export interface InvoiceListResponse { page_size: number; } -export interface CreateInvoiceData { +export interface InvoiceData { operation_type?: OperationType | null; invoice_type?: string | null; invoice_number?: string | null; diff --git a/frontend/src/lib/components/dashboard/company/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/company/create-edit-dialog.svelte deleted file mode 100644 index 96ed1222..00000000 --- a/frontend/src/lib/components/dashboard/company/create-edit-dialog.svelte +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - {title} - - -
{ e.preventDefault(); handleSubmit(); }} class="space-y-4"> - {#if error} -
{error}
- {/if} - -
- - -
- -
- - -
- -
- - -
- -
- - -
- - - - - -
-
-
diff --git a/frontend/src/lib/components/dashboard/exchange-rate/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/exchange-rate/create-edit-dialog.svelte index 940e7f78..5577e18a 100644 --- a/frontend/src/lib/components/dashboard/exchange-rate/create-edit-dialog.svelte +++ b/frontend/src/lib/components/dashboard/exchange-rate/create-edit-dialog.svelte @@ -1,164 +1,187 @@ - - - - {isEdit ? 'Editar' : 'Crear'} Tipo de Cambio - + + + + {title} + -
-
- - -
+ { e.preventDefault(); handleSubmit(); }} class="space-y-4 py-4"> + {#if error} +
+ {error} +
+ {/if} -
- - -
+
+
+ +
+ +
+
-
- - -
+
+ +
+ +

Hasta 6 decimales.

+
+
-
- - -
+
+ +
+ +
+
- {#if error} -

{error}

- {/if} +
+ +
+ +
+
+
-
- - -
-
-
-
+ + + + + +
+
\ No newline at end of file diff --git a/frontend/src/lib/components/dashboard/general_catalogs/classification/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/general_catalogs/classification/create-edit-dialog.svelte new file mode 100644 index 00000000..86612132 --- /dev/null +++ b/frontend/src/lib/components/dashboard/general_catalogs/classification/create-edit-dialog.svelte @@ -0,0 +1,115 @@ + + + + + + {title} + + +
{ e.preventDefault(); handleSubmit(); }} class="space-y-4"> + {#if error} +
+ {error} +
+ {/if} + +
+
+ + +
+
+ + + + + +
+
+
\ No newline at end of file diff --git a/frontend/src/lib/components/dashboard/company/columns.ts b/frontend/src/lib/components/dashboard/general_catalogs/company/columns.ts similarity index 100% rename from frontend/src/lib/components/dashboard/company/columns.ts rename to frontend/src/lib/components/dashboard/general_catalogs/company/columns.ts diff --git a/frontend/src/lib/components/dashboard/general_catalogs/company/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/general_catalogs/company/create-edit-dialog.svelte new file mode 100644 index 00000000..2cc43f4a --- /dev/null +++ b/frontend/src/lib/components/dashboard/general_catalogs/company/create-edit-dialog.svelte @@ -0,0 +1,296 @@ + + + + + + {title} + + +
{ e.preventDefault(); handleSubmit(); }} class="space-y-4"> + {#if error} +
+ {error} +
+ {/if} + + + + General + Programas + Responsable + Config + + + +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+
+
+ + + + + +
+
+
\ No newline at end of file diff --git a/frontend/src/lib/components/dashboard/company/data-table-actions.svelte b/frontend/src/lib/components/dashboard/general_catalogs/company/data-table-actions.svelte similarity index 100% rename from frontend/src/lib/components/dashboard/company/data-table-actions.svelte rename to frontend/src/lib/components/dashboard/general_catalogs/company/data-table-actions.svelte diff --git a/frontend/src/lib/components/dashboard/general_catalogs/concepts/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/general_catalogs/concepts/create-edit-dialog.svelte new file mode 100644 index 00000000..779795b6 --- /dev/null +++ b/frontend/src/lib/components/dashboard/general_catalogs/concepts/create-edit-dialog.svelte @@ -0,0 +1,213 @@ + + + + + + {title} + + +
{ e.preventDefault(); handleSubmit(); }} class="space-y-4"> + {#if error} +
+ {error} +
+ {/if} + + + +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+
+ + + + + +
+
+
\ No newline at end of file diff --git a/frontend/src/lib/components/dashboard/general_catalogs/customs_broker_concepts/create-edite-dialoge.svelte b/frontend/src/lib/components/dashboard/general_catalogs/customs_broker_concepts/create-edite-dialoge.svelte new file mode 100644 index 00000000..c2f4cf21 --- /dev/null +++ b/frontend/src/lib/components/dashboard/general_catalogs/customs_broker_concepts/create-edite-dialoge.svelte @@ -0,0 +1,165 @@ + + + + + + {title} + + {isEdit ? 'Modifica el concepto del agente aduanal' : 'Crea un nuevo concepto'} + + + +
{ e.preventDefault(); handleSubmit(); }} class="space-y-4"> + {#if error} +
+ {error} +
+ {/if} + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + + + + +
+
+
\ No newline at end of file diff --git a/frontend/src/lib/components/dashboard/general_catalogs/error_catalogs/create-edite-dialoge.svelte b/frontend/src/lib/components/dashboard/general_catalogs/error_catalogs/create-edite-dialoge.svelte new file mode 100644 index 00000000..1a8b1db0 --- /dev/null +++ b/frontend/src/lib/components/dashboard/general_catalogs/error_catalogs/create-edite-dialoge.svelte @@ -0,0 +1,141 @@ + + +{#if open} +
+ +
+ +
+

+ {classification ? 'Editar Clasificación' : 'Nueva Clasificación'} +

+ +
+ +
+ +
+ + + {#if classification} +

El código no se puede cambiar.

+ {/if} +
+ +
+ + +
+ + {#if classification} +
+
+ Creado: + {formatDate(classification.created_at)} +
+ {#if classification.updated_at} +
+ Actualizado: + {formatDate(classification.updated_at)} +
+ {/if} +
+ {/if} + +
+ + +
+ +
+
+
+{/if} \ No newline at end of file diff --git a/frontend/src/lib/components/dashboard/general_catalogs/inpc/create-edite-dialoge.svelte b/frontend/src/lib/components/dashboard/general_catalogs/inpc/create-edite-dialoge.svelte new file mode 100644 index 00000000..bb314713 --- /dev/null +++ b/frontend/src/lib/components/dashboard/general_catalogs/inpc/create-edite-dialoge.svelte @@ -0,0 +1,166 @@ + + + + + + {title} + + +
{ e.preventDefault(); handleSubmit(); }} class="space-y-4 py-4"> + {#if error} +
+ {error} +
+ {/if} + +
+
+ +
+ +
+
+ +
+ +
+ +

Formato MM (Ej: 01, 12)

+
+
+ +
+ +
+ +

Hasta 8 decimales

+
+
+
+ + + + + +
+
+
\ No newline at end of file diff --git a/frontend/src/lib/components/dashboard/general_catalogs/legend/create-edite-dialoge.svelte b/frontend/src/lib/components/dashboard/general_catalogs/legend/create-edite-dialoge.svelte new file mode 100644 index 00000000..8e61a795 --- /dev/null +++ b/frontend/src/lib/components/dashboard/general_catalogs/legend/create-edite-dialoge.svelte @@ -0,0 +1,149 @@ + + + + + + {title} + + +
{ e.preventDefault(); handleSubmit(); }} class="space-y-4 py-4"> + {#if error} +
+ {error} +
+ {/if} + +
+
+ +
+ +

Debe ser un número entero.

+
+
+ +
+ +
+ +
+ + {#if location} +
+
+ Creado: + {formatDate(location.created_at)} +
+ {#if location.updated_at} +
+ Actualizado: + {formatDate(location.updated_at)} +
+ {/if} +
+ {/if} + +
+ + +
+ + +
+
+{/if} \ No newline at end of file diff --git a/frontend/src/lib/components/dashboard/general_catalogs/multi_currency_types/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/general_catalogs/multi_currency_types/create-edit-dialog.svelte new file mode 100644 index 00000000..738d2fb7 --- /dev/null +++ b/frontend/src/lib/components/dashboard/general_catalogs/multi_currency_types/create-edit-dialog.svelte @@ -0,0 +1,196 @@ + + + + + + {title} + + +
{ e.preventDefault(); handleSubmit(); }} class="space-y-4 py-4"> + {#if error} +
+ {error} +
+ {/if} + +
+
+ +
+ +

Código de moneda (FK).

+
+
+ +
+ +
+ +

Clave M3 del país (FK).

+
+
+ +
+ +
+ +

Se guarda como entero (YYYYMMDD).

+
+
+ +
+ +
+ +
+
+
+ + + + + +
+
+
\ No newline at end of file diff --git a/frontend/src/lib/components/dashboard/general_catalogs/signatures/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/general_catalogs/signatures/create-edit-dialog.svelte new file mode 100644 index 00000000..ec69a2fa --- /dev/null +++ b/frontend/src/lib/components/dashboard/general_catalogs/signatures/create-edit-dialog.svelte @@ -0,0 +1,163 @@ + + + + + + {title} + + +
{ e.preventDefault(); handleSubmit(); }} class="space-y-4 py-4"> + {#if error} +
+ {error} +
+ {/if} + +
+
+ +
+ +

Máximo 10 caracteres.

+
+
+ +
+ +
+ -
- - {#if location} -
-
- Creado: - {formatDate(location.created_at)} -
- {#if location.updated_at} -
- Actualizado: - {formatDate(location.updated_at)} -
- {/if} + {#if isEdit && location} +
+
+ Creado: + {formatDate(location.created_at)}
- {/if} - -
- - + {#if location.updated_at} +
+ Actualizado: + {formatDate(location.updated_at)} +
+ {/if}
+ {/if} - -
-
-{/if} \ No newline at end of file + + + + + + + \ No newline at end of file diff --git a/frontend/src/lib/components/dashboard/general_catalogs/prevalidators/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/general_catalogs/prevalidators/create-edit-dialog.svelte new file mode 100644 index 00000000..cc7094db --- /dev/null +++ b/frontend/src/lib/components/dashboard/general_catalogs/prevalidators/create-edit-dialog.svelte @@ -0,0 +1,200 @@ + + + + + + {title} + + +
{ e.preventDefault(); handleSubmit(); }} class="space-y-4 py-4"> + {#if error} +
+ {error} +
+ {/if} + +
+ +
+ +
+ +

Máximo 20 caracteres

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