Merge branch 'features/Creacion-formularios_catalogos_generales' into development

This commit is contained in:
AlexeerCT
2025-12-24 15:05:09 -06:00
279 changed files with 23252 additions and 3644 deletions

View File

@@ -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

View File

@@ -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",
)

View File

@@ -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;
"""

View File

@@ -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(

View File

@@ -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

View File

@@ -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."
)

View File

@@ -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

View File

@@ -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)}")

View File

@@ -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

View File

@@ -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

View File

@@ -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")

View File

@@ -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")

View File

@@ -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

View File

@@ -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,

View File

@@ -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")

View File

@@ -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

View File

@@ -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"
)

View File

@@ -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)}")

View File

@@ -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

View File

@@ -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)

View File

@@ -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()

View File

@@ -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

View File

@@ -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})>"

View File

@@ -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))

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -21,3 +21,5 @@ class LegendResponse(LegendBase):
id: int
model_config = ConfigDict(from_attributes=True)
tenant_id : int
company_id : int

View File

@@ -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

View File

@@ -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

View File

@@ -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."
)

View File

@@ -27,5 +27,7 @@ class MultiCurrencyTypeUpdate(BaseModel):
class MultiCurrencyTypeResponse(MultiCurrencyTypeBase):
id: int
company_id: int
tenant_id: int
model_config = ConfigDict(from_attributes=True)

View File

@@ -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()

View File

@@ -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))

View File

@@ -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")

View File

@@ -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)

View File

@@ -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

View File

@@ -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):

View File

@@ -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"""

View File

@@ -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

View File

@@ -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):

View File

@@ -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(

View File

@@ -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)

View File

@@ -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)

View File

@@ -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(

View File

@@ -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")

View File

@@ -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):

View 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")

View 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

View 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

View 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

View File

@@ -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"]

View 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"])

View File

@@ -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)

View File

@@ -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})>"

View File

@@ -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,

View File

@@ -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"),
]

View File

@@ -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
View 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
View 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
View 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()

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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": {

View File

@@ -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": {

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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}`);
}

View File

@@ -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}`);
}

View File

@@ -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}`);
}

View File

@@ -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}`);
}

View File

@@ -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}`);
}

View File

@@ -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}`);
}

View File

@@ -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}`);
}

View File

@@ -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}`);
}

View File

@@ -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()}`);
}

View File

@@ -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}`);
}

View File

@@ -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}`);
}

View File

@@ -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}`);
}

View File

@@ -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()}`);
}

View File

@@ -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()}`);
}

View File

@@ -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}`);
}

View File

@@ -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}`);
}

View File

@@ -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}`);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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}`);
}

View 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()}`);
}
}
};

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
});
}
}
];
}

View File

@@ -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>

View File

@@ -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