Merge branch 'feature/invoices' into development

This commit is contained in:
2026-01-12 12:31:22 -06:00
29 changed files with 2018 additions and 847 deletions

View File

@@ -0,0 +1,17 @@
"""
Módulo de líneas de activos fijos (FA Item Lines) para Anexo 24
"""
from .models import FaLineItem
from .dto import FaLineItemCreateDTO, FaLineItemUpdateDTO, FaLineItemResponseDTO
from .service import FaLineItemService
from .routes import router
__all__ = [
"FaLineItem",
"FaLineItemCreateDTO",
"FaLineItemUpdateDTO",
"FaLineItemResponseDTO",
"FaLineItemService",
"router",
]

View File

@@ -0,0 +1,167 @@
"""
DTOs (Data Transfer Objects) para líneas de activos fijos (FA Item Lines)
"""
from datetime import datetime
from typing import Optional
from pydantic import BaseModel, ConfigDict, Field
class FaLineItemCreateDTO(BaseModel):
"""DTO para crear una línea de activo fijo"""
# line_item_id references the id in a76.item_lines
line_item_id: int = Field(..., description="ID de la línea base en a76.item_lines")
# Asset information (SCAF specific)
asset_number: Optional[str] = Field(
None, max_length=25, description="Número de activo"
)
asset_photo: Optional[str] = Field(
None, max_length=255, description="Foto del activo fijo"
)
equipment_message: Optional[str] = Field(
None, max_length=40, description="Mensaje del equipo"
)
invoice_type_asset: Optional[str] = Field(
None, max_length=6, description="Tipo de factura para activo"
)
return_import_invoice: Optional[str] = Field(
None, max_length=15, description="Factura de importación de retorno"
)
return_import_date: Optional[int] = Field(
None, description="Fecha de factura de importación de retorno"
)
movement_type_import: Optional[str] = Field(
None, max_length=3, description="Tipo de movimiento de importación"
)
# Cross-references for import repair
search_invoice: Optional[str] = Field(
None, max_length=15, description="Factura de búsqueda cruzada"
)
search_line: Optional[int] = Field(None, description="Línea de búsqueda cruzada")
# Search type
search_type: Optional[str] = Field(
None, max_length=10, description="Tipo de búsqueda"
)
# Subitems
is_subitem: Optional[bool] = Field(False, description="Es subpartida")
contains_subitems: Optional[bool] = Field(False, description="Contiene subpartidas")
includes_subitems: Optional[bool] = Field(False, description="Incluye subpartidas")
subitem_number: Optional[int] = Field(None, description="Número de subpartida")
# Special flags
download: Optional[bool] = Field(None, description="Indicador de descarga")
own_equipment: Optional[bool] = Field(None, description="Equipo propio")
omit_annex31: Optional[bool] = Field(None, description="Omitir en Anexo 31")
class Config:
from_attributes = True
class FaLineItemUpdateDTO(BaseModel):
"""DTO para actualizar una línea de activo fijo"""
# Asset information (SCAF specific)
asset_number: Optional[str] = Field(
None, max_length=25, description="Número de activo"
)
asset_photo: Optional[str] = Field(
None, max_length=255, description="Foto del activo fijo"
)
equipment_message: Optional[str] = Field(
None, max_length=40, description="Mensaje del equipo"
)
invoice_type_asset: Optional[str] = Field(
None, max_length=6, description="Tipo de factura para activo"
)
return_import_invoice: Optional[str] = Field(
None, max_length=15, description="Factura de importación de retorno"
)
return_import_date: Optional[int] = Field(
None, description="Fecha de factura de importación de retorno"
)
movement_type_import: Optional[str] = Field(
None, max_length=3, description="Tipo de movimiento de importación"
)
# Cross-references for import repair
search_invoice: Optional[str] = Field(
None, max_length=15, description="Factura de búsqueda cruzada"
)
search_line: Optional[int] = Field(None, description="Línea de búsqueda cruzada")
# Search type
search_type: Optional[str] = Field(
None, max_length=10, description="Tipo de búsqueda"
)
# Subitems
is_subitem: Optional[bool] = Field(False, description="Es subpartida")
contains_subitems: Optional[bool] = Field(False, description="Contiene subpartidas")
includes_subitems: Optional[bool] = Field(False, description="Incluye subpartidas")
subitem_number: Optional[int] = Field(None, description="Número de subpartida")
# Special flags
download: Optional[bool] = Field(None, description="Indicador de descarga")
own_equipment: Optional[bool] = Field(None, description="Equipo propio")
omit_annex31: Optional[bool] = Field(None, description="Omitir en Anexo 31")
class Config:
from_attributes = True
class FaLineItemResponseDTO(BaseModel):
"""DTO para respuesta de línea de activo fijo"""
id: int = Field(..., description="ID de la línea de activo fijo")
tenant_id: int = Field(..., description="ID del tenant")
company_id: int = Field(..., description="ID de la empresa")
# Asset information (SCAF specific)
asset_number: Optional[str] = Field(None, description="Número de activo")
asset_photo: Optional[str] = Field(None, description="Foto del activo fijo")
equipment_message: Optional[str] = Field(None, description="Mensaje del equipo")
invoice_type_asset: Optional[str] = Field(
None, description="Tipo de factura para activo"
)
return_import_invoice: Optional[str] = Field(
None, description="Factura de importación de retorno"
)
return_import_date: Optional[int] = Field(
None, description="Fecha de factura de importación de retorno"
)
movement_type_import: Optional[str] = Field(
None, description="Tipo de movimiento de importación"
)
# Cross-references for import repair
search_invoice: Optional[str] = Field(
None, description="Factura de búsqueda cruzada"
)
search_line: Optional[int] = Field(None, description="Línea de búsqueda cruzada")
# Search type
search_type: Optional[str] = Field(None, description="Tipo de búsqueda")
# Subitems
is_subitem: Optional[bool] = Field(False, description="Es subpartida")
contains_subitems: Optional[bool] = Field(False, description="Contiene subpartidas")
includes_subitems: Optional[bool] = Field(False, description="Incluye subpartidas")
subitem_number: Optional[int] = Field(None, description="Número de subpartida")
# Special flags
download: Optional[bool] = Field(None, description="Indicador de descarga")
own_equipment: Optional[bool] = Field(None, description="Equipo propio")
omit_annex31: Optional[bool] = Field(None, description="Omitir en Anexo 31")
# Timestamps
created_at: datetime = Field(..., description="Fecha de creación")
updated_at: datetime = Field(..., description="Fecha de actualización")
class Config:
from_attributes = True

View File

@@ -1,34 +1,84 @@
from typing import Optional
from sqlalchemy import Boolean, ForeignKey, Integer, String
from sqlalchemy.orm import Mapped, mapped_column
"""
Modelo ORM para datos específicos de líneas de Activos Fijos - Anexo 24
Extensión de a76.item_lines para SCAF (Sistema de Control de Activo Fijo)
"""
from typing import TYPE_CHECKING, Optional
from sqlalchemy import (
Boolean,
ForeignKeyConstraint,
Integer,
PrimaryKeyConstraint,
String,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
class LineItem(Base):
__tablename__ = "line_items"
if TYPE_CHECKING:
from api.v1.modules.a76.items.line_items.models import LineItem
class FaLineItem(Base, TenantScopedMixin, TimestampMixin):
"""
Tabla fa_item_lines: Extensión de Anexo 24 para líneas de Activos Fijos.
Hereda el ID de la tabla item_lines en a76.
"""
__tablename__ = "fa_item_lines"
__table_args__ = (
{"schema": "a24"}
PrimaryKeyConstraint("id", name="fa_item_lines_pkey"),
ForeignKeyConstraint(
["id"], ["a76.item_lines.id"], name="fk_fa_item_lines_master"
),
{"schema": "a24"},
)
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
item_id: Mapped[int] = mapped_column(ForeignKey("a76.items.id"))
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=False)
# Asset information (SCAF specific)
asset_number: Mapped[Optional[str]] = mapped_column(String(25)) # ASSETNUMBER
asset_photo: Mapped[Optional[str]] = mapped_column(String(255)) # FOTOACTIVOFIJO
equipment_message: Mapped[Optional[str]] = mapped_column(String(40)) # EQI_MENSAJE
invoice_type_asset: Mapped[Optional[str]] = mapped_column(String(6)) # TIPOFACTURAASSET
return_import_invoice: Mapped[Optional[str]] = mapped_column(String(15)) # FACTURAIMPORET
return_import_date: Mapped[Optional[int]] = mapped_column(Integer) # FECHAFACIMPORET
movement_type_import: Mapped[Optional[str]] = mapped_column(String(3)) # TIPOMOVIMPO
equipment_message: Mapped[Optional[str]] = mapped_column(String(40)) # EQI_MENSAJE
invoice_type_asset: Mapped[Optional[str]] = mapped_column(
String(6)
) # TIPOFACTURAASSET
return_import_invoice: Mapped[Optional[str]] = mapped_column(
String(15)
) # FACTURAIMPORET
return_import_date: Mapped[Optional[int]] = mapped_column(
Integer
) # FECHAFACIMPORET
movement_type_import: Mapped[Optional[str]] = mapped_column(
String(3)
) # TIPOMOVIMPO
# Cross-references IN CASE OF IMPORT REPAIR
search_invoice: Mapped[Optional[str]] = mapped_column(String(15)) # FACTURAEXPO (when line is import) / FACTURAIMPO (when line is export)
search_line: Mapped[Optional[int]] = mapped_column(Integer) # LINEAEXPO (when line is import) / LINEAIMPO (when line is export)
search_invoice: Mapped[Optional[str]] = mapped_column(
String(15)
) # FACTURAEXPO (when line is import) / FACTURAIMPO (when line is export)
search_line: Mapped[Optional[int]] = mapped_column(
Integer
) # LINEAEXPO (when line is import) / LINEAIMPO (when line is export)
# Search type
search_type: Mapped[Optional[str]] = mapped_column(String(10)) # TIPOBUSQUEDA
# Special flags
download: Mapped[Optional[bool]] = mapped_column(Boolean) # DESCARGA
# Subitems
is_subitem: Mapped[Optional[bool]] = mapped_column(Boolean) # ESSUBPARTIDA
contains_subitems: Mapped[Optional[bool]] = mapped_column(Boolean) # CONTIENESUBP
includes_subitems: Mapped[Optional[bool]] = mapped_column(
Boolean
) # INCUYESUBPARTIDAS
subitem_number: Mapped[Optional[int]] = mapped_column(Integer) # SUBPARTIDA
# Special flags
download: Mapped[Optional[bool]] = mapped_column(Boolean) # DESCARGA
own_equipment: Mapped[Optional[bool]] = mapped_column(Boolean) # EQUIPOPROPIO
omit_annex31: Mapped[Optional[bool]] = mapped_column(Boolean) # OMITITENANEXO31
omit_annex31: Mapped[Optional[bool]] = mapped_column(Boolean) # OMITITENANEXO31
# --- RELACIÓN ---
master_info: Mapped["LineItem"] = relationship("LineItem", back_populates="fa_data")
def __repr__(self) -> str:
return f"<FaLineItem(id={self.id}, asset_number='{self.asset_number}')>"

View File

@@ -0,0 +1,35 @@
"""
Endpoints API para gestión de líneas de activos fijos (FA Item Lines)
"""
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 FaLineItemCreateDTO, FaLineItemResponseDTO, FaLineItemUpdateDTO
from .service import FaLineItemService
# Create router with generic CRUD routes
crud_routes = TenantCRUDRoutes(
service=FaLineItemService,
create_schema=FaLineItemCreateDTO,
update_schema=FaLineItemUpdateDTO,
response_schema=FaLineItemResponseDTO,
prefix="/fa/item-lines",
tags=["a24 / fa / item-lines"],
resource_name="Fixed Asset Line Item",
id_name="fa_line_item_id",
enable_list=True,
enable_filters=True,
default_page_size=50,
max_page_size=100,
)
router = crud_routes.router

View File

@@ -0,0 +1,258 @@
"""
Capa de servicio para lógica de negocio de líneas de activos fijos (FA Item Lines)
"""
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 FaLineItemCreateDTO, FaLineItemResponseDTO, FaLineItemUpdateDTO
from .models import FaLineItem
logger = logging.getLogger(__name__)
class FaLineItemService:
"""Servicio para gestión de líneas 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[FaLineItem], int]:
"""
Obtener todas las líneas de activos fijos con paginación y filtros
"""
query = db.query(FaLineItem).filter(
FaLineItem.tenant_id == tenant_id, FaLineItem.company_id == company_id
)
if filters:
if filters.get("asset_number"):
query = query.filter(
FaLineItem.asset_number.ilike(f"%{filters['asset_number']}%")
)
if filters.get("invoice_type_asset"):
query = query.filter(
FaLineItem.invoice_type_asset == filters["invoice_type_asset"]
)
if filters.get("own_equipment") is not None:
query = query.filter(
FaLineItem.own_equipment == filters["own_equipment"]
)
if filters.get("download") is not None:
query = query.filter(FaLineItem.download == filters["download"])
total = query.count()
items = query.offset(skip).limit(limit).all()
return items, total
@staticmethod
def get_by_id(
db: Session, fa_line_item_id: int, tenant_id: int, company_id: int
) -> Optional[FaLineItem]:
"""Obtener una línea de activo fijo por ID"""
return (
db.query(FaLineItem)
.filter(
FaLineItem.id == fa_line_item_id,
FaLineItem.tenant_id == tenant_id,
FaLineItem.company_id == company_id,
)
.first()
)
@staticmethod
def get_by_line_item_id(
db: Session, line_item_id: int, tenant_id: int, company_id: int
) -> Optional[FaLineItem]:
"""Obtener una línea de activo fijo por line_item_id de a76"""
return (
db.query(FaLineItem)
.filter(
FaLineItem.id == line_item_id,
FaLineItem.tenant_id == tenant_id,
FaLineItem.company_id == company_id,
)
.first()
)
@staticmethod
def create(
db: Session,
fa_line_item_data: FaLineItemCreateDTO,
tenant_id: int,
company_id: int,
) -> FaLineItem:
"""Crear una nueva línea de activo fijo"""
try:
# Verificar que la línea base existe en a76.item_lines
from api.v1.modules.a76.items.line_items.models import LineItem
base_line_item = (
db.query(LineItem)
.filter(
LineItem.id == fa_line_item_data.line_item_id,
LineItem.tenant_id == tenant_id,
LineItem.company_id == company_id,
)
.first()
)
if not base_line_item:
raise HTTPException(
status_code=404,
detail=f"Base line item with id {fa_line_item_data.line_item_id} not found",
)
# Verificar si ya existe una línea FA para esta línea base
existing_fa = (
db.query(FaLineItem)
.filter(
FaLineItem.id == fa_line_item_data.line_item_id,
FaLineItem.tenant_id == tenant_id,
FaLineItem.company_id == company_id,
)
.first()
)
if existing_fa:
raise HTTPException(
status_code=400,
detail=f"FA line item already exists for line_item_id {fa_line_item_data.line_item_id}",
)
# Crear nueva línea FA
fa_line_item = FaLineItem(
id=fa_line_item_data.line_item_id, # Usa el mismo ID que la línea base
tenant_id=tenant_id,
company_id=company_id,
asset_number=fa_line_item_data.asset_number,
asset_photo=fa_line_item_data.asset_photo,
equipment_message=fa_line_item_data.equipment_message,
invoice_type_asset=fa_line_item_data.invoice_type_asset,
return_import_invoice=fa_line_item_data.return_import_invoice,
return_import_date=fa_line_item_data.return_import_date,
movement_type_import=fa_line_item_data.movement_type_import,
search_invoice=fa_line_item_data.search_invoice,
search_line=fa_line_item_data.search_line,
search_type=fa_line_item_data.search_type,
download=fa_line_item_data.download,
own_equipment=fa_line_item_data.own_equipment,
omit_annex31=fa_line_item_data.omit_annex31,
)
db.add(fa_line_item)
db.commit()
db.refresh(fa_line_item)
logger.info(
f"FA line item created: id={fa_line_item.id}, tenant={tenant_id}, company={company_id}"
)
return fa_line_item
except IntegrityError as e:
db.rollback()
logger.error(f"IntegrityError creating FA line item: {str(e)}")
raise HTTPException(
status_code=400,
detail="Error creating FA line item. Constraint violation.",
)
except HTTPException:
db.rollback()
raise
except Exception as e:
db.rollback()
logger.error(f"Error creating FA line item: {str(e)}")
raise HTTPException(
status_code=500, detail="Internal server error creating FA line item"
)
@staticmethod
def update(
db: Session,
fa_line_item_id: int,
fa_line_item_data: FaLineItemUpdateDTO,
tenant_id: int,
company_id: int,
) -> FaLineItem:
"""Actualizar una línea de activo fijo existente"""
try:
fa_line_item = FaLineItemService.get_by_id(
db, fa_line_item_id, tenant_id, company_id
)
if not fa_line_item:
raise HTTPException(
status_code=404,
detail=f"FA line item with id {fa_line_item_id} not found",
)
# Actualizar solo los campos proporcionados
update_data = fa_line_item_data.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(fa_line_item, field, value)
db.commit()
db.refresh(fa_line_item)
logger.info(
f"FA line item updated: id={fa_line_item.id}, tenant={tenant_id}, company={company_id}"
)
return fa_line_item
except HTTPException:
db.rollback()
raise
except Exception as e:
db.rollback()
logger.error(f"Error updating FA line item: {str(e)}")
raise HTTPException(
status_code=500, detail="Internal server error updating FA line item"
)
@staticmethod
def delete(
db: Session, fa_line_item_id: int, tenant_id: int, company_id: int
) -> bool:
"""Eliminar una línea de activo fijo"""
try:
fa_line_item = FaLineItemService.get_by_id(
db, fa_line_item_id, tenant_id, company_id
)
if not fa_line_item:
raise HTTPException(
status_code=404,
detail=f"FA line item with id {fa_line_item_id} not found",
)
db.delete(fa_line_item)
db.commit()
logger.info(
f"FA line item deleted: id={fa_line_item_id}, tenant={tenant_id}, company={company_id}"
)
return True
except HTTPException:
db.rollback()
raise
except Exception as e:
db.rollback()
logger.error(f"Error deleting FA line item: {str(e)}")
raise HTTPException(
status_code=500, detail="Internal server error deleting FA line item"
)

View File

@@ -6,9 +6,13 @@ from fastapi import APIRouter
# Importar routers de submódulos
from .fa.fa_classes.routes import router as fa_classes_router
from .fa.fa_item_lines.routes import router as fa_item_lines_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"])
router.include_router(
fa_item_lines_router, prefix="/a24", tags=["a24 / fa / item-lines"]
)

View File

@@ -1,6 +1,13 @@
from typing import Optional
from decimal import Decimal
from sqlalchemy import ForeignKey, Integer, String, ForeignKeyConstraint, UniqueConstraint, Numeric
from sqlalchemy import (
ForeignKey,
Integer,
String,
ForeignKeyConstraint,
UniqueConstraint,
Numeric,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
@@ -11,15 +18,14 @@ from core.database import Base
class UnitOfMeasureACE(Base, TimestampMixin):
__tablename__ = "unit_of_measure_ace"
__table_args__ = (
UniqueConstraint("code", name="uq_uom_ace_code"),
{"schema": "a76", "extend_existing": True}
UniqueConstraint("code", "tenant_id", "company_id", name="uq_uom_ace_code"),
{"schema": "a76", "extend_existing": True},
)
id: Mapped[int] = mapped_column(
Integer, primary_key=True, autoincrement=True)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
code: Mapped[str] = mapped_column(String(4), nullable=False) # CLAVEACE
description: Mapped[Optional[str]] = mapped_column(
String(49), nullable=True)
description: Mapped[Optional[str]] = mapped_column(String(49), nullable=True)
# 2. GUMOMA
@@ -27,15 +33,14 @@ class UnitOfMeasureACE(Base, TimestampMixin):
class UnitOfMeasureOMA(Base, TimestampMixin):
__tablename__ = "unit_of_measure_oma"
__table_args__ = (
UniqueConstraint("code", name="uq_uom_oma_code"),
{"schema": "a76", "extend_existing": True}
UniqueConstraint("code", "tenant_id", "company_id", name="uq_uom_oma_code"),
{"schema": "a76", "extend_existing": True},
)
id: Mapped[int] = mapped_column(
Integer, primary_key=True, autoincrement=True)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
code: Mapped[str] = mapped_column(String(10), nullable=False) # CLAVEUM
description: Mapped[Optional[str]] = mapped_column(
String(200), nullable=True)
description: Mapped[Optional[str]] = mapped_column(String(200), nullable=True)
# 3. GUMAme
@@ -43,15 +48,16 @@ class UnitOfMeasureOMA(Base, TimestampMixin):
class UnitOfMeasureAmerican(Base, TimestampMixin):
__tablename__ = "unit_of_measure_american"
__table_args__ = (
UniqueConstraint("code", name="uq_uom_american_code"),
{"schema": "a76", "extend_existing": True}
UniqueConstraint(
"code", "tenant_id", "company_id", name="uq_uom_american_code"
),
{"schema": "a76", "extend_existing": True},
)
id: Mapped[int] = mapped_column(
Integer, primary_key=True, autoincrement=True)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
code: Mapped[str] = mapped_column(String(3), nullable=False) # CLAVE
description: Mapped[Optional[str]] = mapped_column(
String(40), nullable=True)
description: Mapped[Optional[str]] = mapped_column(String(40), nullable=True)
# 4. GUMAduana
@@ -59,17 +65,17 @@ class UnitOfMeasureAmerican(Base, TimestampMixin):
class UnitOfMeasureCustoms(Base, TimestampMixin):
__tablename__ = "unit_of_measure_customs"
__table_args__ = (
UniqueConstraint("code", name="uq_uom_customs_code"),
{"schema": "a76", "extend_existing": True}
UniqueConstraint("code", "tenant_id", "company_id", name="uq_uom_customs_code"),
{"schema": "a76", "extend_existing": True},
)
id: Mapped[int] = mapped_column(
Integer, primary_key=True, autoincrement=True)
code: Mapped[str] = mapped_column(String(10), nullable=False) # CLAVE
description: Mapped[Optional[str]] = mapped_column(
String(50), nullable=True)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
code: Mapped[str] = mapped_column(String(2), nullable=False) # CLAVE
description: Mapped[Optional[str]] = mapped_column(String(20), nullable=True)
scaii_unit_code: Mapped[Optional[str]] = mapped_column(
String(5), nullable=True) # UNIDADSCAII
String(5), nullable=True
) # UNIDADSCAII
# 5. GUniMedida (Main)
@@ -77,57 +83,80 @@ class UnitOfMeasureCustoms(Base, TimestampMixin):
class UnitOfMeasure(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "units_of_measure"
__table_args__ = (
UniqueConstraint("code", "tenant_id",
"company_id", name="uq_uom_code"),
UniqueConstraint("code", "tenant_id", "company_id", name="uq_uom_code"),
ForeignKeyConstraint(
["customs_code"],
["a76.unit_of_measure_customs.code"],
["customs_code", "tenant_id", "company_id"],
[
"a76.unit_of_measure_customs.code",
"a76.unit_of_measure_customs.tenant_id",
"a76.unit_of_measure_customs.company_id",
],
use_alter=True,
name="fk_uom_customs"
name="fk_uom_customs",
),
ForeignKeyConstraint(
["american_code"],
["a76.unit_of_measure_american.code"],
["american_code", "tenant_id", "company_id"],
[
"a76.unit_of_measure_american.code",
"a76.unit_of_measure_american.tenant_id",
"a76.unit_of_measure_american.company_id",
],
use_alter=True,
name="fk_uom_american"
name="fk_uom_american",
),
ForeignKeyConstraint(
["ace_code"],
["a76.unit_of_measure_ace.code"],
["ace_code", "tenant_id", "company_id"],
[
"a76.unit_of_measure_ace.code",
"a76.unit_of_measure_ace.tenant_id",
"a76.unit_of_measure_ace.company_id",
],
use_alter=True,
name="fk_uom_ace"
name="fk_uom_ace",
),
ForeignKeyConstraint(
["oma_code"],
["a76.unit_of_measure_oma.code"],
["oma_code", "tenant_id", "company_id"],
[
"a76.unit_of_measure_oma.code",
"a76.unit_of_measure_oma.tenant_id",
"a76.unit_of_measure_oma.company_id",
],
use_alter=True,
name="fk_uom_oma"
name="fk_uom_oma",
),
{"schema": "a76", "extend_existing": True}
{"schema": "a76", "extend_existing": True},
)
id: Mapped[int] = mapped_column(
Integer, primary_key=True, autoincrement=True)
code: Mapped[str] = mapped_column(String(10), nullable=False) # CLAVEUNI
description: Mapped[Optional[str]] = mapped_column(
String(100), nullable=True)
description_en: Mapped[Optional[str]] = mapped_column(
String(100), nullable=True)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
code: Mapped[str] = mapped_column(String(5), nullable=False) # CLAVEUNI
description: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
description_en: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
customs_code: Mapped[Optional[str]] = mapped_column(
String(10), nullable=True) # CLAVE_AMEX
String(2), nullable=True
) # CLAVE_AMEX
american_code: Mapped[Optional[str]] = mapped_column(
String(3), nullable=True) # CLAVE_AAMER
String(3), nullable=True
) # CLAVE_AAMER
ace_code: Mapped[Optional[str]] = mapped_column(
String(4), nullable=True) # CLAVEACE
String(4), nullable=True
) # CLAVEACE
oma_code: Mapped[Optional[str]] = mapped_column(
String(10), nullable=True) # CLAVEOMA
String(10), nullable=True
) # CLAVEOMA
# Relationships omitted for simplicity or need explicit primaryjoin
customs_unit: Mapped[Optional["UnitOfMeasureCustoms"]] = relationship()
american_unit: Mapped[Optional["UnitOfMeasureAmerican"]] = relationship()
ace_unit: Mapped[Optional["UnitOfMeasureACE"]] = relationship()
oma_unit: Mapped[Optional["UnitOfMeasureOMA"]] = relationship()
american_unit: Mapped[Optional["UnitOfMeasureAmerican"]] = relationship(
overlaps="customs_unit"
)
ace_unit: Mapped[Optional["UnitOfMeasureACE"]] = relationship(
overlaps="american_unit,customs_unit"
)
oma_unit: Mapped[Optional["UnitOfMeasureOMA"]] = relationship(
overlaps="ace_unit,american_unit,customs_unit"
)
# 6. GUniMed (General/Conversion)
@@ -135,40 +164,48 @@ class UnitOfMeasure(Base, TenantScopedMixin, TimestampMixin):
class UnitOfMeasureGeneral(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "units_of_measure_general"
__table_args__ = (
UniqueConstraint("code", "tenant_id", "company_id",
name="uq_uom_general_code"),
UniqueConstraint("code", "tenant_id", "company_id", name="uq_uom_general_code"),
ForeignKeyConstraint(
["customs_code"],
["a76.unit_of_measure_customs.code"],
["customs_code", "tenant_id", "company_id"],
[
"a76.unit_of_measure_customs.code",
"a76.unit_of_measure_customs.tenant_id",
"a76.unit_of_measure_customs.company_id",
],
use_alter=True,
name="fk_uom_general_customs"
name="fk_uom_general_customs",
),
ForeignKeyConstraint(
["ace_code"],
["a76.unit_of_measure_ace.code"],
["ace_code", "tenant_id", "company_id"],
[
"a76.unit_of_measure_ace.code",
"a76.unit_of_measure_ace.tenant_id",
"a76.unit_of_measure_ace.company_id",
],
use_alter=True,
name="fk_uom_general_ace"
name="fk_uom_general_ace",
),
{"schema": "a76", "extend_existing": True}
{"schema": "a76", "extend_existing": True},
)
id: Mapped[int] = mapped_column(
Integer, primary_key=True, autoincrement=True)
code: Mapped[str] = mapped_column(String(10), nullable=False) # UNIDAD
description: Mapped[Optional[str]] = mapped_column(
String(100), nullable=True)
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
code: Mapped[str] = mapped_column(String(5), nullable=False) # UNIDAD
description: Mapped[Optional[str]] = mapped_column(String(100), nullable=True)
conversion_factor: Mapped[Optional[Decimal]] = mapped_column(
Numeric(13, 6), nullable=True)
mexico_unit: Mapped[Optional[str]] = mapped_column(
String(10), nullable=True)
Numeric(13, 6), nullable=True
)
mexico_unit: Mapped[Optional[str]] = mapped_column(String(5), nullable=True)
# UNIDAD_AME (Note: GUniMed has UNIDAD_AME varchar(5), but GUMAme has CLAVE varchar(3). Keeping as string for now)
american_unit_code: Mapped[Optional[str]
] = mapped_column(String(5), nullable=True)
american_unit_code: Mapped[Optional[str]] = mapped_column(String(5), nullable=True)
customs_code: Mapped[Optional[str]] = mapped_column(
String(10), nullable=True) # CLAVE_ADUANA
String(2), nullable=True
) # CLAVE_ADUANA
ace_code: Mapped[Optional[str]] = mapped_column(
String(4), nullable=True) # CLAVEACE
String(4), nullable=True
) # CLAVEACE
customs_unit: Mapped[Optional["UnitOfMeasureCustoms"]] = relationship()
ace_unit: Mapped[Optional["UnitOfMeasureACE"]] = relationship(overlaps="customs_unit")
ace_unit: Mapped[Optional["UnitOfMeasureACE"]] = relationship(
overlaps="customs_unit"
)

View File

@@ -1,9 +1,12 @@
from sqlalchemy import func
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.a76.customs_brokers.models import CustomsBroker
from api.v1.modules.public.reference_data.incoterms.models import Incoterm
from ....models import InvoiceComplianceMx
from api.v1.modules.a76.items.models import Item
from api.v1.modules.public.reference_data.currency_types.models import CurrencyType
from api.v1.modules.public.reference_data.customs_sections.models import CustomsSection
@@ -38,7 +41,7 @@ def validate_common(
)
if not invoice.compliance_mx.is_regime_change:
if not pedimento.operation_type == 1:
if not pedimento.operation_type == "imp":
errors.add_error(
field="compliance_mx.pedimento_id",
message="El Pedimento seleccionado no corresponde a una Importación.",
@@ -106,10 +109,24 @@ def validate_common(
)
if pedimento.pedimento_type == "consolidated":
if (
invoice.invoice_date < pedimento.pedimento_dates.entry_date
or invoice.invoice_date > pedimento.pedimento_dates.end_date
):
# Convertir invoice_date a date si es datetime para poder comparar
invoice_date = (
invoice.invoice_date.date()
if hasattr(invoice.invoice_date, "date")
else invoice.invoice_date
)
entry_date = (
pedimento.pedimento_dates.entry_date.date()
if hasattr(pedimento.pedimento_dates.entry_date, "date")
else pedimento.pedimento_dates.entry_date
)
end_date = (
pedimento.pedimento_dates.end_date.date()
if hasattr(pedimento.pedimento_dates.end_date, "date")
else pedimento.pedimento_dates.end_date
)
if invoice_date < entry_date or invoice_date > 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}.",
@@ -138,23 +155,23 @@ def validate_common(
)
duplicated_remesa = (
db.query(Pedimentos)
db.query(InvoiceComplianceMx)
.filter(
Pedimentos.remesa == invoice.compliance_mx.remesa,
Pedimentos.id != invoice.compliance_mx.pedimento_id,
Pedimentos.tenant_id == tenant_id,
Pedimentos.company_id == company_id,
InvoiceComplianceMx.remesa == invoice.compliance_mx.remesa,
InvoiceComplianceMx.tenant_id == tenant_id,
InvoiceComplianceMx.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,
)
if duplicated_remesa and hasattr(invoice, "id"):
if invoice.id != duplicated_remesa.invoice_id:
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 not invoice.compliance_mx.is_pedimento_pending:
errors.add_error(
@@ -190,7 +207,7 @@ def validate_common(
exchange_rate_exists = (
db.query(ExchangeRate)
.filter(
ExchangeRate.date == invoice.invoice_date,
func.date(ExchangeRate.date) == invoice.invoice_date,
ExchangeRate.tenant_id == tenant_id,
ExchangeRate.company_id == company_id,
)
@@ -204,6 +221,8 @@ def validate_common(
code="EXCHANGE_RATE_NOT_FOUND",
value=invoice.financials.exchange_rate,
)
else:
invoice.financials.exchange_rate = exchange_rate_exists.value
if invoice.compliance_mx.is_regime_change:
if invoice.document_type in ["EXD", "ETE", "ETR"]:
@@ -281,11 +300,11 @@ def validate_common(
)
customs_broker_exists = (
db.query(ClientProvider)
db.query(CustomsBroker)
.filter(
ClientProvider.id == invoice.compliance_mx.customs_broker_id,
ClientProvider.tenant_id == tenant_id,
ClientProvider.company_id == company_id,
CustomsBroker.id == invoice.compliance_mx.customs_broker_id,
CustomsBroker.tenant_id == tenant_id,
CustomsBroker.company_id == company_id,
)
.first()
)
@@ -387,7 +406,11 @@ def validate_common(
value=invoice.financials.currency,
)
if invoice.financials.currency == "manual":
if invoice.financials.currency == "foreign":
invoice.financials.currency_type = "USD"
elif invoice.financials.currency == "local":
invoice.financials.currency_type = "MXN"
elif invoice.financials.currency == "manual":
if not invoice.financials.currency_type:
errors.add_error(
field="financials.currency_type",

View File

@@ -1,20 +1,36 @@
from enum import Enum
from typing import Optional, List
from sqlalchemy import BigInteger, Boolean, Date, ForeignKey, Integer, Numeric, String, Text, TIMESTAMP
from typing import Optional, List, TYPE_CHECKING
from sqlalchemy import (
BigInteger,
Boolean,
Date,
ForeignKey,
Integer,
Numeric,
String,
Text,
TIMESTAMP,
)
from sqlalchemy.orm import Mapped, mapped_column, relationship
from core.database import Base
from datetime import datetime
from ....common.base_models import TenantScopedMixin, TimestampMixin
if TYPE_CHECKING:
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
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"
@@ -24,6 +40,7 @@ class DestinationOriginCove(str, Enum):
INTERIOR_PAIS = "interior_pais"
MPIO_CABORCA_SON = "mpio_caborca_son"
class OperationType(str, Enum):
IMP = "imp" # Importación
EXP = "exp" # Exportación
@@ -55,39 +72,73 @@ class InvoiceHeader(Base, TenantScopedMixin, TimestampMixin):
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
# Identifiers
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
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
alternate_invoice: Mapped[Optional[str]] = mapped_column(String(99)) # FACTURAALTERNA
related_doc_id: Mapped[Optional[int]] = mapped_column(
Integer
) # IDRELDOC / Para Rectificaciones
alternate_invoice: Mapped[Optional[str]] = mapped_column(
String(99)
) # FACTURAALTERNA
invoice_ref: Mapped[Optional[str]] = mapped_column(String(19)) # FACTURAEXPOREF
proforma_number: Mapped[Optional[str]] = mapped_column(String(20)) # NUMEROPROFORMA
# Dates
invoice_date: Mapped[datetime] = mapped_column(Date) # FECHAFACTURA
capture_date: Mapped[datetime] = mapped_column(TIMESTAMP(timezone=False), default=datetime.now) # FECHACAPTURA + HORAACTUAL
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[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
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
# Comments
observation_es: Mapped[Optional[str]] = mapped_column(Text) # OBSERVACIONE / Observaciones en español
observation_en: Mapped[Optional[str]] = mapped_column(Text) # OBSERVACIONI / Observaciones en inglés
observation_es: Mapped[Optional[str]] = mapped_column(
Text
) # OBSERVACIONE / Observaciones en español
observation_en: Mapped[Optional[str]] = mapped_column(
Text
) # OBSERVACIONI / Observaciones en inglés
comments_status: Mapped[Optional[str]] = mapped_column(Text) # COMENTARIOSESTATUS
vu_observations: Mapped[Optional[str]] = mapped_column(String(500)) # OBSERVACIONESVU / Observaciones VUCEM
vu_observations: Mapped[Optional[str]] = mapped_column(
String(500)
) # OBSERVACIONESVU / Observaciones VUCEM
# Digital Archive Links
cfdi_uuid: Mapped[Optional[str]] = mapped_column(String(100)) # CFDIUUID
@@ -96,36 +147,59 @@ class InvoiceHeader(Base, TenantScopedMixin, TimestampMixin):
# Control & Subcompany
subcompany: Mapped[Optional[str]] = mapped_column(String(5)) # SUBEMPRESA
party_count: Mapped[Optional[int]] = mapped_column(Integer) # CANT_PARTIDAS / Cantidad de partidas
party_count: Mapped[Optional[int]] = mapped_column(
Integer
) # CANT_PARTIDAS / Cantidad de partidas
# Generation flags
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[bool]] = mapped_column(Boolean, default=False) # APLICADESCMANUAL
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[bool]] = mapped_column(
Boolean, default=False
) # APLICADESCMANUAL
# Bulk & Downloads
is_bulk: Mapped[Optional[bool]] = mapped_column(Boolean) # ESAGRANEL / Es a granel
download_substance: Mapped[Optional[bool]] = mapped_column(Boolean) # DESCARGASUST / Descarga de sustancia
download_class: Mapped[Optional[bool]] = mapped_column(Boolean) # DESCARGACLASE / Descarga de clase
download_def: Mapped[Optional[bool]] = mapped_column(Boolean) # DESCARGADEF / Descarga definitiva
download_substance: Mapped[Optional[bool]] = mapped_column(
Boolean
) # DESCARGASUST / Descarga de sustancia
download_class: Mapped[Optional[bool]] = mapped_column(
Boolean
) # DESCARGACLASE / Descarga de clase
download_def: Mapped[Optional[bool]] = mapped_column(
Boolean
) # DESCARGADEF / Descarga definitiva
# Additional fields
payment_terms: Mapped[Optional[str]] = mapped_column(String(200)) # TERMINOSPAGO / Términos de pago
payment_terms: Mapped[Optional[str]] = mapped_column(
String(200)
) # TERMINOSPAGO / Términos de pago
handling_fees: Mapped[Optional[float]] = mapped_column(Numeric(19, 8)) # MANIOBRAS
option_iv18: Mapped[Optional[str]] = mapped_column(String(50)) # OPCIONIV18
enajenation_goods: Mapped[Optional[bool]] = mapped_column(Boolean) # ENAJENACIONBIENES / Enajenación de bienes
enajenation_goods: Mapped[Optional[bool]] = mapped_column(
Boolean
) # ENAJENACIONBIENES / Enajenación de bienes
# Relationships
compliance_mx: Mapped[Optional["InvoiceComplianceMx"]] = relationship(
back_populates="header", cascade="all, delete-orphan", uselist=False)
back_populates="header", cascade="all, delete-orphan", uselist=False
)
financials: Mapped[Optional["InvoiceFinancials"]] = relationship(
back_populates="header", cascade="all, delete-orphan", uselist=False)
back_populates="header", cascade="all, delete-orphan", uselist=False
)
details: Mapped[List["InvoiceSalesDetails"]] = relationship(
back_populates="header", cascade="all, delete-orphan")
back_populates="header", cascade="all, delete-orphan"
)
collections: Mapped[List["InvoiceCollections"]] = relationship(
back_populates="header", cascade="all, delete-orphan")
logistics: Mapped[List["InvoiceLogistics"]] = relationship(
back_populates="header", cascade="all, delete-orphan")
back_populates="header", cascade="all, delete-orphan"
)
logistics: Mapped[Optional["InvoiceLogistics"]] = relationship(
back_populates="header", cascade="all, delete-orphan", uselist=False
)
# --- 2. Compliance MX (invoice_compliance_mx) ---
@@ -133,81 +207,186 @@ class InvoiceComplianceMx(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "invoice_compliance_mx"
__table_args__ = ({"schema": "a76"},)
invoice_id: Mapped[int] = mapped_column(ForeignKey("a76.invoice_header.id"), primary_key=True)
invoice_id: Mapped[int] = mapped_column(
ForeignKey("a76.invoice_header.id"), primary_key=True
)
# Core Customs Data
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
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
destination: Mapped[Optional[str]] = mapped_column(String(3)) # DESTINO / Código de destino
manifest_number: Mapped[Optional[str]] = mapped_column(String(15)) # MANIFIESTO / Número de manifiesto
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
destination: Mapped[Optional[str]] = mapped_column(
String(3)
) # DESTINO / Código de destino
manifest_number: Mapped[Optional[str]] = mapped_column(
String(15)
) # MANIFIESTO / Número de manifiesto
# Clients & Providers
provider_header: Mapped[Optional[str]] = mapped_column(String(20)) # PROVEEDOREXPORTADOR
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[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[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[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
provider_header: Mapped[Optional[str]] = mapped_column(
String(20)
) # PROVEEDOREXPORTADOR
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[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[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[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
broker_invoice_date: Mapped[Optional[datetime]] = mapped_column(Date) # FECHAFACBROKER / Fecha factura broker
broker_invoice_num: Mapped[Optional[str]] = mapped_column(
String(20)
) # NUMFACTURABROKER / Número factura broker
broker_invoice_date: Mapped[Optional[datetime]] = mapped_column(
Date
) # FECHAFACBROKER / Fecha factura broker
# Flags & Specific Regimes
is_mixed: Mapped[Optional[bool]] = mapped_column(Boolean) # ESMIXTO / Es mixto
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[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)
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[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[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
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
edocument: Mapped[Optional[str]] = mapped_column(String(50)) # EDOCUMENT / Documento electrónico
electronic_signature: Mapped[Optional[str]] = mapped_column(String(999)) # FIRMAELECTRONICA / Firma electrónica
certificate_number: Mapped[Optional[str]] = mapped_column(String(99)) # NUMEROCERTIFICADO / Número de certificado
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[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
edocument: Mapped[Optional[str]] = mapped_column(
String(50)
) # EDOCUMENT / Documento electrónico
electronic_signature: Mapped[Optional[str]] = mapped_column(
String(999)
) # FIRMAELECTRONICA / Firma electrónica
certificate_number: Mapped[Optional[str]] = mapped_column(
String(99)
) # NUMEROCERTIFICADO / Número de certificado
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[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
# Additional Control
contingency_mode: Mapped[Optional[bool]] = mapped_column(Boolean) # MODOCONTINGENCIA / Modo contingencia
enclosure: Mapped[Optional[str]] = mapped_column(String(4)) # RECINTO / Recinto fiscal
guide_type_to_identify: Mapped[Optional[str]] = mapped_column(String(1)) # TIPODEGUIAAIDENTIFICAR / Tipo de guía a identificar
location: Mapped[Optional[str]] = mapped_column(String(200)) # LOCALIZACION / Localización
contingency_mode: Mapped[Optional[bool]] = mapped_column(
Boolean
) # MODOCONTINGENCIA / Modo contingencia
enclosure: Mapped[Optional[str]] = mapped_column(
String(4)
) # RECINTO / Recinto fiscal
guide_type_to_identify: Mapped[Optional[str]] = mapped_column(
String(1)
) # TIPODEGUIAAIDENTIFICAR / Tipo de guía a identificar
location: Mapped[Optional[str]] = mapped_column(
String(200)
) # LOCALIZACION / Localización
# DOT & Official
dot_code: Mapped[Optional[str]] = mapped_column(String(20)) # CLAVEDOT / Clave DOT
subdivision: Mapped[Optional[str]] = mapped_column(String(20)) # SUBDIVISION / Subdivisión
acts_as: Mapped[Optional[str]] = mapped_column(String(20)) # FUNGECOMOCO / Funge como
movement_type: Mapped[Optional[str]] = mapped_column(String(31)) # TIPOMOV / Tipo de movimiento
office_document: Mapped[Optional[str]] = mapped_column(String(30)) # OFICIO / Oficio
reason_export: Mapped[Optional[str]] = mapped_column(String(1)) # RAZONEXPORTACION / Razón de exportación
signature_key: Mapped[Optional[str]] = mapped_column(String(10)) # CLAVEFIRMA / Clave de firma
# SM specific
sem_id: Mapped[Optional[int]] = mapped_column(Integer) # SEM / ID SEM (de SFacEntradaSM/SFacSalidaSM)
subdivision: Mapped[Optional[str]] = mapped_column(
String(20)
) # SUBDIVISION / Subdivisión
acts_as: Mapped[Optional[str]] = mapped_column(
String(20)
) # FUNGECOMOCO / Funge como
movement_type: Mapped[Optional[str]] = mapped_column(
String(31)
) # TIPOMOV / Tipo de movimiento
office_document: Mapped[Optional[str]] = mapped_column(
String(30)
) # OFICIO / Oficio
reason_export: Mapped[Optional[str]] = mapped_column(
String(1)
) # RAZONEXPORTACION / Razón de exportación
signature_key: Mapped[Optional[str]] = mapped_column(
String(10)
) # CLAVEFIRMA / Clave de firma
# Relationship
# SM specific
sem_id: Mapped[Optional[int]] = mapped_column(
Integer
) # SEM / ID SEM (de SFacEntradaSM/SFacSalidaSM)
# Relationships
header: Mapped["InvoiceHeader"] = relationship(back_populates="compliance_mx")
pedimento: Mapped[Optional["Pedimentos"]] = relationship(
foreign_keys=[pedimento_id], lazy="joined"
)
# --- 3. Financials (invoice_financials) ---
@@ -219,62 +398,138 @@ class InvoiceFinancials(Base, TenantScopedMixin, TimestampMixin):
invoice_id: Mapped[int] = mapped_column(ForeignKey("a76.invoice_header.id"))
# Currency
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
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
# Merchandise Values (MN = National Currency, ME = Foreign Currency, MC = Third Currency)
value_mn: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # VALORIMPOMN/VALOREXPOMN/VALORENTMN/VALORSALMN
value_me: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # VALORIMPOME/VALOREXPOME/VALORENTME/VALORSALME
value_mc: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # VALORIMPOMC/VALOREXPOMC
value_mn: Mapped[Optional[float]] = mapped_column(
Numeric(23, 8), default=0
) # VALORIMPOMN/VALOREXPOMN/VALORENTMN/VALORSALMN
value_me: Mapped[Optional[float]] = mapped_column(
Numeric(23, 8), default=0
) # VALORIMPOME/VALOREXPOME/VALORENTME/VALORSALME
value_mc: Mapped[Optional[float]] = mapped_column(
Numeric(23, 8), default=0
) # VALORIMPOMC/VALOREXPOMC
# Customs Value
customs_value_mn: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # VALORADUANASMN / Valor en aduanas MN
customs_value_me: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # VALORADUANASME / Valor en aduanas ME
customs_value_mn: Mapped[Optional[float]] = mapped_column(
Numeric(23, 8), default=0
) # VALORADUANASMN / Valor en aduanas MN
customs_value_me: Mapped[Optional[float]] = mapped_column(
Numeric(23, 8), default=0
) # VALORADUANASME / Valor en aduanas ME
# Raw Materials
raw_material_value_mn: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # VALORMPMN / Valor materia prima MN
raw_material_value_me: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # VALORMPME / Valor materia prima ME
raw_material_value_mn: Mapped[Optional[float]] = mapped_column(
Numeric(23, 8), default=0
) # VALORMPMN / Valor materia prima MN
raw_material_value_me: Mapped[Optional[float]] = mapped_column(
Numeric(23, 8), default=0
) # VALORMPME / Valor materia prima ME
# Aggregate Value
aggregate_value_mn: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # VALORAGREMN / Valor agregado MN
aggregate_value_me: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # VALORAGREME / Valor agregado ME
aggregate_value_mc: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # VALORAGREMC / Valor agregado MC
aggregate_value_mn: Mapped[Optional[float]] = mapped_column(
Numeric(23, 8), default=0
) # VALORAGREMN / Valor agregado MN
aggregate_value_me: Mapped[Optional[float]] = mapped_column(
Numeric(23, 8), default=0
) # VALORAGREME / Valor agregado ME
aggregate_value_mc: Mapped[Optional[float]] = mapped_column(
Numeric(23, 8), default=0
) # VALORAGREMC / Valor agregado MC
# Mexican Merchandise Value
mexican_value_mn: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # VALORVMEXMN / Valor mercancía mexicana MN
mexican_value_me: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # VALORVMEXME / Valor mercancía mexicana ME
mexican_value_mc: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # VALORVMEXMC / Valor mercancía mexicana MC
mexican_value_mn: Mapped[Optional[float]] = mapped_column(
Numeric(23, 8), default=0
) # VALORVMEXMN / Valor mercancía mexicana MN
mexican_value_me: Mapped[Optional[float]] = mapped_column(
Numeric(23, 8), default=0
) # VALORVMEXME / Valor mercancía mexicana ME
mexican_value_mc: Mapped[Optional[float]] = mapped_column(
Numeric(23, 8), default=0
) # VALORVMEXMC / Valor mercancía mexicana MC
# National Packaging
national_packaging_mn: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # VALEMPAQUENACMN / Valor empaque nacional MN
national_packaging_me: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # VALEMPAQUENACME / Valor empaque nacional ME
national_packaging_mc: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # VALEMPAQUENACMC / Valor empaque nacional MC
national_packaging_mn: Mapped[Optional[float]] = mapped_column(
Numeric(23, 8), default=0
) # VALEMPAQUENACMN / Valor empaque nacional MN
national_packaging_me: Mapped[Optional[float]] = mapped_column(
Numeric(23, 8), default=0
) # VALEMPAQUENACME / Valor empaque nacional ME
national_packaging_mc: Mapped[Optional[float]] = mapped_column(
Numeric(23, 8), default=0
) # VALEMPAQUENACMC / Valor empaque nacional MC
# Costs & Increments
freight: Mapped[Optional[float]] = mapped_column(Numeric(19, 8), default=0) # FLETE / Flete
insurance: Mapped[Optional[float]] = mapped_column(Numeric(19, 8), default=0) # SEGUROS / Seguros
insurance_value: Mapped[Optional[float]] = mapped_column(Numeric(19, 8), default=0) # VALSEGUROS / Valor seguros
packaging: Mapped[Optional[float]] = mapped_column(Numeric(19, 8), default=0) # EMBALAJES / Embalajes
other_increments: Mapped[Optional[float]] = mapped_column(Numeric(19, 8), default=0) # OTROSINCREMENTA / Otros incrementables
total_increments_mn: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # TOTALINCREMMN / Total incrementables MN
total_increments_me: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # TOTALINCREMME / Total incrementables ME
freight: Mapped[Optional[float]] = mapped_column(
Numeric(19, 8), default=0
) # FLETE / Flete
insurance: Mapped[Optional[float]] = mapped_column(
Numeric(19, 8), default=0
) # SEGUROS / Seguros
insurance_value: Mapped[Optional[float]] = mapped_column(
Numeric(19, 8), default=0
) # VALSEGUROS / Valor seguros
packaging: Mapped[Optional[float]] = mapped_column(
Numeric(19, 8), default=0
) # EMBALAJES / Embalajes
other_increments: Mapped[Optional[float]] = mapped_column(
Numeric(19, 8), default=0
) # OTROSINCREMENTA / Otros incrementables
total_increments_mn: Mapped[Optional[float]] = mapped_column(
Numeric(23, 8), default=0
) # TOTALINCREMMN / Total incrementables MN
total_increments_me: Mapped[Optional[float]] = mapped_column(
Numeric(23, 8), default=0
) # TOTALINCREMME / Total incrementables ME
# Taxes
iva_mn: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # IVAEXPOMN/VALORIVAMN / IVA en MN
iva_me: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # IVAEXPOME/VALORIVAME / IVA en ME
iva_mc: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # IVAEXPOMC / IVA en MC
iva_factor: Mapped[Optional[str]] = mapped_column(String(10)) # FACTORIVA / Factor IVA (puede ser varchar en imports)
tax_value_me: Mapped[Optional[float]] = mapped_column(Numeric(23, 8), default=0) # VALORIMPUESTOME / Valor impuesto ME
seal_value_2500: Mapped[Optional[bool]] = mapped_column(Boolean) # SELLOVALOR2500 / Sello valor 2500
iva_mn: Mapped[Optional[float]] = mapped_column(
Numeric(23, 8), default=0
) # IVAEXPOMN/VALORIVAMN / IVA en MN
iva_me: Mapped[Optional[float]] = mapped_column(
Numeric(23, 8), default=0
) # IVAEXPOME/VALORIVAME / IVA en ME
iva_mc: Mapped[Optional[float]] = mapped_column(
Numeric(23, 8), default=0
) # IVAEXPOMC / IVA en MC
iva_factor: Mapped[Optional[str]] = mapped_column(
String(10)
) # FACTORIVA / Factor IVA (puede ser varchar en imports)
tax_value_me: Mapped[Optional[float]] = mapped_column(
Numeric(23, 8), default=0
) # VALORIMPUESTOME / Valor impuesto ME
seal_value_2500: Mapped[Optional[bool]] = mapped_column(
Boolean
) # SELLOVALOR2500 / Sello valor 2500
# Weights & Quantities
total_quantity: Mapped[Optional[float]] = mapped_column(Numeric(19, 8)) # CANTEXPO/CANTIMPO / Cantidad total
gross_weight: Mapped[Optional[float]] = mapped_column(Numeric(19, 8)) # PESOBRUTO / Peso bruto
net_weight: Mapped[Optional[float]] = mapped_column(Numeric(19, 8)) # PESONETO / Peso neto
bundle_count: Mapped[Optional[int]] = mapped_column(Integer) # CANTBULTOS / Cantidad de bultos
weight_factor: Mapped[Optional[float]] = mapped_column(Numeric(19, 8)) # FACTORPESO / Factor de peso
total_quantity: Mapped[Optional[float]] = mapped_column(
Numeric(19, 8)
) # CANTEXPO/CANTIMPO / Cantidad total
gross_weight: Mapped[Optional[float]] = mapped_column(
Numeric(19, 8)
) # PESOBRUTO / Peso bruto
net_weight: Mapped[Optional[float]] = mapped_column(
Numeric(19, 8)
) # PESONETO / Peso neto
bundle_count: Mapped[Optional[int]] = mapped_column(
Integer
) # CANTBULTOS / Cantidad de bultos
weight_factor: Mapped[Optional[float]] = mapped_column(
Numeric(19, 8)
) # FACTORPESO / Factor de peso
# Relationship
header: Mapped["InvoiceHeader"] = relationship(back_populates="financials")
@@ -288,62 +543,136 @@ class InvoiceLogistics(Base, TenantScopedMixin, TimestampMixin):
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
invoice_id: Mapped[int] = mapped_column(ForeignKey("a76.invoice_header.id"))
# Carrier Info
carrier_id: Mapped[Optional[str]] = mapped_column(String(10)) # TRANSPORTISTA / Transportista
transport_id: Mapped[Optional[str]] = mapped_column(String(10)) # NUMTRAILER / Transportista
transport_us_id: Mapped[Optional[str]] = mapped_column(String(10)) # TRANSPORTISTAAME / Transportista americano
transport_type: Mapped[TransportType] = mapped_column(String(15), default="none") # TRANSPORTE / Tipo de transporte
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[bool]] = mapped_column(Boolean, default=False) # ESFERROCARRIL / Es ferrocarril
rail_id: Mapped[Optional[str]] = mapped_column(String(31)) # IDFERRORCARRIL / ID ferrocarril
# Carrier Info
carrier_id: Mapped[Optional[str]] = mapped_column(
String(10)
) # TRANSPORTISTA / Transportista
transport_id: Mapped[Optional[str]] = mapped_column(
String(10)
) # NUMTRAILER / Transportista
transport_us_id: Mapped[Optional[str]] = mapped_column(
String(10)
) # TRANSPORTISTAAME / Transportista americano
transport_type: Mapped[TransportType] = mapped_column(
String(15), default="none"
) # TRANSPORTE / Tipo de transporte
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[bool]] = mapped_column(
Boolean, default=False
) # ESFERROCARRIL / Es ferrocarril
rail_id: Mapped[Optional[str]] = mapped_column(
String(31)
) # IDFERRORCARRIL / ID ferrocarril
# Vehicle & Tracking
vehicle_num: Mapped[Optional[str]] = mapped_column(String(20)) # NUMVEHICULO / Número de vehículo
license_plate: Mapped[Optional[str]] = mapped_column(String(20)) # NUMTRASPORTE / Número de transporte/placa
license_plate_complete: Mapped[Optional[str]] = mapped_column(String(40)) # NUMTRASPORTECOMPLE / Número transporte completo
trailer_num: Mapped[Optional[str]] = mapped_column(String(20)) # NUMTRAILER / Número de trailer
seal_number: Mapped[Optional[str]] = mapped_column(String(15)) # PRECINTO / Precinto
guide_number: Mapped[Optional[str]] = mapped_column(String(20)) # NUMEROGUIA / Número de guía
bill_number: Mapped[Optional[str]] = mapped_column(String(15)) # BILLNUMBER / Número de bill
reference_number: Mapped[Optional[str]] = mapped_column(String(14)) # NUMREFERENCIA / Número de referencia
shipment_number: Mapped[Optional[str]] = mapped_column(String(19)) # NUMEMBARQUE / Número de embarque
vehicle_num: Mapped[Optional[str]] = mapped_column(
String(20)
) # NUMVEHICULO / Número de vehículo
license_plate: Mapped[Optional[str]] = mapped_column(
String(20)
) # NUMTRASPORTE / Número de transporte/placa
license_plate_complete: Mapped[Optional[str]] = mapped_column(
String(40)
) # NUMTRASPORTECOMPLE / Número transporte completo
trailer_num: Mapped[Optional[str]] = mapped_column(
String(20)
) # NUMTRAILER / Número de trailer
seal_number: Mapped[Optional[str]] = mapped_column(
String(15)
) # PRECINTO / Precinto
guide_number: Mapped[Optional[str]] = mapped_column(
String(20)
) # NUMEROGUIA / Número de guía
bill_number: Mapped[Optional[str]] = mapped_column(
String(15)
) # BILLNUMBER / Número de bill
reference_number: Mapped[Optional[str]] = mapped_column(
String(14)
) # NUMREFERENCIA / Número de referencia
shipment_number: Mapped[Optional[str]] = mapped_column(
String(19)
) # NUMEMBARQUE / Número de embarque
# Incoterms
incoterm: Mapped[Optional[str]] = mapped_column(String(5)) # INCOTERM / Término de comercio internacional
incoterm: Mapped[Optional[str]] = mapped_column(
String(5)
) # INCOTERM / Término de comercio internacional
# Identifiers & Complements
identifier_1: Mapped[Optional[str]] = mapped_column(String(2)) # IDENTIFICADOR / Identificador 1
complement_1: Mapped[Optional[str]] = mapped_column(String(30)) # COMPLEMENTO1 / Complemento 1
identifier_2: Mapped[Optional[str]] = mapped_column(String(2)) # IDENTIFICADOR2 / Identificador 2
complement_2: Mapped[Optional[str]] = mapped_column(String(30)) # COMPLEMENTO2 / Complemento 2
identifier_1: Mapped[Optional[str]] = mapped_column(
String(2)
) # IDENTIFICADOR / Identificador 1
complement_1: Mapped[Optional[str]] = mapped_column(
String(30)
) # COMPLEMENTO1 / Complemento 1
identifier_2: Mapped[Optional[str]] = mapped_column(
String(2)
) # IDENTIFICADOR2 / Identificador 2
complement_2: Mapped[Optional[str]] = mapped_column(
String(30)
) # COMPLEMENTO2 / Complemento 2
# Weight & Container Info
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
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
# Locations & Routes
origin_location: Mapped[Optional[str]] = mapped_column(String(200)) # ORIGENUBICACION / Ubicación de origen
destination_location: Mapped[Optional[str]] = mapped_column(String(200)) # DESTINOUBICACION / Ubicación de destino
transport_itinerary: Mapped[Optional[str]] = mapped_column(String(1000)) # ITINERARIOTRANPORTE / Itinerario del transporte
destination_goods: Mapped[Optional[str]] = mapped_column(String(50)) # DESTINOMCIA / Destino de mercancía
origin_location: Mapped[Optional[str]] = mapped_column(
String(200)
) # ORIGENUBICACION / Ubicación de origen
destination_location: Mapped[Optional[str]] = mapped_column(
String(200)
) # DESTINOUBICACION / Ubicación de destino
transport_itinerary: Mapped[Optional[str]] = mapped_column(
String(1000)
) # ITINERARIOTRANPORTE / Itinerario del transporte
destination_goods: Mapped[Optional[str]] = mapped_column(
String(50)
) # DESTINOMCIA / Destino de mercancía
# Logistics Dates
entry_exit_date: Mapped[Optional[datetime]] = mapped_column(Date) # FECHAENTRADA/FECHAENVIO/FECHARECIBO / Fecha entrada/salida
delivery_date: Mapped[Optional[datetime]] = mapped_column(Date) # FECHAENTREGA / Fecha de entrega
entry_exit_date: Mapped[Optional[datetime]] = mapped_column(
Date
) # FECHAENTRADA/FECHAENVIO/FECHARECIBO / Fecha entrada/salida
delivery_date: Mapped[Optional[datetime]] = mapped_column(
Date
) # FECHAENTREGA / Fecha de entrega
# Delivery Control
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
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
payment_date: Mapped[Optional[datetime]] = mapped_column(Date) # FECHAPAGO / Fecha de pago
payment_receipt_num: Mapped[Optional[str]] = mapped_column(String(20)) # NUMRECIBOPAGO / Número de recibo de pago
payment_date: Mapped[Optional[datetime]] = mapped_column(
Date
) # FECHAPAGO / Fecha de pago
payment_receipt_num: Mapped[Optional[str]] = mapped_column(
String(20)
) # NUMRECIBOPAGO / Número de recibo de pago
# CTM Process
is_ctm_process: Mapped[Optional[bool]] = mapped_column(Boolean, default=False) # 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")
@@ -358,12 +687,20 @@ class InvoiceSalesDetails(Base, TenantScopedMixin, TimestampMixin):
invoice_id: Mapped[int] = mapped_column(ForeignKey("a76.invoice_header.id"))
line_number: Mapped[int] = mapped_column(Integer) # LINEA / Número de línea
sales_order: Mapped[Optional[str]] = mapped_column(String(20)) # ORDENVENTA / Orden de venta
sales_order: Mapped[Optional[str]] = mapped_column(
String(20)
) # ORDENVENTA / Orden de venta
# Specific Custom Fields
colors_description: Mapped[Optional[str]] = mapped_column(String(49)) # COLORES / Descripción de colores
square_color_code: Mapped[Optional[str]] = mapped_column(String(1)) # COLORCUADRITO / Código de color cuadrito
line_bundles: Mapped[Optional[int]] = mapped_column(Integer) # CANTBULTOS / Cantidad de bultos de la línea
colors_description: Mapped[Optional[str]] = mapped_column(
String(49)
) # COLORES / Descripción de colores
square_color_code: Mapped[Optional[str]] = mapped_column(
String(1)
) # COLORCUADRITO / Código de color cuadrito
line_bundles: Mapped[Optional[int]] = mapped_column(
Integer
) # CANTBULTOS / Cantidad de bultos de la línea
# Relationship
header: Mapped["InvoiceHeader"] = relationship(back_populates="details")
@@ -378,9 +715,11 @@ class InvoiceCollections(Base, TenantScopedMixin, TimestampMixin):
invoice_id: Mapped[int] = mapped_column(ForeignKey("a76.invoice_header.id"))
line_number: Mapped[int] = mapped_column(Integer) # LINEA / Número de línea
invoice_number: Mapped[Optional[str]] = mapped_column(String(15)) # FACTURA / Número de factura
invoice_number: Mapped[Optional[str]] = mapped_column(
String(15)
) # FACTURA / Número de factura
concept: Mapped[Optional[str]] = mapped_column(String(100)) # CONCEPTO / Concepto
# Relationship
header: Mapped["InvoiceHeader"] = relationship(back_populates="collections")
concept: Mapped[Optional[str]] = mapped_column(String(100)) # CONCEPTO / Conce
concept: Mapped[Optional[str]] = mapped_column(String(100)) # CONCEPTO / Conce

View File

@@ -2,253 +2,267 @@ from typing import Literal, Optional, List
from datetime import datetime, date
from decimal import Decimal
from pydantic import BaseModel, Field
from .models import DestinationOriginCove, OperationType, Currency, TransportType, WeightUnit
from .models import (
DestinationOriginCove,
OperationType,
Currency,
TransportType,
WeightUnit,
)
# --- Base Schemas ---
class InvoiceHeaderBase(BaseModel):
"""Base fields for Invoice Header"""
system: Optional[str] = Field(
None, max_length=12, description="System of origin")
system: Optional[str] = Field(None, max_length=12, description="System of origin")
operation_type: Optional[OperationType] = Field(
..., 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")
None, max_length=5, description="Invoice type key"
)
document_type: str = Field(
..., max_length=3, description="Document type (Regimen Aduanero)")
..., max_length=3, description="Document type (Regimen Aduanero)"
)
invoice_number: Optional[str] = Field(
None, max_length=100, description="Invoice number")
None, max_length=100, description="Invoice number"
)
project_number: Optional[str] = Field(
None, max_length=14, description="Project number")
None, max_length=14, description="Project number"
)
purchase_order: Optional[str] = Field(
None, max_length=50, description="Purchase order")
None, max_length=50, description="Purchase order"
)
related_doc_id: Optional[int] = Field(
None, description="Related document ID for rectifications")
None, description="Related document ID for rectifications"
)
alternate_invoice: Optional[str] = Field(
None, max_length=99, description="Alternate invoice")
None, max_length=99, description="Alternate invoice"
)
invoice_ref: Optional[str] = Field(
None, max_length=19, description="Invoice reference")
None, max_length=19, description="Invoice reference"
)
proforma_number: Optional[str] = Field(
None, max_length=20, description="Proforma number")
None, max_length=20, description="Proforma number"
)
invoice_date: date = Field(..., description="Invoice date")
emission_date: Optional[date] = Field(None, description="Emission date")
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")
capture_user: Optional[str] = Field(
None, max_length=20, description="Capture user")
who_updated: Optional[str] = Field(None, max_length=20, description="Who updated")
capture_user: Optional[str] = Field(None, max_length=20, description="Capture user")
traffic_light_status: Optional[str] = Field(
None, max_length=50, description="Traffic light status")
None, max_length=50, description="Traffic light status"
)
process_log: Optional[str] = Field(
None, max_length=300, description="Processing log")
None, max_length=300, description="Processing log"
)
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")
observation_en: Optional[str] = Field(
None, description="Observations in English")
comments_status: Optional[str] = Field(
None, description="Comments status")
None, max_length=2, description="Report status"
)
observation_es: Optional[str] = Field(None, description="Observations in Spanish")
observation_en: Optional[str] = Field(None, description="Observations in English")
comments_status: Optional[str] = Field(None, description="Comments status")
vu_observations: Optional[str] = Field(
None, max_length=500, description="VUCEM observations")
cfdi_uuid: Optional[str] = Field(
None, max_length=100, description="CFDI UUID")
None, max_length=500, description="VUCEM observations"
)
cfdi_uuid: Optional[str] = Field(None, max_length=100, description="CFDI UUID")
path_pdf: Optional[str] = Field(
None, max_length=500, description="Path to PDF file")
None, max_length=500, description="Path to PDF file"
)
path_xml: Optional[str] = Field(
None, max_length=500, description="Path to XML file")
subcompany: Optional[str] = Field(
None, max_length=5, description="Subcompany")
None, max_length=500, description="Path to XML file"
)
subcompany: Optional[str] = Field(None, max_length=5, description="Subcompany")
party_count: Optional[int] = Field(None, description="Quantity of parties")
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[bool] = Field(False, description="Apply manual discount")
None, max_length=12, description="Generate description of parties"
)
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")
download_class: Optional[bool] = Field(
None, description="Download class")
download_def: Optional[bool] = Field(
None, description="Definitive download")
download_substance: Optional[bool] = Field(None, description="Download substance")
download_class: Optional[bool] = Field(None, description="Download class")
download_def: Optional[bool] = Field(None, description="Definitive download")
payment_terms: Optional[str] = Field(
None, max_length=200, description="Payment terms")
None, max_length=200, description="Payment terms"
)
handling_fees: Optional[Decimal] = Field(None, description="Handling fees")
option_iv18: Optional[str] = Field(
None, max_length=50, description="Option IV18")
enajenation_goods: Optional[bool] = Field(
None, description="Enajenation of goods")
option_iv18: Optional[str] = Field(None, max_length=50, description="Option IV18")
enajenation_goods: Optional[bool] = Field(None, description="Enajenation of goods")
class InvoiceComplianceMxBase(BaseModel):
"""Base fields for Compliance MX"""
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)")
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")
port_of_entry: Optional[str] = Field(
None, max_length=6, description="Port of entry")
None, max_length=6, description="Port of entry"
)
destination: Optional[str] = Field(
None, max_length=3, description="Destination code")
None, max_length=3, description="Destination code"
)
manifest_number: Optional[str] = Field(
None, max_length=15, description="Manifest number")
provider_header: str = Field(
None, max_length=20, description="Provider header")
provider_id: int = Field(
None, description="Provider ID")
sold_to_header: str = Field(
None, max_length=20, description="Sold to header")
sold_to_id: int = Field(
None, description="Sold to ID")
shipped_to_header: str = Field(
None, max_length=20, description="Shipped to header")
shipped_to_id:int = Field(
None, description="Shipped to ID")
None, max_length=15, description="Manifest number"
)
provider_header: str = Field(None, max_length=20, description="Provider header")
provider_id: int = Field(None, description="Provider ID")
sold_to_header: str = Field(None, max_length=20, description="Sold to header")
sold_to_id: int = Field(None, description="Sold to ID")
shipped_to_header: str = Field(None, max_length=20, description="Shipped to header")
shipped_to_id: int = Field(None, description="Shipped to ID")
shipped_by_header: Optional[int] = Field(
None, max_length=20, description="Shipped by header")
shipped_by_id: Optional[int] = Field(
None, description="Shipped by ID")
customs_broker_id: int = Field(
None, description="Customs broker ID")
None, max_length=20, description="Shipped by header"
)
shipped_by_id: Optional[int] = Field(None, description="Shipped by ID")
customs_broker_id: int = Field(None, description="Customs broker ID")
customs_broker_us_id: Optional[int] = Field(
None, description="US customs broker ID")
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(
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")
None, max_length=20, description="Broker invoice number"
)
broker_invoice_date: Optional[date] = Field(None, description="Broker invoice date")
is_mixed: Optional[bool] = Field(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[bool] = Field(
False, 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")
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: 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")
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(
None, max_length=50, description="E-document")
None, description="Was reviewed by company"
)
edocument: Optional[str] = Field(None, max_length=50, description="E-document")
electronic_signature: Optional[str] = Field(
None, max_length=999, description="Electronic signature")
None, max_length=999, description="Electronic signature"
)
certificate_number: Optional[str] = Field(
None, max_length=99, description="Certificate number")
niu_number: Optional[str] = Field(
None, max_length=19, description="NIU number")
None, max_length=99, description="Certificate number"
)
niu_number: Optional[str] = Field(None, max_length=19, description="NIU number")
bill_of_lading_count: Optional[str] = Field(
None, max_length=12, description="Bill of lading count")
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[DestinationOriginCove] = Field('franja_front_norte', max_length=20, description="Origin/Destination COVE")
None, max_length=204, description="VUCEM addendum"
)
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(
None, description="Customs person line")
contingency_mode: Optional[bool] = Field(
None, description="Contingency mode")
enclosure: Optional[str] = Field(
None, max_length=4, description="Enclosure")
None, max_length=19, description="VUCEM operation number"
)
customs_person_line: Optional[int] = Field(None, description="Customs person line")
contingency_mode: Optional[bool] = Field(None, description="Contingency mode")
enclosure: Optional[str] = Field(None, max_length=4, description="Enclosure")
guide_type_to_identify: Optional[str] = Field(
None, max_length=1, description="Guide type to identify")
location: Optional[str] = Field(
None, max_length=200, description="Location")
dot_code: Optional[str] = Field(
None, max_length=20, description="DOT code")
subdivision: Optional[str] = Field(
None, max_length=20, description="Subdivision")
acts_as: Optional[str] = Field(
None, max_length=20, description="Acts as")
None, max_length=1, description="Guide type to identify"
)
location: Optional[str] = Field(None, max_length=200, description="Location")
dot_code: Optional[str] = Field(None, max_length=20, description="DOT code")
subdivision: Optional[str] = Field(None, max_length=20, description="Subdivision")
acts_as: Optional[str] = Field(None, max_length=20, description="Acts as")
movement_type: Optional[str] = Field(
None, max_length=31, description="Movement type")
None, max_length=31, description="Movement type"
)
office_document: Optional[str] = Field(
None, max_length=30, description="Office document")
None, max_length=30, description="Office document"
)
reason_export: Optional[str] = Field(
None, max_length=1, description="Reason for export")
None, max_length=1, description="Reason for export"
)
signature_key: Optional[str] = Field(
None, max_length=10, description="Signature key")
None, max_length=10, description="Signature key"
)
sem_id: Optional[int] = Field(None, description="SEM ID")
class InvoiceFinancialsBase(BaseModel):
"""Base fields for Financials"""
currency: Currency = Field(
None, max_length=7, description="Currency code")
currency_type: Optional[str] = Field(
"USD", description="Currency type")
currency: Currency = Field(None, max_length=7, description="Currency code")
currency_type: Optional[str] = Field("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")
None, description="Exchange rate currency to currency"
)
value_mn: Optional[Decimal] = Field(None, description="Value in MXN")
value_me: Optional[Decimal] = Field(
None, description="Value in foreign currency")
value_mc: Optional[Decimal] = Field(
None, description="Value in third currency")
value_me: Optional[Decimal] = Field(None, description="Value in foreign currency")
value_mc: Optional[Decimal] = Field(None, description="Value in third currency")
customs_value_mn: Optional[Decimal] = Field(
None, description="Customs value in MXN")
None, description="Customs value in MXN"
)
customs_value_me: Optional[Decimal] = Field(
None, description="Customs value in foreign currency")
None, description="Customs value in foreign currency"
)
raw_material_value_mn: Optional[Decimal] = Field(
None, description="Raw material value in MXN")
None, description="Raw material value in MXN"
)
raw_material_value_me: Optional[Decimal] = Field(
None, description="Raw material value in foreign currency")
None, description="Raw material value in foreign currency"
)
aggregate_value_mn: Optional[Decimal] = Field(
None, description="Aggregate value in MXN")
None, description="Aggregate value in MXN"
)
aggregate_value_me: Optional[Decimal] = Field(
None, description="Aggregate value in foreign currency")
None, description="Aggregate value in foreign currency"
)
aggregate_value_mc: Optional[Decimal] = Field(
None, description="Aggregate value in third currency")
None, description="Aggregate value in third currency"
)
mexican_value_mn: Optional[Decimal] = Field(
None, description="Mexican merchandise value in MXN")
None, description="Mexican merchandise value in MXN"
)
mexican_value_me: Optional[Decimal] = Field(
None, description="Mexican merchandise value in foreign currency")
None, description="Mexican merchandise value in foreign currency"
)
mexican_value_mc: Optional[Decimal] = Field(
None, description="Mexican merchandise value in third currency")
None, description="Mexican merchandise value in third currency"
)
national_packaging_mn: Optional[Decimal] = Field(
None, description="National packaging in MXN")
None, description="National packaging in MXN"
)
national_packaging_me: Optional[Decimal] = Field(
None, description="National packaging in foreign currency")
None, description="National packaging in foreign currency"
)
national_packaging_mc: Optional[Decimal] = Field(
None, description="National packaging in third currency")
None, description="National packaging in third currency"
)
freight: Optional[Decimal] = Field(None, description="Freight cost")
insurance: Optional[Decimal] = Field(None, description="Insurance cost")
insurance_value: Optional[Decimal] = Field(
None, description="Insurance value")
insurance_value: Optional[Decimal] = Field(None, description="Insurance value")
packaging: Optional[Decimal] = Field(None, description="Packaging")
other_increments: Optional[Decimal] = Field(
None, description="Other increments")
other_increments: Optional[Decimal] = Field(None, description="Other increments")
total_increments_mn: Optional[Decimal] = Field(
None, description="Total increments in MXN")
None, description="Total increments in MXN"
)
total_increments_me: Optional[Decimal] = Field(
None, description="Total increments in foreign currency")
None, description="Total increments in foreign currency"
)
iva_mn: Optional[Decimal] = Field(None, description="IVA in MXN")
iva_me: Optional[Decimal] = Field(
None, description="IVA in foreign currency")
iva_mc: Optional[Decimal] = Field(
None, description="IVA in third currency")
iva_me: Optional[Decimal] = Field(None, description="IVA in foreign currency")
iva_mc: Optional[Decimal] = Field(None, description="IVA in third currency")
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(
None, description="Seal value 2500")
total_quantity: Optional[Decimal] = Field(
None, description="Total quantity")
None, description="Tax value in foreign currency"
)
seal_value_2500: Optional[bool] = Field(None, description="Seal value 2500")
total_quantity: Optional[Decimal] = Field(None, description="Total quantity")
gross_weight: Optional[Decimal] = Field(None, description="Gross weight")
net_weight: Optional[Decimal] = Field(None, description="Net weight")
bundle_count: Optional[int] = Field(None, description="Bundle count")
@@ -257,131 +271,142 @@ class InvoiceFinancialsBase(BaseModel):
class InvoiceLogisticsBase(BaseModel):
"""Base fields for Logistics"""
carrier_id: Optional[str] = Field(
None, max_length=10, description="Carrier ID")
transport_id: Optional[str] = Field(
None, max_length=10, description="Transport ID")
carrier_id: Optional[str] = Field(None, max_length=10, description="Carrier ID")
transport_id: Optional[str] = Field(None, max_length=10, description="Transport ID")
transport_us_id: Optional[str] = Field(
None, max_length=10, description="US transport ID")
None, max_length=10, description="US transport ID"
)
transport_type: TransportType = Field(
'none', max_length=15, description="Transport type")
"none", max_length=15, description="Transport type"
)
transport_num: Optional[str] = Field(
None, max_length=20, description="Transport number")
None, max_length=20, description="Transport number"
)
transport_mode: Optional[str] = Field(
30, max_length=15, description="Transport mode")
driver_name: Optional[str] = Field(
None, max_length=80, description="Driver name")
is_rail: Optional[bool] = Field(
False, description="Is rail transport")
rail_id: Optional[str] = Field(
None, max_length=31, description="Rail ID")
30, max_length=15, description="Transport mode"
)
driver_name: Optional[str] = Field(None, max_length=80, description="Driver name")
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(
None, max_length=20, description="Vehicle number")
None, max_length=20, description="Vehicle number"
)
license_plate: Optional[str] = Field(
None, max_length=20, description="License plate")
None, max_length=20, description="License plate"
)
license_plate_complete: Optional[str] = Field(
None, max_length=40, description="Complete license plate")
None, max_length=40, description="Complete license plate"
)
trailer_num: Optional[str] = Field(
None, max_length=20, description="Trailer number")
seal_number: Optional[str] = Field(
None, max_length=15, description="Seal number")
guide_number: Optional[str] = Field(
None, max_length=20, description="Guide number")
bill_number: Optional[str] = Field(
None, max_length=15, description="Bill number")
None, max_length=20, description="Trailer number"
)
seal_number: Optional[str] = Field(None, max_length=15, description="Seal number")
guide_number: Optional[str] = Field(None, max_length=20, description="Guide number")
bill_number: Optional[str] = Field(None, max_length=15, description="Bill number")
reference_number: Optional[str] = Field(
None, max_length=14, description="Reference number")
None, max_length=14, description="Reference number"
)
shipment_number: Optional[str] = Field(
None, max_length=19, description="Shipment number")
incoterm: Optional[str] = Field(
None, max_length=5, description="Incoterm")
identifier_1: Optional[str] = Field(
None, max_length=2, description="Identifier 1")
complement_1: Optional[str] = Field(
None, max_length=30, description="Complement 1")
identifier_2: Optional[str] = Field(
None, max_length=2, description="Identifier 2")
complement_2: Optional[str] = Field(
None, max_length=30, description="Complement 2")
None, max_length=19, description="Shipment number"
)
incoterm: Optional[str] = Field(None, max_length=5, description="Incoterm")
identifier_1: Optional[str] = Field(None, max_length=2, description="Identifier 1")
complement_1: Optional[str] = Field(None, max_length=30, description="Complement 1")
identifier_2: Optional[str] = Field(None, max_length=2, description="Identifier 2")
complement_2: Optional[str] = Field(None, max_length=30, description="Complement 2")
weight_type: WeightUnit = Field(
default="kgs", max_length=3, description="Weight type")
default="kgs", max_length=3, description="Weight type"
)
container_types: Optional[str] = Field(
None, max_length=500, description="Container types")
None, max_length=500, description="Container types"
)
vehicle_data: Optional[str] = Field(
None, max_length=500, description="Vehicle data")
None, max_length=500, description="Vehicle data"
)
origin_location: Optional[str] = Field(
None, max_length=200, description="Origin location")
None, max_length=200, description="Origin location"
)
destination_location: Optional[str] = Field(
None, max_length=200, description="Destination location")
None, max_length=200, description="Destination location"
)
transport_itinerary: Optional[str] = Field(
None, max_length=1000, description="Transport itinerary")
None, max_length=1000, description="Transport itinerary"
)
destination_goods: Optional[str] = Field(
None, max_length=50, description="Destination of goods")
entry_exit_date: Optional[date] = Field(
None, description="Entry/Exit date")
delivery_date: Optional[date] = Field(
None, description="Delivery date")
delivered_status: Optional[str] = Field(
None, max_length=2, description="Delivered status")
received_by: Optional[str] = Field(
None, max_length=50, description="Received by")
payment_date: Optional[date] = Field(
None, description="Payment date")
None, max_length=50, description="Destination of goods"
)
entry_exit_date: Optional[date] = Field(None, description="Entry/Exit date")
delivery_date: Optional[date] = Field(None, description="Delivery date")
delivered_status: Optional[bool] = Field(False, description="Delivered status")
received_by: Optional[str] = Field(None, max_length=50, description="Received by")
payment_date: Optional[date] = Field(None, description="Payment date")
payment_receipt_num: Optional[str] = Field(
None, max_length=20, description="Payment receipt number")
is_ctm_process: Optional[bool] = Field(
False, description="Is CTM process")
None, max_length=20, description="Payment receipt number"
)
is_ctm_process: Optional[bool] = Field(False, description="Is CTM process")
class InvoiceSalesDetailsBase(BaseModel):
"""Base fields for Sales Details"""
line_number: int = Field(..., description="Line number")
sales_order: Optional[str] = Field(
None, max_length=20, description="Sales order")
sales_order: Optional[str] = Field(None, max_length=20, description="Sales order")
colors_description: Optional[str] = Field(
None, max_length=49, description="Colors description")
None, max_length=49, description="Colors description"
)
square_color_code: Optional[str] = Field(
None, max_length=1, description="Square color code")
None, max_length=1, description="Square color code"
)
line_bundles: Optional[int] = Field(None, description="Line bundles count")
class InvoiceCollectionsBase(BaseModel):
"""Base fields for Collections"""
line_number: int = Field(..., description="Line number")
invoice_number: Optional[str] = Field(
None, max_length=15, description="Invoice number")
None, max_length=15, description="Invoice number"
)
concept: Optional[str] = Field(None, max_length=100, description="Concept")
# --- Create Schemas ---
class InvoiceComplianceMxCreate(InvoiceComplianceMxBase):
"""Schema for creating Compliance MX"""
pass
class InvoiceFinancialsCreate(InvoiceFinancialsBase):
"""Schema for creating Financials"""
pass
class InvoiceLogisticsCreate(InvoiceLogisticsBase):
"""Schema for creating Logistics"""
pass
class InvoiceSalesDetailsCreate(InvoiceSalesDetailsBase):
"""Schema for creating Sales Details"""
pass
class InvoiceCollectionsCreate(InvoiceCollectionsBase):
"""Schema for creating Collections"""
pass
class InvoiceHeaderCreate(InvoiceHeaderBase):
"""Schema for creating Invoice Header with nested relations"""
compliance_mx: Optional[InvoiceComplianceMxCreate] = None
financials: Optional[InvoiceFinancialsCreate] = None
logistics: Optional[InvoiceLogisticsCreate] = None
@@ -391,33 +416,40 @@ class InvoiceHeaderCreate(InvoiceHeaderBase):
# --- Update Schemas ---
class InvoiceComplianceMxUpdate(InvoiceComplianceMxBase):
"""Schema for updating Compliance MX"""
pass
class InvoiceFinancialsUpdate(InvoiceFinancialsBase):
"""Schema for updating Financials"""
pass
class InvoiceLogisticsUpdate(InvoiceLogisticsBase):
"""Schema for updating Logistics"""
pass
class InvoiceSalesDetailsUpdate(InvoiceSalesDetailsBase):
"""Schema for updating Sales Details"""
line_number: Optional[int] = None
class InvoiceCollectionsUpdate(InvoiceCollectionsBase):
"""Schema for updating Collections"""
line_number: Optional[int] = None
class InvoiceHeaderUpdate(InvoiceHeaderBase):
"""Schema for updating Invoice Header with nested relations"""
id: int
compliance_mx: Optional[InvoiceComplianceMxUpdate] = None
financials: Optional[InvoiceFinancialsUpdate] = None
@@ -428,9 +460,25 @@ class InvoiceHeaderUpdate(InvoiceHeaderBase):
# --- Response Schemas ---
class PedimentoBasicInfo(BaseModel):
"""Basic Pedimento information for Invoice response"""
pedimento_number: Optional[str] = None
pedimento_code: Optional[str] = None
customs_office: Optional[str] = None
license: Optional[str] = None
year: Optional[str] = None
class Config:
from_attributes = True
class InvoiceComplianceMxResponse(InvoiceComplianceMxBase):
"""Schema for Compliance MX response"""
invoice_id: int
pedimento: Optional[PedimentoBasicInfo] = None
class Config:
from_attributes = True
@@ -438,6 +486,7 @@ class InvoiceComplianceMxResponse(InvoiceComplianceMxBase):
class InvoiceFinancialsResponse(InvoiceFinancialsBase):
"""Schema for Financials response"""
id: int
invoice_id: int
@@ -447,6 +496,7 @@ class InvoiceFinancialsResponse(InvoiceFinancialsBase):
class InvoiceLogisticsResponse(InvoiceLogisticsBase):
"""Schema for Logistics response"""
id: int
invoice_id: int
@@ -456,6 +506,7 @@ class InvoiceLogisticsResponse(InvoiceLogisticsBase):
class InvoiceSalesDetailsResponse(InvoiceSalesDetailsBase):
"""Schema for Sales Details response"""
id: int
invoice_id: int
@@ -465,6 +516,7 @@ class InvoiceSalesDetailsResponse(InvoiceSalesDetailsBase):
class InvoiceCollectionsResponse(InvoiceCollectionsBase):
"""Schema for Collections response"""
id: int
invoice_id: int
@@ -474,13 +526,14 @@ class InvoiceCollectionsResponse(InvoiceCollectionsBase):
class InvoiceHeaderResponse(InvoiceHeaderBase):
"""Schema for Invoice Header response with nested relations"""
id: int
capture_date: datetime
compliance_mx: Optional[InvoiceComplianceMxResponse] = None
financials: Optional[InvoiceFinancialsResponse] = None
logistics: Optional[InvoiceLogisticsResponse] = []
details: Optional[InvoiceSalesDetailsResponse] = []
collections: Optional[InvoiceCollectionsResponse] = []
logistics: Optional[InvoiceLogisticsResponse] = None
details: Optional[List[InvoiceSalesDetailsResponse]] = []
collections: Optional[List[InvoiceCollectionsResponse]] = []
class Config:
from_attributes = True

View File

@@ -90,19 +90,20 @@ class InvoiceService:
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)
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
# Si hay errores, lanzar excepción ANTES de intentar crear
errors.raise_if_errors("Error al crear la factura")
# Extract nested data
compliance_data = invoice_data.compliance_mx
financials_data = invoice_data.financials
logistics_data = invoice_data.logistics
details_data = invoice_data.details or []
collections_data = invoice_data.collections or []
try:
# Extract nested data
compliance_data = invoice_data.compliance_mx
financials_data = invoice_data.financials
logistics_data = invoice_data.logistics or []
details_data = invoice_data.details or []
collections_data = invoice_data.collections or []
# Create main invoice header
raw_invoice_dict = invoice_data.model_dump(
@@ -148,8 +149,8 @@ class InvoiceService:
db.add(new_financials)
# Create logistics entries
for logistics_item in logistics_data:
raw_log_dict = logistics_item.model_dump()
if logistics_data:
raw_log_dict = logistics_data.model_dump()
logistics_dict = clean_dict(raw_log_dict)
logistics_dict["invoice_id"] = new_invoice.id

View File

@@ -12,7 +12,7 @@ class LineCustom(Base):
Customs details for line items
Consolidates all line-level data from Q and S tables
"""
__tablename__ = "line_customs"
__tablename__ = "item_line_customs"
__table_args__ = {
"schema": "a76",
}

View File

@@ -11,7 +11,7 @@ class LineDescription(Base):
Description details for line items
Consolidates all line-level data from Q and S tables
"""
__tablename__ = "line_descriptions"
__tablename__ = "item_line_descriptions"
__table_args__ = {
"schema": "a76",
}

View File

@@ -12,7 +12,7 @@ class LineFinancial(Base):
Financial details for line items
Consolidates all line-level data from Q and S tables
"""
__tablename__ = "line_financials"
__tablename__ = "item_line_financials"
__table_args__ = {
"schema": "a76",
}

View File

@@ -12,6 +12,11 @@ if TYPE_CHECKING:
from ..line_customs.models import LineCustom
from ..line_descriptions.models import LineDescription
from ..line_references.models import LineReference
from api.v1.modules.a76.classes.models import Class
from api.v1.modules.a76.general_catalogs.units_of_measure.models import (
UnitOfMeasure,
)
from api.v1.modules.a24.fa.fa_item_lines.models import FaLineItem
class LineItem(Base, TenantScopedMixin, TimestampMixin):
@@ -19,6 +24,7 @@ class LineItem(Base, TenantScopedMixin, TimestampMixin):
Unified line items for all items
Consolidates all line-level data from Q and S tables
"""
__tablename__ = "item_lines"
__table_args__ = {
"schema": "a76",
@@ -26,154 +32,152 @@ class LineItem(Base, TenantScopedMixin, TimestampMixin):
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
item_id: Mapped[int] = mapped_column(ForeignKey("a76.items.id"))
line_number: Mapped[int] = mapped_column(
Integer) # LINEAIMPO/LINEAEXPO/LINEA
line_number: Mapped[int] = mapped_column(Integer) # LINEAIMPO/LINEAEXPO/LINEA
# Part identification
part_number: Mapped[Optional[str]] = mapped_column(
ForeignKey("a76.parts.id")) # NUMPARTE
component_part_number: Mapped[Optional[str]] = mapped_column(
ForeignKey("a76.parts.id")) # NUMPARTECOM
class_code: Mapped[Optional[str]] = mapped_column(
ForeignKey("a76.classes.id")) # CLASE
part_number_id: Mapped[Optional[str]] = mapped_column(
ForeignKey("a76.parts.id")
) # NUMPARTE
component_part_number_id: Mapped[Optional[str]] = mapped_column(
ForeignKey("a76.parts.id")
) # NUMPARTECOM
class_id: Mapped[Optional[str]] = mapped_column(
ForeignKey("a76.classes.id")
) # CLASE
# Unit of measure
unit_of_measure: Mapped[Optional[str]] = mapped_column(
ForeignKey("a76.units_of_measure_general.id")) # UNIDADMEDIDA/UNIMED
ForeignKey("a76.units_of_measure.id")
) # UNIDADMEDIDA/UNIMED
alternate_unit: Mapped[Optional[str]] = mapped_column(
ForeignKey("a76.units_of_measure_general.id")) # UNIMEDALTERNA
ForeignKey("a76.units_of_measure.id")
) # UNIMEDALTERNA
uma_key: Mapped[Optional[str]] = mapped_column(String(2)) # CLAVEUMA
auxiliary_unit: Mapped[Optional[str]] = mapped_column(
String(5)) # UNIMEDAUXILIAR
auxiliary_unit: Mapped[Optional[str]] = mapped_column(String(5)) # UNIMEDAUXILIAR
# Permits and certificates
permit_number: Mapped[Optional[str]] = mapped_column(
String(20)) # NUMPERMISO
permit_number: Mapped[Optional[str]] = mapped_column(String(20)) # NUMPERMISO
page_line: Mapped[Optional[str]] = mapped_column(String(10)) # PAGRENGLON
has_certificate: Mapped[Optional[bool]] = mapped_column(
Boolean) # TIENECO/CERTORIGEN
Boolean
) # TIENECO/CERTORIGEN
certificate_number: Mapped[Optional[str]] = mapped_column(
String(10)) # NOCERTIFICADO
octave_permit: Mapped[Optional[str]] = mapped_column(
String(20)) # PERMISOROCTAVA
permits_ped: Mapped[Optional[str]] = mapped_column(
String(500)) # PERMISOSPED
String(10)
) # NOCERTIFICADO
octave_permit: Mapped[Optional[str]] = mapped_column(String(20)) # PERMISOROCTAVA
permits_ped: Mapped[Optional[str]] = mapped_column(String(500)) # PERMISOSPED
# FDA
has_fda_code: Mapped[Optional[bool]] = mapped_column(
Boolean) # LLEVACODFDA
fda_key: Mapped[Optional[str]] = mapped_column(String(10)) # CLAVEFDA
# Subitem flags
is_subitem: Mapped[Optional[bool]] = mapped_column(Boolean) # ESSUBPARTIDA
contains_subitems: Mapped[Optional[bool]
] = mapped_column(Boolean) # CONTIENESUBP
includes_subitems: Mapped[Optional[bool]] = mapped_column(
Boolean) # INCUYESUBPARTIDAS
subitem_number: Mapped[Optional[bool]
] = mapped_column(Boolean) # SUBPARTIDA
has_fda_code: Mapped[Optional[bool]] = mapped_column(Boolean) # LLEVACODFDA
fda_key: Mapped[Optional[str]] = mapped_column(String(10)) # CLAVEFDA
# Special flags
is_military_mcia: Mapped[Optional[bool]
] = mapped_column(Boolean) # ESMCIAMILITAR
is_military_mcia: Mapped[Optional[bool]] = mapped_column(Boolean) # ESMCIAMILITAR
# IV32 (Tax identification)
iv32_type_key: Mapped[Optional[str]] = mapped_column(
String(5)) # CLAVETIPOIV32
iv32_number: Mapped[Optional[str]] = mapped_column(
String(35)) # NUMEROIV32
iv32_type_key: Mapped[Optional[str]] = mapped_column(String(5)) # CLAVETIPOIV32
iv32_number: Mapped[Optional[str]] = mapped_column(String(35)) # NUMEROIV32
# IN CASE OF EXPO
scrap_invoice: Mapped[Optional[str]] = mapped_column(
String(15)) # FACTURASCRAP
consecutive_destination: Mapped[Optional[int]
] = mapped_column(Integer) # CONSECUTIVODES
ctm_section: Mapped[Optional[str]] = mapped_column(
String(3)) # APARTADOCTM
scrap_invoice: Mapped[Optional[str]] = mapped_column(String(15)) # FACTURASCRAP
consecutive_destination: Mapped[Optional[int]] = mapped_column(
Integer
) # CONSECUTIVODES
ctm_section: Mapped[Optional[str]] = mapped_column(String(3)) # APARTADOCTM
# Tax payment
tax_payment: Mapped[Optional[bool]] = mapped_column(
Boolean) # PAGOIMPUESTO
tax_payment: Mapped[Optional[bool]] = mapped_column(Boolean) # PAGOIMPUESTO
payment_method: Mapped[Optional[str]] = mapped_column(
String(9)) # FORMAPAGO/FORMAPAGOTIGI
igi_amount: Mapped[Optional[Decimal]] = mapped_column(
Numeric(23, 8)) # MONTOIGI
String(9)
) # FORMAPAGO/FORMAPAGOTIGI
igi_amount: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # MONTOIGI
igi_payment_method: Mapped[Optional[str]] = mapped_column(
String(9)) # FORMAPAGOTIGI
String(9)
) # FORMAPAGOTIGI
# FCC
fcc_key: Mapped[Optional[str]] = mapped_column(String(30)) # CLAVEFCC
# Valuation method
valuation_method: Mapped[Optional[str]
] = mapped_column(String(2)) # METVALOR
valuation_method: Mapped[Optional[str]] = mapped_column(String(2)) # METVALOR
valuation_determined_value: Mapped[Optional[Decimal]] = mapped_column(
Numeric(29, 8)) # METVALORVALORDETERMINADO/METVALORACIONVALORDETERMINADO
Numeric(29, 8)
) # METVALORVALORDETERMINADO/METVALORACIONVALORDETERMINADO
valuation_reason: Mapped[Optional[str]] = mapped_column(
String(500)) # METVALORMOTIVODEUSO/METVALORACIONMOTIVODEUSO
String(500)
) # METVALORMOTIVODEUSO/METVALORACIONMOTIVODEUSO
# Container rules
container_rule: Mapped[Optional[str]] = mapped_column(
String(50)) # CONTENEDORREGLA
container_rule: Mapped[Optional[str]] = mapped_column(String(50)) # CONTENEDORREGLA
container_parts_ii: Mapped[Optional[str]] = mapped_column(
String(50)) # CONTENEDORPARTESII
String(50)
) # CONTENEDORPARTESII
# APHIS
consecutive_aphis: Mapped[Optional[int]] = mapped_column(
Integer) # CONSECUTIVOAPHIS
Integer
) # CONSECUTIVOAPHIS
# BOM/Commercial
bom_version: Mapped[Optional[int]] = mapped_column(Integer) # VERSIONBOM
bill_version: Mapped[Optional[int]] = mapped_column(Integer) # VERSIONBILL
# TLCAN value
tlcan_value: Mapped[Optional[Decimal]] = mapped_column(
Numeric(23, 8)) # VALORTLCAN
tlcan_value: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORTLCAN
# Identifier
identifier: Mapped[Optional[str]] = mapped_column(
String(2)) # IDENTIFICADOR
identifier: Mapped[Optional[str]] = mapped_column(String(2)) # IDENTIFICADOR
# Validation fields
validation_zero: Mapped[Optional[int]] = mapped_column(
Integer) # VALIDACIONZERO
validation_one: Mapped[Optional[int]] = mapped_column(
Integer) # VALIDACIONUNO
validation_zero: Mapped[Optional[int]] = mapped_column(Integer) # VALIDACIONZERO
validation_one: Mapped[Optional[int]] = mapped_column(Integer) # VALIDACIONUNO
# Material type
material_type: Mapped[Optional[str]] = mapped_column(
String(50)) # TIPOMAT/TIPODENUMPARTE
String(50)
) # TIPOMAT/TIPODENUMPARTE
# Order concept
order_type: Mapped[Optional[str]] = mapped_column(
String(50)) # TIPODEORDEN
order_type: Mapped[Optional[str]] = mapped_column(String(50)) # TIPODEORDEN
line_concept: Mapped[Optional[str]] = mapped_column(
String(50)) # CONCEPTODELAPARTIDA
String(50)
) # CONCEPTODELAPARTIDA
# Review dispatch
review_dispatch: Mapped[Optional[str]] = mapped_column(
String(10)) # REVISARDESP
review_dispatch: Mapped[Optional[str]] = mapped_column(String(10)) # REVISARDESP
# Take component from PT
take_component_pt: Mapped[Optional[int]
] = mapped_column(Integer) # TOMARCOMOPT
take_component_pt: Mapped[Optional[int]] = mapped_column(Integer) # TOMARCOMOPT
# Pallet
pallet2: Mapped[Optional[int]] = mapped_column(SmallInteger) # PALLET2
# Wildcard field
wildcard_field: Mapped[Optional[str]] = mapped_column(
String(100)) # CAMPOCOMODIN
wildcard_field: Mapped[Optional[str]] = mapped_column(String(100)) # CAMPOCOMODIN
# Relationships
item: Mapped["Item"] = relationship(back_populates="lines")
financial: Mapped[Optional["LineFinancial"]] = relationship(
back_populates="line", cascade="all, delete-orphan", uselist=False)
back_populates="line", cascade="all, delete-orphan", uselist=False
)
quantity: Mapped[Optional["LineQuantity"]] = relationship(
back_populates="line", cascade="all, delete-orphan", uselist=False)
back_populates="line", cascade="all, delete-orphan", uselist=False
)
customs: Mapped[Optional["LineCustom"]] = relationship(
back_populates="line", cascade="all, delete-orphan", uselist=False)
back_populates="line", cascade="all, delete-orphan", uselist=False
)
description: Mapped[Optional["LineDescription"]] = relationship(
back_populates="line", cascade="all, delete-orphan", uselist=False)
back_populates="line", cascade="all, delete-orphan", uselist=False
)
reference: Mapped[Optional["LineReference"]] = relationship(
back_populates="line", cascade="all, delete-orphan", uselist=False)
back_populates="line", cascade="all, delete-orphan", uselist=False
)
class_info: Mapped[Optional["Class"]] = relationship(
foreign_keys=[class_id], viewonly=True
)
unit_of_measure_info: Mapped[Optional["UnitOfMeasure"]] = relationship(
foreign_keys=[unit_of_measure], viewonly=True
)
fa_data: Mapped[Optional["FaLineItem"]] = relationship(
"FaLineItem", back_populates="master_info", uselist=False
)

View File

@@ -1,6 +1,6 @@
from decimal import Decimal
from typing import Optional
from pydantic import BaseModel, Field, ConfigDict, field_validator
from typing import Optional, Any
from pydantic import BaseModel, Field, ConfigDict, field_validator, model_validator
# Import nested schemas
from ..line_customs.schemas import (
@@ -29,6 +29,13 @@ from ..line_references.schemas import (
LineReferenceResponse,
)
from api.v1.modules.a24.fa.fa_item_lines.dto import (
FaLineItemCreateDTO,
FaLineItemUpdateDTO,
FaLineItemResponseDTO,
)
# ============================================================================
# LINE ITEM SCHEMAS
# ============================================================================
@@ -40,16 +47,13 @@ class LineItemBase(BaseModel):
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"
part_number_id: Optional[int] = Field(None, description="Part number")
component_part_number_id: Optional[int] = Field(
None, description="Component part number"
)
class_code: Optional[str] = Field(None, max_length=20, description="Class code")
class_id: Optional[int] = Field(None, description="Class code")
@field_validator(
"class_code",
"part_number",
"component_part_number",
"unit_of_measure",
"alternate_unit",
mode="before",
@@ -91,12 +95,6 @@ class LineItemBase(BaseModel):
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"
@@ -210,6 +208,9 @@ class LineItemCreate(LineItemBase):
reference: Optional[LineReferenceCreate] = Field(
None, description="Reference data for this line"
)
fa_data: Optional[FaLineItemCreateDTO] = Field(
None, description="Fixed Asset data for this line"
)
class LineItemUpdate(LineItemBase):
@@ -231,6 +232,9 @@ class LineItemUpdate(LineItemBase):
reference: Optional[LineReferenceUpdate] = Field(
None, description="Reference data for this line"
)
fa_data: Optional[FaLineItemUpdateDTO] = Field(
None, description="Fixed Asset data for this line"
)
class LineItemResponse(LineItemBase):
@@ -243,5 +247,38 @@ class LineItemResponse(LineItemBase):
customs: Optional[LineCustomResponse] = None
description: Optional[LineDescriptionResponse] = None
reference: Optional[LineReferenceResponse] = None
fa_data: Optional[FaLineItemResponseDTO] = None
# Fields populated from relationships
class_code: Optional[str] = None
class_description: Optional[str] = None
unit_of_measure_code: Optional[str] = None
model_config = ConfigDict(from_attributes=True)
@model_validator(mode="before")
@classmethod
def extract_relationship_info(cls, data: Any) -> Any:
"""Extract class_code, class_description and unit_of_measure_code from relationships"""
if isinstance(data, dict):
return data
# It's an ORM object
result = {}
for key in cls.model_fields.keys():
if hasattr(data, key):
result[key] = getattr(data, key)
# Extract class info
if hasattr(data, "class_info") and data.class_info is not None:
result["class_code"] = data.class_info.class_code
result["class_description"] = data.class_info.description_es
# Extract unit of measure code
if (
hasattr(data, "unit_of_measure_info")
and data.unit_of_measure_info is not None
):
result["unit_of_measure_code"] = data.unit_of_measure_info.code
return result

View File

@@ -12,7 +12,7 @@ class LineQuantity(Base):
Quantity details for line items
Consolidates all line-level data from Q and S tables
"""
__tablename__ = "line_quantities"
__tablename__ = "item_line_quantities"
__table_args__ = {
"schema": "a76",
}

View File

@@ -11,7 +11,7 @@ class LineReference(Base):
Reference details for line items
Consolidates all line-level data from Q and S tables
"""
__tablename__ = "line_references"
__tablename__ = "item_line_references"
__table_args__ = {
"schema": "a76",
}

View File

@@ -6,6 +6,7 @@ Item -> LineItem -> LineFinancial
-> LineCustoms
-> LineDescription
-> LineReference
-> FaLineItem (Fixed Assets - a24)
"""
import logging
@@ -24,6 +25,7 @@ from .line_quantities.models import LineQuantity
from .line_customs.models import LineCustom
from .line_descriptions.models import LineDescription
from .line_references.models import LineReference
from api.v1.modules.a24.fa.fa_item_lines.models import FaLineItem
from .models import Item
logger = logging.getLogger(__name__)
@@ -47,6 +49,9 @@ class ItemService:
joinedload(Item.lines).joinedload(LineItem.customs),
joinedload(Item.lines).joinedload(LineItem.description),
joinedload(Item.lines).joinedload(LineItem.reference),
joinedload(Item.lines).joinedload(LineItem.class_info),
joinedload(Item.lines).joinedload(LineItem.unit_of_measure_info),
joinedload(Item.lines).joinedload(LineItem.fa_data),
)
.filter(
Item.id == item_id,
@@ -74,6 +79,9 @@ class ItemService:
joinedload(Item.lines).joinedload(LineItem.customs),
joinedload(Item.lines).joinedload(LineItem.description),
joinedload(Item.lines).joinedload(LineItem.reference),
joinedload(Item.lines).joinedload(LineItem.class_info),
joinedload(Item.lines).joinedload(LineItem.unit_of_measure_info),
joinedload(Item.lines).joinedload(LineItem.fa_data),
)
.filter(
Item.tenant_id == tenant_id,
@@ -122,6 +130,7 @@ class ItemService:
joinedload(Item.lines).joinedload(LineItem.customs),
joinedload(Item.lines).joinedload(LineItem.description),
joinedload(Item.lines).joinedload(LineItem.reference),
joinedload(Item.lines).joinedload(LineItem.fa_data),
)
.filter(
Item.invoice_id == invoice_id,
@@ -164,6 +173,7 @@ class ItemService:
customs_data = line_data.customs
description_data = line_data.description
reference_data = line_data.reference
fa_data = line_data.fa_data
line_dict = line_data.model_dump(
exclude={
@@ -172,6 +182,7 @@ class ItemService:
"customs",
"description",
"reference",
"fa_data",
}
)
line_dict["item_id"] = db_item.id
@@ -210,7 +221,7 @@ class ItemService:
description_dict["item_line_id"] = db_line.id
db_description = LineDescription(**description_dict)
db.add(db_description)
# Create reference data if provided
if reference_data:
reference_dict = reference_data.model_dump()
@@ -218,9 +229,17 @@ class ItemService:
db_reference = LineReference(**reference_dict)
db.add(db_reference)
# Create FA data if provided
if fa_data:
fa_dict = fa_data.model_dump()
fa_dict["id"] = db_line.id # FA table uses same ID as line item
fa_dict["tenant_id"] = tenant_id
fa_dict["company_id"] = company_id
db_fa = FaLineItem(**fa_dict)
db.add(db_fa)
db.commit()
db.refresh(db_item)
return db_item
except IntegrityError as e:
@@ -273,6 +292,7 @@ class ItemService:
customs_data = line_data.customs
description_data = line_data.description
reference_data = line_data.reference
fa_data = line_data.fa_data
line_dict = line_data.model_dump(
exclude={
@@ -281,6 +301,7 @@ class ItemService:
"customs",
"description",
"reference",
"fa_data",
},
exclude_unset=True,
)
@@ -320,6 +341,14 @@ class ItemService:
reference_dict["item_line_id"] = db_line.id
db.add(LineReference(**reference_dict))
# Create FA data if provided
if fa_data is not None:
fa_dict = fa_data.model_dump(exclude_unset=True)
fa_dict["id"] = db_line.id # FA table uses same ID as line item
fa_dict["tenant_id"] = tenant_id
fa_dict["company_id"] = company_id
db.add(FaLineItem(**fa_dict))
db.commit()
db.refresh(db_item)
return db_item