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.
This commit is contained in:
2026-01-12 12:23:40 -06:00
parent 6845dcd670
commit 8dab1d6e46
19 changed files with 852 additions and 173 deletions

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,35 @@
"""
Endpoints API para gestión de líneas de activos fijos (FA Item Lines)
"""
from typing import Any, Dict
from fastapi import Depends, Query
from sqlalchemy.orm import Session
from core.database import get_core_db
from core.security import get_current_user
from api.v1.common.tenant_crud_routes import (
TenantCRUDRoutes,
validate_access_to_resource,
)
from .dto import FaLineItemCreateDTO, FaLineItemResponseDTO, FaLineItemUpdateDTO
from .service import FaLineItemService
# Create router with generic CRUD routes
crud_routes = TenantCRUDRoutes(
service=FaLineItemService,
create_schema=FaLineItemCreateDTO,
update_schema=FaLineItemUpdateDTO,
response_schema=FaLineItemResponseDTO,
prefix="/fa/item-lines",
tags=["a24 / fa / item-lines"],
resource_name="Fixed Asset Line Item",
id_name="fa_line_item_id",
enable_list=True,
enable_filters=True,
default_page_size=50,
max_page_size=100,
)
router = crud_routes.router

View File

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

View File

@@ -6,9 +6,13 @@ from fastapi import APIRouter
# Importar routers de submódulos
from .fa.fa_classes.routes import router as fa_classes_router
from .fa.fa_item_lines.routes import router as fa_item_lines_router
# Router principal de A24
router = APIRouter()
# Registrar routers de FA (Fixed Assets)
router.include_router(fa_classes_router, prefix="/a24", tags=["a24 / fa / classes"])
router.include_router(
fa_item_lines_router, prefix="/a24", tags=["a24 / fa / item-lines"]
)

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,11 @@ if TYPE_CHECKING:
from ..line_customs.models import LineCustom
from ..line_descriptions.models import LineDescription
from ..line_references.models import LineReference
from api.v1.modules.a76.classes.models import Class
from api.v1.modules.a76.general_catalogs.units_of_measure.models import (
UnitOfMeasure,
)
from api.v1.modules.a24.fa.fa_item_lines.models import FaLineItem
class LineItem(Base, TenantScopedMixin, TimestampMixin):
@@ -19,6 +24,7 @@ class LineItem(Base, TenantScopedMixin, TimestampMixin):
Unified line items for all items
Consolidates all line-level data from Q and S tables
"""
__tablename__ = "item_lines"
__table_args__ = {
"schema": "a76",
@@ -26,154 +32,152 @@ class LineItem(Base, TenantScopedMixin, TimestampMixin):
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
item_id: Mapped[int] = mapped_column(ForeignKey("a76.items.id"))
line_number: Mapped[int] = mapped_column(
Integer) # LINEAIMPO/LINEAEXPO/LINEA
line_number: Mapped[int] = mapped_column(Integer) # LINEAIMPO/LINEAEXPO/LINEA
# Part identification
part_number: Mapped[Optional[str]] = mapped_column(
ForeignKey("a76.parts.id")) # NUMPARTE
component_part_number: Mapped[Optional[str]] = mapped_column(
ForeignKey("a76.parts.id")) # NUMPARTECOM
class_code: Mapped[Optional[str]] = mapped_column(
ForeignKey("a76.classes.id")) # CLASE
part_number_id: Mapped[Optional[str]] = mapped_column(
ForeignKey("a76.parts.id")
) # NUMPARTE
component_part_number_id: Mapped[Optional[str]] = mapped_column(
ForeignKey("a76.parts.id")
) # NUMPARTECOM
class_id: Mapped[Optional[str]] = mapped_column(
ForeignKey("a76.classes.id")
) # CLASE
# Unit of measure
unit_of_measure: Mapped[Optional[str]] = mapped_column(
ForeignKey("a76.units_of_measure.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
)

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ Item -> LineItem -> LineFinancial
-> LineCustoms
-> LineDescription
-> LineReference
-> FaLineItem (Fixed Assets - a24)
"""
import logging
@@ -24,6 +25,7 @@ from .line_quantities.models import LineQuantity
from .line_customs.models import LineCustom
from .line_descriptions.models import LineDescription
from .line_references.models import LineReference
from api.v1.modules.a24.fa.fa_item_lines.models import FaLineItem
from .models import Item
logger = logging.getLogger(__name__)
@@ -47,6 +49,9 @@ class ItemService:
joinedload(Item.lines).joinedload(LineItem.customs),
joinedload(Item.lines).joinedload(LineItem.description),
joinedload(Item.lines).joinedload(LineItem.reference),
joinedload(Item.lines).joinedload(LineItem.class_info),
joinedload(Item.lines).joinedload(LineItem.unit_of_measure_info),
joinedload(Item.lines).joinedload(LineItem.fa_data),
)
.filter(
Item.id == item_id,
@@ -74,6 +79,9 @@ class ItemService:
joinedload(Item.lines).joinedload(LineItem.customs),
joinedload(Item.lines).joinedload(LineItem.description),
joinedload(Item.lines).joinedload(LineItem.reference),
joinedload(Item.lines).joinedload(LineItem.class_info),
joinedload(Item.lines).joinedload(LineItem.unit_of_measure_info),
joinedload(Item.lines).joinedload(LineItem.fa_data),
)
.filter(
Item.tenant_id == tenant_id,
@@ -122,6 +130,7 @@ class ItemService:
joinedload(Item.lines).joinedload(LineItem.customs),
joinedload(Item.lines).joinedload(LineItem.description),
joinedload(Item.lines).joinedload(LineItem.reference),
joinedload(Item.lines).joinedload(LineItem.fa_data),
)
.filter(
Item.invoice_id == invoice_id,
@@ -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

View File

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

View File

@@ -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';
}
</script>
<div class="lg:col-span-5 space-y-3">
<!-- Is Item/Subitem and Continue Sub-Items -->
<!-- Is Item/Subitem and Contains Sub-Items -->
<div class="grid grid-cols-2 gap-2">
<fieldset class="border rounded-md p-2 space-y-2">
<legend class="text-xs font-semibold px-2 bg-gray-200 dark:bg-gray-700">Is</legend>
@@ -45,9 +52,9 @@
</fieldset>
<fieldset class="border rounded-md p-2 space-y-2">
<legend class="text-xs font-semibold px-2 bg-gray-200 dark:bg-gray-700">Continue Sub-Items</legend>
<legend class="text-xs font-semibold px-2 bg-gray-200 dark:bg-gray-700">Contains Sub-Items</legend>
<RadioGroup.Root
value={continueSubPartidasValue}
value={containsSubPartidasValue}
onValueChange={setContinueSubPartidas}
class="flex gap-3">
<div class="flex items-center space-x-2">
@@ -67,7 +74,7 @@
<div class="space-y-1">
<Label for="num_parte" class="text-xs">Part Number:</Label>
<div class="flex gap-1">
<Input id="num_parte" bind:value={lineItem.part_number} class="h-7 text-xs" />
<Input id="num_parte" bind:value={lineItem.part_number_id} class="h-7 text-xs" />
</div>
</div>

View File

@@ -24,7 +24,7 @@
<div class="col-span-4 space-y-1">
<Label for="clase" class="text-xs font-medium">* Class:</Label>
<div class="flex gap-1">
<Input id="clase" bind:value={lineItem.class_code} class="h-8 text-xs" />
<Input id="clase" bind:value={lineItem.class_id} class="h-8 text-xs" />
</div>
</div>
</div>

View File

@@ -25,9 +25,24 @@
let gross_weight = 0;
let items = $state<Item[]>([]);
let displayedItems = $state<Item[]>([]);
let displayedItems = $state<any[]>([]);
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 @@
>
<Table.Root>
<Table.Header class="sticky top-0 bg-background z-10">
<Table.Row>
<Table.Head>Referencia</Table.Head>
<Table.Head>Orden</Table.Head>
<Table.Head>Almacén</Table.Head>
<Table.Head>Ubicación</Table.Head>
<Table.Row>
<Table.Head>Línea</Table.Head>
<Table.Head>P/S</Table.Head>
<Table.Head>Clase</Table.Head>
<Table.Head>Descripcion Clase</Table.Head>
<Table.Head>Cant. Importada</Table.Head>
<Table.Head>U.M.</Table.Head>
<Table.Head>Preferencia</Table.Head>
<Table.Head>Contiene Subpartida</Table.Head>
<Table.Head>Partida Principal</Table.Head>
<Table.Head class="text-right w-[120px]">Acciones</Table.Head>
</Table.Row>
</Table.Header>
@@ -382,10 +400,15 @@
{:else}
{#each displayedItems as item (item.id)}
<Table.Row>
<Table.Cell>{item.line_number}</Table.Cell>
<Table.Cell>{item.is_subitem ? 'S' : 'P'}</Table.Cell>
<Table.Cell>{item.class_code || '-'}</Table.Cell>
<Table.Cell>{item.class_description || '-'}</Table.Cell>
<Table.Cell>{item.quantity?.quantity || '0'}</Table.Cell>
<Table.Cell>{item.unit_of_measure_code || '-'}</Table.Cell>
<Table.Cell>{item.reference_number || '-'}</Table.Cell>
<Table.Cell>{item.order || '-'}</Table.Cell>
<Table.Cell>{item.warehouse || '-'}</Table.Cell>
<Table.Cell>{item.location || '-'}</Table.Cell>
<Table.Cell>{item.contains_subitems || '-'}</Table.Cell>
<Table.Cell>{item.warehouse || '-'}</Table.Cell>
<Table.Cell class="text-right">
<div class="flex justify-end gap-2">
<Button size="icon" variant="ghost" onclick={() => handleEdit(item)}>
@@ -410,9 +433,9 @@
</Table.Root>
</div>
{#if items.length > 0}
{#if flattenedLines.length > 0}
<div class="text-xs text-muted-foreground text-right">
Mostrando {displayedItems.length} de {items.length} items
Mostrando {displayedItems.length} de {flattenedLines.length} líneas
</div>
{/if}
</div>