Merge remote-tracking branch 'origin/development' into feature/reportes_facturas
This commit is contained in:
107
backend/api/v1/modules/a24/fa/fa_classes/dto.py
Normal file
107
backend/api/v1/modules/a24/fa/fa_classes/dto.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""
|
||||
DTOs (Data Transfer Objects) para módulo de clases de activos fijos (FA)
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class FAClassCreateDTO(BaseModel):
|
||||
"""DTO para crear una clase de activo fijo"""
|
||||
|
||||
class_id: int = Field(..., description="ID de la clase base en a76.classes")
|
||||
|
||||
import_tariff_code: Optional[str] = Field(
|
||||
None, max_length=10, description="Código de fracción de importación"
|
||||
)
|
||||
import_tariff_type: Optional[str] = Field(
|
||||
None, max_length=6, description="Tipo de fracción de importación"
|
||||
)
|
||||
export_tariff_code: Optional[str] = Field(
|
||||
None, max_length=10, description="Código de fracción de exportación"
|
||||
)
|
||||
export_tariff_type: Optional[str] = Field(
|
||||
None, max_length=6, description="Tipo de fracción de exportación"
|
||||
)
|
||||
depreciation_rate: Optional[Decimal] = Field(
|
||||
None, description="Tasa de depreciación anual", ge=0, le=100
|
||||
)
|
||||
fda_code: Optional[str] = Field(
|
||||
None, max_length=20, description="Código FDA"
|
||||
)
|
||||
eccn_code: Optional[str] = Field(
|
||||
None, max_length=20, description="Código ECCN (Export Control Classification Number)"
|
||||
)
|
||||
class_enabled: Optional[bool] = Field(
|
||||
True, description="Indica si la clase está habilitada"
|
||||
)
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class FAClassUpdateDTO(BaseModel):
|
||||
"""DTO para actualizar una clase de activo fijo"""
|
||||
|
||||
import_tariff_code: Optional[str] = Field(
|
||||
None, max_length=10, description="Código de fracción de importación"
|
||||
)
|
||||
import_tariff_type: Optional[str] = Field(
|
||||
None, max_length=6, description="Tipo de fracción de importación"
|
||||
)
|
||||
export_tariff_code: Optional[str] = Field(
|
||||
None, max_length=10, description="Código de fracción de exportación"
|
||||
)
|
||||
export_tariff_type: Optional[str] = Field(
|
||||
None, max_length=6, description="Tipo de fracción de exportación"
|
||||
)
|
||||
depreciation_rate: Optional[Decimal] = Field(
|
||||
None, description="Tasa de depreciación anual", ge=0, le=100
|
||||
)
|
||||
fda_code: Optional[str] = Field(
|
||||
None, max_length=20, description="Código FDA"
|
||||
)
|
||||
eccn_code: Optional[str] = Field(
|
||||
None, max_length=20, description="Código ECCN"
|
||||
)
|
||||
class_enabled: Optional[bool] = Field(
|
||||
None, description="Indica si la clase está habilitada"
|
||||
)
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class FAClassResponseDTO(BaseModel):
|
||||
"""DTO para respuesta de clase de activo fijo"""
|
||||
|
||||
id: int
|
||||
tenant_id: int
|
||||
company_id: int
|
||||
class_id: int
|
||||
import_tariff_code: Optional[str] = None
|
||||
import_tariff_type: Optional[str] = None
|
||||
export_tariff_code: Optional[str] = None
|
||||
export_tariff_type: Optional[str] = None
|
||||
depreciation_rate: Optional[Decimal] = None
|
||||
fda_code: Optional[str] = None
|
||||
eccn_code: Optional[str] = None
|
||||
class_enabled: Optional[bool] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class FAClassListResponseDTO(BaseModel):
|
||||
"""DTO para respuesta de lista paginada de clases de activos fijos"""
|
||||
|
||||
items: list[FAClassResponseDTO]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -1,4 +1,5 @@
|
||||
from decimal import Decimal
|
||||
from typing import Optional
|
||||
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
@@ -24,11 +25,11 @@ class QClasses(Base, TenantScopedMixin, TimestampMixin):
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
class_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
import_tariff_code: Mapped[str] = mapped_column(String(10)) # FRACCIONIMPO
|
||||
import_tariff_type: Mapped[str] = mapped_column(String(6)) # TIPOFRACIMPO
|
||||
export_tariff_code: Mapped[str] = mapped_column(String(10)) # FRACCIONEXPO
|
||||
export_tariff_type: Mapped[str] = mapped_column(String(6)) # TIPOFRACEXPO
|
||||
depreciation_rate: Mapped[Decimal] = mapped_column(Numeric(5, 2)) # TASADEPRECIA
|
||||
fda_code: Mapped[str] = mapped_column(String(20)) # FDA
|
||||
eccn_code: Mapped[str] = mapped_column(String(20)) # ECCN
|
||||
class_enabled: Mapped[bool] = mapped_column(Boolean) # HABILITADESHABILITACLASE
|
||||
import_tariff_code: Mapped[Optional[str]] = mapped_column(String(10), nullable=True) # FRACCIONIMPO
|
||||
import_tariff_type: Mapped[Optional[str]] = mapped_column(String(6), nullable=True) # TIPOFRACIMPO
|
||||
export_tariff_code: Mapped[Optional[str]] = mapped_column(String(10), nullable=True) # FRACCIONEXPO
|
||||
export_tariff_type: Mapped[Optional[str]] = mapped_column(String(6), nullable=True) # TIPOFRACEXPO
|
||||
depreciation_rate: Mapped[Optional[Decimal]] = mapped_column(Numeric(5, 2), nullable=True) # TASADEPRECIA
|
||||
fda_code: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) # FDA
|
||||
eccn_code: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) # ECCN
|
||||
class_enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) # HABILITADESHABILITACLASE
|
||||
|
||||
32
backend/api/v1/modules/a24/fa/fa_classes/routes.py
Normal file
32
backend/api/v1/modules/a24/fa/fa_classes/routes.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""
|
||||
Endpoints API para gestión de clases de activos fijos (FA)
|
||||
"""
|
||||
|
||||
from typing import Any, Dict
|
||||
from fastapi import Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user
|
||||
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes, validate_access_to_resource
|
||||
|
||||
from .dto import FAClassCreateDTO, FAClassResponseDTO, FAClassUpdateDTO
|
||||
from .service import FAClassService
|
||||
|
||||
# Create router with generic CRUD routes
|
||||
crud_routes = TenantCRUDRoutes(
|
||||
service=FAClassService,
|
||||
create_schema=FAClassCreateDTO,
|
||||
update_schema=FAClassUpdateDTO,
|
||||
response_schema=FAClassResponseDTO,
|
||||
prefix="/fa/classes",
|
||||
tags=["a24 / fa / classes"],
|
||||
resource_name="Fixed Asset Class",
|
||||
id_name="fa_class_id",
|
||||
enable_list=True,
|
||||
enable_filters=True,
|
||||
default_page_size=50,
|
||||
max_page_size=100,
|
||||
)
|
||||
|
||||
router = crud_routes.router
|
||||
223
backend/api/v1/modules/a24/fa/fa_classes/service.py
Normal file
223
backend/api/v1/modules/a24/fa/fa_classes/service.py
Normal file
@@ -0,0 +1,223 @@
|
||||
"""
|
||||
Capa de servicio para lógica de negocio de clases de activos fijos (FA)
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy import and_
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .dto import FAClassCreateDTO, FAClassResponseDTO, FAClassUpdateDTO
|
||||
from .models import QClasses
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FAClassService:
|
||||
"""Servicio para gestión de clases de activos fijos"""
|
||||
|
||||
@staticmethod
|
||||
def get_all(
|
||||
db: Session,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
) -> tuple[List[QClasses], int]:
|
||||
"""
|
||||
Obtener todas las clases de activos fijos con paginación y filtros
|
||||
"""
|
||||
query = db.query(QClasses).filter(
|
||||
QClasses.tenant_id == tenant_id, QClasses.company_id == company_id
|
||||
)
|
||||
|
||||
if filters:
|
||||
if filters.get("class_id"):
|
||||
query = query.filter(QClasses.class_id == filters["class_id"])
|
||||
if filters.get("fda_code"):
|
||||
query = query.filter(
|
||||
QClasses.fda_code.ilike(f"%{filters['fda_code']}%")
|
||||
)
|
||||
if filters.get("class_enabled") is not None:
|
||||
query = query.filter(
|
||||
QClasses.class_enabled == filters["class_enabled"]
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
items = query.offset(skip).limit(limit).all()
|
||||
|
||||
return items, total
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(
|
||||
db: Session, fa_class_id: int, tenant_id: int, company_id: int
|
||||
) -> Optional[QClasses]:
|
||||
"""Obtener una clase de activo fijo por ID"""
|
||||
return (
|
||||
db.query(QClasses)
|
||||
.filter(
|
||||
QClasses.id == fa_class_id,
|
||||
QClasses.tenant_id == tenant_id,
|
||||
QClasses.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_by_class_id(
|
||||
db: Session, class_id: int, tenant_id: int, company_id: int
|
||||
) -> Optional[QClasses]:
|
||||
"""Obtener una clase de activo fijo por class_id de a76"""
|
||||
return (
|
||||
db.query(QClasses)
|
||||
.filter(
|
||||
QClasses.class_id == class_id,
|
||||
QClasses.tenant_id == tenant_id,
|
||||
QClasses.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
db: Session, fa_class_data: FAClassCreateDTO, tenant_id: int, company_id: int
|
||||
) -> QClasses:
|
||||
"""Crear una nueva clase de activo fijo"""
|
||||
try:
|
||||
# Verificar que la clase base existe en a76.classes
|
||||
from api.v1.modules.a76.classes.models import Class
|
||||
|
||||
base_class = (
|
||||
db.query(Class)
|
||||
.filter(
|
||||
Class.id == fa_class_data.class_id,
|
||||
Class.tenant_id == tenant_id,
|
||||
Class.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not base_class:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Base class with id {fa_class_data.class_id} not found"
|
||||
)
|
||||
|
||||
# Verificar que no exista ya una clase de activo fijo para esta clase base
|
||||
existing = FAClassService.get_by_class_id(
|
||||
db, fa_class_data.class_id, tenant_id, company_id
|
||||
)
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Fixed asset class already exists for class_id {fa_class_data.class_id}"
|
||||
)
|
||||
|
||||
data_dict = fa_class_data.model_dump()
|
||||
|
||||
new_fa_class = QClasses(
|
||||
**data_dict,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
)
|
||||
|
||||
db.add(new_fa_class)
|
||||
db.commit()
|
||||
db.refresh(new_fa_class)
|
||||
|
||||
logger.info(
|
||||
f"Created fixed asset class {new_fa_class.id} for class_id {new_fa_class.class_id}"
|
||||
)
|
||||
|
||||
return new_fa_class
|
||||
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
logger.error(f"IntegrityError creating fixed asset class: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Database constraint violation: {str(e.orig)}"
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error creating fixed asset class: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@staticmethod
|
||||
def update(
|
||||
db: Session,
|
||||
fa_class_id: int,
|
||||
tenant_id: int,
|
||||
fa_class_data: FAClassUpdateDTO,
|
||||
company_id: int,
|
||||
) -> QClasses:
|
||||
"""Actualizar una clase de activo fijo"""
|
||||
fa_class = FAClassService.get_by_id(db, fa_class_id, tenant_id, company_id)
|
||||
|
||||
if not fa_class:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Fixed asset class {fa_class_id} not found"
|
||||
)
|
||||
|
||||
try:
|
||||
update_data = fa_class_data.model_dump(exclude_unset=True)
|
||||
|
||||
for key, value in update_data.items():
|
||||
setattr(fa_class, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(fa_class)
|
||||
|
||||
logger.info(f"Updated fixed asset class {fa_class_id}")
|
||||
|
||||
return fa_class
|
||||
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
logger.error(f"IntegrityError updating fixed asset class: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Database constraint violation: {str(e.orig)}"
|
||||
)
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error updating fixed asset class: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@staticmethod
|
||||
def delete(
|
||||
db: Session, fa_class_id: int, tenant_id: int, company_id: int
|
||||
) -> None:
|
||||
"""Eliminar una clase de activo fijo"""
|
||||
fa_class = FAClassService.get_by_id(db, fa_class_id, tenant_id, company_id)
|
||||
|
||||
if not fa_class:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Fixed asset class {fa_class_id} not found"
|
||||
)
|
||||
|
||||
try:
|
||||
db.delete(fa_class)
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Deleted fixed asset class {fa_class_id}")
|
||||
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
logger.error(f"IntegrityError deleting fixed asset class: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Cannot delete: Fixed asset class is referenced by other records"
|
||||
)
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error deleting fixed asset class: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
48
backend/api/v1/modules/a24/fa/fa_parts/models.py
Normal file
48
backend/api/v1/modules/a24/fa/fa_parts/models.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
Modelo ORM para datos específicos de Activos Fijos (Q-Partes) - Anexo 24
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
from sqlalchemy import (
|
||||
Integer,
|
||||
PrimaryKeyConstraint,
|
||||
String,
|
||||
ForeignKeyConstraint
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
from api.v1.modules.a76.parts.models import Part
|
||||
|
||||
|
||||
class FaPart(Base, TenantScopedMixin, TimestampMixin):
|
||||
"""
|
||||
Tabla fa_partes: Extensión de Anexo 24 para Activos Fijos.
|
||||
"""
|
||||
|
||||
__tablename__ = "fa_partes"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id", name="fa_partes_pkey"),
|
||||
ForeignKeyConstraint(
|
||||
["id"], ["a76.parts.id"], name="fk_fa_partes_master"
|
||||
),
|
||||
{"schema": "a24"},
|
||||
)
|
||||
|
||||
# El ID hereda el valor de la tabla parts
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=False)
|
||||
|
||||
# --- CAMPOS ESPECÍFICOS FISCALES (Q-PARTES) ---
|
||||
origin_country: Mapped[Optional[str]] = mapped_column(String(3)) # PAIS
|
||||
sector: Mapped[Optional[str]] = mapped_column(String(8)) # SECTOR
|
||||
fraction_type: Mapped[Optional[str]] = mapped_column(String(7)) # TIPOFRACCION
|
||||
|
||||
# --- RELACIÓN ---
|
||||
# Usamos string "Part" para evitar que truene al inicializar los mappers
|
||||
master_info: Mapped["Part"] = relationship("Part", back_populates="fa_data")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<FaPart(id={self.id}, sector='{self.sector}')>"
|
||||
@@ -8,7 +8,7 @@ class SClasses(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "inv_classes" # SClases
|
||||
__table_args__ = (
|
||||
ForeignKeyConstraint(
|
||||
["class_id"], ["a76.clases.class_id"], name="fk_sclasses_classes"
|
||||
["class_id"], ["a76.classes.id"], name="fk_sclasses_classes"
|
||||
),
|
||||
PrimaryKeyConstraint("id", name="sclases_pk"),
|
||||
{"schema": "a24"},
|
||||
|
||||
108
backend/api/v1/modules/a24/inv/inv_parts/models.py
Normal file
108
backend/api/v1/modules/a24/inv/inv_parts/models.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
Modelo ORM para datos específicos de Inventario y Manufactura (S-Partes) - Anexo 24
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
from decimal import Decimal
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
from sqlalchemy import (
|
||||
Integer,
|
||||
Numeric,
|
||||
PrimaryKeyConstraint,
|
||||
String,
|
||||
Boolean,
|
||||
ForeignKeyConstraint
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
from api.v1.modules.a76.parts.models import Part
|
||||
|
||||
|
||||
class InvPart(Base, TenantScopedMixin, TimestampMixin):
|
||||
"""
|
||||
Tabla inv_partes: Extensión de Anexo 24 para Inventarios (SPartes).
|
||||
"""
|
||||
|
||||
__tablename__ = "inv_partes"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id", name="inv_partes_pkey"),
|
||||
ForeignKeyConstraint(
|
||||
["id"], ["a76.parts.id"], name="fk_inv_partes_master"
|
||||
),
|
||||
{"schema": "a24"},
|
||||
)
|
||||
|
||||
# Relación 1:1 - El ID es el mismo de la tabla maestra
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=False)
|
||||
|
||||
# --- 1. ATRIBUTOS PRINCIPALES DE INVENTARIO ---
|
||||
part_type: Mapped[Optional[str]] = mapped_column(String(10)) # TIPOPARTE
|
||||
material_type: Mapped[Optional[str]] = mapped_column(String(10)) # TIPOMAT
|
||||
|
||||
reference_number: Mapped[Optional[str]] = mapped_column(String(70)) # NUMPARTEREF
|
||||
flex_reference_number: Mapped[Optional[str]] = mapped_column(String(120)) # NUMPARTEREFFLEX
|
||||
|
||||
# Conversiones
|
||||
equivalent_uom: Mapped[Optional[str]] = mapped_column(String(5)) # UNIMEDEQUIV
|
||||
conversion_factor: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8)) # FACTORCONV
|
||||
|
||||
stock_uom: Mapped[Optional[str]] = mapped_column(String(5)) # UMEXISTENCIA
|
||||
alternate_uom: Mapped[Optional[str]] = mapped_column(String(5)) # UNIMEDALTERNA
|
||||
conversion_uom: Mapped[Optional[str]] = mapped_column(String(9)) # UMCONVERSION
|
||||
|
||||
# Valor Agregado
|
||||
added_value: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORAGREGADO
|
||||
added_value_type: Mapped[Optional[str]] = mapped_column(String(2)) # TIPOVA
|
||||
|
||||
assigned_client: Mapped[Optional[str]] = mapped_column(String(50)) # CLIENTEASIGNADO
|
||||
supplier_code: Mapped[Optional[str]] = mapped_column(String(8)) # PROVEEDOR
|
||||
is_textile: Mapped[Optional[str]] = mapped_column(String(2)) # ESTEXTIL
|
||||
|
||||
# --- 2. MANUFACTURA Y PELIGROSIDAD ---
|
||||
bom_version: Mapped[Optional[int]] = mapped_column(Integer) # VERSIONBOM / VERSIONBILL
|
||||
is_repair: Mapped[Optional[str]] = mapped_column(String(3)) # ESREPARACION
|
||||
is_hazardous: Mapped[Optional[str]] = mapped_column(String(1)) # ESMATPELIGROSO
|
||||
|
||||
emergency_number: Mapped[Optional[str]] = mapped_column(String(30)) # NUMEMERGENCIA
|
||||
danger_class: Mapped[Optional[str]] = mapped_column(String(4)) # CLASEDEPELIGRO
|
||||
packaging_group: Mapped[Optional[str]] = mapped_column(String(3)) # GRUPOEMBALAJE
|
||||
|
||||
# Dimensiones
|
||||
width: Mapped[Optional[str]] = mapped_column(String(50)) # ANCHURA
|
||||
thickness: Mapped[Optional[str]] = mapped_column(String(50)) # ESPESOR
|
||||
specification: Mapped[Optional[str]] = mapped_column(String(50)) # SPEC
|
||||
|
||||
# --- 3. COSTOS DETALLADOS Y ADUANA US ---
|
||||
total_value: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORTOTAL
|
||||
direct_labor: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # TRABAJODIREC
|
||||
general_expenses: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # GASTOGRALES
|
||||
total_expenses: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # TOTALGASTOS
|
||||
|
||||
depreciation: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # DEPRECIACION
|
||||
tooling: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # TOOLING
|
||||
material_consumed: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # MATCONSUMED
|
||||
profit: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # GANANCIA
|
||||
|
||||
# Fracciones Internacionales
|
||||
us_fraction_alt: Mapped[Optional[str]] = mapped_column(String(13)) # FRACEUA
|
||||
ca_fraction: Mapped[Optional[str]] = mapped_column(String(13)) # FRACCANADA
|
||||
ad_valorem_us: Mapped[Optional[Decimal]] = mapped_column(Numeric(5, 2)) # ADVALOREMAME
|
||||
|
||||
# Nafta / USMCA
|
||||
nafta_result: Mapped[Optional[str]] = mapped_column(String(19)) # RESULTADOCALCULONAFTA
|
||||
nafta_percentage: Mapped[Optional[Decimal]] = mapped_column(Numeric(5, 2)) # PORCENTAJECALCULONAFTA
|
||||
|
||||
# Impuestos Específicos (Derechos de Trámite Admon)
|
||||
dta: Mapped[Optional[str]] = mapped_column(String(19)) # DTA
|
||||
dtb: Mapped[Optional[str]] = mapped_column(String(19)) # DTB
|
||||
dtg: Mapped[Optional[str]] = mapped_column(String(19)) # DTG
|
||||
|
||||
# --- RELACIÓN ---
|
||||
# Usamos string "Part" para evitar problemas de carga
|
||||
master_info: Mapped["Part"] = relationship("Part", back_populates="inv_data")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<InvPart(id={self.id}, part_type='{self.part_type}')>"
|
||||
14
backend/api/v1/modules/a24/router.py
Normal file
14
backend/api/v1/modules/a24/router.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
Router principal del módulo A24 (SCAF - Sistema de Control de Activo Fijo)
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter
|
||||
|
||||
# Importar routers de submódulos
|
||||
from .fa.fa_classes.routes import router as fa_classes_router
|
||||
|
||||
# Router principal de A24
|
||||
router = APIRouter()
|
||||
|
||||
# Registrar routers de FA (Fixed Assets)
|
||||
router.include_router(fa_classes_router, prefix="/a24", tags=["a24 / fa / classes"])
|
||||
@@ -4,6 +4,7 @@ Reemplaza schemas.py siguiendo enfoque DDD y estilo NestJS
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
@@ -12,24 +13,23 @@ from pydantic import BaseModel, ConfigDict, Field
|
||||
class ClassCreateDTO(BaseModel):
|
||||
"""DTO para crear una clase"""
|
||||
|
||||
client_id: int = Field(..., description="Client key")
|
||||
class_code: str = Field(..., max_length=8, description="Class code")
|
||||
description_es: Optional[str] = Field(
|
||||
None, max_length=500, description="Description in Spanish"
|
||||
description_es: str = Field(
|
||||
..., max_length=500, description="Description in Spanish (required)"
|
||||
)
|
||||
description_en: Optional[str] = Field(
|
||||
None, max_length=500, description="Description in English"
|
||||
)
|
||||
material_key: Optional[str] = Field(
|
||||
None,
|
||||
material_key: str = Field(
|
||||
...,
|
||||
max_length=10,
|
||||
description="Material key (homologated TIPOMAT/TIPOMATEQUIPO)",
|
||||
description="Material key - Fixed Asset Type (required)",
|
||||
)
|
||||
unit_of_measure: Optional[str] = Field(
|
||||
None, max_length=5, description="Unit of measure (homologated UNIMEDIDA)"
|
||||
unit_of_measure: str = Field(
|
||||
..., max_length=5, description="Unit of measure - U.M. comercial (required)"
|
||||
)
|
||||
fraction: Optional[str] = Field(
|
||||
None, max_length=10, description="Mexican tariff fraction"
|
||||
fraction: str = Field(
|
||||
..., max_length=20, description="Mexican tariff fraction (required)"
|
||||
)
|
||||
us_fraction: Optional[str] = Field(
|
||||
None, max_length=16, description="US tariff fraction"
|
||||
@@ -48,9 +48,45 @@ class ClassCreateDTO(BaseModel):
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class ClassCreateDTOFA(ClassCreateDTO):
|
||||
"""DTO para crear una clase de activo fijo (clase base + extensión FA)"""
|
||||
|
||||
# Campos específicos de activos fijos (a24.fa_classes)
|
||||
import_tariff_code: Optional[str] = Field(
|
||||
None, max_length=10, description="Código de fracción de importación"
|
||||
)
|
||||
import_tariff_type: Optional[str] = Field(
|
||||
None, max_length=6, description="Tipo de fracción de importación"
|
||||
)
|
||||
export_tariff_code: Optional[str] = Field(
|
||||
None, max_length=10, description="Código de fracción de exportación"
|
||||
)
|
||||
export_tariff_type: Optional[str] = Field(
|
||||
None, max_length=6, description="Tipo de fracción de exportación"
|
||||
)
|
||||
depreciation_rate: Optional[Decimal] = Field(
|
||||
None, ge=0, le=100, description="Tasa de depreciación anual (%)"
|
||||
)
|
||||
fda_code: Optional[str] = Field(
|
||||
None, max_length=20, description="Código FDA"
|
||||
)
|
||||
eccn_code: Optional[str] = Field(
|
||||
None, max_length=20, description="Código ECCN"
|
||||
)
|
||||
class_enabled: Optional[bool] = Field(
|
||||
True, description="Indica si la clase está habilitada"
|
||||
)
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class ClassUpdateDTO(BaseModel):
|
||||
"""DTO para actualizar una clase"""
|
||||
|
||||
class_code: Optional[str] = Field(
|
||||
None, max_length=8, description="Class code"
|
||||
)
|
||||
description_es: Optional[str] = Field(
|
||||
None, max_length=500, description="Description in Spanish"
|
||||
)
|
||||
@@ -66,7 +102,7 @@ class ClassUpdateDTO(BaseModel):
|
||||
None, max_length=5, description="Unit of measure (homologated UNIMEDIDA)"
|
||||
)
|
||||
fraction: Optional[str] = Field(
|
||||
None, max_length=10, description="Mexican tariff fraction"
|
||||
None, max_length=20, description="Mexican tariff fraction"
|
||||
)
|
||||
us_fraction: Optional[str] = Field(
|
||||
None, max_length=16, description="US tariff fraction"
|
||||
@@ -81,8 +117,7 @@ class ClassUpdateDTO(BaseModel):
|
||||
None, max_length=4, description="IVA exempt fraction"
|
||||
)
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
model_config = ConfigDict(from_attributes=True, extra='forbid') # Explicitly forbid extra fields
|
||||
|
||||
|
||||
class ClassResponseDTO(BaseModel):
|
||||
@@ -91,7 +126,6 @@ class ClassResponseDTO(BaseModel):
|
||||
id: int
|
||||
tenant_id: int
|
||||
company_id: int
|
||||
client_id: int
|
||||
class_code: str
|
||||
description_es: Optional[str] = None
|
||||
description_en: Optional[str] = None
|
||||
@@ -108,10 +142,26 @@ class ClassResponseDTO(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class ClassResponseDTOFA(ClassResponseDTO):
|
||||
"""DTO para respuesta de clase de activo fijo (incluye campos FA)"""
|
||||
|
||||
# Campos de a24.fa_classes
|
||||
fa_id: Optional[int] = None
|
||||
import_tariff_code: Optional[str] = None
|
||||
import_tariff_type: Optional[str] = None
|
||||
export_tariff_code: Optional[str] = None
|
||||
export_tariff_type: Optional[str] = None
|
||||
depreciation_rate: Optional[Decimal] = None
|
||||
fda_code: Optional[str] = None
|
||||
eccn_code: Optional[str] = None
|
||||
class_enabled: Optional[bool] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class ClassBasicDTO(BaseModel):
|
||||
"""DTO para información básica de clase"""
|
||||
|
||||
client_id: int
|
||||
class_code: str
|
||||
description_es: Optional[str] = None
|
||||
description_en: Optional[str] = None
|
||||
@@ -137,7 +187,6 @@ class ClassListDTO(BaseModel):
|
||||
class ClassSearchDTO(BaseModel):
|
||||
"""DTO para búsqueda de clases"""
|
||||
|
||||
client_id: Optional[int] = Field(None, description="Filter by client key")
|
||||
class_code: Optional[str] = Field(None, description="Search by class code")
|
||||
description: Optional[str] = Field(None, description="Search in descriptions")
|
||||
material_key: Optional[str] = Field(None, description="Filter by material key")
|
||||
@@ -147,4 +196,4 @@ class ClassSearchDTO(BaseModel):
|
||||
)
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
from_attributes = True
|
||||
@@ -31,9 +31,6 @@ class Class(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "classes"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id", name="classes_pkey"),
|
||||
ForeignKeyConstraint(
|
||||
["client_id"], ["a76.clients_and_providers.id"], name="fk_classes_client"
|
||||
),
|
||||
ForeignKeyConstraint(
|
||||
["material_key"],
|
||||
["public.material_types.key"],
|
||||
@@ -47,15 +44,13 @@ class Class(Base, TenantScopedMixin, TimestampMixin):
|
||||
UniqueConstraint(
|
||||
"tenant_id",
|
||||
"company_id",
|
||||
"client_id",
|
||||
"class_code",
|
||||
name="ufa_classes_client_id_class_code",
|
||||
name="uq_classes_tenant_company_code",
|
||||
),
|
||||
{"schema": "a76"},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
client_id: Mapped[int] = mapped_column(Integer)
|
||||
|
||||
# Unique constraint compuesta
|
||||
class_code: Mapped[str] = mapped_column(String(8)) # CLASE
|
||||
@@ -75,7 +70,7 @@ class Class(Base, TenantScopedMixin, TimestampMixin):
|
||||
) # UNIMED - homologated from UNIMEDIDA
|
||||
|
||||
# Tariff fractions
|
||||
fraction: Mapped[Optional[str]] = mapped_column(String(10)) # FRACCION
|
||||
fraction: Mapped[Optional[str]] = mapped_column(String(20)) # FRACCION
|
||||
us_fraction: Mapped[Optional[str]] = mapped_column(
|
||||
String(16)
|
||||
) # FRACCIONAME - US tariff fraction
|
||||
@@ -98,11 +93,11 @@ class Class(Base, TenantScopedMixin, TimestampMixin):
|
||||
|
||||
# Inverse relationship with GParts that have this class
|
||||
parts: Mapped[list["Part"]] = relationship(
|
||||
primaryjoin="and_(Class.client_id == Part.client_id, Class.class_code == Part.part_class)",
|
||||
foreign_keys="[Part.client_id, Part.part_class]",
|
||||
primaryjoin="and_(Class.class_code == Part.part_class)",
|
||||
foreign_keys="[Part.part_class]",
|
||||
viewonly=True,
|
||||
back_populates="part_class_info",
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Class(client_id={self.client_id}, class_code='{self.class_code}', description='{self.description_es}')>"
|
||||
return f"<Class(class_code='{self.class_code}', description='{self.description_es}')>"
|
||||
|
||||
@@ -2,13 +2,19 @@
|
||||
Endpoints API para gestión de clases SCAII y SCAF
|
||||
"""
|
||||
|
||||
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
|
||||
from typing import Dict, Any
|
||||
from fastapi import Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .dto import ClassCreateDTO, ClassResponseDTO, ClassUpdateDTO
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user
|
||||
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes, validate_access_to_resource
|
||||
|
||||
from .dto import ClassCreateDTO, ClassCreateDTOFA, ClassResponseDTO, ClassResponseDTOFA, ClassUpdateDTO
|
||||
from .service import ClassService
|
||||
|
||||
# Create router with generic CRUD routes
|
||||
router = TenantCRUDRoutes(
|
||||
crud_routes = TenantCRUDRoutes(
|
||||
service=ClassService,
|
||||
create_schema=ClassCreateDTO,
|
||||
update_schema=ClassUpdateDTO,
|
||||
@@ -16,9 +22,35 @@ router = TenantCRUDRoutes(
|
||||
prefix="/classes",
|
||||
tags=["a76 / classes"],
|
||||
resource_name="Class",
|
||||
id_name="class_id",
|
||||
id_name="id",
|
||||
enable_list=True,
|
||||
enable_filters=True,
|
||||
default_page_size=50,
|
||||
max_page_size=100,
|
||||
).router
|
||||
max_page_size=1000,
|
||||
)
|
||||
|
||||
router = crud_routes.router
|
||||
|
||||
@router.post(
|
||||
"/fa",
|
||||
response_model=ClassResponseDTOFA,
|
||||
status_code=201,
|
||||
summary="Create Fixed Asset Class",
|
||||
description="Create a class with FA extension in a single transaction",
|
||||
)
|
||||
async def create_fa_class(
|
||||
class_data: ClassCreateDTOFA,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
"""Create a fixed asset class (both base class and FA extension)"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"create_fa_class endpoint called with: {class_data.model_dump()}")
|
||||
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
result = ClassService.create_fa_class(db, class_data, tenant_id, company_id)
|
||||
|
||||
return result
|
||||
@@ -13,8 +13,10 @@ from sqlalchemy.orm import Session
|
||||
from .dto import (
|
||||
ClassBasicDTO,
|
||||
ClassCreateDTO,
|
||||
ClassCreateDTOFA,
|
||||
ClassListDTO,
|
||||
ClassResponseDTO,
|
||||
ClassResponseDTOFA,
|
||||
ClassSearchDTO,
|
||||
ClassUpdateDTO,
|
||||
)
|
||||
@@ -38,13 +40,12 @@ class ClassService:
|
||||
"""
|
||||
Get all classes for a tenant with pagination and filters
|
||||
"""
|
||||
logger.info(f"get_all called with tenant_id={tenant_id}, company_id={company_id}, skip={skip}, limit={limit}")
|
||||
query = db.query(Class).filter(
|
||||
Class.tenant_id == tenant_id, Class.company_id == company_id
|
||||
)
|
||||
|
||||
if filters:
|
||||
if filters.get("client_id"):
|
||||
query = query.filter(Class.client_id == filters["client_id"])
|
||||
if filters.get("class_code"):
|
||||
query = query.filter(
|
||||
Class.class_code.ilike(f"%{filters['class_code']}%")
|
||||
@@ -70,7 +71,8 @@ class ClassService:
|
||||
|
||||
total = query.count()
|
||||
items = query.offset(skip).limit(limit).all()
|
||||
|
||||
|
||||
logger.info(f"get_all returning {len(items)} items out of {total} total")
|
||||
return items, total
|
||||
|
||||
@staticmethod
|
||||
@@ -102,25 +104,25 @@ class ClassService:
|
||||
existing = db.query(Class).filter(
|
||||
Class.tenant_id == tenant_id,
|
||||
Class.company_id == company_id,
|
||||
Class.client_id == data_dict["client_id"],
|
||||
Class.class_code == data_dict["class_code"]
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Class with code '{data_dict['class_code']}' already exists for this tenant and company"
|
||||
detail=f" El código de clase '{data_dict['class_code']}' ya existe. Por favor use un código diferente."
|
||||
)
|
||||
|
||||
# Validate material_key exists if provided
|
||||
if data_dict.get("material_key"):
|
||||
from api.v1.modules.public.reference_data.material_types.models import MaterialType
|
||||
material_exists = db.query(MaterialType).filter(
|
||||
MaterialType.key == data_dict["material_key"]
|
||||
).first()
|
||||
if not material_exists:
|
||||
# Set to None if material_key doesn't exist
|
||||
data_dict["material_key"] = None
|
||||
# Validate material_key exists (now required)
|
||||
from api.v1.modules.public.reference_data.material_types.models import MaterialType
|
||||
material_exists = db.query(MaterialType).filter(
|
||||
MaterialType.key == data_dict["material_key"]
|
||||
).first()
|
||||
if not material_exists:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Material type '{data_dict['material_key']}' does not exist"
|
||||
)
|
||||
|
||||
class_obj = Class(**data_dict)
|
||||
class_obj.tenant_id = tenant_id
|
||||
@@ -147,11 +149,16 @@ class ClassService:
|
||||
company_id: int,
|
||||
) -> Optional[Class]:
|
||||
"""Update a class"""
|
||||
logger.info(f"Update called for class_id={class_id}, tenant_id={tenant_id}, company_id={company_id}")
|
||||
logger.info(f"Update data received: {class_data.model_dump(exclude_unset=True)}")
|
||||
|
||||
class_obj = ClassService.get_by_id(db, class_id, tenant_id, company_id)
|
||||
if not class_obj:
|
||||
logger.warning(f"Class {class_id} not found for tenant {tenant_id}, company {company_id}")
|
||||
return None
|
||||
|
||||
update_data = class_data.model_dump(exclude_unset=True)
|
||||
logger.info(f"Update data after model_dump: {update_data}")
|
||||
|
||||
# Validate material_key exists if provided
|
||||
if "material_key" in update_data and update_data["material_key"]:
|
||||
@@ -163,24 +170,183 @@ class ClassService:
|
||||
# Set to None if material_key doesn't exist
|
||||
update_data["material_key"] = None
|
||||
|
||||
# Validate class_code is unique if being changed
|
||||
if "class_code" in update_data and update_data["class_code"]:
|
||||
new_code = update_data["class_code"]
|
||||
# Check if another class with this code exists (excluding current class)
|
||||
# The unique constraint is on (tenant_id, company_id, class_code)
|
||||
existing_class = db.query(Class).filter(
|
||||
Class.class_code == new_code,
|
||||
Class.tenant_id == tenant_id,
|
||||
Class.company_id == company_id,
|
||||
Class.id != class_id # Exclude current class
|
||||
).first()
|
||||
|
||||
logger.info(f"Checking for duplicate class_code '{new_code}'")
|
||||
if existing_class:
|
||||
logger.warning(f"Duplicate class_code found: {existing_class.id}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"El código '{new_code}' ya está en uso para este cliente. Por favor ingrese un código diferente."
|
||||
)
|
||||
|
||||
for field, value in update_data.items():
|
||||
setattr(class_obj, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(class_obj)
|
||||
return class_obj
|
||||
try:
|
||||
logger.info(f"Attempting to commit changes for class {class_id}")
|
||||
db.commit()
|
||||
db.refresh(class_obj)
|
||||
logger.info(f"Successfully updated class {class_id}")
|
||||
return class_obj
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
error_msg = str(e.orig)
|
||||
logger.error(f"IntegrityError updating class {class_id}: {error_msg}")
|
||||
|
||||
# Check if it's a duplicate class_code error
|
||||
if "already exists" in error_msg.lower() or "duplicate" in error_msg.lower():
|
||||
# Extract the code from update_data if it was changed
|
||||
code = update_data.get("class_code", class_obj.class_code)
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"El código '{code}' ya está en uso. Por favor ingrese un código diferente."
|
||||
)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Error al actualizar la clase: {error_msg}"
|
||||
)
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Unexpected error updating class {class_id}: {type(e).__name__}: {str(e)}")
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def delete(db: Session, class_id: int, tenant_id: int, company_id: int) -> bool:
|
||||
"""Delete a class"""
|
||||
"""Delete a class (and its FA extension if exists)"""
|
||||
from api.v1.modules.a24.fa.fa_classes.models import QClasses
|
||||
|
||||
class_obj = ClassService.get_by_id(db, class_id, tenant_id, company_id)
|
||||
if not class_obj:
|
||||
return False
|
||||
|
||||
# Delete FA extension first (if exists) to avoid FK constraint violation
|
||||
fa_extension = db.query(QClasses).filter(
|
||||
QClasses.class_id == class_id,
|
||||
QClasses.tenant_id == tenant_id
|
||||
).first()
|
||||
|
||||
if fa_extension:
|
||||
db.delete(fa_extension)
|
||||
|
||||
# Now delete the base class
|
||||
db.delete(class_obj)
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def create_fa_class(
|
||||
db: Session, class_data: ClassCreateDTOFA, tenant_id: int, company_id: int
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a fixed asset class (both a76.classes and a24.fa_classes)
|
||||
Returns a dict with both records combined
|
||||
"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"create_fa_class called with data: {class_data.model_dump()}")
|
||||
|
||||
from api.v1.modules.a24.fa.fa_classes.models import QClasses
|
||||
|
||||
# Extract base class fields
|
||||
base_fields = {
|
||||
"class_code", "description_es", "description_en",
|
||||
"material_key", "unit_of_measure", "fraction", "us_fraction",
|
||||
"sub_key", "physical_review", "iva_exempt_fraction"
|
||||
}
|
||||
base_data = {k: v for k, v in class_data.model_dump().items() if k in base_fields}
|
||||
|
||||
# Extract FA-specific fields
|
||||
fa_fields = {
|
||||
"import_tariff_code", "import_tariff_type", "export_tariff_code",
|
||||
"export_tariff_type", "depreciation_rate", "fda_code", "eccn_code",
|
||||
"class_enabled"
|
||||
}
|
||||
fa_data = {k: v for k, v in class_data.model_dump().items() if k in fa_fields}
|
||||
|
||||
try:
|
||||
# 1. Create base class
|
||||
base_dto = ClassCreateDTO(**base_data)
|
||||
base_class = ClassService.create(db, base_dto, tenant_id, company_id)
|
||||
|
||||
# 2. Create FA extension
|
||||
fa_obj = QClasses(**fa_data)
|
||||
fa_obj.class_id = base_class.id
|
||||
fa_obj.tenant_id = tenant_id
|
||||
fa_obj.company_id = company_id
|
||||
|
||||
db.add(fa_obj)
|
||||
db.commit()
|
||||
db.refresh(fa_obj)
|
||||
|
||||
# 3. Combine response - build dict manually to avoid SQLAlchemy internals
|
||||
combined_response = {
|
||||
# Base class fields
|
||||
"id": base_class.id,
|
||||
"tenant_id": base_class.tenant_id,
|
||||
"company_id": base_class.company_id,
|
||||
"class_code": base_class.class_code,
|
||||
"description_es": base_class.description_es,
|
||||
"description_en": base_class.description_en,
|
||||
"material_key": base_class.material_key,
|
||||
"unit_of_measure": base_class.unit_of_measure,
|
||||
"fraction": base_class.fraction,
|
||||
"us_fraction": base_class.us_fraction,
|
||||
"sub_key": base_class.sub_key,
|
||||
"physical_review": base_class.physical_review,
|
||||
"iva_exempt_fraction": base_class.iva_exempt_fraction,
|
||||
"created_at": base_class.created_at,
|
||||
"updated_at": base_class.updated_at,
|
||||
# FA extension fields
|
||||
"fa_id": fa_obj.id,
|
||||
"import_tariff_code": fa_obj.import_tariff_code,
|
||||
"import_tariff_type": fa_obj.import_tariff_type,
|
||||
"export_tariff_code": fa_obj.export_tariff_code,
|
||||
"export_tariff_type": fa_obj.export_tariff_type,
|
||||
"depreciation_rate": fa_obj.depreciation_rate,
|
||||
"fda_code": fa_obj.fda_code,
|
||||
"eccn_code": fa_obj.eccn_code,
|
||||
"class_enabled": fa_obj.class_enabled,
|
||||
}
|
||||
|
||||
return combined_response
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
# If FA creation fails, rollback base class too
|
||||
if 'base_class' in locals():
|
||||
try:
|
||||
db.delete(base_class)
|
||||
db.commit()
|
||||
except:
|
||||
pass
|
||||
|
||||
# Extract and improve error message
|
||||
error_msg = str(e)
|
||||
if "already exists" in error_msg.lower() or "duplicad" in error_msg.lower():
|
||||
# Extract code from error if possible
|
||||
code = class_data.class_code
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"El código '{code}' ya está en uso. Por favor ingrese un código diferente."
|
||||
)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Error al crear clase de activo fijo: {error_msg}"
|
||||
)
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
@@ -203,7 +369,6 @@ class ClassService:
|
||||
self.db.query(Class)
|
||||
.filter(
|
||||
and_(
|
||||
Class.client_id == class_data.client_id,
|
||||
Class.class_code == class_data.class_code,
|
||||
)
|
||||
)
|
||||
@@ -213,12 +378,11 @@ class ClassService:
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Class with client_id '{class_data.client_id}' and class_code '{class_data.class_code}' already exists",
|
||||
detail=f"Class with class_code '{class_data.class_code}' already exists",
|
||||
)
|
||||
|
||||
# Crear clase
|
||||
db_class = Class(
|
||||
client_id=class_data.client_id,
|
||||
class_code=class_data.class_code,
|
||||
description_spanish=class_data.description_spanish,
|
||||
description_english=class_data.description_english,
|
||||
@@ -242,7 +406,7 @@ class ClassService:
|
||||
logger.error(f"IntegrityError creating class: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Class with this client_id and class_code already exists",
|
||||
detail="Class with this class_code already exists",
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
@@ -251,12 +415,11 @@ class ClassService:
|
||||
logger.error(f"Error creating class: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Error creating class")
|
||||
|
||||
def get_class(self, client_id: int, class_code: str) -> Optional[ClassResponseDTO]:
|
||||
def get_class(self, class_code: str) -> Optional[ClassResponseDTO]:
|
||||
"""
|
||||
Obtiene una clase por clave compuesta
|
||||
|
||||
Args:
|
||||
client_id: Clave del cliente
|
||||
class_code: Código de clase
|
||||
|
||||
Returns:
|
||||
@@ -264,7 +427,7 @@ class ClassService:
|
||||
"""
|
||||
class_obj = (
|
||||
self.db.query(Class)
|
||||
.filter(and_(Class.client_id == client_id, Class.class_code == class_code))
|
||||
.filter(and_(Class.class_code == class_code))
|
||||
.first()
|
||||
)
|
||||
|
||||
@@ -293,9 +456,6 @@ class ClassService:
|
||||
|
||||
# Aplicar filtros si se proporcionan
|
||||
if search_params:
|
||||
if search_params.client_id:
|
||||
query = query.filter(Class.client_id == search_params.client_id)
|
||||
|
||||
if search_params.class_code:
|
||||
query = query.filter(
|
||||
Class.class_code.ilike(f"%{search_params.class_code}%")
|
||||
@@ -342,13 +502,12 @@ class ClassService:
|
||||
)
|
||||
|
||||
def update_class(
|
||||
self, client_id: int, class_code: str, class_data: ClassUpdateDTO
|
||||
self, class_code: str, class_data: ClassUpdateDTO
|
||||
) -> Optional[ClassResponseDTO]:
|
||||
"""
|
||||
Actualiza una clase
|
||||
|
||||
Args:
|
||||
client_id: Clave del cliente
|
||||
class_code: Código de clase
|
||||
class_data: Datos a actualizar
|
||||
|
||||
@@ -357,7 +516,7 @@ class ClassService:
|
||||
"""
|
||||
class_obj = (
|
||||
self.db.query(Class)
|
||||
.filter(and_(Class.client_id == client_id, Class.class_code == class_code))
|
||||
.filter(and_(Class.class_code == class_code))
|
||||
.first()
|
||||
)
|
||||
|
||||
@@ -377,15 +536,14 @@ class ClassService:
|
||||
|
||||
except Exception as e:
|
||||
self.db.rollback()
|
||||
logger.error(f"Error updating class {client_id}-{class_code}: {str(e)}")
|
||||
logger.error(f"Error updating class {class_code}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Error updating class")
|
||||
|
||||
def delete_class(self, client_id: int, class_code: str) -> bool:
|
||||
def delete_class(self, class_code: str) -> bool:
|
||||
"""
|
||||
Elimina una clase
|
||||
|
||||
Args:
|
||||
client_id: Clave del cliente
|
||||
class_code: Código de clase
|
||||
|
||||
Returns:
|
||||
@@ -393,7 +551,7 @@ class ClassService:
|
||||
"""
|
||||
class_obj = (
|
||||
self.db.query(Class)
|
||||
.filter(and_(Class.client_id == client_id, Class.class_code == class_code))
|
||||
.filter(and_(Class.class_code == class_code))
|
||||
.first()
|
||||
)
|
||||
|
||||
@@ -406,7 +564,7 @@ class ClassService:
|
||||
return True
|
||||
except Exception as e:
|
||||
self.db.rollback()
|
||||
logger.error(f"Error deleting class {client_id}-{class_code}: {str(e)}")
|
||||
logger.error(f"Error deleting class {class_code}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail="Error deleting class")
|
||||
|
||||
def search_by_fraction(self, fraction: str) -> List[ClassBasicDTO]:
|
||||
@@ -416,19 +574,6 @@ class ClassService:
|
||||
)
|
||||
return [ClassBasicDTO.model_validate(class_obj) for class_obj in classes]
|
||||
|
||||
def search_by_client(
|
||||
self, client_id: int, skip: int = 0, limit: int = 100
|
||||
) -> List[ClassBasicDTO]:
|
||||
"""Obtiene todas las clases de un cliente específico"""
|
||||
classes = (
|
||||
self.db.query(Class)
|
||||
.filter(Class.client_id == client_id)
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
return [ClassBasicDTO.model_validate(class_obj) for class_obj in classes]
|
||||
|
||||
def search_by_material(self, material_key: str) -> List[ClassBasicDTO]:
|
||||
"""Busca clases por clave de material"""
|
||||
classes = (
|
||||
@@ -451,9 +596,6 @@ class ClassService:
|
||||
"""Obtiene estadísticas básicas de clases"""
|
||||
total_classes = self.db.query(Class).count()
|
||||
|
||||
# Contar por clientes
|
||||
clients_count = self.db.query(Class.client_id).distinct().count()
|
||||
|
||||
# Contar por revisión física
|
||||
physical_review_stats = {}
|
||||
for i in range(3): # Asumiendo valores 0, 1, 2
|
||||
@@ -468,7 +610,6 @@ class ClassService:
|
||||
|
||||
return {
|
||||
"total_classes": total_classes,
|
||||
"clients_with_classes": clients_count,
|
||||
"classes_with_fraction": with_fraction,
|
||||
"classes_with_us_fraction": with_us_fraction,
|
||||
**physical_review_stats,
|
||||
|
||||
@@ -116,9 +116,7 @@ class ClientProviderCreateDTO(BaseModel):
|
||||
)
|
||||
position: Optional[str] = Field(None, max_length=30, description="Position")
|
||||
incoterm: Optional[str] = Field(None, max_length=19, description="Incoterm")
|
||||
is_national_provider: Optional[str] = Field(
|
||||
None, max_length=2, description="Is national provider"
|
||||
)
|
||||
is_national_provider: Optional[bool] = None
|
||||
is_active: Optional[bool] = Field(None, description="Enabled/Disabled status")
|
||||
|
||||
# Nested DTOs
|
||||
@@ -159,9 +157,7 @@ class ClientProviderUpdateDTO(BaseModel):
|
||||
)
|
||||
position: Optional[str] = Field(None, max_length=30, description="Position")
|
||||
incoterm: Optional[str] = Field(None, max_length=19, description="Incoterm")
|
||||
is_national_provider: Optional[str] = Field(
|
||||
None, max_length=2, description="Is national provider"
|
||||
)
|
||||
is_national_provider: Optional[bool] = None
|
||||
is_active: Optional[bool] = Field(None, description="Enabled/Disabled status")
|
||||
|
||||
# Nested DTOs
|
||||
@@ -193,7 +189,7 @@ class ClientProviderResponseDTO(BaseModel):
|
||||
responsible: Optional[str] = None
|
||||
position: Optional[str] = None
|
||||
incoterm: Optional[str] = None
|
||||
is_national_provider: Optional[str] = None
|
||||
is_national_provider: Optional[bool] = None
|
||||
is_active: Optional[bool] = None
|
||||
tenant_id: int
|
||||
company_id: int
|
||||
|
||||
@@ -5,6 +5,7 @@ from pydantic import BaseModel
|
||||
|
||||
class CustomsBrokerBaseDTO(BaseModel):
|
||||
"""Base fields for CustomsBroker"""
|
||||
|
||||
type: Optional[str] = None
|
||||
name: Optional[str] = None
|
||||
address: Optional[str] = None
|
||||
@@ -25,16 +26,20 @@ class CustomsBrokerBaseDTO(BaseModel):
|
||||
|
||||
class CustomsBrokerCreateDTO(CustomsBrokerBaseDTO):
|
||||
"""Schema for creating a new CustomsBroker"""
|
||||
|
||||
broker_key: str
|
||||
|
||||
|
||||
class CustomsBrokerUpdateDTO(CustomsBrokerBaseDTO):
|
||||
"""Schema for updating an existing CustomsBroker"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CustomsBrokerResponseDTO(CustomsBrokerBaseDTO):
|
||||
"""Schema for CustomsBroker response"""
|
||||
|
||||
id: int
|
||||
broker_key: str
|
||||
tenant_id: int
|
||||
company_id: int
|
||||
|
||||
@@ -1,35 +1,64 @@
|
||||
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
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user, validate_access_to_resource
|
||||
|
||||
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
|
||||
from . import dto, services
|
||||
|
||||
# Create main router
|
||||
router = APIRouter()
|
||||
|
||||
# Create CRUD routes for CustomsBroker using TenantCRUDRoutes
|
||||
customs_broker_crud = TenantCRUDRoutes(
|
||||
service=services.CustomsBrokerService,
|
||||
create_schema=dto.CustomsBrokerCreateDTO,
|
||||
update_schema=dto.CustomsBrokerUpdateDTO,
|
||||
response_schema=dto.CustomsBrokerResponseDTO,
|
||||
prefix="/customs-brokers", # No prefix since it's already in the parent router
|
||||
prefix="/customs-brokers",
|
||||
tags=[],
|
||||
resource_name="Customs Broker",
|
||||
id_name="broker_key",
|
||||
id_type=str,
|
||||
enable_list=True, # Enable list endpoint with pagination
|
||||
enable_list=True,
|
||||
)
|
||||
|
||||
# Include the CRUD routes
|
||||
router.include_router(customs_broker_crud.router)
|
||||
|
||||
|
||||
# Additional routes for child resources (CustomsBrokerVU and CustomsBrokerPersonnel)
|
||||
# These remain as manual routes since they have different patterns
|
||||
@router.patch(
|
||||
"/customs-brokers/{broker_key}",
|
||||
response_model=dto.CustomsBrokerResponseDTO,
|
||||
)
|
||||
def update_customs_broker(
|
||||
broker_key: str,
|
||||
broker_data: dto.CustomsBrokerUpdateDTO,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Actualización parcial (PATCH).
|
||||
"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
broker = services.CustomsBrokerService.get_by_id(db, broker_key, tenant_id, company_id)
|
||||
if not broker:
|
||||
raise HTTPException(status_code=404, detail="Customs Broker not found")
|
||||
|
||||
updated_broker = services.CustomsBrokerService.update(
|
||||
db=db,
|
||||
broker_key=broker_key,
|
||||
tenant_id=tenant_id,
|
||||
broker_data=broker_data,
|
||||
company_id=company_id
|
||||
)
|
||||
|
||||
if not updated_broker:
|
||||
raise HTTPException(status_code=400, detail="Error updating Customs Broker")
|
||||
|
||||
return updated_broker
|
||||
|
||||
|
||||
@router.put(
|
||||
"/customs-broker-vu/{broker_key}",
|
||||
@@ -44,7 +73,6 @@ def update_customs_broker_vu(
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
# Verify the broker exists and belongs to the tenant/company
|
||||
broker = services.CustomsBrokerService.get_by_id(db, broker_key, tenant_id, company_id)
|
||||
if not broker:
|
||||
raise HTTPException(status_code=404, detail="Customs Broker not found")
|
||||
@@ -69,7 +97,6 @@ def update_customs_broker_personnel(
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
# Verify the broker exists and belongs to the tenant/company
|
||||
broker = services.CustomsBrokerService.get_by_id(db, broker_key, tenant_id, company_id)
|
||||
if not broker:
|
||||
raise HTTPException(status_code=404, detail="Customs Broker not found")
|
||||
@@ -81,5 +108,4 @@ def update_customs_broker_personnel(
|
||||
raise HTTPException(
|
||||
status_code=404, detail="Customs Broker Personnel not found"
|
||||
)
|
||||
return updated_personnel
|
||||
|
||||
return updated_personnel
|
||||
1
backend/api/v1/modules/a76/doc_types_dig/__init__.py
Normal file
1
backend/api/v1/modules/a76/doc_types_dig/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Document Types for Digitization module
|
||||
32
backend/api/v1/modules/a76/doc_types_dig/dto.py
Normal file
32
backend/api/v1/modules/a76/doc_types_dig/dto.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class DocumentTypeDigitizationBase(BaseModel):
|
||||
"""Base schema for Document Type Digitization"""
|
||||
|
||||
code: str = Field(..., max_length=10, description="Código del tipo de documento")
|
||||
description: str = Field(..., description="Descripción del tipo de documento")
|
||||
active: bool = Field(default=True, description="Indica si el tipo está activo")
|
||||
|
||||
|
||||
class DocumentTypeDigitizationCreate(DocumentTypeDigitizationBase):
|
||||
"""Schema for creating a Document Type Digitization"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DocumentTypeDigitizationUpdate(BaseModel):
|
||||
"""Schema for updating a Document Type Digitization"""
|
||||
|
||||
code: str | None = Field(None, max_length=10)
|
||||
description: str | None = None
|
||||
active: bool | None = None
|
||||
|
||||
|
||||
class DocumentTypeDigitizationResponse(DocumentTypeDigitizationBase):
|
||||
"""Schema for Document Type Digitization response"""
|
||||
|
||||
id: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
25
backend/api/v1/modules/a76/doc_types_dig/models.py
Normal file
25
backend/api/v1/modules/a76/doc_types_dig/models.py
Normal file
@@ -0,0 +1,25 @@
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
from sqlalchemy import Boolean, Integer, PrimaryKeyConstraint, String, Text, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
|
||||
class DocumentTypeDigitization(Base, TenantScopedMixin, TimestampMixin):
|
||||
"""Tipos de documentos para digitalización de pedimentos"""
|
||||
|
||||
__tablename__ = "document_types_digitization"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id", name="document_types_digitization_pkey"),
|
||||
UniqueConstraint(
|
||||
"tenant_id",
|
||||
"company_id",
|
||||
"code",
|
||||
name="document_types_digitization_code_key",
|
||||
),
|
||||
{"schema": "a76"},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer)
|
||||
code: Mapped[str] = mapped_column(String(10), nullable=False, index=True)
|
||||
description: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
170
backend/api/v1/modules/a76/doc_types_dig/routes.py
Normal file
170
backend/api/v1/modules/a76/doc_types_dig/routes.py
Normal file
@@ -0,0 +1,170 @@
|
||||
from typing import List
|
||||
|
||||
from api.v1.modules.a76.doc_types_dig.dto import (
|
||||
DocumentTypeDigitizationCreate,
|
||||
DocumentTypeDigitizationResponse,
|
||||
DocumentTypeDigitizationUpdate,
|
||||
)
|
||||
from api.v1.modules.a76.doc_types_dig.models import DocumentTypeDigitization
|
||||
from core.database import get_core_db
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
router = APIRouter(prefix="/document-types-digitization", tags=["Document Types Digitization"])
|
||||
|
||||
|
||||
@router.get("", response_model=List[DocumentTypeDigitizationResponse])
|
||||
def get_all_document_types(
|
||||
active_only: bool = True,
|
||||
db: Session = Depends(get_core_db),
|
||||
):
|
||||
"""
|
||||
Obtener todos los tipos de documentos para digitalización
|
||||
|
||||
Args:
|
||||
active_only: Si es True, solo devuelve los tipos activos
|
||||
"""
|
||||
query = select(DocumentTypeDigitization)
|
||||
|
||||
if active_only:
|
||||
query = query.where(DocumentTypeDigitization.active == True)
|
||||
|
||||
query = query.order_by(DocumentTypeDigitization.code)
|
||||
|
||||
result = db.execute(query)
|
||||
document_types = result.scalars().all()
|
||||
|
||||
return document_types
|
||||
|
||||
|
||||
@router.get("/{document_type_id}", response_model=DocumentTypeDigitizationResponse)
|
||||
def get_document_type(
|
||||
document_type_id: int,
|
||||
db: Session = Depends(get_core_db),
|
||||
):
|
||||
"""Obtener un tipo de documento por ID"""
|
||||
result = db.execute(
|
||||
select(DocumentTypeDigitization).where(DocumentTypeDigitization.id == document_type_id)
|
||||
)
|
||||
document_type = result.scalar_one_or_none()
|
||||
|
||||
if not document_type:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Tipo de documento con ID {document_type_id} no encontrado"
|
||||
)
|
||||
|
||||
return document_type
|
||||
|
||||
|
||||
@router.get("/by-code/{code}", response_model=DocumentTypeDigitizationResponse)
|
||||
def get_document_type_by_code(
|
||||
code: str,
|
||||
db: Session = Depends(get_core_db),
|
||||
):
|
||||
"""Obtener un tipo de documento por código"""
|
||||
result = db.execute(
|
||||
select(DocumentTypeDigitization).where(DocumentTypeDigitization.code == code)
|
||||
)
|
||||
document_type = result.scalar_one_or_none()
|
||||
|
||||
if not document_type:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Tipo de documento con código {code} no encontrado"
|
||||
)
|
||||
|
||||
return document_type
|
||||
|
||||
|
||||
@router.post("", response_model=DocumentTypeDigitizationResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_document_type(
|
||||
document_type_data: DocumentTypeDigitizationCreate,
|
||||
db: Session = Depends(get_core_db),
|
||||
):
|
||||
"""Crear un nuevo tipo de documento"""
|
||||
# Verificar si el código ya existe
|
||||
result = db.execute(
|
||||
select(DocumentTypeDigitization).where(DocumentTypeDigitization.code == document_type_data.code)
|
||||
)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Ya existe un tipo de documento con el código {document_type_data.code}"
|
||||
)
|
||||
|
||||
new_document_type = DocumentTypeDigitization(**document_type_data.model_dump())
|
||||
db.add(new_document_type)
|
||||
db.commit()
|
||||
db.refresh(new_document_type)
|
||||
|
||||
return new_document_type
|
||||
|
||||
|
||||
@router.put("/{document_type_id}", response_model=DocumentTypeDigitizationResponse)
|
||||
def update_document_type(
|
||||
document_type_id: int,
|
||||
document_type_data: DocumentTypeDigitizationUpdate,
|
||||
db: Session = Depends(get_core_db),
|
||||
):
|
||||
"""Actualizar un tipo de documento existente"""
|
||||
result = db.execute(
|
||||
select(DocumentTypeDigitization).where(DocumentTypeDigitization.id == document_type_id)
|
||||
)
|
||||
document_type = result.scalar_one_or_none()
|
||||
|
||||
if not document_type:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Tipo de documento con ID {document_type_id} no encontrado"
|
||||
)
|
||||
|
||||
# Actualizar solo los campos proporcionados
|
||||
update_data = document_type_data.model_dump(exclude_unset=True)
|
||||
|
||||
# Verificar si el nuevo código ya existe (si se está actualizando)
|
||||
if "code" in update_data and update_data["code"] != document_type.code:
|
||||
result = db.execute(
|
||||
select(DocumentTypeDigitization).where(DocumentTypeDigitization.code == update_data["code"])
|
||||
)
|
||||
existing = result.scalar_one_or_none()
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Ya existe un tipo de documento con el código {update_data['code']}"
|
||||
)
|
||||
|
||||
for field, value in update_data.items():
|
||||
setattr(document_type, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(document_type)
|
||||
|
||||
return document_type
|
||||
|
||||
|
||||
@router.delete("/{document_type_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_document_type(
|
||||
document_type_id: int,
|
||||
db: Session = Depends(get_core_db),
|
||||
):
|
||||
"""Eliminar un tipo de documento (soft delete, marca como inactivo)"""
|
||||
result = db.execute(
|
||||
select(DocumentTypeDigitization).where(DocumentTypeDigitization.id == document_type_id)
|
||||
)
|
||||
document_type = result.scalar_one_or_none()
|
||||
|
||||
if not document_type:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Tipo de documento con ID {document_type_id} no encontrado"
|
||||
)
|
||||
|
||||
# Soft delete - solo marcar como inactivo
|
||||
document_type.active = False
|
||||
db.commit()
|
||||
|
||||
return None
|
||||
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
Depreciation Catalog Module
|
||||
"""
|
||||
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
Depreciation Catalog DTOs
|
||||
"""
|
||||
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class DepreciationCatalogCreate(BaseModel):
|
||||
"""DTO for creating a depreciation catalog entry"""
|
||||
fraction: str = Field(..., max_length=10)
|
||||
description: str = Field(..., max_length=500)
|
||||
depreciation_rate: float = Field(..., ge=0, le=100)
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class DepreciationCatalogUpdate(BaseModel):
|
||||
"""DTO for updating a depreciation catalog entry"""
|
||||
fraction: str | None = Field(None, max_length=10)
|
||||
description: str | None = Field(None, max_length=500)
|
||||
depreciation_rate: float | None = Field(None, ge=0, le=100)
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class DepreciationCatalogResponse(BaseModel):
|
||||
"""DTO for depreciation catalog response"""
|
||||
id: int
|
||||
tenant_id: int
|
||||
company_id: int
|
||||
fraction: str
|
||||
description: str
|
||||
depreciation_rate: float
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
Depreciation Catalog Model
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, Integer, String, Numeric, ForeignKey, PrimaryKeyConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
|
||||
|
||||
class DepreciationCatalog(Base, TenantScopedMixin, TimestampMixin):
|
||||
"""Depreciation Catalog Model"""
|
||||
|
||||
__tablename__ = 'depreciation_catalog'
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id", name="depreciation_catalog_pkey"),
|
||||
{'schema': 'a76'}
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
|
||||
# Fields
|
||||
fraction: Mapped[str] = mapped_column(String(10), nullable=False, index=True)
|
||||
description: Mapped[str] = mapped_column(String(500), nullable=False, index=True)
|
||||
depreciation_rate: Mapped[float] = mapped_column(Numeric(5, 2), nullable=False)
|
||||
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
Depreciation Catalog Routes
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user
|
||||
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes, validate_access_to_resource
|
||||
from api.v1.modules.a76.general_catalogs.depreciation_catalog.service import DepreciationCatalogService
|
||||
from api.v1.modules.a76.general_catalogs.depreciation_catalog.dto import (
|
||||
DepreciationCatalogCreate,
|
||||
DepreciationCatalogUpdate,
|
||||
DepreciationCatalogResponse
|
||||
)
|
||||
|
||||
# Create base router with generic CRUD routes (disabled list because we'll create a custom one)
|
||||
base_router = TenantCRUDRoutes(
|
||||
service=DepreciationCatalogService,
|
||||
create_schema=DepreciationCatalogCreate,
|
||||
update_schema=DepreciationCatalogUpdate,
|
||||
response_schema=DepreciationCatalogResponse,
|
||||
prefix="/depreciation-catalog",
|
||||
tags=["a76 / general catalogs / depreciation catalog"],
|
||||
resource_name="DepreciationCatalog",
|
||||
id_name="depreciation_catalog_id",
|
||||
enable_list=False, # Disable default list, we'll add custom one
|
||||
enable_filters=False,
|
||||
default_page_size=100,
|
||||
max_page_size=1000,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/depreciation-catalog", tags=["a76 / general catalogs / depreciation catalog"])
|
||||
|
||||
# Custom list endpoint with search filter
|
||||
@router.get(
|
||||
"/",
|
||||
response_model=Dict[str, Any],
|
||||
summary="List Depreciation Catalog",
|
||||
description="Get paginated list of Depreciation Catalog entries with optional search filter",
|
||||
)
|
||||
async def list_depreciation_catalog(
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
page: int = Query(1, ge=1, description="Page number"),
|
||||
page_size: int = Query(100, ge=1, le=1000, description="Page size"),
|
||||
search: Optional[str] = Query(None, description="Search in fraction or description"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
items, total = DepreciationCatalogService.get_all(
|
||||
db, tenant_id, company_id, page, page_size, search
|
||||
)
|
||||
|
||||
return {
|
||||
"items": [DepreciationCatalogResponse.model_validate(item) for item in items],
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"pages": (total + page_size - 1) // page_size,
|
||||
}
|
||||
|
||||
# Include other CRUD routes from base router
|
||||
router.include_router(base_router.router)
|
||||
@@ -0,0 +1,34 @@
|
||||
"""
|
||||
Datos semilla para Catálogo de Depreciación
|
||||
Basado en artículo 34 de la Ley del Impuesto Sobre la Renta
|
||||
"""
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
# Formato: (fraction, description, depreciation_rate)
|
||||
seed = [
|
||||
("I", "a) Para inmuebles declarados como monumentos arqueológicos, artísticos, históricos o patrimoniales, conforme a la Ley Federal sobre Monumentos y Zonas Arqueológicos, Artísticos e Históricos, que cuenten con el certificado de restauración expedido por el Instituto Nacional de Antropología e Historia o el Instituto Nacional de Bellas Artes.", Decimal("10.00")),
|
||||
("I", "b) En los demás casos.", Decimal("5.00")),
|
||||
("II", "a) Para bombas de suministro de combustible a trenes.", Decimal("3.00")),
|
||||
("II", "b) Para vías férreas.", Decimal("5.00")),
|
||||
("II", "c) Para carros de ferrocarril, locomotoras, armones y autoarmones.", Decimal("6.00")),
|
||||
("II", "d) Para maquinaria niveladora de vías, desclavadoras, esmeriles para vías, gatos de motor para levantar la vía, removedora, insertadora y taladradora de durmientes.", Decimal("7.00")),
|
||||
("II", "e) Para el equipo de comunicación, señalización y telemando.", Decimal("10.00")),
|
||||
("III", "Para mobiliario y equipo de oficina.", Decimal("10.00")),
|
||||
("IV", "Para embarcaciones.", Decimal("6.00")),
|
||||
("IX", "Para semovientes y vegetales.", Decimal("100.00")),
|
||||
("V", "a) Para los dedicados a la aerofumigación agrícola.", Decimal("25.00")),
|
||||
("V", "b) Para los demás.", Decimal("10.00")),
|
||||
("VI", "Para automóviles, autobuses, camiones de carga, tractocamiones, montacargas y remolques.", Decimal("25.00")),
|
||||
("VII", "Para computadoras personales de escritorio y portátiles; servidores; impresoras, lectores ópticos, graficadores, lectores de código de barras, digitalizadores, unidades de almacenamiento externo y concentradores de redes de cómputo.", Decimal("30.00")),
|
||||
("VIII", "Para dados, troqueles, moldes, matrices y herramental.", Decimal("35.00")),
|
||||
("X", "a) Para torres de transmisión y cables, excepto los de fibra óptica.", Decimal("5.00")),
|
||||
("X", "b) Para sistemas de radio, incluyendo equipo de transmisión y manejo que utiliza el espectro radioeléctrico, tales como el de radiotransmisión de microonda digital o analógica, torres de microondas y guías de onda.", Decimal("8.00")),
|
||||
("X", "c) Para equipo utilizado en la transmisión, tales como circuitos de la planta interna que no forman parte de la conmutación y cuyas funciones se enfocan hacia las troncales que llegan a la central telefónica, incluye multiplexores, equipos concentradores y ruteadores.", Decimal("10.00")),
|
||||
("X", "d) Para equipo de la central telefónica destinado a la conmutación de llamadas de tecnología distinta a la electromecánica.", Decimal("25.00")),
|
||||
("X", "e) Para los demás.", Decimal("10.00")),
|
||||
("XI", "a) Para el segmento satelital en el espacio, incluyendo el cuerpo principal del satélite, los transpondedores, las antenas para la transmisión y recepción de comunicaciones digitales y análogas, y el equipo de monitoreo en el satélite.", Decimal("8.00")),
|
||||
("XI", "b) Para el equipo satelital en tierra, incluyendo las antenas para la transmisión y recepción de comunicaciones digitales y análogas y el equipo para el monitoreo del satélite.", Decimal("10.00")),
|
||||
("XII", "Para adaptaciones que se realicen a instalaciones que impliquen adiciones o mejoras al activo fijo, siempre que dichas adaptaciones tengan como finalidad facilitar a las personas con discapacidad a que se refiere el artículo 186 de esta Ley, el acceso y uso de las instalaciones del contribuyente.", Decimal("100.00")),
|
||||
("XIII", "Para maquinaria y equipo para la generación de energía proveniente de fuentes renovables o de sistemas de cogeneración de electricidad eficiente.", Decimal("100.00")),
|
||||
]
|
||||
@@ -0,0 +1,91 @@
|
||||
"""
|
||||
Depreciation Catalog Service
|
||||
"""
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import or_, func
|
||||
from api.v1.modules.a76.general_catalogs.depreciation_catalog.models import DepreciationCatalog
|
||||
|
||||
|
||||
class DepreciationCatalogService:
|
||||
"""Service for depreciation catalog operations"""
|
||||
|
||||
@staticmethod
|
||||
def get_all(
|
||||
db: Session,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
page: int = 1,
|
||||
page_size: int = 100,
|
||||
search: str | None = None
|
||||
):
|
||||
"""Get all depreciation catalog entries with optional search"""
|
||||
query = db.query(DepreciationCatalog).filter(
|
||||
DepreciationCatalog.tenant_id == tenant_id,
|
||||
DepreciationCatalog.company_id == company_id
|
||||
)
|
||||
|
||||
# Apply search filter if provided
|
||||
if search:
|
||||
search_filter = or_(
|
||||
DepreciationCatalog.fraction.ilike(f"%{search}%"),
|
||||
DepreciationCatalog.description.ilike(f"%{search}%"),
|
||||
func.cast(DepreciationCatalog.depreciation_rate, db.String).ilike(f"%{search}%")
|
||||
)
|
||||
query = query.filter(search_filter)
|
||||
|
||||
# Get total count before pagination
|
||||
total = query.count()
|
||||
|
||||
# Apply pagination
|
||||
offset = (page - 1) * page_size
|
||||
items = query.order_by(
|
||||
DepreciationCatalog.fraction,
|
||||
DepreciationCatalog.depreciation_rate
|
||||
).offset(offset).limit(page_size).all()
|
||||
|
||||
return items, total
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(db: Session, id: int, tenant_id: int, company_id: int):
|
||||
"""Get depreciation catalog entry by ID"""
|
||||
return db.query(DepreciationCatalog).filter(
|
||||
DepreciationCatalog.id == id,
|
||||
DepreciationCatalog.tenant_id == tenant_id,
|
||||
DepreciationCatalog.company_id == company_id
|
||||
).first()
|
||||
|
||||
@staticmethod
|
||||
def create(db: Session, data: dict):
|
||||
"""Create new depreciation catalog entry"""
|
||||
entry = DepreciationCatalog(**data)
|
||||
db.add(entry)
|
||||
db.commit()
|
||||
db.refresh(entry)
|
||||
return entry
|
||||
|
||||
@staticmethod
|
||||
def update(db: Session, id: int, tenant_id: int, company_id: int, data: dict):
|
||||
"""Update depreciation catalog entry"""
|
||||
entry = DepreciationCatalogService.get_by_id(db, id, tenant_id, company_id)
|
||||
if not entry:
|
||||
return None
|
||||
|
||||
for key, value in data.items():
|
||||
if value is not None:
|
||||
setattr(entry, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(entry)
|
||||
return entry
|
||||
|
||||
@staticmethod
|
||||
def delete(db: Session, id: int, tenant_id: int, company_id: int):
|
||||
"""Delete depreciation catalog entry"""
|
||||
entry = DepreciationCatalogService.get_by_id(db, id, tenant_id, company_id)
|
||||
if not entry:
|
||||
return False
|
||||
|
||||
db.delete(entry)
|
||||
db.commit()
|
||||
return True
|
||||
@@ -1,10 +1,18 @@
|
||||
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
|
||||
from typing import Any, Dict, Optional
|
||||
from fastapi import Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from api.v1.common.tenant_crud_routes import (
|
||||
TenantCRUDRoutes,
|
||||
validate_access_to_resource,
|
||||
get_core_db,
|
||||
get_current_user,
|
||||
)
|
||||
|
||||
from .dto import ExchangeRateCreateDTO, ExchangeRateResponseDTO, ExchangeRateUpdateDTO
|
||||
from .services import ExchangeRateService
|
||||
|
||||
# Create router using TenantCRUDRoutes factory
|
||||
router = TenantCRUDRoutes(
|
||||
route_handler = TenantCRUDRoutes(
|
||||
service=ExchangeRateService,
|
||||
create_schema=ExchangeRateCreateDTO,
|
||||
update_schema=ExchangeRateUpdateDTO,
|
||||
@@ -13,8 +21,48 @@ router = TenantCRUDRoutes(
|
||||
tags=[],
|
||||
resource_name="Exchange Rate",
|
||||
id_name="id", # Using numeric ID
|
||||
enable_list=True, # Enable GET /exchange-rate with pagination
|
||||
enable_list=False, # Disable default list to provide custom one with filters
|
||||
enable_filters=False,
|
||||
default_page_size=50,
|
||||
max_page_size=100,
|
||||
).router
|
||||
)
|
||||
|
||||
router = route_handler.router
|
||||
|
||||
|
||||
@router.get(
|
||||
"/",
|
||||
response_model=Dict[str, Any],
|
||||
summary="List Exchange Rates",
|
||||
description="Get paginated list of exchange rates with optional date filter",
|
||||
)
|
||||
async def list_exchange_rates(
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
date: Optional[str] = Query(None, description="Filter by date (YYYY-MM-DD)"),
|
||||
page: int = Query(1, ge=1, description="Page number"),
|
||||
page_size: int = Query(
|
||||
50,
|
||||
ge=1,
|
||||
le=100,
|
||||
description="Page size",
|
||||
),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
skip = (page - 1) * page_size
|
||||
filters = {}
|
||||
if date:
|
||||
filters["date"] = date
|
||||
|
||||
items, total = ExchangeRateService.get_all(
|
||||
db, tenant_id, company_id, skip, page_size, filters
|
||||
)
|
||||
|
||||
return {
|
||||
"items": [ExchangeRateResponseDTO.model_validate(item) for item in items],
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from typing import Optional, Tuple, List, Dict, Any
|
||||
from datetime import datetime, time
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import cast, Date
|
||||
|
||||
from . import dto, models
|
||||
|
||||
@@ -26,8 +28,23 @@ class ExchangeRateService:
|
||||
# Apply filters if provided
|
||||
if filters:
|
||||
if filters.get("date"):
|
||||
query = query.filter(
|
||||
models.ExchangeRate.date == filters["date"])
|
||||
# Use range query to utilize index on (tenant_id, company_id, date) efficiently
|
||||
# filters["date"] is expected to be 'YYYY-MM-DD'
|
||||
try:
|
||||
date_str = filters["date"]
|
||||
date_val = datetime.strptime(date_str, "%Y-%m-%d").date()
|
||||
start_date = datetime.combine(date_val, time.min)
|
||||
end_date = datetime.combine(date_val, time.max)
|
||||
|
||||
query = query.filter(
|
||||
models.ExchangeRate.date >= start_date,
|
||||
models.ExchangeRate.date <= end_date,
|
||||
)
|
||||
except (ValueError, TypeError):
|
||||
# Fallback to cast if date format is invalid or logic fails, though validation should catch this
|
||||
query = query.filter(
|
||||
cast(models.ExchangeRate.date, Date) == filters["date"]
|
||||
)
|
||||
if filters.get("local_currency"):
|
||||
query = query.filter(
|
||||
models.ExchangeRate.local_currency == filters["local_currency"]
|
||||
@@ -38,8 +55,12 @@ class ExchangeRateService:
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
exchange_rates = query.order_by(
|
||||
models.ExchangeRate.date.desc()).offset(skip).limit(limit).all()
|
||||
exchange_rates = (
|
||||
query.order_by(models.ExchangeRate.date.desc())
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
.all()
|
||||
)
|
||||
|
||||
return exchange_rates, total
|
||||
|
||||
@@ -67,7 +88,9 @@ class ExchangeRateService:
|
||||
) -> models.ExchangeRate:
|
||||
"""Create a new exchange rate"""
|
||||
new_exchange_rate = models.ExchangeRate(
|
||||
**exchange_rate_data.model_dump(), tenant_id=tenant_id, company_id=company_id
|
||||
**exchange_rate_data.model_dump(),
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id
|
||||
)
|
||||
db.add(new_exchange_rate)
|
||||
db.commit()
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
"""
|
||||
__init__.py para módulo de catálogo FDA
|
||||
"""
|
||||
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
DTOs para el catálogo de claves FDA
|
||||
"""
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class FDACatalogCreate(BaseModel):
|
||||
"""DTO para crear una entrada en el catálogo FDA"""
|
||||
fda_key: str = Field(..., max_length=20, description="Clave FDA")
|
||||
description: str = Field(..., max_length=500, description="Descripción")
|
||||
fda_code: Optional[str] = Field(None, max_length=50, description="Código FDA")
|
||||
requirements: Optional[str] = Field(None, max_length=500, description="Requerimientos")
|
||||
manufacturer_number: Optional[str] = Field(None, max_length=50, description="Número de fabricante")
|
||||
country_of_production: Optional[str] = Field(None, max_length=100, description="País de producción")
|
||||
storage_status: Optional[str] = Field(None, max_length=100, description="Estatus de almacenaje")
|
||||
warehouse_code: Optional[str] = Field(None, max_length=20, description="Código de almacén")
|
||||
call_atl: Optional[str] = Field(None, max_length=20, description="CallAtl")
|
||||
|
||||
model_config = {
|
||||
"from_attributes": True
|
||||
}
|
||||
|
||||
|
||||
class FDACatalogUpdate(BaseModel):
|
||||
"""DTO para actualizar una entrada en el catálogo FDA"""
|
||||
fda_key: Optional[str] = Field(None, max_length=20, description="Clave FDA")
|
||||
description: Optional[str] = Field(None, max_length=500, description="Descripción")
|
||||
fda_code: Optional[str] = Field(None, max_length=50, description="Código FDA")
|
||||
requirements: Optional[str] = Field(None, max_length=500, description="Requerimientos")
|
||||
manufacturer_number: Optional[str] = Field(None, max_length=50, description="Número de fabricante")
|
||||
country_of_production: Optional[str] = Field(None, max_length=100, description="País de producción")
|
||||
storage_status: Optional[str] = Field(None, max_length=100, description="Estatus de almacenaje")
|
||||
warehouse_code: Optional[str] = Field(None, max_length=20, description="Código de almacén")
|
||||
call_atl: Optional[str] = Field(None, max_length=20, description="CallAtl")
|
||||
|
||||
model_config = {
|
||||
"from_attributes": True
|
||||
}
|
||||
|
||||
|
||||
class FDACatalogResponse(BaseModel):
|
||||
"""DTO para respuesta del catálogo FDA"""
|
||||
id: int
|
||||
fda_key: str
|
||||
description: str
|
||||
fda_code: Optional[str]
|
||||
requirements: Optional[str]
|
||||
manufacturer_number: Optional[str]
|
||||
country_of_production: Optional[str]
|
||||
storage_status: Optional[str]
|
||||
warehouse_code: Optional[str]
|
||||
call_atl: Optional[str]
|
||||
created_at: Optional[str]
|
||||
updated_at: Optional[str]
|
||||
|
||||
model_config = {
|
||||
"from_attributes": True
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
"""
|
||||
Modelo para el catálogo de claves FDA
|
||||
"""
|
||||
from typing import Optional
|
||||
from sqlalchemy import String, Integer, UniqueConstraint, PrimaryKeyConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from core.database import Base
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
|
||||
|
||||
class FDACatalog(Base, TenantScopedMixin, TimestampMixin):
|
||||
"""Catálogo de claves FDA"""
|
||||
__tablename__ = "fda_catalog"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id", name="fda_catalog_pkey"),
|
||||
UniqueConstraint('tenant_id', 'company_id', 'fda_key', name='idx_fda_catalog_unique'),
|
||||
{'schema': 'a76'}
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
fda_key: Mapped[str] = mapped_column(String(20), nullable=False, index=True)
|
||||
description: Mapped[str] = mapped_column(String(500), nullable=False, index=True)
|
||||
fda_code: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
requirements: Mapped[Optional[str]] = mapped_column(String(500))
|
||||
manufacturer_number: Mapped[Optional[str]] = mapped_column(String(50))
|
||||
country_of_production: Mapped[Optional[str]] = mapped_column(String(100))
|
||||
storage_status: Mapped[Optional[str]] = mapped_column(String(100))
|
||||
warehouse_code: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
call_atl: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
Rutas para el catálogo de claves FDA
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, HTTPException, status, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Any, Dict, Optional
|
||||
from api.v1.modules.a76.general_catalogs.fda_catalog.service import FDACatalogService
|
||||
from api.v1.modules.a76.general_catalogs.fda_catalog.dto import FDACatalogResponse
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user
|
||||
from api.v1.common.tenant_crud_routes import validate_access_to_resource
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/fda-catalog",
|
||||
tags=["a76 / general catalogs / fda catalog"]
|
||||
)
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_fda_catalog(
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
page: int = Query(1, ge=1, description="Page number"),
|
||||
page_size: int = Query(50, ge=1, le=1000, description="Page size"),
|
||||
search: Optional[str] = Query(None, description="Search in FDA key, description or code"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""Listar entradas del catálogo FDA con búsqueda y paginación"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
filters = {"search": search} if search else {}
|
||||
result = FDACatalogService.get_all(db, tenant_id, company_id, page, page_size, filters)
|
||||
|
||||
return {
|
||||
"items": result["items"],
|
||||
"total": result["total"],
|
||||
"page": result["page"],
|
||||
"page_size": result["page_size"],
|
||||
"pages": result["pages"]
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{id}")
|
||||
async def get_fda_catalog(
|
||||
id: int,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user)
|
||||
):
|
||||
"""Obtener una entrada del catálogo FDA por ID"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
entry = FDACatalogService.get_by_id(db, tenant_id, company_id, id)
|
||||
if not entry:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Entrada no encontrada")
|
||||
|
||||
return {
|
||||
"id": entry.id,
|
||||
"fda_key": entry.fda_key,
|
||||
"description": entry.description,
|
||||
"fda_code": entry.fda_code,
|
||||
"requirements": entry.requirements,
|
||||
"manufacturer_number": entry.manufacturer_number,
|
||||
"country_of_production": entry.country_of_production,
|
||||
"storage_status": entry.storage_status,
|
||||
"warehouse_code": entry.warehouse_code,
|
||||
"call_atl": entry.call_atl
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
"""
|
||||
Datos semilla para Catálogo FDA
|
||||
|
||||
Catálogo de clasificación de la Food and Drug Administration (FDA)
|
||||
para importaciones/exportaciones de productos regulados.
|
||||
"""
|
||||
|
||||
seed = [
|
||||
# (fda_key, description, fda_code, requirements, manufacturer_number, country_of_production)
|
||||
("3012", "Dispositivos médicos clase I", "MED-I", "Registro FDA", "MFR001", "US"),
|
||||
("3013", "Dispositivos médicos clase II", "MED-II", "Registro FDA + 510(k)", "MFR002", "US"),
|
||||
("3014", "Suplementos alimenticios", "SUP-01", "Registro Establecimiento", "MFR003", "MX"),
|
||||
("3015", "Medicamentos de venta libre", "OTC-01", "Monografía FDA", "MFR004", "US"),
|
||||
("3016", "Cosméticos", "COS-01", "Registro Voluntario", "MFR005", "MX"),
|
||||
]
|
||||
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
Servicio para el catálogo de claves FDA
|
||||
"""
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from api.v1.modules.a76.general_catalogs.fda_catalog.models import FDACatalog
|
||||
from api.v1.modules.a76.general_catalogs.fda_catalog.dto import FDACatalogCreate, FDACatalogUpdate
|
||||
|
||||
|
||||
class FDACatalogService:
|
||||
"""Servicio CRUD para catálogo de FDA"""
|
||||
|
||||
@staticmethod
|
||||
def get_all(db: Session, tenant_id: int, company_id: int, page: int = 1, page_size: int = 50, filters: dict = None):
|
||||
"""Obtener todas las entradas del catálogo FDA con paginación y búsqueda"""
|
||||
if filters is None:
|
||||
filters = {}
|
||||
|
||||
query = select(FDACatalog).where(
|
||||
(FDACatalog.tenant_id == tenant_id) &
|
||||
(FDACatalog.company_id == company_id)
|
||||
)
|
||||
|
||||
# Búsqueda multi-campo
|
||||
search = filters.get('search', '').strip()
|
||||
if search:
|
||||
search_pattern = f"%{search}%"
|
||||
query = query.where(
|
||||
(FDACatalog.fda_key.ilike(search_pattern)) |
|
||||
(FDACatalog.description.ilike(search_pattern)) |
|
||||
(FDACatalog.fda_code.ilike(search_pattern))
|
||||
)
|
||||
|
||||
# Contar total
|
||||
total_query = select(FDACatalog).where(
|
||||
(FDACatalog.tenant_id == tenant_id) &
|
||||
(FDACatalog.company_id == company_id)
|
||||
)
|
||||
if search:
|
||||
total_query = total_query.where(
|
||||
(FDACatalog.fda_key.ilike(search_pattern)) |
|
||||
(FDACatalog.description.ilike(search_pattern)) |
|
||||
(FDACatalog.fda_code.ilike(search_pattern))
|
||||
)
|
||||
total = db.execute(select(FDACatalog).distinct()).scalars().all().__len__()
|
||||
|
||||
# Paginación
|
||||
offset = (page - 1) * page_size
|
||||
query = query.offset(offset).limit(page_size)
|
||||
|
||||
items = db.execute(query).scalars().all()
|
||||
|
||||
return {
|
||||
"items": items,
|
||||
"total": len(items),
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"pages": (len(items) + page_size - 1) // page_size
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(db: Session, tenant_id: int, company_id: int, id: int):
|
||||
"""Obtener una entrada por ID"""
|
||||
return db.execute(
|
||||
select(FDACatalog).where(
|
||||
(FDACatalog.id == id) &
|
||||
(FDACatalog.tenant_id == tenant_id) &
|
||||
(FDACatalog.company_id == company_id)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
|
||||
@staticmethod
|
||||
def create(db: Session, tenant_id: int, company_id: int, data: FDACatalogCreate):
|
||||
"""Crear una nueva entrada"""
|
||||
entry = FDACatalog(
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
**data.model_dump()
|
||||
)
|
||||
db.add(entry)
|
||||
db.commit()
|
||||
db.refresh(entry)
|
||||
return entry
|
||||
|
||||
@staticmethod
|
||||
def update(db: Session, tenant_id: int, company_id: int, id: int, data: FDACatalogUpdate):
|
||||
"""Actualizar una entrada"""
|
||||
entry = FDACatalogService.get_by_id(db, tenant_id, company_id, id)
|
||||
if not entry:
|
||||
return None
|
||||
|
||||
for field, value in data.model_dump(exclude_unset=True).items():
|
||||
setattr(entry, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(entry)
|
||||
return entry
|
||||
|
||||
@staticmethod
|
||||
def delete(db: Session, tenant_id: int, company_id: int, id: int):
|
||||
"""Eliminar una entrada"""
|
||||
entry = FDACatalogService.get_by_id(db, tenant_id, company_id, id)
|
||||
if not entry:
|
||||
return False
|
||||
|
||||
db.delete(entry)
|
||||
db.commit()
|
||||
return True
|
||||
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
Módulo de fracciones arancelarias (Tariff Fractions)
|
||||
"""
|
||||
|
||||
from .models import TariffFraction
|
||||
from .routes import router
|
||||
|
||||
__all__ = ["TariffFraction", "router"]
|
||||
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
DTOs (Data Transfer Objects) para fracciones arancelarias
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class TariffFractionCreateDTO(BaseModel):
|
||||
"""DTO para crear una fracción arancelaria"""
|
||||
|
||||
code: str = Field(..., max_length=10, description="Código completo de la fracción")
|
||||
fraction: str = Field(..., max_length=15, description="Fracción formateada")
|
||||
description: Optional[str] = Field(None, max_length=1000, description="Descripción")
|
||||
nico: Optional[str] = Field(None, max_length=10, description="Código NICO")
|
||||
umt: Optional[str] = Field(None, max_length=10, description="Unidad de medida de tarifa")
|
||||
adv_impo: Optional[str] = Field(None, max_length=20, description="Ad valorem importación")
|
||||
adv_expo: Optional[str] = Field(None, max_length=20, description="Ad valorem exportación")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class TariffFractionUpdateDTO(BaseModel):
|
||||
"""DTO para actualizar una fracción arancelaria"""
|
||||
|
||||
fraction: Optional[str] = Field(None, max_length=15, description="Fracción formateada")
|
||||
description: Optional[str] = Field(None, max_length=1000, description="Descripción")
|
||||
nico: Optional[str] = Field(None, max_length=10, description="Código NICO")
|
||||
umt: Optional[str] = Field(None, max_length=10, description="Unidad de medida de tarifa")
|
||||
adv_impo: Optional[str] = Field(None, max_length=20, description="Ad valorem importación")
|
||||
adv_expo: Optional[str] = Field(None, max_length=20, description="Ad valorem exportación")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class TariffFractionResponseDTO(BaseModel):
|
||||
"""DTO para respuesta de fracción arancelaria"""
|
||||
|
||||
id: int
|
||||
tenant_id: int
|
||||
company_id: int
|
||||
code: str
|
||||
fraction: str
|
||||
description: Optional[str] = None
|
||||
nico: Optional[str] = None
|
||||
umt: Optional[str] = None
|
||||
adv_impo: Optional[str] = None
|
||||
adv_expo: Optional[str] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class TariffFractionBasicDTO(BaseModel):
|
||||
"""DTO para información básica de fracción arancelaria"""
|
||||
|
||||
code: str
|
||||
fraction: str
|
||||
description: Optional[str] = None
|
||||
nico: Optional[str] = None
|
||||
umt: Optional[str] = None
|
||||
adv_impo: Optional[str] = None
|
||||
adv_expo: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class TariffFractionListDTO(BaseModel):
|
||||
"""DTO para lista de fracciones arancelarias"""
|
||||
|
||||
items: list[TariffFractionBasicDTO]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
pages: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class TariffFractionSearchDTO(BaseModel):
|
||||
"""DTO para búsqueda de fracciones arancelarias"""
|
||||
|
||||
code: Optional[str] = Field(None, description="Buscar por código")
|
||||
fraction: Optional[str] = Field(None, description="Buscar por fracción")
|
||||
description: Optional[str] = Field(None, description="Buscar en descripción")
|
||||
nico: Optional[str] = Field(None, description="Filtrar por NICO")
|
||||
umt: Optional[str] = Field(None, description="Filtrar por UMT")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
Modelos ORM para fracciones arancelarias (SITAR-SCAII)
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
from sqlalchemy import Integer, PrimaryKeyConstraint, String, Numeric
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
|
||||
class TariffFraction(Base, TenantScopedMixin, TimestampMixin):
|
||||
"""
|
||||
Modelo para fracciones arancelarias mexicanas (SITAR-SCAII)
|
||||
Corresponde a la tabla sFracciones
|
||||
"""
|
||||
|
||||
__tablename__ = "tariff_fractions"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id", name="tariff_fractions_pkey"),
|
||||
{"schema": "a76", "extend_existing": True},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
|
||||
# Código completo de la fracción (ej: 01012101)
|
||||
code: Mapped[str] = mapped_column(String(10), unique=True, index=True)
|
||||
|
||||
# Fracción formateada (ej: 0101.21.01)
|
||||
fraction: Mapped[str] = mapped_column(String(15), index=True)
|
||||
|
||||
# Descripción de la fracción
|
||||
description: Mapped[Optional[str]] = mapped_column(String(1000))
|
||||
|
||||
# Código NICO
|
||||
nico: Mapped[Optional[str]] = mapped_column(String(10))
|
||||
|
||||
# Unidad de medida de tarifa (UMT)
|
||||
umt: Mapped[Optional[str]] = mapped_column(String(10))
|
||||
|
||||
# Ad valorem de importación
|
||||
adv_impo: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
|
||||
# Ad valorem de exportación
|
||||
adv_expo: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<TariffFraction(code='{self.code}', fraction='{self.fraction}', description='{self.description[:50]}...')>"
|
||||
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
Endpoints API para fracciones arancelarias
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user
|
||||
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes, validate_access_to_resource
|
||||
|
||||
from .dto import (
|
||||
TariffFractionCreateDTO,
|
||||
TariffFractionResponseDTO,
|
||||
TariffFractionUpdateDTO,
|
||||
)
|
||||
from .service import TariffFractionService
|
||||
|
||||
# Create base router with generic CRUD routes (disabled list because we'll create a custom one)
|
||||
base_router = TenantCRUDRoutes(
|
||||
service=TariffFractionService,
|
||||
create_schema=TariffFractionCreateDTO,
|
||||
update_schema=TariffFractionUpdateDTO,
|
||||
response_schema=TariffFractionResponseDTO,
|
||||
prefix="/tariff-fractions",
|
||||
tags=["a76 / general catalogs / tariff fractions"],
|
||||
resource_name="TariffFraction",
|
||||
id_name="tariff_fraction_id",
|
||||
enable_list=False, # Disable default list, we'll add custom one
|
||||
enable_filters=False,
|
||||
default_page_size=50,
|
||||
max_page_size=10000,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/tariff-fractions", tags=["a76 / general catalogs / tariff fractions"])
|
||||
|
||||
# Custom list endpoint with search filter
|
||||
@router.get(
|
||||
"/",
|
||||
response_model=Dict[str, Any],
|
||||
summary="List Tariff Fractions",
|
||||
description="Get paginated list of Tariff Fractions with optional search filter",
|
||||
)
|
||||
async def list_tariff_fractions(
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
page: int = Query(1, ge=1, description="Page number"),
|
||||
page_size: int = Query(50, ge=1, le=10000, description="Page size"),
|
||||
search: Optional[str] = Query(None, description="Search in code, fraction, description, nico, or umt"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
skip = (page - 1) * page_size
|
||||
filters = {}
|
||||
if search:
|
||||
filters["search"] = search
|
||||
|
||||
items, total = TariffFractionService.get_all(
|
||||
db, tenant_id, company_id, skip, page_size, filters
|
||||
)
|
||||
|
||||
return {
|
||||
"items": [TariffFractionResponseDTO.model_validate(item) for item in items],
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"pages": (total + page_size - 1) // page_size,
|
||||
}
|
||||
|
||||
# Include other CRUD routes from base router
|
||||
router.include_router(base_router.router)
|
||||
|
||||
8189
backend/api/v1/modules/a76/general_catalogs/tariff_fractions/seed.py
Normal file
8189
backend/api/v1/modules/a76/general_catalogs/tariff_fractions/seed.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,190 @@
|
||||
"""
|
||||
Service para fracciones arancelarias
|
||||
"""
|
||||
|
||||
from typing import List, Optional, Tuple, Dict, Any
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from fastapi import HTTPException
|
||||
import logging
|
||||
|
||||
from .models import TariffFraction
|
||||
from .dto import TariffFractionCreateDTO, TariffFractionUpdateDTO
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TariffFractionService:
|
||||
"""Service para gestionar fracciones arancelarias"""
|
||||
|
||||
@staticmethod
|
||||
def get_all(
|
||||
db: Session,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
) -> Tuple[List[TariffFraction], int]:
|
||||
"""Obtiene todas las fracciones arancelarias con filtros opcionales"""
|
||||
|
||||
query = db.query(TariffFraction).filter(
|
||||
TariffFraction.tenant_id == tenant_id,
|
||||
TariffFraction.company_id == company_id,
|
||||
)
|
||||
|
||||
# Aplicar filtros
|
||||
if filters:
|
||||
# Si hay un filtro 'search', buscar en múltiples campos
|
||||
if filters.get("search"):
|
||||
search_term = f"%{filters['search']}%"
|
||||
query = query.filter(
|
||||
TariffFraction.code.ilike(search_term) |
|
||||
TariffFraction.fraction.ilike(search_term) |
|
||||
TariffFraction.description.ilike(search_term) |
|
||||
TariffFraction.nico.ilike(search_term) |
|
||||
TariffFraction.umt.ilike(search_term)
|
||||
)
|
||||
else:
|
||||
# Filtros individuales
|
||||
if filters.get("code"):
|
||||
query = query.filter(TariffFraction.code.ilike(f"%{filters['code']}%"))
|
||||
if filters.get("fraction"):
|
||||
query = query.filter(TariffFraction.fraction.ilike(f"%{filters['fraction']}%"))
|
||||
if filters.get("description"):
|
||||
query = query.filter(TariffFraction.description.ilike(f"%{filters['description']}%"))
|
||||
if filters.get("nico"):
|
||||
query = query.filter(TariffFraction.nico.ilike(f"%{filters['nico']}%"))
|
||||
if filters.get("umt"):
|
||||
query = query.filter(TariffFraction.umt.ilike(f"%{filters['umt']}%"))
|
||||
|
||||
total = query.count()
|
||||
items = query.offset(skip).limit(limit).all()
|
||||
|
||||
return items, total
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(
|
||||
db: Session,
|
||||
tariff_fraction_id: int,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
) -> Optional[TariffFraction]:
|
||||
"""Obtiene una fracción arancelaria por ID"""
|
||||
|
||||
return (
|
||||
db.query(TariffFraction)
|
||||
.filter(
|
||||
TariffFraction.id == tariff_fraction_id,
|
||||
TariffFraction.tenant_id == tenant_id,
|
||||
TariffFraction.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_by_code(
|
||||
db: Session,
|
||||
code: str,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
) -> Optional[TariffFraction]:
|
||||
"""Obtiene una fracción arancelaria por código"""
|
||||
|
||||
return (
|
||||
db.query(TariffFraction)
|
||||
.filter(
|
||||
TariffFraction.code == code,
|
||||
TariffFraction.tenant_id == tenant_id,
|
||||
TariffFraction.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
db: Session,
|
||||
tariff_fraction_data: TariffFractionCreateDTO,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
) -> TariffFraction:
|
||||
"""Crea una nueva fracción arancelaria"""
|
||||
|
||||
try:
|
||||
tariff_fraction = TariffFraction(
|
||||
**tariff_fraction_data.model_dump(),
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
)
|
||||
db.add(tariff_fraction)
|
||||
db.commit()
|
||||
db.refresh(tariff_fraction)
|
||||
return tariff_fraction
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error creating tariff fraction: {e}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Tariff fraction with this code already exists",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def update(
|
||||
db: Session,
|
||||
tariff_fraction_id: int,
|
||||
tariff_fraction_data: TariffFractionUpdateDTO,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
) -> Optional[TariffFraction]:
|
||||
"""Actualiza una fracción arancelaria existente"""
|
||||
|
||||
tariff_fraction = TariffFractionService.get_by_id(
|
||||
db, tariff_fraction_id, tenant_id, company_id
|
||||
)
|
||||
|
||||
if not tariff_fraction:
|
||||
return None
|
||||
|
||||
try:
|
||||
update_data = tariff_fraction_data.model_dump(exclude_unset=True)
|
||||
for key, value in update_data.items():
|
||||
setattr(tariff_fraction, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(tariff_fraction)
|
||||
return tariff_fraction
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error updating tariff fraction: {e}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Error updating tariff fraction",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def delete(
|
||||
db: Session,
|
||||
tariff_fraction_id: int,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
) -> bool:
|
||||
"""Elimina una fracción arancelaria"""
|
||||
|
||||
tariff_fraction = TariffFractionService.get_by_id(
|
||||
db, tariff_fraction_id, tenant_id, company_id
|
||||
)
|
||||
|
||||
if not tariff_fraction:
|
||||
return False
|
||||
|
||||
try:
|
||||
db.delete(tariff_fraction)
|
||||
db.commit()
|
||||
return True
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error deleting tariff fraction: {e}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Cannot delete tariff fraction - may be in use",
|
||||
)
|
||||
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
Datos semilla para Unidades de Medida Comerciales
|
||||
Migrado desde frontend para centralizar en backend
|
||||
"""
|
||||
|
||||
# Formato: (code, description, description_en, customs_code, american_code, ace_code, oma_code)
|
||||
seed = [
|
||||
('BARR', 'BARRIL', 'BARREL', '8', 'BBL', '', 'BLL'),
|
||||
('BD FT', 'PIE TABLA', 'BD FEET', '5', 'FT', '', 'BFT'),
|
||||
('BOLS', 'BOLSA', 'BAG', '6', 'PCS', '', 'BG'),
|
||||
('BTL', 'BOTELLA', 'BOTTLE', '21', 'PCS', '', 'BO'),
|
||||
('BULT', 'BULTO', 'BULK', '6', 'PCS', '', 'VQ'),
|
||||
('CAJA', 'CAJA', 'BOX', '20', '', '', 'BX'),
|
||||
('CARAT', 'CARAT', 'CARAT', '22', '', '', 'HE'),
|
||||
('CBZA', 'CABEZA', 'HEAD', '7', 'PCS', '', 'Z4'),
|
||||
('CIEN', 'CIENTO', 'CIEN', '18', '', '', 'CEN'),
|
||||
('CM', 'CENTIMETRO', 'CM', '3', 'CM', '', 'CMT'),
|
||||
('CM2', 'CENTIMETRO CUADRADO', 'CM2', '4', 'CM2', '', 'CMK'),
|
||||
('DEC', 'DECENA', '', '17', '', '', 'DC'),
|
||||
('DM', 'DECIMETRO', 'DM', '3', '', '', 'DMT'),
|
||||
('DM2', 'DECIMETRO CUADRADO', 'SQ DM', '4', '', '', 'DMK'),
|
||||
('DOCE', 'DOCENA', 'DOZ', '19', 'DOZ', 'DZ', 'DZN'),
|
||||
('FOZ', 'ONZA LIQUIDA', 'FOZ', '8', 'FOZ', '', 'OZA'),
|
||||
('FT', 'PIES', 'FT', '3', 'FT', '', 'LF'),
|
||||
('FT2', 'PIE CUADRADO', 'FT2', '4', 'SFT', '', 'FTK'),
|
||||
('GAL', 'GALON', 'GAL', '8', 'GAL', '', 'GLL'),
|
||||
('GR', 'GRAMO', 'GRAM', '2', '', '', 'GRM'),
|
||||
('IN', 'PULGADA', 'IN', '3', '', '', 'LI'),
|
||||
('IN2', 'PULGADA CUADRADA', 'IN2', '4', '', '', 'INK'),
|
||||
('JGO', 'JUEGO', 'SET', '12', '', '', 'SET'),
|
||||
('KGS', 'KILOGRAMOS', 'KGS', '1', 'KG2', '', 'KGM'),
|
||||
('LB', 'LIBRAS', 'LB', '1', '', '', 'LBR'),
|
||||
('LT', 'LITRO', 'LT', '8', 'L', '', 'LTR'),
|
||||
('M2', 'METRO CUADRADO', 'M2', '4', 'M2', '', 'MTK'),
|
||||
('M3', 'METRO CUBICO', 'M3', '5', 'M3', '', 'MTQ'),
|
||||
('MI', 'MILLA', 'MILE', '3', 'KM', '', 'SMI'),
|
||||
('MILLR', 'MILLAR', 'MILLR', '11', '', '', 'MIL'),
|
||||
('MT', 'METROS', 'MT', '3', 'M', '', 'MTR'),
|
||||
('OZ', 'ONZA', 'OZ', '8', 'FOZ', '', 'OZ'),
|
||||
('PAR', 'PAR', 'PAIR', '9', '', '', 'PB'),
|
||||
('PQ', 'PAQUETE', 'PACKAGE', '6', 'PCS', '', 'PK_1'),
|
||||
('PZA', 'PIEZA', 'PCS', '6', 'PCS', '', 'C62_1'),
|
||||
('QGL', 'CUARTO DE GALON', 'QGL', '8', '', '', 'QT'),
|
||||
('ROLL', 'ROLLO', 'ROLL', '6', '', '', 'RO'),
|
||||
('TON', 'TONELADA', 'TON', '14', 'TON', '', 'TNE_1'),
|
||||
('TOZ', 'ONZA TROY', 'TOZ', '1', 'TOZ', '', 'APZ'),
|
||||
('YD', 'YARDA', 'YD', '3', 'YD', '', 'YRD'),
|
||||
('YD2', 'YARDA CUADRADA', 'YD2', '4', 'SYD', '', 'YDK'),
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
"""
|
||||
US Tariff Fractions Catalog Module
|
||||
"""
|
||||
|
||||
from .models import USTariffFraction
|
||||
from .routes import router
|
||||
|
||||
__all__ = ["USTariffFraction", "router"]
|
||||
@@ -0,0 +1,48 @@
|
||||
"""
|
||||
DTOs para fracciones arancelarias americanas
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
|
||||
|
||||
class USTariffFractionCreateDTO(BaseModel):
|
||||
"""DTO para crear fracción arancelaria americana"""
|
||||
|
||||
code: str = Field(..., max_length=16, description="Código de fracción americana")
|
||||
prefix: Optional[str] = Field(None, max_length=10, description="Prefijo")
|
||||
type_code: Optional[str] = Field(None, max_length=10, description="Código de tipo")
|
||||
ad_valorem: Optional[float] = Field(None, description="Porcentaje ad valorem")
|
||||
fixed_cost: Optional[float] = Field(None, description="Tasa fija")
|
||||
unit_of_measure: Optional[str] = Field(None, max_length=10, description="Unidad de medida")
|
||||
description: Optional[str] = Field(None, description="Descripción")
|
||||
|
||||
|
||||
class USTariffFractionUpdateDTO(BaseModel):
|
||||
"""DTO para actualizar fracción arancelaria americana"""
|
||||
|
||||
prefix: Optional[str] = Field(None, max_length=10)
|
||||
type_code: Optional[str] = Field(None, max_length=10)
|
||||
ad_valorem: Optional[float] = None
|
||||
fixed_cost: Optional[float] = None
|
||||
unit_of_measure: Optional[str] = Field(None, max_length=10)
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class USTariffFractionResponseDTO(BaseModel):
|
||||
"""DTO para respuesta de fracción arancelaria americana"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
code: str
|
||||
prefix: Optional[str] = None
|
||||
type_code: Optional[str] = None
|
||||
ad_valorem: Optional[float] = None
|
||||
fixed_cost: Optional[float] = None
|
||||
unit_of_measure: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
@@ -0,0 +1,62 @@
|
||||
"""
|
||||
Modelos para fracciones arancelarias americanas
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy import String, Numeric, TIMESTAMP, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from core.database import Base
|
||||
|
||||
|
||||
class USTariffFraction(Base):
|
||||
"""Modelo para fracciones arancelarias americanas (US HTS codes)"""
|
||||
|
||||
__tablename__ = "us_tariff_fractions"
|
||||
__table_args__ = {"schema": "a76"}
|
||||
|
||||
# Primary Key
|
||||
id: Mapped[int] = mapped_column(primary_key=True, index=True)
|
||||
|
||||
# Tenant/Company
|
||||
tenant_id: Mapped[int] = mapped_column(index=True, nullable=False)
|
||||
company_id: Mapped[int] = mapped_column(index=True, nullable=False)
|
||||
|
||||
# Datos principales
|
||||
code: Mapped[str] = mapped_column(
|
||||
String(16), nullable=False, comment="Código de fracción americana"
|
||||
)
|
||||
prefix: Mapped[Optional[str]] = mapped_column(
|
||||
String(10), comment="Prefijo de clasificación"
|
||||
)
|
||||
type_code: Mapped[Optional[str]] = mapped_column(
|
||||
String(10), comment="Código de tipo"
|
||||
)
|
||||
ad_valorem: Mapped[Optional[float]] = mapped_column(
|
||||
Numeric(10, 2), comment="Porcentaje ad valorem"
|
||||
)
|
||||
fixed_cost: Mapped[Optional[float]] = mapped_column(
|
||||
Numeric(15, 8), comment="Tasa fija"
|
||||
)
|
||||
unit_of_measure: Mapped[Optional[str]] = mapped_column(
|
||||
String(10), comment="Unidad de medida"
|
||||
)
|
||||
description: Mapped[Optional[str]] = mapped_column(
|
||||
String, comment="Descripción de la fracción"
|
||||
)
|
||||
|
||||
# Timestamps
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
TIMESTAMP(timezone=True), server_default=func.now(), nullable=False
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
TIMESTAMP(timezone=True),
|
||||
server_default=func.now(),
|
||||
onupdate=func.now(),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"<USTariffFraction {self.code}>"
|
||||
@@ -0,0 +1,73 @@
|
||||
"""
|
||||
Endpoints API para fracciones arancelarias americanas
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user
|
||||
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes, validate_access_to_resource
|
||||
|
||||
from .dto import (
|
||||
USTariffFractionCreateDTO,
|
||||
USTariffFractionResponseDTO,
|
||||
USTariffFractionUpdateDTO,
|
||||
)
|
||||
from .service import USTariffFractionService
|
||||
|
||||
# Create base router with generic CRUD routes (disabled list because we'll create a custom one)
|
||||
base_router = TenantCRUDRoutes(
|
||||
service=USTariffFractionService,
|
||||
create_schema=USTariffFractionCreateDTO,
|
||||
update_schema=USTariffFractionUpdateDTO,
|
||||
response_schema=USTariffFractionResponseDTO,
|
||||
prefix="/us-tariff-fractions",
|
||||
tags=["a76 / general catalogs / us tariff fractions"],
|
||||
resource_name="USTariffFraction",
|
||||
id_name="us_tariff_fraction_id",
|
||||
enable_list=False, # Disable default list, we'll add custom one
|
||||
enable_filters=False,
|
||||
default_page_size=50,
|
||||
max_page_size=10000,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/us-tariff-fractions", tags=["a76 / general catalogs / us tariff fractions"])
|
||||
|
||||
# Custom list endpoint with search filter
|
||||
@router.get(
|
||||
"/",
|
||||
response_model=Dict[str, Any],
|
||||
summary="List US Tariff Fractions",
|
||||
description="Get paginated list of US Tariff Fractions with optional search filter",
|
||||
)
|
||||
async def list_us_tariff_fractions(
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
page: int = Query(1, ge=1, description="Page number"),
|
||||
page_size: int = Query(50, ge=1, le=10000, description="Page size"),
|
||||
search: Optional[str] = Query(None, description="Search in code, description, or prefix"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
skip = (page - 1) * page_size
|
||||
filters = {}
|
||||
if search:
|
||||
filters["search"] = search
|
||||
|
||||
items, total = USTariffFractionService.get_all(
|
||||
db, tenant_id, company_id, skip, page_size, filters
|
||||
)
|
||||
|
||||
return {
|
||||
"items": [USTariffFractionResponseDTO.model_validate(item) for item in items],
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"pages": (total + page_size - 1) // page_size,
|
||||
}
|
||||
|
||||
# Include other CRUD routes from base router
|
||||
router.include_router(base_router.router)
|
||||
@@ -0,0 +1,417 @@
|
||||
"""
|
||||
Datos semilla para Fracciones Arancelarias de Estados Unidos
|
||||
|
||||
Catálogo de fracciones HTS/Schedule B para importaciones/exportaciones con EE.UU.
|
||||
Total: 406 registros
|
||||
|
||||
Estructura: (code, prefix, type_code, ad_valorem, fixed_cost, unit_of_measure, description)
|
||||
"""
|
||||
|
||||
seed = [
|
||||
('0902300090', '', 'PO', '0.00', '0.00000000', 'PZA', 'BOLSA DE TE')
|
||||
,('2508400150', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('2520200000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('2525200000', '', 'PO', '0.00', '0.00000000', 'KGS', 'PITMENT BASED ON TITANIUM DIOXIDE')
|
||||
,('2526200000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('2707999090', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('2710121550', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('2710129000', '', 'PO', '0.00', '0.00000000', 'LT', 'MOLD RELEASE')
|
||||
,('2710129050', '', 'PO', '0.00', '0.00000000', 'LT', '')
|
||||
,('2710190650', '', 'PO', '0.00', '0.00000000', 'LT', 'MOLD RELEASE')
|
||||
,('2710199000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('2712902000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('2839905000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('2905120050', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('2905145050', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('2909430000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('2909496000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('2914115000', '', 'PO', '0.00', '0.00000000', 'LT', 'BUTYL ACETATE')
|
||||
,('2914120000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('2915905050', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('2924293600', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('2929108090', '', 'PO', '0.00', '0.00000000', 'KGS', 'Urethane, Iso Side')
|
||||
,('3206110000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3206190000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3206495060', '', 'PO', '0.00', '0.00000000', 'KGS', 'RESIN')
|
||||
,('3206496050', '', 'PO', '0.00', '0.00000000', '', 'PIGMENTO COLORANTE')
|
||||
,('3208100000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3208200000', '', 'PO', '0.00', '0.00000000', 'LT', 'LACA ALQUIDICA NITROCELULOSA')
|
||||
,('3208900000', '', 'PO', '0.00', '0.00000000', '', 'PAINT')
|
||||
,('3209100000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('3209900000', 'S', 'PO', '0.00', '0.00000000', 'LT', 'LIQUID PAINT')
|
||||
,('3212900050', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3214100020', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3215905000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3402205100', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3402905050', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3403195000', '', 'PO', '0.00', '0.00000000', 'LT', 'Mold Release, Stoner, GL')
|
||||
,('3403990000', '', 'PO', '0.00', '0.00000000', '', 'PASTE FOR POLISHING METALS')
|
||||
,('3404905150', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3405400000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3405900000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3506105000', 'US', '', '0.00', '0.00000000', 'PZA', 'ADHESIVO EN AEROSOL')
|
||||
,('3506915000', '', 'PO', '0.00', '0.00000000', 'LT', 'Adhesive')
|
||||
,('3506990000', '', 'PO', '0.00', '0.00000000', 'LT', 'Adhesive')
|
||||
,('3802.20.00.00', '', 'PO', '0.00', '0.00000000', 'KGS', '')
|
||||
,('3811900000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3814005090', '', 'PO', '0.00', '0.00000000', 'LT', 'THINNER')
|
||||
,('3815901000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3815903000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3815905000', '', 'PO', '0.00', '0.00000000', 'PZA', 'CATALYST')
|
||||
,('3820000000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3824910000', '', 'PO', '0.00', '0.00000000', 'LT', 'LIQUIDO PARA PAVONAR METAL A BASE DE ACIDOS')
|
||||
,('3824991900', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3824999297', '', 'PO', '0.00', '0.00000000', 'PZA', 'WELDING ANTI SPLATTER')
|
||||
,('3825900000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3903190000', '', 'PO', '0.00', '0.00000000', 'KGS', '')
|
||||
,('3903905000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3905300000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3906905050', 'US', '', '0.00', '0.00000000', 'PZA', 'POLIMERO ACRILICO')
|
||||
,('3907200000', '', 'PO', '0.00', '0.00000000', 'KGS', 'POLYURETHANE BASED RESIN')
|
||||
,('3907210000', '', 'PO', '0.00', '0.00000000', 'PZA', 'POLYURETHANE-BASED RESIN')
|
||||
,('3907300000', 'US', '', '0.00', '0.00000000', 'KGS', 'RESINA EPOXICA A Y B')
|
||||
,('3907915000', '', 'PO', '0.00', '0.00000000', 'KGS', 'resin')
|
||||
,('3907995050', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3909100000', '', 'PO', '0.00', '0.00000000', 'KGS', 'GRANULATED PLASTIC (PLASTIC RESIN)')
|
||||
,('3909310000', '', 'PO', '0.00', '0.00000000', 'KGS', 'ISOCIANATE')
|
||||
,('3909390000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3909505000', '', 'PO', '0.00', '0.00000000', 'LT', 'POLYURETHANE ELASTOMER PART A AND B')
|
||||
,('3909506000', '', 'PO', '0.00', '0.00000000', 'KGS', '')
|
||||
,('3909900000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3910000000', '', 'PO', '0.00', '0.00000000', 'LT', 'SEALER CONDITIONER PART B')
|
||||
,('3911902500', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3911909050', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3915900090', '', 'PO', '0.00', '0.00000000', 'KGS', 'DESPERDICIO DE PLASTICO')
|
||||
,('3917230000', '', 'PO', '0.00', '0.00000000', 'PZA', 'PLASTIC HOSE')
|
||||
,('3917320050', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('39173299', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('3917330000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('39173399', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('3917390050', '', 'PO', '0.00', '0.00000000', 'PZA', 'PLASTIC TUBE')
|
||||
,('3919101050', 'US', '', '0.00', '0.00000000', 'PZA', 'CINTA ADHESIVA')
|
||||
,('3919102055', 'US', '', '0.00', '0.00000000', 'PZA', 'CINTA ADHESIVA (PLASTICA)')
|
||||
,('3919905060', '', '', '0.00', '0.00000000', 'PZA', 'CINTA ADHESIVA (PLASTICO TRANSPARENTE)')
|
||||
,('3920100000', '', 'PO', '0.00', '0.00000000', 'PZA', 'burbuja')
|
||||
,('3920200055', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3920515000', '', 'PO', '0.00', '0.00000000', 'PZA', 'ACRYLIC POLYMER SHEET')
|
||||
,('3921135000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3921905050', 'S', '', '0.00', '0.00000000', 'PZA', 'PELICULA DE PLASTICO PARA FLEJAR')
|
||||
,('3923109000', '', 'PO', '0.00', '0.00000000', 'PZA', 'CAJA DE PLASTICO')
|
||||
,('3923210095', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3923290000', '', 'PO', '0.00', '0.00000000', 'PZA', 'Bag')
|
||||
,('3923300090', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3923500000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3923900080', 'S', 'PO', '0.00', '0.00000000', 'PZA', 'Plastic container')
|
||||
,('3926209050', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3926400090', 'S', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('3926902100', 'US', '', '0.00', '0.00000000', 'PZA', 'PROTECTORES DE PLASTICO P/OIDO')
|
||||
,('3926903500', 'S', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('3926909985', 'S', 'PO', '0.00', '0.00000000', 'PZA', 'PLASTIC')
|
||||
,('3926909987', 'S', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('3926909989', 'S', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('3926909990', '', 'PO', '0.00', '0.00000000', 'PZA', 'PLACTIC BASE')
|
||||
,('3926909995', 'S', 'PO', '0.00', '0.00000000', '', 'PLASTIC DOME')
|
||||
,('3926909996', '', 'PO', '0.00', '0.00000000', 'PZA', 'Foam Plug, Hosiery Leg')
|
||||
,('4015190002', 'US', '', '0.00', '0.00000000', 'PZA', 'GUANTES DE CAUCHO')
|
||||
,('4016930000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('4016935050', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('4016992000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('4016993510', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('4016996000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('4016996050', '', 'PO', '0.00', '0.00000000', 'PZA', 'BALL')
|
||||
,('4201003000', 'S', 'PO', '0.00', '0.00000000', 'PZA', 'Dog leashes, collars, muzzles, harnesses and similar dog equipment')
|
||||
,('4201006000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('4410190060', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('4415109000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('4415208000', 'S', 'PO', '0.00', '0.00000000', 'PZA', 'WOOD PALLETS')
|
||||
,('4417004000', '', 'PO', '0.00', '0.00000000', '', 'Paint brush and paint roller handles')
|
||||
,('4421904000', '', 'PO', '5.10', '0.00000000', '', 'NECKCAP WOOD')
|
||||
,('4421909750', 'US', '', '0.00', '0.00000000', 'PZA', 'PALILLO DE MADERA')
|
||||
,('4421914000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('4421994000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('4421999880', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('4503106000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('4802693000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('4804394000', '', 'PO', '0.00', '0.00000000', '', 'Wrapping paper')
|
||||
,('4804590000', 'S', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('4805400000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('4808100000', 'S', 'PO', '0.00', '0.00000000', 'PZA', 'CORRUGATED CARTON SEPARATOR')
|
||||
,('4808906000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('4810137040', '', 'PO', '0.00', '0.00000000', 'PZA', 'ROLLO DE CABLE DE ACER')
|
||||
,('4811412100', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('4817100000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('4818200020', 'US', '', '0.00', '0.00000000', 'PZA', 'TOALLAS DESECHABLES DE PAPEL TISSUE')
|
||||
,('4819100040', 'S', 'PO', '0.00', '0.00000000', 'PZA', 'BOX')
|
||||
,('4819504060', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('4821104000', '', 'PO', '0.00', '0.00000000', 'PZA', 'LABEL')
|
||||
,('4821904000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('4822900000', 'S', 'PO', '0.00', '0.00000000', 'PZA', 'CARTON BOX')
|
||||
,('4823700040', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('4823901000', '', 'PO', '0.00', '0.00000000', 'PZA', 'PAPER CUP')
|
||||
,('4823908600', '', 'PO', '0.00', '0.00000000', 'PZA', 'BASES DE CARTON PRENSADO')
|
||||
,('4823908850', 'US', '', '0.00', '0.00000000', 'PZA', 'CINTA ADHESIVA DE PAPEL')
|
||||
,('4901990091', '', 'PO', '0.00', '0.00000000', 'PZA', 'MANUALES')
|
||||
,('4911100080', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('4911998000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('5407619975', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('5508200000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('5703308085', '', 'PO', '0.00', '0.00000000', 'PZA', 'PASTO SINTETICO (MUESTRAS)')
|
||||
,('5806310000', '', 'PO', '0.00', '0.00000000', 'MT', 'CINTA TEXTIL')
|
||||
,('5806322000', '', 'PO', '0.00', '0.00000000', 'MT', 'VELCRO')
|
||||
,('59039001', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('5903903090', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('5911102000', '', 'PO', '0.00', '0.00000000', 'MT', '')
|
||||
,('5911900040', '', 'PO', '0.00', '0.00000000', 'PZA', 'Cords, braids and the like of a kind used in industry as packing or lubricating material')
|
||||
,('5911900080', '', 'PO', '0.00', '0.00000000', 'MT', 'COVER FORM 3/4 FEMALE FOAM LINEN')
|
||||
,('6116100000', 'US', '', '0.00', '0.00000000', 'PZA', 'GUANTES DE ALGOHODON CON RECUBRIMIENTO DE NITRILIO')
|
||||
,('6116920000', 'US', '', '0.00', '0.00000000', 'PZA', 'GUANTES DE ALGOHODON')
|
||||
,('6116929400', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('6306192120', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('6307100000', 'US', '', '0.00', '0.00000000', 'PZA', 'FIBRA SINTETICA P/LIMPIAR')
|
||||
,('6307909089', 'S', 'PO', '0.00', '0.00000000', '', 'BAG')
|
||||
,('6307909889', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('6307909891', '', 'PO', '0.00', '0.00000000', 'PZA', 'bolsa de tela')
|
||||
,('6307909995', 'US', '', '0.00', '0.00000000', 'PZA', 'MASCARILLA DE PROTECCION CONTRA EL POLVO')
|
||||
,('6403919015', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('6403999031', '', 'PO', '10.00', '0.00000000', 'PAR', 'LEATHER SHOES')
|
||||
,('6406109090', '', 'PO', '0.00', '0.00000000', 'PZA', 'HUARACHE')
|
||||
,('6506106075', '', 'PO', '0.00', '0.00000000', 'PZA', 'PROTECTION HELMET')
|
||||
,('6804220000', 'US', '', '0.00', '0.00000000', 'PZA', 'DISCO ABRASIVO CIRCULARES')
|
||||
,('6805100000', 'US', '', '0.00', '0.00000000', 'PZA', 'DISCO ABRASIVO EN ROLLO')
|
||||
,('6805100199', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('6805300000', 'US', '', '0.00', '0.00000000', 'PZA', 'LIJA CON SOPORTE DE PLASTICO CELULAR')
|
||||
,('6805305000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('7007190000', '', 'PO', '0.00', '0.00000000', 'PZA', 'TEMPERED GLASS BASE')
|
||||
,('7009921000', '', 'PO', '0.00', '0.00000000', 'PZA', 'ESPEJO ENMARCADO')
|
||||
,('7010905055', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('7019905050', '', 'PO', '0.00', '0.00000000', '', 'FIBRA DE VIDRIO')
|
||||
,('7019905150', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('7020006000', '', 'PO', '0.00', '0.00000000', 'PZA', 'BASE DE VIDRIO')
|
||||
,('7020009990', '', 'PO', '0.00', '0.00000000', '', 'GLASS BOARD')
|
||||
,('7206900000', '', 'PO', '0.00', '0.00000000', 'PZA', 'TUBO')
|
||||
,('7215100080', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('7215905000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('7226928050', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('7228400000', '', 'PO', '0.00', '0.00000000', 'PZA', 'METAL BAR')
|
||||
,('7228608000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('7304598080', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('7306200000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('7306305090', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('7306905000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('7307225000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('7307929000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('7308909590', '', 'PO', '0.00', '0.00000000', 'PZA', 'RACK')
|
||||
,('7309000090', '', 'PO', '0.00', '0.00000000', 'PZA', 'TANQUE HERMETICO DE ACERO')
|
||||
,('7310100050', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('7310290050', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('7312109090', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('7315827000', '', 'PO', '0.00', '0.00000000', '', 'CHAIN')
|
||||
,('7317007500', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('7318130060', '', 'PO', '0.00', '0.00000000', '', 'EYE HOOK')
|
||||
,('73181504', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('7318150400', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('7318152000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('7318158066', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('7318158085', '', '', '0.00', '0.00000000', 'PZA', 'TORNILLO DE ACERO CON TUERCA')
|
||||
,('7318158688', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('7318159000', 'S', 'PO', '0.00', '0.00000000', 'PZA', 'TORNILLO DE ACERO MARIPOSA')
|
||||
,('7318160085', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('7318190000', '', 'PO', '0.00', '0.00000000', 'PZA', 'REMACHE')
|
||||
,('7318210090', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('7318220000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('7318230000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('7318240000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('7318290000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('7319909000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('7320205060', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('7321811000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('7325995000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('7326901000', '', 'PO', '0.00', '0.00000000', '', 'HOOK')
|
||||
,('7326908605', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('7326908688', 'S', 'PO', '0.00', '0.00000000', 'PZA', 'METAL INSERTS')
|
||||
,('7326908695', '', 'PO', '0.00', '0.00000000', 'PZA', 'STEEL FASTENER')
|
||||
,('73269099', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('7412200090', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('74122001', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('7415100000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('7415390000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('7419995050', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('7607196000', '', 'PO', '0.00', '0.00000000', 'PZA', 'aluminium')
|
||||
,('7609000000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('7616109090', '', 'PO', '0.00', '0.00000000', 'PZA', 'tachuela')
|
||||
,('7616995090', 'S', 'PO', '0.00', '0.00000000', '', 'SOPORTE DE ALUMINIO')
|
||||
,('7616995190', 'S', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('7618000000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('7806008000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('7907006000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('8021230000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8202200060', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8203109000', '', 'PO', '0.00', '0.00000000', 'PZA', 'ESTUCHE DE LIMAS')
|
||||
,('8203208000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('82032099', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8204110060', '', 'PO', '0.00', '0.00000000', '', 'WRENCH')
|
||||
,('8205511500', '', 'PO', '0.00', '0.00000000', '', 'CEPILLO DE ALAMBRE')
|
||||
,('8205599000', 'US', '', '0.00', '0.00000000', 'PZA', 'DESPACHADOR DE CINTA MANUAL')
|
||||
,('8205700060', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8205700090', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('8205906000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('8207502055', '', 'PO', '0.00', '0.00000000', '', 'DRILL BIT')
|
||||
,('8207907585', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('8208906000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8302200000', '', 'PO', '0.00', '0.00000000', '', 'RUEDAS PARA BASE DE MANIQUI')
|
||||
,('8302426000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8302498090', 'S', 'PO', '0.00', '0.00000000', 'PZA', 'Kit, Knob,Hand')
|
||||
,('8302500000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8309900000', '', 'PO', '0.00', '0.00000000', 'PZA', 'TAPA METALICA')
|
||||
,('8309900090', 'S', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8310000000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('8413600090', '', 'PO', '0.00', '0.00000000', 'PZA', 'PUMPS OF MACHINE MOLDING')
|
||||
,('8413919060', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8413919080', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8413919096', '', 'PO', '0.00', '0.00000000', 'PZA', 'DIAPHRAGM FOR PUMP')
|
||||
,('8414.59.6595', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('8414100000', '', 'PO', '0.00', '0.00000000', 'PZA', 'VACUM PUMP')
|
||||
,('8414510090', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('8414519090', '', 'PO', '0.00', '0.00000000', 'PZA', 'FAN')
|
||||
,('8414596595', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8414809000', '', 'PO', '0.00', '0.00000000', 'PZA', 'DUST COLLECTOR')
|
||||
,('8414909080', '', 'PO', '0.00', '0.00000000', 'PZA', 'DISPOSITIVO DE FILTRACCION PARA BOMBA NEUMATICA')
|
||||
,('8419390180', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('8421210000', '', 'PO', '0.00', '0.00000000', 'PZA', 'FEEDER-DISASSEMBLY DISASSEMBLED')
|
||||
,('8421230000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8421290065', '', 'PO', '0.00', '0.00000000', 'PZA', 'FILTER')
|
||||
,('8421390115', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8421390190', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8421398015', '', 'PO', '0.00', '0.00000000', 'PZA', 'FILTER')
|
||||
,('8421398040', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8421398090', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('8421990180', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8422309191', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8422401190', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('8422409180', '', 'PO', '0.00', '0.00000000', 'PZA', 'STRAPPING MACHINE')
|
||||
,('8424209000', '', 'PO', '0.00', '0.00000000', 'PZA', 'PISTOLA AEROGRAFICA')
|
||||
,('8424890000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8424900100', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8424900500', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('8424902000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8424909080', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8425110000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8427108095', '', 'PO', '0.00', '0.00000000', 'PZA', 'Pallet jack')
|
||||
,('8427900000', '', 'PO', '0.00', '0.00000000', 'PZA', 'PALLET JACK')
|
||||
,('8456111050', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8459290090', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8461500090', '', 'PO', '0.00', '0.00000000', 'PZA', 'BELT SAW')
|
||||
,('8461508090', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8465910091', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8465930012', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('8466306085', '', 'PO', '0.00', '0.00000000', 'PZA', 'CILINDRO NEUMATICO')
|
||||
,('8466925090', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8467.19.5090', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('8467111080', 'US', '', '0.00', '0.00000000', 'PZA', 'PULIDOR NEUMATICO MANUAL')
|
||||
,('8467195090', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('8467210070', '', 'PO', '0.00', '0.00000000', '', 'ROTARY')
|
||||
,('8467220090', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8467290090', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('8467895090', '', 'PO', '0.00', '0.00000000', 'PZA', 'Pneumatic screwdriver')
|
||||
,('8467920050', '', 'PO', '0.00', '0.00000000', '', 'pulidor')
|
||||
,('8467920090', 'US', '', '0.00', '0.00000000', 'PZA', 'DISCO DE URETANO P/ PULIDOR NEUMATICO')
|
||||
,('8471410150', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('8471500150', '', 'PO', '0.00', '0.00000000', '', 'CPU')
|
||||
,('8471608000', '', 'PO', '0.00', '0.00000000', '', 'SCANNER')
|
||||
,('8471609050', '', 'PO', '0.00', '0.00000000', 'PZA', 'SCANNER')
|
||||
,('8477590100', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8477800000', '', 'PO', '0.00', '0.00000000', 'PZA', 'MACHINE FOR FILLING AIR BAGS')
|
||||
,('8477800100', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8477900000', '', 'PO', '0.00', '0.00000000', 'PZA', 'Presses setup the mold during blowing process')
|
||||
,('8477902580', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8477908595', '', 'PO', '0.00', '0.00000000', 'PZA', 'DISC')
|
||||
,('8477908695', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8479820040', '', 'PO', '0.00', '0.00000000', '', 'ROTARY MACHINE')
|
||||
,('8479820080', '', 'PO', '0.00', '0.00000000', 'PZA', 'APARATO AGITADOR')
|
||||
,('8479830100', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8479899199', '', 'PO', '0.00', '0.00000000', 'PZA', 'PORTABLE DOCK PLATE')
|
||||
,('8479899499', '', 'PO', '0.00', '0.00000000', 'PZA', 'MAQUINA APLICADORA')
|
||||
,('8479899599', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8479899797', '', 'PO', '0.00', '0.00000000', 'PZA', 'COLECTOR DE POLVO')
|
||||
,('8479909496', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('84799099', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8480718045', 'S', 'PO', '0.00', '0.00000000', 'PZA', 'MOLDE')
|
||||
,('8480718060', '', 'PO', '0.00', '0.00000000', 'PZA', 'TIG TORCH')
|
||||
,('8480719090', '', 'PO', '0.00', '0.00000000', 'PZA', 'Semi-finished mold')
|
||||
,('8481100090', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('8481200080', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('84812099', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8481400000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('8481809020', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('8481809050', '', 'PO', '0.00', '0.00000000', 'PZA', 'NOZZLE, CONE DUT')
|
||||
,('8481909085', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8483308090', '', 'PO', '0.00', '0.00000000', 'PZA', 'BUJES')
|
||||
,('8484200000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8484900000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8501106080', '', 'PO', '0.00', '0.00000000', 'PZA', 'MOTOR ELECTRICO INCLUYE ACCESORIOS')
|
||||
,('8504404000', '', 'PO', '0.00', '0.00000000', 'PZA', 'CONTROLADOR DE VELOCIDAD')
|
||||
,('8504409580', '', 'PO', '0.00', '0.00000000', 'PZA', 'BATTERY CHARGER')
|
||||
,('8505110090', '', 'PO', '0.00', '0.00000000', 'PZA', 'INSERT')
|
||||
,('8505200000', '', 'PO', '0.00', '0.00000000', 'PZA', 'BARRIER')
|
||||
,('8507208091', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('8512902000', 'S', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8514908000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8515390040', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8515800080', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('85160808000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('8516808000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('8517620010', '', 'PO', '0.00', '0.00000000', '', 'WIRELESS ACCESS POINT')
|
||||
,('8523520010', '', 'PO', '0.00', '0.00000000', 'PZA', 'RF Label')
|
||||
,('8525805050', '', 'PO', '0.00', '0.00000000', 'PZA', 'CAMARA DE CIRCUITO CERRADO')
|
||||
,('8533408070', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8536100040', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8536490050', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('8536507000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8536908585', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8537103000', 'S', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8537109070', '', 'PO', '0.00', '0.00000000', '', 'DIGITAL PANEL CONTROL')
|
||||
,('8543908885', 'S', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8544190000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('8544429090', '', 'PO', '0.00', '0.00000000', 'PZA', 'SENSOR CABLE')
|
||||
,('8547200000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('8713100000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('8716805090', '', 'PO', '0.00', '0.00000000', 'PZA', 'UTILITY CART')
|
||||
,('9018390050', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('9023000000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('9024800000', '', 'PO', '0.00', '0.00000000', '', 'TESTING MACHINE')
|
||||
,('9026102010', '', 'PO', '0.00', '0.00000000', '', 'DIGITAL VACUUM GAUGE')
|
||||
,('9026106000', '', 'PO', '0.00', '0.00000000', 'PZA', 'FLOAT & GAUGE')
|
||||
,('9026204000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('9026208000', '', 'PO', '0.00', '0.00000000', 'PZA', 'REGULATOR')
|
||||
,('9027304040', '', 'PO', '0.00', '0.00000000', 'PZA', 'SPECTROPHOTOMETER')
|
||||
,('9031.80.8085', '', 'PO', '1.70', '0.00000000', '', 'FIXTURA')
|
||||
,('9031808085', '', 'PO', '1.70', '2.25000000', 'PZA', 'APARATO')
|
||||
,('9031907000', '', 'PO', '0.00', '0.00000000', 'PZA', 'FIXTURE')
|
||||
,('9032200000', '', 'PO', '0.00', '0.00000000', 'PZA', 'PRESOSTATO')
|
||||
,('9106100000', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('9403100040', '', 'PO', '0.00', '0.00000000', 'PZA', 'MESA DE METAL CON BASE DE MADERA')
|
||||
,('9403200030', '', 'PO', '0.00', '0.00000000', 'PZA', 'ESTANTE DE ACERO PARA TOTE')
|
||||
,('9506310000', '', 'PO', '0.00', '0.00000000', '', 'GOLF CLUB')
|
||||
,('9506320000', '', 'PO', '0.00', '0.00000000', 'PZA', 'BALL')
|
||||
,('9506620000', '', 'PO', '0.00', '0.00000000', 'PZA', 'PELOTA')
|
||||
,('9506628060', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('9506996080', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('9603404050', 'US', '', '0.00', '0.00000000', 'PZA', 'BROCHAS CON MANGO DE PLASTICO Y CERDAS DE FIBRA SINTETICA')
|
||||
,('9604000000', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('9607190060', '', 'PO', '0.00', '0.00000000', '', '')
|
||||
,('9612101020', '', 'PO', '0.00', '0.00000000', 'PZA', 'RIBBON')
|
||||
,('9612109090', '', 'PO', '0.00', '0.00000000', 'PZA', 'RIBBON')
|
||||
,('9618000000', 'S', 'PO', '0.00', '79.00000000', 'PZA', 'MANIQUIES')
|
||||
,('9618009900', '', 'PO', '0.00', '0.00000000', 'PZA', '')
|
||||
,('9801001095', '', 'PO', '0.00', '4.35000000', '', 'RESINA')
|
||||
,('9801001097', 'S', 'PO', '0.00', '4.35000000', 'PZA', '')
|
||||
,('9801001098', '', 'PO', '0.00', '0.00000000', 'PZA', 'US RETURNS')
|
||||
,('9801002500', '', 'PO', '0.00', '0.00000000', 'PZA', 'CHINA ARTICLES')
|
||||
,('9802004040', 'US', 'PO', '0.00', '0.00000000', 'PZA', 'MANIQUIS REPARADOS')
|
||||
,('9802005060', '', 'PO', '0.00', '0.00000000', '', 'Repaired in Mexico')
|
||||
,('9861661098', 'M6363', 'PO', '0.00', '8.10000000', 'PZA', '')
|
||||
,('MX4415109000', '', 'PO', '0.00', '0.00000000', 'PZA', 'CAJON DE MADERA')
|
||||
,('MX4820400000', '', 'PO', '0.00', '0.50000000', '', 'FORMULARIOS DE PAPEL')
|
||||
,('MX7326909980', '', 'PO', '0.00', '0.00000000', '', 'STEEL PANEL')
|
||||
,('MX8309900000', '', 'PO', '0.00', '0.00000000', 'PZA', 'CAP')
|
||||
]
|
||||
@@ -0,0 +1,129 @@
|
||||
"""
|
||||
Service para fracciones arancelarias americanas
|
||||
"""
|
||||
|
||||
from typing import List, Optional, Tuple, Dict, Any
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from fastapi import HTTPException
|
||||
import logging
|
||||
|
||||
from .models import USTariffFraction
|
||||
from .dto import USTariffFractionCreateDTO, USTariffFractionUpdateDTO
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class USTariffFractionService:
|
||||
"""Service para gestionar fracciones arancelarias americanas"""
|
||||
|
||||
@staticmethod
|
||||
def get_all(
|
||||
db: Session,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
) -> Tuple[List[USTariffFraction], int]:
|
||||
"""Obtiene todas las fracciones arancelarias americanas con filtros opcionales"""
|
||||
|
||||
query = db.query(USTariffFraction).filter(
|
||||
USTariffFraction.tenant_id == tenant_id,
|
||||
USTariffFraction.company_id == company_id,
|
||||
)
|
||||
|
||||
# Aplicar filtros
|
||||
if filters:
|
||||
# Si hay un filtro 'search', buscar en múltiples campos
|
||||
if filters.get("search"):
|
||||
search_term = f"%{filters['search']}%"
|
||||
query = query.filter(
|
||||
USTariffFraction.code.ilike(search_term) |
|
||||
USTariffFraction.description.ilike(search_term) |
|
||||
USTariffFraction.prefix.ilike(search_term)
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
items = query.order_by(USTariffFraction.code).offset(skip).limit(limit).all()
|
||||
|
||||
return items, total
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(
|
||||
db: Session, tenant_id: int, company_id: int, fraction_id: int
|
||||
) -> Optional[USTariffFraction]:
|
||||
"""Obtiene una fracción arancelaria americana por ID"""
|
||||
return (
|
||||
db.query(USTariffFraction)
|
||||
.filter(
|
||||
USTariffFraction.id == fraction_id,
|
||||
USTariffFraction.tenant_id == tenant_id,
|
||||
USTariffFraction.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
db: Session,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
fraction_data: USTariffFractionCreateDTO,
|
||||
) -> USTariffFraction:
|
||||
"""Crea una nueva fracción arancelaria americana"""
|
||||
try:
|
||||
db_fraction = USTariffFraction(
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
**fraction_data.model_dump(),
|
||||
)
|
||||
db.add(db_fraction)
|
||||
db.commit()
|
||||
db.refresh(db_fraction)
|
||||
return db_fraction
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error creando fracción americana: {e}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Ya existe una fracción americana con este código",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def update(
|
||||
db: Session,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
fraction_id: int,
|
||||
fraction_data: USTariffFractionUpdateDTO,
|
||||
) -> Optional[USTariffFraction]:
|
||||
"""Actualiza una fracción arancelaria americana existente"""
|
||||
db_fraction = USTariffFractionService.get_by_id(
|
||||
db, tenant_id, company_id, fraction_id
|
||||
)
|
||||
if not db_fraction:
|
||||
return None
|
||||
|
||||
update_data = fraction_data.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(db_fraction, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_fraction)
|
||||
return db_fraction
|
||||
|
||||
@staticmethod
|
||||
def delete(
|
||||
db: Session, tenant_id: int, company_id: int, fraction_id: int
|
||||
) -> bool:
|
||||
"""Elimina una fracción arancelaria americana"""
|
||||
db_fraction = USTariffFractionService.get_by_id(
|
||||
db, tenant_id, company_id, fraction_id
|
||||
)
|
||||
if not db_fraction:
|
||||
return False
|
||||
|
||||
db.delete(db_fraction)
|
||||
db.commit()
|
||||
return True
|
||||
@@ -0,0 +1,28 @@
|
||||
from core.exceptions import ErrorCollector
|
||||
from .. import models
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
|
||||
def invoice_exists(
|
||||
db: Session,
|
||||
invoice_number: str,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
errors: ErrorCollector
|
||||
) -> bool:
|
||||
invoice_exists = (
|
||||
db.query(models.InvoiceHeader.id)
|
||||
.filter(
|
||||
models.InvoiceHeader.invoice_number == invoice_number,
|
||||
models.InvoiceHeader.tenant_id == tenant_id,
|
||||
models.InvoiceHeader.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if invoice_exists:
|
||||
errors.add_duplicate_error(
|
||||
"invoice_number",
|
||||
invoice_number,
|
||||
f"Ya existe una factura con el número '{invoice_number}'",
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
15
backend/api/v1/modules/a76/invoices/common/mappers.py
Normal file
15
backend/api/v1/modules/a76/invoices/common/mappers.py
Normal file
@@ -0,0 +1,15 @@
|
||||
""" """
|
||||
|
||||
def clean_dict(data_dict: dict) -> dict:
|
||||
cleaned = {}
|
||||
for key, value in data_dict.items():
|
||||
|
||||
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
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
from sqlalchemy.orm import Session
|
||||
from .... import schemas
|
||||
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
|
||||
from api.v1.modules.a76.general_catalogs.exchange_rate.models import ExchangeRate
|
||||
from api.v1.modules.a76.clients_and_providers.models import ClientProvider
|
||||
from api.v1.modules.public.reference_data.incoterms.models import Incoterm
|
||||
from api.v1.modules.a76.items.models import Item
|
||||
from ....models import TransportType, Currency, WeightUnit
|
||||
from core.exceptions import ErrorCollector
|
||||
|
||||
|
||||
def validate_common(
|
||||
db: Session,
|
||||
invoice: schemas.InvoiceHeaderCreate,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
errors: ErrorCollector,
|
||||
):
|
||||
if invoice.compliance_mx.pedimento_id:
|
||||
pedimento = (
|
||||
db.query(Pedimentos)
|
||||
.filter(
|
||||
Pedimentos.id == invoice.compliance_mx.pedimento_id,
|
||||
Pedimentos.tenant_id == tenant_id,
|
||||
Pedimentos.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not pedimento:
|
||||
errors.add_error(
|
||||
field="compliance_mx.pedimento_id",
|
||||
message="El Pedimento no existe en el Catálogo de Pedimentos.",
|
||||
solution=["Verifica el ID", "Revisa el catálogo"],
|
||||
code="NOT_FOUND",
|
||||
value=invoice.compliance_mx.pedimento_id,
|
||||
)
|
||||
|
||||
if not invoice.compliance_mx.is_regime_change:
|
||||
if not pedimento.operation_type == 1:
|
||||
errors.add_error(
|
||||
field="compliance_mx.pedimento_id",
|
||||
message="El Pedimento seleccionado no corresponde a una Importación.",
|
||||
solution=["Selecciona un Pedimento de Importación"],
|
||||
code="INVALID_OPERATION_TYPE",
|
||||
value=pedimento.operation_type,
|
||||
)
|
||||
else:
|
||||
if pedimento.regime in ["EXD", "ETE", "ETR"]:
|
||||
errors.add_error(
|
||||
field="compliance_mx.pedimento_id",
|
||||
message="El Pedimento seleccionado corresponde a una Exportación, no a una Importación.",
|
||||
solution=["Selecciona un Pedimento de Importación"],
|
||||
code="INVALID_REGIME",
|
||||
value=pedimento.regime,
|
||||
)
|
||||
else:
|
||||
if invoice.document_type.upper().strip() != pedimento.regime:
|
||||
errors.add_error(
|
||||
field="document_type",
|
||||
message=f"El Tipo de Documento '{invoice.document_type}' no coincide con el Régimen '{pedimento.regime}' del Pedimento seleccionado.",
|
||||
solution=[
|
||||
"Ajusta el Tipo de Documento o selecciona otro Pedimento"
|
||||
],
|
||||
code="REGIME_MISMATCH",
|
||||
value=invoice.document_type,
|
||||
)
|
||||
else:
|
||||
if pedimento.operation_type != 2:
|
||||
errors.add_error(
|
||||
field="compliance_mx.pedimento_id",
|
||||
message="El Pedimento seleccionado no corresponde a una Importacion Definitiva.",
|
||||
solution=["Selecciona un Pedimento de Importacion Definitiva"],
|
||||
code="INVALID_OPERATION_TYPE",
|
||||
value=pedimento.operation_type,
|
||||
)
|
||||
else:
|
||||
if pedimento.regime != "IMD":
|
||||
errors.add_error(
|
||||
field="compliance_mx.pedimento_id",
|
||||
message=f"El Pedimento {pedimento.customs_office}-{pedimento.license}-{pedimento.pedimento_number} no corresponde a una Importacion Definitiva.",
|
||||
solution=["Selecciona un Pedimento de Importacion Definitiva"],
|
||||
code="INVALID_REGIME",
|
||||
value=pedimento.regime,
|
||||
)
|
||||
else:
|
||||
if invoice.document_type.upper().strip() != pedimento.regime:
|
||||
errors.add_error(
|
||||
field="document_type",
|
||||
message=f"El Tipo de Documento '{invoice.document_type}' no coincide con el Régimen '{pedimento.regime}' del Pedimento seleccionado.",
|
||||
solution=[
|
||||
"Ajusta el Tipo de Documento o selecciona otro Pedimento"
|
||||
],
|
||||
code="REGIME_MISMATCH",
|
||||
value=invoice.document_type,
|
||||
)
|
||||
else:
|
||||
if pedimento.pedimento_code not in ["A1", "A3"]:
|
||||
errors.add_error(
|
||||
field="compliance_mx.pedimento_id",
|
||||
message=f"El Pedimento seleccionado no es de tipo A1 o A3 requerido para Cambio de Régimen.",
|
||||
solution=["Selecciona un Pedimento de tipo A1 o A3"],
|
||||
code="INVALID_PEDEMENTO_CODE",
|
||||
value=pedimento.pedimento_code,
|
||||
)
|
||||
|
||||
if pedimento.pedimento_type == "consolidated":
|
||||
if (
|
||||
invoice.invoice_date < pedimento.pedimento_dates.entry_date
|
||||
or invoice.invoice_date > pedimento.pedimento_dates.end_date
|
||||
):
|
||||
errors.add_error(
|
||||
field="invoice_date",
|
||||
message=f"La Fecha de la Factura {invoice.invoice_date} no está dentro del rango de fechas del Pedimento {pedimento.customs_office}-{pedimento.license}-{pedimento.pedimento_number}.",
|
||||
solution=[
|
||||
f"Capturar una Fecha de Factura, entre la Fecha de Inicio: {pedimento.pedimento_dates.entry_date} y la Fecha Final: {pedimento.pedimento_dates.end_date} ."
|
||||
],
|
||||
code="DATE_OUT_OF_RANGE",
|
||||
value=invoice.invoice_date,
|
||||
)
|
||||
|
||||
if not invoice.compliance_mx.remesa:
|
||||
errors.add_error(
|
||||
field="compliance_mx.remesa",
|
||||
message="El campo Remesa es obligatorio cuando se asocia un Pedimento.",
|
||||
solution=["Proporciona un valor para Remesa"],
|
||||
code="REQUIRED_FIELD",
|
||||
value=invoice.compliance_mx.remesa,
|
||||
)
|
||||
elif invoice.compliance_mx.remesa == 0:
|
||||
errors.add_error(
|
||||
field="compliance_mx.remesa",
|
||||
message="El campo Remesa no puede ser cero cuando se asocia un Pedimento.",
|
||||
solution=["Proporciona un valor válido para Remesa"],
|
||||
code="INVALID_VALUE",
|
||||
value=invoice.compliance_mx.remesa,
|
||||
)
|
||||
|
||||
duplicated_remesa = (
|
||||
db.query(Pedimentos)
|
||||
.filter(
|
||||
Pedimentos.remesa == invoice.compliance_mx.remesa,
|
||||
Pedimentos.id != invoice.compliance_mx.pedimento_id,
|
||||
Pedimentos.tenant_id == tenant_id,
|
||||
Pedimentos.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if duplicated_remesa:
|
||||
errors.add_error(
|
||||
field="compliance_mx.remesa",
|
||||
message="El valor de Remesa ya está asociado a otro Pedimento.",
|
||||
solution=["Proporciona un valor único para Remesa"],
|
||||
code="DUPLICATE_VALUE",
|
||||
value=invoice.compliance_mx.remesa,
|
||||
)
|
||||
else:
|
||||
if invoice.compliance_mx.remesa and not invoice.compliance_mx.pedimento_id:
|
||||
errors.add_error(
|
||||
field="compliance_mx.pedimento_id",
|
||||
message="El campo Pedimento es obligatorio cuando se proporciona Remesa.",
|
||||
solution=["Proporciona un ID de Pedimento"],
|
||||
code="REQUIRED_FIELD",
|
||||
value=invoice.compliance_mx.pedimento_id,
|
||||
)
|
||||
|
||||
if len(invoice.invoice_number) > 100:
|
||||
errors.add_error(
|
||||
field="invoice_number",
|
||||
message="El número de factura excede la longitud máxima de 100 caracteres.",
|
||||
solution=["Acorta el número de factura a 100 caracteres o menos"],
|
||||
code="MAX_LENGTH_EXCEEDED",
|
||||
value=invoice.invoice_number,
|
||||
)
|
||||
|
||||
if not invoice.financials.exchange_rate or invoice.financials.exchange_rate <= 0:
|
||||
exchange_rate_exists = (
|
||||
db.query(ExchangeRate)
|
||||
.filter(
|
||||
ExchangeRate.date == invoice.invoice_date,
|
||||
ExchangeRate.tenant_id == tenant_id,
|
||||
ExchangeRate.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not exchange_rate_exists:
|
||||
errors.add_error(
|
||||
field="financials.exchange_rate",
|
||||
message=f"No existe un Tipo de Cambio registrado para la fecha {invoice.invoice_date.date()}.",
|
||||
solution=["Registra el Tipo de Cambio en el catálogo correspondiente"],
|
||||
code="EXCHANGE_RATE_NOT_FOUND",
|
||||
value=invoice.financials.exchange_rate,
|
||||
)
|
||||
|
||||
if invoice.compliance_mx.is_regime_change:
|
||||
if invoice.document_type in ["EXD", "ETE", "ETR"]:
|
||||
errors.add_error(
|
||||
field="document_type",
|
||||
message="El Tipo de Documento no puede ser de Exportación cuando se trata de un Cambio de Régimen.",
|
||||
solution=[
|
||||
"Selecciona un Tipo de Documento válido para Cambio de Régimen"
|
||||
],
|
||||
code="INVALID_DOCUMENT_TYPE",
|
||||
value=invoice.document_type,
|
||||
)
|
||||
else:
|
||||
if invoice.document_type == "IMD":
|
||||
errors.add_error(
|
||||
field="document_type",
|
||||
message="El Tipo de Documento no puede ser 'IMD' a menos que sea un Cambio de Régimen.",
|
||||
solution=["Selecciona un Tipo de Documento válido"],
|
||||
code="INVALID_DOCUMENT_TYPE",
|
||||
value=invoice.document_type,
|
||||
)
|
||||
|
||||
provider_exists = (
|
||||
db.query(ClientProvider)
|
||||
.filter(
|
||||
ClientProvider.id == invoice.provider_id,
|
||||
ClientProvider.tenant_id == tenant_id,
|
||||
ClientProvider.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not provider_exists:
|
||||
errors.add_error(
|
||||
field="provider_id",
|
||||
message="El Proveedor no existe en el Catálogo de Clientes y Proveedores.",
|
||||
solution=["Verifica el ID del Proveedor", "Revisa el catálogo"],
|
||||
code="NOT_FOUND",
|
||||
value=invoice.provider_id,
|
||||
)
|
||||
|
||||
selled_to_exists = (
|
||||
db.query(ClientProvider)
|
||||
.filter(
|
||||
ClientProvider.id == invoice.selled_to_id,
|
||||
ClientProvider.tenant_id == tenant_id,
|
||||
ClientProvider.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not selled_to_exists:
|
||||
errors.add_error(
|
||||
field="selled_to_id",
|
||||
message="El Cliente no existe en el Catálogo de Clientes y Proveedores.",
|
||||
solution=["Verifica el ID del Cliente", "Revisa el catálogo"],
|
||||
code="NOT_FOUND",
|
||||
value=invoice.selled_to_id,
|
||||
)
|
||||
|
||||
shipped_to_exists = (
|
||||
db.query(ClientProvider)
|
||||
.filter(
|
||||
ClientProvider.id == invoice.shipped_to_id,
|
||||
ClientProvider.tenant_id == tenant_id,
|
||||
ClientProvider.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not shipped_to_exists:
|
||||
errors.add_error(
|
||||
field="shipped_to_id",
|
||||
message="El Destinatario no existe en el Catálogo de Clientes y Proveedores.",
|
||||
solution=["Verifica el ID del Destinatario", "Revisa el catálogo"],
|
||||
code="NOT_FOUND",
|
||||
value=invoice.shipped_to_id,
|
||||
)
|
||||
|
||||
customs_broker_exists = (
|
||||
db.query(ClientProvider)
|
||||
.filter(
|
||||
ClientProvider.id == invoice.customs_broker_id,
|
||||
ClientProvider.tenant_id == tenant_id,
|
||||
ClientProvider.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not customs_broker_exists:
|
||||
errors.add_error(
|
||||
field="customs_broker_id",
|
||||
message="El Agente Aduanal no existe en el Catálogo de Clientes y Proveedores.",
|
||||
solution=["Verifica el ID del Agente Aduanal", "Revisa el catálogo"],
|
||||
code="NOT_FOUND",
|
||||
value=invoice.customs_broker_id,
|
||||
)
|
||||
|
||||
if invoice.logistics.carrier_id:
|
||||
carrier_exists = (
|
||||
db.query(ClientProvider)
|
||||
.filter(
|
||||
ClientProvider.id == invoice.logistics.carrier_id,
|
||||
ClientProvider.tenant_id == tenant_id,
|
||||
ClientProvider.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not carrier_exists:
|
||||
errors.add_error(
|
||||
field="logistics.carrier_id",
|
||||
message="El Transportista no existe en el Catálogo de Clientes y Proveedores.",
|
||||
solution=["Verifica el ID del Transportista", "Revisa el catálogo"],
|
||||
code="NOT_FOUND",
|
||||
value=invoice.logistics.carrier_id,
|
||||
)
|
||||
|
||||
if invoice.logistics.transport_type not in [t.value for t in TransportType]:
|
||||
errors.add_error(
|
||||
field="logistics.transport_type",
|
||||
message="El Tipo de Transporte proporcionado no es válido.",
|
||||
solution=[
|
||||
f"Selecciona un Tipo de Transporte válido: {[t.value for t in TransportType]}"
|
||||
],
|
||||
code="INVALID_TRANSPORT_TYPE",
|
||||
value=invoice.logistics.transport_type,
|
||||
)
|
||||
else:
|
||||
if invoice.logistics.transport_type == "none" and invoice.logistics.transport_num:
|
||||
errors.add_error(
|
||||
field="logistics.transport_num",
|
||||
message="El Número de Transporte no debe proporcionarse cuando el Tipo de Transporte es 'none'.",
|
||||
solution=["Elimina el Número de Transporte o selecciona un Tipo de Transporte válido"],
|
||||
code="INVALID_VALUE",
|
||||
value=invoice.logistics.transport_num,
|
||||
)
|
||||
else:
|
||||
if not invoice.logistics.transport_num and invoice.logistics.transport_type != "none":
|
||||
errors.add_error(
|
||||
field="logistics.transport_num",
|
||||
message="El Número de Transporte es obligatorio cuando se proporciona un Tipo de Transporte distinto de 'none'.",
|
||||
solution=["Proporciona un Número de Transporte válido"],
|
||||
code="REQUIRED_FIELD",
|
||||
value=invoice.logistics.transport_num,
|
||||
)
|
||||
|
||||
|
||||
invoice.financials.currency = (invoice.financials.currency or "foreign")
|
||||
|
||||
if invoice.financials.currency not in [c.value for c in Currency]:
|
||||
errors.add_error(
|
||||
field="financials.currency",
|
||||
message="La Moneda proporcionada no es válida.",
|
||||
solution=[
|
||||
f"Selecciona una Moneda válida: {[c.value for c in Currency]}"
|
||||
],
|
||||
code="INVALID_CURRENCY",
|
||||
value=invoice.financials.currency,
|
||||
)
|
||||
else:
|
||||
has_items = db.query(Item).filter(
|
||||
Item.invoice_id == invoice.id,
|
||||
Item.tenant_id == tenant_id,
|
||||
Item.company_id == company_id,
|
||||
).first()
|
||||
if has_items:
|
||||
errors.add_error(
|
||||
field="items",
|
||||
message=f"La opcion tipo de moneda {invoice.financials.currency} no puede ser modificada ya que la factura tiene items asociados.",
|
||||
solution=["Verifica la moneda de los items asociados a la factura."],
|
||||
code="CURRENCY_CANNOT_BE_CHANGED",
|
||||
value=invoice.financials.currency,
|
||||
)
|
||||
|
||||
if invoice.logistics.incoterms:
|
||||
incoterm_exists = (
|
||||
db.query(Incoterm)
|
||||
.filter(
|
||||
Incoterm.code == invoice.logistics.incoterms,
|
||||
Incoterm.tenant_id == tenant_id,
|
||||
Incoterm.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not incoterm_exists:
|
||||
errors.add_error(
|
||||
field="logistics.incoterms",
|
||||
message="El Incoterm no existe en el Catálogo de Incoterms.",
|
||||
solution=["Verifica el código del Incoterm", "Revisa el catálogo"],
|
||||
code="NOT_FOUND",
|
||||
value=invoice.logistics.incoterms,
|
||||
)
|
||||
|
||||
if invoice.logistics.weight_type not in [w.value for w in WeightUnit]:
|
||||
errors.add_error(
|
||||
field="logistics.weight_type",
|
||||
message="La Unidad de Peso proporcionada no es válida.",
|
||||
solution=[
|
||||
f"Selecciona una Unidad de Peso válida: {[w.value for w in WeightUnit]}"
|
||||
],
|
||||
code="INVALID_WEIGHT_UNIT",
|
||||
value=invoice.logistics.weight_type,
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from api.v1.modules.a76.general_catalogs.exchange_rate.models import ExchangeRate
|
||||
from core.exceptions import ErrorCollector
|
||||
from ....schemas import InvoiceHeaderCreate
|
||||
from .common import validate_common
|
||||
|
||||
def validate_create(db: Session, invoice: InvoiceHeaderCreate, tenant_id: int, company_id: int, errors: ErrorCollector) -> None:
|
||||
""" Valida la creación de una nueva factura de importe temporal """
|
||||
|
||||
if not invoice.operation_type:
|
||||
errors.add_required_error("operation_type")
|
||||
|
||||
if not invoice.invoice_type:
|
||||
errors.add_required_error("invoice_type")
|
||||
|
||||
if not invoice.document_type:
|
||||
errors.add_required_error("document_type")
|
||||
|
||||
if not invoice.invoice_number:
|
||||
errors.add_required_error("invoice_number")
|
||||
|
||||
if not invoice.invoice_date:
|
||||
errors.add_required_error("invoice_date")
|
||||
|
||||
if not invoice.compliance_mx.provider_id:
|
||||
errors.add_required_error("compliance_mx.provider_id")
|
||||
|
||||
if not invoice.compliance_mx.sold_to_id:
|
||||
errors.add_required_error("compliance_mx.sold_to_id")
|
||||
|
||||
if not invoice.compliance_mx.shipped_to_id:
|
||||
errors.add_required_error("compliance_mx.shipped_to_id")
|
||||
|
||||
if not invoice.compliance_mx.customs_broker_id:
|
||||
errors.add_required_error("compliance_mx.customs_broker_id")
|
||||
|
||||
if errors.has_errors():
|
||||
"""Se retorna por que hay campos obligatiorios para las validaciones que tienen que ser llenados"""
|
||||
return
|
||||
|
||||
validate_common(db, invoice, tenant_id, company_id, errors)
|
||||
|
||||
if errors.has_errors():
|
||||
"""Se retorna por que fallaron las validaciones generales"""
|
||||
return
|
||||
|
||||
if not invoice.compliance_mx.pedimento_id:
|
||||
invoice.compliance_mx.remesa = None
|
||||
|
||||
if not invoice.financials.exchange_rate:
|
||||
invoice.financials.exchange_rate = db.query(ExchangeRate.value).filter(ExchangeRate.date == invoice.invoice_date).scalar()
|
||||
|
||||
invoice.document_type = (invoice.document_type or "").upper()
|
||||
|
||||
if not invoice.logistics.transport_type:
|
||||
invoice.logistics.transport_type = "none"
|
||||
|
||||
if invoice.logistics.transport_type == "none" and invoice.logistics.transport_num:
|
||||
invoice.logistics.transport_num = None
|
||||
|
||||
if not invoice.financials.currency:
|
||||
invoice.financials.currency = "foreign"
|
||||
|
||||
if invoice.financials.currency == "local":
|
||||
invoice.financials.currency_type = "MXN"
|
||||
elif invoice.financials.currency_type == "foreign":
|
||||
invoice.financials.currency = "USD"
|
||||
elif invoice.financials.currency_type == "manual":
|
||||
invoice.financials.currency_type = invoice.financials.currency_type.upper()
|
||||
|
||||
invoice.logistics.incoterm = (invoice.logistics.incoterm or "").upper()
|
||||
|
||||
if not invoice.logistics.weight_type:
|
||||
invoice.logistics.weight_type = "kgs"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
def validate_update():
|
||||
pass
|
||||
@@ -6,6 +6,23 @@ from core.database import Base
|
||||
from datetime import datetime
|
||||
from ....common.base_models import TenantScopedMixin, TimestampMixin
|
||||
|
||||
class Currency(str, Enum):
|
||||
FOREIGN = "foreign"
|
||||
LOCAL = "local"
|
||||
MANUAL = "manual"
|
||||
|
||||
class WeightUnit(str, Enum):
|
||||
KGS = "kgs"
|
||||
LBS = "lbs"
|
||||
|
||||
class DestinationOriginCove(str, Enum):
|
||||
EDO_BC_PARC_SON = "edo_bc_parc_son"
|
||||
ESTADO_BCS = "estado_bcs"
|
||||
ESTADO_ROO = "estado_roo"
|
||||
MPIO_SALINA_CRUZ_OAX = "mpio_salina_cruz_oxa"
|
||||
FRANJA_FRONT_NORTE = "franja_front_norte"
|
||||
INTERIOR_PAIS = "interior_pais"
|
||||
MPIO_CABORCA_SON = "mpio_caborca_son"
|
||||
|
||||
class OperationType(str, Enum):
|
||||
IMP = "imp" # Importación
|
||||
@@ -38,10 +55,11 @@ class InvoiceHeader(Base, TenantScopedMixin, TimestampMixin):
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||||
|
||||
# Identifiers
|
||||
system: Mapped[Optional[str]] = mapped_column(String(12)) # SISTEMA / Sistema de origen <-- no tiene campo en la antigua base de datos, sera para fixed_asset(scaf), inventory(scaii)
|
||||
operation_type: Mapped[OperationType] = mapped_column(String(10)) # TIPOMOVIMIENTO / Clasifica imp/exp/sm/ctm
|
||||
invoice_type: Mapped[Optional[str]] = mapped_column(ForeignKey("public.invoice_types.key")) # TIPOFACTURA / TIPODOC
|
||||
invoice_number: Mapped[Optional[str]] = mapped_column(String(20)) # FACTURAIMPO/FACTURAEXPO/FACTURAREMISION/FACTURAENVIO/FACTURASALIDA
|
||||
system: Mapped[str] = mapped_column(String(12)) # SISTEMA / Sistema de origen <-- no tiene campo en la antigua base de datos, sera para fixed_asset(scaf), inventory(scaii)
|
||||
operation_type: Mapped[OperationType] = mapped_column(String(11)) # TIPOMOVIMIENTO / Clasifica imp/exp/sm/ctm
|
||||
invoice_type: Mapped[str] = mapped_column(ForeignKey("public.invoice_types.key")) # TIPOFACTURA / TIPODOC
|
||||
document_type: Mapped[str] = mapped_column(ForeignKey("public.pedimento_regimens.code")) # CLAVEDOCUMENTO / Clave de documento
|
||||
invoice_number: Mapped[str] = mapped_column(String(100)) # FACTURAIMPO/FACTURAEXPO/FACTURAREMISION/FACTURAENVIO/FACTURASALIDA
|
||||
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
|
||||
@@ -50,20 +68,20 @@ class InvoiceHeader(Base, TenantScopedMixin, TimestampMixin):
|
||||
proforma_number: Mapped[Optional[str]] = mapped_column(String(20)) # NUMEROPROFORMA
|
||||
|
||||
# Dates
|
||||
invoice_date: Mapped[Optional[datetime]] = mapped_column(Date) # FECHAFACTURA
|
||||
invoice_date: Mapped[datetime] = mapped_column(Date) # FECHAFACTURA
|
||||
capture_date: Mapped[datetime] = mapped_column(TIMESTAMP(timezone=False), default=datetime.now) # FECHACAPTURA + HORAACTUAL
|
||||
emission_date: Mapped[Optional[datetime]] = mapped_column(Date) # FECHAEMISION
|
||||
|
||||
# Status & Control
|
||||
is_updated: Mapped[Optional[bool]] = mapped_column(Boolean) # ESTATUS
|
||||
is_updated: Mapped[bool] = mapped_column(Boolean) # ESTATUS
|
||||
is_updated_rec: Mapped[Optional[bool]] = mapped_column(Boolean) # ESTATUSREC / Estatus de recepción
|
||||
is_updated_rep: Mapped[Optional[bool]] = mapped_column(Boolean) # ESTATUSREP / Estatus de reporte
|
||||
updated_date: Mapped[Optional[datetime]] = mapped_column(TIMESTAMP(timezone=False)) # FECHAACTUALIZACION / FECHAACTUAL
|
||||
who_updated: Mapped[Optional[str]] = mapped_column(String(20)) # USUARIOACT / Quien actualizó
|
||||
capture_user: Mapped[Optional[str]] = mapped_column(String(20)) # USUARIOCAP / Usuario que capturó
|
||||
|
||||
traffic_light_status: Mapped[Optional[str]] = mapped_column(String(50)) # SEMAFORO / SEMAFOROEXPO/IMPO
|
||||
process_log: Mapped[Optional[str]] = mapped_column(String(300)) # COMOFUEPROCESADA
|
||||
status_rec: Mapped[Optional[int]] = mapped_column(Integer) # ESTATUSREC / Estatus de recepción
|
||||
status_rep: Mapped[Optional[str]] = mapped_column(String(2)) # ESTATUSREP / Estatus de reporte
|
||||
process_log: Mapped[Optional[str]] = mapped_column(String(300)) # COMOFUEPROCESADA
|
||||
|
||||
# Comments
|
||||
observation_es: Mapped[Optional[str]] = mapped_column(Text) # OBSERVACIONE / Observaciones en español
|
||||
@@ -81,9 +99,9 @@ class InvoiceHeader(Base, TenantScopedMixin, TimestampMixin):
|
||||
party_count: Mapped[Optional[int]] = mapped_column(Integer) # CANT_PARTIDAS / Cantidad de partidas
|
||||
|
||||
# Generation flags
|
||||
generate_id: Mapped[Optional[str]] = mapped_column(String(1)) # GENERAID
|
||||
generate_id: Mapped[Optional[bool]] = mapped_column(Boolean, default=False) # GENERAID
|
||||
generate_desc_parties: Mapped[Optional[str]] = mapped_column(String(12)) # GENDESCPARTIDAS / Generar descripción de partidas
|
||||
apply_manual_discount: Mapped[Optional[str]] = mapped_column(String(1)) # APLICADESCMANUAL
|
||||
apply_manual_discount: Mapped[Optional[bool]] = mapped_column(Boolean, default=False) # APLICADESCMANUAL
|
||||
|
||||
# Bulk & Downloads
|
||||
is_bulk: Mapped[Optional[bool]] = mapped_column(Boolean) # ESAGRANEL / Es a granel
|
||||
@@ -118,9 +136,9 @@ class InvoiceComplianceMx(Base, TenantScopedMixin, TimestampMixin):
|
||||
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
|
||||
pedimento_k1: Mapped[Optional[str]] = mapped_column(String(15)) # PEDIMENTOK1
|
||||
pedimento_id: Mapped[Optional[int]] = mapped_column(ForeignKey("a76.pedimentos.id")) # PEDIMENTO/PEDIMENTOIMPO/EXPO
|
||||
pedimento_r1: Mapped[Optional[int]] = mapped_column(ForeignKey("a76.pedimentos.id")) # PEDIMENTOR1
|
||||
pedimento_k1: Mapped[Optional[int]] = mapped_column(ForeignKey("a76.pedimentos.id")) # PEDIMENTOK1
|
||||
remesa: Mapped[Optional[int]] = mapped_column(Integer) # REMESA
|
||||
aduana: Mapped[Optional[str]] = mapped_column(ForeignKey("public.customs_sections.customs_code")) # ADUANA_CRUCE
|
||||
port_of_entry: Mapped[Optional[str]] = mapped_column(String(6)) # PUERTOENTRADA / Puerto de entrada
|
||||
@@ -129,15 +147,15 @@ class InvoiceComplianceMx(Base, TenantScopedMixin, TimestampMixin):
|
||||
|
||||
# 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
|
||||
provider_id: Mapped[Optional[int]] = 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
|
||||
sold_to_id: Mapped[Optional[int]] = 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_to_id: Mapped[Optional[int]] = 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/ENVIADOPOR
|
||||
customs_broker_id: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.customs_brokers.id")) # AADUANAL / Agente aduanal
|
||||
customs_broker_us_id: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.customs_brokers.id")) # AADUANALAME / Agente aduanal americano
|
||||
shipped_by_id: Mapped[Optional[int]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # VENDIDOPOR/ENVIADOPOR
|
||||
customs_broker_id: Mapped[Optional[int]] = mapped_column(ForeignKey("a76.customs_brokers.id")) # AADUANAL / Agente aduanal
|
||||
customs_broker_us_id: Mapped[Optional[int]] = mapped_column(ForeignKey("a76.customs_brokers.id")) # AADUANALAME / Agente aduanal americano
|
||||
|
||||
# Broker Invoice
|
||||
broker_invoice_num: Mapped[Optional[str]] = mapped_column(String(20)) # NUMFACTURABROKER / Número factura broker
|
||||
@@ -148,15 +166,15 @@ class InvoiceComplianceMx(Base, TenantScopedMixin, TimestampMixin):
|
||||
waste_type: Mapped[Optional[str]] = mapped_column(String(1)) # TIPODESPERDICIO / Tipo de desperdicio
|
||||
scrap_type: Mapped[Optional[str]] = mapped_column(String(1)) # TIPOSCRAP / Tipo de scrap
|
||||
appendix_17: Mapped[Optional[int]] = mapped_column(Integer) # APENDICE17 / Apéndice 17
|
||||
is_regime_change: Mapped[Optional[str]] = mapped_column(String(1)) # ESCAMBIOREGIMEN / Es cambio de régimen
|
||||
is_regime_change: Mapped[Optional[bool]] = mapped_column(Boolean, default=False) # ESCAMBIOREGIMEN / Es cambio de régimen
|
||||
which_exchange_rate: Mapped[Optional[str]] = mapped_column(String(5)) # CUALTIPOCAMBIO / Cuál tipo de cambio
|
||||
value_method: Mapped[Optional[str]] = mapped_column(String(2)) # METVALOR / Método de valoración
|
||||
act_value: Mapped[Optional[str]] = mapped_column(String(5)) # ACTVALOR / Actualizar valor
|
||||
is_pedimento_pending: Mapped[Optional[bool]] = mapped_column(Boolean, default=False) # PED_PENDIENTE_ASIGNAR (Mapear 1 -> True, 0 -> False)
|
||||
|
||||
# Ownership & Balances
|
||||
is_owner_of_goods: Mapped[Optional[str]] = mapped_column(String(2)) # ESDUENOMCIA / Es dueño de mercancía
|
||||
generate_balances: Mapped[Optional[str]] = mapped_column(String(2)) # GENERARSALDOS / Generar saldos
|
||||
is_owner_of_goods: Mapped[Optional[bool]] = mapped_column(Boolean, default=False) # ESDUENOMCIA / Es dueño de mercancía
|
||||
generate_balances: Mapped[Optional[bool]] = mapped_column(Boolean, default=False) # GENERARSALDOS / Generar saldos
|
||||
was_reviewed_by_company: Mapped[Optional[bool]] = mapped_column(Boolean) # FUEREVISADAMCIA / Fue revisada por la compañía
|
||||
|
||||
# VUCEM / Digital
|
||||
@@ -166,7 +184,7 @@ class InvoiceComplianceMx(Base, TenantScopedMixin, TimestampMixin):
|
||||
niu_number: Mapped[Optional[str]] = mapped_column(String(19)) # NUMERONIU / Número NIU
|
||||
bill_of_lading_count: Mapped[Optional[str]] = mapped_column(String(12)) # CANTGUIASEMBARQUE / Cantidad guías embarque
|
||||
addendum_vu: Mapped[Optional[str]] = mapped_column(String(204)) # ADENDAVU / Adenda VUCEM
|
||||
origin_destination_cove: Mapped[Optional[str]] = mapped_column(String(19)) # DESTINOORIGENCOVE / Destino/Origen COVE
|
||||
origin_destination_cove: Mapped[Optional[DestinationOriginCove]] = mapped_column(String(20)) # DESTINOORIGENCOVE / Destino/Origen COVE
|
||||
vucem_operation_num: Mapped[Optional[str]] = mapped_column(String(19)) # NUMOPERACIONVU / Número operación VUCEM
|
||||
customs_person_line: Mapped[Optional[int]] = mapped_column(Integer) # LINEAPERSONAAA / Línea persona agente aduanal
|
||||
|
||||
@@ -201,7 +219,7 @@ class InvoiceFinancials(Base, TenantScopedMixin, TimestampMixin):
|
||||
invoice_id: Mapped[int] = mapped_column(ForeignKey("a76.invoice_header.id"))
|
||||
|
||||
# Currency
|
||||
currency: Mapped[Optional[str]] = mapped_column(String(3)) # CLAVEMONEDA / Clave de moneda
|
||||
currency: Mapped[Currency] = mapped_column(String(7)) # CLAVEMONEDA / Clave de moneda
|
||||
currency_type: Mapped[Optional[str]] = mapped_column(ForeignKey("public.currency_types.code")) # TIPOMONEDA / TIPOCLAVEMONEDA
|
||||
exchange_rate: Mapped[Optional[float]] = mapped_column(Numeric(13, 6)) # TIPOCAMBIO / Tipo de cambio
|
||||
exchange_rate_mm: Mapped[Optional[float]] = mapped_column(Numeric(13, 6)) # TIPOCAMBIOMM / Tipo de cambio moneda a moneda
|
||||
@@ -278,7 +296,7 @@ class InvoiceLogistics(Base, TenantScopedMixin, TimestampMixin):
|
||||
transport_num: Mapped[Optional[str]] = mapped_column(String(20)) # NUMTRASPORTE / Número de transporte
|
||||
transport_mode: Mapped[Optional[str]] = mapped_column(String(15)) # MODTRANS / Modo de transporte
|
||||
driver_name: Mapped[Optional[str]] = mapped_column(String(80)) # CONDUCTOR / Nombre del conductor
|
||||
is_rail: Mapped[Optional[str]] = mapped_column(String(2)) # ESFERROCARRIL / Es ferrocarril
|
||||
is_rail: Mapped[Optional[bool]] = mapped_column(Boolean, default=False) # ESFERROCARRIL / Es ferrocarril
|
||||
rail_id: Mapped[Optional[str]] = mapped_column(String(31)) # IDFERRORCARRIL / ID ferrocarril
|
||||
|
||||
# Vehicle & Tracking
|
||||
@@ -302,7 +320,7 @@ class InvoiceLogistics(Base, TenantScopedMixin, TimestampMixin):
|
||||
complement_2: Mapped[Optional[str]] = mapped_column(String(30)) # COMPLEMENTO2 / Complemento 2
|
||||
|
||||
# Weight & Container Info
|
||||
weight_type: Mapped[Optional[str]] = mapped_column(String(6)) # TIPOPESO / Tipo de peso
|
||||
weight_type: Mapped[WeightUnit] = mapped_column(String(3)) # TIPOPESO / Tipo de peso
|
||||
container_types: Mapped[Optional[str]] = mapped_column(String(500)) # CONTENEDORESTIPO / Tipos de contenedores
|
||||
vehicle_data: Mapped[Optional[str]] = mapped_column(String(500)) # DATOSVEHICULO / Datos del vehículo
|
||||
|
||||
@@ -317,7 +335,7 @@ class InvoiceLogistics(Base, TenantScopedMixin, TimestampMixin):
|
||||
delivery_date: Mapped[Optional[datetime]] = mapped_column(Date) # FECHAENTREGA / Fecha de entrega
|
||||
|
||||
# Delivery Control
|
||||
delivered_status: Mapped[Optional[str]] = mapped_column(String(2)) # ENTREGADO / Estado de entrega
|
||||
delivered_status: Mapped[Optional[bool]] = mapped_column(Boolean, default=False) # ENTREGADO / Estado de entrega
|
||||
received_by: Mapped[Optional[str]] = mapped_column(String(50)) # RECIBIDOPOR / Recibido por
|
||||
|
||||
# Payment Info
|
||||
@@ -325,7 +343,7 @@ class InvoiceLogistics(Base, TenantScopedMixin, TimestampMixin):
|
||||
payment_receipt_num: Mapped[Optional[str]] = mapped_column(String(20)) # NUMRECIBOPAGO / Número de recibo de pago
|
||||
|
||||
# CTM Process
|
||||
is_ctm_process: Mapped[Optional[str]] = mapped_column(String(2)) # SETRATAPROCESOCTM / Se trata de proceso CTM
|
||||
is_ctm_process: Mapped[Optional[bool]] = mapped_column(Boolean, default=False) # SETRATAPROCESOCTM / Se trata de proceso CTM
|
||||
|
||||
# Relationship
|
||||
header: Mapped["InvoiceHeader"] = relationship(back_populates="logistics")
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
from typing import Optional, List
|
||||
from typing import Literal, Optional, List
|
||||
from datetime import datetime, date
|
||||
from decimal import Decimal
|
||||
from pydantic import BaseModel, Field
|
||||
from .models import OperationType
|
||||
from .models import DestinationOriginCove, OperationType, Currency, TransportType, WeightUnit
|
||||
|
||||
|
||||
# --- Base Schemas ---
|
||||
@@ -11,11 +11,13 @@ class InvoiceHeaderBase(BaseModel):
|
||||
system: Optional[str] = Field(
|
||||
None, max_length=12, description="System of origin")
|
||||
operation_type: Optional[OperationType] = Field(
|
||||
None, max_length=10, description="Operation type: imp/exp/sm/ctm")
|
||||
..., description="Operation type: imp/exp/sm/ctm")
|
||||
invoice_type: Optional[str] = Field(
|
||||
None, max_length=5, description="Invoice type key")
|
||||
document_type: str = Field(
|
||||
..., max_length=3, description="Document type (Regimen Aduanero)")
|
||||
invoice_number: Optional[str] = Field(
|
||||
None, max_length=20, description="Invoice number")
|
||||
None, max_length=100, description="Invoice number")
|
||||
project_number: Optional[str] = Field(
|
||||
None, max_length=14, description="Project number")
|
||||
purchase_order: Optional[str] = Field(
|
||||
@@ -28,9 +30,9 @@ class InvoiceHeaderBase(BaseModel):
|
||||
None, max_length=19, description="Invoice reference")
|
||||
proforma_number: Optional[str] = Field(
|
||||
None, max_length=20, description="Proforma number")
|
||||
invoice_date: Optional[date] = Field(None, description="Invoice date")
|
||||
invoice_date: date = Field(..., description="Invoice date")
|
||||
emission_date: Optional[date] = Field(None, description="Emission date")
|
||||
is_updated: Optional[bool] = Field(None, description="Status")
|
||||
is_updated: bool = Field(False, description="Status")
|
||||
updated_date: Optional[datetime] = Field(None, description="Update date")
|
||||
who_updated: Optional[str] = Field(
|
||||
None, max_length=20, description="Who updated")
|
||||
@@ -40,8 +42,8 @@ class InvoiceHeaderBase(BaseModel):
|
||||
None, max_length=50, description="Traffic light status")
|
||||
process_log: Optional[str] = Field(
|
||||
None, max_length=300, description="Processing log")
|
||||
status_rec: Optional[int] = Field(None, description="Reception status")
|
||||
status_rep: Optional[str] = Field(
|
||||
is_updated_rec: Optional[int] = Field(None, description="Reception status")
|
||||
is_updated_rep: Optional[str] = Field(
|
||||
None, max_length=2, description="Report status")
|
||||
observation_es: Optional[str] = Field(
|
||||
None, description="Observations in Spanish")
|
||||
@@ -60,12 +62,10 @@ class InvoiceHeaderBase(BaseModel):
|
||||
subcompany: Optional[str] = Field(
|
||||
None, max_length=5, description="Subcompany")
|
||||
party_count: Optional[int] = Field(None, description="Quantity of parties")
|
||||
generate_id: Optional[str] = Field(
|
||||
None, max_length=1, description="Generate ID")
|
||||
generate_id: Optional[bool] = Field(False, description="Generate ID")
|
||||
generate_desc_parties: Optional[str] = Field(
|
||||
None, max_length=12, description="Generate description of parties")
|
||||
apply_manual_discount: Optional[str] = Field(
|
||||
None, max_length=1, description="Apply manual discount")
|
||||
apply_manual_discount: Optional[bool] = Field(False, description="Apply manual discount")
|
||||
is_bulk: Optional[bool] = Field(None, description="Is bulk")
|
||||
download_substance: Optional[bool] = Field(
|
||||
None, description="Download substance")
|
||||
@@ -84,66 +84,64 @@ class InvoiceHeaderBase(BaseModel):
|
||||
|
||||
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)")
|
||||
pedimento_k1: Optional[str] = Field(
|
||||
None, max_length=15, description="Pedimento K1")
|
||||
pedimento_id: Optional[int] = Field(
|
||||
None, description="Pedimento id")
|
||||
pedimento_r1: Optional[int] = Field(
|
||||
None, description="Pedimento id (R1)")
|
||||
pedimento_k1: Optional[int] = Field(
|
||||
None, description="Pedimento id (K1)")
|
||||
remesa: Optional[int] = Field(None, description="Remesa")
|
||||
aduana: Optional[str] = Field(
|
||||
None, max_length=5, description="Customs office")
|
||||
aduana: Optional[str] = Field(None, max_length=5, description="Customs office")
|
||||
port_of_entry: Optional[str] = Field(
|
||||
None, max_length=6, description="Port of entry")
|
||||
destination: Optional[str] = Field(
|
||||
None, max_length=3, description="Destination code")
|
||||
manifest_number: Optional[str] = Field(
|
||||
None, max_length=15, description="Manifest number")
|
||||
provider_header: Optional[str] = Field(
|
||||
provider_header: str = Field(
|
||||
None, max_length=20, description="Provider header")
|
||||
provider_id: Optional[str] = Field(
|
||||
provider_id: int = Field(
|
||||
None, description="Provider ID")
|
||||
sold_to_header: Optional[str] = Field(
|
||||
sold_to_header: str = Field(
|
||||
None, max_length=20, description="Sold to header")
|
||||
sold_to_id: Optional[str] = Field(
|
||||
sold_to_id: int = Field(
|
||||
None, description="Sold to ID")
|
||||
shipped_to_header: Optional[str] = Field(
|
||||
shipped_to_header: str = Field(
|
||||
None, max_length=20, description="Shipped to header")
|
||||
shipped_to_id: Optional[str] = Field(
|
||||
shipped_to_id:int = Field(
|
||||
None, description="Shipped to ID")
|
||||
shipped_by_header: Optional[str] = Field(
|
||||
shipped_by_header: Optional[int] = Field(
|
||||
None, max_length=20, description="Shipped by header")
|
||||
shipped_by_id: Optional[str] = Field(
|
||||
shipped_by_id: Optional[int] = Field(
|
||||
None, description="Shipped by ID")
|
||||
customs_broker_id: Optional[str] = Field(
|
||||
customs_broker_id: int = Field(
|
||||
None, description="Customs broker ID")
|
||||
customs_broker_us_id: Optional[str] = Field(
|
||||
customs_broker_us_id: Optional[int] = Field(
|
||||
None, description="US customs broker ID")
|
||||
broker_invoice_num: Optional[str] = Field(
|
||||
None, max_length=20, description="Broker invoice number")
|
||||
broker_invoice_date: Optional[date] = Field(
|
||||
None, description="Broker invoice date")
|
||||
is_mixed: Optional[bool] = Field(
|
||||
None, description="Is mixed operation")
|
||||
False, description="Is mixed operation")
|
||||
waste_type: Optional[str] = Field(
|
||||
None, max_length=1, description="Waste type")
|
||||
scrap_type: Optional[str] = Field(
|
||||
None, max_length=1, description="Scrap type")
|
||||
appendix_17: Optional[int] = Field(None, description="Appendix 17")
|
||||
is_regime_change: Optional[str] = Field(
|
||||
None, max_length=1, description="Is regime change")
|
||||
is_regime_change: Optional[bool] = Field(
|
||||
False, description="Is regime change")
|
||||
which_exchange_rate: Optional[str] = Field(
|
||||
None, max_length=5, description="Which exchange rate")
|
||||
value_method: Optional[str] = Field(
|
||||
None, max_length=2, description="Value method")
|
||||
act_value: Optional[str] = Field(
|
||||
None, max_length=5, description="Act value")
|
||||
is_pedimento_pending: Optional[bool] = Field(
|
||||
None, description="Is pedimento pending")
|
||||
is_owner_of_goods: Optional[str] = Field(
|
||||
None, max_length=2, description="Is owner of goods")
|
||||
generate_balances: Optional[str] = Field(
|
||||
None, max_length=2, description="Generate balances")
|
||||
is_pedimento_pending: bool = Field(..., description="Is pedimento pending")
|
||||
is_owner_of_goods: Optional[bool] = Field(
|
||||
False, description="Is owner of goods")
|
||||
generate_balances: Optional[bool] = Field(
|
||||
False, description="Generate balances")
|
||||
was_reviewed_by_company: Optional[bool] = Field(
|
||||
None, description="Was reviewed by company")
|
||||
edocument: Optional[str] = Field(
|
||||
@@ -158,8 +156,7 @@ class InvoiceComplianceMxBase(BaseModel):
|
||||
None, max_length=12, description="Bill of lading count")
|
||||
addendum_vu: Optional[str] = Field(
|
||||
None, max_length=204, description="VUCEM addendum")
|
||||
origin_destination_cove: Optional[str] = Field(
|
||||
None, max_length=19, description="Origin/Destination COVE")
|
||||
origin_destination_cove: Optional[DestinationOriginCove] = Field('franja_front_norte', max_length=20, description="Origin/Destination COVE")
|
||||
vucem_operation_num: Optional[str] = Field(
|
||||
None, max_length=19, description="VUCEM operation number")
|
||||
customs_person_line: Optional[int] = Field(
|
||||
@@ -191,11 +188,11 @@ class InvoiceComplianceMxBase(BaseModel):
|
||||
|
||||
class InvoiceFinancialsBase(BaseModel):
|
||||
"""Base fields for Financials"""
|
||||
currency: Optional[str] = Field(
|
||||
None, max_length=3, description="Currency code")
|
||||
currency: Currency = Field(
|
||||
None, max_length=7, description="Currency code")
|
||||
currency_type: Optional[str] = Field(
|
||||
None, description="Currency type")
|
||||
exchange_rate: Optional[Decimal] = Field(None, description="Exchange rate")
|
||||
"USD", description="Currency type")
|
||||
exchange_rate: Decimal = Field(0.00, description="Exchange rate")
|
||||
exchange_rate_mm: Optional[Decimal] = Field(
|
||||
None, description="Exchange rate currency to currency")
|
||||
value_mn: Optional[Decimal] = Field(None, description="Value in MXN")
|
||||
@@ -245,8 +242,7 @@ class InvoiceFinancialsBase(BaseModel):
|
||||
None, description="IVA in foreign currency")
|
||||
iva_mc: Optional[Decimal] = Field(
|
||||
None, description="IVA in third currency")
|
||||
iva_factor: Optional[str] = Field(
|
||||
None, max_length=10, description="IVA factor")
|
||||
iva_factor: Optional[Decimal] = Field(None, description="IVA factor")
|
||||
tax_value_me: Optional[Decimal] = Field(
|
||||
None, description="Tax value in foreign currency")
|
||||
seal_value_2500: Optional[bool] = Field(
|
||||
@@ -267,16 +263,16 @@ class InvoiceLogisticsBase(BaseModel):
|
||||
None, max_length=10, description="Transport ID")
|
||||
transport_us_id: Optional[str] = Field(
|
||||
None, max_length=10, description="US transport ID")
|
||||
transport_type: Optional[str] = Field(
|
||||
None, max_length=15, description="Transport type")
|
||||
transport_type: TransportType = Field(
|
||||
'none', max_length=15, description="Transport type")
|
||||
transport_num: Optional[str] = Field(
|
||||
None, max_length=20, description="Transport number")
|
||||
transport_mode: Optional[str] = Field(
|
||||
None, max_length=15, description="Transport mode")
|
||||
30, 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")
|
||||
is_rail: Optional[bool] = Field(
|
||||
False, description="Is rail transport")
|
||||
rail_id: Optional[str] = Field(
|
||||
None, max_length=31, description="Rail ID")
|
||||
vehicle_num: Optional[str] = Field(
|
||||
@@ -307,8 +303,8 @@ class InvoiceLogisticsBase(BaseModel):
|
||||
None, max_length=2, description="Identifier 2")
|
||||
complement_2: Optional[str] = Field(
|
||||
None, max_length=30, description="Complement 2")
|
||||
weight_type: Optional[str] = Field(
|
||||
None, max_length=6, description="Weight type")
|
||||
weight_type: WeightUnit = Field(
|
||||
default="kgs", max_length=3, description="Weight type")
|
||||
container_types: Optional[str] = Field(
|
||||
None, max_length=500, description="Container types")
|
||||
vehicle_data: Optional[str] = Field(
|
||||
@@ -333,8 +329,8 @@ class InvoiceLogisticsBase(BaseModel):
|
||||
None, description="Payment date")
|
||||
payment_receipt_num: Optional[str] = Field(
|
||||
None, max_length=20, description="Payment receipt number")
|
||||
is_ctm_process: Optional[str] = Field(
|
||||
None, max_length=2, description="Is CTM process")
|
||||
is_ctm_process: Optional[bool] = Field(
|
||||
False, description="Is CTM process")
|
||||
|
||||
|
||||
class InvoiceSalesDetailsBase(BaseModel):
|
||||
|
||||
@@ -1,15 +1,22 @@
|
||||
import traceback
|
||||
from typing import Optional, List, Tuple
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import and_
|
||||
from core.exceptions import ErrorCollector, DuplicateResourceException
|
||||
from .common.mappers import clean_dict
|
||||
from .imports.temporary.validators.create import validate_create
|
||||
from .imports.temporary.validators.update import validate_update
|
||||
from .common.common_validators import invoice_exists
|
||||
|
||||
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]:
|
||||
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)
|
||||
@@ -39,25 +46,32 @@ class InvoiceService:
|
||||
# Apply filters if provided
|
||||
if filters:
|
||||
if filters.get("status"):
|
||||
query = query.filter(
|
||||
models.InvoiceHeader.status == filters["status"])
|
||||
query = query.filter(models.InvoiceHeader.status == filters["status"])
|
||||
if filters.get("operation_type"):
|
||||
query = query.filter(
|
||||
models.InvoiceHeader.operation_type == filters["operation_type"])
|
||||
models.InvoiceHeader.operation_type == filters["operation_type"]
|
||||
)
|
||||
if filters.get("invoice_type"):
|
||||
query = query.filter(
|
||||
models.InvoiceHeader.invoice_type == filters["invoice_type"])
|
||||
models.InvoiceHeader.invoice_type == filters["invoice_type"]
|
||||
)
|
||||
if filters.get("invoice_number"):
|
||||
query = query.filter(models.InvoiceHeader.invoice_number.ilike(
|
||||
f"%{filters['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']}%")
|
||||
f"%{filters['pedimento']}%"
|
||||
)
|
||||
)
|
||||
if not filters.get("invoice_type") and filters.get("operation_type") == "exp":
|
||||
query = query.filter(
|
||||
models.InvoiceHeader.operation_type != "REPAR")
|
||||
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()
|
||||
@@ -68,30 +82,19 @@ class InvoiceService:
|
||||
db: Session,
|
||||
invoice_data: schemas.InvoiceHeaderCreate,
|
||||
tenant_id: int,
|
||||
company_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
|
||||
|
||||
|
||||
# Validaciones con ErrorCollector
|
||||
errors = ErrorCollector()
|
||||
|
||||
# Validar si la factura ya existe
|
||||
#invoice_exists(db, invoice_data.invoice_number, tenant_id, company_id, errors)
|
||||
#validate_create(db, invoice_data, tenant_id, company_id, errors)
|
||||
|
||||
# Si hay errores, lanzar excepción
|
||||
errors.raise_if_errors("Error al crear la factura")
|
||||
|
||||
try:
|
||||
# Extract nested data
|
||||
@@ -103,27 +106,32 @@ class InvoiceService:
|
||||
|
||||
# Create main invoice header
|
||||
raw_invoice_dict = invoice_data.model_dump(
|
||||
exclude={"compliance_mx", "financials",
|
||||
"logistics", "details", "collections"}
|
||||
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)
|
||||
|
||||
@@ -131,11 +139,11 @@ class InvoiceService:
|
||||
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)
|
||||
|
||||
@@ -143,7 +151,7 @@ class InvoiceService:
|
||||
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
|
||||
@@ -154,7 +162,7 @@ class InvoiceService:
|
||||
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
|
||||
@@ -165,7 +173,7 @@ class InvoiceService:
|
||||
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
|
||||
@@ -180,7 +188,7 @@ class InvoiceService:
|
||||
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
|
||||
traceback.print_exc() # Esto imprime el error real en la consola
|
||||
print("--------------------------------\n")
|
||||
raise e
|
||||
|
||||
@@ -190,20 +198,24 @@ class InvoiceService:
|
||||
invoice_id: int,
|
||||
tenant_id: int,
|
||||
invoice_data: schemas.InvoiceHeaderUpdate,
|
||||
company_id: int
|
||||
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)
|
||||
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
|
||||
exclude={
|
||||
"compliance_mx",
|
||||
"financials",
|
||||
"logistics",
|
||||
"details",
|
||||
"collections",
|
||||
},
|
||||
exclude_unset=True,
|
||||
)
|
||||
for key, value in update_dict.items():
|
||||
setattr(invoice, key, value)
|
||||
@@ -211,15 +223,21 @@ class InvoiceService:
|
||||
# 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():
|
||||
for key, value in invoice_data.compliance_mx.model_dump(
|
||||
exclude_unset=True
|
||||
).items():
|
||||
# Parche rápido para update
|
||||
if value == "": value = None
|
||||
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')
|
||||
|
||||
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
|
||||
@@ -229,8 +247,11 @@ class InvoiceService:
|
||||
# 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
|
||||
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()
|
||||
@@ -247,10 +268,9 @@ class InvoiceService:
|
||||
@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)
|
||||
invoice = InvoiceService.get_by_id(db, invoice_id, tenant_id, company_id)
|
||||
if invoice:
|
||||
db.delete(invoice)
|
||||
db.commit()
|
||||
return True
|
||||
return False
|
||||
return False
|
||||
|
||||
@@ -1,159 +1,241 @@
|
||||
from decimal import Decimal
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
from pydantic import BaseModel, Field, ConfigDict, field_validator
|
||||
|
||||
# Import nested schemas
|
||||
from ..line_customs.schemas import (
|
||||
LineCustomCreate,
|
||||
LineCustomUpdate,
|
||||
LineCustomResponse
|
||||
LineCustomResponse,
|
||||
)
|
||||
from ..line_descriptions.schemas import (
|
||||
LineDescriptionCreate,
|
||||
LineDescriptionUpdate,
|
||||
LineDescriptionResponse
|
||||
LineDescriptionResponse,
|
||||
)
|
||||
from ..line_quantities.schemas import (
|
||||
LineQuantityCreate,
|
||||
LineQuantityUpdate,
|
||||
LineQuantityResponse
|
||||
LineQuantityResponse,
|
||||
)
|
||||
from ..line_financials.schemas import (
|
||||
LineFinancialCreate,
|
||||
LineFinancialUpdate,
|
||||
LineFinancialResponse
|
||||
LineFinancialResponse,
|
||||
)
|
||||
from ..line_references.schemas import (
|
||||
LineReferenceCreate,
|
||||
LineReferenceUpdate,
|
||||
LineReferenceResponse
|
||||
LineReferenceResponse,
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# LINE ITEM SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class LineItemBase(BaseModel):
|
||||
"""Base schema for line items"""
|
||||
|
||||
line_number: int = Field(..., description="Line number")
|
||||
|
||||
|
||||
# Part identification
|
||||
part_number: Optional[str] = Field(None, max_length=50, description="Part number")
|
||||
component_part_number: Optional[str] = Field(None, max_length=50, description="Component part number")
|
||||
component_part_number: Optional[str] = Field(
|
||||
None, max_length=50, description="Component part number"
|
||||
)
|
||||
class_code: Optional[str] = Field(None, max_length=20, description="Class code")
|
||||
|
||||
|
||||
@field_validator(
|
||||
"class_code",
|
||||
"part_number",
|
||||
"component_part_number",
|
||||
"unit_of_measure",
|
||||
"alternate_unit",
|
||||
mode="before",
|
||||
)
|
||||
@classmethod
|
||||
def convert_to_string(cls, v):
|
||||
"""Convert integers to strings for FK fields"""
|
||||
if v is not None and not isinstance(v, str):
|
||||
return str(v)
|
||||
return v
|
||||
|
||||
# Unit of measure
|
||||
unit_of_measure: Optional[str] = Field(None, max_length=10, description="Unit of measure")
|
||||
alternate_unit: Optional[str] = Field(None, max_length=10, description="Alternate unit")
|
||||
unit_of_measure: Optional[str] = Field(
|
||||
None, max_length=10, description="Unit of measure"
|
||||
)
|
||||
alternate_unit: Optional[str] = Field(
|
||||
None, max_length=10, description="Alternate unit"
|
||||
)
|
||||
uma_key: Optional[str] = Field(None, max_length=2, description="UMA key")
|
||||
auxiliary_unit: Optional[str] = Field(None, max_length=5, description="Auxiliary unit")
|
||||
|
||||
auxiliary_unit: Optional[str] = Field(
|
||||
None, max_length=5, description="Auxiliary unit"
|
||||
)
|
||||
|
||||
# Permits and certificates
|
||||
permit_number: Optional[str] = Field(None, max_length=20, description="Permit number")
|
||||
permit_number: Optional[str] = Field(
|
||||
None, max_length=20, description="Permit number"
|
||||
)
|
||||
page_line: Optional[str] = Field(None, max_length=10, description="Page line")
|
||||
has_certificate: Optional[bool] = Field(None, description="Has certificate")
|
||||
certificate_number: Optional[str] = Field(None, max_length=10, description="Certificate number")
|
||||
octave_permit: Optional[str] = Field(None, max_length=20, description="Octave permit")
|
||||
certificate_number: Optional[str] = Field(
|
||||
None, max_length=10, description="Certificate number"
|
||||
)
|
||||
octave_permit: Optional[str] = Field(
|
||||
None, max_length=20, description="Octave permit"
|
||||
)
|
||||
permits_ped: Optional[str] = Field(None, max_length=500, description="PED permits")
|
||||
|
||||
|
||||
# FDA
|
||||
has_fda_code: Optional[bool] = Field(None, description="Has FDA code")
|
||||
fda_key: Optional[str] = Field(None, max_length=10, description="FDA key")
|
||||
|
||||
|
||||
# Subitem flags
|
||||
is_subitem: Optional[bool] = Field(None, description="Is subitem")
|
||||
contains_subitems: Optional[bool] = Field(None, description="Contains subitems")
|
||||
includes_subitems: Optional[bool] = Field(None, description="Includes subitems")
|
||||
subitem_number: Optional[bool] = Field(None, description="Subitem number")
|
||||
|
||||
|
||||
# Special flags
|
||||
is_military_mcia: Optional[bool] = Field(None, description="Is military merchandise")
|
||||
|
||||
is_military_mcia: Optional[bool] = Field(
|
||||
None, description="Is military merchandise"
|
||||
)
|
||||
|
||||
# IV32
|
||||
iv32_type_key: Optional[str] = Field(None, max_length=5, description="IV32 type key")
|
||||
iv32_type_key: Optional[str] = Field(
|
||||
None, max_length=5, description="IV32 type key"
|
||||
)
|
||||
iv32_number: Optional[str] = Field(None, max_length=35, description="IV32 number")
|
||||
|
||||
|
||||
# Export specific
|
||||
scrap_invoice: Optional[str] = Field(None, max_length=15, description="Scrap invoice")
|
||||
consecutive_destination: Optional[int] = Field(None, description="Consecutive destination")
|
||||
scrap_invoice: Optional[str] = Field(
|
||||
None, max_length=15, description="Scrap invoice"
|
||||
)
|
||||
consecutive_destination: Optional[int] = Field(
|
||||
None, description="Consecutive destination"
|
||||
)
|
||||
ctm_section: Optional[str] = Field(None, max_length=3, description="CTM section")
|
||||
|
||||
|
||||
# Tax payment
|
||||
tax_payment: Optional[bool] = Field(None, description="Tax payment")
|
||||
payment_method: Optional[str] = Field(None, max_length=9, description="Payment method")
|
||||
payment_method: Optional[str] = Field(
|
||||
None, max_length=9, description="Payment method"
|
||||
)
|
||||
igi_amount: Optional[Decimal] = Field(None, description="IGI amount")
|
||||
igi_payment_method: Optional[str] = Field(None, max_length=9, description="IGI payment method")
|
||||
|
||||
igi_payment_method: Optional[str] = Field(
|
||||
None, max_length=9, description="IGI payment method"
|
||||
)
|
||||
|
||||
# FCC
|
||||
fcc_key: Optional[str] = Field(None, max_length=30, description="FCC key")
|
||||
|
||||
|
||||
# Valuation method
|
||||
valuation_method: Optional[str] = Field(None, max_length=2, description="Valuation method")
|
||||
valuation_determined_value: Optional[Decimal] = Field(None, description="Valuation determined value")
|
||||
valuation_reason: Optional[str] = Field(None, max_length=500, description="Valuation reason")
|
||||
|
||||
valuation_method: Optional[str] = Field(
|
||||
None, max_length=2, description="Valuation method"
|
||||
)
|
||||
valuation_determined_value: Optional[Decimal] = Field(
|
||||
None, description="Valuation determined value"
|
||||
)
|
||||
valuation_reason: Optional[str] = Field(
|
||||
None, max_length=500, description="Valuation reason"
|
||||
)
|
||||
|
||||
# Container rules
|
||||
container_rule: Optional[str] = Field(None, max_length=50, description="Container rule")
|
||||
container_parts_ii: Optional[str] = Field(None, max_length=50, description="Container parts II")
|
||||
|
||||
container_rule: Optional[str] = Field(
|
||||
None, max_length=50, description="Container rule"
|
||||
)
|
||||
container_parts_ii: Optional[str] = Field(
|
||||
None, max_length=50, description="Container parts II"
|
||||
)
|
||||
|
||||
# APHIS
|
||||
consecutive_aphis: Optional[int] = Field(None, description="Consecutive APHIS")
|
||||
|
||||
|
||||
# BOM/Commercial
|
||||
bom_version: Optional[int] = Field(None, description="BOM version")
|
||||
bill_version: Optional[int] = Field(None, description="Bill version")
|
||||
|
||||
|
||||
# TLCAN value
|
||||
tlcan_value: Optional[Decimal] = Field(None, description="TLCAN value")
|
||||
|
||||
|
||||
# Identifier
|
||||
identifier: Optional[str] = Field(None, max_length=2, description="Identifier")
|
||||
|
||||
|
||||
# Validation fields
|
||||
validation_zero: Optional[int] = Field(None, description="Validation zero")
|
||||
validation_one: Optional[int] = Field(None, description="Validation one")
|
||||
|
||||
|
||||
# Material type
|
||||
material_type: Optional[str] = Field(None, max_length=50, description="Material type")
|
||||
|
||||
material_type: Optional[str] = Field(
|
||||
None, max_length=50, description="Material type"
|
||||
)
|
||||
|
||||
# Order concept
|
||||
order_type: Optional[str] = Field(None, max_length=50, description="Order type")
|
||||
line_concept: Optional[str] = Field(None, max_length=50, description="Line concept")
|
||||
|
||||
|
||||
# Review dispatch
|
||||
review_dispatch: Optional[str] = Field(None, max_length=10, description="Review dispatch")
|
||||
|
||||
review_dispatch: Optional[str] = Field(
|
||||
None, max_length=10, description="Review dispatch"
|
||||
)
|
||||
|
||||
# Take component from PT
|
||||
take_component_pt: Optional[int] = Field(None, description="Take component from PT")
|
||||
|
||||
|
||||
# Pallet
|
||||
pallet2: Optional[int] = Field(None, description="Pallet 2")
|
||||
|
||||
|
||||
# Wildcard field
|
||||
wildcard_field: Optional[str] = Field(None, max_length=100, description="Wildcard field")
|
||||
wildcard_field: Optional[str] = Field(
|
||||
None, max_length=100, description="Wildcard field"
|
||||
)
|
||||
|
||||
|
||||
class LineItemCreate(LineItemBase):
|
||||
"""Schema for creating line item with all nested data"""
|
||||
financial: Optional[LineFinancialCreate] = Field(None, description="Financial data for this line")
|
||||
quantity: Optional[LineQuantityCreate] = Field(None, description="Quantity data for this line")
|
||||
customs: Optional[LineCustomCreate] = Field(None, description="Customs data for this line")
|
||||
description: Optional[LineDescriptionCreate] = Field(None, description="Description data for this line")
|
||||
reference: Optional[LineReferenceCreate] = Field(None, description="Reference data for this line")
|
||||
|
||||
financial: Optional[LineFinancialCreate] = Field(
|
||||
None, description="Financial data for this line"
|
||||
)
|
||||
quantity: Optional[LineQuantityCreate] = Field(
|
||||
None, description="Quantity data for this line"
|
||||
)
|
||||
customs: Optional[LineCustomCreate] = Field(
|
||||
None, description="Customs data for this line"
|
||||
)
|
||||
description: Optional[LineDescriptionCreate] = Field(
|
||||
None, description="Description data for this line"
|
||||
)
|
||||
reference: Optional[LineReferenceCreate] = Field(
|
||||
None, description="Reference data for this line"
|
||||
)
|
||||
|
||||
|
||||
class LineItemUpdate(LineItemBase):
|
||||
"""Schema for updating line item with all nested data"""
|
||||
|
||||
line_number: Optional[int] = Field(None, description="Line number")
|
||||
financial: Optional[LineFinancialUpdate] = Field(None, description="Financial data for this line")
|
||||
quantity: Optional[LineQuantityUpdate] = Field(None, description="Quantity data for this line")
|
||||
customs: Optional[LineCustomUpdate] = Field(None, description="Customs data for this line")
|
||||
description: Optional[LineDescriptionUpdate] = Field(None, description="Description data for this line")
|
||||
reference: Optional[LineReferenceUpdate] = Field(None, description="Reference data for this line")
|
||||
financial: Optional[LineFinancialUpdate] = Field(
|
||||
None, description="Financial data for this line"
|
||||
)
|
||||
quantity: Optional[LineQuantityUpdate] = Field(
|
||||
None, description="Quantity data for this line"
|
||||
)
|
||||
customs: Optional[LineCustomUpdate] = Field(
|
||||
None, description="Customs data for this line"
|
||||
)
|
||||
description: Optional[LineDescriptionUpdate] = Field(
|
||||
None, description="Description data for this line"
|
||||
)
|
||||
reference: Optional[LineReferenceUpdate] = Field(
|
||||
None, description="Reference data for this line"
|
||||
)
|
||||
|
||||
|
||||
class LineItemResponse(LineItemBase):
|
||||
"""Schema for line item response with all nested data"""
|
||||
|
||||
id: int
|
||||
item_id: int
|
||||
financial: Optional[LineFinancialResponse] = None
|
||||
|
||||
@@ -106,7 +106,7 @@ class SubassemblyEntry(Base, TenantScopedMixin, TimestampMixin):
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
remission_line: Mapped[int] = mapped_column(Integer) # LINEAREMISION
|
||||
exit_invoice: Mapped[Optional[str]] = mapped_column(
|
||||
String(15)) # FACTURASALIDA
|
||||
String(15)) # FACTURASALIDA
|
||||
exit_line: Mapped[Optional[int]] = mapped_column(Integer) # LINEASALIDA
|
||||
|
||||
# ============================================================================
|
||||
|
||||
@@ -36,10 +36,7 @@ class ItemService:
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(
|
||||
db: Session,
|
||||
item_id: int,
|
||||
tenant_id: int,
|
||||
company_id: int
|
||||
db: Session, item_id: int, tenant_id: int, company_id: int
|
||||
) -> Optional[Item]:
|
||||
"""Get an item by ID with tenant/company validation"""
|
||||
return (
|
||||
@@ -91,8 +88,7 @@ class ItemService:
|
||||
if filters.get("item_type"):
|
||||
query = query.filter(Item.item_type == filters["item_type"])
|
||||
if filters.get("system_origin"):
|
||||
query = query.filter(Item.system_origin ==
|
||||
filters["system_origin"])
|
||||
query = query.filter(Item.system_origin == filters["system_origin"])
|
||||
if filters.get("search"):
|
||||
search_term = f"%{filters['search']}%"
|
||||
query = query.filter(
|
||||
@@ -151,6 +147,12 @@ class ItemService:
|
||||
lines_data = item_data.lines or []
|
||||
item_dict = item_data.model_dump(exclude={"lines"})
|
||||
|
||||
# DEBUG: Log incoming data
|
||||
print(f"\n🔍 DEBUG CREATE ITEM:")
|
||||
print(f" Item data: {item_dict}")
|
||||
print(f" Lines count: {len(lines_data)}")
|
||||
print(f" Tenant ID: {tenant_id}, Company ID: {company_id}")
|
||||
|
||||
# Add tenant and company
|
||||
item_dict["tenant_id"] = tenant_id
|
||||
item_dict["company_id"] = company_id
|
||||
@@ -160,8 +162,11 @@ class ItemService:
|
||||
db.add(db_item)
|
||||
db.flush() # Get the item ID
|
||||
|
||||
print(f" ✅ Item created with ID: {db_item.id}")
|
||||
|
||||
# Create line items if provided
|
||||
for line_data in lines_data:
|
||||
for idx, line_data in enumerate(lines_data):
|
||||
print(f"\n 📝 Processing line {idx + 1}/{len(lines_data)}")
|
||||
# Extract nested data from line
|
||||
financial_data = line_data.financial
|
||||
quantity_data = line_data.quantity
|
||||
@@ -169,16 +174,31 @@ class ItemService:
|
||||
description_data = line_data.description
|
||||
reference_data = line_data.reference
|
||||
|
||||
print(f" Line data: {line_data.model_dump()}")
|
||||
print(f" Has financial: {financial_data is not None}")
|
||||
print(f" Has quantity: {quantity_data is not None}")
|
||||
print(f" Has customs: {customs_data is not None}")
|
||||
print(f" Has description: {description_data is not None}")
|
||||
print(f" Has reference: {reference_data is not None}")
|
||||
|
||||
line_dict = line_data.model_dump(
|
||||
exclude={"financial", "quantity",
|
||||
"customs", "description", "reference"}
|
||||
exclude={
|
||||
"financial",
|
||||
"quantity",
|
||||
"customs",
|
||||
"description",
|
||||
"reference",
|
||||
}
|
||||
)
|
||||
line_dict["item_id"] = db_item.id
|
||||
line_dict["tenant_id"] = tenant_id
|
||||
line_dict["company_id"] = company_id
|
||||
|
||||
# Create line item
|
||||
db_line = LineItem(**line_dict)
|
||||
db.add(db_line)
|
||||
db.flush() # Get the line ID
|
||||
print(f" ✅ Line created with ID: {db_line.id}")
|
||||
|
||||
# Create financial data if provided
|
||||
if financial_data:
|
||||
@@ -186,6 +206,7 @@ class ItemService:
|
||||
financial_dict["item_line_id"] = db_line.id
|
||||
db_financial = LineFinancial(**financial_dict)
|
||||
db.add(db_financial)
|
||||
print(f" ✅ Financial data added")
|
||||
|
||||
# Create quantity data if provided
|
||||
if quantity_data:
|
||||
@@ -193,6 +214,7 @@ class ItemService:
|
||||
quantity_dict["item_line_id"] = db_line.id
|
||||
db_quantity = LineQuantity(**quantity_dict)
|
||||
db.add(db_quantity)
|
||||
print(f" ✅ Quantity data added")
|
||||
|
||||
# Create customs data if provided
|
||||
if customs_data:
|
||||
@@ -200,6 +222,7 @@ class ItemService:
|
||||
customs_dict["item_line_id"] = db_line.id
|
||||
db_customs = LineCustom(**customs_dict)
|
||||
db.add(db_customs)
|
||||
print(f" ✅ Customs data added")
|
||||
|
||||
# Create description data if provided
|
||||
if description_data:
|
||||
@@ -207,6 +230,7 @@ class ItemService:
|
||||
description_dict["item_line_id"] = db_line.id
|
||||
db_description = LineDescription(**description_dict)
|
||||
db.add(db_description)
|
||||
print(f" ✅ Description data added")
|
||||
|
||||
# Create reference data if provided
|
||||
if reference_data:
|
||||
@@ -214,9 +238,12 @@ class ItemService:
|
||||
reference_dict["item_line_id"] = db_line.id
|
||||
db_reference = LineReference(**reference_dict)
|
||||
db.add(db_reference)
|
||||
print(f" ✅ Reference data added")
|
||||
|
||||
print(f"\n 💾 Committing transaction...")
|
||||
db.commit()
|
||||
db.refresh(db_item)
|
||||
print(f" ✅ Transaction committed successfully!")
|
||||
return db_item
|
||||
|
||||
except IntegrityError as e:
|
||||
@@ -248,8 +275,7 @@ class ItemService:
|
||||
|
||||
# Extract lines data
|
||||
lines_data = item_data.lines
|
||||
item_dict = item_data.model_dump(
|
||||
exclude={"lines"}, exclude_unset=True)
|
||||
item_dict = item_data.model_dump(exclude={"lines"}, exclude_unset=True)
|
||||
|
||||
# Update item fields
|
||||
for key, value in item_dict.items():
|
||||
@@ -272,43 +298,48 @@ class ItemService:
|
||||
reference_data = line_data.reference
|
||||
|
||||
line_dict = line_data.model_dump(
|
||||
exclude={"financial", "quantity",
|
||||
"customs", "description", "reference"},
|
||||
exclude_unset=True
|
||||
exclude={
|
||||
"financial",
|
||||
"quantity",
|
||||
"customs",
|
||||
"description",
|
||||
"reference",
|
||||
},
|
||||
exclude_unset=True,
|
||||
)
|
||||
line_dict["item_id"] = db_item.id
|
||||
line_dict["tenant_id"] = tenant_id
|
||||
line_dict["company_id"] = company_id
|
||||
|
||||
db_line = LineItem(**line_dict)
|
||||
db.add(db_line)
|
||||
db.flush()
|
||||
|
||||
# Create nested data if provided
|
||||
if financial_data is not None:
|
||||
financial_dict = financial_data.model_dump(
|
||||
exclude_unset=True)
|
||||
financial_dict = financial_data.model_dump(exclude_unset=True)
|
||||
financial_dict["item_line_id"] = db_line.id
|
||||
db.add(LineFinancial(**financial_dict))
|
||||
|
||||
if quantity_data is not None:
|
||||
quantity_dict = quantity_data.model_dump(
|
||||
exclude_unset=True)
|
||||
quantity_dict = quantity_data.model_dump(exclude_unset=True)
|
||||
quantity_dict["item_line_id"] = db_line.id
|
||||
db.add(LineQuantity(**quantity_dict))
|
||||
|
||||
if customs_data is not None:
|
||||
customs_dict = customs_data.model_dump(
|
||||
exclude_unset=True)
|
||||
customs_dict = customs_data.model_dump(exclude_unset=True)
|
||||
customs_dict["item_line_id"] = db_line.id
|
||||
db.add(LineCustom(**customs_dict))
|
||||
|
||||
if description_data is not None:
|
||||
description_dict = description_data.model_dump(
|
||||
exclude_unset=True)
|
||||
exclude_unset=True
|
||||
)
|
||||
description_dict["item_line_id"] = db_line.id
|
||||
db.add(LineDescription(**description_dict))
|
||||
|
||||
if reference_data is not None:
|
||||
reference_dict = reference_data.model_dump(
|
||||
exclude_unset=True)
|
||||
reference_dict = reference_data.model_dump(exclude_unset=True)
|
||||
reference_dict["item_line_id"] = db_line.id
|
||||
db.add(LineReference(**reference_dict))
|
||||
|
||||
|
||||
@@ -1,232 +1,137 @@
|
||||
"""
|
||||
DTOs (Data Transfer Objects) para módulo de partes/componentes
|
||||
Reemplaza schemas.py siguiendo enfoque DDD y estilo NestJS
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Optional
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
# --- SUB-DTO: DATOS ADUANALES (FaData) ---
|
||||
class FaDataDTO(BaseModel):
|
||||
origin_country: Optional[str] = None
|
||||
sector: Optional[str] = None
|
||||
fraction_type: Optional[str] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
# --- SUB-DTO: DATOS DE INVENTARIO Y COSTEO (InvData) ---
|
||||
class InvDataDTO(BaseModel):
|
||||
part_type: Optional[str] = None
|
||||
material_type: Optional[str] = None
|
||||
reference_number: Optional[str] = None
|
||||
flex_reference_number: Optional[str] = None
|
||||
equivalent_uom: Optional[str] = None
|
||||
conversion_factor: Optional[Decimal] = None
|
||||
stock_uom: Optional[str] = None
|
||||
alternate_uom: Optional[str] = None
|
||||
conversion_uom: Optional[str] = None
|
||||
added_value: Optional[Decimal] = None
|
||||
added_value_type: Optional[str] = None
|
||||
assigned_client: Optional[str] = None
|
||||
supplier_code: Optional[str] = None
|
||||
is_textile: Optional[str] = None
|
||||
bom_version: Optional[int] = None
|
||||
is_repair: Optional[str] = None
|
||||
is_hazardous: Optional[str] = None
|
||||
emergency_number: Optional[str] = None
|
||||
danger_class: Optional[str] = None
|
||||
packaging_group: Optional[str] = None
|
||||
width: Optional[str] = None
|
||||
thickness: Optional[str] = None
|
||||
specification: Optional[str] = None
|
||||
|
||||
# Desglose de Costos (Anexo 24 - Valor Agregado)
|
||||
total_value: Optional[Decimal] = None
|
||||
direct_labor: Optional[Decimal] = None
|
||||
general_expenses: Optional[Decimal] = None
|
||||
total_expenses: Optional[Decimal] = None
|
||||
depreciation: Optional[Decimal] = None
|
||||
tooling: Optional[Decimal] = None
|
||||
material_consumed: Optional[Decimal] = None
|
||||
profit: Optional[Decimal] = None
|
||||
|
||||
# Fracciones adicionales y TLCAN/T-MEC
|
||||
us_fraction_alt: Optional[str] = None
|
||||
ca_fraction: Optional[str] = None
|
||||
ad_valorem_us: Optional[Decimal] = None
|
||||
nafta_result: Optional[str] = None
|
||||
nafta_percentage: Optional[Decimal] = None
|
||||
dta: Optional[str] = None
|
||||
dtb: Optional[str] = None
|
||||
dtg: Optional[str] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class PartCreateDTO(BaseModel):
|
||||
"""DTO para crear una parte"""
|
||||
|
||||
client_id: int = Field(..., description="Client key")
|
||||
part_number: str = Field(..., max_length=49, description="Part number")
|
||||
fraction: Optional[str] = Field(None, max_length=10, description="Tariff fraction")
|
||||
description_spanish: Optional[str] = Field(
|
||||
None, max_length=500, description="Description in Spanish"
|
||||
)
|
||||
description_english: Optional[str] = Field(
|
||||
None, max_length=500, description="Description in English"
|
||||
)
|
||||
part_class: Optional[str] = Field(None, max_length=8, description="Part class")
|
||||
unit_of_measure: Optional[str] = Field(
|
||||
None, max_length=5, description="Unit of measure"
|
||||
)
|
||||
commercial_part_number: Optional[str] = Field(
|
||||
None, max_length=70, description="Commercial part number"
|
||||
)
|
||||
country_of_origin: Optional[str] = Field(
|
||||
None, max_length=3, description="Country of origin code"
|
||||
)
|
||||
|
||||
# Pricing and currency
|
||||
unit_cost: Optional[Decimal] = Field(None, description="Unit cost")
|
||||
currency_type: Optional[str] = Field(
|
||||
None, max_length=2, description="Currency type"
|
||||
)
|
||||
currency_key: Optional[str] = Field(None, max_length=3, description="Currency key")
|
||||
|
||||
# Weight information
|
||||
unit_weight: Optional[Decimal] = Field(None, description="Unit weight")
|
||||
weight_type: Optional[str] = Field(None, max_length=6, description="Weight type")
|
||||
|
||||
# Classification and regulatory
|
||||
us_fraction: Optional[str] = Field(
|
||||
None, max_length=16, description="US tariff fraction"
|
||||
)
|
||||
fda_key: Optional[str] = Field(None, max_length=20, description="FDA key")
|
||||
fcc_key: Optional[str] = Field(None, max_length=30, description="FCC key")
|
||||
license_code: Optional[str] = Field(None, max_length=3, description="License code")
|
||||
eccn: Optional[str] = Field(
|
||||
None, max_length=20, description="Export Control Classification Number"
|
||||
)
|
||||
export_code: Optional[str] = Field(None, max_length=2, description="Export code")
|
||||
exclusion_symbol: Optional[str] = Field(
|
||||
None, max_length=19, description="Exclusion symbol"
|
||||
)
|
||||
|
||||
# Additional information
|
||||
supplier: Optional[str] = Field(None, max_length=14, description="Supplier")
|
||||
alternate_unit_measure: Optional[str] = Field(
|
||||
None, max_length=14, description="Alternate unit of measure"
|
||||
)
|
||||
added_value: Optional[Decimal] = Field(None, description="Added value")
|
||||
|
||||
# Status and media
|
||||
is_active: Optional[bool] = Field(None, description="Enabled/Disabled status")
|
||||
creation_date: Optional[int] = Field(None, description="Creation date")
|
||||
part_photo: Optional[str] = Field(
|
||||
None, max_length=255, description="Part photo URL"
|
||||
)
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PartUpdateDTO(BaseModel):
|
||||
"""DTO para actualizar una parte"""
|
||||
|
||||
fraction: Optional[str] = Field(None, max_length=10, description="Tariff fraction")
|
||||
description_spanish: Optional[str] = Field(
|
||||
None, max_length=500, description="Description in Spanish"
|
||||
)
|
||||
description_english: Optional[str] = Field(
|
||||
None, max_length=500, description="Description in English"
|
||||
)
|
||||
part_class: Optional[str] = Field(None, max_length=8, description="Part class")
|
||||
unit_of_measure: Optional[str] = Field(
|
||||
None, max_length=5, description="Unit of measure"
|
||||
)
|
||||
commercial_part_number: Optional[str] = Field(
|
||||
None, max_length=70, description="Commercial part number"
|
||||
)
|
||||
country_of_origin: Optional[str] = Field(
|
||||
None, max_length=3, description="Country of origin code"
|
||||
)
|
||||
|
||||
# Pricing and currency
|
||||
unit_cost: Optional[Decimal] = Field(None, description="Unit cost")
|
||||
currency_type: Optional[str] = Field(
|
||||
None, max_length=2, description="Currency type"
|
||||
)
|
||||
currency_key: Optional[str] = Field(None, max_length=3, description="Currency key")
|
||||
|
||||
# Weight information
|
||||
unit_weight: Optional[Decimal] = Field(None, description="Unit weight")
|
||||
weight_type: Optional[str] = Field(None, max_length=6, description="Weight type")
|
||||
|
||||
# Classification and regulatory
|
||||
us_fraction: Optional[str] = Field(
|
||||
None, max_length=16, description="US tariff fraction"
|
||||
)
|
||||
fda_key: Optional[str] = Field(None, max_length=20, description="FDA key")
|
||||
fcc_key: Optional[str] = Field(None, max_length=30, description="FCC key")
|
||||
license_code: Optional[str] = Field(None, max_length=3, description="License code")
|
||||
eccn: Optional[str] = Field(
|
||||
None, max_length=20, description="Export Control Classification Number"
|
||||
)
|
||||
export_code: Optional[str] = Field(None, max_length=2, description="Export code")
|
||||
exclusion_symbol: Optional[str] = Field(
|
||||
None, max_length=19, description="Exclusion symbol"
|
||||
)
|
||||
|
||||
# Additional information
|
||||
supplier: Optional[str] = Field(None, max_length=14, description="Supplier")
|
||||
alternate_unit_measure: Optional[str] = Field(
|
||||
None, max_length=14, description="Alternate unit of measure"
|
||||
)
|
||||
added_value: Optional[Decimal] = Field(None, description="Added value")
|
||||
|
||||
# Status and media
|
||||
is_active: Optional[bool] = Field(None, description="Enabled/Disabled status")
|
||||
part_photo: Optional[str] = Field(
|
||||
None, max_length=255, description="Part photo URL"
|
||||
)
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PartResponseDTO(BaseModel):
|
||||
"""DTO para respuesta de parte"""
|
||||
|
||||
class PartBase(BaseModel):
|
||||
client_id: int
|
||||
part_number: str
|
||||
fraction: Optional[str] = None
|
||||
part_number: str = Field(..., max_length=70)
|
||||
commercial_part_number: Optional[str] = None
|
||||
|
||||
description_spanish: Optional[str] = None
|
||||
description_english: Optional[str] = None
|
||||
part_class: Optional[str] = None
|
||||
unit_of_measure: Optional[str] = None
|
||||
commercial_part_number: Optional[str] = None
|
||||
country_of_origin: Optional[str] = None
|
||||
|
||||
# Pricing and currency
|
||||
unit_of_measure: Optional[str] = "PZ"
|
||||
|
||||
unit_cost: Optional[Decimal] = None
|
||||
currency_type: Optional[str] = None
|
||||
currency_key: Optional[str] = None
|
||||
|
||||
# Weight information
|
||||
currency_type: Optional[str] = None
|
||||
|
||||
unit_weight: Optional[Decimal] = None
|
||||
weight_type: Optional[str] = None
|
||||
|
||||
# Classification and regulatory
|
||||
|
||||
fraction: Optional[str] = None
|
||||
us_fraction: Optional[str] = None
|
||||
|
||||
# Regulatorios
|
||||
fda_key: Optional[str] = None
|
||||
fcc_key: Optional[str] = None
|
||||
license_code: Optional[str] = None
|
||||
eccn: Optional[str] = None
|
||||
export_code: Optional[str] = None
|
||||
exclusion_symbol: Optional[str] = None
|
||||
|
||||
# Additional information
|
||||
supplier: Optional[str] = None
|
||||
alternate_unit_measure: Optional[str] = None
|
||||
added_value: Optional[Decimal] = None
|
||||
|
||||
# Status and dates
|
||||
is_active: Optional[bool] = None
|
||||
creation_date: Optional[int] = None
|
||||
modification_date: Optional[int] = None
|
||||
modification_date_iso: Optional[datetime] = None
|
||||
|
||||
# Media
|
||||
|
||||
is_active: bool = True
|
||||
part_photo: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
# Anidados
|
||||
fa_data: Optional[FaDataDTO] = None
|
||||
inv_data: Optional[InvDataDTO] = None
|
||||
|
||||
# --- CREACIÓN ---
|
||||
class PartCreateDTO(PartBase):
|
||||
# Opcional para que lo tome de la URL si no viene en el body
|
||||
company_id: Optional[int] = None
|
||||
|
||||
# --- ACTUALIZACIÓN ---
|
||||
class PartUpdateDTO(PartBase):
|
||||
client_id: Optional[int] = None
|
||||
part_number: Optional[str] = None
|
||||
# Todo opcional para PATCH
|
||||
pass
|
||||
|
||||
# --- RESPUESTA ---
|
||||
class PartResponseDTO(PartBase):
|
||||
id: int
|
||||
tenant_id: int
|
||||
company_id: int
|
||||
creation_date: Optional[int] = None
|
||||
modification_date_iso: Optional[datetime] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
# --- BÁSICO (Para listados ligeros) ---
|
||||
class PartBasicDTO(BaseModel):
|
||||
"""DTO para información básica de parte"""
|
||||
|
||||
client_id: int
|
||||
id: int
|
||||
part_number: str
|
||||
description_spanish: Optional[str] = None
|
||||
description_english: Optional[str] = None
|
||||
part_class: Optional[str] = None
|
||||
unit_cost: Optional[Decimal] = None
|
||||
currency_key: Optional[str] = None
|
||||
is_active: Optional[bool] = None
|
||||
fraction: Optional[str] = None
|
||||
is_active: bool
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PartListDTO(BaseModel):
|
||||
"""DTO para lista de partes"""
|
||||
|
||||
parts: list[PartBasicDTO]
|
||||
# --- LISTADO PAGINADO ---
|
||||
class PartListResponseDTO(BaseModel):
|
||||
items: List[PartResponseDTO]
|
||||
total: int
|
||||
page: int
|
||||
size: int
|
||||
page_size: int
|
||||
pages: int
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class PartSearchDTO(BaseModel):
|
||||
"""DTO para búsqueda de partes"""
|
||||
|
||||
client_id: Optional[int] = Field(None, description="Filter by client key")
|
||||
part_number: Optional[str] = Field(None, description="Search by part number")
|
||||
description: Optional[str] = Field(None, description="Search in descriptions")
|
||||
fraction: Optional[str] = Field(None, description="Filter by tariff fraction")
|
||||
supplier: Optional[str] = Field(None, description="Filter by supplier")
|
||||
enabled_only: bool = Field(False, description="Show only enabled parts")
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Modelos ORM para gestión de partes/componentes
|
||||
Modelos ORM para gestión de partes/componentes - Anexo 76 (Master Data)
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
@@ -16,34 +16,46 @@ from sqlalchemy import (
|
||||
String,
|
||||
UniqueConstraint,
|
||||
Boolean,
|
||||
DateTime,
|
||||
)
|
||||
|
||||
# Importante usar relationship y Mapped
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from api.v1.modules.a76.classes.models import Class
|
||||
from api.v1.modules.public.reference_data.countries.models import Country
|
||||
from api.v1.modules.public.reference_data.currency_types.models import CurrencyType
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.models import (
|
||||
UnitOfMeasure,
|
||||
)
|
||||
from api.v1.modules.a24.fa.fa_parts.models import FaPart
|
||||
from api.v1.modules.a24.inv.inv_parts.models import InvPart
|
||||
|
||||
|
||||
class Part(Base, TenantScopedMixin, TimestampMixin):
|
||||
"""
|
||||
Modelo para la tabla GPartes - Información de partes en los sistemas SCAII (N), SCAF (S) Y WINSAAI (W)
|
||||
"""
|
||||
|
||||
__tablename__ = "parts"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id", name="parts_pkey"),
|
||||
ForeignKeyConstraint(
|
||||
["country_of_origin"], ["public.countries.m3_key"], name="fk_parts_country"
|
||||
),
|
||||
ForeignKeyConstraint(
|
||||
["currency_key"], ["public.currency_types.code"], name="fk_parts_currency"
|
||||
),
|
||||
ForeignKeyConstraint(
|
||||
["unit_of_measure", "tenant_id", "company_id"],
|
||||
["a76.units_of_measure.code", "a76.units_of_measure.tenant_id",
|
||||
"a76.units_of_measure.company_id"],
|
||||
[
|
||||
"a76.units_of_measure.code",
|
||||
"a76.units_of_measure.tenant_id",
|
||||
"a76.units_of_measure.company_id",
|
||||
],
|
||||
),
|
||||
# Puente hacia la tabla de clases
|
||||
ForeignKeyConstraint(
|
||||
["part_class", "tenant_id", "company_id"],
|
||||
[
|
||||
"a76.classes.class_code",
|
||||
"a76.classes.tenant_id",
|
||||
"a76.classes.company_id",
|
||||
],
|
||||
name="fk_parts_class",
|
||||
),
|
||||
UniqueConstraint(
|
||||
"tenant_id", "company_id", "part_number", name="client_part_ukey"
|
||||
@@ -52,80 +64,60 @@ class Part(Base, TenantScopedMixin, TimestampMixin):
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
|
||||
# Unique constraint compuesta
|
||||
client_id: Mapped[int] = mapped_column(Integer)
|
||||
part_number: Mapped[str] = mapped_column(String(50))
|
||||
part_number: Mapped[str] = mapped_column(String(70))
|
||||
commercial_part_number: Mapped[Optional[str]] = mapped_column(String(70))
|
||||
|
||||
# Basic information
|
||||
fraction: Mapped[Optional[str]] = mapped_column(String(10))
|
||||
description_spanish: Mapped[Optional[str]] = mapped_column(String(500))
|
||||
description_english: Mapped[Optional[str]] = mapped_column(String(500))
|
||||
part_class: Mapped[Optional[str]] = mapped_column(String(8))
|
||||
unit_of_measure: Mapped[Optional[str]] = mapped_column(
|
||||
String(5)
|
||||
)
|
||||
commercial_part_number: Mapped[Optional[str]] = mapped_column(String(70))
|
||||
country_of_origin: Mapped[Optional[str]] = mapped_column(String(3))
|
||||
unit_of_measure: Mapped[Optional[str]] = mapped_column(String(5))
|
||||
|
||||
# Pricing and currency
|
||||
unit_cost: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8))
|
||||
currency_type: Mapped[Optional[str]] = mapped_column(String(2))
|
||||
currency_key: Mapped[Optional[str]] = mapped_column(String(3))
|
||||
|
||||
# Weight information
|
||||
unit_weight: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8))
|
||||
weight_type: Mapped[Optional[str]] = mapped_column(String(6))
|
||||
|
||||
# Classification and regulatory
|
||||
us_fraction: Mapped[Optional[str]] = mapped_column(
|
||||
String(16)) # FRACCIONAME
|
||||
fraction: Mapped[Optional[str]] = mapped_column(String(10))
|
||||
us_fraction: Mapped[Optional[str]] = mapped_column(String(16))
|
||||
fda_key: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
fcc_key: Mapped[Optional[str]] = mapped_column(String(30))
|
||||
license_code: Mapped[Optional[str]] = mapped_column(String(3))
|
||||
eccn: Mapped[Optional[str]] = mapped_column(
|
||||
String(20)
|
||||
) # Export Control Classification Number
|
||||
eccn: Mapped[Optional[str]] = mapped_column(String(20))
|
||||
export_code: Mapped[Optional[str]] = mapped_column(String(2))
|
||||
exclusion_symbol: Mapped[Optional[str]] = mapped_column(
|
||||
String(19)) # SIMBOLOEXCLIC
|
||||
exclusion_symbol: Mapped[Optional[str]] = mapped_column(String(19))
|
||||
|
||||
# Additional information
|
||||
supplier: Mapped[Optional[str]] = mapped_column(String(14))
|
||||
alternate_unit_measure: Mapped[Optional[str]] = mapped_column(String(14))
|
||||
added_value: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8))
|
||||
|
||||
# Status and dates
|
||||
is_active: Mapped[Optional[bool]] = mapped_column(Boolean)
|
||||
creation_date: Mapped[Optional[int]
|
||||
] = mapped_column() # FECHACREACIONPARTE
|
||||
modification_date: Mapped[Optional[int]] = mapped_column() # FECHAMODIFICA
|
||||
modification_date_iso: Mapped[Optional[datetime]] = (
|
||||
mapped_column()
|
||||
) # FECHAMODIFICA_ISO
|
||||
|
||||
# Media
|
||||
is_active: Mapped[Optional[bool]] = mapped_column(Boolean, default=True)
|
||||
part_photo: Mapped[Optional[str]] = mapped_column(String(255))
|
||||
|
||||
# Relationships
|
||||
country: Mapped[Optional["Country"]] = relationship(
|
||||
foreign_keys=[country_of_origin]
|
||||
)
|
||||
currency: Mapped[Optional["CurrencyType"]] = relationship(
|
||||
foreign_keys=[currency_key]
|
||||
)
|
||||
creation_date: Mapped[Optional[int]] = mapped_column()
|
||||
modification_date: Mapped[Optional[int]] = mapped_column()
|
||||
modification_date_iso: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||||
|
||||
# --- RELACIONES ---
|
||||
currency: Mapped[Optional["CurrencyType"]] = relationship("CurrencyType")
|
||||
unit_of_measure_info: Mapped[Optional["UnitOfMeasure"]] = relationship(
|
||||
foreign_keys=[unit_of_measure]
|
||||
"UnitOfMeasure"
|
||||
)
|
||||
part_class_info: Mapped[Optional["Class"]] = relationship(
|
||||
"Class", back_populates="parts", overlaps="unit_of_measure_info"
|
||||
)
|
||||
|
||||
# Relationship with Class through composite foreign key
|
||||
# Note: This requires both client_id and part_class to match client_id and class_code in Class
|
||||
part_class_info: Mapped[Optional["Class"]] = relationship(
|
||||
primaryjoin="and_(Part.client_id == Class.client_id, Part.part_class == Class.class_code)",
|
||||
foreign_keys="[Part.client_id, Part.part_class]",
|
||||
viewonly=True,
|
||||
back_populates="parts",
|
||||
# Extensiones Anexo 24
|
||||
fa_data: Mapped[Optional["FaPart"]] = relationship(
|
||||
"FaPart",
|
||||
back_populates="master_info",
|
||||
uselist=False,
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
inv_data: Mapped[Optional["InvPart"]] = relationship(
|
||||
"InvPart",
|
||||
back_populates="master_info",
|
||||
uselist=False,
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Part(client_id={self.client_id}, part_number='{self.part_number}', description='{self.description_spanish}')>"
|
||||
return f"<Part(id={self.id}, part_number='{self.part_number}')>"
|
||||
|
||||
@@ -1,393 +1,22 @@
|
||||
"""
|
||||
Endpoints API para gestión de partes/componentes
|
||||
Endpoints API para gestión de partes (SCAII)
|
||||
"""
|
||||
|
||||
from typing import List, Optional
|
||||
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes
|
||||
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .dto import (
|
||||
PartBasicDTO,
|
||||
PartCreateDTO,
|
||||
PartListDTO,
|
||||
PartResponseDTO,
|
||||
PartSearchDTO,
|
||||
PartUpdateDTO,
|
||||
)
|
||||
from .dto import PartCreateDTO, PartResponseDTO, PartUpdateDTO
|
||||
from .service import PartService
|
||||
|
||||
router = APIRouter(prefix="/parts")
|
||||
|
||||
|
||||
@router.post("/", response_model=PartResponseDTO, status_code=status.HTTP_201_CREATED)
|
||||
async def create_part(
|
||||
part_data: PartCreateDTO,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Create a new part in the system
|
||||
"""
|
||||
# Validate access to the tenant and company
|
||||
tenant_id = current_user.get("tenant_id")
|
||||
company_id = current_user.get("company_id")
|
||||
|
||||
if not tenant_id or not company_id:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Access denied: Tenant or Company not found"
|
||||
)
|
||||
|
||||
service = PartService(db)
|
||||
return service.create_part(part_data)
|
||||
|
||||
|
||||
@router.get("/", response_model=PartListDTO)
|
||||
async def list_parts(
|
||||
skip: int = Query(0, ge=0, description="Number of records to skip"),
|
||||
limit: int = Query(
|
||||
100, ge=1, le=1000, description="Maximum number of records to return"
|
||||
),
|
||||
client_id: Optional[int] = Query(None, description="Filter by client key"),
|
||||
part_number: Optional[str] = Query(None, description="Search by part number"),
|
||||
description: Optional[str] = Query(None, description="Search in descriptions"),
|
||||
fraction: Optional[str] = Query(None, description="Filter by tariff fraction"),
|
||||
supplier: Optional[str] = Query(None, description="Filter by supplier"),
|
||||
enabled_only: bool = Query(False, description="Show only enabled parts"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
List parts with optional filters and pagination
|
||||
"""
|
||||
# Validate access to the tenant and company
|
||||
tenant_id = current_user.get("tenant_id")
|
||||
company_id = current_user.get("company_id")
|
||||
|
||||
if not tenant_id or not company_id:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Access denied: Tenant or Company not found"
|
||||
)
|
||||
|
||||
service = PartService(db)
|
||||
search_params = PartSearchDTO(
|
||||
client_id=client_id,
|
||||
part_number=part_number,
|
||||
description=description,
|
||||
fraction=fraction,
|
||||
supplier=supplier,
|
||||
enabled_only=enabled_only,
|
||||
)
|
||||
return service.list_parts(skip, limit, search_params)
|
||||
|
||||
|
||||
@router.get("/client/{client_id}", response_model=List[PartBasicDTO])
|
||||
async def get_parts_by_client(
|
||||
client_id: int,
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Get all parts for a specific client
|
||||
"""
|
||||
# Validate access to the tenant and company
|
||||
tenant_id = current_user.get("tenant_id")
|
||||
company_id = current_user.get("company_id")
|
||||
|
||||
if not tenant_id or not company_id:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Access denied: Tenant or Company not found"
|
||||
)
|
||||
|
||||
service = PartService(db)
|
||||
return service.search_by_client(client_id, skip, limit)
|
||||
|
||||
|
||||
@router.get("/search/fraction/{fraction}", response_model=List[PartBasicDTO])
|
||||
async def search_by_fraction(
|
||||
fraction: str,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Search parts by tariff fraction
|
||||
"""
|
||||
# Validate access to the tenant and company
|
||||
tenant_id = current_user.get("tenant_id")
|
||||
company_id = current_user.get("company_id")
|
||||
|
||||
if not tenant_id or not company_id:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Access denied: Tenant or Company not found"
|
||||
)
|
||||
|
||||
service = PartService(db)
|
||||
return service.search_by_fraction(fraction)
|
||||
|
||||
|
||||
@router.get("/search/supplier/{supplier}", response_model=List[PartBasicDTO])
|
||||
async def search_by_supplier(
|
||||
supplier: str,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Search parts by supplier
|
||||
"""
|
||||
# Validate access to the tenant and company
|
||||
tenant_id = current_user.get("tenant_id")
|
||||
company_id = current_user.get("company_id")
|
||||
|
||||
if not tenant_id or not company_id:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Access denied: Tenant or Company not found"
|
||||
)
|
||||
|
||||
service = PartService(db)
|
||||
return service.search_by_supplier(supplier)
|
||||
|
||||
|
||||
@router.get("/search/country/{country_code}", response_model=List[PartBasicDTO])
|
||||
async def get_parts_by_country(
|
||||
country_code: str,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Get parts by country of origin
|
||||
"""
|
||||
# Validate access to the tenant and company
|
||||
tenant_id = current_user.get("tenant_id")
|
||||
company_id = current_user.get("company_id")
|
||||
|
||||
if not tenant_id or not company_id:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Access denied: Tenant or Company not found"
|
||||
)
|
||||
|
||||
service = PartService(db)
|
||||
return service.get_parts_by_country(country_code)
|
||||
|
||||
|
||||
@router.get("/statistics", response_model=dict)
|
||||
async def get_parts_statistics(
|
||||
db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Get basic parts statistics
|
||||
"""
|
||||
# Validate access to the tenant and company
|
||||
tenant_id = current_user.get("tenant_id")
|
||||
company_id = current_user.get("company_id")
|
||||
|
||||
if not tenant_id or not company_id:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Access denied: Tenant or Company not found"
|
||||
)
|
||||
|
||||
service = PartService(db)
|
||||
return service.get_parts_statistics()
|
||||
|
||||
|
||||
@router.get("/{client_id}/{part_number}", response_model=PartResponseDTO)
|
||||
async def get_part(
|
||||
client_id: int,
|
||||
part_number: str,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Get part by composite key (client_id + part_number)
|
||||
"""
|
||||
# Validate access to the tenant and company
|
||||
tenant_id = current_user.get("tenant_id")
|
||||
company_id = current_user.get("company_id")
|
||||
|
||||
if not tenant_id or not company_id:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Access denied: Tenant or Company not found"
|
||||
)
|
||||
|
||||
service = PartService(db)
|
||||
part = service.get_part(client_id, part_number)
|
||||
if not part:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Part with client_id '{client_id}' and part_number '{part_number}' not found",
|
||||
)
|
||||
return part
|
||||
|
||||
|
||||
@router.put("/{client_id}/{part_number}", response_model=PartResponseDTO)
|
||||
async def update_part(
|
||||
client_id: int,
|
||||
part_number: str,
|
||||
part_data: PartUpdateDTO,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Update part information
|
||||
"""
|
||||
# Validate access to the tenant and company
|
||||
tenant_id = current_user.get("tenant_id")
|
||||
company_id = current_user.get("company_id")
|
||||
|
||||
if not tenant_id or not company_id:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Access denied: Tenant or Company not found"
|
||||
)
|
||||
|
||||
service = PartService(db)
|
||||
part = service.update_part(client_id, part_number, part_data)
|
||||
if not part:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Part with client_id '{client_id}' and part_number '{part_number}' not found",
|
||||
)
|
||||
return part
|
||||
|
||||
|
||||
@router.delete("/{client_id}/{part_number}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_part(
|
||||
client_id: int,
|
||||
part_number: str,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Delete part from the system
|
||||
|
||||
Note: This will completely remove the part from the system.
|
||||
"""
|
||||
# Validate access to the tenant and company
|
||||
tenant_id = current_user.get("tenant_id")
|
||||
company_id = current_user.get("company_id")
|
||||
|
||||
if not tenant_id or not company_id:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Access denied: Tenant or Company not found"
|
||||
)
|
||||
|
||||
service = PartService(db)
|
||||
if not service.delete_part(client_id, part_number):
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Part with client_id '{client_id}' and part_number '{part_number}' not found",
|
||||
)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/{client_id}/{part_number}/toggle-status", response_model=PartResponseDTO
|
||||
)
|
||||
async def toggle_part_status(
|
||||
client_id: int,
|
||||
part_number: str,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Toggle part enabled/disabled status
|
||||
"""
|
||||
# Validate access to the tenant and company
|
||||
tenant_id = current_user.get("tenant_id")
|
||||
company_id = current_user.get("company_id")
|
||||
|
||||
if not tenant_id or not company_id:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Access denied: Tenant or Company not found"
|
||||
)
|
||||
|
||||
service = PartService(db)
|
||||
part = service.toggle_status(client_id, part_number)
|
||||
if not part:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Part with client_id '{client_id}' and part_number '{part_number}' not found",
|
||||
)
|
||||
return part
|
||||
|
||||
|
||||
# Endpoints específicos para información detallada
|
||||
@router.get("/{client_id}/{part_number}/basic", response_model=PartBasicDTO)
|
||||
async def get_part_basic_info(
|
||||
client_id: int,
|
||||
part_number: str,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Get basic information for a part
|
||||
"""
|
||||
# Validate access to the tenant and company
|
||||
tenant_id = current_user.get("tenant_id")
|
||||
company_id = current_user.get("company_id")
|
||||
|
||||
if not tenant_id or not company_id:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Access denied: Tenant or Company not found"
|
||||
)
|
||||
|
||||
service = PartService(db)
|
||||
part = service.get_part(client_id, part_number)
|
||||
if not part:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Part with client_id '{client_id}' and part_number '{part_number}' not found",
|
||||
)
|
||||
|
||||
return PartBasicDTO(
|
||||
client_id=part.client_id,
|
||||
part_number=part.part_number,
|
||||
description_spanish=part.description_spanish,
|
||||
description_english=part.description_english,
|
||||
part_class=part.part_class,
|
||||
unit_cost=part.unit_cost,
|
||||
currency_key=part.currency_key,
|
||||
is_active=part.is_active,
|
||||
)
|
||||
|
||||
|
||||
@router.get("/{client_id}/{part_number}/regulatory", response_model=dict)
|
||||
async def get_part_regulatory_info(
|
||||
client_id: int,
|
||||
part_number: str,
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Get regulatory information for a part (FDA, FCC, ECCN, etc.)
|
||||
"""
|
||||
# Validate access to the tenant and company
|
||||
tenant_id = current_user.get("tenant_id")
|
||||
company_id = current_user.get("company_id")
|
||||
|
||||
if not tenant_id or not company_id:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Access denied: Tenant or Company not found"
|
||||
)
|
||||
|
||||
service = PartService(db)
|
||||
part = service.get_part(client_id, part_number)
|
||||
if not part:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Part with client_id '{client_id}' and part_number '{part_number}' not found",
|
||||
)
|
||||
|
||||
return {
|
||||
"client_id": part.client_id,
|
||||
"part_number": part.part_number,
|
||||
"fraction": part.fraction,
|
||||
"us_fraction": part.us_fraction,
|
||||
"fda_key": part.fda_key,
|
||||
"fcc_key": part.fcc_key,
|
||||
"license_code": part.license_code,
|
||||
"eccn": part.eccn,
|
||||
"export_code": part.export_code,
|
||||
"exclusion_symbol": part.exclusion_symbol,
|
||||
}
|
||||
router = TenantCRUDRoutes(
|
||||
service=PartService,
|
||||
create_schema=PartCreateDTO,
|
||||
update_schema=PartUpdateDTO,
|
||||
response_schema=PartResponseDTO,
|
||||
prefix="/parts",
|
||||
tags=["a76 / parts"],
|
||||
resource_name="Part",
|
||||
id_name="part_id",
|
||||
enable_list=True,
|
||||
enable_filters=True,
|
||||
).router
|
||||
@@ -1,309 +1,179 @@
|
||||
"""
|
||||
Capa de servicio para lógica de negocio de partes/componentes
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy import and_, func, or_
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .dto import PartCreateDTO, PartUpdateDTO
|
||||
# Importamos el modelo PRINCIPAL
|
||||
from .models import Part
|
||||
|
||||
# Importamos TODOS los DTOs necesarios
|
||||
from .dto import (
|
||||
PartCreateDTO,
|
||||
PartUpdateDTO,
|
||||
PartResponseDTO,
|
||||
PartBasicDTO # ¡Importante tener este!
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PartService:
|
||||
"""
|
||||
Servicio para gestión de partes/componentes
|
||||
"""
|
||||
"""Servicio para gestión de Partes (Anexo 76 + Anexo 24)"""
|
||||
|
||||
@staticmethod
|
||||
def create_part(db: Session, part_data: PartCreateDTO) -> Part:
|
||||
"""
|
||||
Crear una nueva parte
|
||||
"""
|
||||
def get_all(
|
||||
db: Session,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
) -> tuple[List[Part], int]:
|
||||
|
||||
query = db.query(Part).filter(
|
||||
Part.tenant_id == tenant_id,
|
||||
Part.company_id == company_id
|
||||
)
|
||||
|
||||
if filters:
|
||||
if filters.get("q"):
|
||||
search = f"%{filters['q']}%"
|
||||
query = query.filter(
|
||||
or_(
|
||||
Part.part_number.ilike(search),
|
||||
Part.description_spanish.ilike(search),
|
||||
Part.commercial_part_number.ilike(search)
|
||||
)
|
||||
)
|
||||
# Otros filtros...
|
||||
|
||||
total = query.count()
|
||||
items = query.offset(skip).limit(limit).all()
|
||||
return items, total
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(db: Session, part_id: int, tenant_id: int, company_id: int) -> Optional[Part]:
|
||||
return db.query(Part).filter(
|
||||
Part.id == part_id,
|
||||
Part.tenant_id == tenant_id,
|
||||
Part.company_id == company_id
|
||||
).first()
|
||||
|
||||
@staticmethod
|
||||
def create(db: Session, part_data: PartCreateDTO, tenant_id: int, company_id: int) -> Part:
|
||||
# 1. Preparar datos
|
||||
data = part_data.model_dump()
|
||||
|
||||
# Separar datos anidados
|
||||
fa_dict = data.pop('fa_data', None)
|
||||
inv_dict = data.pop('inv_data', None)
|
||||
|
||||
# Inyectar IDs de contexto (Seguridad Multi-tenant)
|
||||
data['company_id'] = company_id
|
||||
data['tenant_id'] = tenant_id
|
||||
|
||||
# 2. Verificar duplicados (Usando la UniqueConstraint del modelo)
|
||||
existing = db.query(Part).filter(
|
||||
Part.tenant_id == tenant_id,
|
||||
Part.company_id == company_id,
|
||||
Part.part_number == data['part_number']
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"El número de parte '{data['part_number']}' ya existe."
|
||||
)
|
||||
|
||||
# 3. Crear objeto Part
|
||||
db_part = Part(**data)
|
||||
|
||||
# 4. Crear relaciones (Anexo 24)
|
||||
# Importamos aquí para evitar ciclos, usando las rutas de tu modelo
|
||||
if fa_dict:
|
||||
from api.v1.modules.a24.fa.fa_parts.models import FaPart
|
||||
# Importante: Pasar tenant/company también al hijo
|
||||
db_part.fa_data = FaPart(**fa_dict, tenant_id=tenant_id, company_id=company_id)
|
||||
|
||||
if inv_dict:
|
||||
from api.v1.modules.a24.inv.inv_parts.models import InvPart
|
||||
db_part.inv_data = InvPart(**inv_dict, tenant_id=tenant_id, company_id=company_id)
|
||||
|
||||
try:
|
||||
db_part = Part(**part_data.model_dump())
|
||||
db.add(db_part)
|
||||
db.commit()
|
||||
db.refresh(db_part)
|
||||
return db_part
|
||||
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
err_msg = str(e.orig)
|
||||
logger.error(f"Error DB creando parte: {err_msg}")
|
||||
|
||||
if "foreign key" in err_msg:
|
||||
if "unit_of_measure" in err_msg:
|
||||
raise HTTPException(400, "La Unidad de Medida no existe en el catálogo.")
|
||||
if "currency_key" in err_msg:
|
||||
raise HTTPException(400, "La Moneda no existe en el catálogo.")
|
||||
if "part_class" in err_msg:
|
||||
raise HTTPException(400, "La Clase no existe para este cliente.")
|
||||
|
||||
# El error genérico si falla Company/Client
|
||||
raise HTTPException(400, "Error de referencia: Verifique Cliente, Compañía o Catálogos.")
|
||||
|
||||
raise HTTPException(400, "Error al guardar la parte.")
|
||||
|
||||
@staticmethod
|
||||
def update(db: Session, part_id: int, tenant_id: int, part_data: PartUpdateDTO, company_id: int) -> Optional[Part]:
|
||||
db_part = PartService.get_by_id(db, part_id, tenant_id, company_id)
|
||||
if not db_part:
|
||||
return None
|
||||
|
||||
data = part_data.model_dump(exclude_unset=True)
|
||||
fa_dict = data.pop('fa_data', None)
|
||||
inv_dict = data.pop('inv_data', None)
|
||||
|
||||
# Actualizar campos directos
|
||||
for key, value in data.items():
|
||||
setattr(db_part, key, value)
|
||||
|
||||
# Actualizar FA Data
|
||||
if fa_dict is not None:
|
||||
if db_part.fa_data:
|
||||
for k, v in fa_dict.items():
|
||||
setattr(db_part.fa_data, k, v)
|
||||
else:
|
||||
from api.v1.modules.a24.fa.fa_parts.models import FaPart
|
||||
db_part.fa_data = FaPart(**fa_dict, tenant_id=tenant_id, company_id=company_id)
|
||||
|
||||
# Actualizar INV Data
|
||||
if inv_dict is not None:
|
||||
if db_part.inv_data:
|
||||
for k, v in inv_dict.items():
|
||||
setattr(db_part.inv_data, k, v)
|
||||
else:
|
||||
from api.v1.modules.a24.inv.inv_parts.models import InvPart
|
||||
db_part.inv_data = InvPart(**inv_dict, tenant_id=tenant_id, company_id=company_id)
|
||||
|
||||
try:
|
||||
db.commit()
|
||||
db.refresh(db_part)
|
||||
return db_part
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error creating part: {e}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Part with this client_id and part_number already exists",
|
||||
)
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Unexpected error creating part: {e}")
|
||||
raise HTTPException(status_code=500, detail="Error creating part")
|
||||
raise HTTPException(400, f"Error actualizando: {str(e.orig)}")
|
||||
|
||||
@staticmethod
|
||||
def get_part(db: Session, client_id: int, part_number: str) -> Optional[Part]:
|
||||
"""
|
||||
Obtener una parte por clave de cliente y número de parte
|
||||
"""
|
||||
def delete(db: Session, part_id: int, tenant_id: int, company_id: int) -> bool:
|
||||
db_part = PartService.get_by_id(db, part_id, tenant_id, company_id)
|
||||
if not db_part: return False
|
||||
|
||||
try:
|
||||
return (
|
||||
db.query(Part)
|
||||
.filter(
|
||||
and_(Part.client_id == client_id, Part.part_number == part_number)
|
||||
)
|
||||
.first()
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting part: {e}")
|
||||
raise HTTPException(status_code=500, detail="Error retrieving part")
|
||||
|
||||
@staticmethod
|
||||
def get_parts_paginated(
|
||||
db: Session,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
search: Optional[str] = None,
|
||||
client_id: Optional[int] = None,
|
||||
fraction: Optional[str] = None,
|
||||
country_of_origin: Optional[str] = None,
|
||||
) -> tuple[List[Part], int]:
|
||||
"""
|
||||
Obtener partes con paginación y filtros
|
||||
"""
|
||||
try:
|
||||
query = db.query(Part)
|
||||
|
||||
# Aplicar filtros
|
||||
if search:
|
||||
query = query.filter(
|
||||
or_(
|
||||
Part.description_spanish.ilike(f"%{search}%"),
|
||||
Part.description_english.ilike(f"%{search}%"),
|
||||
Part.part_number.ilike(f"%{search}%"),
|
||||
)
|
||||
)
|
||||
|
||||
if client_id is not None:
|
||||
query = query.filter(Part.client_id == client_id)
|
||||
|
||||
if fraction:
|
||||
query = query.filter(Part.fraction == fraction)
|
||||
|
||||
if country_of_origin:
|
||||
query = query.filter(Part.country_of_origin == country_of_origin)
|
||||
|
||||
# Contar total
|
||||
total = query.count()
|
||||
|
||||
# Aplicar paginación
|
||||
parts = query.offset(skip).limit(limit).all()
|
||||
|
||||
return parts, total
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting paginated parts: {e}")
|
||||
raise HTTPException(status_code=500, detail="Error retrieving parts")
|
||||
|
||||
@staticmethod
|
||||
def get_parts_by_client(db: Session, client_id: int) -> List[Part]:
|
||||
"""
|
||||
Obtener todas las partes de un cliente específico
|
||||
"""
|
||||
try:
|
||||
return db.query(Part).filter(Part.client_id == client_id).all()
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting parts by client: {e}")
|
||||
raise HTTPException(status_code=500, detail="Error retrieving client parts")
|
||||
|
||||
@staticmethod
|
||||
def search_parts_by_fraction(db: Session, fraction: str) -> List[Part]:
|
||||
"""
|
||||
Buscar partes por fracción arancelaria
|
||||
"""
|
||||
try:
|
||||
return (
|
||||
db.query(Part)
|
||||
.filter(
|
||||
or_(
|
||||
Part.fraction.ilike(f"%{fraction}%"),
|
||||
Part.us_fraction.ilike(f"%{fraction}%"),
|
||||
)
|
||||
)
|
||||
.all()
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error searching parts by fraction: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail="Error searching parts by fraction"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def search_parts_by_supplier(db: Session, supplier: str) -> List[Part]:
|
||||
"""
|
||||
Buscar partes por proveedor
|
||||
"""
|
||||
try:
|
||||
return db.query(Part).filter(Part.supplier.ilike(f"%{supplier}%")).all()
|
||||
except Exception as e:
|
||||
logger.error(f"Error searching parts by supplier: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail="Error searching parts by supplier"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def search_parts_by_country(db: Session, country_code: str) -> List[Part]:
|
||||
"""
|
||||
Buscar partes por país de origen
|
||||
"""
|
||||
try:
|
||||
return db.query(Part).filter(Part.country_of_origin == country_code).all()
|
||||
except Exception as e:
|
||||
logger.error(f"Error searching parts by country: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail="Error searching parts by country"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def update_part(
|
||||
db: Session, client_id: int, part_number: str, part_data: PartUpdateDTO
|
||||
) -> Optional[Part]:
|
||||
"""
|
||||
Actualizar una parte existente
|
||||
"""
|
||||
try:
|
||||
db_part = PartService.get_part(db, client_id, part_number)
|
||||
if not db_part:
|
||||
return None
|
||||
|
||||
# Actualizar campos
|
||||
for field, value in part_data.model_dump(exclude_unset=True).items():
|
||||
setattr(db_part, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_part)
|
||||
return db_part
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error updating part: {e}")
|
||||
raise HTTPException(status_code=500, detail="Error updating part")
|
||||
|
||||
@staticmethod
|
||||
def delete_part(db: Session, client_id: int, part_number: str) -> bool:
|
||||
"""
|
||||
Eliminar una parte
|
||||
"""
|
||||
try:
|
||||
db_part = PartService.get_part(db, client_id, part_number)
|
||||
if not db_part:
|
||||
return False
|
||||
|
||||
db.delete(db_part)
|
||||
db.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
db.rollback()
|
||||
logger.error(f"Error deleting part: {e}")
|
||||
raise HTTPException(status_code=500, detail="Error deleting part")
|
||||
|
||||
@staticmethod
|
||||
def toggle_part_status(
|
||||
db: Session, client_id: int, part_number: str
|
||||
) -> Optional[Part]:
|
||||
"""
|
||||
Cambiar el estado habilitado/deshabilitado de una parte
|
||||
"""
|
||||
try:
|
||||
db_part = PartService.get_part(db, client_id, part_number)
|
||||
if not db_part:
|
||||
return None
|
||||
|
||||
# Toggle status (assuming 1 = enabled, 0 = disabled)
|
||||
db_part.is_active = 1 if db_part.is_active == 0 else 0
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_part)
|
||||
return db_part
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error toggling part status: {e}")
|
||||
raise HTTPException(status_code=500, detail="Error toggling part status")
|
||||
|
||||
@staticmethod
|
||||
def get_parts_statistics(db: Session) -> dict:
|
||||
"""
|
||||
Obtener estadísticas de partes
|
||||
"""
|
||||
try:
|
||||
total_parts = db.query(Part).count()
|
||||
|
||||
# Partes por cliente
|
||||
parts_by_client = (
|
||||
db.query(Part.client_id, func.count(Part.part_number).label("count"))
|
||||
.group_by(Part.client_id)
|
||||
.all()
|
||||
)
|
||||
|
||||
# Partes por país de origen
|
||||
parts_by_country = (
|
||||
db.query(
|
||||
Part.country_of_origin, func.count(Part.part_number).label("count")
|
||||
)
|
||||
.filter(Part.country_of_origin.isnot(None))
|
||||
.group_by(Part.country_of_origin)
|
||||
.all()
|
||||
)
|
||||
|
||||
# Partes habilitadas vs deshabilitadas
|
||||
enabled_parts = db.query(Part).filter(Part.is_active == 1).count()
|
||||
disabled_parts = db.query(Part).filter(Part.is_active == 0).count()
|
||||
|
||||
return {
|
||||
"total_parts": total_parts,
|
||||
"enabled_parts": enabled_parts,
|
||||
"disabled_parts": disabled_parts,
|
||||
"parts_by_client": [
|
||||
{"client_id": item[0], "count": item[1]} for item in parts_by_client
|
||||
],
|
||||
"parts_by_country": [
|
||||
{"country": item[0], "count": item[1]} for item in parts_by_country
|
||||
],
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting parts statistics: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail="Error retrieving parts statistics"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_part_regulatory_info(
|
||||
db: Session, client_id: int, part_number: str
|
||||
) -> Optional[dict]:
|
||||
"""
|
||||
Obtener información regulatoria específica de una parte
|
||||
"""
|
||||
try:
|
||||
db_part = PartService.get_part(db, client_id, part_number)
|
||||
if not db_part:
|
||||
return None
|
||||
|
||||
return {
|
||||
"client_id": db_part.client_id,
|
||||
"part_number": db_part.part_number,
|
||||
"fraction": db_part.fraction,
|
||||
"us_fraction": db_part.us_fraction,
|
||||
"fda_key": db_part.fda_key,
|
||||
"fcc_key": db_part.fcc_key,
|
||||
"license_code": db_part.license_code,
|
||||
"eccn": db_part.eccn,
|
||||
"export_code": db_part.export_code,
|
||||
"exclusion_symbol": db_part.exclusion_symbol,
|
||||
"country_of_origin": db_part.country_of_origin,
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting part regulatory info: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail="Error retrieving part regulatory information"
|
||||
)
|
||||
raise HTTPException(500, "Error eliminando parte")
|
||||
@@ -23,6 +23,7 @@ class PedimentoConfigAdditionalBase(BaseModel):
|
||||
None, description="Send 502 validation file for consolidated"
|
||||
)
|
||||
add_remove_norms: Optional[bool] = Field(None, description="Add/remove norms")
|
||||
choose_invoice_cove: Optional[bool] = Field(None, description="Choose invoice COVE in items")
|
||||
|
||||
|
||||
class PedimentoConfigAdditionalCreate(PedimentoConfigAdditionalBase):
|
||||
@@ -40,6 +41,7 @@ class PedimentoConfigAdditionalUpdate(BaseModel):
|
||||
enable_import_invoice_recipient: Optional[bool] = None
|
||||
send_502_validation_file_for_consolidated: Optional[bool] = None
|
||||
add_remove_norms: Optional[bool] = None
|
||||
choose_invoice_cove: Optional[bool] = None
|
||||
|
||||
|
||||
class PedimentoConfigAdditionalResponse(PedimentoConfigAdditionalBase):
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
from decimal import Decimal
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
class PedimentoContributionBase(BaseModel):
|
||||
"""Base schema for Pedimento Contribution"""
|
||||
|
||||
pedimento_id: Optional[int] = None
|
||||
contribucion: Optional[str] = None
|
||||
tipo_tasa: Optional[str] = None
|
||||
tasa: Optional[Decimal] = None
|
||||
forma_pago: Optional[str] = None
|
||||
importe: Optional[Decimal] = None
|
||||
gravamen: Optional[str] = None
|
||||
abreviacion: Optional[str] = None
|
||||
forma_pago_2: Optional[str] = None
|
||||
importe_2: Optional[Decimal] = None
|
||||
|
||||
|
||||
class PedimentoContributionCreate(PedimentoContributionBase):
|
||||
"""Schema for creating Pedimento Contribution"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class PedimentoContributionUpdate(BaseModel):
|
||||
"""Schema for updating Pedimento Contribution"""
|
||||
|
||||
contribucion: Optional[str] = None
|
||||
tipo_tasa: Optional[str] = None
|
||||
tasa: Optional[Decimal] = None
|
||||
forma_pago: Optional[str] = None
|
||||
importe: Optional[Decimal] = None
|
||||
gravamen: Optional[str] = None
|
||||
abreviacion: Optional[str] = None
|
||||
forma_pago_2: Optional[str] = None
|
||||
importe_2: Optional[Decimal] = None
|
||||
|
||||
|
||||
class PedimentoContributionResponse(PedimentoContributionBase):
|
||||
"""Schema for Pedimento Contribution response"""
|
||||
|
||||
id: int
|
||||
created_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -7,9 +7,9 @@ from pydantic import BaseModel, ConfigDict, Field
|
||||
class PedimentoDatesBase(BaseModel):
|
||||
"""Base schema for Pedimento Dates"""
|
||||
|
||||
entry_date: Optional[datetime] = Field(None, description="Entry date")
|
||||
entry_date: datetime = Field(..., description="Entry date")
|
||||
pedimento_date: Optional[datetime] = Field(None, description="Pedimento date")
|
||||
payment_date: datetime = Field(..., description="Payment date")
|
||||
payment_date: Optional[datetime] = Field(None, description="Payment date")
|
||||
rectification_payment_date: Optional[datetime] = Field(
|
||||
None, description="Rectification payment date"
|
||||
)
|
||||
@@ -18,7 +18,7 @@ class PedimentoDatesBase(BaseModel):
|
||||
eucan_date: Optional[datetime] = Field(None, description="EUCAN date")
|
||||
original_date: Optional[datetime] = Field(None, description="Original date")
|
||||
start_date: Optional[datetime] = Field(None, description="Start date")
|
||||
end_date: Optional[datetime] = Field(None, description="End date")
|
||||
end_date: datetime = Field(..., description="End date")
|
||||
|
||||
|
||||
class PedimentoDatesCreate(BaseModel):
|
||||
@@ -26,14 +26,14 @@ class PedimentoDatesCreate(BaseModel):
|
||||
|
||||
entry_date: datetime = Field(..., description="Entry date")
|
||||
pedimento_date: Optional[datetime] = Field(None, description="Pedimento date")
|
||||
payment_date: datetime = Field(..., description="Payment date")
|
||||
payment_date: Optional[datetime] = Field(None, description="Payment date")
|
||||
rectification_payment_date: Optional[datetime] = Field(None, description="Rectification payment date")
|
||||
extraction_date: Optional[datetime] = Field(None, description="Extraction date")
|
||||
submission_date: Optional[datetime] = Field(None, description="Submission date")
|
||||
eucan_date: Optional[datetime] = Field(None, description="EUCAN date")
|
||||
original_date: Optional[datetime] = Field(None, description="Original date")
|
||||
start_date: Optional[datetime] = Field(None, description="Start date")
|
||||
end_date: Optional[datetime] = Field(None, description="End date")
|
||||
end_date: datetime = Field(..., description="End date")
|
||||
|
||||
|
||||
class PedimentoDatesUpdate(BaseModel):
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
class PedimentoPackagesBase(BaseModel):
|
||||
"""Base schema for Pedimento Packages"""
|
||||
|
||||
pedimento_id: Optional[int] = None
|
||||
quantity: Optional[int] = None
|
||||
brand: Optional[str] = None
|
||||
number: Optional[str] = None
|
||||
vehicles: Optional[int] = None
|
||||
|
||||
|
||||
class PedimentoPackagesCreate(PedimentoPackagesBase):
|
||||
"""Schema for creating Pedimento Packages"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class PedimentoPackagesUpdate(BaseModel):
|
||||
"""Schema for updating Pedimento Packages"""
|
||||
|
||||
quantity: Optional[int] = None
|
||||
brand: Optional[str] = None
|
||||
number: Optional[str] = None
|
||||
vehicles: Optional[int] = None
|
||||
|
||||
|
||||
class PedimentoPackagesResponse(PedimentoPackagesBase):
|
||||
"""Schema for Pedimento Packages response"""
|
||||
|
||||
id: int
|
||||
created_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class PedimentoTransportCarrierBase(BaseModel):
|
||||
"""Base schema for Pedimento Transport Carrier"""
|
||||
|
||||
pedimento_id: Optional[int] = None
|
||||
carrier: Optional[str] = None
|
||||
rfc: Optional[str] = None
|
||||
curp: Optional[str] = None
|
||||
name: Optional[str] = None
|
||||
address: Optional[str] = None
|
||||
city: Optional[str] = None
|
||||
state: Optional[str] = None
|
||||
country: Optional[str] = None
|
||||
tax_id: Optional[str] = None
|
||||
total_packages: Optional[int] = None
|
||||
identification: Optional[str] = None
|
||||
|
||||
|
||||
class PedimentoTransportCarrierCreate(PedimentoTransportCarrierBase):
|
||||
"""Schema for creating Pedimento Transport Carrier"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class PedimentoTransportCarrierUpdate(BaseModel):
|
||||
"""Schema for updating Pedimento Transport Carrier"""
|
||||
|
||||
carrier: Optional[str] = None
|
||||
rfc: Optional[str] = None
|
||||
curp: Optional[str] = None
|
||||
name: Optional[str] = None
|
||||
address: Optional[str] = None
|
||||
city: Optional[str] = None
|
||||
state: Optional[str] = None
|
||||
country: Optional[str] = None
|
||||
tax_id: Optional[str] = None
|
||||
total_packages: Optional[int] = None
|
||||
identification: Optional[str] = None
|
||||
|
||||
|
||||
class PedimentoTransportCarrierResponse(PedimentoTransportCarrierBase):
|
||||
"""Schema for Pedimento Transport Carrier response"""
|
||||
|
||||
id: int
|
||||
created_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class PedimentoSealBase(BaseModel):
|
||||
"""Base schema for Pedimento Seal"""
|
||||
|
||||
pedimento_id: Optional[int] = None
|
||||
number: Optional[str] = None
|
||||
identification: Optional[str] = None
|
||||
|
||||
|
||||
class PedimentoSealCreate(PedimentoSealBase):
|
||||
"""Schema for creating Pedimento Seal"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class PedimentoSealUpdate(BaseModel):
|
||||
"""Schema for updating Pedimento Seal"""
|
||||
|
||||
number: Optional[str] = None
|
||||
identification: Optional[str] = None
|
||||
|
||||
|
||||
class PedimentoSealResponse(PedimentoSealBase):
|
||||
"""Schema for Pedimento Seal response"""
|
||||
|
||||
id: int
|
||||
created_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class PedimentoContainerBase(BaseModel):
|
||||
"""Base schema for Pedimento Container"""
|
||||
|
||||
pedimento_id: Optional[int] = None
|
||||
number: Optional[str] = None
|
||||
identification: Optional[str] = None
|
||||
type: Optional[str] = None
|
||||
|
||||
|
||||
class PedimentoContainerCreate(PedimentoContainerBase):
|
||||
"""Schema for creating Pedimento Container"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class PedimentoContainerUpdate(BaseModel):
|
||||
"""Schema for updating Pedimento Container"""
|
||||
|
||||
number: Optional[str] = None
|
||||
identification: Optional[str] = None
|
||||
type: Optional[str] = None
|
||||
|
||||
|
||||
class PedimentoContainerResponse(PedimentoContainerBase):
|
||||
"""Schema for Pedimento Container response"""
|
||||
|
||||
id: int
|
||||
created_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class PedimentoGuideBase(BaseModel):
|
||||
"""Base schema for Pedimento Guide"""
|
||||
|
||||
pedimento_id: Optional[int] = None
|
||||
guide: Optional[str] = None
|
||||
identifier: Optional[str] = None
|
||||
|
||||
|
||||
class PedimentoGuideCreate(PedimentoGuideBase):
|
||||
"""Schema for creating Pedimento Guide"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class PedimentoGuideUpdate(BaseModel):
|
||||
"""Schema for updating Pedimento Guide"""
|
||||
|
||||
guide: Optional[str] = None
|
||||
identifier: Optional[str] = None
|
||||
|
||||
|
||||
class PedimentoGuideResponse(PedimentoGuideBase):
|
||||
"""Schema for Pedimento Guide response"""
|
||||
|
||||
id: int
|
||||
created_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -1,33 +1,80 @@
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from enum import IntEnum
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from .pedimento_config_additional import PedimentoConfigAdditionalCreate, PedimentoConfigAdditionalResponse
|
||||
from .pedimento_config_calculations import PedimentoConfigCalculationsCreate, PedimentoConfigCalculationsResponse
|
||||
from .pedimento_config_parameters import PedimentoConfigParametersCreate, PedimentoConfigParametersResponse
|
||||
from .pedimento_config_surcharges import PedimentoConfigSurchargesCreate, PedimentoConfigSurchargesResponse
|
||||
from .pedimento_config_update_rectification import PedimentoConfigUpdateRectificationCreate, PedimentoConfigUpdateRectificationResponse
|
||||
from .pedimento_config_updates import PedimentoConfigUpdatesCreate, PedimentoConfigUpdatesResponse
|
||||
from .pedimento_customs_offices import PedimentoCustomsOfficesCreate, PedimentoCustomsOfficesResponse
|
||||
from ..models.pedimentos import OperationType, PedimentoType
|
||||
from .pedimento_config_additional import (
|
||||
PedimentoConfigAdditionalCreate,
|
||||
PedimentoConfigAdditionalResponse,
|
||||
)
|
||||
from .pedimento_config_calculations import (
|
||||
PedimentoConfigCalculationsCreate,
|
||||
PedimentoConfigCalculationsResponse,
|
||||
)
|
||||
from .pedimento_config_parameters import (
|
||||
PedimentoConfigParametersCreate,
|
||||
PedimentoConfigParametersResponse,
|
||||
)
|
||||
from .pedimento_config_surcharges import (
|
||||
PedimentoConfigSurchargesCreate,
|
||||
PedimentoConfigSurchargesResponse,
|
||||
)
|
||||
from .pedimento_config_update_rectification import (
|
||||
PedimentoConfigUpdateRectificationCreate,
|
||||
PedimentoConfigUpdateRectificationResponse,
|
||||
)
|
||||
from .pedimento_config_updates import (
|
||||
PedimentoConfigUpdatesCreate,
|
||||
PedimentoConfigUpdatesResponse,
|
||||
)
|
||||
from .pedimento_customs_offices import (
|
||||
PedimentoCustomsOfficesCreate,
|
||||
PedimentoCustomsOfficesResponse,
|
||||
)
|
||||
from .pedimento_dates import PedimentoDatesCreate, PedimentoDatesResponse
|
||||
from .pedimento_decrementables import PedimentoDecrementablesCreate, PedimentoDecrementablesResponse
|
||||
from .pedimento_incrementables import PedimentoIncrementablesCreate, PedimentoIncrementablesResponse
|
||||
from .pedimento_decrementables import (
|
||||
PedimentoDecrementablesCreate,
|
||||
PedimentoDecrementablesResponse,
|
||||
)
|
||||
from .pedimento_incrementables import (
|
||||
PedimentoIncrementablesCreate,
|
||||
PedimentoIncrementablesResponse,
|
||||
)
|
||||
from .pedimento_indexes import PedimentoIndexesCreate, PedimentoIndexesResponse
|
||||
from .pedimento_packages_transport import (
|
||||
PedimentoContainerCreate,
|
||||
PedimentoContainerResponse,
|
||||
PedimentoPackagesCreate,
|
||||
PedimentoPackagesResponse,
|
||||
PedimentoSealCreate,
|
||||
PedimentoSealResponse,
|
||||
PedimentoTransportCarrierCreate,
|
||||
PedimentoTransportCarrierResponse,
|
||||
PedimentoGuideCreate,
|
||||
PedimentoGuideResponse,
|
||||
)
|
||||
from .pedimento_contributions import (
|
||||
PedimentoContributionCreate,
|
||||
PedimentoContributionResponse,
|
||||
)
|
||||
from .pedimento_payments import PedimentoPaymentsCreate, PedimentoPaymentsResponse
|
||||
from .pedimento_rectification_destination import PedimentoRectificationDestinationCreate, PedimentoRectificationDestinationResponse
|
||||
from .pedimento_rectification_origin import PedimentoRectificationOriginCreate, PedimentoRectificationOriginResponse
|
||||
from .pedimento_transport_means import PedimentoTransportMeansCreate, PedimentoTransportMeansResponse
|
||||
from .pedimento_rectification_destination import (
|
||||
PedimentoRectificationDestinationCreate,
|
||||
PedimentoRectificationDestinationResponse,
|
||||
)
|
||||
from .pedimento_rectification_origin import (
|
||||
PedimentoRectificationOriginCreate,
|
||||
PedimentoRectificationOriginResponse,
|
||||
)
|
||||
from .pedimento_transport_means import (
|
||||
PedimentoTransportMeansCreate,
|
||||
PedimentoTransportMeansResponse,
|
||||
)
|
||||
from .pedimento_validation import PedimentoValidationCreate, PedimentoValidationResponse
|
||||
|
||||
|
||||
class OperationType(IntEnum):
|
||||
EXPORTACION = 1
|
||||
IMPORTACION = 2
|
||||
|
||||
|
||||
class PedimentosBase(BaseModel):
|
||||
"""Base schema for Pedimentos"""
|
||||
|
||||
@@ -40,18 +87,17 @@ class PedimentosBase(BaseModel):
|
||||
None, max_length=7, description="Pedimento number"
|
||||
)
|
||||
client_id: Optional[int] = Field(None, description="Client ID")
|
||||
operation_type: Optional[int] = Field(None, description="Operation type")
|
||||
pedimento_type: Optional[str] = Field(None, max_length=20, description="Pedimento type")
|
||||
pedimento_code: str = Field(
|
||||
..., max_length=2, description="Pedimento key"
|
||||
)
|
||||
operation_type: Optional[OperationType] = Field(None, description="Operation type")
|
||||
pedimento_type: Optional[PedimentoType] = Field(None, description="Pedimento type")
|
||||
pedimento_code: str = Field(..., max_length=2, description="Pedimento key")
|
||||
regime: str = Field(..., max_length=3, description="Regime")
|
||||
status: Optional[str] = Field(None, max_length=30, description="Status")
|
||||
usd_value: Optional[Decimal] = Field(None, description="USD value")
|
||||
paid_price: Optional[Decimal] = Field(None, description="Paid price")
|
||||
gross_weight: Optional[Decimal] = Field(None, description="Gross weight")
|
||||
exchange_rate: Optional[Decimal] = Field(None, description="Exchange rate")
|
||||
observations: Optional[str] = Field(None, description="Observations")
|
||||
observations: Optional[str] = Field(None, description="Observations")
|
||||
|
||||
|
||||
class PedimentosCreate(PedimentosBase):
|
||||
"""Schema for creating a new Pedimento"""
|
||||
@@ -63,27 +109,36 @@ class PedimentosCreate(PedimentosBase):
|
||||
pedimento_number: str = Field(..., max_length=7, description="Pedimento number")
|
||||
client_id: int = Field(..., description="Client ID")
|
||||
# operation_type, pedimento_type, status son opcionales - se pueden llenar después
|
||||
pedimento_code: str = Field(
|
||||
..., max_length=2, description="Pedimento key"
|
||||
)
|
||||
regime: str = Field(..., max_length=3, description="Regime")
|
||||
|
||||
pedimento_code: str = Field(..., max_length=2, description="Pedimento key")
|
||||
regime: str = Field(..., max_length=3, description="Regime")
|
||||
|
||||
pedimento_dates: Optional[PedimentoDatesCreate] = None
|
||||
pedimento_decrementables: Optional[PedimentoDecrementablesCreate] = None
|
||||
pedimento_incrementables: Optional[PedimentoIncrementablesCreate] = None
|
||||
pedimento_indexes: Optional[PedimentoIndexesCreate] = None
|
||||
pedimento_validation: Optional[PedimentoValidationCreate] = None
|
||||
pedimento_customs_offices: Optional[PedimentoCustomsOfficesCreate] = None
|
||||
pedimento_validation: Optional[PedimentoValidationCreate] = None
|
||||
pedimento_customs_offices: Optional[PedimentoCustomsOfficesCreate] = None
|
||||
pedimento_payments: Optional[PedimentoPaymentsCreate] = None
|
||||
pedimento_rectification_destination: Optional[PedimentoRectificationDestinationCreate] = None
|
||||
pedimento_rectification_destination: Optional[
|
||||
PedimentoRectificationDestinationCreate
|
||||
] = None
|
||||
pedimento_rectification_origin: Optional[PedimentoRectificationOriginCreate] = None
|
||||
pedimento_transport_means: Optional[PedimentoTransportMeansCreate] = None
|
||||
pedimento_transport_means: Optional[PedimentoTransportMeansCreate] = None
|
||||
pedimento_config_additional: Optional[PedimentoConfigAdditionalCreate] = None
|
||||
pedimento_config_calculations: Optional[PedimentoConfigCalculationsCreate] = None
|
||||
pedimento_config_parameters: Optional[PedimentoConfigParametersCreate] = None
|
||||
pedimento_config_surcharges: Optional[PedimentoConfigSurchargesCreate] = None
|
||||
pedimento_config_update_rectification: Optional[PedimentoConfigUpdateRectificationCreate] = None
|
||||
pedimento_config_update_rectification: Optional[
|
||||
PedimentoConfigUpdateRectificationCreate
|
||||
] = None
|
||||
pedimento_config_updates: Optional[PedimentoConfigUpdatesCreate] = None
|
||||
pedimento_packages: Optional[PedimentoPackagesCreate] = None
|
||||
pedimento_transport_carriers: Optional[list[PedimentoTransportCarrierCreate]] = None
|
||||
pedimento_guides: Optional[list[PedimentoGuideCreate]] = None
|
||||
pedimento_contributions: Optional[list[PedimentoContributionCreate]] = None
|
||||
pedimento_seals: Optional[list[PedimentoSealCreate]] = None
|
||||
pedimento_containers: Optional[list[PedimentoContainerCreate]] = None
|
||||
|
||||
|
||||
class PedimentosUpdate(BaseModel):
|
||||
"""Schema for updating a Pedimento"""
|
||||
@@ -93,7 +148,7 @@ class PedimentosUpdate(BaseModel):
|
||||
license: Optional[str] = Field(None, max_length=4)
|
||||
pedimento_number: Optional[str] = Field(None, max_length=7)
|
||||
client_id: Optional[int] = None
|
||||
operation_type: Optional[int] = None
|
||||
operation_type: Optional[str] = Field(None, max_length=3)
|
||||
pedimento_type: Optional[str] = Field(None, max_length=20)
|
||||
pedimento_code: Optional[str] = Field(None, max_length=2)
|
||||
regime: Optional[str] = Field(None, max_length=3)
|
||||
@@ -103,24 +158,34 @@ class PedimentosUpdate(BaseModel):
|
||||
gross_weight: Optional[Decimal] = None
|
||||
exchange_rate: Optional[Decimal] = None
|
||||
observations: Optional[str] = None
|
||||
|
||||
|
||||
# Sub-resources
|
||||
pedimento_dates: Optional[PedimentoDatesCreate] = None
|
||||
pedimento_decrementables: Optional[PedimentoDecrementablesCreate] = None
|
||||
pedimento_incrementables: Optional[PedimentoIncrementablesCreate] = None
|
||||
pedimento_indexes: Optional[PedimentoIndexesCreate] = None
|
||||
pedimento_validation: Optional[PedimentoValidationCreate] = None
|
||||
pedimento_customs_offices: Optional[PedimentoCustomsOfficesCreate] = None
|
||||
pedimento_validation: Optional[PedimentoValidationCreate] = None
|
||||
pedimento_customs_offices: Optional[PedimentoCustomsOfficesCreate] = None
|
||||
pedimento_payments: Optional[PedimentoPaymentsCreate] = None
|
||||
pedimento_rectification_destination: Optional[PedimentoRectificationDestinationCreate] = None
|
||||
pedimento_rectification_destination: Optional[
|
||||
PedimentoRectificationDestinationCreate
|
||||
] = None
|
||||
pedimento_rectification_origin: Optional[PedimentoRectificationOriginCreate] = None
|
||||
pedimento_transport_means: Optional[PedimentoTransportMeansCreate] = None
|
||||
pedimento_transport_means: Optional[PedimentoTransportMeansCreate] = None
|
||||
pedimento_config_additional: Optional[PedimentoConfigAdditionalCreate] = None
|
||||
pedimento_config_calculations: Optional[PedimentoConfigCalculationsCreate] = None
|
||||
pedimento_config_parameters: Optional[PedimentoConfigParametersCreate] = None
|
||||
pedimento_config_surcharges: Optional[PedimentoConfigSurchargesCreate] = None
|
||||
pedimento_config_update_rectification: Optional[PedimentoConfigUpdateRectificationCreate] = None
|
||||
pedimento_config_update_rectification: Optional[
|
||||
PedimentoConfigUpdateRectificationCreate
|
||||
] = None
|
||||
pedimento_config_updates: Optional[PedimentoConfigUpdatesCreate] = None
|
||||
pedimento_packages: Optional[PedimentoPackagesCreate] = None
|
||||
pedimento_transport_carriers: Optional[list[PedimentoTransportCarrierCreate]] = None
|
||||
pedimento_guides: Optional[list[PedimentoGuideCreate]] = None
|
||||
pedimento_contributions: Optional[list[PedimentoContributionCreate]] = None
|
||||
pedimento_seals: Optional[list[PedimentoSealCreate]] = None
|
||||
pedimento_containers: Optional[list[PedimentoContainerCreate]] = None
|
||||
|
||||
|
||||
class PedimentosResponse(PedimentosBase):
|
||||
@@ -129,22 +194,34 @@ class PedimentosResponse(PedimentosBase):
|
||||
id: int
|
||||
tenant_id: int
|
||||
created_at: datetime
|
||||
|
||||
|
||||
pedimento_dates: Optional[PedimentoDatesResponse] = None
|
||||
pedimento_decrementables: Optional[PedimentoDecrementablesResponse] = None
|
||||
pedimento_incrementables: Optional[PedimentoIncrementablesResponse] = None
|
||||
pedimento_indexes: Optional[PedimentoIndexesResponse] = None
|
||||
pedimento_validation: Optional[PedimentoValidationResponse] = None
|
||||
pedimento_customs_offices: Optional[PedimentoCustomsOfficesResponse] = None
|
||||
pedimento_validation: Optional[PedimentoValidationResponse] = None
|
||||
pedimento_customs_offices: Optional[PedimentoCustomsOfficesResponse] = None
|
||||
pedimento_payments: Optional[PedimentoPaymentsResponse] = None
|
||||
pedimento_rectification_destination: Optional[PedimentoRectificationDestinationResponse] = None
|
||||
pedimento_rectification_origin: Optional[PedimentoRectificationOriginResponse] = None
|
||||
pedimento_transport_means: Optional[PedimentoTransportMeansResponse] = None
|
||||
pedimento_rectification_destination: Optional[
|
||||
PedimentoRectificationDestinationResponse
|
||||
] = None
|
||||
pedimento_rectification_origin: Optional[PedimentoRectificationOriginResponse] = (
|
||||
None
|
||||
)
|
||||
pedimento_transport_means: Optional[PedimentoTransportMeansResponse] = None
|
||||
pedimento_config_additional: Optional[PedimentoConfigAdditionalResponse] = None
|
||||
pedimento_config_calculations: Optional[PedimentoConfigCalculationsResponse] = None
|
||||
pedimento_config_parameters: Optional[PedimentoConfigParametersResponse] = None
|
||||
pedimento_config_surcharges: Optional[PedimentoConfigSurchargesResponse] = None
|
||||
pedimento_config_update_rectification: Optional[PedimentoConfigUpdateRectificationResponse] = None
|
||||
pedimento_config_update_rectification: Optional[
|
||||
PedimentoConfigUpdateRectificationResponse
|
||||
] = None
|
||||
pedimento_config_updates: Optional[PedimentoConfigUpdatesResponse] = None
|
||||
pedimento_packages: Optional[PedimentoPackagesResponse] = None
|
||||
pedimento_transport_carriers: Optional[list[PedimentoTransportCarrierResponse]] = None
|
||||
pedimento_guides: Optional[list[PedimentoGuideResponse]] = None
|
||||
pedimento_contributions: Optional[list[PedimentoContributionResponse]] = None
|
||||
pedimento_seals: Optional[list[PedimentoSealResponse]] = None
|
||||
pedimento_containers: Optional[list[PedimentoContainerResponse]] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
84
backend/api/v1/modules/a76/pedmientos/models/__init__.py
Normal file
84
backend/api/v1/modules/a76/pedmientos/models/__init__.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""Models for pedimentos module"""
|
||||
|
||||
# Import all models to ensure they are registered with SQLAlchemy
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_config_additional import (
|
||||
PedimentoConfigAdditional,
|
||||
)
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_config_calculations import (
|
||||
PedimentoConfigCalculations,
|
||||
)
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_config_parameters import (
|
||||
PedimentoConfigParameters,
|
||||
)
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_config_surcharges import (
|
||||
PedimentoConfigSurcharges,
|
||||
)
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_config_update_rectification import (
|
||||
PedimentoConfigUpdateRectification,
|
||||
)
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_config_updates import (
|
||||
PedimentoConfigUpdates,
|
||||
)
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_containers import (
|
||||
PedimentoContainers,
|
||||
)
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_contributions import (
|
||||
PedimentoContributions,
|
||||
)
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_customs_offices import (
|
||||
PedimentoCustomsOffices,
|
||||
)
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_dates import PedimentoDates
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_decrementables import (
|
||||
PedimentoDecrementables,
|
||||
)
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_incrementables import (
|
||||
PedimentoIncrementables,
|
||||
)
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_indexes import PedimentoIndexes
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_packages import PedimentoPackages
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_payments import PedimentoPayments
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_guides import PedimentoGuides
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_rectification_destination import (
|
||||
PedimentoRectificationDestination,
|
||||
)
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_rectification_origin import (
|
||||
PedimentoRectificationOrigin,
|
||||
)
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_seals import PedimentoSeals
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_transport_carriers import (
|
||||
PedimentoTransportCarriers,
|
||||
)
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_transport_means import (
|
||||
PedimentoTransportMeans,
|
||||
)
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_validation import (
|
||||
PedimentoValidation,
|
||||
)
|
||||
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
|
||||
|
||||
__all__ = [
|
||||
"Pedimentos",
|
||||
"PedimentoConfigAdditional",
|
||||
"PedimentoConfigCalculations",
|
||||
"PedimentoConfigParameters",
|
||||
"PedimentoConfigSurcharges",
|
||||
"PedimentoConfigUpdateRectification",
|
||||
"PedimentoConfigUpdates",
|
||||
"PedimentoContainers",
|
||||
"PedimentoContributions",
|
||||
"PedimentoCustomsOffices",
|
||||
"PedimentoDates",
|
||||
"PedimentoDecrementables",
|
||||
"PedimentoIncrementables",
|
||||
"PedimentoIndexes",
|
||||
"PedimentoPackages",
|
||||
"PedimentoGuides",
|
||||
"PedimentoPayments",
|
||||
"PedimentoRectificationDestination",
|
||||
"PedimentoRectificationOrigin",
|
||||
"PedimentoSeals",
|
||||
"PedimentoTransportCarriers",
|
||||
"PedimentoTransportMeans",
|
||||
"PedimentoValidation",
|
||||
]
|
||||
@@ -3,6 +3,7 @@ from typing import TYPE_CHECKING, Optional
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
Boolean,
|
||||
ForeignKeyConstraint,
|
||||
Integer,
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
from sqlalchemy import (
|
||||
ForeignKeyConstraint,
|
||||
Integer,
|
||||
PrimaryKeyConstraint,
|
||||
String,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
|
||||
|
||||
|
||||
class PedimentoContainers(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "pedimento_containers"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id", name="pedimento_containers_pkey"),
|
||||
ForeignKeyConstraint(
|
||||
["pedimento_id"],
|
||||
["a76.pedimentos.id"],
|
||||
ondelete="CASCADE",
|
||||
name="fk_pedimento_containers",
|
||||
),
|
||||
{"schema": "a76"},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer)
|
||||
pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
number: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
identification: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
type: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
||||
|
||||
pedimento: Mapped["Pedimentos"] = relationship(
|
||||
"Pedimentos", back_populates="pedimento_containers"
|
||||
)
|
||||
@@ -0,0 +1,46 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
from sqlalchemy import (
|
||||
ForeignKeyConstraint,
|
||||
Integer,
|
||||
Numeric,
|
||||
PrimaryKeyConstraint,
|
||||
String,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
|
||||
|
||||
|
||||
class PedimentoContributions(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "pedimento_contributions"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id", name="pedimento_contributions_pkey"),
|
||||
ForeignKeyConstraint(
|
||||
["pedimento_id"],
|
||||
["a76.pedimentos.id"],
|
||||
ondelete="CASCADE",
|
||||
name="fk_pedimento_contributions",
|
||||
),
|
||||
{"schema": "a76"},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer)
|
||||
pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
contribucion: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
||||
tipo_tasa: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
tasa: Mapped[Numeric | None] = mapped_column(Numeric(15, 8), nullable=True)
|
||||
forma_pago: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
||||
importe: Mapped[Numeric | None] = mapped_column(Numeric(17, 2), nullable=True)
|
||||
gravamen: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
abreviacion: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
||||
forma_pago_2: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
||||
importe_2: Mapped[Numeric | None] = mapped_column(Numeric(17, 2), nullable=True)
|
||||
|
||||
pedimento: Mapped["Pedimentos"] = relationship(
|
||||
"Pedimentos", back_populates="pedimento_contributions"
|
||||
)
|
||||
@@ -42,16 +42,16 @@ class PedimentoDates(Base, TenantScopedMixin, TimestampMixin):
|
||||
id: Mapped[int] = mapped_column(Integer)
|
||||
pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
entry_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||||
entry_date: Mapped[datetime] = mapped_column(DateTime)
|
||||
pedimento_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||||
payment_date: Mapped[datetime] = mapped_column(DateTime)
|
||||
payment_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||||
rectification_payment_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||||
extraction_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||||
submission_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||||
eucan_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||||
original_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||||
start_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||||
end_date: Mapped[Optional[datetime]] = mapped_column(DateTime)
|
||||
end_date: Mapped[datetime] = mapped_column(DateTime)
|
||||
|
||||
capture_time: Mapped[datetime_time] = mapped_column(Time)
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from decimal import Decimal
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
@@ -40,15 +40,15 @@ class PedimentoDecrementables(Base, TenantScopedMixin, TimestampMixin):
|
||||
id: Mapped[int] = mapped_column(Integer)
|
||||
pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
freight: Mapped[Decimal] = mapped_column(Numeric(13, 2))
|
||||
insurance: Mapped[Decimal] = mapped_column(Numeric(13, 2))
|
||||
loading: Mapped[Decimal] = mapped_column(Numeric(13, 2))
|
||||
unloading: Mapped[Decimal] = mapped_column(Numeric(13, 2))
|
||||
others: Mapped[Decimal] = mapped_column(Numeric(13, 2))
|
||||
currency: Mapped[str] = mapped_column(String(3))
|
||||
currency_factor: Mapped[Decimal] = mapped_column(Numeric(15, 8))
|
||||
not_affect_usd_value: Mapped[int] = mapped_column(SmallInteger)
|
||||
not_affect_customs_value: Mapped[int] = mapped_column(SmallInteger)
|
||||
freight: Mapped[Optional[Decimal]] = mapped_column(Numeric(13, 2))
|
||||
insurance: Mapped[Optional[Decimal]] = mapped_column(Numeric(13, 2))
|
||||
loading: Mapped[Optional[Decimal]] = mapped_column(Numeric(13, 2))
|
||||
unloading: Mapped[Optional[Decimal]] = mapped_column(Numeric(13, 2))
|
||||
others: Mapped[Optional[Decimal]] = mapped_column(Numeric(13, 2))
|
||||
currency: Mapped[Optional[str]] = mapped_column(String(3))
|
||||
currency_factor: Mapped[Optional[Decimal]] = mapped_column(Numeric(15, 8))
|
||||
not_affect_usd_value: Mapped[Optional[int]] = mapped_column(SmallInteger)
|
||||
not_affect_customs_value: Mapped[Optional[int]] = mapped_column(SmallInteger)
|
||||
|
||||
pedimento: Mapped["Pedimentos"] = relationship(
|
||||
"Pedimentos", back_populates="pedimento_decrementables"
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
from sqlalchemy import (
|
||||
ForeignKeyConstraint,
|
||||
Integer,
|
||||
PrimaryKeyConstraint,
|
||||
String,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
|
||||
|
||||
|
||||
class PedimentoGuides(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "pedimento_guides"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id", name="pedimento_guides_pkey"),
|
||||
ForeignKeyConstraint(
|
||||
["pedimento_id"],
|
||||
["a76.pedimentos.id"],
|
||||
ondelete="CASCADE",
|
||||
name="fk_pedimento_guides",
|
||||
),
|
||||
{"schema": "a76"},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer)
|
||||
pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
guide: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
identifier: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
|
||||
pedimento: Mapped["Pedimentos"] = relationship(
|
||||
"Pedimentos", back_populates="pedimento_guides"
|
||||
)
|
||||
@@ -1,5 +1,5 @@
|
||||
from decimal import Decimal
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
@@ -40,16 +40,16 @@ class PedimentoIncrementables(Base, TenantScopedMixin, TimestampMixin):
|
||||
id: Mapped[int] = mapped_column(Integer)
|
||||
pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
insured_value: Mapped[Decimal] = mapped_column(Numeric(13, 2))
|
||||
freight: Mapped[Decimal] = mapped_column(Numeric(13, 2))
|
||||
insurance: Mapped[Decimal] = mapped_column(Numeric(13, 2))
|
||||
packaging: Mapped[Decimal] = mapped_column(Numeric(13, 2))
|
||||
others: Mapped[Decimal] = mapped_column(Numeric(13, 3))
|
||||
deductibles: Mapped[Decimal] = mapped_column(Numeric(13, 3))
|
||||
currency: Mapped[str] = mapped_column(String(3))
|
||||
currency_factor: Mapped[Decimal] = mapped_column(Numeric(15, 8))
|
||||
not_affect_usd_value: Mapped[int] = mapped_column(SmallInteger)
|
||||
not_affect_customs_value: Mapped[int] = mapped_column(SmallInteger)
|
||||
insured_value: Mapped[Optional[Decimal]] = mapped_column(Numeric(13, 2))
|
||||
freight: Mapped[Optional[Decimal]] = mapped_column(Numeric(13, 2))
|
||||
insurance: Mapped[Optional[Decimal]] = mapped_column(Numeric(13, 2))
|
||||
packaging: Mapped[Optional[Decimal]] = mapped_column(Numeric(13, 2))
|
||||
others: Mapped[Optional[Decimal]] = mapped_column(Numeric(13, 3))
|
||||
deductibles: Mapped[Optional[Decimal]] = mapped_column(Numeric(13, 3))
|
||||
currency: Mapped[Optional[str]] = mapped_column(String(3))
|
||||
currency_factor: Mapped[Optional[Decimal]] = mapped_column(Numeric(15, 8))
|
||||
not_affect_usd_value: Mapped[Optional[int]] = mapped_column(SmallInteger)
|
||||
not_affect_customs_value: Mapped[Optional[int]] = mapped_column(SmallInteger)
|
||||
|
||||
pedimento: Mapped["Pedimentos"] = relationship(
|
||||
"Pedimentos", back_populates="pedimento_incrementables"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from decimal import Decimal
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
@@ -39,9 +39,9 @@ class PedimentoIndexes(Base, TenantScopedMixin, TimestampMixin):
|
||||
id: Mapped[int] = mapped_column(Integer)
|
||||
pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
update_factor_type: Mapped[int] = mapped_column(SmallInteger)
|
||||
update_factor: Mapped[Decimal] = mapped_column(Numeric(7, 4))
|
||||
manual_update_factor: Mapped[int] = mapped_column(SmallInteger)
|
||||
update_factor_type: Mapped[Optional[int]] = mapped_column(SmallInteger)
|
||||
update_factor: Mapped[Optional[Decimal]] = mapped_column(Numeric(7, 4))
|
||||
manual_update_factor: Mapped[Optional[int]] = mapped_column(SmallInteger)
|
||||
|
||||
pedimento: Mapped["Pedimentos"] = relationship(
|
||||
"Pedimentos", back_populates="pedimento_indexes"
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
from sqlalchemy import (
|
||||
ForeignKeyConstraint,
|
||||
Integer,
|
||||
Numeric,
|
||||
PrimaryKeyConstraint,
|
||||
String,
|
||||
UniqueConstraint,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
|
||||
|
||||
|
||||
class PedimentoPackages(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "pedimento_packages"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id", name="pedimento_packages_pkey"),
|
||||
ForeignKeyConstraint(
|
||||
["pedimento_id"],
|
||||
["a76.pedimentos.id"],
|
||||
ondelete="CASCADE",
|
||||
name="fk_pedimento_packages",
|
||||
),
|
||||
UniqueConstraint(
|
||||
"tenant_id",
|
||||
"company_id",
|
||||
"pedimento_id",
|
||||
name="pedimento_packages_pedimento_id_key",
|
||||
),
|
||||
{"schema": "a76"},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer)
|
||||
pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
quantity: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
brand: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
number: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
vehicles: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
|
||||
pedimento: Mapped["Pedimentos"] = relationship(
|
||||
"Pedimentos", back_populates="pedimento_packages"
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
from sqlalchemy import (
|
||||
ForeignKeyConstraint,
|
||||
Integer,
|
||||
PrimaryKeyConstraint,
|
||||
String,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
|
||||
|
||||
|
||||
class PedimentoSeals(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "pedimento_seals"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id", name="pedimento_seals_pkey"),
|
||||
ForeignKeyConstraint(
|
||||
["pedimento_id"],
|
||||
["a76.pedimentos.id"],
|
||||
ondelete="CASCADE",
|
||||
name="fk_pedimento_seals",
|
||||
),
|
||||
{"schema": "a76"},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer)
|
||||
pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
number: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
identification: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
|
||||
pedimento: Mapped["Pedimentos"] = relationship(
|
||||
"Pedimentos", back_populates="pedimento_seals"
|
||||
)
|
||||
@@ -0,0 +1,47 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
from sqlalchemy import (
|
||||
ForeignKeyConstraint,
|
||||
Integer,
|
||||
PrimaryKeyConstraint,
|
||||
String,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
|
||||
|
||||
|
||||
class PedimentoTransportCarriers(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "pedimento_transport_carriers"
|
||||
__table_args__ = (
|
||||
PrimaryKeyConstraint("id", name="pedimento_transport_carriers_pkey"),
|
||||
ForeignKeyConstraint(
|
||||
["pedimento_id"],
|
||||
["a76.pedimentos.id"],
|
||||
ondelete="CASCADE",
|
||||
name="fk_pedimento_transport_carriers",
|
||||
),
|
||||
{"schema": "a76"},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer)
|
||||
pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
carrier: Mapped[str | None] = mapped_column(String(200), nullable=True)
|
||||
rfc: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
||||
curp: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
||||
name: Mapped[str | None] = mapped_column(String(200), nullable=True)
|
||||
address: Mapped[str | None] = mapped_column(String(300), nullable=True)
|
||||
city: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
state: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
country: Mapped[str | None] = mapped_column(String(3), nullable=True)
|
||||
tax_id: Mapped[str | None] = mapped_column(String(50), nullable=True)
|
||||
total_packages: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
identification: Mapped[str | None] = mapped_column(String(100), nullable=True)
|
||||
|
||||
pedimento: Mapped["Pedimentos"] = relationship(
|
||||
"Pedimentos", back_populates="pedimento_transport_carriers"
|
||||
)
|
||||
@@ -1,4 +1,5 @@
|
||||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
@@ -34,6 +35,9 @@ if TYPE_CHECKING:
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_config_updates import (
|
||||
PedimentoConfigUpdates,
|
||||
)
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_containers import (
|
||||
PedimentoContainers,
|
||||
)
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_customs_offices import (
|
||||
PedimentoCustomsOffices,
|
||||
)
|
||||
@@ -45,6 +49,15 @@ if TYPE_CHECKING:
|
||||
PedimentoIncrementables,
|
||||
)
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_indexes import PedimentoIndexes
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_packages import (
|
||||
PedimentoPackages,
|
||||
)
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_guides import (
|
||||
PedimentoGuides,
|
||||
)
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_contributions import (
|
||||
PedimentoContributions,
|
||||
)
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_payments import (
|
||||
PedimentoPayments,
|
||||
)
|
||||
@@ -54,6 +67,12 @@ if TYPE_CHECKING:
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_rectification_origin import (
|
||||
PedimentoRectificationOrigin,
|
||||
)
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_seals import (
|
||||
PedimentoSeals,
|
||||
)
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_transport_carriers import (
|
||||
PedimentoTransportCarriers,
|
||||
)
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_transport_means import (
|
||||
PedimentoTransportMeans,
|
||||
)
|
||||
@@ -62,6 +81,17 @@ if TYPE_CHECKING:
|
||||
)
|
||||
|
||||
|
||||
class PedimentoType(str, Enum):
|
||||
NORMAL = "normal"
|
||||
CONSOLIDATED = "consolidated"
|
||||
COMPLEMENTARY = "complementary"
|
||||
AUTOMOBILE = "automobile"
|
||||
|
||||
class OperationType(str, Enum):
|
||||
IMP = "imp" # Importación
|
||||
EXP = "exp" # Exportación
|
||||
|
||||
|
||||
class Pedimentos(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "pedimentos"
|
||||
__table_args__ = (
|
||||
@@ -99,8 +129,8 @@ class Pedimentos(Base, TenantScopedMixin, TimestampMixin):
|
||||
license: Mapped[str] = mapped_column(String(4))
|
||||
pedimento_number: Mapped[str] = mapped_column(String(7))
|
||||
client_id: Mapped[int] = mapped_column(Integer)
|
||||
operation_type: Mapped[int] = mapped_column(Integer)
|
||||
pedimento_type: Mapped[str] = mapped_column(String(20))
|
||||
operation_type: Mapped[OperationType] = mapped_column(String(3))
|
||||
pedimento_type: Mapped[PedimentoType] = mapped_column(String(20))
|
||||
pedimento_code: Mapped[str] = mapped_column(String(2))
|
||||
regime: Mapped[str] = mapped_column(String(3))
|
||||
status: Mapped[Optional[str]] = mapped_column(String(30))
|
||||
@@ -111,59 +141,122 @@ class Pedimentos(Base, TenantScopedMixin, TimestampMixin):
|
||||
observations: Mapped[Optional[str]] = mapped_column(Text)
|
||||
|
||||
pedimento_config_additional: Mapped["PedimentoConfigAdditional"] = relationship(
|
||||
"PedimentoConfigAdditional", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
|
||||
"PedimentoConfigAdditional",
|
||||
uselist=False,
|
||||
back_populates="pedimento",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
pedimento_config_calculations: Mapped["PedimentoConfigCalculations"] = relationship(
|
||||
"PedimentoConfigCalculations", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
|
||||
"PedimentoConfigCalculations",
|
||||
uselist=False,
|
||||
back_populates="pedimento",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
pedimento_config_parameters: Mapped["PedimentoConfigParameters"] = relationship(
|
||||
"PedimentoConfigParameters", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
|
||||
"PedimentoConfigParameters",
|
||||
uselist=False,
|
||||
back_populates="pedimento",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
pedimento_config_surcharges: Mapped["PedimentoConfigSurcharges"] = relationship(
|
||||
"PedimentoConfigSurcharges", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
|
||||
"PedimentoConfigSurcharges",
|
||||
uselist=False,
|
||||
back_populates="pedimento",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
pedimento_config_update_rectification: Mapped[
|
||||
"PedimentoConfigUpdateRectification"
|
||||
] = relationship(
|
||||
"PedimentoConfigUpdateRectification", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
|
||||
"PedimentoConfigUpdateRectification",
|
||||
uselist=False,
|
||||
back_populates="pedimento",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
pedimento_config_updates: Mapped["PedimentoConfigUpdates"] = relationship(
|
||||
"PedimentoConfigUpdates", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
|
||||
"PedimentoConfigUpdates",
|
||||
uselist=False,
|
||||
back_populates="pedimento",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
pedimento_customs_offices: Mapped["PedimentoCustomsOffices"] = relationship(
|
||||
"PedimentoCustomsOffices", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
|
||||
"PedimentoCustomsOffices",
|
||||
uselist=False,
|
||||
back_populates="pedimento",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
pedimento_dates: Mapped["PedimentoDates"] = relationship(
|
||||
"PedimentoDates", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
|
||||
"PedimentoDates",
|
||||
uselist=False,
|
||||
back_populates="pedimento",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
pedimento_decrementables: Mapped["PedimentoDecrementables"] = relationship(
|
||||
"PedimentoDecrementables", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
|
||||
"PedimentoDecrementables",
|
||||
uselist=False,
|
||||
back_populates="pedimento",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
pedimento_incrementables: Mapped["PedimentoIncrementables"] = relationship(
|
||||
"PedimentoIncrementables", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
|
||||
"PedimentoIncrementables",
|
||||
uselist=False,
|
||||
back_populates="pedimento",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
pedimento_indexes: Mapped["PedimentoIndexes"] = relationship(
|
||||
"PedimentoIndexes", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
|
||||
"PedimentoIndexes",
|
||||
uselist=False,
|
||||
back_populates="pedimento",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
pedimento_payments: Mapped["PedimentoPayments"] = relationship(
|
||||
"PedimentoPayments", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
|
||||
"PedimentoPayments",
|
||||
uselist=False,
|
||||
back_populates="pedimento",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
pedimento_rectification_destination: Mapped["PedimentoRectificationDestination"] = (
|
||||
relationship(
|
||||
"PedimentoRectificationDestination",
|
||||
uselist=False,
|
||||
back_populates="pedimento",
|
||||
cascade="all, delete-orphan"
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
)
|
||||
pedimento_rectification_origin: Mapped["PedimentoRectificationOrigin"] = (
|
||||
relationship(
|
||||
"PedimentoRectificationOrigin", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
|
||||
"PedimentoRectificationOrigin",
|
||||
uselist=False,
|
||||
back_populates="pedimento",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
)
|
||||
pedimento_transport_means: Mapped["PedimentoTransportMeans"] = relationship(
|
||||
"PedimentoTransportMeans", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
|
||||
"PedimentoTransportMeans",
|
||||
uselist=False,
|
||||
back_populates="pedimento",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
pedimento_validation: Mapped["PedimentoValidation"] = relationship(
|
||||
"PedimentoValidation", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
|
||||
"PedimentoValidation",
|
||||
uselist=False,
|
||||
back_populates="pedimento",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
pedimento_packages: Mapped["PedimentoPackages"] = relationship(
|
||||
"PedimentoPackages", uselist=False, back_populates="pedimento", cascade="all, delete-orphan"
|
||||
)
|
||||
pedimento_transport_carriers: Mapped[list["PedimentoTransportCarriers"]] = relationship(
|
||||
"PedimentoTransportCarriers", back_populates="pedimento", cascade="all, delete-orphan"
|
||||
)
|
||||
pedimento_guides: Mapped[list["PedimentoGuides"]] = relationship(
|
||||
"PedimentoGuides", back_populates="pedimento", cascade="all, delete-orphan"
|
||||
)
|
||||
pedimento_contributions: Mapped[list["PedimentoContributions"]] = relationship(
|
||||
"PedimentoContributions", back_populates="pedimento", cascade="all, delete-orphan"
|
||||
)
|
||||
pedimento_seals: Mapped[list["PedimentoSeals"]] = relationship(
|
||||
"PedimentoSeals", back_populates="pedimento", cascade="all, delete-orphan"
|
||||
)
|
||||
pedimento_containers: Mapped[list["PedimentoContainers"]] = relationship(
|
||||
"PedimentoContainers", back_populates="pedimento", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
@@ -48,6 +48,12 @@ from ..models.pedimento_config_parameters import PedimentoConfigParameters
|
||||
from ..models.pedimento_config_surcharges import PedimentoConfigSurcharges
|
||||
from ..models.pedimento_config_update_rectification import PedimentoConfigUpdateRectification
|
||||
from ..models.pedimento_config_updates import PedimentoConfigUpdates
|
||||
from ..models.pedimento_packages import PedimentoPackages
|
||||
from ..models.pedimento_transport_carriers import PedimentoTransportCarriers
|
||||
from ..models.pedimento_seals import PedimentoSeals
|
||||
from ..models.pedimento_containers import PedimentoContainers
|
||||
from ..models.pedimento_guides import PedimentoGuides
|
||||
from ..models.pedimento_contributions import PedimentoContributions
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -110,6 +116,12 @@ class PedimentosService:
|
||||
selectinload(Pedimentos.pedimento_config_surcharges),
|
||||
selectinload(Pedimentos.pedimento_config_update_rectification),
|
||||
selectinload(Pedimentos.pedimento_config_updates),
|
||||
selectinload(Pedimentos.pedimento_packages),
|
||||
selectinload(Pedimentos.pedimento_transport_carriers),
|
||||
selectinload(Pedimentos.pedimento_guides),
|
||||
selectinload(Pedimentos.pedimento_contributions),
|
||||
selectinload(Pedimentos.pedimento_seals),
|
||||
selectinload(Pedimentos.pedimento_containers),
|
||||
)
|
||||
.order_by(desc(Pedimentos.created_at))
|
||||
.offset(skip)
|
||||
@@ -160,6 +172,12 @@ class PedimentosService:
|
||||
selectinload(Pedimentos.pedimento_config_surcharges),
|
||||
selectinload(Pedimentos.pedimento_config_update_rectification),
|
||||
selectinload(Pedimentos.pedimento_config_updates),
|
||||
selectinload(Pedimentos.pedimento_packages),
|
||||
selectinload(Pedimentos.pedimento_transport_carriers),
|
||||
selectinload(Pedimentos.pedimento_guides),
|
||||
selectinload(Pedimentos.pedimento_contributions),
|
||||
selectinload(Pedimentos.pedimento_seals),
|
||||
selectinload(Pedimentos.pedimento_containers),
|
||||
)
|
||||
|
||||
return query.first()
|
||||
@@ -199,6 +217,12 @@ class PedimentosService:
|
||||
'pedimento_config_surcharges': pedimento_data.pedimento_config_surcharges,
|
||||
'pedimento_config_update_rectification': pedimento_data.pedimento_config_update_rectification,
|
||||
'pedimento_config_updates': pedimento_data.pedimento_config_updates,
|
||||
'pedimento_packages': pedimento_data.pedimento_packages,
|
||||
'pedimento_transport_carriers': pedimento_data.pedimento_transport_carriers,
|
||||
'pedimento_guides': pedimento_data.pedimento_guides,
|
||||
'pedimento_contributions': getattr(pedimento_data, 'pedimento_contributions', None),
|
||||
'pedimento_seals': pedimento_data.pedimento_seals,
|
||||
'pedimento_containers': pedimento_data.pedimento_containers,
|
||||
}
|
||||
|
||||
# Crear pedimento principal (excluyendo relaciones)
|
||||
@@ -209,7 +233,9 @@ class PedimentosService:
|
||||
'pedimento_rectification_origin', 'pedimento_transport_means',
|
||||
'pedimento_config_additional', 'pedimento_config_calculations',
|
||||
'pedimento_config_parameters', 'pedimento_config_surcharges',
|
||||
'pedimento_config_update_rectification', 'pedimento_config_updates'
|
||||
'pedimento_config_update_rectification', 'pedimento_config_updates',
|
||||
'pedimento_packages', 'pedimento_transport_carriers', 'pedimento_guides',
|
||||
'pedimento_contributions', 'pedimento_seals', 'pedimento_containers'
|
||||
})
|
||||
|
||||
pedimento = Pedimentos(**pedimento_dict)
|
||||
@@ -219,7 +245,7 @@ class PedimentosService:
|
||||
db.add(pedimento)
|
||||
db.flush() # Flush para obtener el ID sin commit
|
||||
|
||||
# Helper function para crear objetos relacionados
|
||||
# Helper function para crear objetos relacionados (uno a uno)
|
||||
def create_related(model_class, data, extra_fields=None):
|
||||
if data or extra_fields:
|
||||
# Inicializar obj_dict desde data si existe, sino como dict vacío
|
||||
@@ -274,6 +300,29 @@ class PedimentosService:
|
||||
create_related(PedimentoConfigUpdates,
|
||||
related_data['pedimento_config_updates'])
|
||||
|
||||
# Crear PedimentoPackages (uno a uno)
|
||||
create_related(PedimentoPackages, related_data['pedimento_packages'])
|
||||
|
||||
# Crear relaciones uno a muchos
|
||||
def create_many(model_class, items):
|
||||
if not items:
|
||||
return
|
||||
for item in items:
|
||||
obj_dict = item.model_dump(exclude_none=True)
|
||||
obj = model_class(**obj_dict)
|
||||
obj.pedimento_id = pedimento.id
|
||||
obj.tenant_id = tenant_id
|
||||
obj.company_id = company_id
|
||||
db.add(obj)
|
||||
|
||||
create_many(PedimentoTransportCarriers,
|
||||
related_data['pedimento_transport_carriers'])
|
||||
create_many(PedimentoGuides, related_data['pedimento_guides'])
|
||||
create_many(PedimentoContributions, related_data['pedimento_contributions'])
|
||||
create_many(PedimentoSeals, related_data['pedimento_seals'])
|
||||
create_many(PedimentoContainers,
|
||||
related_data['pedimento_containers'])
|
||||
|
||||
db.commit()
|
||||
db.refresh(pedimento)
|
||||
return pedimento
|
||||
@@ -323,7 +372,8 @@ class PedimentosService:
|
||||
'pedimento_rectification_origin', 'pedimento_transport_means',
|
||||
'pedimento_config_additional', 'pedimento_config_calculations',
|
||||
'pedimento_config_parameters', 'pedimento_config_surcharges',
|
||||
'pedimento_config_update_rectification', 'pedimento_config_updates'
|
||||
'pedimento_config_update_rectification', 'pedimento_config_updates',
|
||||
'pedimento_packages', 'pedimento_transport_carriers', 'pedimento_guides', 'pedimento_contributions', 'pedimento_seals', 'pedimento_containers'
|
||||
})
|
||||
|
||||
for field, value in update_data.items():
|
||||
@@ -343,13 +393,20 @@ class PedimentosService:
|
||||
if not data:
|
||||
return
|
||||
|
||||
existing = service_class.get_by_pedimento_id(
|
||||
db, pedimento_id, tenant_id, company_id)
|
||||
existing = None
|
||||
if service_class:
|
||||
existing = service_class.get_by_pedimento_id(
|
||||
db, pedimento_id, tenant_id, company_id)
|
||||
else:
|
||||
existing = getattr(pedimento, data_attr, None)
|
||||
|
||||
if existing:
|
||||
# Actualizar existente (excluir pedimento_id, tenant_id, company_id)
|
||||
# Solo actualizar valores no-None para evitar sobrescribir con None
|
||||
for field, value in data.items():
|
||||
if hasattr(existing, field) and field not in ('pedimento_id', 'tenant_id', 'company_id'):
|
||||
setattr(existing, field, value)
|
||||
if value is not None:
|
||||
setattr(existing, field, value)
|
||||
else:
|
||||
# Crear nuevo
|
||||
obj = model_class(**data)
|
||||
@@ -358,6 +415,28 @@ class PedimentosService:
|
||||
obj.company_id = company_id
|
||||
db.add(obj)
|
||||
|
||||
def upsert_one_to_many(model_class, data_attr):
|
||||
full_data = pedimento_data.model_dump()
|
||||
if data_attr not in full_data:
|
||||
return
|
||||
|
||||
items = full_data[data_attr]
|
||||
if items is None:
|
||||
return
|
||||
|
||||
# Reemplazar completamente la colección por simplicidad
|
||||
existing_items = getattr(pedimento, data_attr)
|
||||
if existing_items:
|
||||
for item in list(existing_items):
|
||||
db.delete(item)
|
||||
|
||||
for item in items:
|
||||
obj = model_class(**item)
|
||||
obj.pedimento_id = pedimento_id
|
||||
obj.tenant_id = tenant_id
|
||||
obj.company_id = company_id
|
||||
db.add(obj)
|
||||
|
||||
# Actualizar o crear tablas relacionadas
|
||||
update_or_create_related(
|
||||
PedimentoDatesService, PedimentoDates, 'pedimento_dates')
|
||||
@@ -392,6 +471,15 @@ class PedimentosService:
|
||||
update_or_create_related(
|
||||
PedimentoConfigUpdatesService, PedimentoConfigUpdates, 'pedimento_config_updates')
|
||||
|
||||
# Manejar nuevas relaciones
|
||||
update_or_create_related(None, PedimentoPackages, 'pedimento_packages')
|
||||
upsert_one_to_many(PedimentoTransportCarriers,
|
||||
'pedimento_transport_carriers')
|
||||
upsert_one_to_many(PedimentoGuides, 'pedimento_guides')
|
||||
upsert_one_to_many(PedimentoContributions, 'pedimento_contributions')
|
||||
upsert_one_to_many(PedimentoSeals, 'pedimento_seals')
|
||||
upsert_one_to_many(PedimentoContainers, 'pedimento_containers')
|
||||
|
||||
db.commit()
|
||||
db.refresh(pedimento)
|
||||
return pedimento
|
||||
|
||||
@@ -15,11 +15,16 @@ from .clients_and_providers import router as client_and_provider_router
|
||||
from .general_catalogs.company import router as company_router
|
||||
from .country_rule_oct.routes import router as country_rule_oct_router
|
||||
from .transportation.drivers.routes import router as drivers_router
|
||||
from .doc_types_dig.routes import router as doc_types_dig_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 .general_catalogs.packages.routes import router as package_router
|
||||
from .general_catalogs.ports.routes import router as ports_router
|
||||
from .general_catalogs.tariff_fractions.routes import router as tariff_fractions_router
|
||||
from .general_catalogs.us_tariff_fractions.routes import router as us_tariff_fractions_router
|
||||
from .general_catalogs.depreciation_catalog.routes import router as depreciation_catalog_router
|
||||
from .general_catalogs.fda_catalog.routes import router as fda_catalog_router
|
||||
from .parts import router as parts_router
|
||||
from .pedmientos.router import router as pedimentos_router
|
||||
from .permission_rule_oct.routes import router as permission_rule_oct_router
|
||||
@@ -41,6 +46,8 @@ from .general_catalogs.electronic_notices.routes import router as electronic_not
|
||||
from .transportation.trailers.routes import router as trailers_router
|
||||
from .transportation.transporters.routes import router as transporters_router
|
||||
from .transportation.vehicles.routes import router as vehicles_router
|
||||
from api.v1.modules.public.reference_data.material_types.routes import router as material_types_router
|
||||
|
||||
|
||||
# Router principal
|
||||
router = APIRouter()
|
||||
@@ -60,6 +67,10 @@ router.include_router(
|
||||
)
|
||||
router.include_router(package_router, prefix="/a76")
|
||||
router.include_router(ports_router, prefix="/a76")
|
||||
router.include_router(tariff_fractions_router, prefix="/a76")
|
||||
router.include_router(us_tariff_fractions_router, prefix="/a76")
|
||||
router.include_router(depreciation_catalog_router, prefix="/a76")
|
||||
router.include_router(fda_catalog_router, prefix="/a76")
|
||||
router.include_router(seal_router, prefix="/a76", tags=["a76 / seal"])
|
||||
router.include_router(units_of_measure_router, prefix="/a76")
|
||||
router.include_router(
|
||||
@@ -75,8 +86,9 @@ router.include_router(trailers_router, prefix="/a76/transportation", tags=["a76
|
||||
router.include_router(
|
||||
customs_broker_router, prefix="/a76", tags=["a76 / customs_broker"]
|
||||
)
|
||||
router.include_router(drivers_router, prefix="/a76/transportation", tags=["a76 / drivers"])
|
||||
router.include_router(transporters_router, prefix="/a76/transportation",
|
||||
router.include_router(doc_types_dig_router, prefix="/a76", tags=["a76 / document_types_digitization"])
|
||||
router.include_router(drivers_router, prefix="/a76", tags=["a76 / drivers"])
|
||||
router.include_router(transporters_router, prefix="/a76",
|
||||
tags=["a76 / transporters"])
|
||||
router.include_router(vehicles_router, prefix="/a76/transportation", tags=["a76 / vehicles"])
|
||||
|
||||
@@ -94,3 +106,11 @@ router.include_router(error_catalogs_router, prefix="/a76")
|
||||
router.include_router(doda_router, prefix="/a76")
|
||||
router.include_router(prevalidators_router, prefix="/a76")
|
||||
router.include_router(electronic_notices_router, prefix="/a76")
|
||||
|
||||
|
||||
# Registrar router de tipos de material públicos
|
||||
router.include_router(
|
||||
material_types_router,
|
||||
prefix="/public/reference-data",
|
||||
tags=["Reference Data"]
|
||||
)
|
||||
|
||||
@@ -16,8 +16,8 @@ async def list_countries(
|
||||
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"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Endpoint público para obtener lista de países - no requiere autenticación"""
|
||||
skip = (page - 1) * page_size
|
||||
query = db.query(Country)
|
||||
items = query.offset(skip).limit(page_size).all()
|
||||
|
||||
@@ -55,7 +55,7 @@ seed = [
|
||||
("LTT", "LITAS", "LITUANIA"),
|
||||
("LYD", "DINAR", "LIBIA"),
|
||||
("MAD", "DIRHAM", "MARRUECOS"),
|
||||
("MXP", "PESO", "MEXICO"),
|
||||
("MXN", "PESO", "MEXICO"),
|
||||
("MYR", "RINGGIT", "MALASIA"),
|
||||
("NGN", "NAIRA", "NIGERIA (FED)"),
|
||||
("NIC", "CORDOBA", "NICARAGUA"),
|
||||
|
||||
@@ -8,7 +8,9 @@ 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.a24.router import router as a24_router
|
||||
from .modules.public.router import router as public_router
|
||||
from .modules.a24.router import router as a24_router
|
||||
|
||||
# Router principal
|
||||
router = APIRouter()
|
||||
@@ -16,7 +18,10 @@ router = APIRouter()
|
||||
# Registrar módulos
|
||||
router.include_router(core_router)
|
||||
router.include_router(a76_router)
|
||||
router.include_router(a24_router)
|
||||
router.include_router(public_router)
|
||||
# nuevas rutas de partes de anexo 24
|
||||
router.include_router(a24_router)
|
||||
|
||||
|
||||
# Health check
|
||||
|
||||
Reference in New Issue
Block a user