From f2005e33a092b6363c13fa16474fa3d497422878 Mon Sep 17 00:00:00 2001 From: Kevin_Ramirez Date: Fri, 6 Feb 2026 16:22:04 -0600 Subject: [PATCH 1/5] Se integro el modulo de manifestacion base --- backend/api/v1/common/tenant_crud_routes.py | 4 + .../a76/general_catalogs/ports/service.py | 6 +- backend/api/v1/modules/a76/invoices/routes.py | 49 +- .../api/v1/modules/a76/invoices/schemas.py | 14 +- .../api/v1/modules/a76/invoices/services.py | 10 + .../api/v1/modules/a76/manifests/__init__.py | 0 .../concept_manifestation/__init__.py | 0 .../manifests/concept_manifestation/dtos.py | 48 + .../manifests/concept_manifestation/models.py | 45 + .../manifests/concept_manifestation/routes.py | 112 ++ .../concept_manifestation/service.py | 143 ++ .../a76/manifests/manifest/debug_db.py | 41 + .../v1/modules/a76/manifests/manifest/dtos.py | 66 + .../modules/a76/manifests/manifest/models.py | 52 + .../modules/a76/manifests/manifest/routes.py | 127 ++ .../modules/a76/manifests/manifest/service.py | 192 +++ .../manifests/value_manifestation/__init__.py | 0 .../a76/manifests/value_manifestation/dtos.py | 75 + .../manifests/value_manifestation/models.py | 66 + .../manifests/value_manifestation/service.py | 136 ++ backend/api/v1/modules/a76/router.py | 7 + backend/core/security.py | 12 +- backend/main.py | 3 + check_ports.py | 33 + check_transport_modes.py | 33 + debug_invoices.py | 83 ++ frontend/messages/en.json | 11 + frontend/messages/es.json | 11 + .../lib/api/dashboard/a76/customs-brokers.ts | 81 +- .../a76/general_catalogs/locations.ts | 40 +- .../dashboard/a76/general_catalogs/ports.ts | 72 +- .../src/lib/api/dashboard/a76/invoices.ts | 2 + .../src/lib/api/dashboard/a76/manifests.ts | 117 ++ .../src/lib/api/dashboard/a76/trailers.ts | 40 + .../src/lib/api/dashboard/a76/transporters.ts | 40 + .../src/lib/api/dashboard/a76/vehicles.ts | 32 + .../refrence_data/code_pedimento_regimens.ts | 8 +- .../api/dashboard/refrence_data/containers.ts | 8 +- .../api/dashboard/refrence_data/countries.ts | 10 +- .../dashboard/refrence_data/currency_types.ts | 10 +- .../refrence_data/customs_sections.ts | 10 +- .../refrence_data/customs_warehouses.ts | 10 +- .../api/dashboard/refrence_data/incoterms.ts | 10 +- .../dashboard/refrence_data/invoice_types.ts | 10 +- .../dashboard/refrence_data/material_types.ts | 10 +- .../refrence_data/payment_methods.ts | 10 +- .../refrence_data/pedimento_codes.ts | 10 +- .../refrence_data/pedimento_regimens.ts | 10 +- .../api/dashboard/refrence_data/sectors.ts | 10 +- .../lib/api/dashboard/refrence_data/states.ts | 10 +- .../refrence_data/transport_modes.ts | 29 +- .../refrence_data/transport_types.ts | 29 +- .../refrence_data/valuation_methods.ts | 10 +- .../lib/components/common/Maintenance.svelte | 79 + .../dashboard/export/manifest/columns.ts | 100 ++ .../export/manifest/create-dialog.svelte | 171 +++ .../export/manifest/data-table.svelte | 136 ++ .../export/manifest/delete-dialog.svelte | 77 + .../export/manifest/details-dialog.svelte | 164 ++ .../export/manifest/edit-dialog.svelte | 173 +++ .../manifest/invoice-selection-table.svelte | 123 ++ .../export/manifest/manifest-form.svelte | 1326 +++++++++++++++++ .../modals/broker-selector-dialog.svelte | 125 ++ .../modals/port-selector-dialog.svelte | 136 ++ .../modals/trailer-selector-dialog.svelte | 132 ++ .../transport-mode-selector-dialog.svelte | 115 ++ .../transport-type-selector-dialog.svelte | 121 ++ .../modals/transporter-selector-dialog.svelte | 124 ++ .../modals/vehicle-selector-dialog.svelte | 127 ++ .../modales/client-selector-dialog.svelte | 291 ++-- .../src/lib/components/sidebar/modules.ts | 39 + .../dashboard/export/catalog/+page.svelte | 5 + .../dashboard/export/destruction/+page.svelte | 5 + .../dashboard/export/manifest/+page.server.ts | 94 ++ .../dashboard/export/manifest/+page.svelte | 267 ++++ .../export/manifest/edit/[[id]]/+page.svelte | 9 + .../dashboard/export/repair/+page.svelte | 5 + .../dashboard/export/reports/+page.svelte | 5 + .../export/used-materials/+page.svelte | 5 + 79 files changed, 5614 insertions(+), 347 deletions(-) create mode 100644 backend/api/v1/modules/a76/manifests/__init__.py create mode 100644 backend/api/v1/modules/a76/manifests/concept_manifestation/__init__.py create mode 100644 backend/api/v1/modules/a76/manifests/concept_manifestation/dtos.py create mode 100644 backend/api/v1/modules/a76/manifests/concept_manifestation/models.py create mode 100644 backend/api/v1/modules/a76/manifests/concept_manifestation/routes.py create mode 100644 backend/api/v1/modules/a76/manifests/concept_manifestation/service.py create mode 100644 backend/api/v1/modules/a76/manifests/manifest/debug_db.py create mode 100644 backend/api/v1/modules/a76/manifests/manifest/dtos.py create mode 100644 backend/api/v1/modules/a76/manifests/manifest/models.py create mode 100644 backend/api/v1/modules/a76/manifests/manifest/routes.py create mode 100644 backend/api/v1/modules/a76/manifests/manifest/service.py create mode 100644 backend/api/v1/modules/a76/manifests/value_manifestation/__init__.py create mode 100644 backend/api/v1/modules/a76/manifests/value_manifestation/dtos.py create mode 100644 backend/api/v1/modules/a76/manifests/value_manifestation/models.py create mode 100644 backend/api/v1/modules/a76/manifests/value_manifestation/service.py create mode 100644 check_ports.py create mode 100644 check_transport_modes.py create mode 100644 debug_invoices.py create mode 100644 frontend/src/lib/api/dashboard/a76/manifests.ts create mode 100644 frontend/src/lib/api/dashboard/a76/trailers.ts create mode 100644 frontend/src/lib/api/dashboard/a76/transporters.ts create mode 100644 frontend/src/lib/api/dashboard/a76/vehicles.ts create mode 100644 frontend/src/lib/components/common/Maintenance.svelte create mode 100644 frontend/src/lib/components/dashboard/export/manifest/columns.ts create mode 100644 frontend/src/lib/components/dashboard/export/manifest/create-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/export/manifest/data-table.svelte create mode 100644 frontend/src/lib/components/dashboard/export/manifest/delete-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/export/manifest/details-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/export/manifest/edit-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/export/manifest/invoice-selection-table.svelte create mode 100644 frontend/src/lib/components/dashboard/export/manifest/manifest-form.svelte create mode 100644 frontend/src/lib/components/dashboard/export/manifest/modals/broker-selector-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/export/manifest/modals/port-selector-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/export/manifest/modals/trailer-selector-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/export/manifest/modals/transport-mode-selector-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/export/manifest/modals/transport-type-selector-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/export/manifest/modals/transporter-selector-dialog.svelte create mode 100644 frontend/src/lib/components/dashboard/export/manifest/modals/vehicle-selector-dialog.svelte create mode 100644 frontend/src/routes/dashboard/export/catalog/+page.svelte create mode 100644 frontend/src/routes/dashboard/export/destruction/+page.svelte create mode 100644 frontend/src/routes/dashboard/export/manifest/+page.server.ts create mode 100644 frontend/src/routes/dashboard/export/manifest/+page.svelte create mode 100644 frontend/src/routes/dashboard/export/manifest/edit/[[id]]/+page.svelte create mode 100644 frontend/src/routes/dashboard/export/repair/+page.svelte create mode 100644 frontend/src/routes/dashboard/export/reports/+page.svelte create mode 100644 frontend/src/routes/dashboard/export/used-materials/+page.svelte diff --git a/backend/api/v1/common/tenant_crud_routes.py b/backend/api/v1/common/tenant_crud_routes.py index 78e213d8..91b23321 100644 --- a/backend/api/v1/common/tenant_crud_routes.py +++ b/backend/api/v1/common/tenant_crud_routes.py @@ -1,4 +1,5 @@ from typing import Any, Callable, Dict, Generic, Optional, Type, TypeVar, Union +import logging from core.database import get_core_db from core.security import get_current_user, validate_access_to_resource @@ -6,6 +7,8 @@ from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query from pydantic import BaseModel from sqlalchemy.orm import Session +logger = logging.getLogger(__name__) + # Type variables for generic types ModelType = TypeVar("ModelType") CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseModel) @@ -173,6 +176,7 @@ class TenantCRUDRoutes( db, tenant_id, company_id, skip, page_size, filters ) + return { "items": [ self.response_schema.model_validate(item) for item in items diff --git a/backend/api/v1/modules/a76/general_catalogs/ports/service.py b/backend/api/v1/modules/a76/general_catalogs/ports/service.py index 4d4d9cc2..e10f3529 100644 --- a/backend/api/v1/modules/a76/general_catalogs/ports/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/ports/service.py @@ -17,8 +17,7 @@ class PortService: filters: Optional[Dict[str, Any]] = None ) -> Tuple[List[Port], int]: query = db.query(Port).filter( - Port.tenant_id == tenant_id, - Port.company_id == company_id + Port.tenant_id == tenant_id ) if filters: @@ -38,8 +37,7 @@ class PortService: ) -> Optional[Port]: return db.query(Port).filter( Port.id == id, - Port.tenant_id == tenant_id, - Port.company_id == company_id + Port.tenant_id == tenant_id ).first() @staticmethod diff --git a/backend/api/v1/modules/a76/invoices/routes.py b/backend/api/v1/modules/a76/invoices/routes.py index ceb430d9..05bd8d89 100644 --- a/backend/api/v1/modules/a76/invoices/routes.py +++ b/backend/api/v1/modules/a76/invoices/routes.py @@ -21,7 +21,7 @@ invoice_crud = TenantCRUDRoutes( resource_name="Invoice", id_name="invoice_id", id_type=int, - enable_list=True, # Enable list endpoint with pagination + enable_list=False, # Disable auto-list to override with custom filter enable_filters=True, # Enable filters for status, operation_type, etc. list_permissions=[], get_permissions=[], @@ -36,6 +36,53 @@ invoice_crud = TenantCRUDRoutes( router.include_router(invoice_crud.router) +@router.get("/invoices/", response_model=schemas.InvoiceHeaderListResponse) +def list_invoices( + company_id: int = Query(..., description="Company ID"), + page: int = Query(1, ge=1, description="Page number"), + page_size: int = Query(50, ge=1, le=200, description="Items per page"), + search: str = Query(None, description="Search by invoice number"), + status: bool = Query(None, description="Filter by status"), + operation_type: schemas.OperationType = Query(None, description="Filter by operation type"), + invoice_type: str = Query(None, description="Filter by invoice type"), + manifest_number: str = Query(None, description="Filter by manifest number"), + pedimento: str = Query(None, description="Filter by pedimento"), + db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), +): + """ + List invoices with optional filters, including manifest_number. + """ + print(f"DEBUG: list_invoices called with manifest_number={manifest_number}") + tenant_id = validate_access_to_resource(db, company_id, current_user) + print(f"DEBUG: tenant_id={tenant_id}, company_id={company_id}") + + skip = (page - 1) * page_size + filters = { + "invoice_number": search, + "status": status, + "operation_type": operation_type, + "invoice_type": invoice_type, + "manifest_number": manifest_number, + "pedimento": pedimento, + } + + # Remove None values + filters = {k: v for k, v in filters.items() if v is not None} + + items, total = services.InvoiceService.get_all( + db, tenant_id, company_id, skip=skip, limit=page_size, filters=filters + ) + print(f"DEBUG: InvoiceService returned {len(items)} items, total={total}") + + return { + "items": items, + "total": total, + "page": page, + "page_size": page_size + } + + # Additional nested routes for child resources # --- Logistics Routes --- diff --git a/backend/api/v1/modules/a76/invoices/schemas.py b/backend/api/v1/modules/a76/invoices/schemas.py index a1324e69..77312a77 100644 --- a/backend/api/v1/modules/a76/invoices/schemas.py +++ b/backend/api/v1/modules/a76/invoices/schemas.py @@ -426,10 +426,11 @@ class InvoiceHeaderCreate(InvoiceHeaderBase): # --- Update Schemas --- + class InvoiceComplianceMxUpdate(InvoiceComplianceMxBase): """Schema for updating Compliance MX""" - pass + is_pedimento_pending: Optional[bool] = None class InvoiceFinancialsUpdate(InvoiceFinancialsBase): @@ -460,6 +461,10 @@ class InvoiceHeaderUpdate(InvoiceHeaderBase): """Schema for updating Invoice Header with nested relations""" id: int + document_type: Optional[str] = None + invoice_date: Optional[date] = None + operation_type: Optional[OperationType] = None + compliance_mx: Optional[InvoiceComplianceMxUpdate] = None financials: Optional[InvoiceFinancialsUpdate] = None logistics: Optional[InvoiceLogisticsUpdate] = None @@ -546,3 +551,10 @@ class InvoiceHeaderResponse(InvoiceHeaderBase): class Config: from_attributes = True + + +class InvoiceHeaderListResponse(BaseModel): + items: List[InvoiceHeaderResponse] + total: int + page: int + page_size: int diff --git a/backend/api/v1/modules/a76/invoices/services.py b/backend/api/v1/modules/a76/invoices/services.py index 7042cca8..8ef3964e 100644 --- a/backend/api/v1/modules/a76/invoices/services.py +++ b/backend/api/v1/modules/a76/invoices/services.py @@ -73,6 +73,16 @@ class InvoiceService: ): query = query.filter(models.InvoiceHeader.operation_type != "REPAR") + if filters.get("manifest_number"): + # Avoid duplicate joins if pedimento filter was also applied (though rare in this context) + # For safety, we can just use the relationship attribute directly if mapped, + # but explicit join is clearer given the previous pattern. + # Assuming SQLAlchemy handles the join overlap or we just accept it for now. + # To be safe and consistent with previous 'pedimento' block: + query = query.join(models.InvoiceComplianceMx).filter( + models.InvoiceComplianceMx.manifest_number.ilike(f"%{filters['manifest_number']}%") + ) + total = query.count() items = query.offset(skip).limit(limit).all() return items, total diff --git a/backend/api/v1/modules/a76/manifests/__init__.py b/backend/api/v1/modules/a76/manifests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/api/v1/modules/a76/manifests/concept_manifestation/__init__.py b/backend/api/v1/modules/a76/manifests/concept_manifestation/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/api/v1/modules/a76/manifests/concept_manifestation/dtos.py b/backend/api/v1/modules/a76/manifests/concept_manifestation/dtos.py new file mode 100644 index 00000000..5c88df41 --- /dev/null +++ b/backend/api/v1/modules/a76/manifests/concept_manifestation/dtos.py @@ -0,0 +1,48 @@ +from decimal import Decimal +from typing import List, Optional + +from pydantic import BaseModel, Field + + +class ConceptManifestationBaseDTO(BaseModel): + value_manifestation_id: int = Field(..., description="Value Manifestation ID") + line_number: int = Field(..., description="Line number") + attachment_type: Optional[str] = Field(None, max_length=10, description="Attachment type") + number: Optional[str] = Field(None, max_length=10, description="Number") + merchandise_provider: Optional[str] = Field(None, max_length=100, description="Merchandise provider") + invoice_document: Optional[str] = Field(None, max_length=200, description="Invoice document") + amount: Optional[Decimal] = Field(None, description="Amount") + currency: Optional[str] = Field(None, max_length=3, description="Currency") + concept_load: Optional[str] = Field(None, max_length=200, description="Concept load") + + +class ConceptManifestationCreateDTO(ConceptManifestationBaseDTO): + pass + + +class ConceptManifestationUpdateDTO(BaseModel): + attachment_type: Optional[str] = Field(None, max_length=10, description="Attachment type") + number: Optional[str] = Field(None, max_length=10, description="Number") + merchandise_provider: Optional[str] = Field(None, max_length=100, description="Merchandise provider") + invoice_document: Optional[str] = Field(None, max_length=200, description="Invoice document") + amount: Optional[Decimal] = Field(None, description="Amount") + currency: Optional[str] = Field(None, max_length=3, description="Currency") + concept_load: Optional[str] = Field(None, max_length=200, description="Concept load") + + +class ConceptManifestationResponseDTO(ConceptManifestationBaseDTO): + tenant_id: int + company_id: int + + class Config: + from_attributes = True + + +class ConceptManifestationListDTO(BaseModel): + items: List[ConceptManifestationResponseDTO] + total: int + page: int + page_size: int + + class Config: + from_attributes = True diff --git a/backend/api/v1/modules/a76/manifests/concept_manifestation/models.py b/backend/api/v1/modules/a76/manifests/concept_manifestation/models.py new file mode 100644 index 00000000..0242102f --- /dev/null +++ b/backend/api/v1/modules/a76/manifests/concept_manifestation/models.py @@ -0,0 +1,45 @@ +from decimal import Decimal +from typing import TYPE_CHECKING, Optional + +from api.v1.common.base_models import TenantScopedMixin, TimestampMixin +from core.database import Base +from sqlalchemy import ( + ForeignKeyConstraint, + Index, + Integer, + Numeric, + PrimaryKeyConstraint, + String, +) +from sqlalchemy.orm import Mapped, mapped_column, relationship + +if TYPE_CHECKING: + from api.v1.modules.a76.manifests.value_manifestation.models import ValueManifestation + +class ConceptManifestation(Base, TenantScopedMixin, TimestampMixin): + __tablename__ = "concept_manifestations" + __table_args__ = ( + PrimaryKeyConstraint("value_manifestation_id", "line_number", name="concept_manifestations_pkey"), + ForeignKeyConstraint( + ["value_manifestation_id"], + ["a76.value_manifestations.id"], + name="fk_concept_manifestation_value_manifestation" + ), + Index("idx_concept_manifestations_value_manifestation_id", "value_manifestation_id"), + {"schema": "a76"}, + ) + + value_manifestation_id: Mapped[int] = mapped_column(Integer) + line_number: Mapped[int] = mapped_column(Integer) + attachment_type: Mapped[Optional[str]] = mapped_column(String(10), nullable=True) + number: Mapped[Optional[str]] = mapped_column(String(10), nullable=True) + merchandise_provider: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) + invoice_document: Mapped[Optional[str]] = mapped_column(String(200), nullable=True) + amount: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 9), nullable=True) + currency: Mapped[Optional[str]] = mapped_column(String(3), nullable=True) + concept_load: Mapped[Optional[str]] = mapped_column(String(200), nullable=True) + + value_manifestation: Mapped["ValueManifestation"] = relationship( + "ValueManifestation", + backref="concepts" + ) diff --git a/backend/api/v1/modules/a76/manifests/concept_manifestation/routes.py b/backend/api/v1/modules/a76/manifests/concept_manifestation/routes.py new file mode 100644 index 00000000..e45543e6 --- /dev/null +++ b/backend/api/v1/modules/a76/manifests/concept_manifestation/routes.py @@ -0,0 +1,112 @@ +from typing import Optional + +from fastapi import APIRouter, Depends, Query, HTTPException +from sqlalchemy.orm import Session + +from core.database import get_core_db +from core.security import get_current_user, validate_access_to_resource +from .dtos import ( + ConceptManifestationCreateDTO, + ConceptManifestationUpdateDTO, + ConceptManifestationResponseDTO, + ConceptManifestationListDTO, +) +from .service import ConceptManifestationService + +router = APIRouter(prefix="/concept-manifestations") + + +@router.get("/", response_model=ConceptManifestationListDTO) +async def list_concept_manifestations( + company_id: int = Query(..., description="Company ID"), + value_manifestation_id: Optional[int] = Query( + None, description="Filter by Value Manifestation ID" + ), + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=1000), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): + tenant_id = validate_access_to_resource(db, company_id, current_user) + filters = {} + if value_manifestation_id: + filters["value_manifestation_id"] = value_manifestation_id + + items, total = ConceptManifestationService.get_all( + db, tenant_id, company_id, skip=skip, limit=limit, filters=filters + ) + return { + "items": [ConceptManifestationResponseDTO.model_validate(item) for item in items], + "total": total, + "page": (skip // limit) + 1, + "page_size": limit, + } + + +@router.post("/", response_model=ConceptManifestationResponseDTO) +async def create_concept_manifestation( + data: ConceptManifestationCreateDTO, + company_id: int = Query(..., description="Company ID"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): + tenant_id = validate_access_to_resource(db, company_id, current_user) + return ConceptManifestationService.create(db, data, tenant_id, company_id) + + +@router.get( + "/{value_manifestation_id}/{line_number}", + response_model=ConceptManifestationResponseDTO, +) +async def get_concept_manifestation( + value_manifestation_id: int, + line_number: int, + company_id: int = Query(..., description="Company ID"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): + tenant_id = validate_access_to_resource(db, company_id, current_user) + item = ConceptManifestationService.get_by_id( + db, value_manifestation_id, line_number, tenant_id, company_id + ) + if not item: + raise HTTPException(status_code=404, detail="Concept Manifestation not found") + return item + + +@router.patch( + "/{value_manifestation_id}/{line_number}", + response_model=ConceptManifestationResponseDTO, +) +async def update_concept_manifestation( + value_manifestation_id: int, + line_number: int, + data: ConceptManifestationUpdateDTO, + company_id: int = Query(..., description="Company ID"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): + tenant_id = validate_access_to_resource(db, company_id, current_user) + item = ConceptManifestationService.update( + db, value_manifestation_id, line_number, tenant_id, company_id, data + ) + if not item: + raise HTTPException(status_code=404, detail="Concept Manifestation not found") + return item + + +@router.delete("/{value_manifestation_id}/{line_number}", response_model=bool) +async def delete_concept_manifestation( + value_manifestation_id: int, + line_number: int, + company_id: int = Query(..., description="Company ID"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): + tenant_id = validate_access_to_resource(db, company_id, current_user) + success = ConceptManifestationService.delete( + db, value_manifestation_id, line_number, tenant_id, company_id + ) + if not success: + raise HTTPException(status_code=404, detail="Concept Manifestation not found") + return success diff --git a/backend/api/v1/modules/a76/manifests/concept_manifestation/service.py b/backend/api/v1/modules/a76/manifests/concept_manifestation/service.py new file mode 100644 index 00000000..b0ee853a --- /dev/null +++ b/backend/api/v1/modules/a76/manifests/concept_manifestation/service.py @@ -0,0 +1,143 @@ +import logging +from typing import List, Optional, Dict, Any, Tuple + +from fastapi import HTTPException +from sqlalchemy import or_ +from sqlalchemy.orm import Session + +from .models import ConceptManifestation +from .dtos import ( + ConceptManifestationCreateDTO, + ConceptManifestationUpdateDTO, +) + +logger = logging.getLogger(__name__) + + +class ConceptManifestationService: + @staticmethod + def get_all( + db: Session, + tenant_id: int, + company_id: int, + skip: int = 0, + limit: int = 100, + filters: Optional[Dict[str, Any]] = None, + ) -> Tuple[List[ConceptManifestation], int]: + query = db.query(ConceptManifestation).filter( + ConceptManifestation.tenant_id == tenant_id, + ConceptManifestation.company_id == company_id, + ) + + if filters: + if filters.get("value_manifestation_id"): + query = query.filter( + ConceptManifestation.value_manifestation_id == filters["value_manifestation_id"] + ) + + total = query.count() + items = query.offset(skip).limit(limit).all() + + return items, total + + @staticmethod + def get_by_id( + db: Session, + value_manifestation_id: int, + line_number: int, + tenant_id: int, + company_id: int, + ) -> Optional[ConceptManifestation]: + return ( + db.query(ConceptManifestation) + .filter( + ConceptManifestation.value_manifestation_id == value_manifestation_id, + ConceptManifestation.line_number == line_number, + ConceptManifestation.tenant_id == tenant_id, + ConceptManifestation.company_id == company_id, + ) + .first() + ) + + @staticmethod + def create( + db: Session, + data: ConceptManifestationCreateDTO, + tenant_id: int, + company_id: int, + ) -> ConceptManifestation: + try: + db_item = ConceptManifestation( + **data.model_dump(exclude_unset=True), + tenant_id=tenant_id, + company_id=company_id, + ) + db.add(db_item) + db.commit() + db.refresh(db_item) + return db_item + except Exception as e: + db.rollback() + logger.error(f"Error creating concept manifestation: {str(e)}") + raise HTTPException( + status_code=500, detail="Error creating concept manifestation" + ) + + @staticmethod + def update( + db: Session, + value_manifestation_id: int, + line_number: int, + tenant_id: int, + company_id: int, + data: ConceptManifestationUpdateDTO, + ) -> Optional[ConceptManifestation]: + item = ConceptManifestationService.get_by_id( + db, value_manifestation_id, line_number, tenant_id, company_id + ) + if not item: + return None + + try: + update_data = data.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(item, field, value) + + db.commit() + db.refresh(item) + return item + except Exception as e: + db.rollback() + logger.error( + f"Error updating concept manifestation {value_manifestation_id}-{line_number}: {str(e)}" + ) + raise HTTPException( + status_code=500, detail="Error updating concept manifestation" + ) + + @staticmethod + def delete( + db: Session, + value_manifestation_id: int, + line_number: int, + tenant_id: int, + company_id: int, + ) -> bool: + item = ConceptManifestationService.get_by_id( + db, value_manifestation_id, line_number, tenant_id, company_id + ) + if not item: + return False + + try: + db.delete(item) + db.commit() + return True + except Exception as e: + db.rollback() + logger.error( + f"Error deleting concept manifestation {value_manifestation_id}-{line_number}: {str(e)}" + ) + raise HTTPException( + status_code=500, detail="Error deleting concept manifestation" + ) diff --git a/backend/api/v1/modules/a76/manifests/manifest/debug_db.py b/backend/api/v1/modules/a76/manifests/manifest/debug_db.py new file mode 100644 index 00000000..e191fd3c --- /dev/null +++ b/backend/api/v1/modules/a76/manifests/manifest/debug_db.py @@ -0,0 +1,41 @@ + +import sys +import os +from sqlalchemy import text, inspect + +# Assume we run this from backend/ with PYTHONPATH=. +from core.database import CoreSessionLocal, core_engine as engine + +def check_table(): + inspector = inspect(engine) + schemas = inspector.get_schema_names() + print(f"Schemas found: {schemas}") + + if 'a76' not in schemas: + print("Schema 'a76' does not exist!") + # return # Proceed anyway to check if table exists in public or other schemas? + + # Check for table in a76 schema + try: + table_names = inspector.get_table_names(schema='a76') + print(f"Tables in 'a76': {table_names}") + + if 'manifests' in table_names: + print("Table 'a76.manifests' exists.") + columns = [c['name'] for c in inspector.get_columns('manifests', schema='a76')] + print(f"Columns: {columns}") + + # Try a simple count + try: + with CoreSessionLocal() as db: + result = db.execute(text("SELECT count(*) FROM a76.manifests")) + print(f"Count result: {result.scalar()}") + except Exception as e: + print(f"Error querying table: {e}") + else: + print("Table 'a76.manifests' DOES NOT exist in schema 'a76'.") + except Exception as e: + print(f"Error inspecting schema 'a76': {e}") + +if __name__ == "__main__": + check_table() diff --git a/backend/api/v1/modules/a76/manifests/manifest/dtos.py b/backend/api/v1/modules/a76/manifests/manifest/dtos.py new file mode 100644 index 00000000..ae4675a1 --- /dev/null +++ b/backend/api/v1/modules/a76/manifests/manifest/dtos.py @@ -0,0 +1,66 @@ +from decimal import Decimal +from typing import List, Optional + +from pydantic import BaseModel, Field + + +class ManifestBaseDTO(BaseModel): + manifest_number: Optional[str] = Field(None, max_length=15, description="Manifest number") + importer_details: Optional[str] = Field(None, max_length=60, description="Importer details") + person_in_charge: Optional[str] = Field(None, max_length=60, description="Person in charge") + consigned_to: Optional[str] = Field(None, max_length=8, description="Consigned to") + sent_by: Optional[str] = Field(None, max_length=8, description="Sent by") + foreign_exit_port: Optional[str] = Field(None, max_length=6, description="Foreign exit port") + foreign_exit_port_loc: Optional[str] = Field(None, max_length=4, description="Foreign exit port location") + destination_port: Optional[str] = Field(None, max_length=6, description="Destination port") + destination_port_loc: Optional[str] = Field(None, max_length=4, description="Destination port location") + entry_port: Optional[str] = Field(None, max_length=6, description="Entry port") + entry_port_loc: Optional[str] = Field(None, max_length=4, description="Entry port location") + entry_date: Optional[int] = Field(None, description="Entry date") + net_weight: Optional[Decimal] = Field(None, description="Net weight") + gross_weight: Optional[Decimal] = Field(None, description="Gross weight") + total_value: Optional[Decimal] = Field(None, description="Total value") + description: Optional[str] = Field(None, max_length=2500, description="Description") + broker_code: Optional[str] = Field(None, max_length=5, description="Broker code") + carrier_code: Optional[str] = Field(None, max_length=5, description="Carrier code") + payment_invoice_number: Optional[str] = Field(None, max_length=5, description="Payment invoice number") + seal_number: Optional[str] = Field(None, max_length=30, description="Seal number") + entry_hour: Optional[int] = Field(None, description="Entry hour") + hazardous_material: Optional[str] = Field(None, max_length=2, description="Hazardous material") + transport_mode: Optional[str] = Field(None, max_length=2, description="Transport mode") + transport_code: Optional[str] = Field(None, max_length=14, description="Transport code") + trailer_number: Optional[str] = Field(None, max_length=20, description="Trailer number") + status: Optional[str] = Field(None, max_length=14, description="Status") + status_description: Optional[str] = Field(None, max_length=1000, description="Status description") + manifest_type: Optional[str] = Field(None, max_length=3, description="Manifest type") + + +class ManifestCreateDTO(ManifestBaseDTO): + pass + + +class ManifestUpdateDTO(ManifestBaseDTO): + pass + + +class ManifestResponseDTO(ManifestBaseDTO): + id: int + tenant_id: int + company_id: int + + class Config: + from_attributes = True + + +class ManifestListDTO(BaseModel): + items: List[ManifestResponseDTO] + total: int + page: int + page_size: int + + class Config: + from_attributes = True + + +class InvoiceManifestUpdateDTO(BaseModel): + manifest_number: Optional[str] = Field(None, max_length=15, description="Manifest number") diff --git a/backend/api/v1/modules/a76/manifests/manifest/models.py b/backend/api/v1/modules/a76/manifests/manifest/models.py new file mode 100644 index 00000000..2f97d550 --- /dev/null +++ b/backend/api/v1/modules/a76/manifests/manifest/models.py @@ -0,0 +1,52 @@ +from decimal import Decimal +from typing import Optional + +from api.v1.common.base_models import TenantScopedMixin, TimestampMixin +from core.database import Base +from sqlalchemy import ( + Index, + Integer, + Numeric, + PrimaryKeyConstraint, + String, + Text, +) +from sqlalchemy.orm import Mapped, mapped_column + +class Manifest(Base, TenantScopedMixin, TimestampMixin): + __tablename__ = "manifests" + __table_args__ = ( + Index("idx_manifests_manifest_number", "manifest_number"), + {"schema": "a76"}, + ) + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + + manifest_number: Mapped[str] = mapped_column(String(15), nullable=True) + importer_details: Mapped[Optional[str]] = mapped_column(String(60), nullable=True) + person_in_charge: Mapped[Optional[str]] = mapped_column(String(60), nullable=True) + consigned_to: Mapped[Optional[str]] = mapped_column(String(8), nullable=True) + sent_by: Mapped[Optional[str]] = mapped_column(String(8), nullable=True) + foreign_exit_port: Mapped[Optional[str]] = mapped_column(String(6), nullable=True) + foreign_exit_port_loc: Mapped[Optional[str]] = mapped_column(String(4), nullable=True) + destination_port: Mapped[Optional[str]] = mapped_column(String(6), nullable=True) + destination_port_loc: Mapped[Optional[str]] = mapped_column(String(4), nullable=True) + entry_port: Mapped[Optional[str]] = mapped_column(String(6), nullable=True) + entry_port_loc: Mapped[Optional[str]] = mapped_column(String(4), nullable=True) + entry_date: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + net_weight: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8), nullable=True) + gross_weight: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8), nullable=True) + total_value: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8), nullable=True) + description: Mapped[Optional[str]] = mapped_column(String(2500), nullable=True) + broker_code: Mapped[Optional[str]] = mapped_column(String(5), nullable=True) + carrier_code: Mapped[Optional[str]] = mapped_column(String(5), nullable=True) + payment_invoice_number: Mapped[Optional[str]] = mapped_column(String(5), nullable=True) + seal_number: Mapped[Optional[str]] = mapped_column(String(30), nullable=True) + entry_hour: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + hazardous_material: Mapped[Optional[str]] = mapped_column(String(2), nullable=True) + transport_mode: Mapped[Optional[str]] = mapped_column(String(2), nullable=True) + transport_code: Mapped[Optional[str]] = mapped_column(String(14), nullable=True) + trailer_number: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) + status: Mapped[Optional[str]] = mapped_column(String(14), nullable=True) + status_description: Mapped[Optional[str]] = mapped_column(String(1000), nullable=True) + manifest_type: Mapped[Optional[str]] = mapped_column(String(3), nullable=True) diff --git a/backend/api/v1/modules/a76/manifests/manifest/routes.py b/backend/api/v1/modules/a76/manifests/manifest/routes.py new file mode 100644 index 00000000..f6590c62 --- /dev/null +++ b/backend/api/v1/modules/a76/manifests/manifest/routes.py @@ -0,0 +1,127 @@ +from typing import List, Optional, Dict, Any + +from fastapi import APIRouter, Depends, Query, HTTPException +from sqlalchemy.orm import Session + +from core.database import get_core_db +from core.security import get_current_user, validate_access_to_resource +from .dtos import ( + ManifestCreateDTO, + ManifestUpdateDTO, + ManifestResponseDTO, + ManifestListDTO, + InvoiceManifestUpdateDTO, +) +from .service import ManifestService + +router = APIRouter(prefix="/manifests") + + +@router.get("/", response_model=ManifestListDTO) +async def list_manifests( + company_id: int = Query(..., description="Company ID"), + search: Optional[str] = Query(None, description="Search term"), + manifest_number: Optional[str] = Query(None, description="Manifest number"), + start_date: Optional[int] = Query(None, description="Start date (YYYYMMDD)"), + end_date: Optional[int] = Query(None, description="End date (YYYYMMDD)"), + skip: int = Query(0, ge=0, description="Skip"), + limit: int = Query(50, ge=1, le=100, description="Limit"), + db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), +): + """ + List all manifests for a company with pagination and filters + """ + tenant_id = validate_access_to_resource(db, company_id, current_user) + filters = { + "search": search, + "manifest_number": manifest_number, + "start_date": start_date, + "end_date": end_date, + } + items, total = ManifestService.get_all( + db, tenant_id, company_id, skip=skip, limit=limit, filters=filters + ) + return { + "items": [ManifestResponseDTO.model_validate(item) for item in items], + "total": total, + "page": (skip // limit) + 1, + "page_size": limit, + } + + +@router.post("/", response_model=ManifestResponseDTO) +async def create_manifest( + manifest_data: ManifestCreateDTO, + company_id: int = Query(..., description="Company ID"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): + tenant_id = validate_access_to_resource(db, company_id, current_user) + return ManifestService.create(db, manifest_data, tenant_id, company_id) + + +@router.get("/{manifest_id}", response_model=ManifestResponseDTO) +async def get_manifest( + manifest_id: int, + company_id: int = Query(..., description="Company ID"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): + tenant_id = validate_access_to_resource(db, company_id, current_user) + manifest = ManifestService.get_by_id(db, manifest_id, tenant_id, company_id) + if not manifest: + raise HTTPException(status_code=404, detail="Manifest not found") + return manifest + + +@router.patch("/{manifest_id}", response_model=ManifestResponseDTO) +async def update_manifest( + manifest_id: int, + manifest_data: ManifestUpdateDTO, + company_id: int = Query(..., description="Company ID"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): + tenant_id = validate_access_to_resource(db, company_id, current_user) + manifest = ManifestService.update( + db, manifest_id, tenant_id, company_id, manifest_data + ) + if not manifest: + raise HTTPException(status_code=404, detail="Manifest not found") + return manifest + + +@router.delete("/{manifest_id}", response_model=bool) +async def delete_manifest( + manifest_id: int, + company_id: int = Query(..., description="Company ID"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): + tenant_id = validate_access_to_resource(db, company_id, current_user) + success = ManifestService.delete(db, manifest_id, tenant_id, company_id) + if not success: + raise HTTPException(status_code=404, detail="Manifest not found") + return success + + +@router.patch("/invoices/{invoice_id}/compliance", response_model=bool) +async def update_invoice_manifest( + invoice_id: int, + data: InvoiceManifestUpdateDTO, + company_id: int = Query(..., description="Company ID"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): + """ + Dedicated endpoint to update manifest_number for an invoice. + Safely bypasses all core invoice update validations. + """ + tenant_id = validate_access_to_resource(db, company_id, current_user) + success = ManifestService.update_invoice_manifest( + db, invoice_id, tenant_id, company_id, data.manifest_number + ) + if not success: + raise HTTPException(status_code=404, detail="Invoice not found") + return success diff --git a/backend/api/v1/modules/a76/manifests/manifest/service.py b/backend/api/v1/modules/a76/manifests/manifest/service.py new file mode 100644 index 00000000..b0045ce9 --- /dev/null +++ b/backend/api/v1/modules/a76/manifests/manifest/service.py @@ -0,0 +1,192 @@ +import logging +from typing import List, Optional, Dict, Any, Tuple + +from fastapi import HTTPException +from sqlalchemy import or_ +from sqlalchemy.orm import Session + +from .models import Manifest +from .dtos import ManifestCreateDTO, ManifestUpdateDTO, ManifestResponseDTO + +logger = logging.getLogger(__name__) + + +class ManifestService: + @staticmethod + def get_all( + db: Session, + tenant_id: int, + company_id: int, + skip: int = 0, + limit: int = 100, + filters: Optional[Dict[str, Any]] = None, + ) -> Tuple[List[Manifest], int]: + query = db.query(Manifest).filter( + Manifest.tenant_id == tenant_id, + Manifest.company_id == company_id, + ) + + if filters: + if filters.get("search"): + search_pattern = f"%{filters['search']}%" + query = query.filter( + or_( + Manifest.manifest_number.ilike(search_pattern), + Manifest.description.ilike(search_pattern), + ) + ) + + if filters.get("manifest_number"): + query = query.filter( + Manifest.manifest_number.ilike(f"%{filters['manifest_number']}%") + ) + + if filters.get("start_date"): + query = query.filter(Manifest.entry_date >= filters["start_date"]) + + if filters.get("end_date"): + query = query.filter(Manifest.entry_date <= filters["end_date"]) + + total = query.count() + items = query.offset(skip).limit(limit).all() + + return items, total + + @staticmethod + def get_by_id( + db: Session, manifest_id: int, tenant_id: int, company_id: int + ) -> Optional[Manifest]: + return ( + db.query(Manifest) + .filter( + Manifest.id == manifest_id, + Manifest.tenant_id == tenant_id, + Manifest.company_id == company_id, + ) + .first() + ) + + @staticmethod + def create( + db: Session, + manifest_data: ManifestCreateDTO, + tenant_id: int, + company_id: int, + ) -> Manifest: + try: + db_manifest = Manifest( + **manifest_data.model_dump(exclude_unset=True), + tenant_id=tenant_id, + company_id=company_id, + ) + db.add(db_manifest) + db.commit() + db.refresh(db_manifest) + return db_manifest + except Exception as e: + db.rollback() + logger.error(f"Error creating manifest: {str(e)}") + raise HTTPException(status_code=500, detail="Error creating manifest") + + @staticmethod + def update( + db: Session, + manifest_id: int, + tenant_id: int, + company_id: int, + manifest_data: ManifestUpdateDTO, + ) -> Optional[Manifest]: + manifest = ManifestService.get_by_id(db, manifest_id, tenant_id, company_id) + if not manifest: + return None + + try: + update_data = manifest_data.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(manifest, field, value) + + db.commit() + db.refresh(manifest) + return manifest + except Exception as e: + db.rollback() + logger.error(f"Error updating manifest {manifest_id}: {str(e)}") + raise HTTPException(status_code=500, detail="Error updating manifest") + + @staticmethod + def delete( + db: Session, manifest_id: int, tenant_id: int, company_id: int + ) -> bool: + manifest = ManifestService.get_by_id(db, manifest_id, tenant_id, company_id) + if not manifest: + return False + + try: + db.delete(manifest) + db.commit() + return True + except Exception as e: + db.rollback() + logger.error(f"Error deleting manifest {manifest_id}: {str(e)}") + raise HTTPException(status_code=500, detail="Error deleting manifest") + + @staticmethod + def update_invoice_manifest( + db: Session, + invoice_id: int, + tenant_id: int, + company_id: int, + manifest_number: Optional[str], + ) -> bool: + """ + Update ONLY the manifest_number of an invoice. + Creates compliance_mx record if it doesn't exist. + """ + # Import models here to avoid circular dependencies if any + from api.v1.modules.a76.invoices.models import ( + InvoiceHeader, + InvoiceComplianceMx, + ) + + invoice = ( + db.query(InvoiceHeader) + .filter( + InvoiceHeader.id == invoice_id, + InvoiceHeader.tenant_id == tenant_id, + InvoiceHeader.company_id == company_id, + ) + .first() + ) + + if not invoice: + return False + + try: + if invoice.compliance_mx: + invoice.compliance_mx.manifest_number = manifest_number + else: + # Create new compliance record with defaults for required fields + new_compliance = InvoiceComplianceMx( + invoice_id=invoice.id, + tenant_id=tenant_id, + company_id=company_id, + manifest_number=manifest_number, + # Required fields defaults + is_pedimento_pending=False, + is_regime_change=False, + is_owner_of_goods=False, + generate_balances=False, + is_mixed=False, + ) + db.add(new_compliance) + + db.commit() + return True + except Exception as e: + db.rollback() + logger.error( + f"Error linking invoice {invoice_id} to manifest {manifest_number}: {str(e)}" + ) + raise HTTPException( + status_code=500, detail="Error linking invoice to manifest" + ) diff --git a/backend/api/v1/modules/a76/manifests/value_manifestation/__init__.py b/backend/api/v1/modules/a76/manifests/value_manifestation/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/api/v1/modules/a76/manifests/value_manifestation/dtos.py b/backend/api/v1/modules/a76/manifests/value_manifestation/dtos.py new file mode 100644 index 00000000..37f998ad --- /dev/null +++ b/backend/api/v1/modules/a76/manifests/value_manifestation/dtos.py @@ -0,0 +1,75 @@ +from decimal import Decimal +from typing import List, Optional + +from pydantic import BaseModel, Field + + +class ValueManifestationBaseDTO(BaseModel): + manifestation_number: Optional[str] = Field(None, max_length=100, description="Manifestation number") + pedimento: Optional[str] = Field(None, max_length=15, description="Pedimento") + periodicity: Optional[str] = Field(None, max_length=10, description="Periodicity") + semester: Optional[int] = Field(None, description="Semester") + year: Optional[str] = Field(None, max_length=4, description="Year") + pedimento_type: Optional[str] = Field(None, max_length=3, description="Pedimento type") + aa_code: Optional[str] = Field(None, max_length=5, description="AA code") + patent: Optional[str] = Field(None, max_length=4, description="Patent") + first_name: Optional[str] = Field(None, max_length=80, description="First name") + last_name_paternal: Optional[str] = Field(None, max_length=80, description="Last name paternal") + last_name_maternal: Optional[str] = Field(None, max_length=80, description="Last name maternal") + methods_count: Optional[int] = Field(None, description="Methods count") + merchandise_value_method: Optional[str] = Field(None, max_length=10, description="Merchandise value method") + transaction_value: Optional[int] = Field(None, description="Transaction value") + identical_merchandise_value: Optional[int] = Field(None, description="Identical merchandise value") + similar_merchandise_value: Optional[int] = Field(None, description="Similar merchandise value") + unit_sale_price_value: Optional[int] = Field(None, description="Unit sale price value") + reconstructed_value: Optional[int] = Field(None, description="Reconstructed value") + article_78_value: Optional[int] = Field(None, description="Article 78 value") + provisional_value_declaration: Optional[int] = Field(None, description="Provisional value declaration") + has_attachments: Optional[int] = Field(None, description="Has attachments") + attachment_pages_number: Optional[str] = Field(None, max_length=100, description="Attachment pages number") + transaction_value_paid_price: Optional[Decimal] = Field(None, description="Transaction value paid price") + price_pre_invoice: Optional[int] = Field(None, description="Price pre invoice") + price_other_docs: Optional[int] = Field(None, description="Price other docs") + concept_article_66: Optional[int] = Field(None, description="Concept article 66") + concept_article_66_breakdown: Optional[int] = Field(None, description="Concept article 66 breakdown") + attachment_article_66: Optional[str] = Field(None, max_length=2, description="Attachment article 66") + prepaid_merchandise_article_65: Optional[str] = Field(None, max_length=2, description="Prepaid merchandise article 65") + attachment_article_65: Optional[str] = Field(None, max_length=2, description="Attachment article 65") + tax_base_no_sale: Optional[str] = Field(None, max_length=2, description="Tax base no sale") + exists_circumstances_article_67_71: Optional[str] = Field(None, max_length=2, description="Exists circumstances article 67 71") + customs_value_attachment: Optional[str] = Field(None, max_length=2, description="Customs value attachment") + provisional_value_determination: Optional[str] = Field(None, max_length=2, description="Provisional value determination") + merchandise_value_proof_attachment: Optional[str] = Field(None, max_length=2, description="Merchandise value proof attachment") + legal_rep_rfc: Optional[str] = Field(None, max_length=30, description="Legal rep RFC") + legal_representative: Optional[str] = Field(None, max_length=100, description="Legal representative") + date: Optional[int] = Field(None, description="Date") + selected_invoice: Optional[str] = Field(None, max_length=20, description="Selected invoice") + invoice_option: Optional[str] = Field(None, max_length=3, description="Invoice option") + importer_to_use: Optional[str] = Field(None, max_length=8, description="Importer to use") + + +class ValueManifestationCreateDTO(ValueManifestationBaseDTO): + pass + + +class ValueManifestationUpdateDTO(ValueManifestationBaseDTO): + pass + + +class ValueManifestationResponseDTO(ValueManifestationBaseDTO): + id: int + tenant_id: int + company_id: int + + class Config: + from_attributes = True + + +class ValueManifestationListDTO(BaseModel): + items: List[ValueManifestationResponseDTO] + total: int + page: int + page_size: int + + class Config: + from_attributes = True diff --git a/backend/api/v1/modules/a76/manifests/value_manifestation/models.py b/backend/api/v1/modules/a76/manifests/value_manifestation/models.py new file mode 100644 index 00000000..ae252145 --- /dev/null +++ b/backend/api/v1/modules/a76/manifests/value_manifestation/models.py @@ -0,0 +1,66 @@ +from decimal import Decimal +from typing import Optional + +from api.v1.common.base_models import TenantScopedMixin, TimestampMixin +from core.database import Base +from sqlalchemy import ( + Index, + Integer, + Numeric, + PrimaryKeyConstraint, + SmallInteger, + String, +) +from sqlalchemy.orm import Mapped, mapped_column + + +class ValueManifestation(Base, TenantScopedMixin, TimestampMixin): + __tablename__ = "value_manifestations" + __table_args__ = ( + Index("idx_value_manifestations_manifestation_number", "manifestation_number"), + {"schema": "a76"}, + ) + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + + manifestation_number: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) + pedimento: Mapped[Optional[str]] = mapped_column(String(15), nullable=True) + periodicity: Mapped[Optional[str]] = mapped_column(String(10), nullable=True) + semester: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True) + year: Mapped[Optional[str]] = mapped_column(String(4), nullable=True) + pedimento_type: Mapped[Optional[str]] = mapped_column(String(3), nullable=True) + aa_code: Mapped[Optional[str]] = mapped_column(String(5), nullable=True) + patent: Mapped[Optional[str]] = mapped_column(String(4), nullable=True) + first_name: Mapped[Optional[str]] = mapped_column(String(80), nullable=True) + last_name_paternal: Mapped[Optional[str]] = mapped_column(String(80), nullable=True) + last_name_maternal: Mapped[Optional[str]] = mapped_column(String(80), nullable=True) + methods_count: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + merchandise_value_method: Mapped[Optional[str]] = mapped_column(String(10), nullable=True) + transaction_value: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True) + identical_merchandise_value: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True) + similar_merchandise_value: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True) + unit_sale_price_value: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True) + reconstructed_value: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True) + article_78_value: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True) + provisional_value_declaration: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + has_attachments: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True) + attachment_pages_number: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) + transaction_value_paid_price: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8), nullable=True) + price_pre_invoice: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True) + price_other_docs: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True) + concept_article_66: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True) + concept_article_66_breakdown: Mapped[Optional[int]] = mapped_column(SmallInteger, nullable=True) + attachment_article_66: Mapped[Optional[str]] = mapped_column(String(2), nullable=True) + prepaid_merchandise_article_65: Mapped[Optional[str]] = mapped_column(String(2), nullable=True) + attachment_article_65: Mapped[Optional[str]] = mapped_column(String(2), nullable=True) + tax_base_no_sale: Mapped[Optional[str]] = mapped_column(String(2), nullable=True) + exists_circumstances_article_67_71: Mapped[Optional[str]] = mapped_column(String(2), nullable=True) + customs_value_attachment: Mapped[Optional[str]] = mapped_column(String(2), nullable=True) + provisional_value_determination: Mapped[Optional[str]] = mapped_column(String(2), nullable=True) + merchandise_value_proof_attachment: Mapped[Optional[str]] = mapped_column(String(2), nullable=True) + legal_rep_rfc: Mapped[Optional[str]] = mapped_column(String(30), nullable=True) + legal_representative: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) + date: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) + selected_invoice: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) + invoice_option: Mapped[Optional[str]] = mapped_column(String(3), nullable=True) + importer_to_use: Mapped[Optional[str]] = mapped_column(String(8), nullable=True) diff --git a/backend/api/v1/modules/a76/manifests/value_manifestation/service.py b/backend/api/v1/modules/a76/manifests/value_manifestation/service.py new file mode 100644 index 00000000..7b867837 --- /dev/null +++ b/backend/api/v1/modules/a76/manifests/value_manifestation/service.py @@ -0,0 +1,136 @@ +import logging +from typing import List, Optional, Dict, Any, Tuple + +from fastapi import HTTPException +from sqlalchemy import or_ +from sqlalchemy.orm import Session + +from .models import ValueManifestation +from .dtos import ( + ValueManifestationCreateDTO, + ValueManifestationUpdateDTO, +) + +logger = logging.getLogger(__name__) + + +class ValueManifestationService: + @staticmethod + def get_all( + db: Session, + tenant_id: int, + company_id: int, + skip: int = 0, + limit: int = 100, + filters: Optional[Dict[str, Any]] = None, + ) -> Tuple[List[ValueManifestation], int]: + query = db.query(ValueManifestation).filter( + ValueManifestation.tenant_id == tenant_id, + ValueManifestation.company_id == company_id, + ) + + if filters: + if filters.get("search"): + search_pattern = f"%{filters['search']}%" + query = query.filter( + or_( + ValueManifestation.manifestation_number.ilike(search_pattern), + ValueManifestation.pedimento.ilike(search_pattern), + ) + ) + + total = query.count() + items = query.offset(skip).limit(limit).all() + + return items, total + + @staticmethod + def get_by_id( + db: Session, + value_manifestation_id: int, + tenant_id: int, + company_id: int, + ) -> Optional[ValueManifestation]: + return ( + db.query(ValueManifestation) + .filter( + ValueManifestation.id == value_manifestation_id, + ValueManifestation.tenant_id == tenant_id, + ValueManifestation.company_id == company_id, + ) + .first() + ) + + @staticmethod + def create( + db: Session, + data: ValueManifestationCreateDTO, + tenant_id: int, + company_id: int, + ) -> ValueManifestation: + try: + db_item = ValueManifestation( + **data.model_dump(exclude_unset=True), + tenant_id=tenant_id, + company_id=company_id, + ) + db.add(db_item) + db.commit() + db.refresh(db_item) + return db_item + except Exception as e: + db.rollback() + logger.error(f"Error creating value manifestation: {str(e)}") + raise HTTPException( + status_code=500, detail="Error creating value manifestation" + ) + + @staticmethod + def update( + db: Session, + value_manifestation_id: int, + tenant_id: int, + company_id: int, + data: ValueManifestationUpdateDTO, + ) -> Optional[ValueManifestation]: + item = ValueManifestationService.get_by_id( + db, value_manifestation_id, tenant_id, company_id + ) + if not item: + return None + + try: + update_data = data.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(item, field, value) + + db.commit() + db.refresh(item) + return item + except Exception as e: + db.rollback() + logger.error(f"Error updating value manifestation {value_manifestation_id}: {str(e)}") + raise HTTPException( + status_code=500, detail="Error updating value manifestation" + ) + + @staticmethod + def delete( + db: Session, value_manifestation_id: int, tenant_id: int, company_id: int + ) -> bool: + item = ValueManifestationService.get_by_id( + db, value_manifestation_id, tenant_id, company_id + ) + if not item: + return False + + try: + db.delete(item) + db.commit() + return True + except Exception as e: + db.rollback() + logger.error(f"Error deleting value manifestation {value_manifestation_id}: {str(e)}") + raise HTTPException( + status_code=500, detail="Error deleting value manifestation" + ) diff --git a/backend/api/v1/modules/a76/router.py b/backend/api/v1/modules/a76/router.py index 01c8c57d..5d68a9dc 100644 --- a/backend/api/v1/modules/a76/router.py +++ b/backend/api/v1/modules/a76/router.py @@ -53,6 +53,7 @@ from .reports.importacion.facturas.routes import router as invoices_reports_rout from .reports.importacion.consolidados.routes import router as consolidated_reports_router from .reports.importacion.packing_list.routes import router as packing_list_router from .reports.exportacion.aviso_consolidado.routes import router as aviso_consolidado_export_router +from .manifests.manifest.routes import router as manifests_router @@ -145,4 +146,10 @@ router.include_router( aviso_consolidado_export_router, prefix="/a76/reports/exportacion/aviso_consolidado", tags=["a76 / reports"] +) + +router.include_router( + manifests_router, + prefix="/a76", + tags=["a76 / manifests"] ) \ No newline at end of file diff --git a/backend/core/security.py b/backend/core/security.py index 85111961..0bcde710 100644 --- a/backend/core/security.py +++ b/backend/core/security.py @@ -171,8 +171,9 @@ def validate_company_access( ) return company is not None - finally: - db.close() + except Exception as e: + logger.error(f"Error validating company access: {str(e)}") + return False def validate_access_to_resource( @@ -200,7 +201,12 @@ def validate_access_to_resource( HTTPException: Si no hay tenant_id, no tiene acceso o no tiene los permisos requeridos """ - tenant_id = current_user.get("tenant_id") + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + # Fallback para desarrollo o tokens mal formados que sí tienen el atributo pero en otro lado + # Esto evita el 400 si get_tenant_from_token falla pero el usuario es válido + tenant_id = current_user.get("tenant_id") + if not tenant_id: raise HTTPException(status_code=400, detail="Tenant ID not found in token") diff --git a/backend/main.py b/backend/main.py index b7ed6b69..aaea3aa1 100644 --- a/backend/main.py +++ b/backend/main.py @@ -28,6 +28,9 @@ from api.v1.modules.a76.items.series.models import Serie from api.v1.modules.a76.parts.models import Part from api.v1.modules.a24.fa.fa_parts.models import FaPart from api.v1.modules.a24.inv.inv_parts.models import InvPart +from api.v1.modules.a76.manifests.manifest.models import Manifest +from api.v1.modules.a76.manifests.concept_manifestation.models import ConceptManifestation +from api.v1.modules.a76.manifests.value_manifestation.models import ValueManifestation # Configurar logging logging.basicConfig( diff --git a/check_ports.py b/check_ports.py new file mode 100644 index 00000000..a2570c21 --- /dev/null +++ b/check_ports.py @@ -0,0 +1,33 @@ +import os +import sys + +# Add the backend directory to sys.path +sys.path.append(os.path.join(os.getcwd(), "backend")) + +from core.database import CoreSessionLocal +from api.v1.modules.a76.general_catalogs.ports.models import Port +from sqlalchemy import text + +def check_ports(): + db = CoreSessionLocal() + try: + # Check all ports + ports = db.query(Port).all() + print(f"Total ports found in a76.ports: {len(ports)}") + for i, p in enumerate(ports[:10]): + print(f"Port {i}: ID={p.id}, Code={p.port_code}, Desc={p.description}, Tenant={p.tenant_id}, Company={p.company_id}") + + # Check by tenant if possible (assuming tenant 1) + tenant_ports = db.query(Port).filter(Port.tenant_id == 1).all() + print(f"Ports for Tenant 1: {len(tenant_ports)}") + + # Check schemas + result = db.execute(text("SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'a76'")) + schema_exists = result.fetchone() + print(f"Schema a76 exists: {schema_exists is not None}") + + finally: + db.close() + +if __name__ == "__main__": + check_ports() diff --git a/check_transport_modes.py b/check_transport_modes.py new file mode 100644 index 00000000..0dcfdf84 --- /dev/null +++ b/check_transport_modes.py @@ -0,0 +1,33 @@ +import sys +import os +os.environ["CORE_DB_HOST"] = "localhost" +sys.path.append(os.path.join(os.getcwd(), 'backend')) +from core.database import get_core_db +from sqlalchemy import text + +db = next(get_core_db()) + +try: + print("Checking public.transport_modes data...") + result = db.execute(text("SELECT * FROM public.transport_modes ORDER BY key ASC")) + rows = result.fetchall() + print(f"Found {len(rows)} records:") + for row in rows: + print(f"Key: {row[0]}, Name: {row[1]}") + + if len(rows) == 0: + print("\nTable is empty! Inserting default values for testing...") + db.execute(text("INSERT INTO public.transport_modes (key, name) VALUES ('01', 'Marítimo'), ('02', 'Ferroviario'), ('03', 'Carretero'), ('04', 'Aéreo')")) + db.commit() + print("Inserted 4 default records.") + +except Exception as e: + print(f"Error: {e}") + # Try to see if table exists but schema is different + try: + result = db.execute(text("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'")) + print("\nTables in public schema:") + for row in result: + print(f" - {row[0]}") + except: + pass diff --git a/debug_invoices.py b/debug_invoices.py new file mode 100644 index 00000000..eee53915 --- /dev/null +++ b/debug_invoices.py @@ -0,0 +1,83 @@ +import sys +import os +os.environ["CORE_DB_HOST"] = "localhost" +sys.path.append(os.path.join(os.getcwd(), 'backend')) +from core.database import get_core_db +# Import dependent models to ensure they are registered +from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos +from api.v1.modules.a76.invoices.models import InvoiceHeader +from sqlalchemy import text + +db = next(get_core_db()) + +try: + # PATCH: Update Invoice 147 to Tenant 1 using RAW SQL to bypass ORM FK checks + print("PATCHING: Updating Invoice 147 tenant_id to 1 (RAW SQL)") + db.execute(text("UPDATE a76.invoice_header SET tenant_id = 1 WHERE id = 147")) + db.commit() + + print("Checking for invoices with operation_type='exp'...") + + # Query for a sample of export invoices + invoices = db.query(InvoiceHeader).filter( + InvoiceHeader.operation_type == 'exp' + ).limit(20).all() + + print(f"Found {len(invoices)} export invoices.") + for inv in invoices: + manifest_num = inv.compliance_mx.manifest_number if inv.compliance_mx else "No Compliance" + print(f"ID: {inv.id}, Tenant: {inv.tenant_id}, Company: {inv.company_id}, DeletedAt: {inv.deleted_at}, System: '{inv.system}', Type: '{inv.invoice_type.key if hasattr(inv.invoice_type, 'key') else inv.invoice_type}', Op: '{inv.operation_type}', Manifest: '{manifest_num}'") + + print("\n--- Simulating Backend Query ---") + from api.v1.modules.a76.invoices.models import InvoiceComplianceMx + + # Simulate: company_id=1, manifest_number="48720384" + target_manifest = "48720384" + company_id = 1 + + print(f"\n--- Global Search for '{target_manifest}' ---") + + # Check InvoiceComplianceMx (manifest_number) + compliance_matches = db.query(InvoiceComplianceMx).filter( + InvoiceComplianceMx.manifest_number.ilike(f"%{target_manifest}%") + ).all() + print(f"Matches in InvoiceComplianceMx [manifest_number]: {len(compliance_matches)}") + for m in compliance_matches: + print(f" - ComplianceID: {m.id}, InvoiceID: {m.invoice_id}, Manifest: '{m.manifest_number}'") + # Get parent invoice info + inv = db.query(InvoiceHeader).get(m.invoice_id) + if inv: + print(f" -> Invoice: {inv.invoice_number}, System: {inv.system}, Tenant: {inv.tenant_id}, Company: {inv.company_id}, DeletedAt: {inv.deleted_at}") + + # Check InvoiceHeader (invoice_number) + header_matches = db.query(InvoiceHeader).filter( + InvoiceHeader.invoice_number.ilike(f"%{target_manifest}%") + ).all() + print(f"Matches in InvoiceHeader [invoice_number]: {len(header_matches)}") + for h in header_matches: + print(f" - ID: {h.id}, Number: '{h.invoice_number}', Tenant: {h.tenant_id}, Company: {h.company_id}") + + # Check InvoiceHeader (invoice_ref) + ref_matches = db.query(InvoiceHeader).filter( + InvoiceHeader.invoice_ref.ilike(f"%{target_manifest}%") + ).all() + print(f"Matches in InvoiceHeader [invoice_ref]: {len(ref_matches)}") + for h in ref_matches: + print(f" - ID: {h.id}, Ref: '{h.invoice_ref}', Tenant: {h.tenant_id}, Company: {h.company_id}") + +except Exception as e: + print(f"Error: {e}") + + +print(f"\n--- Instpecting Invoice 147 ---") +inv147 = db.query(InvoiceHeader).get(147) +if inv147: + print(f"ID: {inv147.id}, Number: {inv147.invoice_number}, OpType: {inv147.operation_type}, System: {inv147.system}") + if inv147.compliance_mx: + print(f"Compliance: Manifest={inv147.compliance_mx.manifest_number}") + else: + print(f"Compliance: None") +else: + print("Invoice 147 not found") + + diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 49d51d59..e2563ba5 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -89,6 +89,17 @@ "exportation": "Exportation", "repair": "Repair" }, + "export": { + "title": "Exportation", + "catalog": "Export Catalog", + "repair": "Repair", + "manifest": "Manifest", + "proforma": "Proforma", + "reports": "Reports", + "used_materials": "Used Materials Module", + "destruction": "Destruction", + "special_processes": "Special Processes" + }, "clients_and_providers": "Clients and Providers", "customs_brokers": "Customs Brokers", "client_provider_type": { diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 48dcc061..1caa7503 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -89,6 +89,17 @@ "exportation": "Exportación", "repair": "Reparación" }, + "export": { + "title": "Exportación", + "catalog": "Catálogo de exportación", + "repair": "Reparación", + "manifest": "Manifiesto", + "proforma": "Proforma", + "reports": "Reportes", + "used_materials": "Módulo de materiales utilizados", + "destruction": "Destrucción", + "special_processes": "Procesos Especiales" + }, "clients_and_providers": "Clientes y Proveedores", "customs_brokers": "Agentes Aduanales", "client_provider_type": { diff --git a/frontend/src/lib/api/dashboard/a76/customs-brokers.ts b/frontend/src/lib/api/dashboard/a76/customs-brokers.ts index 49b336e1..4fe32672 100644 --- a/frontend/src/lib/api/dashboard/a76/customs-brokers.ts +++ b/frontend/src/lib/api/dashboard/a76/customs-brokers.ts @@ -1,27 +1,27 @@ import { api } from '$lib/api'; import type { ApiResponse } from '$lib/api'; -export interface CustomsBroker { - id: number; - type?: string | null; - broker_key: string; - name?: string | null; - address?: string | null; - postal_code?: string | null; - city?: string | null; - state?: string | null; - phone?: string | null; - fax?: string | null; - email?: string | null; - country?: string | null; - tax_id?: string | null; - personal_id?: string | null; - position?: string | null; - license: string; - company?: string | null; - contact?: string | null; - tenant_id: string; - company_id: string; +export interface CustomsBroker { + id: number; + type?: string | null; + broker_key: string; + name?: string | null; + address?: string | null; + postal_code?: string | null; + city?: string | null; + state?: string | null; + phone?: string | null; + fax?: string | null; + email?: string | null; + country?: string | null; + tax_id?: string | null; + personal_id?: string | null; + position?: string | null; + license: string; + company?: string | null; + contact?: string | null; + tenant_id: string; + company_id: string; } export interface CustomsBrokerVU { @@ -82,12 +82,19 @@ export interface CreateCustomsBrokerData { company_id: string; } +export interface CustomsBrokerListResponse { + items: CustomsBroker[]; + total: number; + page: number; + page_size: number; +} + /** * API para Agentes Aduanales */ export const customsBrokersApi = { - list: (companyId: string) => { - return api.get(`/v1/a76/customs-brokers?company_id=${companyId}`); + list: (companyId: string, page = 1, pageSize = 50) => { + return api.get(`/v1/a76/customs-brokers?company_id=${companyId}&page=${page}&page_size=${pageSize}`); }, get: (brokerKey: string, companyId: string) => { @@ -99,21 +106,21 @@ export const customsBrokersApi = { }, /** - * Actualiza la información de un agente aduanal - */ - update: (brokerKey: string, data: CreateCustomsBrokerData) => { - const companyId = data.company_id; - return api.put(`/v1/a76/customs-brokers/${brokerKey}/?company_id=${companyId}`, data); - }, + * Actualiza la información de un agente aduanal + */ + update: (brokerKey: string, data: CreateCustomsBrokerData) => { + const companyId = data.company_id; + return api.put(`/v1/a76/customs-brokers/${brokerKey}/?company_id=${companyId}`, data); + }, + + /** + * Elimina un agente aduanal + */ + delete: (brokerKey: string, companyId: string) => { + return api.delete(`/v1/a76/customs-brokers/${brokerKey}/?company_id=${companyId}`); + }, + - /** - * Elimina un agente aduanal - */ - delete: (brokerKey: string, companyId: string) => { - return api.delete(`/v1/a76/customs-brokers/${brokerKey}/?company_id=${companyId}`); - }, - - updateVU: (brokerKey: string, data: CustomsBrokerVU, companyId: string) => { return api.put(`/v1/a76/customs-broker-vu/${brokerKey}?company_id=${companyId}`, data); 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 c0994ade..2139acb9 100644 --- a/frontend/src/lib/api/dashboard/a76/general_catalogs/locations.ts +++ b/frontend/src/lib/api/dashboard/a76/general_catalogs/locations.ts @@ -29,42 +29,36 @@ export interface LocationFilters { page_size?: number; } +import { portsApi, PortType } from './ports'; + export async function getLocations( companyId: number, filters?: LocationFilters ): Promise { - const params = new URLSearchParams({ company_id: companyId.toString() }); - - if (filters) { - if (filters.location_code) params.append('location_code', filters.location_code); - if (filters.location_description) params.append('location_description', filters.location_description); - if (filters.page) params.append('page', filters.page.toString()); - if (filters.page_size) params.append('page_size', filters.page_size.toString()); - } - - return api.get(`/v1/a76/ports/?${params.toString()}`); + const res = await portsApi.list(companyId, filters || {}); + return (res.data || res) as unknown as LocationListResponse; } export async function getLocation( locationId: number, companyId: number ): Promise { - const params = new URLSearchParams({ company_id: companyId.toString() }); - return api.get(`/v1/a76/ports/${locationId}/?${params.toString()}`); + const res = await portsApi.get(locationId, companyId); + return (res.data || res) as unknown as Location; } export async function createLocation( data: LocationCreate, companyId: number ): Promise { - const params = new URLSearchParams({ company_id: companyId.toString() }); - return api.post(`/v1/a76/ports/?${params.toString()}`, { + const res = await portsApi.create({ port_code: data.location_code, location_code: data.location_code, description: null, location_description: data.location_description || null, - port_type: 'ENTRY' - }); + port_type: PortType.ENTRY + }, companyId); + return (res.data || res) as unknown as Location; } export async function updateLocation( @@ -72,19 +66,15 @@ export async function updateLocation( data: LocationUpdate, companyId: number ): Promise { - const params = new URLSearchParams({ company_id: companyId.toString() }); - return api.put( - `/v1/a76/ports/${locationId}/?${params.toString()}`, - { - location_description: data.location_description - } - ); + const res = await portsApi.update(locationId, { + location_description: data.location_description + }, companyId); + return (res.data || res) as unknown as Location; } export async function deleteLocation( locationId: number, companyId: number ): Promise { - const params = new URLSearchParams({ company_id: companyId.toString() }); - return api.delete(`/v1/a76/ports/${locationId}/?${params.toString()}`); + await portsApi.delete(locationId, companyId); } \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/a76/general_catalogs/ports.ts b/frontend/src/lib/api/dashboard/a76/general_catalogs/ports.ts index 8ec0d7b9..2969d6b4 100644 --- a/frontend/src/lib/api/dashboard/a76/general_catalogs/ports.ts +++ b/frontend/src/lib/api/dashboard/a76/general_catalogs/ports.ts @@ -37,38 +37,56 @@ export interface PortUpdate { export interface PortListResponse { items: Port[]; total: number; - page: number; - page_size: number; - pages: number; } -export async function getPorts( - page = 1, - pageSize = 50, - filters: Record = {}, - companyId?: number -): Promise> { - const queryParams = new URLSearchParams({ - page: page.toString(), - page_size: pageSize.toString(), - ...filters - }); +class PortsApi { + private baseUrl = '/v1/a76/ports'; - if (companyId) { - queryParams.append('company_id', companyId.toString()); + async list( + companyId: string | number, + params?: Record + ): Promise> { + const queryParams = new URLSearchParams({ + company_id: companyId.toString(), + ...params + }); + return api.get(`${this.baseUrl}/?${queryParams.toString()}`); } - return await api.get(`/v1/a76/ports/?${queryParams.toString()}`); + async get(id: string | number, companyId: string | number): Promise> { + const queryParams = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.get(`${this.baseUrl}/${id}/?${queryParams.toString()}`); + } + + async create(data: PortCreate, companyId: string | number): Promise> { + const queryParams = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.post(`${this.baseUrl}/?${queryParams.toString()}`, data); + } + + async update(id: string | number, data: PortUpdate, companyId: string | number): Promise> { + const queryParams = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.put(`${this.baseUrl}/${id}/?${queryParams.toString()}`, data); + } + + async delete(id: string | number, companyId: string | number): Promise> { + const queryParams = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.delete(`${this.baseUrl}/${id}/?${queryParams.toString()}`); + } } -export async function createPort(data: PortCreate, companyId: number): Promise> { - return await api.post(`/v1/a76/ports/?company_id=${companyId}`, data); -} +export const portsApi = new PortsApi(); -export async function updatePort(id: number, data: PortUpdate, companyId: number): Promise> { - return await api.put(`/v1/a76/ports/${id}/?company_id=${companyId}`, data); -} - -export async function deletePort(id: number, companyId: number): Promise> { - return await api.delete(`/v1/a76/ports/${id}/?company_id=${companyId}`); -} +/** + * @deprecated Use portsApi.list Instead + */ +export const getPorts = (page = 1, pageSize = 50, filters = {}, companyId?: number) => { + return portsApi.list(companyId || '', { page, page_size: pageSize, ...filters }); +}; diff --git a/frontend/src/lib/api/dashboard/a76/invoices.ts b/frontend/src/lib/api/dashboard/a76/invoices.ts index a8ec14de..91cc735e 100644 --- a/frontend/src/lib/api/dashboard/a76/invoices.ts +++ b/frontend/src/lib/api/dashboard/a76/invoices.ts @@ -231,6 +231,8 @@ export interface Invoice { logistics?: InvoiceLogistics; details?: InvoiceSalesDetails[]; collections?: InvoiceCollections[]; + // Client-side only properties + is_selected?: boolean; } export interface InvoiceListResponse { diff --git a/frontend/src/lib/api/dashboard/a76/manifests.ts b/frontend/src/lib/api/dashboard/a76/manifests.ts new file mode 100644 index 00000000..2350af94 --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/manifests.ts @@ -0,0 +1,117 @@ +import { api, type ApiResponse } from '$lib/api'; + +export interface Manifest { + id: number; + manifest_number?: string; + importer_details?: string; + person_in_charge?: string; + consigned_to?: string; + sent_by?: string; + foreign_exit_port?: string; + foreign_exit_port_loc?: string; + destination_port?: string; + destination_port_loc?: string; + entry_port?: string; + entry_port_loc?: string; + entry_date?: number; + net_weight?: number; + gross_weight?: number; + total_value?: number; + description?: string; + broker_code?: string; + carrier_code?: string; + payment_invoice_number?: string; + seal_number?: string; + entry_hour?: number; + hazardous_material?: string; + transport_mode?: string; + transport_code?: string; + trailer_number?: string; + status?: string; + status_description?: string; + manifest_type?: string; + created_at?: string; + updated_at?: string; + company_id?: number; + tenant_id?: string; +} + +export interface ManifestResponse { + items: Manifest[]; + total: number; + page: number; + page_size: number; +} + +export type ManifestCreate = Omit; +export type ManifestUpdate = Partial; + +class ManifestApi { + private baseUrl = '/v1/a76/manifests'; + + async list( + companyId: string | number, + filters?: Record + ): Promise> { + const params = new URLSearchParams({ + company_id: companyId.toString() + }); + + if (filters) { + Object.entries(filters).forEach(([key, value]) => { + if (value !== undefined && value !== null && value !== '') { + params.append(key, value.toString()); + } + }); + } + + return api.get(`${this.baseUrl}?${params.toString()}`); + } + + async get(companyId: string | number, id: number): Promise> { + const params = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.get(`${this.baseUrl}/${id}?${params.toString()}`); + } + + async create(companyId: string | number, manifest: ManifestCreate): Promise> { + const params = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.post(`${this.baseUrl}?${params.toString()}`, manifest); + } + + async update( + companyId: string | number, + id: number, + manifest: ManifestUpdate + ): Promise> { + const params = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.patch(`${this.baseUrl}/${id}?${params.toString()}`, manifest); + } + + async delete(companyId: string | number, id: number): Promise> { + const params = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.delete(`${this.baseUrl}/${id}?${params.toString()}`); + } + + async updateInvoiceCompliance( + invoiceId: number, + companyId: string | number, + manifestNumber: string | null + ): Promise> { + const params = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.patch(`${this.baseUrl}/invoices/${invoiceId}/compliance?${params.toString()}`, { + manifest_number: manifestNumber + }); + } +} + +export const manifestApi = new ManifestApi(); diff --git a/frontend/src/lib/api/dashboard/a76/trailers.ts b/frontend/src/lib/api/dashboard/a76/trailers.ts new file mode 100644 index 00000000..df739414 --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/trailers.ts @@ -0,0 +1,40 @@ +import { api, type ApiResponse } from '$lib/api'; + +export interface Trailer { + trailer_number: string; + plate_number?: string; + trailer_type_key?: string; + is_active: boolean; + tenant_id?: string; +} + +export interface TrailerResponse { + items: Trailer[]; + total: number; + page: number; + page_size: number; +} + +class TrailersApi { + private baseUrl = '/v1/a76/trailers'; + + async list( + companyId: string | number, + params?: Record + ): Promise> { + const queryParams = new URLSearchParams({ + company_id: companyId.toString(), + ...params + }); + return api.get(`${this.baseUrl}?${queryParams.toString()}`); + } + + async get(id: string, companyId: string | number): Promise> { + const queryParams = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.get(`${this.baseUrl}/${id}?${queryParams.toString()}`); + } +} + +export const trailersApi = new TrailersApi(); diff --git a/frontend/src/lib/api/dashboard/a76/transporters.ts b/frontend/src/lib/api/dashboard/a76/transporters.ts new file mode 100644 index 00000000..6383828b --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/transporters.ts @@ -0,0 +1,40 @@ +import { api, type ApiResponse } from '$lib/api'; + +export interface Transporter { + transporter_key: string; + name: string; + rfc?: string; + is_active: boolean; + tenant_id?: string; +} + +export interface TransporterResponse { + items: Transporter[]; + total: number; + page: number; + page_size: number; +} + +class TransportersApi { + private baseUrl = '/v1/a76/transporters'; + + async list( + companyId: string | number, + params?: Record + ): Promise> { + const queryParams = new URLSearchParams({ + company_id: companyId.toString(), + ...params + }); + return api.get(`${this.baseUrl}?${queryParams.toString()}`); + } + + async get(id: string, companyId: string | number): Promise> { + const queryParams = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.get(`${this.baseUrl}/${id}?${queryParams.toString()}`); + } +} + +export const transportersApi = new TransportersApi(); diff --git a/frontend/src/lib/api/dashboard/a76/vehicles.ts b/frontend/src/lib/api/dashboard/a76/vehicles.ts new file mode 100644 index 00000000..a0c864e4 --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/vehicles.ts @@ -0,0 +1,32 @@ +import { api } from '$lib/api'; + +export interface Vehicle { + vehicle_key: string; + brand?: string; + plate_number?: string; + description?: string; + transport_type?: string; + year?: string; +} + +export interface VehicleListResponse { + items: Vehicle[]; + total: number; +} + +/** + * API para Vehículos + */ +export const vehiclesApi = { + list: (companyId: string, page = 1, pageSize = 50) => { + return api.get( + `/v1/a76/transportation/vehicles?company_id=${companyId}&page=${page}&page_size=${pageSize}` + ); + }, + + get: (vehicleKey: string, companyId: string) => { + return api.get( + `/v1/a76/transportation/vehicles/${vehicleKey}?company_id=${companyId}` + ); + } +}; diff --git a/frontend/src/lib/api/dashboard/refrence_data/code_pedimento_regimens.ts b/frontend/src/lib/api/dashboard/refrence_data/code_pedimento_regimens.ts index 13366b1c..4b927437 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/code_pedimento_regimens.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/code_pedimento_regimens.ts @@ -40,7 +40,7 @@ export const codePedimentoRegimensApi = { list: (page = 1, pageSize = 50) => api.get( // CORRECTO: Slash antes del signo '?' - `/v1/public/refrence_data/code-pedimento-regimens/?page=${page}&page_size=${pageSize}` + `/v1/public/reference_data/code-pedimento-regimens/?page=${page}&page_size=${pageSize}` ), /** @@ -55,19 +55,19 @@ export const codePedimentoRegimensApi = { */ create: (data: CreateCodePedimentoRegimenData) => // CORREGIDO: Añadido slash al final de la ruta - api.post('/v1/public/refrence_data/code-pedimento-regimens/', data), + api.post('/v1/public/reference_data/code-pedimento-regimens/', data), /** * Actualiza un code pedimento regimen existente */ update: (id: number, data: UpdateCodePedimentoRegimenData) => // CORRECTO: Slash después del ID - api.put(`/v1/public/refrence_data/code-pedimento-regimens/${id}/`, data), + api.put(`/v1/public/reference_data/code-pedimento-regimens/${id}/`, data), /** * Elimina un code pedimento regimen */ delete: (id: number) => // CORRECTO: Slash después del ID - api.delete(`/v1/public/refrence_data/code-pedimento-regimens/${id}/`) + api.delete(`/v1/public/reference_data/code-pedimento-regimens/${id}/`) }; \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/refrence_data/containers.ts b/frontend/src/lib/api/dashboard/refrence_data/containers.ts index 36475ba4..67f240d6 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/containers.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/containers.ts @@ -38,7 +38,7 @@ export const containersApi = { list: (page = 1, pageSize = 50) => api.get( // CORRECTO: Slash antes del ? - `/v1/public/refrence_data/containers/?page=${page}&page_size=${pageSize}` + `/v1/public/reference_data/containers/?page=${page}&page_size=${pageSize}` ), /** @@ -55,7 +55,7 @@ export const containersApi = { */ create: (data: CreateContainerData) => // CORREGIDO: Se añadió el slash '/' al final para evitar redirección en POST - api.post('/v1/public/refrence_data/containers/', data), + api.post('/v1/public/reference_data/containers/', data), /** * Actualiza un container existente @@ -64,7 +64,7 @@ export const containersApi = { */ update: (key: number, data: UpdateContainerData) => // CORRECTO: Slash final después de la key - api.put(`/v1/public/refrence_data/containers/${key}/`, data), + api.put(`/v1/public/reference_data/containers/${key}/`, data), /** * Elimina un container @@ -72,5 +72,5 @@ export const containersApi = { */ delete: (key: number) => // CORRECTO: Slash final después de la key - api.delete(`/v1/public/refrence_data/containers/${key}/`) + api.delete(`/v1/public/reference_data/containers/${key}/`) }; \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/refrence_data/countries.ts b/frontend/src/lib/api/dashboard/refrence_data/countries.ts index 09ed1927..1b339c1b 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/countries.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/countries.ts @@ -46,7 +46,7 @@ export const countriesApi = { * @param search - Término de búsqueda (opcional) */ list: (page = 1, pageSize = 50, search?: string) => { - let url = `/v1/public/refrence_data/countries/?page=${page}&page_size=${pageSize}`; + let url = `/v1/public/reference_data/countries/?page=${page}&page_size=${pageSize}`; if (search) { url += `&search=${encodeURIComponent(search)}`; } @@ -59,7 +59,7 @@ export const countriesApi = { */ get: (m3_key: string) => // CORREGIDO: Añadido '/' al final - api.get(`/v1/public/refrence_data/countries/${m3_key}/`), + api.get(`/v1/public/reference_data/countries/${m3_key}/`), /** * Crea un nuevo país @@ -67,7 +67,7 @@ export const countriesApi = { */ create: (data: CreateCountryData) => // CORREGIDO: Añadido '/' al final - api.post('/v1/public/refrence_data/countries/', data), + api.post('/v1/public/reference_data/countries/', data), /** * Actualiza un país existente @@ -76,7 +76,7 @@ export const countriesApi = { */ update: (m3_key: string, data: UpdateCountryData) => // CORREGIDO: Añadido '/' después de la clave - api.put(`/v1/public/refrence_data/countries/${m3_key}/`, data), + api.put(`/v1/public/reference_data/countries/${m3_key}/`, data), /** * Elimina un país @@ -84,5 +84,5 @@ export const countriesApi = { */ delete: (m3_key: string) => // CORREGIDO: Añadido '/' después de la clave - api.delete(`/v1/public/refrence_data/countries/${m3_key}/`) + api.delete(`/v1/public/reference_data/countries/${m3_key}/`) }; \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/refrence_data/currency_types.ts b/frontend/src/lib/api/dashboard/refrence_data/currency_types.ts index 71d2685b..387e1f3f 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/currency_types.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/currency_types.ts @@ -41,7 +41,7 @@ export const currencyTypesApi = { list: (page = 1, pageSize = 50) => api.get( // CORREGIDO: Añadido '/' antes del '?' - `/v1/public/refrence_data/currency-types/?page=${page}&page_size=${pageSize}` + `/v1/public/reference_data/currency-types/?page=${page}&page_size=${pageSize}` ), /** @@ -50,7 +50,7 @@ export const currencyTypesApi = { */ get: (code: string) => // CORREGIDO: Añadido '/' al final - api.get(`/v1/public/refrence_data/currency-types/${code}/`), + api.get(`/v1/public/reference_data/currency-types/${code}/`), /** * Crea un nuevo tipo de moneda @@ -58,7 +58,7 @@ export const currencyTypesApi = { */ create: (data: CreateCurrencyTypeData) => // CORREGIDO: Añadido '/' al final - api.post('/v1/public/refrence_data/currency-types/', data), + api.post('/v1/public/reference_data/currency-types/', data), /** * Actualiza un tipo de moneda existente @@ -67,7 +67,7 @@ export const currencyTypesApi = { */ update: (code: string, data: UpdateCurrencyTypeData) => // CORREGIDO: Añadido '/' después del código - api.put(`/v1/public/refrence_data/currency-types/${code}/`, data), + api.put(`/v1/public/reference_data/currency-types/${code}/`, data), /** * Elimina un tipo de moneda @@ -75,5 +75,5 @@ export const currencyTypesApi = { */ delete: (code: string) => // CORREGIDO: Añadido '/' después del código - api.delete(`/v1/public/refrence_data/currency-types/${code}/`) + api.delete(`/v1/public/reference_data/currency-types/${code}/`) }; \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/refrence_data/customs_sections.ts b/frontend/src/lib/api/dashboard/refrence_data/customs_sections.ts index 782016e1..30811ca1 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/customs_sections.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/customs_sections.ts @@ -38,7 +38,7 @@ export const customsSectionsApi = { list: (page = 1, pageSize = 50) => api.get( // CORREGIDO: Añadido '/' antes de los parámetros - `/v1/public/refrence_data/customs-sections/?page=${page}&page_size=${pageSize}` + `/v1/public/reference_data/customs-sections/?page=${page}&page_size=${pageSize}` ), /** @@ -47,7 +47,7 @@ export const customsSectionsApi = { */ get: (customs_code: string) => // CORREGIDO: Añadido '/' final - api.get(`/v1/public/refrence_data/customs-sections/${customs_code}/`), + api.get(`/v1/public/reference_data/customs-sections/${customs_code}/`), /** * Crea una nueva sección aduanera @@ -55,7 +55,7 @@ export const customsSectionsApi = { */ create: (data: CreateCustomsSectionData) => // CORREGIDO: Añadido '/' final - api.post('/v1/public/refrence_data/customs-sections/', data), + api.post('/v1/public/reference_data/customs-sections/', data), /** * Actualiza una sección aduanera existente @@ -64,7 +64,7 @@ export const customsSectionsApi = { */ update: (customs_code: string, data: UpdateCustomsSectionData) => // CORREGIDO: Añadido '/' después del código - api.put(`/v1/public/refrence_data/customs-sections/${customs_code}/`, data), + api.put(`/v1/public/reference_data/customs-sections/${customs_code}/`, data), /** * Elimina una sección aduanera @@ -72,5 +72,5 @@ export const customsSectionsApi = { */ delete: (customs_code: string) => // CORREGIDO: Añadido '/' después del código - api.delete(`/v1/public/refrence_data/customs-sections/${customs_code}/`) + api.delete(`/v1/public/reference_data/customs-sections/${customs_code}/`) }; \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/refrence_data/customs_warehouses.ts b/frontend/src/lib/api/dashboard/refrence_data/customs_warehouses.ts index 5f9ac0ff..eae2b955 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/customs_warehouses.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/customs_warehouses.ts @@ -41,7 +41,7 @@ export const customsWarehousesApi = { list: (page = 1, pageSize = 50) => api.get( // CORREGIDO: Añadido '/' antes del '?' - `/v1/public/refrence_data/customs-warehouses/?page=${page}&page_size=${pageSize}` + `/v1/public/reference_data/customs-warehouses/?page=${page}&page_size=${pageSize}` ), /** @@ -51,7 +51,7 @@ export const customsWarehousesApi = { */ get: (key: string, customs: string) => // CORREGIDO: Añadido '/' al final de la ruta compuesta - api.get(`/v1/public/refrence_data/customs-warehouses/${key}/${customs}/`), + api.get(`/v1/public/reference_data/customs-warehouses/${key}/${customs}/`), /** * Crea un nuevo recinto fiscalizado @@ -59,7 +59,7 @@ export const customsWarehousesApi = { */ create: (data: CreateCustomsWarehouseData) => // CORREGIDO: Añadido '/' al final - api.post('/v1/public/refrence_data/customs-warehouses/', data), + api.post('/v1/public/reference_data/customs-warehouses/', data), /** * Actualiza un recinto fiscalizado existente @@ -69,7 +69,7 @@ export const customsWarehousesApi = { */ update: (key: string, customs: string, data: UpdateCustomsWarehouseData) => // CORREGIDO: Añadido '/' al final de la ruta compuesta - api.put(`/v1/public/refrence_data/customs-warehouses/${key}/${customs}/`, data), + api.put(`/v1/public/reference_data/customs-warehouses/${key}/${customs}/`, data), /** * Elimina un recinto fiscalizado @@ -78,5 +78,5 @@ export const customsWarehousesApi = { */ delete: (key: string, customs: string) => // CORREGIDO: Añadido '/' al final de la ruta compuesta - api.delete(`/v1/public/refrence_data/customs-warehouses/${key}/${customs}/`) + api.delete(`/v1/public/reference_data/customs-warehouses/${key}/${customs}/`) }; \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/refrence_data/incoterms.ts b/frontend/src/lib/api/dashboard/refrence_data/incoterms.ts index e14d23b2..2543824e 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/incoterms.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/incoterms.ts @@ -41,7 +41,7 @@ export const incotermsApi = { list: (page = 1, pageSize = 50) => api.get( // CORREGIDO: Añadido '/' antes del '?' - `/v1/public/refrence_data/incoterms/?page=${page}&page_size=${pageSize}` + `/v1/public/reference_data/incoterms/?page=${page}&page_size=${pageSize}` ), /** @@ -50,7 +50,7 @@ export const incotermsApi = { */ get: (code: string) => // CORREGIDO: Añadido '/' final - api.get(`/v1/public/refrence_data/incoterms/${code}/`), + api.get(`/v1/public/reference_data/incoterms/${code}/`), /** * Crea un nuevo incoterm @@ -58,7 +58,7 @@ export const incotermsApi = { */ create: (data: CreateIncotermData) => // CORREGIDO: Añadido '/' final - api.post('/v1/public/refrence_data/incoterms/', data), + api.post('/v1/public/reference_data/incoterms/', data), /** * Actualiza un incoterm existente @@ -67,7 +67,7 @@ export const incotermsApi = { */ update: (code: string, data: UpdateIncotermData) => // CORREGIDO: Añadido '/' final después del código - api.put(`/v1/public/refrence_data/incoterms/${code}/`, data), + api.put(`/v1/public/reference_data/incoterms/${code}/`, data), /** * Elimina un incoterm @@ -75,5 +75,5 @@ export const incotermsApi = { */ delete: (code: string) => // CORREGIDO: Añadido '/' final después del código - api.delete(`/v1/public/refrence_data/incoterms/${code}/`) + api.delete(`/v1/public/reference_data/incoterms/${code}/`) }; \ No newline at end of file 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 c089e6ac..87a7c264 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/invoice_types.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/invoice_types.ts @@ -53,7 +53,7 @@ export const invoiceTypesApi = { } return api.get( // CORREGIDO: Añadido '/' antes del '?' - `/v1/public/refrence_data/invoice-types/?${params.toString()}` + `/v1/public/reference_data/invoice-types/?${params.toString()}` ); }, @@ -63,7 +63,7 @@ export const invoiceTypesApi = { */ get: (key: string) => // CORREGIDO: Añadido '/' final - api.get(`/v1/public/refrence_data/invoice-types/${key}/`), + api.get(`/v1/public/reference_data/invoice-types/${key}/`), /** * Crea un nuevo tipo de factura @@ -71,7 +71,7 @@ export const invoiceTypesApi = { */ create: (data: CreateInvoiceTypeData) => // CORREGIDO: Añadido '/' final - api.post('/v1/public/refrence_data/invoice-types/', data), + api.post('/v1/public/reference_data/invoice-types/', data), /** * Actualiza un tipo de factura existente @@ -80,7 +80,7 @@ export const invoiceTypesApi = { */ update: (key: string, data: UpdateInvoiceTypeData) => // CORREGIDO: Añadido '/' después de la key - api.put(`/v1/public/refrence_data/invoice-types/${key}/`, data), + api.put(`/v1/public/reference_data/invoice-types/${key}/`, data), /** * Elimina un tipo de factura @@ -88,5 +88,5 @@ export const invoiceTypesApi = { */ delete: (key: string) => // CORREGIDO: Añadido '/' después de la key - api.delete(`/v1/public/refrence_data/invoice-types/${key}/`) + api.delete(`/v1/public/reference_data/invoice-types/${key}/`) }; \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/refrence_data/material_types.ts b/frontend/src/lib/api/dashboard/refrence_data/material_types.ts index 3aac9a24..d2db1ab3 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/material_types.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/material_types.ts @@ -49,7 +49,7 @@ export const materialTypesApi = { } return api.get( // CORRECTO: Ya tiene el '/' antes del '?' - `/v1/public/refrence_data/material-types/?${params.toString()}` + `/v1/public/reference_data/material-types/?${params.toString()}` ); }, @@ -59,7 +59,7 @@ export const materialTypesApi = { */ get: (key: string) => // CORRECTO: Ya tiene el '/' al final - api.get(`/v1/public/refrence_data/material-types/${key}/`), + api.get(`/v1/public/reference_data/material-types/${key}/`), /** * Crea un nuevo tipo de material @@ -67,7 +67,7 @@ export const materialTypesApi = { */ create: (data: CreateMaterialTypeData) => // CORRECTO: Ya tiene el '/' al final - api.post('/v1/public/refrence_data/material-types/', data), + api.post('/v1/public/reference_data/material-types/', data), /** * Actualiza un tipo de material existente @@ -76,7 +76,7 @@ export const materialTypesApi = { */ update: (key: string, data: UpdateMaterialTypeData) => // CORRECTO: Ya tiene el '/' al final - api.put(`/v1/public/refrence_data/material-types/${key}/`, data), + api.put(`/v1/public/reference_data/material-types/${key}/`, data), /** * Elimina un tipo de material @@ -84,5 +84,5 @@ export const materialTypesApi = { */ delete: (key: string) => // CORRECTO: Ya tiene el '/' al final - api.delete(`/v1/public/refrence_data/material-types/${key}/`) + api.delete(`/v1/public/reference_data/material-types/${key}/`) }; \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/refrence_data/payment_methods.ts b/frontend/src/lib/api/dashboard/refrence_data/payment_methods.ts index 67a8e0f5..e0bcbcaf 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/payment_methods.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/payment_methods.ts @@ -38,7 +38,7 @@ export const paymentMethodsApi = { list: (page = 1, pageSize = 50) => api.get( // CORREGIDO: Añadido '/' antes del '?' - `/v1/public/refrence_data/payment-methods/?page=${page}&page_size=${pageSize}` + `/v1/public/reference_data/payment-methods/?page=${page}&page_size=${pageSize}` ), /** @@ -47,7 +47,7 @@ export const paymentMethodsApi = { */ get: (key: string) => // CORREGIDO: Añadido '/' final - api.get(`/v1/public/refrence_data/payment-methods/${key}/`), + api.get(`/v1/public/reference_data/payment-methods/${key}/`), /** * Crea un nuevo método de pago @@ -55,7 +55,7 @@ export const paymentMethodsApi = { */ create: (data: CreatePaymentMethodData) => // CORREGIDO: Añadido '/' final - api.post('/v1/public/refrence_data/payment-methods/', data), + api.post('/v1/public/reference_data/payment-methods/', data), /** * Actualiza un método de pago existente @@ -64,7 +64,7 @@ export const paymentMethodsApi = { */ update: (key: string, data: UpdatePaymentMethodData) => // CORREGIDO: Añadido '/' después de la key - api.put(`/v1/public/refrence_data/payment-methods/${key}/`, data), + api.put(`/v1/public/reference_data/payment-methods/${key}/`, data), /** * Elimina un método de pago @@ -72,5 +72,5 @@ export const paymentMethodsApi = { */ delete: (key: string) => // CORREGIDO: Añadido '/' después de la key - api.delete(`/v1/public/refrence_data/payment-methods/${key}/`) + api.delete(`/v1/public/reference_data/payment-methods/${key}/`) }; \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/refrence_data/pedimento_codes.ts b/frontend/src/lib/api/dashboard/refrence_data/pedimento_codes.ts index 9eb01c1d..6a5cb837 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/pedimento_codes.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/pedimento_codes.ts @@ -38,7 +38,7 @@ export const pedimentoCodesApi = { list: (page = 1, pageSize = 50) => api.get( // CORREGIDO: Añadido '/' antes del '?' - `/v1/public/refrence_data/pedimento-codes/?page=${page}&page_size=${pageSize}` + `/v1/public/reference_data/pedimento-codes/?page=${page}&page_size=${pageSize}` ), /** @@ -47,7 +47,7 @@ export const pedimentoCodesApi = { */ get: (code: string) => // CORREGIDO: Añadido '/' final - api.get(`/v1/public/refrence_data/pedimento-codes/${code}/`), + api.get(`/v1/public/reference_data/pedimento-codes/${code}/`), /** * Crea una nueva clave de pedimento @@ -55,7 +55,7 @@ export const pedimentoCodesApi = { */ create: (data: CreatePedimentoCodeData) => // CORREGIDO: Añadido '/' final - api.post('/v1/public/refrence_data/pedimento-codes/', data), + api.post('/v1/public/reference_data/pedimento-codes/', data), /** * Actualiza una clave de pedimento existente @@ -64,7 +64,7 @@ export const pedimentoCodesApi = { */ update: (code: string, data: UpdatePedimentoCodeData) => // CORREGIDO: Añadido '/' después del código - api.put(`/v1/public/refrence_data/pedimento-codes/${code}/`, data), + api.put(`/v1/public/reference_data/pedimento-codes/${code}/`, data), /** * Elimina una clave de pedimento @@ -72,5 +72,5 @@ export const pedimentoCodesApi = { */ delete: (code: string) => // CORREGIDO: Añadido '/' después del código - api.delete(`/v1/public/refrence_data/pedimento-codes/${code}/`) + api.delete(`/v1/public/reference_data/pedimento-codes/${code}/`) }; \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/refrence_data/pedimento_regimens.ts b/frontend/src/lib/api/dashboard/refrence_data/pedimento_regimens.ts index 15548325..9466a70d 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/pedimento_regimens.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/pedimento_regimens.ts @@ -38,7 +38,7 @@ export const pedimentoRegimensApi = { list: (page = 1, pageSize = 50) => api.get( // CORREGIDO: Añadido '/' antes del '?' - `/v1/public/refrence_data/pedimento-regimens/?page=${page}&page_size=${pageSize}` + `/v1/public/reference_data/pedimento-regimens/?page=${page}&page_size=${pageSize}` ), /** @@ -47,7 +47,7 @@ export const pedimentoRegimensApi = { */ get: (code: string) => // CORREGIDO: Añadido '/' final - api.get(`/v1/public/refrence_data/pedimento-regimens/${code}/`), + api.get(`/v1/public/reference_data/pedimento-regimens/${code}/`), /** * Crea un nuevo régimen de pedimento @@ -55,7 +55,7 @@ export const pedimentoRegimensApi = { */ create: (data: CreatePedimentoRegimenData) => // CORREGIDO: Añadido '/' final - api.post('/v1/public/refrence_data/pedimento-regimens/', data), + api.post('/v1/public/reference_data/pedimento-regimens/', data), /** * Actualiza un régimen de pedimento existente @@ -64,7 +64,7 @@ export const pedimentoRegimensApi = { */ update: (code: string, data: UpdatePedimentoRegimenData) => // CORREGIDO: Añadido '/' final después del código - api.put(`/v1/public/refrence_data/pedimento-regimens/${code}/`, data), + api.put(`/v1/public/reference_data/pedimento-regimens/${code}/`, data), /** * Elimina un régimen de pedimento @@ -72,5 +72,5 @@ export const pedimentoRegimensApi = { */ delete: (code: string) => // CORREGIDO: Añadido '/' final después del código - api.delete(`/v1/public/refrence_data/pedimento-regimens/${code}/`) + api.delete(`/v1/public/reference_data/pedimento-regimens/${code}/`) }; \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/refrence_data/sectors.ts b/frontend/src/lib/api/dashboard/refrence_data/sectors.ts index 6e8c3fe3..409c9ef0 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/sectors.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/sectors.ts @@ -41,7 +41,7 @@ export const sectorsApi = { list: (page = 1, pageSize = 50) => api.get( // CORREGIDO: Slash antes del '?' - `/v1/public/refrence_data/sectors/?page=${page}&page_size=${pageSize}` + `/v1/public/reference_data/sectors/?page=${page}&page_size=${pageSize}` ), /** @@ -50,7 +50,7 @@ export const sectorsApi = { */ get: (key: string) => // CORREGIDO: Slash final - api.get(`/v1/public/refrence_data/sectors/${key}/`), + api.get(`/v1/public/reference_data/sectors/${key}/`), /** * Crea un nuevo sector @@ -58,7 +58,7 @@ export const sectorsApi = { */ create: (data: CreateSectorData) => // CORREGIDO: Slash final - api.post('/v1/public/refrence_data/sectors/', data), + api.post('/v1/public/reference_data/sectors/', data), /** * Actualiza un sector existente @@ -67,7 +67,7 @@ export const sectorsApi = { */ update: (key: string, data: UpdateSectorData) => // CORREGIDO: Slash final después de la variable - api.put(`/v1/public/refrence_data/sectors/${key}/`, data), + api.put(`/v1/public/reference_data/sectors/${key}/`, data), /** * Elimina un sector @@ -75,5 +75,5 @@ export const sectorsApi = { */ delete: (key: string) => // CORREGIDO: Slash final después de la variable - api.delete(`/v1/public/refrence_data/sectors/${key}/`) + api.delete(`/v1/public/reference_data/sectors/${key}/`) }; \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/refrence_data/states.ts b/frontend/src/lib/api/dashboard/refrence_data/states.ts index 1a5cdb43..93a1db35 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/states.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/states.ts @@ -44,7 +44,7 @@ export const statesApi = { list: (page = 1, pageSize = 50) => api.get( // CORREGIDO: Añadido '/' antes de '?' - `/v1/public/refrence_data/states/?page=${page}&page_size=${pageSize}` + `/v1/public/reference_data/states/?page=${page}&page_size=${pageSize}` ), /** @@ -53,7 +53,7 @@ export const statesApi = { */ get: (m3Key: string) => // CORREGIDO: Añadido '/' al final - api.get(`/v1/public/refrence_data/states/${m3Key}/`), + api.get(`/v1/public/reference_data/states/${m3Key}/`), /** * Crea un nuevo estado @@ -61,7 +61,7 @@ export const statesApi = { */ create: (data: CreateStateData) => // CORREGIDO: Añadido '/' al final - api.post('/v1/public/refrence_data/states/', data), + api.post('/v1/public/reference_data/states/', data), /** * Actualiza un estado existente @@ -70,7 +70,7 @@ export const statesApi = { */ update: (m3Key: string, data: UpdateStateData) => // CORREGIDO: Añadido '/' después de la variable - api.put(`/v1/public/refrence_data/states/${m3Key}/`, data), + api.put(`/v1/public/reference_data/states/${m3Key}/`, data), /** * Elimina un estado @@ -78,5 +78,5 @@ export const statesApi = { */ delete: (m3Key: string) => // CORREGIDO: Añadido '/' después de la variable - api.delete(`/v1/public/refrence_data/states/${m3Key}/`) + api.delete(`/v1/public/reference_data/states/${m3Key}/`) }; \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/refrence_data/transport_modes.ts b/frontend/src/lib/api/dashboard/refrence_data/transport_modes.ts index 858c0347..30d67d22 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/transport_modes.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/transport_modes.ts @@ -5,25 +5,25 @@ import { api } from '$lib/api'; export interface TransportMode { - key: string; - name: string; + key: string; + name: string; } export interface TransportModeListResponse { - items: TransportMode[]; - total: number; - page: number; - page_size: number; + items: TransportMode[]; + total: number; + page: number; + page_size: number; } export interface CreateTransportModeData { - key: string; - name: string; + key: string; + name: string; } export interface UpdateTransportModeData { - key?: string; - name?: string; + key?: string; + name?: string; } /** @@ -37,7 +37,6 @@ export const transportModesApi = { */ list: (page = 1, pageSize = 50) => api.get( - // CORREGIDO: Slash antes del ? `/v1/public/refrence_data/transport-modes/?page=${page}&page_size=${pageSize}` ), @@ -45,8 +44,7 @@ export const transportModesApi = { * Obtiene un modo de transporte por key * @param key - Clave del modo de transporte */ - get: (key: string) => - // CORREGIDO: Slash final después de la key + get: (key: string) => api.get(`/v1/public/refrence_data/transport-modes/${key}/`), /** @@ -54,7 +52,6 @@ export const transportModesApi = { * @param data - Datos del modo de transporte a crear */ create: (data: CreateTransportModeData) => - // CORREGIDO: Slash final en la ruta base api.post('/v1/public/refrence_data/transport-modes/', data), /** @@ -63,14 +60,12 @@ export const transportModesApi = { * @param data - Datos a actualizar */ update: (key: string, data: UpdateTransportModeData) => - // CORREGIDO: Slash final después de la key api.put(`/v1/public/refrence_data/transport-modes/${key}/`, data), /** * Elimina un modo de transporte * @param key - Clave del modo de transporte a eliminar */ - delete: (key: string) => - // CORREGIDO: Slash final después de la key + delete: (key: string) => api.delete(`/v1/public/refrence_data/transport-modes/${key}/`) }; \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/refrence_data/transport_types.ts b/frontend/src/lib/api/dashboard/refrence_data/transport_types.ts index 20ebe1f8..1da90756 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/transport_types.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/transport_types.ts @@ -5,25 +5,25 @@ import { api } from '$lib/api'; export interface TransportType { - transport_code: string; - description: string; + transport_code: string; + description: string; } export interface TransportTypeListResponse { - items: TransportType[]; - total: number; - page: number; - page_size: number; + items: TransportType[]; + total: number; + page: number; + page_size: number; } export interface CreateTransportTypeData { - transport_code: string; - description: string; + transport_code: string; + description: string; } export interface UpdateTransportTypeData { - transport_code?: string; - description?: string; + transport_code?: string; + description?: string; } /** @@ -35,35 +35,30 @@ export const transportTypesApi = { */ list: (page = 1, pageSize = 50) => api.get( - // CORREGIDO: Añadido '/' antes del '?' `/v1/public/refrence_data/transport-types/?page=${page}&page_size=${pageSize}` ), /** * Obtiene un tipo de transporte por transport_code */ - get: (transportCode: string) => - // CORREGIDO: Añadido '/' al final + get: (transportCode: string) => api.get(`/v1/public/refrence_data/transport-types/${transportCode}/`), /** * Crea un nuevo tipo de transporte */ create: (data: CreateTransportTypeData) => - // CORREGIDO: Añadido '/' al final api.post('/v1/public/refrence_data/transport-types/', data), /** * Actualiza un tipo de transporte existente */ update: (transportCode: string, data: UpdateTransportTypeData) => - // CORREGIDO: Añadido '/' después de la variable api.put(`/v1/public/refrence_data/transport-types/${transportCode}/`, data), /** * Elimina un tipo de transporte */ - delete: (transportCode: string) => - // CORREGIDO: Añadido '/' después de la variable + delete: (transportCode: string) => api.delete(`/v1/public/refrence_data/transport-types/${transportCode}/`) }; \ No newline at end of file diff --git a/frontend/src/lib/api/dashboard/refrence_data/valuation_methods.ts b/frontend/src/lib/api/dashboard/refrence_data/valuation_methods.ts index 944151a8..df4a320c 100644 --- a/frontend/src/lib/api/dashboard/refrence_data/valuation_methods.ts +++ b/frontend/src/lib/api/dashboard/refrence_data/valuation_methods.ts @@ -38,7 +38,7 @@ export const valuationMethodsApi = { list: (page = 1, pageSize = 50) => api.get( // CORREGIDO: Añadido '/' antes del '?' - `/v1/public/refrence_data/valuation-methods/?page=${page}&page_size=${pageSize}` + `/v1/public/reference_data/valuation-methods/?page=${page}&page_size=${pageSize}` ), /** @@ -47,7 +47,7 @@ export const valuationMethodsApi = { */ get: (key: string) => // CORREGIDO: Añadido '/' final - api.get(`/v1/public/refrence_data/valuation-methods/${key}/`), + api.get(`/v1/public/reference_data/valuation-methods/${key}/`), /** * Crea un nuevo método de valoración @@ -55,7 +55,7 @@ export const valuationMethodsApi = { */ create: (data: CreateValuationMethodData) => // CORREGIDO: Añadido '/' final - api.post('/v1/public/refrence_data/valuation-methods/', data), + api.post('/v1/public/reference_data/valuation-methods/', data), /** * Actualiza un método de valoración existente @@ -64,7 +64,7 @@ export const valuationMethodsApi = { */ update: (key: string, data: UpdateValuationMethodData) => // CORREGIDO: Añadido '/' después de la variable key - api.put(`/v1/public/refrence_data/valuation-methods/${key}/`, data), + api.put(`/v1/public/reference_data/valuation-methods/${key}/`, data), /** * Elimina un método de valoración @@ -72,5 +72,5 @@ export const valuationMethodsApi = { */ delete: (key: string) => // CORREGIDO: Añadido '/' después de la variable key - api.delete(`/v1/public/refrence_data/valuation-methods/${key}/`) + api.delete(`/v1/public/reference_data/valuation-methods/${key}/`) }; \ No newline at end of file diff --git a/frontend/src/lib/components/common/Maintenance.svelte b/frontend/src/lib/components/common/Maintenance.svelte new file mode 100644 index 00000000..0b7de82d --- /dev/null +++ b/frontend/src/lib/components/common/Maintenance.svelte @@ -0,0 +1,79 @@ + + +
+ {#if visible} +
+
+ +
+ +

+ En Construcción +

+ +

+ MODULO AUN NO DISPONIBLE! +

+ + + {/if} +
+ + diff --git a/frontend/src/lib/components/dashboard/export/manifest/columns.ts b/frontend/src/lib/components/dashboard/export/manifest/columns.ts new file mode 100644 index 00000000..e3a17864 --- /dev/null +++ b/frontend/src/lib/components/dashboard/export/manifest/columns.ts @@ -0,0 +1,100 @@ +import { renderSnippet } from "$lib/components/ui/data-table/index.js"; +import { createRawSnippet } from "svelte"; +import type { ColumnDef } from "@tanstack/table-core"; +import type { Manifest } from "$lib/api/dashboard/a76/manifests"; + +export function createColumns(): ColumnDef[] { + return [ + { + accessorKey: "manifest_number", + header: "Manifesto", + cell: ({ row }) => { + const manifestSnippet = createRawSnippet<[{ value: string | null }]>((getVal) => { + const { value } = getVal(); + return { + render: () => `${value || '-'}` + }; + }); + return renderSnippet(manifestSnippet, { value: row.original.manifest_number ?? null }); + } + }, + { + accessorKey: "carrier_code", + header: "Transportista", + cell: ({ row }) => { + const carrierSnippet = createRawSnippet<[{ value: string | null }]>((getVal) => { + const { value } = getVal(); + return { + render: () => `
${value || '-'}
` + }; + }); + return renderSnippet(carrierSnippet, { value: row.original.carrier_code ?? null }); + } + }, + { + accessorKey: "person_in_charge", + header: "Persona a cargo", + cell: ({ row }) => { + return row.original.person_in_charge || "-"; + } + }, + { + accessorKey: "foreign_exit_port", + header: "PtoSalMex", + cell: ({ row }) => { + return row.original.foreign_exit_port || "-"; + } + }, + { + accessorKey: "destination_port", + header: "Puerto Destino", + cell: ({ row }) => { + return row.original.destination_port || "-"; + } + }, + { + accessorKey: "entry_port", + header: "Puerto arribo", + cell: ({ row }) => { + return row.original.entry_port || "-"; + } + }, + { + accessorKey: "entry_date", + header: "Fecha entrada", + cell: ({ row }) => { + const dateSnippet = createRawSnippet<[{ value: number | null }]>((getVal) => { + const { value } = getVal(); + let formattedDate = '-'; + if (value) { + const dateStr = value.toString(); + if (dateStr.length === 8) { + formattedDate = `${dateStr.substring(6, 8)}/${dateStr.substring(4, 6)}/${dateStr.substring(0, 4)}`; + } + } + return { + render: () => `
${formattedDate}
` + }; + }); + return renderSnippet(dateSnippet, { value: row.original.entry_date ?? null }); + } + }, + { + accessorKey: "status", + header: "Estatus", + cell: ({ row }) => { + const statusSnippet = createRawSnippet<[{ status: string | null }]>((getS) => { + const { status } = getS(); + let colorClass = "bg-muted text-muted-foreground"; + if (status === 'RECIBIDO') colorClass = "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"; + if (status === 'PENDIENTE') colorClass = "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400"; + + return { + render: () => `${status || 'S.E.'}` + }; + }); + return renderSnippet(statusSnippet, { status: row.original.status ?? null }); + } + } + ]; +} diff --git a/frontend/src/lib/components/dashboard/export/manifest/create-dialog.svelte b/frontend/src/lib/components/dashboard/export/manifest/create-dialog.svelte new file mode 100644 index 00000000..92e47893 --- /dev/null +++ b/frontend/src/lib/components/dashboard/export/manifest/create-dialog.svelte @@ -0,0 +1,171 @@ + + + + + + Nuevo Manifiesto + + Ingresa los datos generales del nuevo manifiesto de exportación. + + + +
+
+

+ Información General +

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

Logística

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