diff --git a/backend/api/v1/modules/a24/fa/fa_item_lines/__init__.py b/backend/api/v1/modules/a24/fa/fa_item_lines/__init__.py new file mode 100644 index 00000000..73edc52d --- /dev/null +++ b/backend/api/v1/modules/a24/fa/fa_item_lines/__init__.py @@ -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", +] diff --git a/backend/api/v1/modules/a24/fa/fa_item_lines/dto.py b/backend/api/v1/modules/a24/fa/fa_item_lines/dto.py new file mode 100644 index 00000000..191c38b0 --- /dev/null +++ b/backend/api/v1/modules/a24/fa/fa_item_lines/dto.py @@ -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 diff --git a/backend/api/v1/modules/a24/fa/fa_item_lines/models.py b/backend/api/v1/modules/a24/fa/fa_item_lines/models.py index 5aa2d038..91e9e958 100644 --- a/backend/api/v1/modules/a24/fa/fa_item_lines/models.py +++ b/backend/api/v1/modules/a24/fa/fa_item_lines/models.py @@ -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 \ No newline at end of file + 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"" diff --git a/backend/api/v1/modules/a24/fa/fa_item_lines/routes.py b/backend/api/v1/modules/a24/fa/fa_item_lines/routes.py new file mode 100644 index 00000000..eed5e34d --- /dev/null +++ b/backend/api/v1/modules/a24/fa/fa_item_lines/routes.py @@ -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 diff --git a/backend/api/v1/modules/a24/fa/fa_item_lines/service.py b/backend/api/v1/modules/a24/fa/fa_item_lines/service.py new file mode 100644 index 00000000..f60feec0 --- /dev/null +++ b/backend/api/v1/modules/a24/fa/fa_item_lines/service.py @@ -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" + ) diff --git a/backend/api/v1/modules/a24/router.py b/backend/api/v1/modules/a24/router.py index 237f172e..c6657bac 100644 --- a/backend/api/v1/modules/a24/router.py +++ b/backend/api/v1/modules/a24/router.py @@ -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"] +) diff --git a/backend/api/v1/modules/a76/general_catalogs/units_of_measure/models.py b/backend/api/v1/modules/a76/general_catalogs/units_of_measure/models.py index 00fd42ac..a79cff4b 100644 --- a/backend/api/v1/modules/a76/general_catalogs/units_of_measure/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/units_of_measure/models.py @@ -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" + ) diff --git a/backend/api/v1/modules/a76/invoices/imports/temporary/validators/common.py b/backend/api/v1/modules/a76/invoices/imports/temporary/validators/common.py index 14420299..0440bf81 100644 --- a/backend/api/v1/modules/a76/invoices/imports/temporary/validators/common.py +++ b/backend/api/v1/modules/a76/invoices/imports/temporary/validators/common.py @@ -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", diff --git a/backend/api/v1/modules/a76/invoices/models.py b/backend/api/v1/modules/a76/invoices/models.py index 3bb23271..1bc8f8a0 100644 --- a/backend/api/v1/modules/a76/invoices/models.py +++ b/backend/api/v1/modules/a76/invoices/models.py @@ -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 \ No newline at end of file + concept: Mapped[Optional[str]] = mapped_column(String(100)) # CONCEPTO / Conce diff --git a/backend/api/v1/modules/a76/invoices/schemas.py b/backend/api/v1/modules/a76/invoices/schemas.py index 964d4e16..444f7a61 100644 --- a/backend/api/v1/modules/a76/invoices/schemas.py +++ b/backend/api/v1/modules/a76/invoices/schemas.py @@ -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 diff --git a/backend/api/v1/modules/a76/invoices/services.py b/backend/api/v1/modules/a76/invoices/services.py index 07a5fab9..81a37d23 100644 --- a/backend/api/v1/modules/a76/invoices/services.py +++ b/backend/api/v1/modules/a76/invoices/services.py @@ -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 diff --git a/backend/api/v1/modules/a76/items/line_customs/models.py b/backend/api/v1/modules/a76/items/line_customs/models.py index 1c40448f..f3c99acc 100644 --- a/backend/api/v1/modules/a76/items/line_customs/models.py +++ b/backend/api/v1/modules/a76/items/line_customs/models.py @@ -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", } diff --git a/backend/api/v1/modules/a76/items/line_descriptions/models.py b/backend/api/v1/modules/a76/items/line_descriptions/models.py index dd78b780..beeb9392 100644 --- a/backend/api/v1/modules/a76/items/line_descriptions/models.py +++ b/backend/api/v1/modules/a76/items/line_descriptions/models.py @@ -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", } diff --git a/backend/api/v1/modules/a76/items/line_financials/models.py b/backend/api/v1/modules/a76/items/line_financials/models.py index 04b3002d..eb24934f 100644 --- a/backend/api/v1/modules/a76/items/line_financials/models.py +++ b/backend/api/v1/modules/a76/items/line_financials/models.py @@ -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", } diff --git a/backend/api/v1/modules/a76/items/line_items/models.py b/backend/api/v1/modules/a76/items/line_items/models.py index bb78348a..ad85b357 100644 --- a/backend/api/v1/modules/a76/items/line_items/models.py +++ b/backend/api/v1/modules/a76/items/line_items/models.py @@ -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 + ) diff --git a/backend/api/v1/modules/a76/items/line_items/schemas.py b/backend/api/v1/modules/a76/items/line_items/schemas.py index 4921233f..1bf42d4e 100644 --- a/backend/api/v1/modules/a76/items/line_items/schemas.py +++ b/backend/api/v1/modules/a76/items/line_items/schemas.py @@ -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 diff --git a/backend/api/v1/modules/a76/items/line_quantities/models.py b/backend/api/v1/modules/a76/items/line_quantities/models.py index 1fa41c16..c7a60558 100644 --- a/backend/api/v1/modules/a76/items/line_quantities/models.py +++ b/backend/api/v1/modules/a76/items/line_quantities/models.py @@ -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", } diff --git a/backend/api/v1/modules/a76/items/line_references/models.py b/backend/api/v1/modules/a76/items/line_references/models.py index 11e2459c..1402795c 100644 --- a/backend/api/v1/modules/a76/items/line_references/models.py +++ b/backend/api/v1/modules/a76/items/line_references/models.py @@ -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", } diff --git a/backend/api/v1/modules/a76/items/service.py b/backend/api/v1/modules/a76/items/service.py index 12bf0dbf..a1ad17af 100644 --- a/backend/api/v1/modules/a76/items/service.py +++ b/backend/api/v1/modules/a76/items/service.py @@ -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 diff --git a/frontend/src/lib/api/dashboard/a76/invoices.ts b/frontend/src/lib/api/dashboard/a76/invoices.ts index 4ae71bfb..a8ec14de 100644 --- a/frontend/src/lib/api/dashboard/a76/invoices.ts +++ b/frontend/src/lib/api/dashboard/a76/invoices.ts @@ -15,6 +15,14 @@ export interface InvoiceComplianceMx { pedimento_r1?: number | null; pedimento_k1?: number | null; remesa?: number | null; + // Pedimento relationship data (when joined) + pedimento?: { + pedimento_number?: string; + pedimento_code?: string; + customs_office?: string; + license?: string; + year?: string; + } | null; aduana?: string | null; port_of_entry?: string | null; destination?: string | null; @@ -220,7 +228,7 @@ export interface Invoice { enajenation_goods?: boolean | null; compliance_mx?: InvoiceComplianceMx | null; financials?: InvoiceFinancials | null; - logistics?: InvoiceLogistics[]; + logistics?: InvoiceLogistics; details?: InvoiceSalesDetails[]; collections?: InvoiceCollections[]; } @@ -282,6 +290,7 @@ export interface CreateInvoiceData { } export interface UpdateInvoiceData { + id: number; // Required by backend operation_type?: OperationType | null; invoice_type?: string | null; document_type?: string | null; diff --git a/frontend/src/lib/api/dashboard/a76/items.ts b/frontend/src/lib/api/dashboard/a76/items.ts index 3fe23462..abfbf846 100644 --- a/frontend/src/lib/api/dashboard/a76/items.ts +++ b/frontend/src/lib/api/dashboard/a76/items.ts @@ -76,15 +76,52 @@ export interface LineReferences { serie_id?: number; } +export interface FaLineItem { + id?: number; + tenant_id?: number; + company_id?: number; + + // Asset information (SCAF specific) + asset_number?: string; + asset_photo?: string; + equipment_message?: string; + invoice_type_asset?: string; + return_import_invoice?: string; + return_import_date?: number; + movement_type_import?: string; + + // Cross-references for import repair + search_invoice?: string; + search_line?: number; + + // Search type + search_type?: string; + + // Subitems + is_subitem?: boolean; + contains_subitems?: boolean; + includes_subitems?: boolean; + subitem_number?: number; + + // Special flags + download?: boolean; + own_equipment?: boolean; + omit_annex31?: boolean; + + // Timestamps + created_at?: string; + updated_at?: string; +} + export interface LineItem { id?: number; item_id?: number; line_number: number; // Identification - part_number?: string; - component_part_number?: string; - class_code?: string; + part_number_id?: string; + component_part_number_id?: string; + class_id?: number; identifier?: string; // Unit of Measure @@ -106,6 +143,12 @@ export interface LineItem { payment_method?: string; igi_amount?: number; + // Computed fields from class_info relation + class_code?: string; + class_description?: string; + + // Computed field from unit_of_measure_info relation + unit_of_measure_code?: string; // Nested relations (Singular names to match backend Pydantic models) customs?: LineCustoms; @@ -113,6 +156,7 @@ export interface LineItem { quantity?: LineQuantities; description?: LineDescriptions; reference?: LineReferences; + fa_data?: FaLineItem; // Fixed Asset specific data } export interface Item { diff --git a/frontend/src/lib/components/dashboard/goods/parts/partForm.svelte b/frontend/src/lib/components/dashboard/goods/parts/partForm.svelte index 7ca3bc80..db5d7d67 100644 --- a/frontend/src/lib/components/dashboard/goods/parts/partForm.svelte +++ b/frontend/src/lib/components/dashboard/goods/parts/partForm.svelte @@ -308,7 +308,7 @@ {formData.currency_key} Extranjera (USD) - Nacional (MXP) + Nacional (MXN) Euros (EUR) @@ -518,7 +518,7 @@ {formData.currency_key} Dólares (USD) - Pesos (MXP) + Pesos (MXN) Euros (EUR) @@ -554,7 +554,7 @@
- +
diff --git a/frontend/src/lib/components/dashboard/invoices/columns.ts b/frontend/src/lib/components/dashboard/invoices/columns.ts index c087fae8..821ff1cc 100644 --- a/frontend/src/lib/components/dashboard/invoices/columns.ts +++ b/frontend/src/lib/components/dashboard/invoices/columns.ts @@ -4,32 +4,6 @@ import { createRawSnippet } from "svelte"; import DataTableActions from "./data-table-actions.svelte"; import type { Invoice } from '$lib/api/dashboard/a76/invoices'; -/** - * Formatea un número como moneda MXN - */ -function formatCurrencyMXN(value?: number | null): string { - if (value === null || value === undefined) return '-'; - return new Intl.NumberFormat('es-MX', { - style: 'currency', - currency: 'MXN', - minimumFractionDigits: 2, - maximumFractionDigits: 2 - }).format(value); -} - -/** - * Formatea un número como moneda USD - */ -function formatCurrencyUSD(value?: number | null): string { - if (value === null || value === undefined) return '-'; - return new Intl.NumberFormat('es-MX', { - style: 'currency', - currency: 'USD', - minimumFractionDigits: 2, - maximumFractionDigits: 2 - }).format(value); -} - /** * Formatea una fecha */ @@ -42,54 +16,19 @@ function formatDate(date?: string | null): string { }); } -/** - * Obtiene el color del badge según el tipo de operación - */ -function getOperationTypeColor(type?: string | null): string { - if (!type) return 'bg-gray-100 text-gray-800'; - return type === 'imp' ? 'bg-blue-100 text-blue-800' : 'bg-green-100 text-green-800'; -} - -/** - * Obtiene el color del badge según el semáforo fiscal - */ -function getTrafficLightColor(status?: string | null): string { - if (!status) return 'bg-gray-100 text-gray-800'; - - const statusLower = status.toLowerCase(); - if (statusLower.includes('verde') || statusLower === 'green') return 'bg-green-100 text-green-800'; - if (statusLower.includes('amarillo') || statusLower === 'yellow') return 'bg-yellow-100 text-yellow-800'; - if (statusLower.includes('rojo') || statusLower === 'red') return 'bg-red-100 text-red-800'; - - return 'bg-gray-100 text-gray-800'; -} - export function createColumns(onSuccess?: () => void): ColumnDef[] { return [ - { - accessorKey: "id", - header: "ID", - cell: ({ row }) => { - const idSnippet = createRawSnippet<[{ id: number }]>((getId) => { - const { id } = getId(); - return { - render: () => - `
#${id}
` - }; - }); - return renderSnippet(idSnippet, { id: row.original.id }); - } - }, { accessorKey: "operation_type", header: "Operación", cell: ({ row }) => { - const type = row.original.operation_type; - const colorClass = getOperationTypeColor(type); - const label = type === 'imp' ? 'IMP' : type === 'exp' ? 'EXP' : 'N/A'; + const operationType = row.original.operation_type; - const typeSnippet = createRawSnippet<[{ label: string; colorClass: string }]>((getType) => { - const { label, colorClass } = getType(); + const operationSnippet = createRawSnippet<[{ type?: string | null }]>((getType) => { + const { type } = getType(); + const isImport = type === 'imp'; + const colorClass = isImport ? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200' : 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200'; + const label = isImport ? 'Importación' : type === 'exp' ? 'Exportación' : '-'; return { render: () => ` @@ -97,26 +36,28 @@ export function createColumns(onSuccess?: () => void): ColumnDef[] { ` }; }); - return renderSnippet(typeSnippet, { label, colorClass }); + return renderSnippet(operationSnippet, { type: operationType }); } }, { accessorKey: "invoice_type", - header: "Tipo", + header: "Tipo Factura", cell: ({ row }) => { + const invoiceType = row.original.invoice_type; + const typeSnippet = createRawSnippet<[{ type?: string | null }]>((getType) => { const { type } = getType(); return { render: () => - `
${type || '-'}
` + `
${type || '-'}
` }; }); - return renderSnippet(typeSnippet, { type: row.original.invoice_type }); + return renderSnippet(typeSnippet, { type: invoiceType }); } }, { accessorKey: "invoice_number", - header: "Número de Factura", + header: "Núm. Factura", cell: ({ row }) => { const numberSnippet = createRawSnippet<[{ number?: string | null }]>((getNumber) => { const { number } = getNumber(); @@ -127,100 +68,40 @@ export function createColumns(onSuccess?: () => void): ColumnDef[] { }); return renderSnippet(numberSnippet, { number: row.original.invoice_number }); } - }, - { - accessorKey: "project_number", - header: "Proyecto", - cell: ({ row }) => { - const projectSnippet = createRawSnippet<[{ project?: string | null }]>((getProject) => { - const { project } = getProject(); - return { - render: () => - `
${project || '-'}
` - }; - }); - return renderSnippet(projectSnippet, { project: row.original.project_number }); - } }, { accessorKey: "compliance_mx.pedimento", - header: "Pedimento", + header: "Pedimento 18", cell: ({ row }) => { const pedimento = row.original.compliance_mx?.pedimento; + const fullNumber = pedimento + ? `${pedimento.year || ''}-${pedimento.customs_office || ''}-${pedimento.license || ''}-${pedimento.pedimento_number || ''}` + : null; - const pedimentoSnippet = createRawSnippet<[{ pedimento?: string | null }]>((getPedimento) => { - const { pedimento } = getPedimento(); + const pedimentoSnippet = createRawSnippet<[{ number?: string | null }]>((getPedimento) => { + const { number } = getPedimento(); return { render: () => - `
${pedimento || '-'}
` + `${number || 'N/A'}` }; }); - return renderSnippet(pedimentoSnippet, { pedimento }); + return renderSnippet(pedimentoSnippet, { number: fullNumber }); } }, { - accessorKey: "financials.value_mn", - header: () => { - const headerSnippet = createRawSnippet(() => { - return { - render: () => `
Valor MN
` - }; - }); - return renderSnippet(headerSnippet, {}); - }, + accessorKey: "compliance_mx.remesa", + header: "Remesa", cell: ({ row }) => { - const valueMN = row.original.financials?.value_mn; + const remesa = row.original.compliance_mx?.remesa; - const valueSnippet = createRawSnippet<[{ value: string }]>((getValue) => { - const { value } = getValue(); + const remesaSnippet = createRawSnippet<[{ remesa?: number | null }]>((getRemesa) => { + const { remesa } = getRemesa(); return { render: () => - `
${value}
` + `
${remesa || '-'}
` }; }); - return renderSnippet(valueSnippet, { value: formatCurrencyMXN(valueMN) }); - } - }, - { - accessorKey: "financials.value_me", - header: () => { - const headerSnippet = createRawSnippet(() => { - return { - render: () => `
Valor ME
` - }; - }); - return renderSnippet(headerSnippet, {}); - }, - cell: ({ row }) => { - const valueME = row.original.financials?.value_me; - - const valueSnippet = createRawSnippet<[{ value: string }]>((getValue) => { - const { value } = getValue(); - return { - render: () => - `
${value}
` - }; - }); - return renderSnippet(valueSnippet, { value: formatCurrencyUSD(valueME) }); - } - }, - { - accessorKey: "traffic_light_status", - header: "Semáforo", - cell: ({ row }) => { - const status = row.original.traffic_light_status; - const colorClass = getTrafficLightColor(status); - - const statusSnippet = createRawSnippet<[{ status?: string | null; colorClass: string }]>((getStatus) => { - const { status, colorClass } = getStatus(); - return { - render: () => - ` - ${status || '-'} - ` - }; - }); - return renderSnippet(statusSnippet, { status, colorClass }); + return renderSnippet(remesaSnippet, { remesa }); } }, { @@ -237,6 +118,158 @@ export function createColumns(onSuccess?: () => void): ColumnDef[] { return renderSnippet(dateSnippet, { date: formatDate(row.original.invoice_date) }); } }, + { + accessorKey: "compliance_mx.pedimento.pedimento_code", + header: "Clave Ped.", + cell: ({ row }) => { + const pedimentoCode = row.original.compliance_mx?.pedimento?.pedimento_code; + + const claveSnippet = createRawSnippet<[{ code?: string }]>((getClave) => { + const { code } = getClave(); + return { + render: () => + `
${code || '-'}
` + }; + }); + return renderSnippet(claveSnippet, { code: pedimentoCode }); + } + }, + { + accessorKey: "document_type", + header: "Tipo Doc.", + cell: ({ row }) => { + const typeSnippet = createRawSnippet<[{ type?: string | null }]>((getType) => { + const { type } = getType(); + return { + render: () => + `
${type || '-'}
` + }; + }); + return renderSnippet(typeSnippet, { type: row.original.document_type }); + } + }, + { + accessorKey: "total_items", + header: "Total Partidas", + cell: ({ row }) => { + // El total de items viene del conteo de details + const totalItems = row.original.details?.length || 0; + + const itemsSnippet = createRawSnippet<[{ total: number }]>((getTotal) => { + const { total } = getTotal(); + return { + render: () => + `
${total}
` + }; + }); + return renderSnippet(itemsSnippet, { total: totalItems }); + } + }, + { + accessorKey: "financials.currency", + header: "Moneda", + cell: ({ row }) => { + const currency = row.original.financials?.currency; + + const currencySnippet = createRawSnippet<[{ currency?: string | null }]>((getCurrency) => { + const { currency } = getCurrency(); + return { + render: () => + `
${currency?.toLocaleUpperCase() || '-'}
` + }; + }); + return renderSnippet(currencySnippet, { currency }); + } + }, + { + accessorKey: "financials.currency_type", + header: "Tipo Moneda", + cell: ({ row }) => { + const currencyType = row.original.financials?.currency_type; + + const typeSnippet = createRawSnippet<[{ type?: string | null }]>((getType) => { + const { type } = getType(); + return { + render: () => + `
${type || '-'}
` + }; + }); + return renderSnippet(typeSnippet, { type: currencyType }); + } + }, + { + accessorKey: "logistics.weight_type", + header: "Tipo Peso", + cell: ({ row }) => { + // weight_type está en logistics que es un array, tomamos el primer elemento + const weightType = row.original.logistics?.weight_type; + + const weightSnippet = createRawSnippet<[{ type?: string | null }]>((getType) => { + const { type } = getType(); + return { + render: () => + `
${type?.toUpperCase() || '-'}
` + }; + }); + return renderSnippet(weightSnippet, { type: weightType }); + } + }, + { + accessorKey: "is_updated", + header: "Actualizado", + cell: ({ row }) => { + const isUpdated = row.original.is_updated; + + const updatedSnippet = createRawSnippet<[{ isUpdated?: boolean | null }]>((getUpdated) => { + const { isUpdated } = getUpdated(); + const colorClass = isUpdated ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'; + const label = isUpdated ? 'Sí' : 'No'; + return { + render: () => + ` + ${label} + ` + }; + }); + return renderSnippet(updatedSnippet, { isUpdated }); + } + }, + { + accessorKey: "compliance_mx.is_mixed", + header: "Mixto", + cell: ({ row }) => { + const isMixed = row.original.compliance_mx?.is_mixed; + + const mixedSnippet = createRawSnippet<[{ isMixed?: boolean | null }]>((getMixed) => { + const { isMixed } = getMixed(); + const colorClass = isMixed ? 'bg-blue-100 text-blue-800' : 'bg-gray-100 text-gray-800'; + const label = isMixed ? 'Sí' : 'No'; + return { + render: () => + ` + ${label} + ` + }; + }); + return renderSnippet(mixedSnippet, { isMixed }); + } + }, + { + accessorKey: "related_doc_id", + header: "Doc. Relacionado", + cell: ({ row }) => { + const relDoc = row.original.related_doc_id; + + const relDocSnippet = createRawSnippet<[{ relDoc?: number | null }]>((getRelDoc) => { + const { relDoc } = getRelDoc(); + return { + render: () => + `
${relDoc || '-'}
` + }; + }); + return renderSnippet(relDocSnippet, { relDoc }); + } + }, { id: "actions", cell: ({ row }) => { diff --git a/frontend/src/lib/components/dashboard/invoices/edit/general-tab-form.svelte b/frontend/src/lib/components/dashboard/invoices/edit/general-tab-form.svelte index 6c42b0ac..c7875be8 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/general-tab-form.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/general-tab-form.svelte @@ -71,11 +71,11 @@ exchange_rate: invoice.financials?.exchange_rate || null, // Added exchange_rate weight_type: 'kgs', iva_factor: invoice.financials?.iva_factor || null, - carrier_id: invoice.logistics?.[0]?.carrier_id || null, - transport_id: invoice.logistics?.[0]?.transport_id || '', - driver_name: invoice.logistics?.[0]?.driver_name || '', - transport_type: invoice.logistics?.[0]?.transport_type || '', - transport_num: invoice.logistics?.[0]?.vehicle_num || '', + carrier_id: invoice.logistics?.carrier_id || null, + transport_id: invoice.logistics?.transport_id || '', + driver_name: invoice.logistics?.driver_name || '', + transport_type: invoice.logistics?.transport_type || '', + transport_num: invoice.logistics?.vehicle_num || '', aduana: invoice.compliance_mx?.aduana || '', document_type: invoice.document_type || '', }; diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-configuration.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-configuration.svelte index 178a48de..43237574 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-configuration.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-configuration.svelte @@ -12,20 +12,27 @@ descriptions: LineDescriptions; } = $props(); + // Initialize fa_data for fixed asset system + if (!lineItem.fa_data) { + lineItem.fa_data = {}; + } + // Helper to map boolean to string for RadioGroup - let isSubPartidaValue = $derived(lineItem.is_subitem ? 'subpartida' : 'partida'); + let isSubPartidaValue = $derived(lineItem.fa_data?.is_subitem ? 'subpartida' : 'partida'); function setIsSubPartida(val: string) { - lineItem.is_subitem = val === 'subpartida'; + if (!lineItem.fa_data) lineItem.fa_data = {}; + lineItem.fa_data.is_subitem = val === 'subpartida'; } - let continueSubPartidasValue = $derived(lineItem.includes_subitems ? 'si' : 'no'); + let containsSubPartidasValue = $derived(lineItem.fa_data?.contains_subitems ? 'si' : 'no'); function setContinueSubPartidas(val: string) { - lineItem.includes_subitems = val === 'si'; + if (!lineItem.fa_data) lineItem.fa_data = {}; + lineItem.fa_data.contains_subitems = val === 'si'; }
- +
Is @@ -45,9 +52,9 @@
- Continue Sub-Items + Contains Sub-Items
@@ -67,7 +74,7 @@
- +
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/main-data.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/main-data.svelte index 891d798c..a2522f3a 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/main-data.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/main-data.svelte @@ -24,7 +24,7 @@
- +
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/packages-section.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/packages-section.svelte index 40183ca1..7cf2ad92 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/packages-section.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/packages-section.svelte @@ -24,7 +24,7 @@
- +
@@ -36,11 +36,10 @@
- +
- - +
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte index 3b10300c..d354044a 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte @@ -25,9 +25,24 @@ let gross_weight = 0; let items = $state([]); - let displayedItems = $state([]); + let displayedItems = $state([]); let itemsPerPage = 20; let currentPage = $state(1); + + // Aplanar items en líneas para la tabla + const flattenedLines = $derived( + items.flatMap(item => + (item.lines || []).map(line => ({ + ...line, + item_id: item.id, + reference_number: item.reference_number, + order: item.order, + warehouse: item.warehouse, + location: item.location, + full_item: item + })) + ) + ); let tableContainer: HTMLDivElement | undefined = $state(); let isLoadingMore = $state(false); let isLoadingItems = $state(false); @@ -84,7 +99,7 @@ function loadMoreItems() { const start = 0; const end = currentPage * itemsPerPage; - displayedItems = items.slice(start, end); + displayedItems = flattenedLines.slice(start, end); isLoadingMore = false; } @@ -93,7 +108,7 @@ const threshold = 100; const scrolledToBottom = target.scrollHeight - target.scrollTop - target.clientHeight < threshold; - if (scrolledToBottom && !isLoadingMore && displayedItems.length < items.length) { + if (scrolledToBottom && !isLoadingMore && displayedItems.length < flattenedLines.length) { isLoadingMore = true; currentPage++; loadMoreItems(); @@ -122,18 +137,16 @@ lines: [{ line_number: 1, // LineItem fields - part_number: undefined, - component_part_number: undefined, - class_code: undefined, + part_number_id: undefined, + component_part_number_id: undefined, + class_id: undefined, identifier: undefined, unit_of_measure: undefined, alternate_unit: undefined, permit_number: undefined, page_line: undefined, has_certificate: false, - certificate_number: undefined, - is_subitem: false, - includes_subitems: false, + certificate_number: undefined, tax_payment: false, payment_method: undefined, igi_amount: undefined, @@ -186,11 +199,11 @@ }; } - function handleEdit(item: Item) { + function handleEdit(lineData: any) { isEditMode = true; - selectedItem = item; + selectedItem = lineData.full_item; // Deep clone and normalize numeric values - editingItem = normalizeItemData({ ...item }); + editingItem = normalizeItemData({ ...lineData.full_item }); showItemSheet = true; } @@ -245,8 +258,8 @@ return item; } - function handleDelete(item: Item) { - selectedItem = item; + function handleDelete(lineData: any) { + selectedItem = lineData.full_item; showDeleteDialog = true; } @@ -364,11 +377,16 @@ > - - Referencia - Orden - Almacén - Ubicación + + Línea + P/S + Clase + Descripcion Clase + Cant. Importada + U.M. + Preferencia + Contiene Subpartida + Partida Principal Acciones @@ -382,10 +400,15 @@ {:else} {#each displayedItems as item (item.id)} + {item.line_number} + {item.is_subitem ? 'S' : 'P'} + {item.class_code || '-'} + {item.class_description || '-'} + {item.quantity?.quantity || '0'} + {item.unit_of_measure_code || '-'} {item.reference_number || '-'} - {item.order || '-'} - {item.warehouse || '-'} - {item.location || '-'} + {item.contains_subitems || '-'} + {item.warehouse || '-'}
- {#if items.length > 0} + {#if flattenedLines.length > 0}
- Mostrando {displayedItems.length} de {items.length} items + Mostrando {displayedItems.length} de {flattenedLines.length} líneas
{/if}
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/save-invoice.ts b/frontend/src/lib/components/dashboard/invoices/edit/save-invoice.ts index a685741a..9660f5b3 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/save-invoice.ts +++ b/frontend/src/lib/components/dashboard/invoices/edit/save-invoice.ts @@ -87,7 +87,9 @@ export async function saveInvoice(options: SaveInvoiceOptions): Promise