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:
17
backend/api/v1/modules/a24/fa/fa_item_lines/__init__.py
Normal file
17
backend/api/v1/modules/a24/fa/fa_item_lines/__init__.py
Normal 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",
|
||||
]
|
||||
167
backend/api/v1/modules/a24/fa/fa_item_lines/dto.py
Normal file
167
backend/api/v1/modules/a24/fa/fa_item_lines/dto.py
Normal 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
|
||||
@@ -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}')>"
|
||||
|
||||
35
backend/api/v1/modules/a24/fa/fa_item_lines/routes.py
Normal file
35
backend/api/v1/modules/a24/fa/fa_item_lines/routes.py
Normal 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
|
||||
258
backend/api/v1/modules/a24/fa/fa_item_lines/service.py
Normal file
258
backend/api/v1/modules/a24/fa/fa_item_lines/service.py
Normal 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"
|
||||
)
|
||||
@@ -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"]
|
||||
)
|
||||
|
||||
@@ -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"]:
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user