Merge branch 'features/Creacion-formularios_catalogos_generales' into development
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
"""increase_port_description_length
|
||||
|
||||
Revision ID: 3a012dff0274
|
||||
Revises: 7937209f9718
|
||||
Create Date: 2025-12-24 10:24:49.927020
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '3a012dff0274'
|
||||
down_revision: Union[str, Sequence[str], None] = '7937209f9718'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
pass
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
pass
|
||||
@@ -81,6 +81,7 @@ def upgrade() -> None:
|
||||
sa.Column("description", sa.String(length=50), nullable=False),
|
||||
sa.Column("note", sa.String(length=500), nullable=False),
|
||||
sa.Column("type", sa.String(length=15), nullable=False),
|
||||
sa.Column("operation", sa.String(length=5), nullable=False),
|
||||
sa.PrimaryKeyConstraint("key", name="invoice_types_pkey"),
|
||||
schema="public",
|
||||
)
|
||||
|
||||
@@ -192,13 +192,13 @@ def upgrade() -> None:
|
||||
|
||||
values_it = ", ".join(
|
||||
[
|
||||
f"('{key}', '{desc.replace(chr(39), chr(39)*2)}', '{note.replace(chr(39), chr(39)*2)}', '{type.replace(chr(39), chr(39)*2)}')"
|
||||
for key, desc, note, type in invoice_types_seed
|
||||
f"('{key}', '{desc.replace(chr(39), chr(39)*2)}', '{note.replace(chr(39), chr(39)*2)}', '{type.replace(chr(39), chr(39)*2)}', '{operation.replace(chr(39), chr(39)*2)}')"
|
||||
for key, desc, note, type, operation in invoice_types_seed
|
||||
]
|
||||
)
|
||||
op.execute(
|
||||
f"""
|
||||
INSERT INTO public.invoice_types (key, description, note, type) VALUES
|
||||
INSERT INTO public.invoice_types (key, description, note, type, operation) VALUES
|
||||
{values_it}
|
||||
ON CONFLICT (key) DO NOTHING;
|
||||
"""
|
||||
|
||||
@@ -15,7 +15,8 @@ ServiceType = TypeVar("ServiceType")
|
||||
|
||||
|
||||
class TenantCRUDRoutes(
|
||||
Generic[CreateSchemaType, UpdateSchemaType, ResponseSchemaType, ServiceType]
|
||||
Generic[CreateSchemaType, UpdateSchemaType,
|
||||
ResponseSchemaType, ServiceType]
|
||||
):
|
||||
"""
|
||||
Generic CRUD routes factory for tenant-scoped resources
|
||||
@@ -74,7 +75,8 @@ class TenantCRUDRoutes(
|
||||
prefix: str,
|
||||
tags: list[str],
|
||||
resource_name: str = "Resource",
|
||||
id_name: Optional[str] = None, # For parent resources (e.g., "pedimento_id")
|
||||
# For parent resources (e.g., "pedimento_id")
|
||||
id_name: Optional[str] = None,
|
||||
id_type: Type = int, # Type of the ID (int, str, etc.)
|
||||
parent_id_name: Optional[
|
||||
str
|
||||
@@ -128,9 +130,15 @@ class TenantCRUDRoutes(
|
||||
le=self.max_page_size,
|
||||
description="Page size",
|
||||
),
|
||||
status: Optional[str] = Query(None, description="Filter by status"),
|
||||
status: Optional[str] = Query(
|
||||
None, description="Filter by status"),
|
||||
operation_type: Optional[str] = Query(
|
||||
None, description="Filter by operation type"),
|
||||
invoice_type: Optional[str] = Query(
|
||||
None, description="Filter by invoice type"),
|
||||
db: Session = Depends(self.db_dependency),
|
||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||
current_user: Dict[str, Any] = Depends(
|
||||
self.auth_dependency),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(
|
||||
db, company_id, current_user
|
||||
@@ -140,6 +148,10 @@ class TenantCRUDRoutes(
|
||||
filters = {}
|
||||
if status:
|
||||
filters["status"] = status
|
||||
if operation_type:
|
||||
filters["operation_type"] = operation_type
|
||||
if invoice_type:
|
||||
filters["invoice_type"] = invoice_type
|
||||
|
||||
items, total = self.service.get_all(
|
||||
db, tenant_id, company_id, skip, page_size, filters
|
||||
@@ -172,7 +184,8 @@ class TenantCRUDRoutes(
|
||||
description="Page size",
|
||||
),
|
||||
db: Session = Depends(self.db_dependency),
|
||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||
current_user: Dict[str, Any] = Depends(
|
||||
self.auth_dependency),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(
|
||||
db, company_id, current_user
|
||||
@@ -211,7 +224,8 @@ class TenantCRUDRoutes(
|
||||
**path_params,
|
||||
):
|
||||
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
tenant_id = validate_access_to_resource(
|
||||
db, company_id, current_user)
|
||||
parent_id = path_params.get(self.parent_id_name)
|
||||
|
||||
# Try method with 4 params (pedimento_id, tenant_id, company_id)
|
||||
@@ -225,7 +239,8 @@ class TenantCRUDRoutes(
|
||||
db, parent_id, tenant_id, company_id
|
||||
)
|
||||
else:
|
||||
resource = self.service.get(db, parent_id, tenant_id, company_id)
|
||||
resource = self.service.get(
|
||||
db, parent_id, tenant_id, company_id)
|
||||
|
||||
if not resource:
|
||||
raise HTTPException(
|
||||
@@ -249,7 +264,8 @@ class TenantCRUDRoutes(
|
||||
db: Session = Depends(self.db_dependency),
|
||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
tenant_id = validate_access_to_resource(
|
||||
db, company_id, current_user)
|
||||
|
||||
resource = self.service.get_by_id(
|
||||
db, resource_id, tenant_id, company_id
|
||||
@@ -264,10 +280,10 @@ class TenantCRUDRoutes(
|
||||
# POST route
|
||||
if self.parent_id_name:
|
||||
# Child resource - needs parent_id from path
|
||||
|
||||
|
||||
# Create a closure to capture the schema type
|
||||
create_schema = self.create_schema
|
||||
|
||||
|
||||
@self.router.post(
|
||||
"/",
|
||||
response_model=self.response_schema,
|
||||
@@ -281,17 +297,18 @@ class TenantCRUDRoutes(
|
||||
db: Session = Depends(self.db_dependency),
|
||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
tenant_id = validate_access_to_resource(
|
||||
db, company_id, current_user)
|
||||
|
||||
# For child resources, parent_id validation would go here
|
||||
resource = self.service.create(db, data, tenant_id, company_id)
|
||||
return resource
|
||||
else:
|
||||
# Parent resource - no parent_id needed
|
||||
|
||||
|
||||
# Create a closure to capture the schema type
|
||||
create_schema = self.create_schema
|
||||
|
||||
|
||||
@self.router.post(
|
||||
"/",
|
||||
response_model=self.response_schema,
|
||||
@@ -305,7 +322,8 @@ class TenantCRUDRoutes(
|
||||
db: Session = Depends(self.db_dependency),
|
||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
tenant_id = validate_access_to_resource(
|
||||
db, company_id, current_user)
|
||||
resource = self.service.create(db, data, tenant_id, company_id)
|
||||
return resource
|
||||
|
||||
@@ -314,10 +332,10 @@ class TenantCRUDRoutes(
|
||||
# For child resources: PUT / (parent_id comes from path)
|
||||
if self.parent_id_name:
|
||||
# Child resource
|
||||
|
||||
|
||||
# Create a closure to capture the schema type
|
||||
update_schema = self.update_schema
|
||||
|
||||
|
||||
@self.router.put(
|
||||
"/",
|
||||
response_model=self.response_schema,
|
||||
@@ -331,7 +349,8 @@ class TenantCRUDRoutes(
|
||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||
**path_params,
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
tenant_id = validate_access_to_resource(
|
||||
db, company_id, current_user)
|
||||
parent_id = path_params.get(self.parent_id_name)
|
||||
|
||||
resource = self.service.update(
|
||||
@@ -346,10 +365,10 @@ class TenantCRUDRoutes(
|
||||
|
||||
else:
|
||||
# Parent resource
|
||||
|
||||
|
||||
# Create a closure to capture the schema type
|
||||
update_schema = self.update_schema
|
||||
|
||||
|
||||
@self.router.put(
|
||||
f"/{{{self.id_name}}}",
|
||||
response_model=self.response_schema,
|
||||
@@ -366,7 +385,8 @@ class TenantCRUDRoutes(
|
||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||
):
|
||||
f"""Update {self.resource_name}"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
tenant_id = validate_access_to_resource(
|
||||
db, company_id, current_user)
|
||||
|
||||
resource = self.service.update(
|
||||
db, resource_id, tenant_id, data, company_id
|
||||
@@ -395,10 +415,12 @@ class TenantCRUDRoutes(
|
||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||
**path_params,
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
tenant_id = validate_access_to_resource(
|
||||
db, company_id, current_user)
|
||||
parent_id = path_params.get(self.parent_id_name)
|
||||
|
||||
success = self.service.delete(db, parent_id, tenant_id, company_id)
|
||||
success = self.service.delete(
|
||||
db, parent_id, tenant_id, company_id)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
@@ -422,9 +444,11 @@ class TenantCRUDRoutes(
|
||||
db: Session = Depends(self.db_dependency),
|
||||
current_user: Dict[str, Any] = Depends(self.auth_dependency),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
tenant_id = validate_access_to_resource(
|
||||
db, company_id, current_user)
|
||||
|
||||
success = self.service.delete(db, resource_id, tenant_id, company_id)
|
||||
success = self.service.delete(
|
||||
db, resource_id, tenant_id, company_id)
|
||||
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
|
||||
@@ -14,5 +14,6 @@ class ClassificationConcept(Base, TenantScopedMixin, TimestampMixin):
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
classification: Mapped[str] = mapped_column(
|
||||
String(30), nullable=False) # CLASIFICACION
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
from typing import List, Optional, Tuple, Dict, Any
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from fastapi import HTTPException
|
||||
import logging
|
||||
|
||||
from .models import ClassificationConcept
|
||||
from .dto import ClassificationConceptCreate, ClassificationConceptUpdate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ClassificationConceptService:
|
||||
@staticmethod
|
||||
@@ -76,6 +81,14 @@ class ClassificationConceptService:
|
||||
if not db_obj:
|
||||
return False
|
||||
|
||||
db.delete(db_obj)
|
||||
db.commit()
|
||||
return True
|
||||
try:
|
||||
db.delete(db_obj)
|
||||
db.commit()
|
||||
return True
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error de integridad al eliminar clasificación de concepto {id}: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="No se puede eliminar esta clasificación porque tiene registros relacionados (pedimentos, facturas, etc.). Primero debe eliminar o reasignar esos registros."
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Rutas para gestión de empresa
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -17,7 +17,78 @@ from .service import CompanyService
|
||||
# Main router that includes base CRUD
|
||||
router = APIRouter(prefix="/company")
|
||||
|
||||
# Custom endpoints
|
||||
@router.post(
|
||||
"", # Se suma al prefix, queda POST /api/v1/a76/company
|
||||
response_model=CompanyResponseDTO,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Create a new company",
|
||||
)
|
||||
async def create_company(
|
||||
data: CompanyCreateDTO,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
|
||||
tenant_id = current_user.get("tenant_id")
|
||||
if not tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Tenant ID not found in user data",
|
||||
)
|
||||
|
||||
service = CompanyService(db)
|
||||
return service.create_company_manually(data, tenant_id=tenant_id)
|
||||
|
||||
|
||||
@router.get(
|
||||
"", # GET /api/v1/a76/company with pagination
|
||||
response_model=dict,
|
||||
summary="Get companies with pagination",
|
||||
)
|
||||
async def list_companies(
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
name: Optional[str] = None,
|
||||
rfc: Optional[str] = None,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Get paginated list of companies for current tenant with optional filters"""
|
||||
tenant_id = current_user.get("tenant_id")
|
||||
if not tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Tenant ID not found in user data",
|
||||
)
|
||||
|
||||
skip = (page - 1) * page_size
|
||||
filters = {}
|
||||
if name:
|
||||
filters["name"] = name
|
||||
if rfc:
|
||||
filters["rfc"] = rfc
|
||||
|
||||
service = CompanyService(db)
|
||||
items, total = service.get_all(
|
||||
db,
|
||||
tenant_id,
|
||||
company_id=0, # Not used for companies
|
||||
skip=skip,
|
||||
limit=page_size,
|
||||
filters=filters if filters else None
|
||||
)
|
||||
|
||||
total_pages = (total + page_size - 1) // page_size
|
||||
|
||||
return {
|
||||
"items": [CompanyResponseDTO.model_validate(item) for item in items],
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"pages": total_pages,
|
||||
}
|
||||
|
||||
|
||||
@router.get(
|
||||
"/my-companies",
|
||||
response_model=List[CompanyResponseDTO],
|
||||
@@ -166,17 +237,90 @@ async def get_program_info(
|
||||
"prosec": company.prosec,
|
||||
"prosec_authorization": company.prosec_authorization,
|
||||
}
|
||||
|
||||
# Base CRUD routes using TenantCRUDRoutes
|
||||
base_router = TenantCRUDRoutes(
|
||||
service=CompanyService,
|
||||
create_schema=CompanyCreateDTO,
|
||||
update_schema=CompanyUpdateDTO,
|
||||
response_schema=CompanyResponseDTO,
|
||||
prefix="",
|
||||
tags=[],
|
||||
id_name="id",
|
||||
enable_list=True,
|
||||
enable_filters=True,
|
||||
).router
|
||||
router.include_router(base_router)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{company_id}",
|
||||
response_model=CompanyResponseDTO,
|
||||
summary="Get company by ID",
|
||||
)
|
||||
async def get_company(
|
||||
company_id: int,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Get a specific company by ID"""
|
||||
tenant_id = current_user.get("tenant_id")
|
||||
if not tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Tenant ID not found in user data",
|
||||
)
|
||||
|
||||
company = CompanyService.get_by_id(db, company_id, tenant_id, 0)
|
||||
if not company:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Company not found",
|
||||
)
|
||||
|
||||
return CompanyResponseDTO.model_validate(company)
|
||||
|
||||
|
||||
@router.put(
|
||||
"/{company_id}",
|
||||
response_model=CompanyResponseDTO,
|
||||
summary="Update company",
|
||||
)
|
||||
async def update_company(
|
||||
company_id: int,
|
||||
data: CompanyUpdateDTO,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Update a company"""
|
||||
tenant_id = current_user.get("tenant_id")
|
||||
if not tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Tenant ID not found in user data",
|
||||
)
|
||||
|
||||
updated_company = CompanyService.update(
|
||||
db, company_id, tenant_id, 0, data
|
||||
)
|
||||
if not updated_company:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Company not found",
|
||||
)
|
||||
|
||||
return CompanyResponseDTO.model_validate(updated_company)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{company_id}",
|
||||
status_code=status.HTTP_204_NO_CONTENT,
|
||||
summary="Delete company",
|
||||
)
|
||||
async def delete_company(
|
||||
company_id: int,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Delete a company"""
|
||||
tenant_id = current_user.get("tenant_id")
|
||||
if not tenant_id:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Tenant ID not found in user data",
|
||||
)
|
||||
|
||||
success = CompanyService.delete(db, company_id, tenant_id, 0)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Company not found",
|
||||
)
|
||||
|
||||
return None
|
||||
@@ -64,6 +64,7 @@ class CompanyService:
|
||||
.first()
|
||||
)
|
||||
|
||||
# ESTE ES EL MÉTODO VIEJO QUE CAUSABA PROBLEMAS (Lo dejamos por si acaso)
|
||||
@staticmethod
|
||||
def create(
|
||||
db: Session,
|
||||
@@ -71,7 +72,7 @@ class CompanyService:
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
) -> Company:
|
||||
"""Create a new company"""
|
||||
"""Create a new company (MÉTODO GENÉRICO - NO USAR PARA CREACIÓN MANUAL)"""
|
||||
try:
|
||||
db_company = Company(
|
||||
**company_data.model_dump(exclude_unset=True),
|
||||
@@ -136,10 +137,20 @@ class CompanyService:
|
||||
db.delete(company)
|
||||
db.commit()
|
||||
return True
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
logger.error(f"IntegrityError deleting company {company_id}: {str(e)}")
|
||||
# Check if it's a foreign key constraint
|
||||
if "foreign key constraint" in str(e).lower():
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="No se puede eliminar la empresa porque tiene registros relacionados (facturas, conceptos, etc.)"
|
||||
)
|
||||
raise HTTPException(status_code=400, detail="Error al eliminar la empresa")
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error deleting company {company_id}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Error deleting company")
|
||||
raise HTTPException(status_code=500, detail="Error al eliminar la empresa")
|
||||
|
||||
# Custom methods
|
||||
def get_companies_by_tenant(self, tenant_id: int) -> List[Company]:
|
||||
@@ -159,3 +170,31 @@ class CompanyService:
|
||||
.first()
|
||||
is not None
|
||||
)
|
||||
|
||||
def create_company_manually(self, data: CompanyCreateDTO, tenant_id: int) -> Company:
|
||||
|
||||
try:
|
||||
# 1. Preparar datos
|
||||
obj_data = data.model_dump(exclude_unset=True)
|
||||
|
||||
# 2. Crear objeto SQLAlchemy
|
||||
db_obj = Company(**obj_data, tenant_id=tenant_id)
|
||||
|
||||
# 3. Guardar
|
||||
self.db.add(db_obj)
|
||||
self.db.commit()
|
||||
self.db.refresh(db_obj)
|
||||
|
||||
return db_obj
|
||||
|
||||
except IntegrityError as e:
|
||||
self.db.rollback()
|
||||
logger.error(f"IntegrityError creating company manually: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Error de integridad: Es posible que esta empresa ya exista.",
|
||||
)
|
||||
except Exception as e:
|
||||
self.db.rollback()
|
||||
logger.error(f"Error creating company manually: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Error creando empresa: {str(e)}")
|
||||
@@ -4,6 +4,7 @@ from pydantic import BaseModel, Field, ConfigDict
|
||||
|
||||
class ConceptBase(BaseModel):
|
||||
code: str = Field(..., max_length=15, description="Concept Code (CLAVE)")
|
||||
company_id: int = Field(..., description="Company ID")
|
||||
description: Optional[str] = Field(
|
||||
None, max_length=120, description="Description")
|
||||
description_en: Optional[str] = Field(
|
||||
@@ -18,7 +19,6 @@ class ConceptBase(BaseModel):
|
||||
section: Optional[int] = Field(None, description="Section")
|
||||
classification: Optional[str] = Field(
|
||||
None, max_length=30, description="Classification")
|
||||
company_id: int = Field(..., description="Company ID")
|
||||
|
||||
|
||||
class ConceptCreate(ConceptBase):
|
||||
@@ -32,7 +32,7 @@ class ConceptUpdate(BaseModel):
|
||||
detailed_description: Optional[str] = Field(None, max_length=1000)
|
||||
priority: Optional[int] = None
|
||||
priority_ame: Optional[int] = None
|
||||
first_total: Optional[bool] = None
|
||||
first_total: Optional[bool] = None
|
||||
type: Optional[str] = Field(None, max_length=9)
|
||||
is_printed: Optional[bool] = None
|
||||
section: Optional[int] = None
|
||||
|
||||
@@ -18,25 +18,39 @@ class Concept(Base, TenantScopedMixin, TimestampMixin):
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
company_id: Mapped[int] = mapped_column(
|
||||
Integer, nullable=False) # IDEMPRESA
|
||||
|
||||
code: Mapped[str] = mapped_column(String(15), nullable=False) # CLAVE
|
||||
|
||||
description: Mapped[Optional[str]] = mapped_column(
|
||||
String(120), nullable=True) # DESCRIPCION
|
||||
|
||||
description_en: Mapped[Optional[str]] = mapped_column(
|
||||
String(120), nullable=True) # DESCRIPCIONINGLES
|
||||
|
||||
detailed_description: Mapped[Optional[str]] = mapped_column(
|
||||
String(1000), nullable=True) # DESCRIPCIONDETALLADA
|
||||
|
||||
priority: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, nullable=True) # PRIORIDAD
|
||||
|
||||
priority_ame: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, nullable=True) # PRIORIDADAME
|
||||
|
||||
first_total: Mapped[Optional[bool]] = mapped_column(
|
||||
Boolean, nullable=True) # PRIMERTOTAL
|
||||
|
||||
type: Mapped[Optional[str]] = mapped_column(
|
||||
String(9), nullable=True) # TIPO
|
||||
|
||||
is_printed: Mapped[Optional[bool]] = mapped_column(
|
||||
Boolean, nullable=True) # SEIMPRIME
|
||||
|
||||
section: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, nullable=True) # SECCION
|
||||
|
||||
classification: Mapped[Optional[str]] = mapped_column(String(30), ForeignKey(
|
||||
"a76.classification_concepts.classification"), nullable=True) # CLASIFICACION
|
||||
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
from typing import List, Optional, Tuple, Dict, Any
|
||||
import logging
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from fastapi import HTTPException
|
||||
|
||||
from .models import Concept
|
||||
from .dto import ConceptCreate, ConceptUpdate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ConceptService:
|
||||
@staticmethod
|
||||
@@ -74,6 +80,20 @@ class ConceptService:
|
||||
if not db_obj:
|
||||
return False
|
||||
|
||||
db.delete(db_obj)
|
||||
db.commit()
|
||||
return True
|
||||
try:
|
||||
db.delete(db_obj)
|
||||
db.commit()
|
||||
return True
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
logger.error(f"IntegrityError deleting concept {id}: {str(e)}")
|
||||
if "foreign key constraint" in str(e).lower():
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="No se puede eliminar el concepto porque tiene registros relacionados"
|
||||
)
|
||||
raise HTTPException(status_code=400, detail="Error al eliminar el concepto")
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error deleting concept {id}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Error al eliminar el concepto")
|
||||
|
||||
@@ -4,8 +4,7 @@ from pydantic import BaseModel, Field, ConfigDict
|
||||
|
||||
|
||||
class CustomsBrokerConceptBase(BaseModel):
|
||||
broker_key: str = Field(..., max_length=5,
|
||||
description="Customs Broker Key (CLAVEAA)")
|
||||
broker_key: str = Field(..., max_length=5, description="Customs Broker Key (CLAVEAA)")
|
||||
concept: str = Field(..., max_length=15, description="Concept")
|
||||
amount: Optional[Decimal] = Field(None, description="Amount")
|
||||
priority: Optional[int] = Field(None, description="Priority")
|
||||
|
||||
@@ -9,17 +9,23 @@ from core.database import Base
|
||||
class CustomsBrokerConcept(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "customs_broker_concepts"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("broker_key", "concept", name="uq_broker_concept"),
|
||||
UniqueConstraint("broker_key", "concept", "company_id", name="uq_broker_concept"),
|
||||
{"schema": "a76"}
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
company_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
broker_key: Mapped[str] = mapped_column(
|
||||
String(5), nullable=False) # CLAVEAA
|
||||
|
||||
concept: Mapped[str] = mapped_column(
|
||||
String(15), nullable=False) # CONCEPTO
|
||||
|
||||
amount: Mapped[Optional[Decimal]] = mapped_column(
|
||||
Numeric(11, 2), nullable=True) # IMPORTE
|
||||
|
||||
priority: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, nullable=True) # PRIORIDAD
|
||||
|
||||
@@ -7,7 +7,7 @@ router = TenantCRUDRoutes(
|
||||
create_schema=CustomsBrokerConceptCreate,
|
||||
update_schema=CustomsBrokerConceptUpdate,
|
||||
response_schema=CustomsBrokerConceptResponse,
|
||||
prefix="/customs-broker-concepts",
|
||||
prefix="/customs-broker-concepts",
|
||||
tags=["a76.general_catalogs.customs_broker_concepts"],
|
||||
resource_name="Customs Broker Concept",
|
||||
enable_list=True,
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
from typing import List, Optional, Tuple, Dict, Any
|
||||
import logging
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from fastapi import HTTPException
|
||||
|
||||
from .models import CustomsBrokerConcept
|
||||
from .dto import CustomsBrokerConceptCreate, CustomsBrokerConceptUpdate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CustomsBrokerConceptService:
|
||||
@staticmethod
|
||||
@@ -76,6 +82,20 @@ class CustomsBrokerConceptService:
|
||||
if not db_obj:
|
||||
return False
|
||||
|
||||
db.delete(db_obj)
|
||||
db.commit()
|
||||
return True
|
||||
try:
|
||||
db.delete(db_obj)
|
||||
db.commit()
|
||||
return True
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
logger.error(f"IntegrityError deleting customs broker concept {id}: {str(e)}")
|
||||
if "foreign key constraint" in str(e).lower():
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="No se puede eliminar el concepto porque tiene registros relacionados"
|
||||
)
|
||||
raise HTTPException(status_code=400, detail="Error al eliminar el concepto")
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error deleting customs broker concept {id}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Error al eliminar el concepto")
|
||||
|
||||
@@ -340,7 +340,7 @@ class DodaUpdateDTO(BaseModel):
|
||||
class DodaResponseDTO(BaseModel):
|
||||
"""DTO para responder con datos de un DODA"""
|
||||
|
||||
sys_id: int
|
||||
id: int
|
||||
integration_number: Optional[str] = None
|
||||
doda_date: Optional[int] = None
|
||||
doda_time: Optional[int] = None
|
||||
@@ -383,7 +383,7 @@ class DodaResponseDTO(BaseModel):
|
||||
class DodaDetailResponseDTO(BaseModel):
|
||||
"""DTO detallado para responder con todos los datos de un DODA"""
|
||||
|
||||
sys_id: int
|
||||
id: int
|
||||
integration_number: Optional[str] = None
|
||||
doda_date: Optional[int] = None
|
||||
doda_time: Optional[int] = None
|
||||
|
||||
@@ -151,19 +151,19 @@ class DodaContainerSeal(Base, TenantScopedMixin, TimestampMixin):
|
||||
{"schema": "a76"},
|
||||
)
|
||||
|
||||
# Primary key
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
# Foreign key and line info
|
||||
|
||||
container_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
doda_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
seal_line: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
# Seal information
|
||||
|
||||
seal_value: Mapped[Optional[str]] = mapped_column(String(21))
|
||||
|
||||
# Relationships
|
||||
|
||||
container: Mapped["DodaContainer"] = relationship(
|
||||
"DodaContainer", back_populates="seals_detail"
|
||||
)
|
||||
|
||||
@@ -131,14 +131,21 @@ class DodaService:
|
||||
db: Session, id: int, tenant_id: int, company_id: int
|
||||
) -> bool:
|
||||
"""Delete a DODA"""
|
||||
try:
|
||||
db_doda = DodaService.get_by_id(db, id, tenant_id, company_id)
|
||||
if not db_doda:
|
||||
return False
|
||||
db_doda = DodaService.get_by_id(db, id, tenant_id, company_id)
|
||||
if not db_doda:
|
||||
return False
|
||||
|
||||
try:
|
||||
db.delete(db_doda)
|
||||
db.commit()
|
||||
return True
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error de integridad al eliminar DODA {id}: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="No se puede eliminar este DODA porque tiene registros relacionados. Primero debe eliminar o reasignar esos registros."
|
||||
)
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error deleting DODA: {str(e)}")
|
||||
|
||||
@@ -68,7 +68,7 @@ class ElectronicNoticeUpdateDTO(BaseModel):
|
||||
class ElectronicNoticeResponseDTO(BaseModel):
|
||||
"""DTO para responder con datos de un aviso electrónico"""
|
||||
|
||||
sys_id: int
|
||||
id: int
|
||||
notice_number: Optional[str] = None
|
||||
year: Optional[str] = None
|
||||
patent: Optional[str] = None
|
||||
|
||||
@@ -30,22 +30,27 @@ class EquivalencyItemResponse(EquivalencyItemBase):
|
||||
|
||||
|
||||
class EquivalencyBase(BaseModel):
|
||||
identifier: str = Field(..., max_length=10, description="Identifier")
|
||||
fraccion_mex: str = Field(..., max_length=10, description="Fraccion MX (Identifier)")
|
||||
fraccion_us: str = Field(..., max_length=100, description="Fraccion US (External Field)")
|
||||
description: Optional[str] = Field(
|
||||
None, max_length=200, description="Description")
|
||||
|
||||
|
||||
class EquivalencyCreate(EquivalencyBase):
|
||||
items: Optional[List[EquivalencyItemCreate]] = []
|
||||
pass
|
||||
|
||||
|
||||
class EquivalencyUpdate(BaseModel):
|
||||
identifier: Optional[str] = Field(None, max_length=10)
|
||||
fraccion_mex: Optional[str] = Field(None, max_length=10)
|
||||
fraccion_us: Optional[str] = Field(None, max_length=100)
|
||||
description: Optional[str] = Field(None, max_length=200)
|
||||
|
||||
|
||||
class EquivalencyResponse(EquivalencyBase):
|
||||
id: int
|
||||
items: List[EquivalencyItemResponse] = []
|
||||
tenant_id: int
|
||||
company_id: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -16,7 +16,9 @@ class Equivalency(Base, TenantScopedMixin, TimestampMixin):
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
identifier: Mapped[str] = mapped_column(String(10), nullable=False)
|
||||
|
||||
description: Mapped[Optional[str]] = mapped_column(
|
||||
String(200), nullable=True)
|
||||
|
||||
@@ -39,11 +41,15 @@ class EquivalencyItem(Base, TenantScopedMixin, TimestampMixin):
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
equivalency_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("a76.equivalencies.id"), nullable=False)
|
||||
|
||||
original_field: Mapped[str] = mapped_column(
|
||||
String(100), nullable=False) # Relation to Unit of Measure
|
||||
|
||||
external_field: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||
|
||||
equivalency: Mapped["Equivalency"] = relationship(back_populates="items")
|
||||
|
||||
unit_of_measure: Mapped["UnitOfMeasure"] = relationship()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from typing import List, Optional, Tuple, Dict, Any
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm import Session, joinedload
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from fastapi import HTTPException
|
||||
|
||||
from .models import Equivalency, EquivalencyItem
|
||||
@@ -20,14 +21,26 @@ class EquivalencyService:
|
||||
query = db.query(Equivalency).filter(
|
||||
Equivalency.tenant_id == tenant_id,
|
||||
Equivalency.company_id == company_id
|
||||
)
|
||||
).options(joinedload(Equivalency.items))
|
||||
|
||||
if filters:
|
||||
# Add filters here if needed
|
||||
pass
|
||||
if 'fraccion_mex' in filters:
|
||||
query = query.filter(Equivalency.identifier.ilike(f"%{filters['fraccion_mex']}%"))
|
||||
if 'description' in filters:
|
||||
query = query.filter(Equivalency.description.ilike(f"%{filters['description']}%"))
|
||||
|
||||
total = query.count()
|
||||
items = query.offset(skip).limit(limit).all()
|
||||
|
||||
# Map internal fields to DTO fields
|
||||
for item in items:
|
||||
item.fraccion_mex = item.identifier
|
||||
# Try to find the first item to get fraccion_us
|
||||
if item.items:
|
||||
item.fraccion_us = item.items[0].external_field
|
||||
else:
|
||||
item.fraccion_us = ""
|
||||
|
||||
return items, total
|
||||
|
||||
@staticmethod
|
||||
@@ -37,11 +50,20 @@ class EquivalencyService:
|
||||
tenant_id: int,
|
||||
company_id: int
|
||||
) -> Optional[Equivalency]:
|
||||
return db.query(Equivalency).filter(
|
||||
item = db.query(Equivalency).filter(
|
||||
Equivalency.id == id,
|
||||
Equivalency.tenant_id == tenant_id,
|
||||
Equivalency.company_id == company_id
|
||||
).first()
|
||||
).options(joinedload(Equivalency.items)).first()
|
||||
|
||||
if item:
|
||||
item.fraccion_mex = item.identifier
|
||||
if item.items:
|
||||
item.fraccion_us = item.items[0].external_field
|
||||
else:
|
||||
item.fraccion_us = ""
|
||||
|
||||
return item
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
@@ -50,50 +72,122 @@ class EquivalencyService:
|
||||
tenant_id: int,
|
||||
company_id: int
|
||||
) -> Equivalency:
|
||||
db_obj = Equivalency(
|
||||
identifier=data.identifier,
|
||||
description=data.description,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id
|
||||
)
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
try:
|
||||
# Create Parent
|
||||
db_obj = Equivalency(
|
||||
identifier=data.fraccion_mex,
|
||||
description=data.description,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id
|
||||
)
|
||||
db.add(db_obj)
|
||||
db.flush() # Flush to get ID
|
||||
|
||||
if data.items:
|
||||
for item_data in data.items:
|
||||
item = EquivalencyItem(
|
||||
**item_data.model_dump(),
|
||||
equivalency_id=db_obj.id,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id
|
||||
)
|
||||
db.add(item)
|
||||
# Create Child Item (mapping fraccion_mex -> original_field, fraccion_us -> external_field)
|
||||
item = EquivalencyItem(
|
||||
equivalency_id=db_obj.id,
|
||||
original_field=data.fraccion_mex, # Must exist in units_of_measure
|
||||
external_field=data.fraccion_us,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id
|
||||
)
|
||||
db.add(item)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
|
||||
return db_obj
|
||||
|
||||
# Map for response
|
||||
db_obj.fraccion_mex = db_obj.identifier
|
||||
db_obj.fraccion_us = item.external_field
|
||||
|
||||
return db_obj
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
error_msg = str(e.orig) if hasattr(e, 'orig') else str(e)
|
||||
print(f"IntegrityError in create: {error_msg}")
|
||||
|
||||
if "units_of_measure" in error_msg:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"La Fracción MX '{data.fraccion_mex}' no es válida. Debe existir en el catálogo de Unidades de Medida."
|
||||
)
|
||||
if "uq_equivalency_identifier" in error_msg:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Ya existe una equivalencia para la Fracción MX '{data.fraccion_mex}'."
|
||||
)
|
||||
raise HTTPException(status_code=400, detail=f"Error al guardar: {error_msg}")
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print(f"Error in create: {str(e)}")
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def update(
|
||||
db: Session,
|
||||
id: int,
|
||||
data: EquivalencyUpdate,
|
||||
tenant_id: int,
|
||||
data: EquivalencyUpdate,
|
||||
company_id: int
|
||||
) -> Optional[Equivalency]:
|
||||
db_obj = EquivalencyService.get_by_id(db, id, tenant_id, company_id)
|
||||
if not db_obj:
|
||||
return None
|
||||
|
||||
update_dict = data.model_dump(exclude_unset=True)
|
||||
for key, value in update_dict.items():
|
||||
if key != 'items': # Handle items separately if needed, or ignore for now as per original code
|
||||
setattr(db_obj, key, value)
|
||||
try:
|
||||
if data.fraccion_mex:
|
||||
db_obj.identifier = data.fraccion_mex
|
||||
if data.description:
|
||||
db_obj.description = data.description
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
# Update Item
|
||||
item = None
|
||||
if db_obj.items:
|
||||
item = db_obj.items[0]
|
||||
|
||||
if item:
|
||||
if data.fraccion_us:
|
||||
item.external_field = data.fraccion_us
|
||||
if data.fraccion_mex:
|
||||
item.original_field = data.fraccion_mex
|
||||
else:
|
||||
# Create if missing
|
||||
if data.fraccion_us or data.fraccion_mex:
|
||||
item = EquivalencyItem(
|
||||
equivalency_id=db_obj.id,
|
||||
original_field=data.fraccion_mex or db_obj.identifier,
|
||||
external_field=data.fraccion_us or "",
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id
|
||||
)
|
||||
db.add(item)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
|
||||
# Map for response
|
||||
db_obj.fraccion_mex = db_obj.identifier
|
||||
if db_obj.items:
|
||||
db_obj.fraccion_us = db_obj.items[0].external_field
|
||||
else:
|
||||
db_obj.fraccion_us = ""
|
||||
|
||||
return db_obj
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
error_msg = str(e.orig) if hasattr(e, 'orig') else str(e)
|
||||
print(f"IntegrityError in update: {error_msg}")
|
||||
|
||||
if "units_of_measure" in error_msg:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"La Fracción MX '{data.fraccion_mex or db_obj.identifier}' no es válida. Debe existir en el catálogo de Unidades de Medida."
|
||||
)
|
||||
raise HTTPException(status_code=400, detail=f"Error al actualizar: {error_msg}")
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print(f"Error in update: {str(e)}")
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def delete(
|
||||
@@ -110,7 +204,6 @@ class EquivalencyService:
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
|
||||
class EquivalencyItemService:
|
||||
@staticmethod
|
||||
def get_all(
|
||||
@@ -125,11 +218,6 @@ class EquivalencyItemService:
|
||||
EquivalencyItem.tenant_id == tenant_id,
|
||||
EquivalencyItem.company_id == company_id
|
||||
)
|
||||
|
||||
if filters and "equivalency_id" in filters:
|
||||
query = query.filter(
|
||||
EquivalencyItem.equivalency_id == filters["equivalency_id"])
|
||||
|
||||
total = query.count()
|
||||
items = query.offset(skip).limit(limit).all()
|
||||
return items, total
|
||||
@@ -154,13 +242,6 @@ class EquivalencyItemService:
|
||||
tenant_id: int,
|
||||
company_id: int
|
||||
) -> EquivalencyItem:
|
||||
# Note: equivalency_id should be in data or handled by the caller if it's a nested route
|
||||
# But TenantCRUDRoutes for child resources might pass it in filters or we need to handle it.
|
||||
# For now, assuming it's in data or we don't use child resource feature yet.
|
||||
|
||||
# If using child resource, the parent_id is usually passed in the path.
|
||||
# But TenantCRUDRoutes passes the body.
|
||||
|
||||
db_obj = EquivalencyItem(
|
||||
**data.model_dump(),
|
||||
tenant_id=tenant_id,
|
||||
@@ -180,8 +261,9 @@ class EquivalencyItemService:
|
||||
company_id: int
|
||||
) -> EquivalencyItem:
|
||||
db_obj = EquivalencyItem(
|
||||
**data.model_dump(),
|
||||
equivalency_id=equivalency_id,
|
||||
original_field=data.original_field,
|
||||
external_field=data.external_field,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id
|
||||
)
|
||||
@@ -194,19 +276,17 @@ class EquivalencyItemService:
|
||||
def update(
|
||||
db: Session,
|
||||
id: int,
|
||||
data: EquivalencyItemUpdate,
|
||||
tenant_id: int,
|
||||
data: EquivalencyItemUpdate,
|
||||
company_id: int
|
||||
) -> Optional[EquivalencyItem]:
|
||||
db_obj = EquivalencyItemService.get_by_id(
|
||||
db, id, tenant_id, company_id)
|
||||
db_obj = EquivalencyItemService.get_by_id(db, id, tenant_id, company_id)
|
||||
if not db_obj:
|
||||
return None
|
||||
|
||||
update_dict = data.model_dump(exclude_unset=True)
|
||||
for key, value in update_dict.items():
|
||||
|
||||
for key, value in data.model_dump(exclude_unset=True).items():
|
||||
setattr(db_obj, key, value)
|
||||
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
@@ -218,11 +298,9 @@ class EquivalencyItemService:
|
||||
tenant_id: int,
|
||||
company_id: int
|
||||
) -> bool:
|
||||
db_obj = EquivalencyItemService.get_by_id(
|
||||
db, id, tenant_id, company_id)
|
||||
db_obj = EquivalencyItemService.get_by_id(db, id, tenant_id, company_id)
|
||||
if not db_obj:
|
||||
return False
|
||||
|
||||
db.delete(db_obj)
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
@@ -24,7 +24,8 @@ class ErrorClassification(Base, TenantScopedMixin, TimestampMixin):
|
||||
|
||||
# Primary key
|
||||
id: Mapped[int] = mapped_column(
|
||||
Integer, primary_key=True, autoincrement=True)
|
||||
Integer, primary_key=True, autoincrement=True
|
||||
)
|
||||
|
||||
# Classification code (unique)
|
||||
code: Mapped[str] = mapped_column(String(100), nullable=False, unique=True)
|
||||
@@ -60,7 +61,8 @@ class ErrorCatalog(Base, TenantScopedMixin, TimestampMixin):
|
||||
|
||||
# Primary key
|
||||
id: Mapped[int] = mapped_column(
|
||||
Integer, primary_key=True, autoincrement=True)
|
||||
Integer, primary_key=True, autoincrement=True
|
||||
)
|
||||
|
||||
# Error code (unique)
|
||||
code: Mapped[str] = mapped_column(String(15), nullable=False, unique=True)
|
||||
@@ -77,4 +79,4 @@ class ErrorCatalog(Base, TenantScopedMixin, TimestampMixin):
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<ErrorCatalog(id={self.id}, code={self.code}, description={self.description}, classification_id={self.classification_id})>"
|
||||
return f"<ErrorCatalog(id={self.id}, code={self.code}, description={self.description}, classification_id={self.classification_id})>"
|
||||
@@ -30,6 +30,9 @@ class ExchangeRate(Base, TenantScopedMixin, TimestampMixin):
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
|
||||
date: Mapped[datetime] = mapped_column(DateTime)
|
||||
|
||||
value: Mapped[Optional[Decimal]] = mapped_column(DECIMAL(13, 6))
|
||||
|
||||
local_currency: Mapped[Optional[str]] = mapped_column(String(7))
|
||||
|
||||
foreign_currency: Mapped[Optional[str]] = mapped_column(String(7))
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
|
||||
# Identifier DTOs
|
||||
|
||||
|
||||
class IdentifierBase(BaseModel):
|
||||
code: str = Field(..., max_length=2, description="Identifier Code (CLAVE)")
|
||||
description: Optional[str] = Field(
|
||||
@@ -11,28 +8,23 @@ class IdentifierBase(BaseModel):
|
||||
level: Optional[str] = Field(None, max_length=1, description="Level")
|
||||
complement: Optional[str] = Field(
|
||||
None, max_length=5000, description="Complement")
|
||||
company_id: int = Field(..., description="Company ID")
|
||||
|
||||
|
||||
class IdentifierCreate(IdentifierBase):
|
||||
pass
|
||||
|
||||
|
||||
class IdentifierUpdate(BaseModel):
|
||||
code: Optional[str] = Field(None, max_length=2)
|
||||
description: Optional[str] = Field(None, max_length=1000)
|
||||
level: Optional[str] = Field(None, max_length=1)
|
||||
complement: Optional[str] = Field(None, max_length=5000)
|
||||
|
||||
|
||||
class IdentifierResponse(IdentifierBase):
|
||||
id: int
|
||||
tenant_id: int
|
||||
company_id: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
# Identifier Detail DTOs
|
||||
|
||||
|
||||
class IdentifierDetailBase(BaseModel):
|
||||
invoice_consecutive: Optional[int] = Field(
|
||||
@@ -47,13 +39,10 @@ class IdentifierDetailBase(BaseModel):
|
||||
None, max_length=51, description="Complement 2")
|
||||
complement3: Optional[str] = Field(
|
||||
None, max_length=50, description="Complement 3")
|
||||
company_id: int = Field(..., description="Company ID")
|
||||
|
||||
|
||||
class IdentifierDetailCreate(IdentifierDetailBase):
|
||||
pass
|
||||
|
||||
|
||||
class IdentifierDetailUpdate(BaseModel):
|
||||
invoice_consecutive: Optional[int] = None
|
||||
part_line: Optional[int] = None
|
||||
@@ -63,9 +52,9 @@ class IdentifierDetailUpdate(BaseModel):
|
||||
complement2: Optional[str] = Field(None, max_length=51)
|
||||
complement3: Optional[str] = Field(None, max_length=50)
|
||||
|
||||
|
||||
class IdentifierDetailResponse(IdentifierDetailBase):
|
||||
id: int
|
||||
tenant_id: int
|
||||
company_id: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -14,11 +14,15 @@ class Identifier(Base, TenantScopedMixin, TimestampMixin):
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
code: Mapped[str] = mapped_column(String(2), nullable=False) # CLAVE
|
||||
|
||||
description: Mapped[Optional[str]] = mapped_column(
|
||||
String(1000), nullable=True) # DESCRIPCION
|
||||
|
||||
level: Mapped[Optional[str]] = mapped_column(
|
||||
String(1), nullable=True) # NIVEL
|
||||
|
||||
complement: Mapped[Optional[str]] = mapped_column(
|
||||
String(5000), nullable=True) # COMPLEMENTO
|
||||
|
||||
|
||||
@@ -68,8 +68,8 @@ class IdentifierService:
|
||||
def update(
|
||||
db: Session,
|
||||
id: int,
|
||||
data: IdentifierUpdate,
|
||||
tenant_id: int,
|
||||
data: IdentifierUpdate,
|
||||
company_id: int
|
||||
) -> Optional[Identifier]:
|
||||
db_obj = IdentifierService.get_by_id(db, id, tenant_id, company_id)
|
||||
|
||||
@@ -2,7 +2,6 @@ from typing import Optional
|
||||
from decimal import Decimal
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
|
||||
|
||||
class INPCBase(BaseModel):
|
||||
year: str = Field(..., max_length=4, description="Year (YYYY)")
|
||||
month: str = Field(..., max_length=2, description="Month (MM)")
|
||||
@@ -12,7 +11,6 @@ class INPCBase(BaseModel):
|
||||
class INPCCreate(INPCBase):
|
||||
pass
|
||||
|
||||
|
||||
class INPCUpdate(BaseModel):
|
||||
year: Optional[str] = Field(None, max_length=4)
|
||||
month: Optional[str] = Field(None, max_length=2)
|
||||
@@ -21,5 +19,7 @@ class INPCUpdate(BaseModel):
|
||||
|
||||
class INPCResponse(INPCBase):
|
||||
id: int
|
||||
tenant_id: int
|
||||
company_id: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -16,7 +16,10 @@ class INPC(Base, TenantScopedMixin, TimestampMixin):
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
year: Mapped[str] = mapped_column(String(4), nullable=False) # ANIO
|
||||
|
||||
month: Mapped[str] = mapped_column(String(2), nullable=False) # MES
|
||||
|
||||
value: Mapped[Optional[Decimal]] = mapped_column(
|
||||
Numeric(19, 8), nullable=True) # VALOR
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
from fastapi import APIRouter
|
||||
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
|
||||
from .models import INPC
|
||||
from .dto import INPCCreate, INPCResponse, INPCUpdate
|
||||
from .service import INPCService
|
||||
|
||||
router = APIRouter(prefix="/inpc", tags=["a76.general_catalogs.inpc"])
|
||||
|
||||
inpc_crud = TenantCRUDRoutes(
|
||||
# Usamos TenantCRUDRoutes directamente
|
||||
router = TenantCRUDRoutes(
|
||||
service=INPCService,
|
||||
create_schema=INPCCreate,
|
||||
update_schema=INPCUpdate,
|
||||
response_schema=INPCResponse,
|
||||
prefix="",
|
||||
tags=["INPC"],
|
||||
prefix="/inpc",
|
||||
tags=["a76.general_catalogs.inpc"],
|
||||
resource_name="INPC",
|
||||
enable_list=True,
|
||||
)
|
||||
|
||||
router.include_router(inpc_crud.router)
|
||||
).router
|
||||
@@ -63,8 +63,8 @@ class INPCService:
|
||||
def update(
|
||||
db: Session,
|
||||
id: int,
|
||||
data: INPCUpdate,
|
||||
tenant_id: int,
|
||||
data: INPCUpdate,
|
||||
company_id: int
|
||||
) -> Optional[INPC]:
|
||||
db_obj = INPCService.get_by_id(db, id, tenant_id, company_id)
|
||||
|
||||
@@ -21,3 +21,5 @@ class LegendResponse(LegendBase):
|
||||
id: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
tenant_id : int
|
||||
company_id : int
|
||||
|
||||
@@ -15,6 +15,8 @@ class Legend(Base, TenantScopedMixin, TimestampMixin):
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
code: Mapped[int] = mapped_column(Integer, nullable=False) # CLAVELEY
|
||||
|
||||
description: Mapped[Optional[str]] = mapped_column(
|
||||
String(2000), nullable=True) # DESCLEYENDA
|
||||
|
||||
@@ -4,17 +4,13 @@ from .models import Legend
|
||||
from .dto import LegendCreate, LegendResponse, LegendUpdate
|
||||
from .service import LegendService
|
||||
|
||||
router = APIRouter(prefix="/legends", tags=["a76.general_catalogs.legends"])
|
||||
|
||||
legend_crud = TenantCRUDRoutes(
|
||||
router = TenantCRUDRoutes(
|
||||
service=LegendService,
|
||||
create_schema=LegendCreate,
|
||||
update_schema=LegendUpdate,
|
||||
response_schema=LegendResponse,
|
||||
prefix="",
|
||||
tags=["Legends"],
|
||||
prefix="/legends",
|
||||
tags=["a76.general_catalogs.legends"],
|
||||
resource_name="Legend",
|
||||
enable_list=True,
|
||||
)
|
||||
|
||||
router.include_router(legend_crud.router)
|
||||
).router
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
from typing import List, Optional, Tuple, Dict, Any
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from fastapi import HTTPException
|
||||
import logging
|
||||
|
||||
from .models import Legend
|
||||
from .dto import LegendCreate, LegendUpdate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LegendService:
|
||||
@staticmethod
|
||||
@@ -63,8 +68,8 @@ class LegendService:
|
||||
def update(
|
||||
db: Session,
|
||||
id: int,
|
||||
data: LegendUpdate,
|
||||
tenant_id: int,
|
||||
data: LegendUpdate,
|
||||
company_id: int
|
||||
) -> Optional[Legend]:
|
||||
db_obj = LegendService.get_by_id(db, id, tenant_id, company_id)
|
||||
@@ -90,6 +95,14 @@ class LegendService:
|
||||
if not db_obj:
|
||||
return False
|
||||
|
||||
db.delete(db_obj)
|
||||
db.commit()
|
||||
return True
|
||||
try:
|
||||
db.delete(db_obj)
|
||||
db.commit()
|
||||
return True
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error de integridad al eliminar leyenda {id}: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="No se puede eliminar esta leyenda porque tiene registros relacionados (pedimentos, facturas, etc.). Primero debe eliminar o reasignar esos registros."
|
||||
)
|
||||
|
||||
@@ -27,5 +27,7 @@ class MultiCurrencyTypeUpdate(BaseModel):
|
||||
|
||||
class MultiCurrencyTypeResponse(MultiCurrencyTypeBase):
|
||||
id: int
|
||||
|
||||
company_id: int
|
||||
tenant_id: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -18,13 +18,18 @@ class MultiCurrencyType(Base, TenantScopedMixin, TimestampMixin):
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
currency_type_code: Mapped[str] = mapped_column(
|
||||
String(3), ForeignKey("public.currency_types.code"), nullable=False)
|
||||
|
||||
country_key: Mapped[Optional[str]] = mapped_column(
|
||||
String(3), ForeignKey("public.countries.m3_key"), nullable=True)
|
||||
|
||||
conversion_factor: Mapped[Optional[Decimal]] = mapped_column(
|
||||
Numeric(13, 6), nullable=True)
|
||||
|
||||
publication_date: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
currency_type: Mapped["CurrencyType"] = relationship()
|
||||
|
||||
country: Mapped[Optional["Country"]] = relationship()
|
||||
|
||||
@@ -31,4 +31,4 @@ class Package(Base, TenantScopedMixin, TimestampMixin):
|
||||
plurals: Mapped[Optional[str]] = mapped_column(String(4))
|
||||
plural_in: Mapped[Optional[str]] = mapped_column(String(4))
|
||||
code_ace: Mapped[Optional[str]] = mapped_column(String(4))
|
||||
code_aamex: Mapped[Optional[str]] = mapped_column(String(9))
|
||||
code_aamex: Mapped[Optional[str]] = mapped_column(String(9))
|
||||
@@ -3,11 +3,16 @@ Service layer for Packages (GBultos).
|
||||
"""
|
||||
|
||||
from typing import Optional, Tuple, List, Dict, Any
|
||||
import logging
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from fastapi import HTTPException
|
||||
|
||||
from . import dto, models
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PackageService:
|
||||
"""Service for Package CRUD operations with tenant support"""
|
||||
@@ -109,6 +114,20 @@ class PackageService:
|
||||
if not package:
|
||||
return False
|
||||
|
||||
db.delete(package)
|
||||
db.commit()
|
||||
return True
|
||||
try:
|
||||
db.delete(package)
|
||||
db.commit()
|
||||
return True
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
logger.error(f"IntegrityError deleting package {package_id}: {str(e)}")
|
||||
if "foreign key constraint" in str(e).lower():
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="No se puede eliminar el bulto porque tiene registros relacionados"
|
||||
)
|
||||
raise HTTPException(status_code=400, detail="Error al eliminar el bulto")
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error deleting package {package_id}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Error al eliminar el bulto")
|
||||
|
||||
@@ -63,8 +63,8 @@ class PortService:
|
||||
def update(
|
||||
db: Session,
|
||||
id: int,
|
||||
data: PortUpdate,
|
||||
tenant_id: int,
|
||||
data: PortUpdate,
|
||||
company_id: int
|
||||
) -> Optional[Port]:
|
||||
db_obj = PortService.get_by_id(db, id, tenant_id, company_id)
|
||||
|
||||
@@ -56,32 +56,3 @@ class PrevalidatorResponseDTO(BaseModel):
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PrevalidatorUpdateDTO(BaseModel):
|
||||
"""DTO para actualizar un prevalidador"""
|
||||
|
||||
customs_prevalidator: Optional[str] = Field(
|
||||
None, max_length=20, description="Customs prevalidator"
|
||||
)
|
||||
patent_prevalidator: Optional[str] = Field(
|
||||
None, max_length=20, description="Patent prevalidator"
|
||||
)
|
||||
description: Optional[str] = Field(
|
||||
None, max_length=50, description="Description"
|
||||
)
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PrevalidatorResponseDTO(BaseModel):
|
||||
"""DTO para responder con datos de un prevalidador"""
|
||||
|
||||
code: str
|
||||
customs_prevalidator: Optional[str] = None
|
||||
patent_prevalidator: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@@ -31,7 +31,9 @@ class Prevalidator(Base, TenantScopedMixin, TimestampMixin):
|
||||
|
||||
# Prevalidator information
|
||||
customs_prevalidator: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
|
||||
patent_prevalidator: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
|
||||
description: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
@@ -120,8 +120,8 @@ class PrevalidatorService:
|
||||
def update(
|
||||
db: Session,
|
||||
prevalidator_id: int,
|
||||
prevalidator_data: PrevalidatorUpdateDTO,
|
||||
tenant_id: int,
|
||||
prevalidator_data: PrevalidatorUpdateDTO,
|
||||
company_id: int
|
||||
) -> Optional[Prevalidator]:
|
||||
"""Update a prevalidator"""
|
||||
|
||||
@@ -11,6 +11,7 @@ class SignatureCreateDTO(BaseModel):
|
||||
"""DTO para crear una firma"""
|
||||
|
||||
code: str = Field(..., max_length=10, description="Signature code")
|
||||
|
||||
signature: Optional[str] = Field(
|
||||
None, max_length=1000, description="Signature")
|
||||
photo_path: Optional[str] = Field(
|
||||
@@ -39,6 +40,8 @@ class SignatureResponseDTO(BaseModel):
|
||||
code: str
|
||||
signature: Optional[str] = None
|
||||
photo_path: Optional[str] = None
|
||||
tenant_id: int
|
||||
company_id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@@ -32,6 +32,7 @@ class Signature(Base, TenantScopedMixin, TimestampMixin):
|
||||
|
||||
# Signature information
|
||||
signature: Mapped[Optional[str]] = mapped_column(String(1000))
|
||||
|
||||
photo_path: Mapped[Optional[str]] = mapped_column(String(1000))
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
@@ -5,6 +5,8 @@ Capa de servicio para lógica de negocio de firmas
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from . import dto, models
|
||||
@@ -81,10 +83,18 @@ class SignatureService:
|
||||
new_signature = models.Signature(
|
||||
**signature_data.model_dump(), tenant_id=tenant_id, company_id=company_id
|
||||
)
|
||||
db.add(new_signature)
|
||||
db.commit()
|
||||
db.refresh(new_signature)
|
||||
return new_signature
|
||||
try:
|
||||
db.add(new_signature)
|
||||
db.commit()
|
||||
db.refresh(new_signature)
|
||||
return new_signature
|
||||
except IntegrityError as exc:
|
||||
db.rollback()
|
||||
# Constraint names: signatures_code_unique (code, tenant_id, company_id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Ya existe una firma con ese código para esta compañía",
|
||||
) from exc
|
||||
|
||||
@staticmethod
|
||||
def update(
|
||||
|
||||
@@ -2,27 +2,23 @@ from typing import Optional
|
||||
from decimal import Decimal
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
|
||||
|
||||
class UnitConversionBase(BaseModel):
|
||||
from_unit_code: str = Field(..., max_length=5,
|
||||
description="Source Unit Code")
|
||||
to_unit_code: str = Field(..., max_length=5,
|
||||
description="Target Unit Code")
|
||||
conversion_factor: Optional[Decimal] = Field(
|
||||
None, description="Conversion Factor")
|
||||
|
||||
# 👇 OJO: Son códigos (Strings), no IDs
|
||||
from_unit_code: str = Field(..., max_length=5, description="Source Unit Code")
|
||||
to_unit_code: str = Field(..., max_length=5, description="Target Unit Code")
|
||||
conversion_factor: Optional[Decimal] = Field(None, description="Conversion Factor")
|
||||
|
||||
class UnitConversionCreate(UnitConversionBase):
|
||||
pass
|
||||
|
||||
|
||||
class UnitConversionUpdate(BaseModel):
|
||||
from_unit_code: Optional[str] = Field(None, max_length=5)
|
||||
to_unit_code: Optional[str] = Field(None, max_length=5)
|
||||
conversion_factor: Optional[Decimal] = None
|
||||
|
||||
|
||||
class UnitConversionResponse(UnitConversionBase):
|
||||
id: int
|
||||
tenant_id: int
|
||||
company_id: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -27,8 +27,11 @@ class UnitConversion(Base, TenantScopedMixin, TimestampMixin):
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
from_unit_code: Mapped[str] = mapped_column(String(5), nullable=False)
|
||||
|
||||
to_unit_code: Mapped[str] = mapped_column(String(5), nullable=False)
|
||||
|
||||
conversion_factor: Mapped[Optional[Decimal]] = mapped_column(
|
||||
Numeric(13, 6), nullable=True)
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
from typing import List, Optional, Tuple, Dict, Any
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .models import UnitConversion
|
||||
from .dto import UnitConversionCreate, UnitConversionUpdate
|
||||
@@ -51,10 +54,18 @@ class UnitConversionService:
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id
|
||||
)
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
try:
|
||||
db.add(db_obj)
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
except IntegrityError as exc:
|
||||
db.rollback()
|
||||
# Puede ser FK de unidades o duplicado de (from_unit_code, to_unit_code, tenant, company)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Verifica que los códigos de unidad existan y que la conversión no esté duplicada",
|
||||
) from exc
|
||||
|
||||
@staticmethod
|
||||
def update(
|
||||
@@ -72,9 +83,16 @@ class UnitConversionService:
|
||||
for key, value in update_dict.items():
|
||||
setattr(db_obj, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
try:
|
||||
db.commit()
|
||||
db.refresh(db_obj)
|
||||
return db_obj
|
||||
except IntegrityError as exc:
|
||||
db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Verifica que los códigos de unidad existan y que la conversión no esté duplicada",
|
||||
) from exc
|
||||
|
||||
@staticmethod
|
||||
def delete(
|
||||
|
||||
@@ -181,4 +181,4 @@ class UnitOfMeasureGeneral(Base, TenantScopedMixin, TimestampMixin):
|
||||
String(4), nullable=True) # CLAVEACE
|
||||
|
||||
customs_unit: Mapped[Optional["UnitOfMeasureCustoms"]] = relationship()
|
||||
ace_unit: Mapped[Optional["UnitOfMeasureACE"]] = relationship()
|
||||
ace_unit: Mapped[Optional["UnitOfMeasureACE"]] = relationship(overlaps="customs_unit")
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
from typing import List, Optional, Tuple, Dict, Any, Type
|
||||
from sqlalchemy import Sequence
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from fastapi import HTTPException
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from .models import (
|
||||
UnitOfMeasureACE, UnitOfMeasureOMA, UnitOfMeasureAmerican, UnitOfMeasureCustoms,
|
||||
@@ -102,9 +107,17 @@ class BaseService:
|
||||
if not db_obj:
|
||||
return False
|
||||
|
||||
db.delete(db_obj)
|
||||
db.commit()
|
||||
return True
|
||||
try:
|
||||
db.delete(db_obj)
|
||||
db.commit()
|
||||
return True
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error de integridad al eliminar {cls.model.__name__} {id}: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="No se puede eliminar esta unidad de medida porque tiene registros relacionados. Primero debe eliminar o reasignar esos registros."
|
||||
)
|
||||
|
||||
|
||||
class UnitOfMeasureACEService(BaseService):
|
||||
|
||||
259
backend/api/v1/modules/a76/invoices/models.py
Normal file
259
backend/api/v1/modules/a76/invoices/models.py
Normal file
@@ -0,0 +1,259 @@
|
||||
from enum import Enum
|
||||
from typing import Optional, List
|
||||
from sqlalchemy import BigInteger, Boolean, Date, ForeignKey, Integer, Numeric, String, Text, TIMESTAMP
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from core.database import Base
|
||||
from datetime import datetime
|
||||
from ....common.base_models import TenantScopedMixin, TimestampMixin
|
||||
|
||||
|
||||
class OperationType(str, Enum):
|
||||
IMP = "imp" # Importación
|
||||
EXP = "exp" # Exportación
|
||||
|
||||
|
||||
class TransportType(str, Enum):
|
||||
NONE = "none" # Ninguno
|
||||
TRANSPORT = "transport" # Transporte
|
||||
BOX = "box" # Caja
|
||||
PLATES = "licence plates" # Placas
|
||||
TRUCK = "truck" # Camión
|
||||
VESSEL = "vessel" # Buque
|
||||
BARGE = "rail barge" # Ferrobarcaza
|
||||
CONTAINER = "container" # Contenedor
|
||||
AIRPLANE = "airplane" # Avión
|
||||
GONDOLA = "gondola" # Góndola
|
||||
FLATBED = "flatbed" # Plataforma
|
||||
|
||||
# --- 1. Invoice Header (invoice_header) ---
|
||||
class InvoiceHeader(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "invoice_header"
|
||||
__table_args__ = (
|
||||
{"schema": "a76"},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
BigInteger, primary_key=True, autoincrement=True)
|
||||
|
||||
# Identifiers
|
||||
operation_type: Mapped[OperationType] = mapped_column(
|
||||
String(3)) # TIPOMOVIMIENTO / Clasifica imp/exp
|
||||
invoice_type: Mapped[Optional[str]] = mapped_column(ForeignKey(
|
||||
"public.invoice_types.key")) # TIPOFACTURA (invoice_types)
|
||||
invoice_number: Mapped[Optional[str]] = mapped_column(
|
||||
String(20)) # FACTURAIMPO/FACTURAEXPO/FACTURAREMISION
|
||||
project_number: Mapped[Optional[str]] = mapped_column(
|
||||
String(14)) # NUMPROYECTO
|
||||
purchase_order: Mapped[Optional[str]] = mapped_column(
|
||||
String(50)) # ORDENCOMPRA
|
||||
related_doc_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer) # IDRELDOC (Para Rectificaciones)
|
||||
|
||||
# Dates
|
||||
invoice_date: Mapped[Optional[datetime]
|
||||
] = mapped_column(Date) # FECHAFACTURA
|
||||
capture_date: Mapped[datetime] = mapped_column(
|
||||
TIMESTAMP(timezone=False), default=datetime.now) # FECHACAPTURA + HORAACTUAL
|
||||
|
||||
# Status & Control
|
||||
is_updated: Mapped[Optional[str]] = mapped_column(Boolean) # ESTATUS
|
||||
updated_date: Mapped[Optional[datetime]] = mapped_column(TIMESTAMP(timezone=False)) # FECHAACTUALIZACION
|
||||
who_updated: Mapped[Optional[str]] = mapped_column(String(20)) # QUIENA ACTUALIZO
|
||||
|
||||
traffic_light_status: Mapped[Optional[str]] = mapped_column(String(50)) # SEMAFORO / SEMAFOROEXPO/IMPO
|
||||
process_log: Mapped[Optional[str]] = mapped_column(String(300)) # COMOFUEPROCESADA
|
||||
|
||||
# Comments
|
||||
observation_es: Mapped[Optional[str]] = mapped_column(Text) #OBSERVACIONE
|
||||
observation_en: Mapped[Optional[str]] = mapped_column(Text) #OBSERVACIONI
|
||||
comments_status: Mapped[Optional[str]] = mapped_column(Text) #COMENTARIOSESTATUS
|
||||
|
||||
# Digital Archive Links
|
||||
cfdi_uuid: Mapped[Optional[str]] = mapped_column(String(100)) # CFDIUUID
|
||||
path_pdf: Mapped[Optional[str]] = mapped_column(String(500)) # CFDIPATHPDF
|
||||
path_xml: Mapped[Optional[str]] = mapped_column(String(500)) # CFDIPATHXML
|
||||
|
||||
# Relationships (Para navegacion ORM)
|
||||
compliance_mx: Mapped["InvoiceComplianceMx"] = relationship(
|
||||
back_populates="header", cascade="all, delete-orphan")
|
||||
financials: Mapped["InvoiceFinancials"] = relationship(
|
||||
back_populates="header", cascade="all, delete-orphan")
|
||||
details: Mapped[List["InvoiceSalesDetails"]] = relationship(
|
||||
back_populates="header", cascade="all, delete-orphan")
|
||||
collections: Mapped[List["InvoiceCollections"]] = relationship(
|
||||
back_populates="header", cascade="all, delete-orphan")
|
||||
logistics: Mapped[List["InvoiceLogistics"]] = relationship(
|
||||
back_populates="header", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
# --- 2. Compliance MX (invoice_compliance_mx) ---
|
||||
class InvoiceComplianceMx(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "invoice_compliance_mx"
|
||||
__table_args__ = (
|
||||
{"schema": "a76"},
|
||||
)
|
||||
|
||||
invoice_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("a76.invoice_header.id"), primary_key=True)
|
||||
|
||||
# Core Customs Data
|
||||
pedimento: Mapped[Optional[str]] = mapped_column(
|
||||
String(19)) # PEDIMENTO/PEDIMENTOIMPO/EXPO
|
||||
pedimento_code: Mapped[Optional[str]] = mapped_column(
|
||||
String(5)) # PEDIMENTOR1, K1
|
||||
remesa: Mapped[Optional[int]] = mapped_column(Integer)
|
||||
aduana: Mapped[Optional[str]] = mapped_column(ForeignKey("public.customs_sections.customs_code")) # ADUANA_CRUCE
|
||||
|
||||
# Clients & Providers
|
||||
provider_header: Mapped[Optional[str]] = mapped_column(String(20)) # PROVEEDOREXPORTADOR
|
||||
provider_id: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # PROVEEDOR
|
||||
sold_to_header: Mapped[Optional[str]] = mapped_column(String(20)) # VENDIDOCONSIGNADO
|
||||
sold_to_id: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # VENDIDOA
|
||||
shipped_to_header: Mapped[Optional[str]] = mapped_column(String(20)) # ENVIADOTRANSFERIDO
|
||||
shipped_to_id: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # ENVIADOA
|
||||
shipped_by_header: Mapped[Optional[str]] = mapped_column(String(20)) # ENVIADOPORVENDIDOPOR
|
||||
shipped_by_id: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # VENDIDOPOR
|
||||
|
||||
customs_broker_id: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.customs_brokers.id")) # AADUANAL
|
||||
|
||||
# Flags & Specific Regimes
|
||||
is_mixed: Mapped[Optional[bool]] = mapped_column(Boolean) # ESMIXTO
|
||||
waste_type: Mapped[Optional[str]] = mapped_column(String(1)) # TIPODESPERDICIO
|
||||
appendix_17: Mapped[Optional[int]] = mapped_column(Integer) # APENDICE17
|
||||
|
||||
# VUCEM / Digital
|
||||
edocument: Mapped[Optional[str]] = mapped_column(String(50)) # EDOCUMENT
|
||||
electronic_signature: Mapped[Optional[str]] = mapped_column(String(999)) # FIRMAELECTRONICA
|
||||
sem_id: Mapped[Optional[int]] = mapped_column(Integer) # SEM (de SFacEntradaSM/SFacSalidaSM)
|
||||
|
||||
# Relationship
|
||||
header: Mapped["InvoiceHeader"] = relationship(
|
||||
back_populates="compliance_mx")
|
||||
|
||||
|
||||
# --- 3. Financials (invoice_financials) ---
|
||||
class InvoiceFinancials(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "invoice_financials"
|
||||
__table_args__ = (
|
||||
{"schema": "a76"},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
BigInteger, primary_key=True, autoincrement=True)
|
||||
invoice_id: Mapped[int] = mapped_column(ForeignKey("a76.invoice_header.id"))
|
||||
|
||||
currency: Mapped[Optional[str]] = mapped_column(String(3)) # CLAVEMONEDA
|
||||
currency_type: Mapped[Optional[str]] = mapped_column(ForeignKey("public.currency_types.code")) # TIPOCLAVEMONEDA
|
||||
exchange_rate: Mapped[Optional[float]] = mapped_column(Numeric(13, 6)) # TIPOCAMBIO
|
||||
|
||||
# Merchandise Values
|
||||
value_mn: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # VALORIMPOMN/EXPOMN
|
||||
value_me: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0)
|
||||
customs_value_mn: Mapped[Optional[float]] = mapped_column( Numeric(23, 8), default=0) # VALORADUANASMN
|
||||
|
||||
# Costs & Taxes
|
||||
freight: Mapped[Optional[float]] = mapped_column(Numeric(19, 8), default=0) # FLETE
|
||||
insurance: Mapped[Optional[float]] = mapped_column(Numeric(19, 8), default=0) # VALSEGUROS/SEGUROS
|
||||
iva_mn: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # IVAEXPOMN / VALORIVAMN
|
||||
iva_factor: Mapped[Optional[float]] = mapped_column(Numeric(17, 4)) # FACTORIVA
|
||||
|
||||
# Weights & Quantities
|
||||
total_quantity: Mapped[Optional[float]] = mapped_column(Numeric(19, 8)) # CANTEXPO / CANTIMPO
|
||||
gross_weight: Mapped[Optional[float]] = mapped_column(Numeric(19, 8)) # PESOBRUTO
|
||||
net_weight: Mapped[Optional[float]] = mapped_column(Numeric(19, 8)) # PESONETO
|
||||
bundle_count: Mapped[Optional[int]] = mapped_column(Integer) # CANTBULTOS
|
||||
|
||||
# Relationship
|
||||
header: Mapped["InvoiceHeader"] = relationship(back_populates="financials")
|
||||
|
||||
|
||||
# --- 4. Logistics (invoice_logistics) ---
|
||||
class InvoiceLogistics(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "invoice_logistics"
|
||||
__table_args__ = (
|
||||
{"schema": "a76"},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
BigInteger, primary_key=True, autoincrement=True)
|
||||
invoice_id: Mapped[int] = mapped_column(ForeignKey("a76.invoice_header.id"))
|
||||
|
||||
# Carrier Info
|
||||
carrier_id: Mapped[Optional[str]] = mapped_column(
|
||||
String(10)) # TRANSPORTISTA
|
||||
transport_type: Mapped[TransportType] = mapped_column(
|
||||
String(15), default="none") # TRANSPORTE
|
||||
transport_mode: Mapped[Optional[str]] = mapped_column(
|
||||
String(15)) # MODTRANS
|
||||
driver_name: Mapped[Optional[str]] = mapped_column(String(80)) # CONDUCTOR
|
||||
is_rail: Mapped[Optional[str]] = mapped_column(String(2)) # ESFERROCARRIL
|
||||
rail_id: Mapped[Optional[str]] = mapped_column(
|
||||
String(31)) # IDFERRORCARRIL
|
||||
|
||||
# Vehicle & Tracking
|
||||
vehicle_num: Mapped[Optional[str]] = mapped_column(
|
||||
String(20)) # NUMVEHICULO / NUMTRAILER
|
||||
license_plate: Mapped[Optional[str]] = mapped_column(
|
||||
String(20)) # NUMTRASPORTE
|
||||
seal_number: Mapped[Optional[str]] = mapped_column(String(15)) # PRECINTO
|
||||
guide_number: Mapped[Optional[str]] = mapped_column(
|
||||
String(20)) # NUMEROGUIA
|
||||
|
||||
# Logistics Dates
|
||||
entry_exit_date: Mapped[Optional[datetime]] = mapped_column(
|
||||
Date) # FECHAENTRADA / FECHAENVIO
|
||||
|
||||
# Relationship
|
||||
header: Mapped["InvoiceHeader"] = relationship(back_populates="logistics")
|
||||
|
||||
|
||||
# --- 5. Sales Order Details (invoice_sales_details) ---
|
||||
class InvoiceSalesDetails(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "invoice_sales_details"
|
||||
__table_args__ = (
|
||||
{"schema": "a76"},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
BigInteger, primary_key=True, autoincrement=True)
|
||||
invoice_id: Mapped[int] = mapped_column(ForeignKey("a76.invoice_header.id"))
|
||||
|
||||
line_number: Mapped[int] = mapped_column(Integer) # LINEA
|
||||
sales_order: Mapped[Optional[str]] = mapped_column(
|
||||
String(20)) # ORDENVENTA
|
||||
|
||||
# Specific Custom Fields
|
||||
colors_description: Mapped[Optional[str]
|
||||
] = mapped_column(String(49)) # COLORES
|
||||
square_color_code: Mapped[Optional[str]] = mapped_column(
|
||||
String(1)) # COLORCUADRITO
|
||||
line_bundles: Mapped[Optional[int]] = mapped_column(
|
||||
Integer) # CANTBULTOS (de la linea)
|
||||
|
||||
# Relationship
|
||||
header: Mapped["InvoiceHeader"] = relationship(back_populates="details")
|
||||
|
||||
|
||||
# --- 6. Collections (invoice_collections) ---
|
||||
class InvoiceCollections(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "invoice_collections"
|
||||
__table_args__ = (
|
||||
{"schema": "a76"},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
BigInteger, primary_key=True, autoincrement=True)
|
||||
invoice_id: Mapped[int] = mapped_column(ForeignKey("a76.invoice_header.id"))
|
||||
|
||||
concept: Mapped[Optional[str]] = mapped_column(String(100)) # CONCEPTO
|
||||
is_collected: Mapped[Optional[int]] = mapped_column(Integer) # COBRADO
|
||||
collection_date: Mapped[Optional[datetime]
|
||||
] = mapped_column(Date) # FECHACOBRANZA
|
||||
amount: Mapped[Optional[float]] = mapped_column(Numeric(23, 8)) # VALOR
|
||||
|
||||
collector_user: Mapped[Optional[str]] = mapped_column(
|
||||
String(20)) # FACTCOBRADOR
|
||||
|
||||
# Relationship
|
||||
header: Mapped["InvoiceHeader"] = relationship(
|
||||
back_populates="collections")
|
||||
284
backend/api/v1/modules/a76/invoices/routes.py
Normal file
284
backend/api/v1/modules/a76/invoices/routes.py
Normal file
@@ -0,0 +1,284 @@
|
||||
from typing import Dict, Any
|
||||
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user, validate_access_to_resource
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Path
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from . import schemas, services
|
||||
|
||||
# Create main router
|
||||
router = APIRouter()
|
||||
|
||||
# Create CRUD routes for Invoice Header using TenantCRUDRoutes
|
||||
invoice_crud = TenantCRUDRoutes(
|
||||
service=services.InvoiceService,
|
||||
create_schema=schemas.InvoiceHeaderCreate,
|
||||
update_schema=schemas.InvoiceHeaderUpdate,
|
||||
response_schema=schemas.InvoiceHeaderResponse,
|
||||
prefix="/invoices",
|
||||
tags=[],
|
||||
resource_name="Invoice",
|
||||
id_name="invoice_id",
|
||||
id_type=int,
|
||||
enable_list=True, # Enable list endpoint with pagination
|
||||
enable_filters=True, # Enable filters for status, operation_type, etc.
|
||||
default_page_size=50,
|
||||
max_page_size=200,
|
||||
)
|
||||
|
||||
# Include the main CRUD routes
|
||||
router.include_router(invoice_crud.router)
|
||||
|
||||
|
||||
# Additional nested routes for child resources
|
||||
|
||||
# --- Logistics Routes ---
|
||||
|
||||
@router.get(
|
||||
"/invoices/{invoice_id}/logistics",
|
||||
response_model=list[schemas.InvoiceLogisticsResponse],
|
||||
summary="Get all logistics for an invoice",
|
||||
)
|
||||
def get_invoice_logistics(
|
||||
invoice_id: int = Path(..., description="Invoice ID"),
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
"""Get all logistics entries for a specific invoice"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
# Verify the invoice exists and belongs to the tenant/company
|
||||
invoice = services.InvoiceService.get_by_id(
|
||||
db, invoice_id, tenant_id, company_id)
|
||||
if not invoice:
|
||||
raise HTTPException(status_code=404, detail="Invoice not found")
|
||||
|
||||
logistics = services.InvoiceLogisticsService.get_all_by_invoice(
|
||||
db, invoice_id)
|
||||
return logistics
|
||||
|
||||
|
||||
@router.post(
|
||||
"/invoices/{invoice_id}/logistics",
|
||||
response_model=schemas.InvoiceLogisticsResponse,
|
||||
status_code=201,
|
||||
summary="Add logistics to an invoice",
|
||||
)
|
||||
def create_invoice_logistics(
|
||||
invoice_id: int = Path(..., description="Invoice ID"),
|
||||
logistics_data: schemas.InvoiceLogisticsCreate = ...,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
"""Add a new logistics entry to an invoice"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
# Verify the invoice exists and belongs to the tenant/company
|
||||
invoice = services.InvoiceService.get_by_id(
|
||||
db, invoice_id, tenant_id, company_id)
|
||||
if not invoice:
|
||||
raise HTTPException(status_code=404, detail="Invoice not found")
|
||||
|
||||
logistics = services.InvoiceLogisticsService.create(
|
||||
db, logistics_data, invoice_id, tenant_id, company_id)
|
||||
return logistics
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/invoices/{invoice_id}/logistics/{logistics_id}",
|
||||
status_code=204,
|
||||
summary="Delete logistics from an invoice",
|
||||
)
|
||||
def delete_invoice_logistics(
|
||||
invoice_id: int = Path(..., description="Invoice ID"),
|
||||
logistics_id: int = Path(..., description="Logistics ID"),
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
"""Delete a logistics entry from an invoice"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
# Verify the invoice exists and belongs to the tenant/company
|
||||
invoice = services.InvoiceService.get_by_id(
|
||||
db, invoice_id, tenant_id, company_id)
|
||||
if not invoice:
|
||||
raise HTTPException(status_code=404, detail="Invoice not found")
|
||||
|
||||
success = services.InvoiceLogisticsService.delete(
|
||||
db, logistics_id, invoice_id)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=404, detail="Logistics entry not found")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# --- Sales Details Routes ---
|
||||
|
||||
@router.get(
|
||||
"/invoices/{invoice_id}/details",
|
||||
response_model=list[schemas.InvoiceSalesDetailsResponse],
|
||||
summary="Get all sales details for an invoice",
|
||||
)
|
||||
def get_invoice_details(
|
||||
invoice_id: int = Path(..., description="Invoice ID"),
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
"""Get all sales details for a specific invoice"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
# Verify the invoice exists and belongs to the tenant/company
|
||||
invoice = services.InvoiceService.get_by_id(
|
||||
db, invoice_id, tenant_id, company_id)
|
||||
if not invoice:
|
||||
raise HTTPException(status_code=404, detail="Invoice not found")
|
||||
|
||||
details = services.InvoiceSalesDetailsService.get_all_by_invoice(
|
||||
db, invoice_id)
|
||||
return details
|
||||
|
||||
|
||||
@router.post(
|
||||
"/invoices/{invoice_id}/details",
|
||||
response_model=schemas.InvoiceSalesDetailsResponse,
|
||||
status_code=201,
|
||||
summary="Add sales detail to an invoice",
|
||||
)
|
||||
|
||||
def create_invoice_detail(
|
||||
invoice_id: int = Path(..., description="Invoice ID"),
|
||||
detail_data: schemas.InvoiceSalesDetailsCreate = ...,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
"""Add a new sales detail to an invoice"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
# Verify the invoice exists and belongs to the tenant/company
|
||||
invoice = services.InvoiceService.get_by_id(
|
||||
db, invoice_id, tenant_id, company_id)
|
||||
if not invoice:
|
||||
raise HTTPException(status_code=404, detail="Invoice not found")
|
||||
|
||||
detail = services.InvoiceSalesDetailsService.create(
|
||||
db, detail_data, invoice_id, tenant_id, company_id)
|
||||
return detail
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/invoices/{invoice_id}/details/{detail_id}",
|
||||
status_code=204,
|
||||
summary="Delete sales detail from an invoice",
|
||||
)
|
||||
def delete_invoice_detail(
|
||||
invoice_id: int = Path(..., description="Invoice ID"),
|
||||
detail_id: int = Path(..., description="Detail ID"),
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
"""Delete a sales detail from an invoice"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
# Verify the invoice exists and belongs to the tenant/company
|
||||
invoice = services.InvoiceService.get_by_id(
|
||||
db, invoice_id, tenant_id, company_id)
|
||||
if not invoice:
|
||||
raise HTTPException(status_code=404, detail="Invoice not found")
|
||||
|
||||
success = services.InvoiceSalesDetailsService.delete(
|
||||
db, detail_id, invoice_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Sales detail not found")
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# --- Collections Routes ---
|
||||
|
||||
@router.get(
|
||||
"/invoices/{invoice_id}/collections",
|
||||
response_model=list[schemas.InvoiceCollectionsResponse],
|
||||
summary="Get all collections for an invoice",
|
||||
)
|
||||
def get_invoice_collections(
|
||||
invoice_id: int = Path(..., description="Invoice ID"),
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
"""Get all collections for a specific invoice"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
# Verify the invoice exists and belongs to the tenant/company
|
||||
invoice = services.InvoiceService.get_by_id(
|
||||
db, invoice_id, tenant_id, company_id)
|
||||
if not invoice:
|
||||
raise HTTPException(status_code=404, detail="Invoice not found")
|
||||
|
||||
collections = services.InvoiceCollectionsService.get_all_by_invoice(
|
||||
db, invoice_id)
|
||||
return collections
|
||||
|
||||
|
||||
@router.post(
|
||||
"/invoices/{invoice_id}/collections",
|
||||
response_model=schemas.InvoiceCollectionsResponse,
|
||||
status_code=201,
|
||||
summary="Add collection to an invoice",
|
||||
)
|
||||
def create_invoice_collection(
|
||||
invoice_id: int = Path(..., description="Invoice ID"),
|
||||
collection_data: schemas.InvoiceCollectionsCreate = ...,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
"""Add a new collection to an invoice"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
# Verify the invoice exists and belongs to the tenant/company
|
||||
invoice = services.InvoiceService.get_by_id(
|
||||
db, invoice_id, tenant_id, company_id)
|
||||
if not invoice:
|
||||
raise HTTPException(status_code=404, detail="Invoice not found")
|
||||
|
||||
collection = services.InvoiceCollectionsService.create(
|
||||
db, collection_data, invoice_id, tenant_id, company_id)
|
||||
return collection
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/invoices/{invoice_id}/collections/{collection_id}",
|
||||
status_code=204,
|
||||
summary="Delete collection from an invoice",
|
||||
)
|
||||
def delete_invoice_collection(
|
||||
invoice_id: int = Path(..., description="Invoice ID"),
|
||||
collection_id: int = Path(..., description="Collection ID"),
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
"""Delete a collection from an invoice"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
# Verify the invoice exists and belongs to the tenant/company
|
||||
invoice = services.InvoiceService.get_by_id(
|
||||
db, invoice_id, tenant_id, company_id)
|
||||
if not invoice:
|
||||
raise HTTPException(status_code=404, detail="Invoice not found")
|
||||
|
||||
success = services.InvoiceCollectionsService.delete(
|
||||
db, collection_id, invoice_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="Collection not found")
|
||||
|
||||
return None
|
||||
257
backend/api/v1/modules/a76/invoices/schemas.py
Normal file
257
backend/api/v1/modules/a76/invoices/schemas.py
Normal file
@@ -0,0 +1,257 @@
|
||||
from typing import Optional, List
|
||||
from datetime import datetime, date
|
||||
from decimal import Decimal
|
||||
from pydantic import BaseModel, Field
|
||||
from .models import OperationType
|
||||
|
||||
|
||||
# --- Base Schemas ---
|
||||
class InvoiceHeaderBase(BaseModel):
|
||||
"""Base fields for Invoice Header"""
|
||||
operation_type: Optional[OperationType] = Field(
|
||||
None, max_length=20, description="Operation type: imp/exp")
|
||||
invoice_type: Optional[str] = Field(
|
||||
None, max_length=5, description="Invoice type key")
|
||||
invoice_number: Optional[str] = Field(
|
||||
None, max_length=20, description="Invoice number")
|
||||
project_number: Optional[str] = Field(
|
||||
None, max_length=14, description="Project number")
|
||||
purchase_order: Optional[str] = Field(
|
||||
None, max_length=50, description="Purchase order")
|
||||
related_doc_id: Optional[int] = Field(
|
||||
None, description="Related document ID for rectifications")
|
||||
invoice_date: Optional[date] = Field(None, description="Invoice date")
|
||||
is_updated: Optional[bool] = Field(None, description="Status")
|
||||
traffic_light_status: Optional[str] = Field(
|
||||
None, max_length=50, description="Traffic light status (SEMAFORO)")
|
||||
process_log: Optional[str] = Field(
|
||||
None, max_length=300, description="Processing log")
|
||||
comments_status: Optional[str] = Field(
|
||||
None, description="Comments and observations")
|
||||
cfdi_uuid: Optional[str] = Field(
|
||||
None, max_length=100, description="CFDI UUID")
|
||||
path_pdf: Optional[str] = Field(
|
||||
None, max_length=500, description="Path to PDF file")
|
||||
path_xml: Optional[str] = Field(
|
||||
None, max_length=500, description="Path to XML file")
|
||||
|
||||
|
||||
class InvoiceComplianceMxBase(BaseModel):
|
||||
"""Base fields for Compliance MX"""
|
||||
pedimento: Optional[str] = Field(
|
||||
None, max_length=19, description="Pedimento number")
|
||||
pedimento_code: Optional[str] = Field(
|
||||
None, max_length=5, description="Pedimento code (R1/K1)")
|
||||
remesa: Optional[int] = Field(None, description="Remesa")
|
||||
aduana: Optional[str] = Field(
|
||||
None, max_length=5, description="Customs office")
|
||||
customs_agent: Optional[str] = Field(
|
||||
None, max_length=10, description="Customs agent")
|
||||
is_mixed: Optional[bool] = Field(
|
||||
None, description="Is mixed operation")
|
||||
waste_type: Optional[str] = Field(
|
||||
None, max_length=1, description="Waste type")
|
||||
appendix_17: Optional[int] = Field(None, description="Appendix 17")
|
||||
edocument: Optional[str] = Field(
|
||||
None, max_length=50, description="E-document")
|
||||
electronic_signature: Optional[str] = Field(
|
||||
None, max_length=999, description="Electronic signature")
|
||||
sem_id: Optional[int] = Field(None, description="SEM ID")
|
||||
|
||||
|
||||
class InvoiceFinancialsBase(BaseModel):
|
||||
"""Base fields for Financials"""
|
||||
currency: Optional[str] = Field(
|
||||
None, max_length=3, description="Currency code")
|
||||
exchange_rate: Optional[Decimal] = Field(None, description="Exchange rate")
|
||||
value_mn: Optional[Decimal] = Field(None, description="Value in MXN")
|
||||
value_me: Optional[Decimal] = Field(
|
||||
None, description="Value in foreign currency")
|
||||
customs_value_mn: Optional[Decimal] = Field(
|
||||
None, description="Customs value in MXN")
|
||||
freight: Optional[Decimal] = Field(None, description="Freight cost")
|
||||
insurance: Optional[Decimal] = Field(None, description="Insurance cost")
|
||||
iva_mn: Optional[Decimal] = Field(None, description="IVA in MXN")
|
||||
iva_factor: Optional[Decimal] = Field(None, description="IVA factor")
|
||||
total_quantity: Optional[Decimal] = Field(
|
||||
None, description="Total quantity")
|
||||
gross_weight: Optional[Decimal] = Field(None, description="Gross weight")
|
||||
net_weight: Optional[Decimal] = Field(None, description="Net weight")
|
||||
bundle_count: Optional[int] = Field(None, description="Bundle count")
|
||||
|
||||
|
||||
class InvoiceLogisticsBase(BaseModel):
|
||||
"""Base fields for Logistics"""
|
||||
carrier_id: Optional[str] = Field(
|
||||
None, max_length=10, description="Carrier ID")
|
||||
transport_mode: Optional[str] = Field(
|
||||
None, max_length=15, description="Transport mode")
|
||||
driver_name: Optional[str] = Field(
|
||||
None, max_length=80, description="Driver name")
|
||||
is_rail: Optional[str] = Field(
|
||||
None, max_length=2, description="Is rail transport")
|
||||
rail_id: Optional[str] = Field(None, max_length=31, description="Rail ID")
|
||||
vehicle_num: Optional[str] = Field(
|
||||
None, max_length=20, description="Vehicle number")
|
||||
license_plate: Optional[str] = Field(
|
||||
None, max_length=20, description="License plate")
|
||||
seal_number: Optional[str] = Field(
|
||||
None, max_length=15, description="Seal number")
|
||||
guide_number: Optional[str] = Field(
|
||||
None, max_length=20, description="Guide number")
|
||||
entry_exit_date: Optional[date] = Field(
|
||||
None, description="Entry/Exit date")
|
||||
|
||||
|
||||
class InvoiceSalesDetailsBase(BaseModel):
|
||||
"""Base fields for Sales Details"""
|
||||
line_number: int = Field(..., description="Line number")
|
||||
sales_order: Optional[str] = Field(
|
||||
None, max_length=20, description="Sales order")
|
||||
colors_description: Optional[str] = Field(
|
||||
None, max_length=49, description="Colors description")
|
||||
square_color_code: Optional[str] = Field(
|
||||
None, max_length=1, description="Square color code")
|
||||
line_bundles: Optional[int] = Field(None, description="Line bundles count")
|
||||
|
||||
|
||||
class InvoiceCollectionsBase(BaseModel):
|
||||
"""Base fields for Collections"""
|
||||
concept: Optional[str] = Field(None, max_length=100, description="Concept")
|
||||
is_collected: Optional[int] = Field(None, description="Is collected flag")
|
||||
collection_date: Optional[date] = Field(
|
||||
None, description="Collection date")
|
||||
amount: Optional[Decimal] = Field(None, description="Amount")
|
||||
collector_user: Optional[str] = Field(
|
||||
None, max_length=20, description="Collector user")
|
||||
|
||||
|
||||
# --- Create Schemas ---
|
||||
|
||||
class InvoiceComplianceMxCreate(InvoiceComplianceMxBase):
|
||||
"""Schema for creating Compliance MX"""
|
||||
pass
|
||||
|
||||
|
||||
class InvoiceFinancialsCreate(InvoiceFinancialsBase):
|
||||
"""Schema for creating Financials"""
|
||||
pass
|
||||
|
||||
|
||||
class InvoiceLogisticsCreate(InvoiceLogisticsBase):
|
||||
"""Schema for creating Logistics"""
|
||||
pass
|
||||
|
||||
|
||||
class InvoiceSalesDetailsCreate(InvoiceSalesDetailsBase):
|
||||
"""Schema for creating Sales Details"""
|
||||
pass
|
||||
|
||||
|
||||
class InvoiceCollectionsCreate(InvoiceCollectionsBase):
|
||||
"""Schema for creating Collections"""
|
||||
pass
|
||||
|
||||
|
||||
class InvoiceHeaderCreate(InvoiceHeaderBase):
|
||||
"""Schema for creating Invoice Header with nested relations"""
|
||||
compliance_mx: Optional[InvoiceComplianceMxCreate] = None
|
||||
financials: Optional[InvoiceFinancialsCreate] = None
|
||||
logistics: Optional[List[InvoiceLogisticsCreate]] = None
|
||||
details: Optional[List[InvoiceSalesDetailsCreate]] = None
|
||||
collections: Optional[List[InvoiceCollectionsCreate]] = None
|
||||
|
||||
|
||||
# --- Update Schemas ---
|
||||
|
||||
class InvoiceComplianceMxUpdate(InvoiceComplianceMxBase):
|
||||
"""Schema for updating Compliance MX"""
|
||||
pass
|
||||
|
||||
|
||||
class InvoiceFinancialsUpdate(InvoiceFinancialsBase):
|
||||
"""Schema for updating Financials"""
|
||||
pass
|
||||
|
||||
|
||||
class InvoiceLogisticsUpdate(InvoiceLogisticsBase):
|
||||
"""Schema for updating Logistics"""
|
||||
pass
|
||||
|
||||
|
||||
class InvoiceSalesDetailsUpdate(InvoiceSalesDetailsBase):
|
||||
"""Schema for updating Sales Details"""
|
||||
line_number: Optional[int] = None
|
||||
|
||||
|
||||
class InvoiceCollectionsUpdate(InvoiceCollectionsBase):
|
||||
"""Schema for updating Collections"""
|
||||
pass
|
||||
|
||||
|
||||
class InvoiceHeaderUpdate(InvoiceHeaderBase):
|
||||
"""Schema for updating Invoice Header with nested relations"""
|
||||
compliance_mx: Optional[InvoiceComplianceMxUpdate] = None
|
||||
financials: Optional[InvoiceFinancialsUpdate] = None
|
||||
logistics: Optional[List[InvoiceLogisticsUpdate]] = None
|
||||
details: Optional[List[InvoiceSalesDetailsUpdate]] = None
|
||||
collections: Optional[List[InvoiceCollectionsUpdate]] = None
|
||||
|
||||
|
||||
# --- Response Schemas ---
|
||||
|
||||
class InvoiceComplianceMxResponse(InvoiceComplianceMxBase):
|
||||
"""Schema for Compliance MX response"""
|
||||
invoice_id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class InvoiceFinancialsResponse(InvoiceFinancialsBase):
|
||||
"""Schema for Financials response"""
|
||||
invoice_id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class InvoiceLogisticsResponse(InvoiceLogisticsBase):
|
||||
"""Schema for Logistics response"""
|
||||
logistics_id: int
|
||||
invoice_id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class InvoiceSalesDetailsResponse(InvoiceSalesDetailsBase):
|
||||
"""Schema for Sales Details response"""
|
||||
detail_id: int
|
||||
invoice_id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class InvoiceCollectionsResponse(InvoiceCollectionsBase):
|
||||
"""Schema for Collections response"""
|
||||
collection_id: int
|
||||
invoice_id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class InvoiceHeaderResponse(InvoiceHeaderBase):
|
||||
"""Schema for Invoice Header response with nested relations"""
|
||||
id: int
|
||||
capture_date: datetime
|
||||
compliance_mx: Optional[InvoiceComplianceMxResponse] = None
|
||||
financials: Optional[InvoiceFinancialsResponse] = None
|
||||
logistics: List[InvoiceLogisticsResponse] = []
|
||||
details: List[InvoiceSalesDetailsResponse] = []
|
||||
collections: List[InvoiceCollectionsResponse] = []
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
256
backend/api/v1/modules/a76/invoices/services.py
Normal file
256
backend/api/v1/modules/a76/invoices/services.py
Normal file
@@ -0,0 +1,256 @@
|
||||
import traceback
|
||||
from typing import Optional, List, Tuple
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_
|
||||
|
||||
from . import models, schemas
|
||||
|
||||
class InvoiceService:
|
||||
"""Service for Invoice Header operations"""
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(db: Session, invoice_id: int, tenant_id: int, company_id: int) -> Optional[models.InvoiceHeader]:
|
||||
"""Get an invoice by ID with tenant/company validation"""
|
||||
return (
|
||||
db.query(models.InvoiceHeader)
|
||||
.filter(
|
||||
models.InvoiceHeader.id == invoice_id,
|
||||
models.InvoiceHeader.tenant_id == tenant_id,
|
||||
models.InvoiceHeader.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_all(
|
||||
db: Session,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
filters: Optional[dict] = None,
|
||||
) -> Tuple[List[models.InvoiceHeader], int]:
|
||||
"""Get all invoices for a tenant/company with pagination and optional filters"""
|
||||
query = db.query(models.InvoiceHeader).filter(
|
||||
models.InvoiceHeader.tenant_id == tenant_id,
|
||||
models.InvoiceHeader.company_id == company_id,
|
||||
)
|
||||
|
||||
# Apply filters if provided
|
||||
if filters:
|
||||
if filters.get("status"):
|
||||
query = query.filter(
|
||||
models.InvoiceHeader.status == filters["status"])
|
||||
if filters.get("operation_type"):
|
||||
query = query.filter(
|
||||
models.InvoiceHeader.operation_type == filters["operation_type"])
|
||||
if filters.get("invoice_type"):
|
||||
query = query.filter(
|
||||
models.InvoiceHeader.invoice_type == filters["invoice_type"])
|
||||
if filters.get("invoice_number"):
|
||||
query = query.filter(models.InvoiceHeader.invoice_number.ilike(
|
||||
f"%{filters['invoice_number']}%"))
|
||||
if filters.get("pedimento"):
|
||||
query = query.join(models.InvoiceComplianceMx).filter(
|
||||
models.InvoiceComplianceMx.pedimento.ilike(
|
||||
f"%{filters['pedimento']}%")
|
||||
)
|
||||
if not filters.get("invoice_type") and filters.get("operation_type") == "exp":
|
||||
query = query.filter(
|
||||
models.InvoiceHeader.operation_type != "REPAR")
|
||||
|
||||
total = query.count()
|
||||
items = query.offset(skip).limit(limit).all()
|
||||
return items, total
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
db: Session,
|
||||
invoice_data: schemas.InvoiceHeaderCreate,
|
||||
tenant_id: int,
|
||||
company_id: int
|
||||
) -> models.InvoiceHeader:
|
||||
"""Create a new invoice with all related data"""
|
||||
|
||||
|
||||
def clean_dict(data_dict: dict) -> dict:
|
||||
cleaned = {}
|
||||
for key, value in data_dict.items():
|
||||
|
||||
if key == 'customs_agent':
|
||||
key = 'customs_broker_id'
|
||||
elif key == 'provider':
|
||||
key = 'provider_id'
|
||||
|
||||
|
||||
if isinstance(value, str) and not value.strip():
|
||||
cleaned[key] = None
|
||||
|
||||
elif value == 0 and (key.endswith('_id') or key == 'remesa'):
|
||||
cleaned[key] = None
|
||||
else:
|
||||
cleaned[key] = value
|
||||
return cleaned
|
||||
|
||||
|
||||
try:
|
||||
# Extract nested data
|
||||
compliance_data = invoice_data.compliance_mx
|
||||
financials_data = invoice_data.financials
|
||||
logistics_data = invoice_data.logistics or []
|
||||
details_data = invoice_data.details or []
|
||||
collections_data = invoice_data.collections or []
|
||||
|
||||
# Create main invoice header
|
||||
raw_invoice_dict = invoice_data.model_dump(
|
||||
exclude={"compliance_mx", "financials",
|
||||
"logistics", "details", "collections"}
|
||||
)
|
||||
invoice_dict = clean_dict(raw_invoice_dict)
|
||||
invoice_dict["tenant_id"] = tenant_id
|
||||
invoice_dict["company_id"] = company_id
|
||||
|
||||
new_invoice = models.InvoiceHeader(**invoice_dict)
|
||||
db.add(new_invoice)
|
||||
db.flush() # Flush to get the invoice ID
|
||||
|
||||
# Create compliance_mx if provided
|
||||
if compliance_data:
|
||||
raw_comp_dict = compliance_data.model_dump()
|
||||
# Pasamos los datos por la lavadora para arreglar pedimento, aduana, etc.
|
||||
compliance_dict = clean_dict(raw_comp_dict)
|
||||
|
||||
compliance_dict["invoice_id"] = new_invoice.id
|
||||
compliance_dict["tenant_id"] = tenant_id
|
||||
compliance_dict["company_id"] = company_id
|
||||
|
||||
new_compliance = models.InvoiceComplianceMx(**compliance_dict)
|
||||
db.add(new_compliance)
|
||||
|
||||
# Create financials if provided
|
||||
if financials_data:
|
||||
raw_fin_dict = financials_data.model_dump()
|
||||
financials_dict = clean_dict(raw_fin_dict)
|
||||
|
||||
financials_dict["invoice_id"] = new_invoice.id
|
||||
financials_dict["tenant_id"] = tenant_id
|
||||
financials_dict["company_id"] = company_id
|
||||
|
||||
new_financials = models.InvoiceFinancials(**financials_dict)
|
||||
db.add(new_financials)
|
||||
|
||||
# Create logistics entries
|
||||
for logistics_item in logistics_data:
|
||||
raw_log_dict = logistics_item.model_dump()
|
||||
logistics_dict = clean_dict(raw_log_dict)
|
||||
|
||||
logistics_dict["invoice_id"] = new_invoice.id
|
||||
logistics_dict["tenant_id"] = tenant_id
|
||||
logistics_dict["company_id"] = company_id
|
||||
new_logistics = models.InvoiceLogistics(**logistics_dict)
|
||||
db.add(new_logistics)
|
||||
|
||||
# Create sales details
|
||||
for detail_item in details_data:
|
||||
raw_det_dict = detail_item.model_dump()
|
||||
detail_dict = clean_dict(raw_det_dict)
|
||||
|
||||
detail_dict["invoice_id"] = new_invoice.id
|
||||
detail_dict["tenant_id"] = tenant_id
|
||||
detail_dict["company_id"] = company_id
|
||||
new_detail = models.InvoiceSalesDetails(**detail_dict)
|
||||
db.add(new_detail)
|
||||
|
||||
# Create collections
|
||||
for collection_item in collections_data:
|
||||
raw_col_dict = collection_item.model_dump()
|
||||
collection_dict = clean_dict(raw_col_dict)
|
||||
|
||||
collection_dict["invoice_id"] = new_invoice.id
|
||||
collection_dict["tenant_id"] = tenant_id
|
||||
collection_dict["company_id"] = company_id
|
||||
new_collection = models.InvoiceCollections(**collection_dict)
|
||||
db.add(new_collection)
|
||||
|
||||
db.commit()
|
||||
db.refresh(new_invoice)
|
||||
return new_invoice
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print("\n\n🔥 ERROR AL GUARDAR FACTURA 🔥")
|
||||
print(f"Error: {str(e)}")
|
||||
traceback.print_exc() # Esto imprime el error real en la consola
|
||||
print("--------------------------------\n")
|
||||
raise e
|
||||
|
||||
@staticmethod
|
||||
def update(
|
||||
db: Session,
|
||||
invoice_id: int,
|
||||
tenant_id: int,
|
||||
invoice_data: schemas.InvoiceHeaderUpdate,
|
||||
company_id: int
|
||||
) -> Optional[models.InvoiceHeader]:
|
||||
# ... (El resto de tu código update se queda igual) ...
|
||||
# (Te recomiendo implementar clean_dict aquí también si tienes problemas al editar)
|
||||
invoice = InvoiceService.get_by_id(
|
||||
db, invoice_id, tenant_id, company_id)
|
||||
if not invoice:
|
||||
return None
|
||||
|
||||
# Update main invoice header fields
|
||||
update_dict = invoice_data.model_dump(
|
||||
exclude={"compliance_mx", "financials",
|
||||
"logistics", "details", "collections"},
|
||||
exclude_unset=True
|
||||
)
|
||||
for key, value in update_dict.items():
|
||||
setattr(invoice, key, value)
|
||||
|
||||
# Update compliance_mx if provided
|
||||
if invoice_data.compliance_mx is not None:
|
||||
if invoice.compliance_mx:
|
||||
for key, value in invoice_data.compliance_mx.model_dump(exclude_unset=True).items():
|
||||
# Parche rápido para update
|
||||
if value == "": value = None
|
||||
setattr(invoice.compliance_mx, key, value)
|
||||
else:
|
||||
compliance_dict = invoice_data.compliance_mx.model_dump()
|
||||
# Aplicar limpieza manual si es necesario
|
||||
if 'customs_agent' in compliance_dict: compliance_dict['customs_broker_id'] = compliance_dict.pop('customs_agent')
|
||||
|
||||
compliance_dict["invoice_id"] = invoice.id
|
||||
compliance_dict["tenant_id"] = tenant_id
|
||||
compliance_dict["company_id"] = company_id
|
||||
new_compliance = models.InvoiceComplianceMx(**compliance_dict)
|
||||
db.add(new_compliance)
|
||||
|
||||
# Update financials if provided
|
||||
if invoice_data.financials is not None:
|
||||
if invoice.financials:
|
||||
for key, value in invoice_data.financials.model_dump(exclude_unset=True).items():
|
||||
if value == "": value = None
|
||||
setattr(invoice.financials, key, value)
|
||||
else:
|
||||
financials_dict = invoice_data.financials.model_dump()
|
||||
financials_dict["invoice_id"] = invoice.id
|
||||
financials_dict["tenant_id"] = tenant_id
|
||||
financials_dict["company_id"] = company_id
|
||||
new_financials = models.InvoiceFinancials(**financials_dict)
|
||||
db.add(new_financials)
|
||||
|
||||
db.commit()
|
||||
db.refresh(invoice)
|
||||
return invoice
|
||||
|
||||
@staticmethod
|
||||
def delete(db: Session, invoice_id: int, tenant_id: int, company_id: int) -> bool:
|
||||
"""Delete an invoice and all related data (cascade delete)"""
|
||||
invoice = InvoiceService.get_by_id(
|
||||
db, invoice_id, tenant_id, company_id)
|
||||
if invoice:
|
||||
db.delete(invoice)
|
||||
db.commit()
|
||||
return True
|
||||
return False
|
||||
@@ -8,7 +8,7 @@ from fastapi import APIRouter
|
||||
from .customs_brokers.routes import router as customs_broker_router
|
||||
|
||||
# Importar routers de módulos
|
||||
from ..core.auth import router as auth_router
|
||||
from .invoices.routes import router as invoices_router
|
||||
from .classes import router as classes_router
|
||||
from .clients_and_providers import router as client_and_provider_router
|
||||
from .general_catalogs.company import router as company_router
|
||||
@@ -17,7 +17,6 @@ from .transportation.drivers.routes import router as drivers_router
|
||||
from .general_catalogs.exchange_rate.routes import router as exchange_rate_router
|
||||
from .general_catalogs.identifiers.routes import router as identifiers_router
|
||||
from .fraction_rule_octave.routes import router as fraction_rule_octave_router
|
||||
from ..core.licenses import router as licenses_router
|
||||
from .general_catalogs.packages.routes import router as package_router
|
||||
from .general_catalogs.ports.routes import router as ports_router
|
||||
from .parts import router as parts_router
|
||||
@@ -38,21 +37,15 @@ from .general_catalogs.error_catalogs.routes import router as error_catalogs_rou
|
||||
from .general_catalogs.doda.routes import router as doda_router
|
||||
from .general_catalogs.prevalidators.routes import router as prevalidators_router
|
||||
from .general_catalogs.electronic_notices.routes import router as electronic_notices_router
|
||||
from ..core.tenants import router as tenants_router
|
||||
from .transportation.trailers.routes import router as trailers_router
|
||||
from .transportation.transporters.routes import router as transporters_router
|
||||
from ..core.user_tenant.routes import router as user_tenant_router
|
||||
from .transportation.vehicles.routes import router as vehicles_router
|
||||
|
||||
# Router principal
|
||||
router = APIRouter()
|
||||
|
||||
# Registrar módulos
|
||||
router.include_router(auth_router)
|
||||
router.include_router(tenants_router, prefix="/a76", tags=["a76 / tenants"])
|
||||
router.include_router(user_tenant_router, prefix="/a76",
|
||||
tags=["a76 / user-tenants"])
|
||||
router.include_router(licenses_router, prefix="/a76", tags=["a76 / licenses"])
|
||||
router.include_router(invoices_router, prefix="/a76", tags=["a76 / invoices"])
|
||||
router.include_router(pedimentos_router, prefix="/a76")
|
||||
router.include_router(
|
||||
client_and_provider_router, prefix="/a76", tags=["a76 / clients_and_providers"]
|
||||
|
||||
12
backend/api/v1/modules/core/router.py
Normal file
12
backend/api/v1/modules/core/router.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from .auth.routes import router as auth_router
|
||||
from .licenses.routes import router as licenses_router
|
||||
from .tenants.routes import router as tenants_router
|
||||
from .user_tenant.routes import router as user_tenant_router
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
router.include_router(auth_router)
|
||||
router.include_router(tenants_router, prefix="/core", tags=["core / tenants"])
|
||||
router.include_router(user_tenant_router, prefix="/core", tags=["core / user-tenants"])
|
||||
router.include_router(licenses_router, prefix="/core", tags=["core / licenses"])
|
||||
@@ -8,5 +8,6 @@ class InvoiceTypeDTO(BaseModel):
|
||||
description: str
|
||||
note: Optional[str] = None
|
||||
type: Optional[str] = None
|
||||
operation: Optional[str] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@@ -10,15 +10,11 @@ class InvoiceType(Base):
|
||||
{"schema": "public", "extend_existing": True}, # opcional
|
||||
)
|
||||
|
||||
key: Mapped[str] = mapped_column(
|
||||
String(5), nullable=False
|
||||
) # clave del tipo de factura
|
||||
description: Mapped[str] = mapped_column(
|
||||
String(50), nullable=False
|
||||
) # descripción oficial (en español)
|
||||
# observación o comentario adicional
|
||||
note: Mapped[str] = mapped_column(String(500))
|
||||
type: Mapped[str] = mapped_column(String(15)) # tipo
|
||||
key: Mapped[str] = mapped_column(String(5), nullable=False) # clave del tipo de factura
|
||||
description: Mapped[str] = mapped_column(String(50), nullable=False) # descripción oficial (en español)
|
||||
note: Mapped[str] = mapped_column(String(500))# observación o comentario adicional
|
||||
type: Mapped[str] = mapped_column(String(15))# tipo (MATERIAL, fixed asset, both)
|
||||
operation: Mapped[str] = mapped_column(String(5)) # operación (imp, exp, both)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<InvoiceType(key={self.key}, description={self.description}, origin_type={self.origin_type})>"
|
||||
return f"<InvoiceType(key={self.key}, description={self.description}, type={self.type}, operation={self.operation})>"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user, has_role
|
||||
@@ -11,17 +11,28 @@ from .models import InvoiceType
|
||||
router = APIRouter(prefix="/invoice-types")
|
||||
|
||||
|
||||
@router.get("/", response_model=Dict[str, Any])
|
||||
@router.get("/", response_model=dict)
|
||||
def list_invoice_types(
|
||||
page: int = Query(1, ge=1, description="Número de página"),
|
||||
page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"),
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(50, ge=1, le=100),
|
||||
type: Optional[str] = Query(None, description="Filter by type"),
|
||||
operation: Optional[str] = Query(None, description="Filter by operation type (imp, exp, both)"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
skip = (page - 1) * page_size
|
||||
query = db.query(InvoiceType)
|
||||
items = query.offset(skip).limit(page_size).all()
|
||||
|
||||
# Filter by operation if provided
|
||||
if operation:
|
||||
query = query.filter(
|
||||
(InvoiceType.operation == operation) | (
|
||||
InvoiceType.operation == "both")
|
||||
)
|
||||
|
||||
if type == "imp" and operation == "CR":
|
||||
query = query.filter(InvoiceType.operation != "exp")
|
||||
|
||||
total = query.count()
|
||||
items = query.offset((page - 1) * page_size).limit(page_size).all()
|
||||
return {
|
||||
"items": [InvoiceTypeDTO.model_validate(obj) for obj in items],
|
||||
"total": total,
|
||||
|
||||
@@ -1,38 +1,76 @@
|
||||
seed = [
|
||||
("DONAC", "DONACION", "", "AMBOS"),
|
||||
("EXDEF", "EXPORTACION DEFINITIVA", "", "MATERIAL"),
|
||||
# === TIPOS DE IMPORTACION ===
|
||||
(
|
||||
"TEM",
|
||||
"IMPORTACION TEMPORAL",
|
||||
"IMPORTACION TEMPORAL DE MATERIA PRIMA, COMPONENTES O MATERIALES PARA SER PROCESADOS Y POSTERIORMENTE EXPORTADOS.",
|
||||
"both",
|
||||
"imp",
|
||||
),
|
||||
(
|
||||
"DEF",
|
||||
"IMPORTACION DEFINITIVA",
|
||||
"IMPORTACION DEFINITIVA PARA NACIONALIZACION DE MERCANCIA QUE PERMANECE EN TERRITORIO NACIONAL.",
|
||||
"both",
|
||||
"imp",
|
||||
),
|
||||
(
|
||||
"MEX",
|
||||
"COMPRAS MEXICANAS",
|
||||
"IMPORTACION DE MERCANCIA NACIONAL ADQUIRIDA DE PROVEEDORES MEXICANOS PARA INCORPORAR A PROCESO PRODUCTIVO.",
|
||||
"both",
|
||||
"imp",
|
||||
),
|
||||
(
|
||||
"CR",
|
||||
"CAMBIO DE REGIMEN",
|
||||
"IMPORTACION POR CAMBIO DE REGIMEN DE MERCANCIA TEMPORAL QUE SE NACIONALIZA O RETORNA.",
|
||||
"both",
|
||||
"imp",
|
||||
),
|
||||
|
||||
# === TIPOS DE EXPORTACION ===
|
||||
("DONAC", "DONACION", "", "both", "exp"),
|
||||
("EXDEF", "EXPORTACION DEFINITIVA", "", "material", "exp"),
|
||||
(
|
||||
"MATDE",
|
||||
"MATERIA PRIMA O MATERIAL DEVUELTO",
|
||||
"ESTE PROCESO CONSISTE EN SOLO DESCARGAR LAS PARTES DADAS DE ALTA EN MATERIALES QUE SON RETORNADAS SIN NINGUNA MODIFICACION (A1)",
|
||||
"MATERIAL",
|
||||
"material",
|
||||
"exp",
|
||||
),
|
||||
(
|
||||
"NODES",
|
||||
"NO HACE DESCARGA",
|
||||
"ESTE PROCESO DE ACTUALIZACION CONSISTE EN EXPORTAR UNA MERCANCIA Y NO DESCARGAR, POR LO TANTO NO EXISTE REPORTE DE DESCARGAS Y NO AFECTA SALDOS.",
|
||||
"AMBOS",
|
||||
"both",
|
||||
"exp",
|
||||
),
|
||||
(
|
||||
"PTERM",
|
||||
"PRODUCTO TERMINADO Y VIRTUALES",
|
||||
"EL PRODUCTO TERMINADO Y VIRTUALES DESCARGARAN: 1) APARTIR DE LOS COMPONENTES DE CADA PRODUCTO TERMINADO REGISTRADO EN LAS PARTIDAS DE EXPORTACION. 2) POR PARTE, CON LAS OPCIONES DE PODER DESCARGAR POR SUSTITUTO Y POR CLASE EN CASO DE INSUFICIENCIAS DEL COMPONENTE.",
|
||||
"MATERIAL",
|
||||
"material",
|
||||
"exp",
|
||||
),
|
||||
(
|
||||
"REPAR",
|
||||
"REPARACION",
|
||||
"PROCESO QUE CONSISTE EN DOS ETAPAS: 1) DESCARGA EL PRODUCTO DE REPARACION QUE SE IMPORTO PARA REPARA, 2) DESCARGA EL LISTADO DE COMPONENTES QUE SE AGREGO AL PRODUCTO DE REPARACION",
|
||||
"MATERIAL",
|
||||
"material",
|
||||
"exp",
|
||||
),
|
||||
("SCRAP", "SCRAP", "", "AMBOS"),
|
||||
("SCRAP", "SCRAP", "", "both", "exp"),
|
||||
(
|
||||
"VEMEX",
|
||||
"VENTAS EN MEXICO",
|
||||
"ESTE PROCESO CONSISTE EN LA VENTA EN EL MERCADO NACIONAL DE LOS PRODUCTOS.",
|
||||
"AMBOS",
|
||||
"both",
|
||||
"exp",
|
||||
),
|
||||
("VIRTU", "VIRTUALES", "", "MATERIAL"),
|
||||
("AFIJO", "ACTIVO FIJO", "", "ACTIVO FIJO"),
|
||||
("REEXP", "REEXPEDICION", "", "ACTIVO FIJO"),
|
||||
("VIRTU", "VIRTUALES", "", "material", "exp"),
|
||||
|
||||
# === ACTIVOS FIJOS (AMBAS OPERACIONES) ===
|
||||
("AFIJO", "ACTIVO FIJO", "", "fixed asset", "exp"),
|
||||
("REEXP", "REEXPEDICION", "", "fixed asset", "exp"),
|
||||
]
|
||||
|
||||
@@ -6,6 +6,7 @@ Agrega todos los módulos de la aplicación
|
||||
from fastapi import APIRouter
|
||||
|
||||
# Importar routers de módulos
|
||||
from .modules.core.router import router as core_router
|
||||
from .modules.a76.router import router as a76_router
|
||||
from .modules.public.router import router as public_router
|
||||
|
||||
@@ -13,6 +14,7 @@ from .modules.public.router import router as public_router
|
||||
router = APIRouter()
|
||||
|
||||
# Registrar módulos
|
||||
router.include_router(core_router)
|
||||
router.include_router(a76_router)
|
||||
router.include_router(public_router)
|
||||
|
||||
|
||||
30
backend/check_db.py
Normal file
30
backend/check_db.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import sys
|
||||
import os
|
||||
from sqlalchemy import create_engine, text, inspect
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
# Add the backend directory to the python path
|
||||
sys.path.append(os.path.join(os.getcwd(), 'backend'))
|
||||
|
||||
from core.config import settings
|
||||
|
||||
def check_equivalencies_columns():
|
||||
engine = create_engine(str(settings.core_database_url))
|
||||
inspector = inspect(engine)
|
||||
|
||||
try:
|
||||
print("Checking equivalencies table columns...")
|
||||
columns = inspector.get_columns('equivalencies', schema='a76')
|
||||
for column in columns:
|
||||
print(f"Column: {column['name']} - Type: {column['type']}")
|
||||
|
||||
print("\nChecking equivalency_items table columns...")
|
||||
columns = inspector.get_columns('equivalency_items', schema='a76')
|
||||
for column in columns:
|
||||
print(f"Column: {column['name']} - Type: {column['type']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_equivalencies_columns()
|
||||
30
check_db.py
Normal file
30
check_db.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import sys
|
||||
import os
|
||||
from sqlalchemy import create_engine, text, inspect
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
# Add the backend directory to the python path
|
||||
sys.path.append(os.path.join(os.getcwd(), 'backend'))
|
||||
|
||||
from core.config import settings
|
||||
|
||||
def check_equivalencies_columns():
|
||||
engine = create_engine(str(settings.core_database_url))
|
||||
inspector = inspect(engine)
|
||||
|
||||
try:
|
||||
print("Checking equivalencies table columns...")
|
||||
columns = inspector.get_columns('equivalencies', schema='a76')
|
||||
for column in columns:
|
||||
print(f"Column: {column['name']} - Type: {column['type']}")
|
||||
|
||||
print("\nChecking equivalency_items table columns...")
|
||||
columns = inspector.get_columns('equivalency_items', schema='a76')
|
||||
for column in columns:
|
||||
print(f"Column: {column['name']} - Type: {column['type']}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_equivalencies_columns()
|
||||
23
check_uom.py
Normal file
23
check_uom.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add backend to path
|
||||
sys.path.append(os.path.join(os.getcwd(), 'backend'))
|
||||
|
||||
from core.database import CoreSessionLocal
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure
|
||||
|
||||
def check_units():
|
||||
db = CoreSessionLocal()
|
||||
try:
|
||||
units = db.query(UnitOfMeasure).limit(10).all()
|
||||
print(f"Found {len(units)} units:")
|
||||
for u in units:
|
||||
print(f"Code: {u.code}, Description: {u.description}")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
check_units()
|
||||
@@ -164,7 +164,7 @@ services:
|
||||
- KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-dev-secret}
|
||||
- CORS_ORIGINS=${CORS_ORIGINS:-http://localhost:5173,http://localhost:3000}
|
||||
ports:
|
||||
- "8000:8000"
|
||||
- "5050:8000"
|
||||
depends_on:
|
||||
postgres-a76:
|
||||
condition: service_healthy
|
||||
|
||||
1190
estructura.txt
Normal file
1190
estructura.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -41,11 +41,11 @@
|
||||
"valuation_methods": "Valuation Methods",
|
||||
"countries": "Countries",
|
||||
"ports": "Ports",
|
||||
"unit_measures": "Unit Measures",
|
||||
"um_customs_mex": "U.M. Customs Mexico",
|
||||
"um_customs_ame": "U.M. Customs America",
|
||||
"um_ace": "U.M. ACE",
|
||||
"um_oma": "U.M. OMA",
|
||||
"unit_measures": "Units of Measure - General",
|
||||
"um_customs_mex": "Units of Measure - Mexican Customs",
|
||||
"um_customs_ame": "Units of Measure - American Customs",
|
||||
"um_ace": "Units of Measure - ACE",
|
||||
"um_oma": "Units of Measure - OMA",
|
||||
"conversions": "Conversions",
|
||||
"equivalences": "Equivalences",
|
||||
"exchange_rates": "Exchange Rates",
|
||||
@@ -72,6 +72,18 @@
|
||||
"customs_sections": "Customs Sections",
|
||||
"anexo_22_app_31": "Anexo 22 App 3"
|
||||
},
|
||||
"import_invoices":{
|
||||
"title": "Import Invoices",
|
||||
"temporary": "Temporary",
|
||||
"definitive": "Definitive",
|
||||
"mexican_purchases": "Mexican Purchases",
|
||||
"regime_change": "Regime Change"
|
||||
},
|
||||
"export_invoices": {
|
||||
"title": "Export Invoices",
|
||||
"exportation": "Exportation",
|
||||
"repair": "Repair"
|
||||
},
|
||||
"clients_and_providers": "Clients and Providers",
|
||||
"customs_brokers": "Customs Brokers",
|
||||
"nav_user": {
|
||||
|
||||
@@ -41,11 +41,11 @@
|
||||
"valuation_methods": "Metódos de valoración",
|
||||
"countries": "Países",
|
||||
"ports": "Puertos",
|
||||
"unit_measures": "Unidades de medida",
|
||||
"um_customs_mex": "U.M. Aduanas Mexicanas",
|
||||
"um_customs_ame": "U.M. Aduanas Americanas",
|
||||
"um_ace": "U.M. ACE",
|
||||
"um_oma": "U.M. OMA",
|
||||
"unit_measures": "Unidades de medida general",
|
||||
"um_customs_mex": "Unidades de medida - Aduanas Mexicanas",
|
||||
"um_customs_ame": "Unidades de medida - Aduanas Americanas",
|
||||
"um_ace": "Unidades de medida - ACE",
|
||||
"um_oma": "Unidades de medida - OMA",
|
||||
"conversions": "Conversiones",
|
||||
"equivalences": "Equivalencias",
|
||||
"exchange_rates": "Tipos de cambio",
|
||||
@@ -72,6 +72,18 @@
|
||||
"customs_sections": "Secciones Aduaneras",
|
||||
"anexo_22_app_31": "Anexo 22 App 3"
|
||||
},
|
||||
"import_invoices":{
|
||||
"title": "Facturas de importación",
|
||||
"temporary": "Temporal",
|
||||
"definitive": "Definitiva",
|
||||
"mexican_purchases": "Compras mexicanas",
|
||||
"regime_change": "Cambio de régimen"
|
||||
},
|
||||
"export_invoices": {
|
||||
"title": "Facturas de exportación",
|
||||
"exportation": "Exportación",
|
||||
"repair": "Reparación"
|
||||
},
|
||||
"clients_and_providers": "Clientes y Proveedores",
|
||||
"customs_brokers": "Agentes Aduanales",
|
||||
"nav_user": {
|
||||
|
||||
@@ -194,6 +194,14 @@ async function fetchApi<T = any>(
|
||||
}
|
||||
}
|
||||
|
||||
// Manejar respuestas sin contenido (204 No Content)
|
||||
if (response.status === 204) {
|
||||
return {
|
||||
data: null as T,
|
||||
status: response.status
|
||||
};
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
/**
|
||||
* API Client para Agentes Aduanales (Customs Brokers)
|
||||
* Gestiona las operaciones CRUD para agentes aduanales
|
||||
*/
|
||||
|
||||
import { api } from '$lib/api';
|
||||
|
||||
export interface CustomsBroker {
|
||||
|
||||
@@ -2,60 +2,63 @@ import { api } from '$lib/api';
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
|
||||
export interface ClassificationConcept {
|
||||
id: number;
|
||||
classification: string;
|
||||
description?: string;
|
||||
tenant_id: string;
|
||||
company_id?: string;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
id: number;
|
||||
classification: string;
|
||||
tenant_id: number;
|
||||
company_id: number;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface ClassificationConceptCreate {
|
||||
classification: string;
|
||||
description?: string;
|
||||
classification: string;
|
||||
}
|
||||
|
||||
export interface ClassificationConceptUpdate extends Partial<ClassificationConceptCreate> {}
|
||||
|
||||
export interface ClassificationConceptListResponse {
|
||||
items: ClassificationConcept[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
items: ClassificationConcept[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export async function getClassificationConcepts(
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
filters: Record<string, any> = {}
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<ClassificationConceptListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
|
||||
const response = await api.get(`/a76/classification_concepts?${params.toString()}`);
|
||||
return response.data;
|
||||
return await api.get(`/v1/a76/classification-concepts/?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function getClassificationConcept(id: number): Promise<ClassificationConcept> {
|
||||
const response = await api.get(`/a76/classification_concepts/${id}`);
|
||||
return response.data;
|
||||
export async function getClassificationConcept(id: number, companyId: number): Promise<ApiResponse<ClassificationConcept>> {
|
||||
return await api.get(`/v1/a76/classification-concepts/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
export async function createClassificationConcept(data: ClassificationConceptCreate): Promise<ClassificationConcept> {
|
||||
const response = await api.post('/a76/classification_concepts', data);
|
||||
return response.data;
|
||||
export async function createClassificationConcept(
|
||||
data: ClassificationConceptCreate,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<ClassificationConcept>> {
|
||||
return await api.post(`/v1/a76/classification-concepts/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function updateClassificationConcept(id: number, data: ClassificationConceptUpdate): Promise<ClassificationConcept> {
|
||||
const response = await api.patch(`/a76/classification_concepts/${id}`, data);
|
||||
return response.data;
|
||||
export async function updateClassificationConcept(
|
||||
id: number,
|
||||
data: ClassificationConceptUpdate,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<ClassificationConcept>> {
|
||||
return await api.put(`/v1/a76/classification-concepts/${id}/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteClassificationConcept(id: number): Promise<void> {
|
||||
await api.delete(`/a76/classification_concepts/${id}`);
|
||||
}
|
||||
export async function deleteClassificationConcept(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/classification-concepts/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ export interface Company {
|
||||
tenant_id: number;
|
||||
name: string | null;
|
||||
rfc: string | null;
|
||||
curp?: string | null;
|
||||
main_activity: string | null;
|
||||
program: string | null;
|
||||
program_number: string | null;
|
||||
@@ -17,6 +18,13 @@ export interface Company {
|
||||
responsible_name: string | null;
|
||||
responsible_last_name: string | null;
|
||||
responsible_mother_last_name: string | null;
|
||||
responsible_rfc?: string | null;
|
||||
position?: string | null;
|
||||
has_express_line?: boolean;
|
||||
is_service_company?: boolean;
|
||||
order_format_type?: string | null;
|
||||
ctpat_svi?: string | null;
|
||||
trusted_exporter_number?: string | null;
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
}
|
||||
@@ -24,6 +32,7 @@ export interface Company {
|
||||
export interface CompanyCreate {
|
||||
name?: string | null;
|
||||
rfc?: string | null;
|
||||
curp?: string | null;
|
||||
main_activity?: string | null;
|
||||
program?: string | null;
|
||||
program_number?: string | null;
|
||||
@@ -35,11 +44,19 @@ export interface CompanyCreate {
|
||||
responsible_name?: string | null;
|
||||
responsible_last_name?: string | null;
|
||||
responsible_mother_last_name?: string | null;
|
||||
responsible_rfc?: string | null;
|
||||
position?: string | null;
|
||||
has_express_line?: boolean;
|
||||
is_service_company?: boolean;
|
||||
order_format_type?: string | null;
|
||||
ctpat_svi?: string | null;
|
||||
trusted_exporter_number?: string | null;
|
||||
}
|
||||
|
||||
export interface CompanyUpdate {
|
||||
name?: string | null;
|
||||
rfc?: string | null;
|
||||
curp?: string | null;
|
||||
main_activity?: string | null;
|
||||
program?: string | null;
|
||||
program_number?: string | null;
|
||||
@@ -51,6 +68,13 @@ export interface CompanyUpdate {
|
||||
responsible_name?: string | null;
|
||||
responsible_last_name?: string | null;
|
||||
responsible_mother_last_name?: string | null;
|
||||
responsible_rfc?: string | null;
|
||||
position?: string | null;
|
||||
has_express_line?: boolean;
|
||||
is_service_company?: boolean;
|
||||
order_format_type?: string | null;
|
||||
ctpat_svi?: string | null;
|
||||
trusted_exporter_number?: string | null;
|
||||
}
|
||||
|
||||
export interface CompanyListResponse {
|
||||
@@ -71,21 +95,21 @@ export async function getCompanies(
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
return await api.get(`/a76/company?${queryParams.toString()}`);
|
||||
return await api.get(`/v1/a76/company?${queryParams.toString()}`);
|
||||
}
|
||||
|
||||
export async function getCompany(id: number): Promise<ApiResponse<Company>> {
|
||||
return await api.get(`/a76/company/${id}`);
|
||||
return await api.get(`/v1/a76/company/${id}`);
|
||||
}
|
||||
|
||||
export async function createCompany(data: CompanyCreate): Promise<ApiResponse<Company>> {
|
||||
return await api.post(`/a76/company`, data);
|
||||
return await api.post(`/v1/a76/company`, data);
|
||||
}
|
||||
|
||||
export async function updateCompany(id: number, data: CompanyUpdate): Promise<ApiResponse<Company>> {
|
||||
return await api.put(`/a76/company/${id}`, data);
|
||||
return await api.put(`/v1/a76/company/${id}`, data);
|
||||
}
|
||||
|
||||
export async function deleteCompany(id: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/a76/company/${id}`);
|
||||
return await api.delete(`/v1/a76/company/${id}`);
|
||||
}
|
||||
|
||||
@@ -2,78 +2,80 @@ import { api } from '$lib/api';
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
|
||||
export interface Concept {
|
||||
id: number;
|
||||
code: string;
|
||||
description?: string;
|
||||
description_en?: string;
|
||||
detailed_description?: string;
|
||||
priority?: number;
|
||||
priority_ame?: number;
|
||||
first_total?: boolean;
|
||||
type?: string;
|
||||
is_printed?: boolean;
|
||||
section?: number;
|
||||
classification?: string;
|
||||
tenant_id: string;
|
||||
company_id?: string;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
id: number;
|
||||
code: string;
|
||||
description?: string;
|
||||
description_en?: string;
|
||||
detailed_description?: string;
|
||||
priority?: number;
|
||||
priority_ame?: number;
|
||||
first_total?: boolean;
|
||||
type?: string;
|
||||
is_printed?: boolean;
|
||||
section?: number;
|
||||
classification?: string;
|
||||
tenant_id: string;
|
||||
company_id?: string;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface ConceptCreate {
|
||||
code: string;
|
||||
description?: string;
|
||||
description_en?: string;
|
||||
detailed_description?: string;
|
||||
priority?: number;
|
||||
priority_ame?: number;
|
||||
first_total?: boolean;
|
||||
type?: string;
|
||||
is_printed?: boolean;
|
||||
section?: number;
|
||||
classification?: string;
|
||||
code: string;
|
||||
description?: string;
|
||||
description_en?: string;
|
||||
detailed_description?: string;
|
||||
priority?: number;
|
||||
priority_ame?: number;
|
||||
first_total?: boolean;
|
||||
type?: string;
|
||||
is_printed?: boolean;
|
||||
section?: number;
|
||||
classification?: string;
|
||||
}
|
||||
|
||||
export interface ConceptUpdate extends Partial<ConceptCreate> {}
|
||||
|
||||
export interface ConceptListResponse {
|
||||
items: Concept[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
items: Concept[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export async function getConcepts(
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
filters: Record<string, any> = {}
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {},
|
||||
): Promise<ApiResponse<ConceptListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
|
||||
const response = await api.get(`/a76/concepts?${params.toString()}`);
|
||||
return response.data;
|
||||
return await api.get(`/v1/a76/concepts?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function getConcept(id: number): Promise<Concept> {
|
||||
const response = await api.get(`/a76/concepts/${id}`);
|
||||
return response.data;
|
||||
|
||||
export async function getConcept(id: number, companyId: number): Promise<ApiResponse<Concept>> {
|
||||
return await api.get(`/v1/a76/concepts/${id}?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
export async function createConcept(data: ConceptCreate): Promise<Concept> {
|
||||
const response = await api.post('/a76/concepts', data);
|
||||
return response.data;
|
||||
|
||||
export async function createConcept(data: ConceptCreate, companyId: number): Promise<ApiResponse<Concept>> {
|
||||
return await api.post(`/v1/a76/concepts?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function updateConcept(id: number, data: ConceptUpdate): Promise<Concept> {
|
||||
const response = await api.patch(`/a76/concepts/${id}`, data);
|
||||
return response.data;
|
||||
|
||||
export async function updateConcept(id: number, data: ConceptUpdate, companyId: number): Promise<ApiResponse<Concept>> {
|
||||
return await api.put(`/v1/a76/concepts/${id}?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteConcept(id: number): Promise<void> {
|
||||
await api.delete(`/a76/concepts/${id}`);
|
||||
}
|
||||
|
||||
export async function deleteConcept(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/concepts/${id}?company_id=${companyId}`);
|
||||
}
|
||||
@@ -2,76 +2,74 @@ import { api } from '$lib/api';
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
|
||||
export interface CustomsBrokerConcept {
|
||||
id: number;
|
||||
code: string;
|
||||
description?: string;
|
||||
description_en?: string;
|
||||
detailed_description?: string;
|
||||
priority?: number;
|
||||
first_total?: boolean;
|
||||
type?: string;
|
||||
is_printed?: boolean;
|
||||
section?: number;
|
||||
classification?: string;
|
||||
tenant_id: string;
|
||||
company_id?: string;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
id: number;
|
||||
code: string;
|
||||
description?: string;
|
||||
description_en?: string;
|
||||
detailed_description?: string;
|
||||
priority?: number;
|
||||
first_total?: boolean;
|
||||
type?: string;
|
||||
is_printed?: boolean;
|
||||
section?: number;
|
||||
classification?: string;
|
||||
tenant_id: string;
|
||||
company_id?: string;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface CustomsBrokerConceptCreate {
|
||||
code: string;
|
||||
description?: string;
|
||||
description_en?: string;
|
||||
detailed_description?: string;
|
||||
priority?: number;
|
||||
first_total?: boolean;
|
||||
type?: string;
|
||||
is_printed?: boolean;
|
||||
section?: number;
|
||||
classification?: string;
|
||||
code: string;
|
||||
description?: string;
|
||||
description_en?: string;
|
||||
detailed_description?: string;
|
||||
priority?: number;
|
||||
first_total?: boolean;
|
||||
type?: string;
|
||||
is_printed?: boolean;
|
||||
section?: number;
|
||||
classification?: string;
|
||||
}
|
||||
|
||||
export interface CustomsBrokerConceptUpdate extends Partial<CustomsBrokerConceptCreate> {}
|
||||
|
||||
export interface CustomsBrokerConceptListResponse {
|
||||
items: CustomsBrokerConcept[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
items: CustomsBrokerConcept[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export async function getCustomsBrokerConcepts(
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
filters: Record<string, any> = {}
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {},
|
||||
): Promise<ApiResponse<CustomsBrokerConceptListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
|
||||
const response = await api.get(`/a76/customs_broker_concepts?${params.toString()}`);
|
||||
return response.data;
|
||||
return await api.get(`/v1/a76/customs-broker-concepts?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function getCustomsBrokerConcept(id: number): Promise<CustomsBrokerConcept> {
|
||||
const response = await api.get(`/a76/customs_broker_concepts/${id}`);
|
||||
return response.data;
|
||||
export async function getCustomsBrokerConcept(id: number, companyId: number): Promise<ApiResponse<CustomsBrokerConcept>> {
|
||||
return await api.get(`/v1/a76/customs-broker-concepts/${id}?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
export async function createCustomsBrokerConcept(data: CustomsBrokerConceptCreate): Promise<CustomsBrokerConcept> {
|
||||
const response = await api.post('/a76/customs_broker_concepts', data);
|
||||
return response.data;
|
||||
export async function createCustomsBrokerConcept(data: CustomsBrokerConceptCreate, companyId: number): Promise<ApiResponse<CustomsBrokerConcept>> {
|
||||
return await api.post(`/v1/a76/customs-broker-concepts?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function updateCustomsBrokerConcept(id: number, data: CustomsBrokerConceptUpdate): Promise<CustomsBrokerConcept> {
|
||||
const response = await api.patch(`/a76/customs_broker_concepts/${id}`, data);
|
||||
return response.data;
|
||||
export async function updateCustomsBrokerConcept(id: number, data: CustomsBrokerConceptUpdate, companyId: number): Promise<ApiResponse<CustomsBrokerConcept>> {
|
||||
return await api.put(`/v1/a76/customs-broker-concepts/${id}?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteCustomsBrokerConcept(id: number): Promise<void> {
|
||||
await api.delete(`/a76/customs_broker_concepts/${id}`);
|
||||
export async function deleteCustomsBrokerConcept(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/customs-broker-concepts/${id}?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { api } from '$lib/api';
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
|
||||
export interface CustomsBrokerConcept {
|
||||
id: number;
|
||||
code: string;
|
||||
description?: string;
|
||||
description_en?: string;
|
||||
detailed_description?: string;
|
||||
priority?: number;
|
||||
first_total?: boolean;
|
||||
type?: string;
|
||||
is_printed?: boolean;
|
||||
section?: number;
|
||||
classification?: string;
|
||||
tenant_id: string;
|
||||
company_id?: string;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface CustomsBrokerConceptCreate {
|
||||
code: string;
|
||||
description?: string;
|
||||
description_en?: string;
|
||||
detailed_description?: string;
|
||||
priority?: number;
|
||||
first_total?: boolean;
|
||||
type?: string;
|
||||
is_printed?: boolean;
|
||||
section?: number;
|
||||
classification?: string;
|
||||
}
|
||||
|
||||
export interface CustomsBrokerConceptUpdate extends Partial<CustomsBrokerConceptCreate> {}
|
||||
|
||||
export interface CustomsBrokerConceptListResponse {
|
||||
items: CustomsBrokerConcept[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export async function getCustomsBrokerConcepts(
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {},
|
||||
): Promise<ApiResponse<CustomsBrokerConceptListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
|
||||
return await api.get(`/v1/a76/customs-broker-concepts?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function getCustomsBrokerConcept(id: number, companyId: number): Promise<ApiResponse<CustomsBrokerConcept>> {
|
||||
return await api.get(`/v1/a76/customs-broker-concepts/${id}?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
export async function createCustomsBrokerConcept(data: CustomsBrokerConceptCreate, companyId: number): Promise<ApiResponse<CustomsBrokerConcept>> {
|
||||
return await api.post(`/v1/a76/customs-broker-concepts?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function updateCustomsBrokerConcept(id: number, data: CustomsBrokerConceptUpdate, companyId: number): Promise<ApiResponse<CustomsBrokerConcept>> {
|
||||
return await api.put(`/v1/a76/customs-broker-concepts/${id}?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteCustomsBrokerConcept(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/customs-broker-concepts/${id}?company_id=${companyId}`);
|
||||
}
|
||||
@@ -1,61 +1,186 @@
|
||||
import { api } from '$lib/api';
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
|
||||
export interface DODA {
|
||||
id: number;
|
||||
code: string;
|
||||
description?: string;
|
||||
tenant_id: string;
|
||||
company_id?: string;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
|
||||
export interface DodaContainerSeal {
|
||||
id: number;
|
||||
doda_sys_id: number;
|
||||
seal_line: number;
|
||||
seal_value?: string;
|
||||
}
|
||||
|
||||
export interface DODACreate {
|
||||
code: string;
|
||||
description?: string;
|
||||
export interface DodaContainerSealCreate {
|
||||
seal_value?: string;
|
||||
}
|
||||
|
||||
export interface DODAUpdate extends Partial<DODACreate> {}
|
||||
|
||||
export interface DODAListResponse {
|
||||
items: DODA[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
export interface DodaContainer {
|
||||
id: number;
|
||||
doda_sys_id: number;
|
||||
container_line: number;
|
||||
container_value?: string;
|
||||
seals?: string;
|
||||
seals_detail?: DodaContainerSeal[];
|
||||
}
|
||||
|
||||
export async function getDODAs(
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<DODAListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
|
||||
const response = await api.get(`/a76/doda?${params.toString()}`);
|
||||
return response.data;
|
||||
export interface DodaContainerCreate {
|
||||
container_value?: string;
|
||||
seals?: string;
|
||||
seals_detail?: DodaContainerSealCreate[];
|
||||
}
|
||||
|
||||
export async function getDODA(id: number): Promise<DODA> {
|
||||
const response = await api.get(`/a76/doda/${id}`);
|
||||
return response.data;
|
||||
|
||||
export interface DodaAmericanPedimento {
|
||||
id: number;
|
||||
doda_sys_id: number;
|
||||
american_pedimento_line: number;
|
||||
american_pedimento_type?: string;
|
||||
american_pedimento_value?: string;
|
||||
}
|
||||
|
||||
export async function createDODA(data: DODACreate): Promise<DODA> {
|
||||
const response = await api.post('/a76/doda', data);
|
||||
return response.data;
|
||||
export interface DodaAmericanPedimentoCreate {
|
||||
american_pedimento_type?: string;
|
||||
american_pedimento_value?: string;
|
||||
}
|
||||
|
||||
export async function updateDODA(id: number, data: DODAUpdate): Promise<DODA> {
|
||||
const response = await api.patch(`/a76/doda/${id}`, data);
|
||||
return response.data;
|
||||
export interface DodaPedimento {
|
||||
id: number;
|
||||
doda_sys_id: number;
|
||||
pedimento_line: number;
|
||||
authorization_patent?: string;
|
||||
document?: string;
|
||||
shipment?: string;
|
||||
cove?: string;
|
||||
umc?: string;
|
||||
effective_amount_usd?: number;
|
||||
difference_amount_usd?: number;
|
||||
dta_niu?: string;
|
||||
article_7?: boolean;
|
||||
pedimento_sys_id?: number;
|
||||
invoice_line?: number;
|
||||
part_ii_line?: number;
|
||||
pedimento_type?: string;
|
||||
zero_packaging_validation?: boolean;
|
||||
}
|
||||
|
||||
export async function deleteDODA(id: number): Promise<void> {
|
||||
await api.delete(`/a76/doda/${id}`);
|
||||
export interface DodaPedimentoCreate {
|
||||
authorization_patent?: string;
|
||||
document?: string;
|
||||
shipment?: string;
|
||||
|
||||
effective_amount_usd?: number;
|
||||
}
|
||||
|
||||
|
||||
export interface Doda {
|
||||
id: number;
|
||||
|
||||
integration_number?: string;
|
||||
doda_date?: number;
|
||||
doda_time?: number;
|
||||
dispatch_customs?: string;
|
||||
customs_sections?: string;
|
||||
patent?: string;
|
||||
pedimentos?: string;
|
||||
caat?: string;
|
||||
transport_identification?: string;
|
||||
fast_id?: string;
|
||||
operation_type?: string;
|
||||
status?: string;
|
||||
|
||||
|
||||
containers?: DodaContainer[];
|
||||
american_pedimentos?: DodaAmericanPedimento[];
|
||||
pedimentos_detail?: DodaPedimento[];
|
||||
|
||||
|
||||
tenant_id?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
|
||||
export interface DodaCreate {
|
||||
integration_number?: string;
|
||||
doda_date?: number;
|
||||
doda_time?: number;
|
||||
dispatch_customs?: string;
|
||||
customs_sections?: string;
|
||||
patent?: string;
|
||||
pedimentos?: string;
|
||||
caat?: string;
|
||||
transport_identification?: string;
|
||||
fast_id?: string;
|
||||
operation_type?: string;
|
||||
selected?: boolean;
|
||||
user_selected?: string;
|
||||
last_user?: string;
|
||||
responsible?: string;
|
||||
carrier?: string;
|
||||
shipments?: string;
|
||||
pedimento_type?: string;
|
||||
original_chain?: string;
|
||||
serial_number?: string;
|
||||
electronic_signature?: string;
|
||||
transaction_number?: string;
|
||||
status?: string;
|
||||
linq_sat_qr?: string;
|
||||
sat_certificate?: string;
|
||||
sat_digital_seal?: string;
|
||||
xml_doda_sent_path?: string;
|
||||
xml_doda_response_path?: string;
|
||||
sat_original_chain?: string;
|
||||
customs_clearance?: number;
|
||||
unique_badge_number?: string;
|
||||
}
|
||||
|
||||
export interface DodaUpdate extends Partial<DodaCreate> {}
|
||||
|
||||
export interface DodaListResponse {
|
||||
items: Doda[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
|
||||
export async function getDodas(
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
filters: Record<string, any> = {},
|
||||
companyId?: number
|
||||
): Promise<ApiResponse<DodaListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
if (companyId) {
|
||||
params.append('company_id', companyId.toString());
|
||||
}
|
||||
const response = await api.get(`/v1/a76/doda?${params.toString()}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getDoda(id: number, companyId?: number): Promise<Doda> {
|
||||
const params = new URLSearchParams();
|
||||
if (companyId) {
|
||||
params.append('company_id', companyId.toString());
|
||||
}
|
||||
const response = await api.get(`/v1/a76/doda/${id}?${params.toString()}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function createDoda(data: DodaCreate, companyId: number): Promise<Doda> {
|
||||
const response = await api.post(`/v1/a76/doda?company_id=${companyId}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function updateDoda(id: number, data: DodaUpdate, companyId: number): Promise<Doda> {
|
||||
const response = await api.patch(`/v1/a76/doda/${id}?company_id=${companyId}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function deleteDoda(id: number, companyId: number): Promise<void> {
|
||||
await api.delete(`/v1/a76/doda/${id}?company_id=${companyId}`);
|
||||
}
|
||||
@@ -2,60 +2,89 @@ import { api } from '$lib/api';
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
|
||||
export interface ElectronicNotice {
|
||||
id: number;
|
||||
code: string;
|
||||
description?: string;
|
||||
tenant_id: string;
|
||||
company_id?: string;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
id: number;
|
||||
// Campos del Modelo Python
|
||||
notice_number?: string;
|
||||
year?: string;
|
||||
patent?: string;
|
||||
pedimento?: string;
|
||||
file_sent?: string;
|
||||
file_response?: string;
|
||||
status?: string;
|
||||
invoice?: string;
|
||||
validation_acknowledgment?: string;
|
||||
fea?: string;
|
||||
certificate_number?: string;
|
||||
|
||||
// Mixins
|
||||
tenant_id: number;
|
||||
company_id: number;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface ElectronicNoticeCreate {
|
||||
code: string;
|
||||
description?: string;
|
||||
notice_number?: string;
|
||||
year?: string;
|
||||
patent?: string;
|
||||
pedimento?: string;
|
||||
file_sent?: string;
|
||||
file_response?: string;
|
||||
status?: string;
|
||||
invoice?: string;
|
||||
validation_acknowledgment?: string;
|
||||
fea?: string;
|
||||
certificate_number?: string;
|
||||
}
|
||||
|
||||
export interface ElectronicNoticeUpdate extends Partial<ElectronicNoticeCreate> {}
|
||||
|
||||
export interface ElectronicNoticeListResponse {
|
||||
items: ElectronicNotice[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
items: ElectronicNotice[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export async function getElectronicNotices(
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
filters: Record<string, any> = {}
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
filters: Record<string, any> = {},
|
||||
companyId?: number
|
||||
): Promise<ApiResponse<ElectronicNoticeListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
if (companyId) {
|
||||
params.append('company_id', companyId.toString());
|
||||
}
|
||||
|
||||
const response = await api.get(`/a76/electronic_notices?${params.toString()}`);
|
||||
return response.data;
|
||||
const response = await api.get(`/v1/a76/electronic-notices/?${params.toString()}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getElectronicNotice(id: number): Promise<ElectronicNotice> {
|
||||
const response = await api.get(`/a76/electronic_notices/${id}`);
|
||||
return response.data;
|
||||
export async function getElectronicNotice(id: number, companyId?: number): Promise<ElectronicNotice> {
|
||||
const params = new URLSearchParams();
|
||||
if (companyId) {
|
||||
params.append('company_id', companyId.toString());
|
||||
}
|
||||
const response = await api.get(`/v1/a76/electronic-notices/${id}?${params.toString()}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function createElectronicNotice(data: ElectronicNoticeCreate): Promise<ElectronicNotice> {
|
||||
const response = await api.post('/a76/electronic_notices', data);
|
||||
return response.data;
|
||||
export async function createElectronicNotice(data: ElectronicNoticeCreate, companyId: number): Promise<ElectronicNotice> {
|
||||
const response = await api.post(`/v1/a76/electronic-notices/?company_id=${companyId}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function updateElectronicNotice(id: number, data: ElectronicNoticeUpdate): Promise<ElectronicNotice> {
|
||||
const response = await api.patch(`/a76/electronic_notices/${id}`, data);
|
||||
return response.data;
|
||||
export async function updateElectronicNotice(id: number, data: ElectronicNoticeUpdate, companyId: number): Promise<ElectronicNotice> {
|
||||
const response = await api.put(`/v1/a76/electronic-notices/${id}?company_id=${companyId}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function deleteElectronicNotice(id: number): Promise<void> {
|
||||
await api.delete(`/a76/electronic_notices/${id}`);
|
||||
}
|
||||
export async function deleteElectronicNotice(id: number, companyId: number): Promise<void> {
|
||||
await api.delete(`/v1/a76/electronic-notices/${id}?company_id=${companyId}`);
|
||||
}
|
||||
@@ -2,62 +2,71 @@ import { api } from '$lib/api';
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
|
||||
export interface Equivalency {
|
||||
id: number;
|
||||
fraccion_mex: string;
|
||||
fraccion_us: string;
|
||||
description?: string;
|
||||
tenant_id: string;
|
||||
company_id?: string;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
id: number;
|
||||
fraccion_mex: string;
|
||||
fraccion_us: string;
|
||||
description?: string;
|
||||
tenant_id: number;
|
||||
company_id: number;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface EquivalencyCreate {
|
||||
fraccion_mex: string;
|
||||
fraccion_us: string;
|
||||
description?: string;
|
||||
fraccion_mex: string;
|
||||
fraccion_us: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface EquivalencyUpdate extends Partial<EquivalencyCreate> {}
|
||||
export interface EquivalencyUpdate {
|
||||
fraccion_mex?: string;
|
||||
fraccion_us?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface EquivalencyListResponse {
|
||||
items: Equivalency[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
items: Equivalency[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export async function getEquivalencies(
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
filters: Record<string, any> = {}
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<EquivalencyListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
|
||||
const response = await api.get(`/a76/equivalencies?${params.toString()}`);
|
||||
return response.data;
|
||||
return await api.get(`/v1/a76/equivalencies/?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function getEquivalency(id: number): Promise<Equivalency> {
|
||||
const response = await api.get(`/a76/equivalencies/${id}`);
|
||||
return response.data;
|
||||
export async function getEquivalency(id: number, companyId: number): Promise<ApiResponse<Equivalency>> {
|
||||
return await api.get(`/v1/a76/equivalencies/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
export async function createEquivalency(data: EquivalencyCreate): Promise<Equivalency> {
|
||||
const response = await api.post('/a76/equivalencies', data);
|
||||
return response.data;
|
||||
export async function createEquivalency(
|
||||
data: EquivalencyCreate,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<Equivalency>> {
|
||||
return await api.post(`/v1/a76/equivalencies/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function updateEquivalency(id: number, data: EquivalencyUpdate): Promise<Equivalency> {
|
||||
const response = await api.patch(`/a76/equivalencies/${id}`, data);
|
||||
return response.data;
|
||||
export async function updateEquivalency(
|
||||
id: number,
|
||||
data: EquivalencyUpdate,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<Equivalency>> {
|
||||
return await api.put(`/v1/a76/equivalencies/${id}/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteEquivalency(id: number): Promise<void> {
|
||||
await api.delete(`/a76/equivalencies/${id}`);
|
||||
export async function deleteEquivalency(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/equivalencies/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
@@ -1,61 +1,145 @@
|
||||
import { api } from '$lib/api';
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
|
||||
// ==========================================
|
||||
// ERROR CLASSIFICATION
|
||||
// ==========================================
|
||||
|
||||
export interface ErrorClassification {
|
||||
id: number;
|
||||
code: string;
|
||||
level?: string;
|
||||
errors?: ErrorCatalog[];
|
||||
tenant_id?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface ErrorClassificationCreate {
|
||||
code: string;
|
||||
level?: string;
|
||||
}
|
||||
|
||||
export interface ErrorClassificationUpdate {
|
||||
level?: string;
|
||||
}
|
||||
|
||||
export interface ErrorClassificationListResponse {
|
||||
items: ErrorClassification[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
|
||||
export interface ErrorCatalog {
|
||||
id: number;
|
||||
code: string;
|
||||
description?: string;
|
||||
tenant_id: string;
|
||||
company_id?: string;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
id: number;
|
||||
code: string;
|
||||
description?: string;
|
||||
classification_id?: number;
|
||||
classification?: ErrorClassification;
|
||||
tenant_id?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface ErrorCatalogCreate {
|
||||
code: string;
|
||||
description?: string;
|
||||
code: string;
|
||||
description?: string;
|
||||
classification_id?: number;
|
||||
}
|
||||
|
||||
export interface ErrorCatalogUpdate extends Partial<ErrorCatalogCreate> {}
|
||||
export interface ErrorCatalogUpdate {
|
||||
description?: string;
|
||||
classification_id?: number;
|
||||
}
|
||||
|
||||
export interface ErrorCatalogListResponse {
|
||||
items: ErrorCatalog[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
items: ErrorCatalog[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export async function getErrorClassifications(
|
||||
companyId: number,
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ErrorClassificationListResponse> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
|
||||
const response = await api.get(`/v1/a76/error-catalogs/classifications/?${params.toString()}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getErrorClassification(id: number, companyId: number): Promise<ErrorClassification> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
const response = await api.get(`/v1/a76/error-catalogs/classifications/${id}?${params.toString()}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function createErrorClassification(data: ErrorClassificationCreate, companyId: number): Promise<ErrorClassification> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
const response = await api.post(`/v1/a76/error-catalogs/classifications/?${params.toString()}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function updateErrorClassification(id: number, data: ErrorClassificationUpdate, companyId: number): Promise<ErrorClassification> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
const response = await api.put(`/v1/a76/error-catalogs/classifications/${id}?${params.toString()}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function deleteErrorClassification(id: number, companyId: number): Promise<void> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
await api.delete(`/v1/a76/error-catalogs/classifications/${id}?${params.toString()}`);
|
||||
}
|
||||
|
||||
// --- Catalogs ---
|
||||
|
||||
export async function getErrorCatalogs(
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<ErrorCatalogListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
companyId: number,
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ErrorCatalogListResponse> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
|
||||
const response = await api.get(`/a76/error_catalogs?${params.toString()}`);
|
||||
return response.data;
|
||||
const response = await api.get(`/v1/a76/error-catalogs/?${params.toString()}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getErrorCatalog(id: number): Promise<ErrorCatalog> {
|
||||
const response = await api.get(`/a76/error_catalogs/${id}`);
|
||||
return response.data;
|
||||
export async function getErrorCatalog(id: number, companyId: number): Promise<ErrorCatalog> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
const response = await api.get(`/v1/a76/error-catalogs/${id}?${params.toString()}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function createErrorCatalog(data: ErrorCatalogCreate): Promise<ErrorCatalog> {
|
||||
const response = await api.post('/a76/error_catalogs', data);
|
||||
return response.data;
|
||||
export async function createErrorCatalog(data: ErrorCatalogCreate, companyId: number): Promise<ErrorCatalog> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
const response = await api.post(`/v1/a76/error-catalogs/?${params.toString()}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function updateErrorCatalog(id: number, data: ErrorCatalogUpdate): Promise<ErrorCatalog> {
|
||||
const response = await api.patch(`/a76/error_catalogs/${id}`, data);
|
||||
return response.data;
|
||||
export async function updateErrorCatalog(id: number, data: ErrorCatalogUpdate, companyId: number): Promise<ErrorCatalog> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
const response = await api.put(`/v1/a76/error-catalogs/${id}?${params.toString()}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function deleteErrorCatalog(id: number): Promise<void> {
|
||||
await api.delete(`/a76/error_catalogs/${id}`);
|
||||
}
|
||||
export async function deleteErrorCatalog(id: number, companyId: number): Promise<void> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
await api.delete(`/v1/a76/error-catalogs/${id}?${params.toString()}`);
|
||||
}
|
||||
@@ -7,8 +7,8 @@ export interface Identifier {
|
||||
description: string | null;
|
||||
level: string | null;
|
||||
complement: string | null;
|
||||
company_id: number;
|
||||
tenant_id: number;
|
||||
company_id: number;
|
||||
tenant_id: number;
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
}
|
||||
@@ -18,7 +18,6 @@ export interface IdentifierCreate {
|
||||
description?: string | null;
|
||||
level?: string | null;
|
||||
complement?: string | null;
|
||||
company_id: number;
|
||||
}
|
||||
|
||||
export interface IdentifierUpdate {
|
||||
@@ -39,24 +38,37 @@ export interface IdentifierListResponse {
|
||||
export async function getIdentifiers(
|
||||
page = 1,
|
||||
pageSize = 50,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<IdentifierListResponse>> {
|
||||
const queryParams = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
return await api.get(`/a76/identifiers?${queryParams.toString()}`);
|
||||
|
||||
return await api.get(`/v1/a76/identifiers/?${queryParams.toString()}`);
|
||||
}
|
||||
|
||||
export async function createIdentifier(data: IdentifierCreate): Promise<ApiResponse<Identifier>> {
|
||||
return await api.post('/a76/identifiers', data);
|
||||
export async function createIdentifier(
|
||||
data: IdentifierCreate,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<Identifier>> {
|
||||
return await api.post(`/v1/a76/identifiers/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function updateIdentifier(id: number, data: IdentifierUpdate): Promise<ApiResponse<Identifier>> {
|
||||
return await api.put(`/a76/identifiers/${id}`, data);
|
||||
export async function updateIdentifier(
|
||||
id: number,
|
||||
data: IdentifierUpdate,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<Identifier>> {
|
||||
return await api.put(`/v1/a76/identifiers/${id}/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteIdentifier(id: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/a76/identifiers/${id}`);
|
||||
}
|
||||
export async function deleteIdentifier(
|
||||
id: number,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/identifiers/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
@@ -2,62 +2,67 @@ import { api } from '$lib/api';
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
|
||||
export interface INPC {
|
||||
id: number;
|
||||
year: string;
|
||||
month: string;
|
||||
value?: number;
|
||||
tenant_id: string;
|
||||
company_id?: string;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
id: number;
|
||||
year: string;
|
||||
month: string;
|
||||
value?: number;
|
||||
tenant_id: number;
|
||||
company_id: number;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface INPCCreate {
|
||||
year: string;
|
||||
month: string;
|
||||
value?: number;
|
||||
year: string;
|
||||
month: string;
|
||||
value?: number;
|
||||
}
|
||||
|
||||
export interface INPCUpdate extends Partial<INPCCreate> {}
|
||||
|
||||
export interface INPCListResponse {
|
||||
items: INPC[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
items: INPC[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export async function getINPCs(
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
filters: Record<string, any> = {}
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<INPCListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
|
||||
const response = await api.get(`/a76/inpc?${params.toString()}`);
|
||||
return response.data;
|
||||
return await api.get(`/v1/a76/inpc/?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function getINPC(id: number): Promise<INPC> {
|
||||
const response = await api.get(`/a76/inpc/${id}`);
|
||||
return response.data;
|
||||
export async function getINPC(id: number, companyId: number): Promise<ApiResponse<INPC>> {
|
||||
return await api.get(`/v1/a76/inpc/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
export async function createINPC(data: INPCCreate): Promise<INPC> {
|
||||
const response = await api.post('/a76/inpc', data);
|
||||
return response.data;
|
||||
export async function createINPC(
|
||||
data: INPCCreate,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<INPC>> {
|
||||
return await api.post(`/v1/a76/inpc/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function updateINPC(id: number, data: INPCUpdate): Promise<INPC> {
|
||||
const response = await api.patch(`/a76/inpc/${id}`, data);
|
||||
return response.data;
|
||||
export async function updateINPC(
|
||||
id: number,
|
||||
data: INPCUpdate,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<INPC>> {
|
||||
return await api.put(`/v1/a76/inpc/${id}/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteINPC(id: number): Promise<void> {
|
||||
await api.delete(`/a76/inpc/${id}`);
|
||||
export async function deleteINPC(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/inpc/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
@@ -2,60 +2,64 @@ import { api } from '$lib/api';
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
|
||||
export interface Legend {
|
||||
id: number;
|
||||
code: string;
|
||||
description?: string;
|
||||
tenant_id: string;
|
||||
company_id?: string;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
id: number;
|
||||
code: number;
|
||||
description?: string;
|
||||
tenant_id: number;
|
||||
company_id: number;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface LegendCreate {
|
||||
code: string;
|
||||
description?: string;
|
||||
code: number;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface LegendUpdate extends Partial<LegendCreate> {}
|
||||
|
||||
export interface LegendListResponse {
|
||||
items: Legend[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
items: Legend[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export async function getLegends(
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
filters: Record<string, any> = {}
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<LegendListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
|
||||
const response = await api.get(`/a76/legends?${params.toString()}`);
|
||||
return response.data;
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
return await api.get(`/v1/a76/legends/?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function getLegend(id: number): Promise<Legend> {
|
||||
const response = await api.get(`/a76/legends/${id}`);
|
||||
return response.data;
|
||||
export async function getLegend(id: number, companyId: number): Promise<ApiResponse<Legend>> {
|
||||
return await api.get(`/v1/a76/legends/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
export async function createLegend(data: LegendCreate): Promise<Legend> {
|
||||
const response = await api.post('/a76/legends', data);
|
||||
return response.data;
|
||||
export async function createLegend(
|
||||
data: LegendCreate,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<Legend>> {
|
||||
return await api.post(`/v1/a76/legends/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function updateLegend(id: number, data: LegendUpdate): Promise<Legend> {
|
||||
const response = await api.patch(`/a76/legends/${id}`, data);
|
||||
return response.data;
|
||||
export async function updateLegend(
|
||||
id: number,
|
||||
data: LegendUpdate,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<Legend>> {
|
||||
return await api.put(`/v1/a76/legends/${id}/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteLegend(id: number): Promise<void> {
|
||||
await api.delete(`/a76/legends/${id}`);
|
||||
}
|
||||
export async function deleteLegend(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/legends/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
@@ -1,52 +1,90 @@
|
||||
/**
|
||||
* API Client para Locations - Ubicaciones relacionadas con puertos
|
||||
* Basado en los campos location_code y location_description del módulo de puertos
|
||||
*/
|
||||
import type { PaginatedResponse } from '$lib/types';
|
||||
import { api } from '$lib/api';
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
|
||||
export interface Location {
|
||||
location_code: string;
|
||||
location_description: string | null;
|
||||
id: number;
|
||||
location_code: string;
|
||||
location_description: string | null;
|
||||
company_id: number;
|
||||
tenant_id: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Nota: Las ubicaciones están integradas en el módulo de puertos.
|
||||
* Este archivo proporciona tipos para trabajar con ubicaciones,
|
||||
* pero las operaciones se realizan a través del módulo de puertos.
|
||||
*
|
||||
* Ver: /a76/ports para operaciones relacionadas con ubicaciones
|
||||
*/
|
||||
|
||||
/**
|
||||
* Obtiene ubicaciones únicas de los puertos
|
||||
* Esta función extrae las ubicaciones únicas de la lista de puertos
|
||||
*/
|
||||
export async function getLocationsFromPorts(): Promise<ApiResponse<Location[]>> {
|
||||
const portsResponse = await api.get('/a76/ports?page_size=1000');
|
||||
|
||||
if (portsResponse.error || !portsResponse.data) {
|
||||
return {
|
||||
error: portsResponse.error || 'Error al obtener puertos',
|
||||
status: portsResponse.status
|
||||
};
|
||||
}
|
||||
|
||||
// Extraer ubicaciones únicas
|
||||
const locationMap = new Map<string, Location>();
|
||||
const ports = portsResponse.data.items || [];
|
||||
|
||||
ports.forEach((port: any) => {
|
||||
if (port.location_code && !locationMap.has(port.location_code)) {
|
||||
locationMap.set(port.location_code, {
|
||||
location_code: port.location_code,
|
||||
location_description: port.location_description
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
data: Array.from(locationMap.values()),
|
||||
status: 200
|
||||
};
|
||||
export interface LocationCreate {
|
||||
location_code: string;
|
||||
location_description?: string | null;
|
||||
}
|
||||
|
||||
export interface LocationUpdate {
|
||||
location_description?: string | null;
|
||||
}
|
||||
|
||||
export interface LocationListResponse extends PaginatedResponse {
|
||||
items: Location[];
|
||||
}
|
||||
|
||||
export interface LocationFilters {
|
||||
location_code?: string;
|
||||
location_description?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}
|
||||
|
||||
export async function getLocations(
|
||||
companyId: number,
|
||||
filters?: LocationFilters
|
||||
): Promise<LocationListResponse> {
|
||||
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<LocationListResponse>(`/v1/a76/ports/?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function getLocation(
|
||||
locationId: number,
|
||||
companyId: number
|
||||
): Promise<Location> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.get<Location>(`/v1/a76/ports/${locationId}?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function createLocation(
|
||||
data: LocationCreate,
|
||||
companyId: number
|
||||
): Promise<Location> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.post<Location>(`/v1/a76/ports/?${params.toString()}`, {
|
||||
port_code: data.location_code,
|
||||
location_code: data.location_code,
|
||||
description: null,
|
||||
location_description: data.location_description || null,
|
||||
port_type: 'ENTRY'
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateLocation(
|
||||
locationId: number,
|
||||
data: LocationUpdate,
|
||||
companyId: number
|
||||
): Promise<Location> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.put<Location>(
|
||||
`/v1/a76/ports/${locationId}?${params.toString()}`,
|
||||
{
|
||||
location_description: data.location_description
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteLocation(
|
||||
locationId: number,
|
||||
companyId: number
|
||||
): Promise<void> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.delete(`/v1/a76/ports/${locationId}?${params.toString()}`);
|
||||
}
|
||||
@@ -1,61 +1,78 @@
|
||||
import { api } from '$lib/api';
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
import type { PaginatedResponse } from '$lib/types';
|
||||
|
||||
export interface MultiCurrencyType {
|
||||
id: number;
|
||||
key: string;
|
||||
description?: string;
|
||||
tenant_id: string;
|
||||
company_id?: string;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
currency_type_code: string;
|
||||
country_key: string | null;
|
||||
conversion_factor: number | null;
|
||||
publication_date: number;
|
||||
company_id: number;
|
||||
tenant_id: number;
|
||||
}
|
||||
|
||||
export interface MultiCurrencyTypeCreate {
|
||||
key: string;
|
||||
description?: string;
|
||||
currency_type_code: string;
|
||||
country_key?: string | null;
|
||||
conversion_factor?: number | null;
|
||||
publication_date: number;
|
||||
}
|
||||
|
||||
export interface MultiCurrencyTypeUpdate extends Partial<MultiCurrencyTypeCreate> {}
|
||||
export interface MultiCurrencyTypeUpdate {
|
||||
currency_type_code?: string;
|
||||
country_key?: string | null;
|
||||
conversion_factor?: number | null;
|
||||
publication_date?: number;
|
||||
}
|
||||
|
||||
export interface MultiCurrencyTypeListResponse {
|
||||
export interface MultiCurrencyTypeListResponse extends PaginatedResponse {
|
||||
items: MultiCurrencyType[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export async function getMultiCurrencyTypes(
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<MultiCurrencyTypeListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
companyId: number,
|
||||
page?: number,
|
||||
pageSize?: number
|
||||
): Promise<MultiCurrencyTypeListResponse> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
if (page) params.append('page', page.toString());
|
||||
if (pageSize) params.append('page_size', pageSize.toString());
|
||||
|
||||
const response = await api.get(`/a76/multi_currency_types?${params.toString()}`);
|
||||
return response.data;
|
||||
return api.get<MultiCurrencyTypeListResponse>(`/v1/a76/multi-currency-types/?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function getMultiCurrencyType(id: number): Promise<MultiCurrencyType> {
|
||||
const response = await api.get(`/a76/multi_currency_types/${id}`);
|
||||
return response.data;
|
||||
export async function getMultiCurrencyType(
|
||||
multiCurrencyTypeId: number,
|
||||
companyId: number
|
||||
): Promise<MultiCurrencyType> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.get<MultiCurrencyType>(`/v1/a76/multi-currency-types/${multiCurrencyTypeId}?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function createMultiCurrencyType(data: MultiCurrencyTypeCreate): Promise<MultiCurrencyType> {
|
||||
const response = await api.post('/a76/multi_currency_types', data);
|
||||
return response.data;
|
||||
export async function createMultiCurrencyType(
|
||||
data: MultiCurrencyTypeCreate,
|
||||
companyId: number
|
||||
): Promise<MultiCurrencyType> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.post<MultiCurrencyType>(`/v1/a76/multi-currency-types/?${params.toString()}`, data);
|
||||
}
|
||||
|
||||
export async function updateMultiCurrencyType(id: number, data: MultiCurrencyTypeUpdate): Promise<MultiCurrencyType> {
|
||||
const response = await api.patch(`/a76/multi_currency_types/${id}`, data);
|
||||
return response.data;
|
||||
export async function updateMultiCurrencyType(
|
||||
multiCurrencyTypeId: number,
|
||||
data: MultiCurrencyTypeUpdate,
|
||||
companyId: number
|
||||
): Promise<MultiCurrencyType> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.put<MultiCurrencyType>(
|
||||
`/v1/a76/multi-currency-types/${multiCurrencyTypeId}?${params.toString()}`,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteMultiCurrencyType(id: number): Promise<void> {
|
||||
await api.delete(`/a76/multi_currency_types/${id}`);
|
||||
}
|
||||
export async function deleteMultiCurrencyType(
|
||||
multiCurrencyTypeId: number,
|
||||
companyId: number
|
||||
): Promise<void> {
|
||||
const params = new URLSearchParams({ company_id: companyId.toString() });
|
||||
return api.delete(`/v1/a76/multi-currency-types/${multiCurrencyTypeId}?${params.toString()}`);
|
||||
}
|
||||
@@ -4,17 +4,17 @@ import type { ApiResponse } from '$lib/api';
|
||||
export interface Package {
|
||||
id: number;
|
||||
tenant_id: number;
|
||||
company_id: number;
|
||||
key: string;
|
||||
description_es: string | null;
|
||||
description_en: string | null;
|
||||
weight_unit: number | null;
|
||||
plurals: string | null;
|
||||
plural_in: string | null;
|
||||
code_ace: string | null;
|
||||
code_aamex: string | null;
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
company_id: number;
|
||||
key: string;
|
||||
description_es: string | null;
|
||||
description_en: string | null;
|
||||
weight_unit: number | null;
|
||||
plurals: string | null;
|
||||
plural_in: string | null;
|
||||
code_ace: string | null;
|
||||
code_aamex: string | null;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface PackageCreate {
|
||||
@@ -26,19 +26,9 @@ export interface PackageCreate {
|
||||
plural_in?: string | null;
|
||||
code_ace?: string | null;
|
||||
code_aamex?: string | null;
|
||||
company_id: number;
|
||||
}
|
||||
|
||||
export interface PackageUpdate {
|
||||
key?: string;
|
||||
description_es?: string | null;
|
||||
description_en?: string | null;
|
||||
weight_unit?: number | null;
|
||||
plurals?: string | null;
|
||||
plural_in?: string | null;
|
||||
code_ace?: string | null;
|
||||
code_aamex?: string | null;
|
||||
}
|
||||
export interface PackageUpdate extends Partial<PackageCreate> {}
|
||||
|
||||
export interface PackageListResponse {
|
||||
items: Package[];
|
||||
@@ -48,31 +38,44 @@ export interface PackageListResponse {
|
||||
pages: number;
|
||||
}
|
||||
|
||||
|
||||
export async function getPackages(
|
||||
page = 1,
|
||||
pageSize = 50,
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
companyId: number, // Obligatorio por el Mixin
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<PackageListResponse>> {
|
||||
const queryParams = new URLSearchParams({
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
return await api.get(`/a76/packages?${queryParams.toString()}`);
|
||||
|
||||
return await api.get(`/v1/a76/packages?${params.toString()}`);
|
||||
}
|
||||
|
||||
export async function getPackage(id: number): Promise<ApiResponse<Package>> {
|
||||
return await api.get(`/a76/packages/${id}`);
|
||||
|
||||
export async function getPackage(id: number, companyId: number): Promise<ApiResponse<Package>> {
|
||||
return await api.get(`/v1/a76/packages/${id}?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
export async function createPackage(data: PackageCreate): Promise<ApiResponse<Package>> {
|
||||
return await api.post(`/a76/packages`, data);
|
||||
export async function createPackage(
|
||||
data: PackageCreate,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<Package>> {
|
||||
return await api.post(`/v1/a76/packages?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function updatePackage(id: number, data: PackageUpdate): Promise<ApiResponse<Package>> {
|
||||
return await api.put(`/a76/packages/${id}`, data);
|
||||
|
||||
export async function updatePackage(
|
||||
id: number,
|
||||
data: PackageUpdate,
|
||||
companyId: number
|
||||
): Promise<ApiResponse<Package>> {
|
||||
return await api.put(`/v1/a76/packages/${id}?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deletePackage(id: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/a76/packages/${id}`);
|
||||
}
|
||||
export async function deletePackage(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/packages/${id}?company_id=${companyId}`);
|
||||
}
|
||||
@@ -45,24 +45,30 @@ export interface PortListResponse {
|
||||
export async function getPorts(
|
||||
page = 1,
|
||||
pageSize = 50,
|
||||
filters: Record<string, any> = {}
|
||||
filters: Record<string, any> = {},
|
||||
companyId?: number
|
||||
): Promise<ApiResponse<PortListResponse>> {
|
||||
const queryParams = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
return await api.get(`/a76/ports?${queryParams.toString()}`);
|
||||
|
||||
if (companyId) {
|
||||
queryParams.append('company_id', companyId.toString());
|
||||
}
|
||||
|
||||
return await api.get(`/v1/a76/ports?${queryParams.toString()}`);
|
||||
}
|
||||
|
||||
export async function createPort(data: PortCreate): Promise<ApiResponse<Port>> {
|
||||
return await api.post('/a76/ports', data);
|
||||
export async function createPort(data: PortCreate, companyId: number): Promise<ApiResponse<Port>> {
|
||||
return await api.post(`/v1/a76/ports?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function updatePort(id: number, data: PortUpdate): Promise<ApiResponse<Port>> {
|
||||
return await api.put(`/a76/ports/${id}`, data);
|
||||
export async function updatePort(id: number, data: PortUpdate, companyId: number): Promise<ApiResponse<Port>> {
|
||||
return await api.put(`/v1/a76/ports/${id}?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deletePort(id: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/a76/ports/${id}`);
|
||||
export async function deletePort(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/ports/${id}?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
@@ -2,60 +2,73 @@ import { api } from '$lib/api';
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
|
||||
export interface Prevalidator {
|
||||
id: number;
|
||||
code: string;
|
||||
description?: string;
|
||||
tenant_id: string;
|
||||
company_id?: string;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
id: number;
|
||||
code: string;
|
||||
description?: string;
|
||||
customs_prevalidator?: string;
|
||||
patent_prevalidator?: string;
|
||||
|
||||
tenant_id: number;
|
||||
company_id: number;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface PrevalidatorCreate {
|
||||
code: string;
|
||||
description?: string;
|
||||
code: string;
|
||||
description?: string;
|
||||
customs_prevalidator?: string;
|
||||
patent_prevalidator?: string;
|
||||
}
|
||||
|
||||
export interface PrevalidatorUpdate extends Partial<PrevalidatorCreate> {}
|
||||
|
||||
export interface PrevalidatorListResponse {
|
||||
items: Prevalidator[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
items: Prevalidator[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export async function getPrevalidators(
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
filters: Record<string, any> = {}
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
filters: Record<string, any> = {},
|
||||
companyId?: number
|
||||
): Promise<ApiResponse<PrevalidatorListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
if (companyId) {
|
||||
params.append('company_id', companyId.toString());
|
||||
}
|
||||
|
||||
const response = await api.get(`/a76/prevalidators?${params.toString()}`);
|
||||
return response.data;
|
||||
const response = await api.get(`/v1/a76/prevalidators/?${params.toString()}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getPrevalidator(id: number): Promise<Prevalidator> {
|
||||
const response = await api.get(`/a76/prevalidators/${id}`);
|
||||
return response.data;
|
||||
export async function getPrevalidator(id: number, companyId?: number): Promise<Prevalidator> {
|
||||
const params = new URLSearchParams();
|
||||
if (companyId) {
|
||||
params.append('company_id', companyId.toString());
|
||||
}
|
||||
const response = await api.get(`/v1/a76/prevalidators/${id}?${params.toString()}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function createPrevalidator(data: PrevalidatorCreate): Promise<Prevalidator> {
|
||||
const response = await api.post('/a76/prevalidators', data);
|
||||
return response.data;
|
||||
export async function createPrevalidator(data: PrevalidatorCreate, companyId: number): Promise<Prevalidator> {
|
||||
const response = await api.post(`/v1/a76/prevalidators/?company_id=${companyId}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function updatePrevalidator(id: number, data: PrevalidatorUpdate): Promise<Prevalidator> {
|
||||
const response = await api.patch(`/a76/prevalidators/${id}`, data);
|
||||
return response.data;
|
||||
export async function updatePrevalidator(id: number, data: PrevalidatorUpdate, companyId: number): Promise<Prevalidator> {
|
||||
const response = await api.put(`/v1/a76/prevalidators/${id}?company_id=${companyId}`, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function deletePrevalidator(id: number): Promise<void> {
|
||||
await api.delete(`/a76/prevalidators/${id}`);
|
||||
export async function deletePrevalidator(id: number, companyId: number): Promise<void> {
|
||||
await api.delete(`/v1/a76/prevalidators/${id}?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
@@ -2,62 +2,83 @@ import { api } from '$lib/api';
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
|
||||
export interface Signature {
|
||||
id: number;
|
||||
name: string;
|
||||
position?: string;
|
||||
certificate?: string;
|
||||
tenant_id: string;
|
||||
company_id?: string;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
id: number;
|
||||
|
||||
code: string;
|
||||
signature: string | null;
|
||||
photo_path: string | null;
|
||||
|
||||
|
||||
tenant_id: number;
|
||||
company_id: number;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface SignatureCreate {
|
||||
name: string;
|
||||
position?: string;
|
||||
certificate?: string;
|
||||
code: string;
|
||||
signature?: string | null;
|
||||
photo_path?: string | null;
|
||||
|
||||
}
|
||||
|
||||
export interface SignatureUpdate extends Partial<SignatureCreate> {}
|
||||
|
||||
export interface SignatureListResponse {
|
||||
items: Signature[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
items: Signature[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
|
||||
export async function getSignatures(
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
filters: Record<string, any> = {}
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<SignatureListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
|
||||
const response = await api.get(`/a76/signatures?${params.toString()}`);
|
||||
return response.data;
|
||||
|
||||
const response = await api.get(`/v1/a76/signatures/?${params.toString()}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getSignature(id: number): Promise<Signature> {
|
||||
const response = await api.get(`/a76/signatures/${id}`);
|
||||
return response.data;
|
||||
|
||||
export async function getSignature(id: number, companyId: number): Promise<Signature> {
|
||||
const response = await api.get(`/v1/a76/signatures/${id}/?company_id=${companyId}`);
|
||||
if (response.error) throw new Error(response.error);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function createSignature(data: SignatureCreate): Promise<Signature> {
|
||||
const response = await api.post('/a76/signatures', data);
|
||||
return response.data;
|
||||
|
||||
export async function createSignature(
|
||||
data: SignatureCreate,
|
||||
companyId: number
|
||||
): Promise<Signature> {
|
||||
const response = await api.post(`/v1/a76/signatures/?company_id=${companyId}`, data);
|
||||
if (response.error) throw new Error(response.error);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function updateSignature(id: number, data: SignatureUpdate): Promise<Signature> {
|
||||
const response = await api.patch(`/a76/signatures/${id}`, data);
|
||||
return response.data;
|
||||
export async function updateSignature(
|
||||
id: number,
|
||||
data: SignatureUpdate,
|
||||
companyId: number
|
||||
): Promise<Signature> {
|
||||
const response = await api.put(`/v1/a76/signatures/${id}/?company_id=${companyId}`, data);
|
||||
if (response.error) throw new Error(response.error);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function deleteSignature(id: number): Promise<void> {
|
||||
await api.delete(`/a76/signatures/${id}`);
|
||||
}
|
||||
export async function deleteSignature(id: number, companyId: number): Promise<void> {
|
||||
const response = await api.delete(`/v1/a76/signatures/${id}/?company_id=${companyId}`);
|
||||
if (response.error) throw new Error(response.error);
|
||||
}
|
||||
@@ -2,62 +2,80 @@ import { api } from '$lib/api';
|
||||
import type { ApiResponse } from '$lib/api';
|
||||
|
||||
export interface UnitConversion {
|
||||
id: number;
|
||||
from_unit_id: number;
|
||||
to_unit_id: number;
|
||||
conversion_factor: number;
|
||||
tenant_id: string;
|
||||
company_id?: string;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
id: number;
|
||||
from_unit_code: string;
|
||||
to_unit_code: string;
|
||||
conversion_factor: number;
|
||||
tenant_id: number; // number
|
||||
company_id: number; // number
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface UnitConversionCreate {
|
||||
from_unit_id: number;
|
||||
to_unit_id: number;
|
||||
conversion_factor: number;
|
||||
from_unit_code: string; // Ej: "KGM"
|
||||
to_unit_code: string; // Ej: "LBR"
|
||||
conversion_factor: number;
|
||||
// company_id va en la URL
|
||||
}
|
||||
|
||||
export interface UnitConversionUpdate extends Partial<UnitConversionCreate> {}
|
||||
|
||||
export interface UnitConversionListResponse {
|
||||
items: UnitConversion[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
items: UnitConversion[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
|
||||
export async function getUnitConversions(
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
filters: Record<string, any> = {}
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
companyId: number, // 👈 Obligatorio
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<UnitConversionListResponse>> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
...filters
|
||||
});
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
|
||||
const response = await api.get(`/a76/unit_conversions?${params.toString()}`);
|
||||
return response.data;
|
||||
// Agregamos /v1 y prefijo.
|
||||
// NOTA: Revisa si en tu router definiste "unit_conversions" o "unit-conversions"
|
||||
const response = await api.get(`/v1/a76/unit-conversions/?${params.toString()}`);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getUnitConversion(id: number): Promise<UnitConversion> {
|
||||
const response = await api.get(`/a76/unit_conversions/${id}`);
|
||||
return response.data;
|
||||
export async function getUnitConversion(id: number, companyId: number): Promise<UnitConversion> {
|
||||
const response = await api.get(`/v1/a76/unit-conversions/${id}/?company_id=${companyId}`);
|
||||
if (response.error) throw new Error(response.error);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function createUnitConversion(data: UnitConversionCreate): Promise<UnitConversion> {
|
||||
const response = await api.post('/a76/unit_conversions', data);
|
||||
return response.data;
|
||||
export async function createUnitConversion(
|
||||
data: UnitConversionCreate,
|
||||
companyId: number
|
||||
): Promise<UnitConversion> {
|
||||
const response = await api.post(`/v1/a76/unit-conversions/?company_id=${companyId}`, data);
|
||||
if (response.error) throw new Error(response.error);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function updateUnitConversion(id: number, data: UnitConversionUpdate): Promise<UnitConversion> {
|
||||
const response = await api.patch(`/a76/unit_conversions/${id}`, data);
|
||||
return response.data;
|
||||
|
||||
export async function updateUnitConversion(
|
||||
id: number,
|
||||
data: UnitConversionUpdate,
|
||||
companyId: number
|
||||
): Promise<UnitConversion> {
|
||||
const response = await api.put(`/v1/a76/unit-conversions/${id}/?company_id=${companyId}`, data);
|
||||
if (response.error) throw new Error(response.error);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function deleteUnitConversion(id: number): Promise<void> {
|
||||
await api.delete(`/a76/unit_conversions/${id}`);
|
||||
}
|
||||
export async function deleteUnitConversion(id: number, companyId: number): Promise<void> {
|
||||
const response = await api.delete(`/v1/a76/unit-conversions/${id}/?company_id=${companyId}`);
|
||||
if (response.error) throw new Error(response.error);
|
||||
}
|
||||
@@ -31,26 +31,28 @@ export interface UnitOfMeasureACEListResponse {
|
||||
export async function getUnitsOfMeasureACE(
|
||||
page = 1,
|
||||
pageSize = 50,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<UnitOfMeasureACEListResponse>> {
|
||||
const queryParams = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
return await api.get(`/a76/units-of-measure/ace?${queryParams.toString()}`);
|
||||
return await api.get(`/v1/a76/units-of-measure/ace/?${queryParams.toString()}`);
|
||||
}
|
||||
|
||||
export async function createUnitOfMeasureACE(data: UnitOfMeasureACECreate): Promise<ApiResponse<UnitOfMeasureACE>> {
|
||||
return await api.post('/a76/units-of-measure/ace', data);
|
||||
export async function createUnitOfMeasureACE(data: UnitOfMeasureACECreate, companyId: number): Promise<ApiResponse<UnitOfMeasureACE>> {
|
||||
return await api.post(`/v1/a76/units-of-measure/ace/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function updateUnitOfMeasureACE(id: number, data: UnitOfMeasureACEUpdate): Promise<ApiResponse<UnitOfMeasureACE>> {
|
||||
return await api.put(`/a76/units-of-measure/ace/${id}`, data);
|
||||
export async function updateUnitOfMeasureACE(id: number, data: UnitOfMeasureACEUpdate, companyId: number): Promise<ApiResponse<UnitOfMeasureACE>> {
|
||||
return await api.put(`/v1/a76/units-of-measure/ace/${id}/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteUnitOfMeasureACE(id: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/a76/units-of-measure/ace/${id}`);
|
||||
export async function deleteUnitOfMeasureACE(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/units-of-measure/ace/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
// --- OMA ---
|
||||
@@ -81,28 +83,30 @@ export interface UnitOfMeasureOMAListResponse {
|
||||
}
|
||||
|
||||
export async function getUnitsOfMeasureOMA(
|
||||
page = 1,
|
||||
pageSize = 50,
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<UnitOfMeasureOMAListResponse>> {
|
||||
const queryParams = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
return await api.get(`/a76/units-of-measure/oma?${queryParams.toString()}`);
|
||||
return await api.get(`/v1/a76/units-of-measure/oma/?${queryParams.toString()}`);
|
||||
}
|
||||
|
||||
export async function createUnitOfMeasureOMA(data: UnitOfMeasureOMACreate): Promise<ApiResponse<UnitOfMeasureOMA>> {
|
||||
return await api.post('/a76/units-of-measure/oma', data);
|
||||
export async function createUnitOfMeasureOMA(data: UnitOfMeasureOMACreate, companyId: number): Promise<ApiResponse<UnitOfMeasureOMA>> {
|
||||
return await api.post(`/v1/a76/units-of-measure/oma/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function updateUnitOfMeasureOMA(id: number, data: UnitOfMeasureOMAUpdate): Promise<ApiResponse<UnitOfMeasureOMA>> {
|
||||
return await api.put(`/a76/units-of-measure/oma/${id}`, data);
|
||||
export async function updateUnitOfMeasureOMA(id: number, data: UnitOfMeasureOMAUpdate, companyId: number): Promise<ApiResponse<UnitOfMeasureOMA>> {
|
||||
return await api.put(`/v1/a76/units-of-measure/oma/${id}/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteUnitOfMeasureOMA(id: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/a76/units-of-measure/oma/${id}`);
|
||||
export async function deleteUnitOfMeasureOMA(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/units-of-measure/oma/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
// --- American ---
|
||||
@@ -133,26 +137,139 @@ export interface UnitOfMeasureAmericanListResponse {
|
||||
}
|
||||
|
||||
export async function getUnitsOfMeasureAmerican(
|
||||
page = 1,
|
||||
pageSize = 50,
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<UnitOfMeasureAmericanListResponse>> {
|
||||
const queryParams = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
return await api.get(`/a76/units-of-measure/american?${queryParams.toString()}`);
|
||||
return await api.get(`/v1/a76/units-of-measure/american/?${queryParams.toString()}`);
|
||||
}
|
||||
|
||||
export async function createUnitOfMeasureAmerican(data: UnitOfMeasureAmericanCreate): Promise<ApiResponse<UnitOfMeasureAmerican>> {
|
||||
return await api.post('/a76/units-of-measure/american', data);
|
||||
export async function createUnitOfMeasureAmerican(data: UnitOfMeasureAmericanCreate, companyId: number): Promise<ApiResponse<UnitOfMeasureAmerican>> {
|
||||
return await api.post(`/v1/a76/units-of-measure/american/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function updateUnitOfMeasureAmerican(id: number, data: UnitOfMeasureAmericanUpdate): Promise<ApiResponse<UnitOfMeasureAmerican>> {
|
||||
return await api.put(`/a76/units-of-measure/american/${id}`, data);
|
||||
export async function updateUnitOfMeasureAmerican(id: number, data: UnitOfMeasureAmericanUpdate, companyId: number): Promise<ApiResponse<UnitOfMeasureAmerican>> {
|
||||
return await api.put(`/v1/a76/units-of-measure/american/${id}/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteUnitOfMeasureAmerican(id: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/a76/units-of-measure/american/${id}`);
|
||||
export async function deleteUnitOfMeasureAmerican(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/units-of-measure/american/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
// --- General ---
|
||||
export interface UnitOfMeasureGeneral {
|
||||
id: number;
|
||||
code: string;
|
||||
description: string | null;
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface UnitOfMeasureGeneralCreate {
|
||||
code: string;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export interface UnitOfMeasureGeneralUpdate {
|
||||
code?: string;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export interface UnitOfMeasureGeneralListResponse {
|
||||
items: UnitOfMeasureGeneral[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export async function getUnitsOfMeasureGeneral(
|
||||
page = 1,
|
||||
pageSize = 50,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<UnitOfMeasureGeneralListResponse>> {
|
||||
const queryParams = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
return await api.get(`/v1/a76/units-of-measure/general/?${queryParams.toString()}`);
|
||||
}
|
||||
|
||||
export async function createUnitOfMeasureGeneral(data: UnitOfMeasureGeneralCreate, companyId: number): Promise<ApiResponse<UnitOfMeasureGeneral>> {
|
||||
return await api.post(`/v1/a76/units-of-measure/general/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function updateUnitOfMeasureGeneral(id: number, data: UnitOfMeasureGeneralUpdate, companyId: number): Promise<ApiResponse<UnitOfMeasureGeneral>> {
|
||||
return await api.put(`/v1/a76/units-of-measure/general/${id}/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteUnitOfMeasureGeneral(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/units-of-measure/general/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
// --- Customs ---
|
||||
export interface UnitOfMeasureCustoms {
|
||||
id: number;
|
||||
code: string;
|
||||
description: string | null;
|
||||
scaii_unit_code: string | null;
|
||||
created_at: string | null;
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface UnitOfMeasureCustomsCreate {
|
||||
code: string;
|
||||
description?: string | null;
|
||||
scaii_unit_code?: string | null;
|
||||
}
|
||||
|
||||
export interface UnitOfMeasureCustomsUpdate {
|
||||
code?: string;
|
||||
description?: string | null;
|
||||
scaii_unit_code?: string | null;
|
||||
}
|
||||
|
||||
export interface UnitOfMeasureCustomsListResponse {
|
||||
items: UnitOfMeasureCustoms[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export async function getUnitsOfMeasureCustoms(
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<UnitOfMeasureCustomsListResponse>> {
|
||||
const queryParams = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString(),
|
||||
company_id: companyId.toString(),
|
||||
...filters
|
||||
});
|
||||
return await api.get(`/v1/a76/units-of-measure/customs/?${queryParams.toString()}`);
|
||||
}
|
||||
|
||||
export async function createUnitOfMeasureCustoms(data: UnitOfMeasureCustomsCreate, companyId: number): Promise<ApiResponse<UnitOfMeasureCustoms>> {
|
||||
return await api.post(`/v1/a76/units-of-measure/customs/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function updateUnitOfMeasureCustoms(id: number, data: UnitOfMeasureCustomsUpdate, companyId: number): Promise<ApiResponse<UnitOfMeasureCustoms>> {
|
||||
return await api.put(`/v1/a76/units-of-measure/customs/${id}/?company_id=${companyId}`, data);
|
||||
}
|
||||
|
||||
export async function deleteUnitOfMeasureCustoms(id: number, companyId: number): Promise<ApiResponse<void>> {
|
||||
return await api.delete(`/v1/a76/units-of-measure/customs/${id}/?company_id=${companyId}`);
|
||||
}
|
||||
|
||||
318
frontend/src/lib/api/dashboard/a76/invoices.ts
Normal file
318
frontend/src/lib/api/dashboard/a76/invoices.ts
Normal file
@@ -0,0 +1,318 @@
|
||||
/**
|
||||
* API Client para Facturas (Invoices)
|
||||
* Gestiona las operaciones CRUD para facturas y sus relaciones
|
||||
*/
|
||||
import { api } from '$lib/api';
|
||||
|
||||
export type OperationType = 'imp' | 'exp';
|
||||
export type TransportType = 'none' | 'transport' | 'box' | 'licence plates' | 'truck' | 'vessel' | 'rail barge' | 'container' | 'airplane' | 'gondola' | 'flatbed';
|
||||
|
||||
// --- Interfaces ---
|
||||
|
||||
export interface InvoiceComplianceMx {
|
||||
invoice_id?: number;
|
||||
pedimento?: string | null;
|
||||
pedimento_code?: string | null;
|
||||
remesa?: number | null;
|
||||
aduana?: string | null;
|
||||
provider_header?: string | null;
|
||||
provider_id?: string | null;
|
||||
sold_to_header?: string | null;
|
||||
sold_to_id?: string | null;
|
||||
shipped_to_header?: string | null;
|
||||
shipped_to_id?: string | null;
|
||||
shipped_by_header?: string | null;
|
||||
shipped_by_id?: string | null;
|
||||
customs_broker_id?: string | null;
|
||||
is_mixed?: boolean | null;
|
||||
waste_type?: string | null;
|
||||
appendix_17?: number | null;
|
||||
edocument?: string | null;
|
||||
electronic_signature?: string | null;
|
||||
sem_id?: number | null;
|
||||
}
|
||||
|
||||
export interface InvoiceFinancials {
|
||||
id?: number;
|
||||
invoice_id?: number;
|
||||
currency?: string | null;
|
||||
currency_type?: string | null;
|
||||
exchange_rate?: number | null;
|
||||
value_mn?: number | null;
|
||||
value_me?: number | null;
|
||||
customs_value_mn?: number | null;
|
||||
freight?: number | null;
|
||||
insurance?: number | null;
|
||||
iva_mn?: number | null;
|
||||
iva_factor?: number | null;
|
||||
total_quantity?: number | null;
|
||||
gross_weight?: number | null;
|
||||
net_weight?: number | null;
|
||||
bundle_count?: number | null;
|
||||
}
|
||||
|
||||
export interface InvoiceLogistics {
|
||||
id?: number;
|
||||
invoice_id?: number;
|
||||
carrier_id?: string | null;
|
||||
transport_type?: TransportType | null;
|
||||
transport_mode?: string | null;
|
||||
driver_name?: string | null;
|
||||
is_rail?: string | null;
|
||||
rail_id?: string | null;
|
||||
vehicle_num?: string | null;
|
||||
license_plate?: string | null;
|
||||
seal_number?: string | null;
|
||||
guide_number?: string | null;
|
||||
entry_exit_date?: string | null;
|
||||
}
|
||||
|
||||
export interface InvoiceSalesDetails {
|
||||
id?: number;
|
||||
invoice_id?: number;
|
||||
line_number: number;
|
||||
sales_order?: string | null;
|
||||
colors_description?: string | null;
|
||||
square_color_code?: string | null;
|
||||
line_bundles?: number | null;
|
||||
}
|
||||
|
||||
export interface InvoiceCollections {
|
||||
id?: number;
|
||||
invoice_id?: number;
|
||||
concept?: string | null;
|
||||
is_collected?: number | null;
|
||||
collection_date?: string | null;
|
||||
amount?: number | null;
|
||||
collector_user?: string | null;
|
||||
}
|
||||
|
||||
export interface Invoice {
|
||||
id: number;
|
||||
tenant_id: number;
|
||||
company_id: number;
|
||||
operation_type?: OperationType | null;
|
||||
invoice_type?: string | null;
|
||||
invoice_number?: string | null;
|
||||
project_number?: string | null;
|
||||
purchase_order?: string | null;
|
||||
related_doc_id?: number | null;
|
||||
invoice_date?: string | null;
|
||||
capture_date: string;
|
||||
is_updated?: boolean | null;
|
||||
updated_date?: string | null;
|
||||
who_updated?: string | null;
|
||||
traffic_light_status?: string | null;
|
||||
process_log?: string | null;
|
||||
observation_es?: string | null;
|
||||
observation_en?: string | null;
|
||||
comments_status?: string | null;
|
||||
cfdi_uuid?: string | null;
|
||||
path_pdf?: string | null;
|
||||
path_xml?: string | null;
|
||||
compliance_mx?: InvoiceComplianceMx | null;
|
||||
financials?: InvoiceFinancials | null;
|
||||
logistics?: InvoiceLogistics[];
|
||||
details?: InvoiceSalesDetails[];
|
||||
collections?: InvoiceCollections[];
|
||||
}
|
||||
|
||||
export interface InvoiceListResponse {
|
||||
items: Invoice[];
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
}
|
||||
|
||||
export interface InvoiceData {
|
||||
operation_type?: OperationType | null;
|
||||
invoice_type?: string | null;
|
||||
invoice_number?: string | null;
|
||||
project_number?: string | null;
|
||||
purchase_order?: string | null;
|
||||
related_doc_id?: number | null;
|
||||
invoice_date?: string | null;
|
||||
traffic_light_status?: string | null;
|
||||
process_log?: string | null;
|
||||
observation_es?: string | null;
|
||||
observation_en?: string | null;
|
||||
comments_status?: string | null;
|
||||
cfdi_uuid?: string | null;
|
||||
path_pdf?: string | null;
|
||||
path_xml?: string | null;
|
||||
compliance_mx?: Omit<InvoiceComplianceMx, 'invoice_id'> | null;
|
||||
financials?: Omit<InvoiceFinancials, 'id' | 'invoice_id'> | null;
|
||||
logistics?: Omit<InvoiceLogistics, 'id' | 'invoice_id'>[] | null;
|
||||
details?: Omit<InvoiceSalesDetails, 'id' | 'invoice_id'>[] | null;
|
||||
collections?: Omit<InvoiceCollections, 'id' | 'invoice_id'>[] | null;
|
||||
}
|
||||
|
||||
export interface UpdateInvoiceData {
|
||||
operation_type?: OperationType | null;
|
||||
invoice_type?: string | null;
|
||||
invoice_number?: string | null;
|
||||
project_number?: string | null;
|
||||
purchase_order?: string | null;
|
||||
related_doc_id?: number | null;
|
||||
invoice_date?: string | null;
|
||||
traffic_light_status?: string | null;
|
||||
process_log?: string | null;
|
||||
observation_es?: string | null;
|
||||
observation_en?: string | null;
|
||||
comments_status?: string | null;
|
||||
cfdi_uuid?: string | null;
|
||||
path_pdf?: string | null;
|
||||
path_xml?: string | null;
|
||||
compliance_mx?: Partial<InvoiceComplianceMx> | null;
|
||||
financials?: Partial<InvoiceFinancials> | null;
|
||||
logistics?: Partial<InvoiceLogistics>[] | null;
|
||||
details?: Partial<InvoiceSalesDetails>[] | null;
|
||||
collections?: Partial<InvoiceCollections>[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* API para Facturas
|
||||
*/
|
||||
export const invoicesApi = {
|
||||
/**
|
||||
* Lista todas las facturas con paginación
|
||||
*/
|
||||
list: (companyId: number, page = 1, pageSize = 50, filters?: Record<string, any>) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString(),
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString()
|
||||
});
|
||||
|
||||
// Agregar filtros si existen
|
||||
if (filters) {
|
||||
Object.entries(filters).forEach(([key, value]) => {
|
||||
if (value !== null && value !== undefined && value !== '') {
|
||||
params.append(key, String(value));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return api.get<InvoiceListResponse>(`/v1/a76/invoices?${params.toString()}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Obtiene una factura por ID
|
||||
*/
|
||||
get: (invoiceId: number, companyId: number) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.get<Invoice>(`/v1/a76/invoices/${invoiceId}?${params.toString()}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* Crea una nueva factura
|
||||
*/
|
||||
create: (companyId: number, data: CreateInvoiceData) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.post<Invoice>(`/v1/a76/invoices?${params.toString()}`, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Actualiza una factura existente
|
||||
*/
|
||||
update: (invoiceId: number, companyId: number, data: UpdateInvoiceData) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.put<Invoice>(`/v1/a76/invoices/${invoiceId}?${params.toString()}`, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Elimina una factura
|
||||
*/
|
||||
delete: (invoiceId: number, companyId: number) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.delete(`/v1/a76/invoices/${invoiceId}?${params.toString()}`);
|
||||
},
|
||||
|
||||
// --- Nested Resources ---
|
||||
|
||||
/**
|
||||
* Logística de factura
|
||||
*/
|
||||
logistics: {
|
||||
list: (invoiceId: number, companyId: number) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.get<InvoiceLogistics[]>(`/v1/a76/invoices/${invoiceId}/logistics?${params.toString()}`);
|
||||
},
|
||||
|
||||
create: (invoiceId: number, companyId: number, data: Omit<InvoiceLogistics, 'id' | 'invoice_id'>) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.post<InvoiceLogistics>(`/v1/a76/invoices/${invoiceId}/logistics?${params.toString()}`, data);
|
||||
},
|
||||
|
||||
delete: (invoiceId: number, logisticsId: number, companyId: number) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.delete(`/v1/a76/invoices/${invoiceId}/logistics/${logisticsId}?${params.toString()}`);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Detalles de venta de factura
|
||||
*/
|
||||
details: {
|
||||
list: (invoiceId: number, companyId: number) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.get<InvoiceSalesDetails[]>(`/v1/a76/invoices/${invoiceId}/details?${params.toString()}`);
|
||||
},
|
||||
|
||||
create: (invoiceId: number, companyId: number, data: Omit<InvoiceSalesDetails, 'id' | 'invoice_id'>) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.post<InvoiceSalesDetails>(`/v1/a76/invoices/${invoiceId}/details?${params.toString()}`, data);
|
||||
},
|
||||
|
||||
delete: (invoiceId: number, detailId: number, companyId: number) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.delete(`/v1/a76/invoices/${invoiceId}/details/${detailId}?${params.toString()}`);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Cobranzas de factura
|
||||
*/
|
||||
collections: {
|
||||
list: (invoiceId: number, companyId: number) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.get<InvoiceCollections[]>(`/v1/a76/invoices/${invoiceId}/collections?${params.toString()}`);
|
||||
},
|
||||
|
||||
create: (invoiceId: number, companyId: number, data: Omit<InvoiceCollections, 'id' | 'invoice_id'>) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.post<InvoiceCollections>(`/v1/a76/invoices/${invoiceId}/collections?${params.toString()}`, data);
|
||||
},
|
||||
|
||||
delete: (invoiceId: number, collectionId: number, companyId: number) => {
|
||||
const params = new URLSearchParams({
|
||||
company_id: companyId.toString()
|
||||
});
|
||||
return api.delete(`/v1/a76/invoices/${invoiceId}/collections/${collectionId}?${params.toString()}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -9,6 +9,7 @@ export interface InvoiceType {
|
||||
description: string;
|
||||
note?: string;
|
||||
type?: string;
|
||||
operation?: string;
|
||||
}
|
||||
|
||||
export interface InvoiceTypeListResponse {
|
||||
@@ -40,11 +41,20 @@ export const invoiceTypesApi = {
|
||||
* Lista todos los tipos de factura con paginación
|
||||
* @param page - Número de página (por defecto 1)
|
||||
* @param pageSize - Tamaño de página (por defecto 50)
|
||||
* @param operation - Filtrar por tipo de operación (imp, exp)
|
||||
*/
|
||||
list: (page = 1, pageSize = 50) =>
|
||||
api.get<InvoiceTypeListResponse>(
|
||||
`/v1/public/refrence_data/invoice-types?page=${page}&page_size=${pageSize}`
|
||||
),
|
||||
list: (page = 1, pageSize = 50, operation?: string) => {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: pageSize.toString()
|
||||
});
|
||||
if (operation) {
|
||||
params.append('operation', operation);
|
||||
}
|
||||
return api.get<InvoiceTypeListResponse>(
|
||||
`/v1/public/refrence_data/invoice-types?${params.toString()}`
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Obtiene un tipo de factura por key
|
||||
|
||||
@@ -1 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="107" height="128" viewBox="0 0 107 128"><title>svelte-logo</title><path d="M94.157 22.819c-10.4-14.885-30.94-19.297-45.792-9.835L22.282 29.608A29.92 29.92 0 0 0 8.764 49.65a31.5 31.5 0 0 0 3.108 20.231 30 30 0 0 0-4.477 11.183 31.9 31.9 0 0 0 5.448 24.116c10.402 14.887 30.942 19.297 45.791 9.835l26.083-16.624A29.92 29.92 0 0 0 98.235 78.35a31.53 31.53 0 0 0-3.105-20.232 30 30 0 0 0 4.474-11.182 31.88 31.88 0 0 0-5.447-24.116" style="fill:#ff3e00"/><path d="M45.817 106.582a20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.503 18 18 0 0 1 .624-2.435l.49-1.498 1.337.981a33.6 33.6 0 0 0 10.203 5.098l.97.294-.09.968a5.85 5.85 0 0 0 1.052 3.878 6.24 6.24 0 0 0 6.695 2.485 5.8 5.8 0 0 0 1.603-.704L69.27 76.28a5.43 5.43 0 0 0 2.45-3.631 5.8 5.8 0 0 0-.987-4.371 6.24 6.24 0 0 0-6.698-2.487 5.7 5.7 0 0 0-1.6.704l-9.953 6.345a19 19 0 0 1-5.296 2.326 20.72 20.72 0 0 1-22.237-8.243 19.17 19.17 0 0 1-3.277-14.502 17.99 17.99 0 0 1 8.13-12.052l26.081-16.623a19 19 0 0 1 5.3-2.329 20.72 20.72 0 0 1 22.237 8.243 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-.624 2.435l-.49 1.498-1.337-.98a33.6 33.6 0 0 0-10.203-5.1l-.97-.294.09-.968a5.86 5.86 0 0 0-1.052-3.878 6.24 6.24 0 0 0-6.696-2.485 5.8 5.8 0 0 0-1.602.704L37.73 51.72a5.42 5.42 0 0 0-2.449 3.63 5.79 5.79 0 0 0 .986 4.372 6.24 6.24 0 0 0 6.698 2.486 5.8 5.8 0 0 0 1.602-.704l9.952-6.342a19 19 0 0 1 5.295-2.328 20.72 20.72 0 0 1 22.237 8.242 19.17 19.17 0 0 1 3.277 14.503 18 18 0 0 1-8.13 12.053l-26.081 16.622a19 19 0 0 1-5.3 2.328" style="fill:#fff"/></svg>
|
||||
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<linearGradient id="techGradient" x1="16" y1="16" x2="48" y2="48" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#00F2FE" /> <stop offset="100%" stop-color="#4FACFE" /> </linearGradient>
|
||||
</defs>
|
||||
|
||||
<rect width="64" height="64" rx="18" fill="#0F172A"/>
|
||||
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M32 14L46 26V40L32 52L18 40V26L32 14ZM32 20.5L23 28.2V35.8L32 43.5L41 35.8V28.2L32 20.5Z" fill="url(#techGradient)"/>
|
||||
|
||||
<path d="M32 20.5V30M32 34V43.5" stroke="#0F172A" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M23 35.8L32 30M41 35.8L32 30" stroke="#0F172A" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 781 B |
@@ -1,127 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as Dialog from "$lib/components/ui/dialog";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { createCompany, updateCompany, type Company } from "$lib/api/dashboard/a76/general_catalogs/company";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: Company | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? "Editar Empresa" : "Nueva Empresa");
|
||||
|
||||
let formData = $state({
|
||||
name: item?.name || '',
|
||||
rfc: item?.rfc || '',
|
||||
main_activity: item?.main_activity || '',
|
||||
program: item?.program || '',
|
||||
program_number: item?.program_number || ''
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
if (item) {
|
||||
formData = {
|
||||
name: item.name || '',
|
||||
rfc: item.rfc || '',
|
||||
main_activity: item.main_activity || '',
|
||||
program: item.program || '',
|
||||
program_number: item.program_number || ''
|
||||
};
|
||||
} else {
|
||||
formData = {
|
||||
name: '',
|
||||
rfc: '',
|
||||
main_activity: '',
|
||||
program: '',
|
||||
program_number: ''
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function handleSubmit() {
|
||||
error = null;
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
if (!formData.name.trim()) throw new Error('El nombre es requerido');
|
||||
|
||||
const dataToSend = {
|
||||
name: formData.name.trim(),
|
||||
rfc: formData.rfc.trim() || null,
|
||||
main_activity: formData.main_activity.trim() || null,
|
||||
program: formData.program.trim() || null,
|
||||
program_number: formData.program_number.trim() || null
|
||||
};
|
||||
|
||||
let response;
|
||||
if (isEdit && item) {
|
||||
response = await updateCompany(item.id, dataToSend);
|
||||
} else {
|
||||
response = await createCompany(dataToSend);
|
||||
}
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Error al guardar';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="max-w-md">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4">
|
||||
{#if error}
|
||||
<div class="text-destructive text-sm">{error}</div>
|
||||
{/if}
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="name">Nombre <span class="text-destructive">*</span></Label>
|
||||
<Input id="name" bind:value={formData.name} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="rfc">RFC</Label>
|
||||
<Input id="rfc" bind:value={formData.rfc} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="program">Programa</Label>
|
||||
<Input id="program" bind:value={formData.program} />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="program_number">No. Programa</Label>
|
||||
<Input id="program_number" bind:value={formData.program_number} />
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false}>Cancelar</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : 'Guardar'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -1,381 +1,221 @@
|
||||
<script lang="ts">
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as Dialog from "$lib/components/ui/dialog";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { customsBrokersApi, type CreateCustomsBrokerData } from "$lib/api/dashboard/a76/customs-brokers";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as Dialog from "$lib/components/ui/dialog";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import { Separator } from "$lib/components/ui/separator";
|
||||
import { Loader2 } from "lucide-svelte";
|
||||
import type { CreateCustomsBrokerData, CustomsBroker } from "$lib/api/dashboard/a76/customs-brokers"; // Ajusta la ruta
|
||||
import { toast } from "svelte-sonner";
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
// --- Props ---
|
||||
export let open = false;
|
||||
export let mode: "create" | "edit" = "create";
|
||||
export let initialData: CustomsBroker | null = null;
|
||||
export let companyId: string; // Necesario según tu API
|
||||
|
||||
let formData = $state({
|
||||
broker_key: "",
|
||||
name: "",
|
||||
type: "",
|
||||
address: "",
|
||||
postal_code: "",
|
||||
city: "",
|
||||
state: "",
|
||||
phone: "",
|
||||
fax: "",
|
||||
email: "",
|
||||
country: "",
|
||||
tax_id: "",
|
||||
personal_id: "",
|
||||
position: "",
|
||||
license: "",
|
||||
company: "",
|
||||
contact: ""
|
||||
});
|
||||
// La función onSave ahora devuelve una promesa para manejar el loading aquí
|
||||
export let onSave: (data: CreateCustomsBrokerData) => Promise<void>;
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
// --- Estado ---
|
||||
let loading = false;
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!companyStore.activeCompany) {
|
||||
error = "No hay compañía seleccionada";
|
||||
return;
|
||||
}
|
||||
// Estado del formulario
|
||||
let formData: CreateCustomsBrokerData = {
|
||||
broker_key: "",
|
||||
license: "",
|
||||
name: "",
|
||||
tax_id: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
fax: "",
|
||||
contact: "",
|
||||
address: "",
|
||||
postal_code: "",
|
||||
city: "",
|
||||
state: "",
|
||||
country: "",
|
||||
tenant_id: "", // Se llenará en el submit o por defecto
|
||||
company_id: ""
|
||||
};
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
// --- Reactividad ---
|
||||
$: if (open) {
|
||||
if (mode === "edit" && initialData) {
|
||||
// Cargar datos existentes
|
||||
formData = {
|
||||
...initialData,
|
||||
// Aseguramos que no sean null/undefined para los inputs
|
||||
name: initialData.name || "",
|
||||
tax_id: initialData.tax_id || "",
|
||||
email: initialData.email || "",
|
||||
phone: initialData.phone || "",
|
||||
fax: initialData.fax || "",
|
||||
contact: initialData.contact || "",
|
||||
address: initialData.address || "",
|
||||
postal_code: initialData.postal_code || "",
|
||||
city: initialData.city || "",
|
||||
state: initialData.state || "",
|
||||
country: initialData.country || "",
|
||||
license: initialData.license || ""
|
||||
};
|
||||
} else {
|
||||
// Reset para crear
|
||||
formData = {
|
||||
broker_key: "",
|
||||
license: "",
|
||||
name: "",
|
||||
tax_id: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
fax: "",
|
||||
contact: "",
|
||||
address: "",
|
||||
postal_code: "",
|
||||
city: "",
|
||||
state: "",
|
||||
country: "MEX", // Valor por defecto sugerido
|
||||
tenant_id: "default", // Ajustar según lógica de tu app
|
||||
company_id: companyId
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const payload: CreateCustomsBrokerData = {
|
||||
broker_key: formData.broker_key,
|
||||
name: formData.name || null,
|
||||
type: formData.type || null,
|
||||
address: formData.address || null,
|
||||
postal_code: formData.postal_code || null,
|
||||
city: formData.city || null,
|
||||
state: formData.state || null,
|
||||
phone: formData.phone || null,
|
||||
fax: formData.fax || null,
|
||||
email: formData.email || null,
|
||||
country: formData.country || null,
|
||||
tax_id: formData.tax_id || null,
|
||||
personal_id: formData.personal_id || null,
|
||||
position: formData.position || null,
|
||||
license: formData.license || null,
|
||||
company: formData.company || null,
|
||||
contact: formData.contact || null,
|
||||
tenant_id: "1", // TODO: Get from user context
|
||||
company_id: companyStore.activeCompany.id.toString()
|
||||
};
|
||||
// --- Handlers ---
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
loading = true;
|
||||
|
||||
const response = await customsBrokersApi.create(payload);
|
||||
// Validaciones básicas
|
||||
if (!formData.broker_key) {
|
||||
toast.error("La Clave del Agente es obligatoria");
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
if (!formData.license) {
|
||||
toast.error("La Patente es obligatoria");
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.error) {
|
||||
if (response.status === 401) {
|
||||
error = 'Sesión expirada. Recargando página...';
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 1500);
|
||||
} else {
|
||||
error = response.error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Inyectar company_id si no viene
|
||||
const payload = { ...formData, company_id: companyId };
|
||||
|
||||
// Éxito
|
||||
open = false;
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : "Error al guardar";
|
||||
console.error("Error saving:", e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpenChange(newOpen: boolean) {
|
||||
if (!newOpen) {
|
||||
// Limpiar form al cerrar
|
||||
formData = {
|
||||
broker_key: "",
|
||||
name: "",
|
||||
type: "",
|
||||
address: "",
|
||||
postal_code: "",
|
||||
city: "",
|
||||
state: "",
|
||||
phone: "",
|
||||
fax: "",
|
||||
email: "",
|
||||
country: "",
|
||||
tax_id: "",
|
||||
personal_id: "",
|
||||
position: "",
|
||||
license: "",
|
||||
company: "",
|
||||
contact: ""
|
||||
};
|
||||
error = null;
|
||||
}
|
||||
open = newOpen;
|
||||
}
|
||||
await onSave(payload);
|
||||
open = false;
|
||||
toast.success(mode === 'create' ? "Agente creado correctamente" : "Agente actualizado correctamente");
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toast.error("Error al guardar el agente aduanal");
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open onOpenChange={handleOpenChange}>
|
||||
<Dialog.Content class="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Nuevo Agente Aduanal</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Completa los datos para crear un nuevo agente aduanal.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>
|
||||
{mode === "create" ? "Nuevo Agente Aduanal" : "Editar Agente Aduanal"}
|
||||
</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Ingresa los datos generales del agente. La configuración de VU y Personal se gestiona aparte.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={handleSubmit} class="space-y-6">
|
||||
{#if error}
|
||||
<div class="rounded-lg border border-destructive bg-destructive/10 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="grid gap-6 py-4">
|
||||
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-sm font-medium leading-none text-muted-foreground">Identificación</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="broker_key">Clave Agente *</Label>
|
||||
<Input id="broker_key" bind:value={formData.broker_key} placeholder="Ej. 550" disabled={mode === 'edit' || loading} />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="license">Patente *</Label>
|
||||
<Input id="license" bind:value={formData.license} placeholder="Ej. 3421" disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2 col-span-2">
|
||||
<Label for="name">Nombre / Razón Social</Label>
|
||||
<Input id="name" bind:value={formData.name} placeholder="Nombre del Agente o Agencia" disabled={loading} />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="tax_id">RFC</Label>
|
||||
<Input id="tax_id" bind:value={formData.tax_id} placeholder="RFC de la agencia" disabled={loading} />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="contact">Nombre Contacto</Label>
|
||||
<Input id="contact" bind:value={formData.contact} placeholder="Persona de contacto" disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Información básica -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-sm font-semibold">Información Básica</h3>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="broker_key">Clave *</Label>
|
||||
<Input
|
||||
id="broker_key"
|
||||
bind:value={formData.broker_key}
|
||||
placeholder="Ej: 12345"
|
||||
maxlength={5}
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="type">Tipo</Label>
|
||||
<Input
|
||||
id="type"
|
||||
bind:value={formData.type}
|
||||
placeholder="Tipo de agente"
|
||||
maxlength={9}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-sm font-medium leading-none text-muted-foreground">Contacto</h4>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="space-y-2 col-span-1">
|
||||
<Label for="phone">Teléfono</Label>
|
||||
<Input id="phone" bind:value={formData.phone} disabled={loading} />
|
||||
</div>
|
||||
<div class="space-y-2 col-span-2">
|
||||
<Label for="email">Correo Electrónico</Label>
|
||||
<Input id="email" type="email" bind:value={formData.email} disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="name">Nombre</Label>
|
||||
<Input
|
||||
id="name"
|
||||
bind:value={formData.name}
|
||||
placeholder="Nombre del agente aduanal"
|
||||
maxlength={80}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="license">Patente</Label>
|
||||
<Input
|
||||
id="license"
|
||||
bind:value={formData.license}
|
||||
placeholder="Número de patente"
|
||||
maxlength={4}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-sm font-medium leading-none text-muted-foreground">Dirección Fiscal</h4>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="address">Calle y Número</Label>
|
||||
<Input id="address" bind:value={formData.address} disabled={loading} />
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="company">Empresa</Label>
|
||||
<Input
|
||||
id="company"
|
||||
bind:value={formData.company}
|
||||
placeholder="Empresa del agente"
|
||||
maxlength={200}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="postal_code">C.P.</Label>
|
||||
<Input id="postal_code" bind:value={formData.postal_code} disabled={loading} />
|
||||
</div>
|
||||
<div class="space-y-2 col-span-2">
|
||||
<Label for="city">Ciudad</Label>
|
||||
<Input id="city" bind:value={formData.city} disabled={loading} />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="state">Estado</Label>
|
||||
<Input id="state" bind:value={formData.state} disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="country">País</Label>
|
||||
<Input id="country" bind:value={formData.country} disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Información de contacto -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-sm font-semibold">Información de Contacto</h3>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="phone">Teléfono</Label>
|
||||
<Input
|
||||
id="phone"
|
||||
bind:value={formData.phone}
|
||||
placeholder="Número telefónico"
|
||||
maxlength={30}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="fax">Fax</Label>
|
||||
<Input
|
||||
id="fax"
|
||||
bind:value={formData.fax}
|
||||
placeholder="Número de fax"
|
||||
maxlength={30}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
bind:value={formData.email}
|
||||
placeholder="correo@ejemplo.com"
|
||||
maxlength={100}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="contact">Contacto</Label>
|
||||
<Input
|
||||
id="contact"
|
||||
bind:value={formData.contact}
|
||||
placeholder="Nombre del contacto"
|
||||
maxlength={80}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dirección -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-sm font-semibold">Dirección</h3>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="address">Dirección</Label>
|
||||
<Input
|
||||
id="address"
|
||||
bind:value={formData.address}
|
||||
placeholder="Calle y número"
|
||||
maxlength={1500}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="postal_code">Código Postal</Label>
|
||||
<Input
|
||||
id="postal_code"
|
||||
bind:value={formData.postal_code}
|
||||
placeholder="C.P."
|
||||
maxlength={15}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="city">Ciudad</Label>
|
||||
<Input
|
||||
id="city"
|
||||
bind:value={formData.city}
|
||||
placeholder="Ciudad"
|
||||
maxlength={30}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="state">Estado</Label>
|
||||
<Input
|
||||
id="state"
|
||||
bind:value={formData.state}
|
||||
placeholder="Estado"
|
||||
maxlength={30}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="country">País</Label>
|
||||
<Input
|
||||
id="country"
|
||||
bind:value={formData.country}
|
||||
placeholder="País"
|
||||
maxlength={3}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Información fiscal -->
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-sm font-semibold">Información Fiscal</h3>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="tax_id">RFC</Label>
|
||||
<Input
|
||||
id="tax_id"
|
||||
bind:value={formData.tax_id}
|
||||
placeholder="RFC"
|
||||
maxlength={30}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="personal_id">CURP</Label>
|
||||
<Input
|
||||
id="personal_id"
|
||||
bind:value={formData.personal_id}
|
||||
placeholder="CURP"
|
||||
maxlength={20}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="position">Posición</Label>
|
||||
<Input
|
||||
id="position"
|
||||
bind:value={formData.position}
|
||||
placeholder="Cargo o posición"
|
||||
maxlength={30}
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => (open = false)} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{#if loading}
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="h-4 w-4 animate-spin rounded-full border-2 border-primary-foreground border-t-transparent"></div>
|
||||
Guardando...
|
||||
</div>
|
||||
{:else}
|
||||
Crear Agente Aduanal
|
||||
{/if}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
<Dialog.Footer>
|
||||
<Button variant="outline" on:click={() => (open = false)} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button on:click={handleSubmit} disabled={loading}>
|
||||
{#if loading}
|
||||
<Loader2 class="mr-2 h-4 w-4 animate-spin" />
|
||||
Guardando
|
||||
{:else}
|
||||
Guardar Agente
|
||||
{/if}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -5,44 +5,44 @@ import DataTableActions from './data-table-actions.svelte';
|
||||
|
||||
export function createColumns(onSuccess?: () => void): ColumnDef<ExchangeRate>[] {
|
||||
return [
|
||||
{
|
||||
accessorKey: 'date',
|
||||
header: 'Fecha',
|
||||
cell: ({ row }) => {
|
||||
const dateStr = row.original.date;
|
||||
if (!dateStr) return 'N/A';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('es-MX');
|
||||
{
|
||||
accessorKey: 'date',
|
||||
header: 'Fecha',
|
||||
cell: ({ row }) => {
|
||||
const dateStr = row.original.date;
|
||||
if (!dateStr) return 'N/A';
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('es-MX');
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'value',
|
||||
header: 'Valor',
|
||||
cell: ({ row }) => {
|
||||
const value = row.original.value;
|
||||
if (value === null || value === undefined) return 'N/A';
|
||||
return value.toFixed(6);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'local_currency',
|
||||
header: 'Moneda Local',
|
||||
cell: ({ row }) => row.original.local_currency ?? 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'foreign_currency',
|
||||
header: 'Moneda Extranjera',
|
||||
cell: ({ row }) => row.original.foreign_currency ?? 'N/A'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Acciones',
|
||||
cell: ({ row }) => {
|
||||
return renderComponent(DataTableActions, {
|
||||
item: row.original,
|
||||
onSuccess
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'value',
|
||||
header: 'Tipo de Cambio',
|
||||
cell: ({ row }) => {
|
||||
const value = row.original.value;
|
||||
if (value === null || value === undefined) return 'N/A';
|
||||
return value.toFixed(6);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'local_currency',
|
||||
header: 'Moneda Local',
|
||||
cell: ({ row }) => row.original.local_currency ?? 'N/A'
|
||||
},
|
||||
{
|
||||
accessorKey: 'foreign_currency',
|
||||
header: 'Moneda Extranjera',
|
||||
cell: ({ row }) => row.original.foreign_currency ?? 'N/A'
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Acciones',
|
||||
cell: ({ row }) => {
|
||||
return renderComponent(DataTableActions, {
|
||||
item: row.original,
|
||||
onSuccess
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -1,164 +1,151 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { LoaderCircle } from 'lucide-svelte';
|
||||
import type {
|
||||
ExchangeRate,
|
||||
ExchangeRateCreate,
|
||||
ExchangeRateUpdate
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/exchange-rate';
|
||||
import { createExchangeRate, updateExchangeRate } from '$lib/api/dashboard/a76/general_catalogs/exchange-rate';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { Button } from "$lib/components/ui/button";
|
||||
import * as Dialog from "$lib/components/ui/dialog";
|
||||
import { Input } from "$lib/components/ui/input";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import {
|
||||
createExchangeRate,
|
||||
updateExchangeRate,
|
||||
type ExchangeRate
|
||||
} from "$lib/api/dashboard/a76/general_catalogs/exchange-rate";
|
||||
import { companyStore } from "$lib/stores/company.svelte";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
item?: ExchangeRate | null;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSuccess?: (item: ExchangeRate) => void;
|
||||
}
|
||||
let {
|
||||
open = $bindable(false),
|
||||
item = null,
|
||||
onSuccess
|
||||
}: {
|
||||
open: boolean;
|
||||
item?: ExchangeRate | null;
|
||||
onSuccess?: () => void;
|
||||
} = $props();
|
||||
|
||||
let { open = $bindable(false), item = null, onOpenChange, onSuccess }: Props = $props();
|
||||
const isEdit = $derived(!!item);
|
||||
const title = $derived(isEdit ? "Editar Tipo de Cambio" : "Nuevo Tipo de Cambio");
|
||||
|
||||
let formData = $state<ExchangeRateCreate | ExchangeRateUpdate>({
|
||||
date: '',
|
||||
value: null,
|
||||
local_currency: null,
|
||||
foreign_currency: null
|
||||
});
|
||||
let formData = $state({
|
||||
date: '',
|
||||
value: null as number | null,
|
||||
local_currency: '',
|
||||
foreign_currency: ''
|
||||
});
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
let isEdit = $derived(!!item);
|
||||
// Cargar datos al abrir
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
if (item && item.date) {
|
||||
const formattedDate = item.date.includes('T') ? item.date.split('T')[0] : item.date;
|
||||
formData = {
|
||||
date: formattedDate,
|
||||
value: item.value,
|
||||
local_currency: item.local_currency || '',
|
||||
foreign_currency: item.foreign_currency || ''
|
||||
};
|
||||
} else {
|
||||
formData = {
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
value: null,
|
||||
local_currency: 'MXN',
|
||||
foreign_currency: 'USD'
|
||||
};
|
||||
}
|
||||
error = null;
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (item) {
|
||||
const date = new Date(item.date);
|
||||
const dateStr = date.toISOString().split('T')[0];
|
||||
|
||||
formData = {
|
||||
date: dateStr,
|
||||
value: item.value,
|
||||
local_currency: item.local_currency,
|
||||
foreign_currency: item.foreign_currency
|
||||
};
|
||||
} else {
|
||||
const today = new Date();
|
||||
const dateStr = today.toISOString().split('T')[0];
|
||||
formData = {
|
||||
date: dateStr,
|
||||
value: null,
|
||||
local_currency: null,
|
||||
foreign_currency: null
|
||||
};
|
||||
}
|
||||
error = null;
|
||||
});
|
||||
async function handleSubmit() {
|
||||
loading = true;
|
||||
error = null;
|
||||
try {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) throw new Error('No hay una compañía seleccionada');
|
||||
|
||||
async function handleSubmit(e: Event) {
|
||||
e.preventDefault();
|
||||
loading = true;
|
||||
error = null;
|
||||
if (!formData.date) throw new Error('La fecha es requerida');
|
||||
if (formData.value === null) throw new Error('El valor es requerido');
|
||||
|
||||
const dataToSend = {
|
||||
date: formData.date,
|
||||
value: Number(formData.value),
|
||||
local_currency: formData.local_currency?.trim().toUpperCase() || null,
|
||||
foreign_currency: formData.foreign_currency?.trim().toUpperCase() || null
|
||||
};
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
error = 'No hay una empresa seleccionada';
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
if (isEdit && item) {
|
||||
await updateExchangeRate(item.id, dataToSend, companyId);
|
||||
alert(`✅ Tipo de cambio actualizado correctamente`);
|
||||
} else {
|
||||
await createExchangeRate(dataToSend, companyId);
|
||||
alert(`✅ Tipo de cambio creado correctamente`);
|
||||
}
|
||||
|
||||
try {
|
||||
let result: ExchangeRate;
|
||||
if (isEdit && item) {
|
||||
result = await updateExchangeRate(item.id, formData as ExchangeRateUpdate, companyId);
|
||||
} else {
|
||||
result = await createExchangeRate(formData as ExchangeRateCreate, companyId);
|
||||
}
|
||||
|
||||
if (onSuccess) {
|
||||
onSuccess(result);
|
||||
}
|
||||
onOpenChange(false);
|
||||
} catch (err: any) {
|
||||
error = err.message || `Error al ${isEdit ? 'actualizar' : 'crear'} el tipo de cambio`;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
open = false;
|
||||
if (onSuccess) onSuccess();
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Error al guardar';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root {open} onOpenChange={onOpenChange}>
|
||||
<Dialog.Content class="sm:max-w-[500px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{isEdit ? 'Editar' : 'Crear'} Tipo de Cambio</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay class="fixed inset-0 z-[9999] bg-black/80 backdrop-blur-sm" />
|
||||
|
||||
<Dialog.Content class="fixed left-[50%] top-[50%] z-[10000] w-full max-w-[500px] translate-x-[-50%] translate-y-[-50%] border bg-background p-6 shadow-lg sm:rounded-lg">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
|
||||
<form onsubmit={handleSubmit} class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="date">Fecha *</Label>
|
||||
<Input
|
||||
id="date"
|
||||
type="date"
|
||||
bind:value={formData.date}
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4 py-4">
|
||||
{#if error}
|
||||
<div class="rounded-md bg-destructive/15 p-3 text-sm text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="value">Tipo de Cambio *</Label>
|
||||
<Input
|
||||
id="value"
|
||||
type="number"
|
||||
step="0.000001"
|
||||
bind:value={formData.value}
|
||||
placeholder="0.000000"
|
||||
required
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid gap-4">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="date" class="text-right">Fecha *</Label>
|
||||
<div class="col-span-3">
|
||||
<Input id="date" type="date" bind:value={formData.date} disabled={loading} required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="local_currency">Moneda Local</Label>
|
||||
<Input
|
||||
id="local_currency"
|
||||
type="text"
|
||||
maxlength="7"
|
||||
bind:value={formData.local_currency}
|
||||
placeholder="MXN"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="value" class="text-right">Valor *</Label>
|
||||
<div class="col-span-3">
|
||||
<Input id="value" type="number" step="0.000001" bind:value={formData.value} disabled={loading} required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="foreign_currency">Moneda Extranjera</Label>
|
||||
<Input
|
||||
id="foreign_currency"
|
||||
type="text"
|
||||
maxlength="7"
|
||||
bind:value={formData.foreign_currency}
|
||||
placeholder="USD"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="local_currency" class="text-right">Local</Label>
|
||||
<div class="col-span-3">
|
||||
<Input id="local_currency" bind:value={formData.local_currency} maxlength={3} disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<p class="text-sm text-destructive">{error}</p>
|
||||
{/if}
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="foreign_currency" class="text-right">Extranjera</Label>
|
||||
<div class="col-span-3">
|
||||
<Input id="foreign_currency" bind:value={formData.foreign_currency} maxlength={3} disabled={loading} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button type="button" variant="outline" onclick={() => onOpenChange(false)} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{#if loading}
|
||||
<LoaderCircle class="mr-2 h-4 w-4 animate-spin" />
|
||||
{/if}
|
||||
{isEdit ? 'Actualizar' : 'Crear'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
<Dialog.Footer>
|
||||
<Button type="button" variant="outline" onclick={() => open = false} disabled={loading}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Guardando...' : isEdit ? 'Actualizar' : 'Crear'}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
@@ -27,23 +27,21 @@
|
||||
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
alert('No hay compañía seleccionada');
|
||||
alert('❌ Error: No hay compañía seleccionada');
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
error = null;
|
||||
|
||||
try {
|
||||
await deleteExchangeRate(item.id, companyId);
|
||||
|
||||
// Éxito
|
||||
alert(`✅ Tipo de cambio del ${new Date(item.date).toLocaleDateString('es-MX')} eliminado correctamente`);
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
} catch (err: any) {
|
||||
error = err.message || 'Error al eliminar el tipo de cambio';
|
||||
alert(`Error: ${error}`);
|
||||
alert(`❌ Error: ${error}`);
|
||||
console.error('Error deleting:', err);
|
||||
} finally {
|
||||
loading = false;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user