From 8dab1d6e4681d5efade0c292a587fd76c99f955d Mon Sep 17 00:00:00 2001 From: acazares Date: Mon, 12 Jan 2026 12:23:40 -0600 Subject: [PATCH] feat: Implement Fixed Asset (FA) line item management - Added new schemas for FA line items in the backend, including creation, update, and response DTOs. - Updated existing line item schemas to include FA data. - Modified database models to reflect new table names for line quantities and references. - Enhanced ItemService to handle FA data during item creation and updates. - Introduced new routes and service layer for FA line items, including CRUD operations. - Updated frontend components to support FA line item data, including new fields and UI adjustments. - Implemented data flattening for improved item display in the dashboard. --- .../modules/a24/fa/fa_item_lines/__init__.py | 17 ++ .../v1/modules/a24/fa/fa_item_lines/dto.py | 167 ++++++++++++ .../v1/modules/a24/fa/fa_item_lines/models.py | 96 +++++-- .../v1/modules/a24/fa/fa_item_lines/routes.py | 35 +++ .../modules/a24/fa/fa_item_lines/service.py | 258 ++++++++++++++++++ backend/api/v1/modules/a24/router.py | 4 + .../imports/temporary/validators/common.py | 5 +- .../modules/a76/items/line_customs/models.py | 2 +- .../a76/items/line_descriptions/models.py | 2 +- .../a76/items/line_financials/models.py | 2 +- .../v1/modules/a76/items/line_items/models.py | 166 +++++------ .../modules/a76/items/line_items/schemas.py | 67 ++++- .../a76/items/line_quantities/models.py | 2 +- .../a76/items/line_references/models.py | 2 +- backend/api/v1/modules/a76/items/service.py | 54 +++- frontend/src/lib/api/dashboard/a76/items.ts | 50 +++- .../edit/items/fa/item-configuration.svelte | 23 +- .../invoices/edit/items/fa/main-data.svelte | 2 +- .../invoices/edit/items/items-tab-form.svelte | 71 +++-- 19 files changed, 852 insertions(+), 173 deletions(-) create mode 100644 backend/api/v1/modules/a24/fa/fa_item_lines/__init__.py create mode 100644 backend/api/v1/modules/a24/fa/fa_item_lines/dto.py create mode 100644 backend/api/v1/modules/a24/fa/fa_item_lines/routes.py create mode 100644 backend/api/v1/modules/a24/fa/fa_item_lines/service.py 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/invoices/imports/temporary/validators/common.py b/backend/api/v1/modules/a76/invoices/imports/temporary/validators/common.py index 55ca2578..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,3 +1,4 @@ +from sqlalchemy import func from sqlalchemy.orm import Session from .... import schemas from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos @@ -206,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, ) @@ -220,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"]: 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 7adaafaf..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.id")) # UNIDADMEDIDA/UNIMED + ForeignKey("a76.units_of_measure.id") + ) # UNIDADMEDIDA/UNIMED alternate_unit: Mapped[Optional[str]] = mapped_column( - ForeignKey("a76.units_of_measure.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 6bd82acf..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, @@ -145,7 +154,7 @@ class ItemService: try: # Extract lines data lines_data = item_data.lines or [] - item_dict = item_data.model_dump(exclude={"lines"}) + item_dict = item_data.model_dump(exclude={"lines"}) # Add tenant and company item_dict["tenant_id"] = tenant_id @@ -154,16 +163,17 @@ class ItemService: # Create the item db_item = Item(**item_dict) db.add(db_item) - db.flush() # Get the item ID + db.flush() # Get the item ID # Create line items if provided - for idx, line_data in enumerate(lines_data): + for idx, line_data in enumerate(lines_data): # Extract nested data from line financial_data = line_data.financial quantity_data = line_data.quantity customs_data = line_data.customs description_data = line_data.description - reference_data = line_data.reference + 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 @@ -181,45 +192,54 @@ class ItemService: # Create line item db_line = LineItem(**line_dict) db.add(db_line) - db.flush() # Get the line ID + db.flush() # Get the line ID # Create financial data if provided if financial_data: financial_dict = financial_data.model_dump() financial_dict["item_line_id"] = db_line.id db_financial = LineFinancial(**financial_dict) - db.add(db_financial) + db.add(db_financial) # Create quantity data if provided if quantity_data: quantity_dict = quantity_data.model_dump() quantity_dict["item_line_id"] = db_line.id db_quantity = LineQuantity(**quantity_dict) - db.add(db_quantity) + db.add(db_quantity) # Create customs data if provided if customs_data: customs_dict = customs_data.model_dump() customs_dict["item_line_id"] = db_line.id db_customs = LineCustom(**customs_dict) - db.add(db_customs) + db.add(db_customs) # Create description data if provided if description_data: description_dict = description_data.model_dump() description_dict["item_line_id"] = db_line.id db_description = LineDescription(**description_dict) - db.add(db_description) + db.add(db_description) # Create reference data if provided if reference_data: reference_dict = reference_data.model_dump() reference_dict["item_line_id"] = db_line.id db_reference = LineReference(**reference_dict) - db.add(db_reference) - + 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) + db.refresh(db_item) return db_item except IntegrityError as e: @@ -272,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={ @@ -280,6 +301,7 @@ class ItemService: "customs", "description", "reference", + "fa_data", }, exclude_unset=True, ) @@ -319,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/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/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/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}