refactor: restructure item interfaces and update related components
- Renamed LineItem interface to Item and adjusted properties accordingly. - Updated CreateItemData and UpdateItemData interfaces to reflect new structure. - Modified components to use the new Item interface, removing nested lines. - Adjusted data binding in item configuration, main data, and other related components. - Simplified item creation and editing logic by removing unnecessary nesting. - Ensured all references to line items are updated to reflect the new structure.
This commit is contained in:
@@ -16,7 +16,7 @@ from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from api.v1.modules.a76.items.line_items.models import LineItem
|
||||
from api.v1.modules.a76.items.models import LineItem
|
||||
|
||||
|
||||
class FaLineItem(Base, TenantScopedMixin, TimestampMixin):
|
||||
|
||||
@@ -96,7 +96,7 @@ class FaLineItemService:
|
||||
"""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
|
||||
from api.v1.modules.a76.items.models import LineItem
|
||||
|
||||
base_line_item = (
|
||||
db.query(LineItem)
|
||||
|
||||
@@ -7,7 +7,7 @@ from api.v1.modules.a76.clients_and_providers.models import ClientProvider
|
||||
from api.v1.modules.a76.customs_brokers.models import CustomsBroker
|
||||
from api.v1.modules.public.reference_data.incoterms.models import Incoterm
|
||||
from ....models import InvoiceComplianceMx
|
||||
from api.v1.modules.a76.items.models import Item
|
||||
from api.v1.modules.a76.items.models import LineItem
|
||||
from api.v1.modules.public.reference_data.currency_types.models import CurrencyType
|
||||
from api.v1.modules.public.reference_data.customs_sections.models import CustomsSection
|
||||
from ....models import TransportType, Currency, WeightUnit
|
||||
@@ -435,11 +435,11 @@ def validate_common(
|
||||
# Only check for existing items during update operations (when invoice has an id)
|
||||
if hasattr(invoice, "id"):
|
||||
has_items = (
|
||||
db.query(Item)
|
||||
db.query(LineItem)
|
||||
.filter(
|
||||
Item.invoice_id == invoice.id,
|
||||
Item.tenant_id == tenant_id,
|
||||
Item.company_id == company_id,
|
||||
LineItem.invoice_id == invoice.id,
|
||||
LineItem.tenant_id == tenant_id,
|
||||
LineItem.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
@@ -4,16 +4,14 @@ Items module - Annex 76 Compliance
|
||||
|
||||
# Import models in correct order to avoid circular dependencies
|
||||
# LineItem must be imported before models that reference it
|
||||
from .line_items.models import LineItem
|
||||
from .line_financials.models import LineFinancial
|
||||
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 .models import Item, CTMReceipt, SubassemblyEntry
|
||||
from .models import LineItem, CTMReceipt, SubassemblyEntry
|
||||
|
||||
__all__ = [
|
||||
"Item",
|
||||
"LineItem",
|
||||
"LineFinancial",
|
||||
"LineQuantity",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from sqlalchemy import func
|
||||
from core.exceptions import ErrorCollector
|
||||
from ..line_items import models
|
||||
from .. import models
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
|
||||
@@ -21,11 +21,11 @@ def item_exists(db: Session, item_line: int, tenant_id: int, company_id: int):
|
||||
def count_items(db: Session, invoice_id: int, tenant_id: int, company_id: int):
|
||||
count = (
|
||||
db.query(func.count())
|
||||
.select_from(models.Item)
|
||||
.select_from(models.LineItem)
|
||||
.filter(
|
||||
models.Item.invoice_id == invoice_id,
|
||||
models.Item.tenant_id == tenant_id,
|
||||
models.Item.company_id == company_id,
|
||||
models.LineItem.invoice_id == invoice_id,
|
||||
models.LineItem.tenant_id == tenant_id,
|
||||
models.LineItem.company_id == company_id,
|
||||
)
|
||||
.scalar()
|
||||
)
|
||||
|
||||
@@ -6,8 +6,7 @@ from sqlalchemy import func
|
||||
|
||||
from ....common.fractions import search_fraction_preference
|
||||
from ....common.common_validators import item_exists
|
||||
from ....models import Item
|
||||
from ....line_items.models import LineItem
|
||||
from ....models import LineItem
|
||||
from ....line_customs.models import FractionType, LineCustom
|
||||
from api.v1.modules.a76.items.schemas import LineItemCreate
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader
|
||||
@@ -26,21 +25,13 @@ from api.v1.modules.a76.general_catalogs.company.models import Company
|
||||
def validate_common(
|
||||
db: Session,
|
||||
line: LineItemCreate,
|
||||
invoice_id: int, # Para creación, se pasa directamente; para update, se consulta del item
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
errors: ErrorCollector,
|
||||
line_number: int,
|
||||
):
|
||||
# Para updates, line.item_id existe; para creates, es None
|
||||
item_header = None
|
||||
if line.item_id:
|
||||
item_header = db.query(Item).filter(Item.id == line.item_id).first()
|
||||
if item_header:
|
||||
invoice_id = item_header.invoice_id
|
||||
|
||||
invoice: InvoiceHeader = invoice_exists_by_id(
|
||||
db, invoice_id, tenant_id, company_id, errors
|
||||
db, line.invoice_id, tenant_id, company_id, errors
|
||||
)
|
||||
line_item: LineItem = item_exists(db, line.line_number, tenant_id, company_id)
|
||||
|
||||
@@ -164,7 +155,7 @@ def validate_common(
|
||||
code="PACKAGE_QUANTITY_MUST_BE_GREATER_THAN_ZERO",
|
||||
)
|
||||
else:
|
||||
if (line.quantity.package_quantity or line.quantity.package_quantity > 0) and not line.quantity.package_id:
|
||||
if line.quantity.package_quantity and (line.quantity.package_quantity > 0 and not line.quantity.package_id):
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].quantity.package_id",
|
||||
message="El paquete es obligatorio cuando se proporciona la cantidad de paquetes.",
|
||||
@@ -301,8 +292,8 @@ def validate_common(
|
||||
code="AMERICAN_FRACTION_NOT_FOUND",
|
||||
)
|
||||
|
||||
if item_header and item_header.order:
|
||||
if len(item_header.order) > 20:
|
||||
if line.order:
|
||||
if len(line.order) > 20:
|
||||
errors.add_error(
|
||||
field=f"item.order",
|
||||
message="El campo orden no debe exceder los 20 caracteres.",
|
||||
|
||||
@@ -4,7 +4,7 @@ from sqlalchemy.orm import Session
|
||||
from ....common.common_validators import count_items
|
||||
from core.exceptions import ErrorCollector
|
||||
|
||||
from ....line_items.models import LineItem
|
||||
from ....models import LineItem
|
||||
from ....line_financials.models import LineFinancial
|
||||
from ....line_financials.schemas import LineFinancialCreate
|
||||
from ....line_quantities.models import LineQuantity
|
||||
@@ -15,7 +15,7 @@ from ....line_descriptions.models import LineDescription
|
||||
from ....line_descriptions.schemas import LineDescriptionCreate
|
||||
from ....line_references.models import LineReference
|
||||
from api.v1.modules.a24.fa.fa_item_lines.models import FaLineItem
|
||||
from ....models import Item
|
||||
from ....models import LineItem
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader
|
||||
from api.v1.modules.a76.classes.models import Class
|
||||
from api.v1.modules.a76.general_catalogs.packages.models import Package
|
||||
@@ -27,8 +27,7 @@ from .common import validate_common
|
||||
|
||||
def validate_create(
|
||||
db: Session,
|
||||
line, # LineItemCreate schema (Pydantic)
|
||||
invoice_id: int, # Passed from service
|
||||
line: LineItem, # LineItemCreate schema (Pydantic)
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
errors: ErrorCollector,
|
||||
@@ -42,16 +41,7 @@ def validate_create(
|
||||
line: LineItemCreate schema with nested data (financial, quantity, customs, etc.)
|
||||
invoice_id: ID of the invoice this line belongs to
|
||||
fa_data: FaLineItemCreateDTO or None (None for INV system)
|
||||
"""
|
||||
# Inicializar nested schemas si no existen (para poder validar y modificar)
|
||||
if not line.financial:
|
||||
line.financial = LineFinancialCreate()
|
||||
if not line.quantity:
|
||||
line.quantity = LineQuantityCreate()
|
||||
if not line.customs:
|
||||
line.customs = LineCustomCreate()
|
||||
if not line.description:
|
||||
line.description = LineDescriptionCreate()
|
||||
"""
|
||||
|
||||
# Access fa_data safely
|
||||
fa_data = getattr(line, "fa_data", None)
|
||||
@@ -101,8 +91,8 @@ def validate_create(
|
||||
principal_item_exists = db.query(
|
||||
exists().where(
|
||||
(LineItem.id == FaLineItem.id)
|
||||
& (LineItem.item_id == Item.id)
|
||||
& (Item.invoice_id == invoice_id)
|
||||
& (LineItem.id == LineItem.id)
|
||||
& (LineItem.invoice_id == line.invoice_id)
|
||||
& (LineItem.line_number == line_number)
|
||||
& (FaLineItem.is_subitem == False)
|
||||
& (FaLineItem.contains_subitems == True)
|
||||
@@ -131,14 +121,14 @@ def validate_create(
|
||||
code="SUBITEM_NUMBER_INVALID",
|
||||
)
|
||||
|
||||
validate_common(db, line, invoice_id, tenant_id, company_id, errors, line_number)
|
||||
validate_common(db, line, tenant_id, company_id, errors, line_number)
|
||||
|
||||
if not errors.has_errors():
|
||||
# Obtener la factura para acceder a tipo de cambio, moneda y peso
|
||||
invoice: InvoiceHeader = (
|
||||
db.query(InvoiceHeader)
|
||||
.filter(
|
||||
InvoiceHeader.id == invoice_id,
|
||||
InvoiceHeader.id == line.invoice_id,
|
||||
InvoiceHeader.tenant_id == tenant_id,
|
||||
InvoiceHeader.company_id == company_id,
|
||||
)
|
||||
|
||||
@@ -3,8 +3,7 @@ from sqlalchemy.orm import Session
|
||||
from api.v1.modules.a76.invoices.common.common_validators import invoice_exists
|
||||
from core.exceptions import ErrorCollector
|
||||
|
||||
from ....line_items.models import LineItem
|
||||
from ....models import Item
|
||||
from ....models import LineItem
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader
|
||||
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.models import (
|
||||
USTariffFraction,
|
||||
@@ -15,8 +14,7 @@ from .common import validate_common
|
||||
def validate_update(
|
||||
db: Session,
|
||||
line: LineItem,
|
||||
existing_line: LineItem,
|
||||
invoice_id: int, # Passed from service
|
||||
existing_line: LineItem,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
errors: ErrorCollector,
|
||||
@@ -26,14 +24,14 @@ def validate_update(
|
||||
Validar y procesar actualización parcial de línea de importación temporal.
|
||||
Si un campo no se proporciona, se mantiene el valor existente.
|
||||
"""
|
||||
validate_common(db, line, invoice_id, tenant_id, company_id, errors, line_number)
|
||||
validate_common(db, line, tenant_id, company_id, errors, line_number)
|
||||
|
||||
if not errors.has_errors():
|
||||
# Obtener la factura para acceder a tipo de cambio, moneda y peso
|
||||
invoice: InvoiceHeader = (
|
||||
db.query(InvoiceHeader)
|
||||
.filter(
|
||||
InvoiceHeader.id == invoice_id,
|
||||
InvoiceHeader.id == line.invoice_id,
|
||||
InvoiceHeader.tenant_id == tenant_id,
|
||||
InvoiceHeader.company_id == company_id
|
||||
)
|
||||
@@ -81,6 +79,8 @@ def validate_update(
|
||||
# Mantener peso existente
|
||||
line.quantity.net_weight = existing_line.quantity.net_weight
|
||||
|
||||
print(f"After weight conversion: net_weight={line.quantity.net_weight}, gross_weight={line.quantity.gross_weight}, weight_type={invoice_weight_type}")
|
||||
|
||||
# Convertir peso bruto si se proporcionó
|
||||
if line.quantity.gross_weight is not None:
|
||||
gross_weight_input = line.quantity.gross_weight
|
||||
@@ -141,8 +141,8 @@ def validate_update(
|
||||
line.customs.advalorem_american = existing_line.customs.advalorem_american
|
||||
|
||||
# Orden de compra
|
||||
if not line.reference.purchase_order:
|
||||
line.reference.purchase_order = existing_line.reference.purchase_order
|
||||
if not line.order:
|
||||
line.order = existing_line.order
|
||||
|
||||
# Descripciones
|
||||
if not line.description.description_spanish:
|
||||
@@ -176,8 +176,8 @@ def validate_update(
|
||||
|
||||
|
||||
# Número de parte
|
||||
if not line.part_number:
|
||||
line.part_number = existing_line.part_number
|
||||
if not line.part_number_id:
|
||||
line.part_number_id = existing_line.part_number_id
|
||||
|
||||
# Pago de impuesto
|
||||
if line.tax_payment is None:
|
||||
|
||||
@@ -5,7 +5,7 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..line_items.models import LineItem
|
||||
from ..models import LineItem
|
||||
|
||||
class FractionType:
|
||||
"""Enumeration for fraction types"""
|
||||
|
||||
@@ -4,7 +4,7 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..line_items.models import LineItem
|
||||
from ..models import LineItem
|
||||
|
||||
class LineDescription(Base):
|
||||
"""
|
||||
|
||||
@@ -5,7 +5,7 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..line_items.models import LineItem
|
||||
from ..models import LineItem
|
||||
|
||||
class LineFinancial(Base):
|
||||
"""
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
"""Line items module"""
|
||||
from .models import LineItem
|
||||
from .schemas import (
|
||||
LineItemBase,
|
||||
LineItemCreate,
|
||||
LineItemUpdate,
|
||||
LineItemResponse,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"LineItem",
|
||||
"LineItemBase",
|
||||
"LineItemCreate",
|
||||
"LineItemUpdate",
|
||||
"LineItemResponse",
|
||||
]
|
||||
@@ -1,200 +0,0 @@
|
||||
from decimal import Decimal
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
String,
|
||||
Integer,
|
||||
Numeric,
|
||||
SmallInteger,
|
||||
ForeignKey,
|
||||
ForeignKeyConstraint,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
|
||||
from api.v1.modules.a76.classes.models import Class
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..models import Item
|
||||
from ..line_financials.models import LineFinancial
|
||||
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
|
||||
|
||||
|
||||
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"},)
|
||||
|
||||
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
|
||||
|
||||
# Part identification
|
||||
part_number: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey("a76.parts.id")
|
||||
) # NUMPARTE
|
||||
component_part_number: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey("a76.parts.id")
|
||||
) # NUMPARTECOM
|
||||
class_id: Mapped[Optional[int]] = mapped_column(
|
||||
ForeignKey("a76.classes.id")
|
||||
) # CLASE
|
||||
|
||||
# Unit of measure
|
||||
unit_of_measure: Mapped[Optional[int]] = mapped_column(
|
||||
ForeignKey("a76.units_of_measure.id")
|
||||
) # UNIDADMEDIDA/UNIMED
|
||||
alternate_unit: Mapped[Optional[int]] = mapped_column(
|
||||
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
|
||||
|
||||
# Permits and certificates
|
||||
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
|
||||
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
|
||||
|
||||
# FDA
|
||||
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
|
||||
|
||||
# IV32 (Tax identification)
|
||||
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
|
||||
|
||||
# Tax payment
|
||||
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
|
||||
igi_payment_method: Mapped[Optional[str]] = mapped_column(
|
||||
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_determined_value: Mapped[Optional[Decimal]] = mapped_column(
|
||||
Numeric(29, 8)
|
||||
) # METVALORVALORDETERMINADO/METVALORACIONVALORDETERMINADO
|
||||
valuation_reason: Mapped[Optional[str]] = mapped_column(
|
||||
String(500)
|
||||
) # METVALORMOTIVODEUSO/METVALORACIONMOTIVODEUSO
|
||||
|
||||
# Container rules
|
||||
container_rule: Mapped[Optional[str]] = mapped_column(String(50)) # CONTENEDORREGLA
|
||||
container_parts_ii: Mapped[Optional[str]] = mapped_column(
|
||||
String(50)
|
||||
) # CONTENEDORPARTESII
|
||||
|
||||
# APHIS
|
||||
consecutive_aphis: Mapped[Optional[int]] = mapped_column(
|
||||
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
|
||||
|
||||
# Identifier
|
||||
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
|
||||
|
||||
# Material type
|
||||
material_type: Mapped[Optional[str]] = mapped_column(
|
||||
String(50)
|
||||
) # TIPOMAT/TIPODENUMPARTE
|
||||
|
||||
# Order concept
|
||||
order_type: Mapped[Optional[str]] = mapped_column(String(50)) # TIPODEORDEN
|
||||
line_concept: Mapped[Optional[str]] = mapped_column(
|
||||
String(50)
|
||||
) # CONCEPTODELAPARTIDA
|
||||
|
||||
# Review dispatch
|
||||
review_dispatch: Mapped[Optional[str]] = mapped_column(String(10)) # REVISARDESP
|
||||
|
||||
# Take component from PT
|
||||
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
|
||||
|
||||
# Relationships
|
||||
item: Mapped["Item"] = relationship(back_populates="lines")
|
||||
financial: Mapped[Optional["LineFinancial"]] = relationship(
|
||||
back_populates="line", cascade="all, delete-orphan", uselist=False
|
||||
)
|
||||
quantity: Mapped[Optional["LineQuantity"]] = relationship(
|
||||
back_populates="line", cascade="all, delete-orphan", uselist=False
|
||||
)
|
||||
customs: Mapped[Optional["LineCustom"]] = relationship(
|
||||
back_populates="line", cascade="all, delete-orphan", uselist=False
|
||||
)
|
||||
description: Mapped[Optional["LineDescription"]] = relationship(
|
||||
back_populates="line", cascade="all, delete-orphan", uselist=False
|
||||
)
|
||||
reference: Mapped[Optional["LineReference"]] = relationship(
|
||||
back_populates="line", cascade="all, delete-orphan", uselist=False
|
||||
)
|
||||
class_info: Mapped[Optional["Class"]] = relationship(
|
||||
"api.v1.modules.a76.classes.models.Class",
|
||||
foreign_keys=[class_id],
|
||||
viewonly=True,
|
||||
)
|
||||
unit_of_measure_info: Mapped[Optional["UnitOfMeasure"]] = relationship(
|
||||
"api.v1.modules.a76.general_catalogs.units_of_measure.models.UnitOfMeasure",
|
||||
foreign_keys=[unit_of_measure],
|
||||
viewonly=True,
|
||||
)
|
||||
fa_data: Mapped[Optional["FaLineItem"]] = relationship(
|
||||
"FaLineItem",
|
||||
back_populates="master_info",
|
||||
cascade="all, delete-orphan",
|
||||
uselist=False,
|
||||
)
|
||||
part_info: Mapped[Optional["api.v1.modules.a76.parts.models.Part"]] = relationship(
|
||||
"api.v1.modules.a76.parts.models.Part",
|
||||
foreign_keys=[part_number],
|
||||
viewonly=True,
|
||||
)
|
||||
@@ -1,293 +0,0 @@
|
||||
from decimal import Decimal
|
||||
from typing import Optional, Any
|
||||
from pydantic import BaseModel, Field, ConfigDict, field_validator, model_validator
|
||||
|
||||
# Import nested schemas
|
||||
from ..line_customs.schemas import (
|
||||
LineCustomCreate,
|
||||
LineCustomUpdate,
|
||||
LineCustomResponse,
|
||||
)
|
||||
from ..line_descriptions.schemas import (
|
||||
LineDescriptionCreate,
|
||||
LineDescriptionUpdate,
|
||||
LineDescriptionResponse,
|
||||
)
|
||||
from ..line_quantities.schemas import (
|
||||
LineQuantityCreate,
|
||||
LineQuantityUpdate,
|
||||
LineQuantityResponse,
|
||||
)
|
||||
from ..line_financials.schemas import (
|
||||
LineFinancialCreate,
|
||||
LineFinancialUpdate,
|
||||
LineFinancialResponse,
|
||||
)
|
||||
from ..line_references.schemas import (
|
||||
LineReferenceCreate,
|
||||
LineReferenceUpdate,
|
||||
LineReferenceResponse,
|
||||
)
|
||||
|
||||
from api.v1.modules.a24.fa.fa_item_lines.dto import (
|
||||
FaLineItemCreateDTO,
|
||||
FaLineItemUpdateDTO,
|
||||
FaLineItemResponseDTO,
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# LINE ITEM SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class LineItemBase(BaseModel):
|
||||
"""Base schema for line items"""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
item_id: int = Field(..., description="ID of the parent item")
|
||||
line_number: int = Field(..., description="Line number")
|
||||
|
||||
# Part identification
|
||||
part_number_id: Optional[int] = Field(
|
||||
None,
|
||||
description="Part number",
|
||||
alias="part_number",
|
||||
serialization_alias="part_number_id",
|
||||
)
|
||||
component_part_number_id: Optional[int] = Field(
|
||||
None,
|
||||
description="Component part number",
|
||||
alias="component_part_number",
|
||||
serialization_alias="component_part_number_id",
|
||||
)
|
||||
class_id: Optional[int] = Field(None, description="Class code")
|
||||
|
||||
# Unit of measure
|
||||
unit_of_measure: Optional[int] = Field(None, description="Unit of measure")
|
||||
alternate_unit: Optional[int] = Field(None, description="Alternate unit")
|
||||
uma_key: Optional[str] = Field(None, max_length=2, description="UMA key")
|
||||
auxiliary_unit: Optional[str] = Field(
|
||||
None, max_length=5, description="Auxiliary unit"
|
||||
)
|
||||
|
||||
# Permits and certificates
|
||||
permit_number: Optional[str] = Field(
|
||||
None, max_length=20, description="Permit number"
|
||||
)
|
||||
page_line: Optional[str] = Field(None, max_length=10, description="Page line")
|
||||
has_certificate: Optional[bool] = Field(None, description="Has certificate")
|
||||
certificate_number: Optional[str] = Field(
|
||||
None, max_length=10, description="Certificate number"
|
||||
)
|
||||
octave_permit: Optional[str] = Field(
|
||||
None, max_length=20, description="Octave permit"
|
||||
)
|
||||
permits_ped: Optional[str] = Field(None, max_length=500, description="PED permits")
|
||||
|
||||
# FDA
|
||||
has_fda_code: Optional[bool] = Field(None, description="Has FDA code")
|
||||
fda_key: Optional[str] = Field(None, max_length=10, description="FDA key")
|
||||
|
||||
# Special flags
|
||||
is_military_mcia: Optional[bool] = Field(
|
||||
None, description="Is military merchandise"
|
||||
)
|
||||
|
||||
# IV32
|
||||
iv32_type_key: Optional[str] = Field(
|
||||
None, max_length=5, description="IV32 type key"
|
||||
)
|
||||
iv32_number: Optional[str] = Field(None, max_length=35, description="IV32 number")
|
||||
|
||||
# Export specific
|
||||
scrap_invoice: Optional[str] = Field(
|
||||
None, max_length=15, description="Scrap invoice"
|
||||
)
|
||||
consecutive_destination: Optional[int] = Field(
|
||||
None, description="Consecutive destination"
|
||||
)
|
||||
ctm_section: Optional[str] = Field(None, max_length=3, description="CTM section")
|
||||
|
||||
# Tax payment
|
||||
tax_payment: Optional[bool] = Field(None, description="Tax payment")
|
||||
payment_method: Optional[str] = Field(
|
||||
None, max_length=9, description="Payment method"
|
||||
)
|
||||
igi_amount: Optional[Decimal] = Field(None, description="IGI amount")
|
||||
igi_payment_method: Optional[str] = Field(
|
||||
None, max_length=9, description="IGI payment method"
|
||||
)
|
||||
|
||||
# FCC
|
||||
fcc_key: Optional[str] = Field(None, max_length=30, description="FCC key")
|
||||
|
||||
# Valuation method
|
||||
valuation_method: Optional[str] = Field(
|
||||
None, max_length=2, description="Valuation method"
|
||||
)
|
||||
valuation_determined_value: Optional[Decimal] = Field(
|
||||
None, description="Valuation determined value"
|
||||
)
|
||||
valuation_reason: Optional[str] = Field(
|
||||
None, max_length=500, description="Valuation reason"
|
||||
)
|
||||
|
||||
# Container rules
|
||||
container_rule: Optional[str] = Field(
|
||||
None, max_length=50, description="Container rule"
|
||||
)
|
||||
container_parts_ii: Optional[str] = Field(
|
||||
None, max_length=50, description="Container parts II"
|
||||
)
|
||||
|
||||
# APHIS
|
||||
consecutive_aphis: Optional[int] = Field(None, description="Consecutive APHIS")
|
||||
|
||||
# BOM/Commercial
|
||||
bom_version: Optional[int] = Field(None, description="BOM version")
|
||||
bill_version: Optional[int] = Field(None, description="Bill version")
|
||||
|
||||
# TLCAN value
|
||||
tlcan_value: Optional[Decimal] = Field(None, description="TLCAN value")
|
||||
|
||||
# Identifier
|
||||
identifier: Optional[str] = Field(None, max_length=2, description="Identifier")
|
||||
|
||||
# Validation fields
|
||||
validation_zero: Optional[int] = Field(None, description="Validation zero")
|
||||
validation_one: Optional[int] = Field(None, description="Validation one")
|
||||
|
||||
# Material type
|
||||
material_type: Optional[str] = Field(
|
||||
None, max_length=50, description="Material type"
|
||||
)
|
||||
|
||||
# Order concept
|
||||
order_type: Optional[str] = Field(None, max_length=50, description="Order type")
|
||||
line_concept: Optional[str] = Field(None, max_length=50, description="Line concept")
|
||||
|
||||
# Review dispatch
|
||||
review_dispatch: Optional[str] = Field(
|
||||
None, max_length=10, description="Review dispatch"
|
||||
)
|
||||
|
||||
# Take component from PT
|
||||
take_component_pt: Optional[int] = Field(None, description="Take component from PT")
|
||||
|
||||
# Pallet
|
||||
pallet2: Optional[int] = Field(None, description="Pallet 2")
|
||||
|
||||
# Wildcard field
|
||||
wildcard_field: Optional[str] = Field(
|
||||
None, max_length=100, description="Wildcard field"
|
||||
)
|
||||
|
||||
|
||||
class LineItemCreate(LineItemBase):
|
||||
"""Schema for creating line item with all nested data"""
|
||||
|
||||
# Override base fields - estos se asignan automáticamente en el service
|
||||
item_id: Optional[int] = Field(
|
||||
None, description="ID of the parent item (auto-assigned)"
|
||||
)
|
||||
line_number: Optional[int] = Field(None, description="Line number (auto-assigned)")
|
||||
|
||||
financial: Optional[LineFinancialCreate] = Field(
|
||||
None, description="Financial data for this line"
|
||||
)
|
||||
quantity: Optional[LineQuantityCreate] = Field(
|
||||
None, description="Quantity data for this line"
|
||||
)
|
||||
customs: Optional[LineCustomCreate] = Field(
|
||||
None, description="Customs data for this line"
|
||||
)
|
||||
description: Optional[LineDescriptionCreate] = Field(
|
||||
None, description="Description data for this line"
|
||||
)
|
||||
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):
|
||||
"""Schema for updating line item with all nested data"""
|
||||
|
||||
# Override base fields - todos opcionales en updates
|
||||
item_id: Optional[int] = Field(None, description="ID of the parent item")
|
||||
line_number: Optional[int] = Field(None, description="Line number")
|
||||
financial: Optional[LineFinancialUpdate] = Field(
|
||||
None, description="Financial data for this line"
|
||||
)
|
||||
quantity: Optional[LineQuantityUpdate] = Field(
|
||||
None, description="Quantity data for this line"
|
||||
)
|
||||
customs: Optional[LineCustomUpdate] = Field(
|
||||
None, description="Customs data for this line"
|
||||
)
|
||||
description: Optional[LineDescriptionUpdate] = Field(
|
||||
None, description="Description data for this line"
|
||||
)
|
||||
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):
|
||||
"""Schema for line item response with all nested data"""
|
||||
|
||||
id: int
|
||||
item_id: int
|
||||
financial: Optional[LineFinancialResponse] = None
|
||||
quantity: Optional[LineQuantityResponse] = None
|
||||
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)
|
||||
|
||||
# Map model field names to schema field names for aliased fields
|
||||
if hasattr(data, "part_number"):
|
||||
result["part_number_id"] = data.part_number
|
||||
if hasattr(data, "component_part_number"):
|
||||
result["component_part_number_id"] = data.component_part_number
|
||||
|
||||
# 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
|
||||
@@ -7,7 +7,7 @@ from core.database import Base
|
||||
from api.v1.modules.a76.general_catalogs.packages.models import Package
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..line_items.models import LineItem
|
||||
from ..models import LineItem
|
||||
|
||||
class LineQuantity(Base):
|
||||
"""
|
||||
|
||||
@@ -4,7 +4,7 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..line_items.models import LineItem
|
||||
from ..models import LineItem
|
||||
|
||||
class LineReference(Base):
|
||||
"""
|
||||
|
||||
@@ -3,26 +3,45 @@ Normalized Database Schema for SCAF (Fixed Assets) and SCAII (Parts Inventory)
|
||||
SQLAlchemy v2 - Annex 24 Compliance
|
||||
"""
|
||||
|
||||
from typing import Optional, TYPE_CHECKING, List
|
||||
from sqlalchemy import Boolean, String, Integer, ForeignKey
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from core.database import Base
|
||||
from decimal import Decimal
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
String,
|
||||
Integer,
|
||||
Numeric,
|
||||
SmallInteger,
|
||||
ForeignKey
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .line_items.models import LineItem
|
||||
from api.v1.modules.a76.classes.models import Class
|
||||
from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .line_financials.models import LineFinancial
|
||||
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 api.v1.modules.a76.parts.models import Part
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader
|
||||
|
||||
# ============================================================================
|
||||
# CORE ENTITIES
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class Item(Base, TenantScopedMixin, TimestampMixin):
|
||||
class LineItem(Base, TenantScopedMixin, TimestampMixin):
|
||||
"""
|
||||
Unified item header table for all import/export operations
|
||||
Consolidates headers from both SCAF and SCAII systems
|
||||
"""
|
||||
__tablename__ = "items"
|
||||
__tablename__ = "item_lines"
|
||||
__table_args__ = {
|
||||
"schema": "a76",
|
||||
}
|
||||
@@ -30,6 +49,129 @@ class Item(Base, TenantScopedMixin, TimestampMixin):
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
invoice_id: Mapped[int] = mapped_column(ForeignKey("a76.invoice_header.id")) # CONSECUTIVO
|
||||
|
||||
line_number: Mapped[int] = mapped_column(Integer) # LINEAIMPO/LINEAEXPO/LINEA
|
||||
|
||||
# Part identification
|
||||
part_number_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey("a76.parts.id")
|
||||
) # NUMPARTE
|
||||
component_part_number_id: Mapped[Optional[int]] = mapped_column(
|
||||
Integer, ForeignKey("a76.parts.id")
|
||||
) # NUMPARTECOM
|
||||
class_id: Mapped[Optional[int]] = mapped_column(
|
||||
ForeignKey("a76.classes.id")
|
||||
) # CLASE
|
||||
|
||||
# Unit of measure
|
||||
unit_of_measure: Mapped[Optional[int]] = mapped_column(
|
||||
ForeignKey("a76.units_of_measure.id")
|
||||
) # UNIDADMEDIDA/UNIMED
|
||||
alternate_unit: Mapped[Optional[int]] = mapped_column(
|
||||
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
|
||||
|
||||
# Permits and certificates
|
||||
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
|
||||
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
|
||||
|
||||
# FDA
|
||||
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
|
||||
|
||||
# IV32 (Tax identification)
|
||||
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
|
||||
|
||||
# Tax payment
|
||||
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
|
||||
igi_payment_method: Mapped[Optional[str]] = mapped_column(
|
||||
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_determined_value: Mapped[Optional[Decimal]] = mapped_column(
|
||||
Numeric(29, 8)
|
||||
) # METVALORVALORDETERMINADO/METVALORACIONVALORDETERMINADO
|
||||
valuation_reason: Mapped[Optional[str]] = mapped_column(
|
||||
String(500)
|
||||
) # METVALORMOTIVODEUSO/METVALORACIONMOTIVODEUSO
|
||||
|
||||
# Container rules
|
||||
container_rule: Mapped[Optional[str]] = mapped_column(String(50)) # CONTENEDORREGLA
|
||||
container_parts_ii: Mapped[Optional[str]] = mapped_column(
|
||||
String(50)
|
||||
) # CONTENEDORPARTESII
|
||||
|
||||
# APHIS
|
||||
consecutive_aphis: Mapped[Optional[int]] = mapped_column(
|
||||
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
|
||||
|
||||
# Identifier
|
||||
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
|
||||
|
||||
# Material type
|
||||
material_type: Mapped[Optional[str]] = mapped_column(
|
||||
String(50)
|
||||
) # TIPOMAT/TIPODENUMPARTE
|
||||
|
||||
# Order concept
|
||||
order_type: Mapped[Optional[str]] = mapped_column(String(50)) # TIPODEORDEN
|
||||
line_concept: Mapped[Optional[str]] = mapped_column(
|
||||
String(50)
|
||||
) # CONCEPTODELAPARTIDA
|
||||
|
||||
# Review dispatch
|
||||
review_dispatch: Mapped[Optional[str]] = mapped_column(String(10)) # REVISARDESP
|
||||
|
||||
# Take component from PT
|
||||
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
|
||||
|
||||
# Item references
|
||||
reference_number: Mapped[Optional[str]] = mapped_column(
|
||||
String(20)) # NUMREFERENCIA
|
||||
@@ -48,12 +190,46 @@ class Item(Base, TenantScopedMixin, TimestampMixin):
|
||||
warehouse: Mapped[Optional[str]] = mapped_column(String(30)) # BODEGA
|
||||
location: Mapped[Optional[str]] = mapped_column(
|
||||
String(200)) # LOCALIZACION
|
||||
|
||||
# Relationships (one-to-many)
|
||||
lines: Mapped[List["LineItem"]] = relationship(
|
||||
"LineItem", back_populates="item", cascade="all, delete-orphan")
|
||||
|
||||
invoice: Mapped["InvoiceHeader"] = relationship("InvoiceHeader")
|
||||
|
||||
# Relationships
|
||||
financial: Mapped[Optional["LineFinancial"]] = relationship(
|
||||
back_populates="line", cascade="all, delete-orphan", uselist=False
|
||||
)
|
||||
quantity: Mapped[Optional["LineQuantity"]] = relationship(
|
||||
back_populates="line", cascade="all, delete-orphan", uselist=False
|
||||
)
|
||||
customs: Mapped[Optional["LineCustom"]] = relationship(
|
||||
back_populates="line", cascade="all, delete-orphan", uselist=False
|
||||
)
|
||||
description: Mapped[Optional["LineDescription"]] = relationship(
|
||||
back_populates="line", cascade="all, delete-orphan", uselist=False
|
||||
)
|
||||
reference: Mapped[Optional["LineReference"]] = relationship(
|
||||
back_populates="line", cascade="all, delete-orphan", uselist=False
|
||||
)
|
||||
class_info: Mapped[Optional["Class"]] = relationship(
|
||||
"api.v1.modules.a76.classes.models.Class",
|
||||
foreign_keys=[class_id],
|
||||
viewonly=True,
|
||||
)
|
||||
unit_of_measure_info: Mapped[Optional["UnitOfMeasure"]] = relationship(
|
||||
"api.v1.modules.a76.general_catalogs.units_of_measure.models.UnitOfMeasure",
|
||||
foreign_keys=[unit_of_measure],
|
||||
viewonly=True,
|
||||
)
|
||||
fa_data: Mapped[Optional["FaLineItem"]] = relationship(
|
||||
"FaLineItem",
|
||||
back_populates="master_info",
|
||||
cascade="all, delete-orphan",
|
||||
uselist=False,
|
||||
)
|
||||
part_info: Mapped[Optional["Part"]] = relationship(
|
||||
"Part",
|
||||
foreign_keys=[part_number_id],
|
||||
viewonly=True,
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# SUPPORTING TABLES
|
||||
@@ -189,8 +365,8 @@ MIGRATION STRATEGY FROM ORIGINAL TABLES TO NORMALIZED SCHEMA:
|
||||
7. QUERYING EXAMPLES:
|
||||
```python
|
||||
# Get all imports (both systems)
|
||||
session.query(Item).filter(
|
||||
Item.item_type.in_(['IMPORT', 'EQUIPMENT_IMPORT_TEMP', 'EQUIPMENT_IMPORT_DEF'])
|
||||
session.query(LineItem).filter(
|
||||
LineItem.item_type.in_(['IMPORT', 'EQUIPMENT_IMPORT_TEMP', 'EQUIPMENT_IMPORT_DEF'])
|
||||
)
|
||||
|
||||
# Get all lines for a specific part across all items
|
||||
@@ -199,8 +375,8 @@ MIGRATION STRATEGY FROM ORIGINAL TABLES TO NORMALIZED SCHEMA:
|
||||
)
|
||||
|
||||
# Get SCAF equipment with depreciation
|
||||
session.query(Item).join(LineItem).filter(
|
||||
Item.system_origin == 'SCAF',
|
||||
session.query(LineItem).join(LineItem).filter(
|
||||
LineItem.system_origin == 'SCAF',
|
||||
LineItem.value_depreciated_usd.isnot(None)
|
||||
)
|
||||
```
|
||||
|
||||
@@ -11,10 +11,10 @@ from core.database import get_core_db
|
||||
from core.security import get_current_user, validate_access_to_resource
|
||||
|
||||
from .schemas import (
|
||||
ItemCreate,
|
||||
ItemUpdate,
|
||||
ItemResponse,
|
||||
ItemListResponse,
|
||||
LineItemCreate,
|
||||
LineItemUpdate,
|
||||
LineItemResponse,
|
||||
LineItemListResponse,
|
||||
)
|
||||
from .service import ItemService
|
||||
|
||||
@@ -25,9 +25,9 @@ router = APIRouter(prefix="/items", tags=["Items"])
|
||||
# ITEM CRUD ENDPOINTS
|
||||
# ============================================================================
|
||||
|
||||
@router.post("/", response_model=ItemResponse, status_code=status.HTTP_201_CREATED)
|
||||
@router.post("/", response_model=LineItemResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_item(
|
||||
item_data: ItemCreate,
|
||||
item_data: LineItemCreate,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
@@ -36,7 +36,6 @@ async def create_item(
|
||||
Create a new item with multiple line items and their nested data
|
||||
|
||||
The item follows a one-to-many relationship structure:
|
||||
- Item has many LineItems
|
||||
- Each LineItem has one LineFinancial
|
||||
- Each LineItem has one LineQuantity
|
||||
- Each LineItem has one LineCustoms
|
||||
@@ -49,7 +48,7 @@ async def create_item(
|
||||
return service.create(db, item_data, tenant_id, company_id)
|
||||
|
||||
|
||||
@router.get("/{item_id}", response_model=ItemResponse)
|
||||
@router.get("/{item_id}", response_model=LineItemResponse)
|
||||
async def get_item(
|
||||
item_id: int = Path(..., description="Item ID"),
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
@@ -70,7 +69,7 @@ async def get_item(
|
||||
return item
|
||||
|
||||
|
||||
@router.get("/", response_model=ItemListResponse)
|
||||
@router.get("/", response_model=LineItemListResponse)
|
||||
async def list_items(
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
skip: int = Query(0, ge=0, description="Number of records to skip"),
|
||||
@@ -109,7 +108,7 @@ async def list_items(
|
||||
items, total = service.get_all(
|
||||
db, tenant_id, company_id, skip, limit, filters)
|
||||
|
||||
return ItemListResponse(
|
||||
return LineItemListResponse(
|
||||
total=total,
|
||||
items=items,
|
||||
skip=skip,
|
||||
@@ -117,10 +116,10 @@ async def list_items(
|
||||
)
|
||||
|
||||
|
||||
@router.put("/{item_id}", response_model=ItemResponse)
|
||||
@router.put("/{item_id}", response_model=LineItemResponse)
|
||||
async def update_item(
|
||||
item_id: int = Path(..., description="Item ID"),
|
||||
item_data: ItemUpdate = ...,
|
||||
item_data: LineItemUpdate = ...,
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
@@ -168,7 +167,7 @@ async def delete_item(
|
||||
# ADDITIONAL ENDPOINTS FOR INVOICE
|
||||
# ============================================================================
|
||||
|
||||
@router.get("/invoice/{invoice_id}/items", response_model=ItemListResponse)
|
||||
@router.get("/invoice/{invoice_id}/items", response_model=LineItemListResponse)
|
||||
async def get_items_by_invoice(
|
||||
invoice_id: int = Path(..., description="Invoice ID"),
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
@@ -186,7 +185,7 @@ async def get_items_by_invoice(
|
||||
items, total = service.get_by_invoice(
|
||||
db, invoice_id, tenant_id, company_id, skip, limit)
|
||||
|
||||
return ItemListResponse(
|
||||
return LineItemListResponse(
|
||||
total=total,
|
||||
items=items,
|
||||
skip=skip,
|
||||
|
||||
@@ -1,19 +1,46 @@
|
||||
"""
|
||||
Schemas for Items and related entities
|
||||
Complete nested one-to-one structure:
|
||||
Item -> LineItem -> LineFinancial -> LineQuantity -> LineCustoms -> LineDescription -> LineReference
|
||||
LineItem -> LineFinancial -> LineQuantity -> LineCustoms -> LineDescription -> LineReference
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from typing import Any, Optional
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
from pydantic import BaseModel, Field, ConfigDict, model_validator
|
||||
|
||||
# Import schemas from individual modules
|
||||
from .line_items.schemas import (
|
||||
LineItemCreate,
|
||||
LineItemUpdate,
|
||||
LineItemResponse
|
||||
# Import nested schemas
|
||||
from .line_customs.schemas import (
|
||||
LineCustomCreate,
|
||||
LineCustomUpdate,
|
||||
LineCustomResponse,
|
||||
)
|
||||
from .line_descriptions.schemas import (
|
||||
LineDescriptionCreate,
|
||||
LineDescriptionUpdate,
|
||||
LineDescriptionResponse,
|
||||
)
|
||||
from .line_quantities.schemas import (
|
||||
LineQuantityCreate,
|
||||
LineQuantityUpdate,
|
||||
LineQuantityResponse,
|
||||
)
|
||||
from .line_financials.schemas import (
|
||||
LineFinancialCreate,
|
||||
LineFinancialUpdate,
|
||||
LineFinancialResponse,
|
||||
)
|
||||
from .line_references.schemas import (
|
||||
LineReferenceCreate,
|
||||
LineReferenceUpdate,
|
||||
LineReferenceResponse,
|
||||
)
|
||||
|
||||
from api.v1.modules.a24.fa.fa_item_lines.dto import (
|
||||
FaLineItemCreateDTO,
|
||||
FaLineItemUpdateDTO,
|
||||
FaLineItemResponseDTO,
|
||||
)
|
||||
|
||||
|
||||
@@ -21,53 +48,294 @@ from .line_items.schemas import (
|
||||
# ITEM SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
class ItemBase(BaseModel):
|
||||
|
||||
class LineItemBase(BaseModel):
|
||||
"""Base schema for items"""
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
invoice_id: int = Field(..., description="Invoice ID")
|
||||
line_number: int = Field(..., description="Line number")
|
||||
|
||||
# Part identification
|
||||
part_number_id: Optional[int] = Field(
|
||||
None,
|
||||
description="Part number",
|
||||
alias="part_number",
|
||||
serialization_alias="part_number_id",
|
||||
)
|
||||
component_part_number_id: Optional[int] = Field(
|
||||
None,
|
||||
description="Component part number",
|
||||
alias="component_part_number",
|
||||
serialization_alias="component_part_number_id",
|
||||
)
|
||||
class_id: Optional[int] = Field(None, description="Class code")
|
||||
|
||||
# Unit of measure
|
||||
unit_of_measure: Optional[int] = Field(None, description="Unit of measure")
|
||||
alternate_unit: Optional[int] = Field(None, description="Alternate unit")
|
||||
uma_key: Optional[str] = Field(None, max_length=2, description="UMA key")
|
||||
auxiliary_unit: Optional[str] = Field(
|
||||
None, max_length=5, description="Auxiliary unit"
|
||||
)
|
||||
|
||||
# Permits and certificates
|
||||
permit_number: Optional[str] = Field(
|
||||
None, max_length=20, description="Permit number"
|
||||
)
|
||||
page_line: Optional[str] = Field(None, max_length=10, description="Page line")
|
||||
has_certificate: Optional[bool] = Field(None, description="Has certificate")
|
||||
certificate_number: Optional[str] = Field(
|
||||
None, max_length=10, description="Certificate number"
|
||||
)
|
||||
octave_permit: Optional[str] = Field(
|
||||
None, max_length=20, description="Octave permit"
|
||||
)
|
||||
permits_ped: Optional[str] = Field(None, max_length=500, description="PED permits")
|
||||
|
||||
# FDA
|
||||
has_fda_code: Optional[bool] = Field(None, description="Has FDA code")
|
||||
fda_key: Optional[str] = Field(None, max_length=10, description="FDA key")
|
||||
|
||||
# Special flags
|
||||
is_military_mcia: Optional[bool] = Field(
|
||||
None, description="Is military merchandise"
|
||||
)
|
||||
|
||||
# IV32
|
||||
iv32_type_key: Optional[str] = Field(
|
||||
None, max_length=5, description="IV32 type key"
|
||||
)
|
||||
iv32_number: Optional[str] = Field(None, max_length=35, description="IV32 number")
|
||||
|
||||
# Export specific
|
||||
scrap_invoice: Optional[str] = Field(
|
||||
None, max_length=15, description="Scrap invoice"
|
||||
)
|
||||
consecutive_destination: Optional[int] = Field(
|
||||
None, description="Consecutive destination"
|
||||
)
|
||||
ctm_section: Optional[str] = Field(None, max_length=3, description="CTM section")
|
||||
|
||||
# Tax payment
|
||||
tax_payment: Optional[bool] = Field(None, description="Tax payment")
|
||||
payment_method: Optional[str] = Field(
|
||||
None, max_length=9, description="Payment method"
|
||||
)
|
||||
igi_amount: Optional[Decimal] = Field(None, description="IGI amount")
|
||||
igi_payment_method: Optional[str] = Field(
|
||||
None, max_length=9, description="IGI payment method"
|
||||
)
|
||||
|
||||
# FCC
|
||||
fcc_key: Optional[str] = Field(None, max_length=30, description="FCC key")
|
||||
|
||||
# Valuation method
|
||||
valuation_method: Optional[str] = Field(
|
||||
None, max_length=2, description="Valuation method"
|
||||
)
|
||||
valuation_determined_value: Optional[Decimal] = Field(
|
||||
None, description="Valuation determined value"
|
||||
)
|
||||
valuation_reason: Optional[str] = Field(
|
||||
None, max_length=500, description="Valuation reason"
|
||||
)
|
||||
|
||||
# Container rules
|
||||
container_rule: Optional[str] = Field(
|
||||
None, max_length=50, description="Container rule"
|
||||
)
|
||||
container_parts_ii: Optional[str] = Field(
|
||||
None, max_length=50, description="Container parts II"
|
||||
)
|
||||
|
||||
# APHIS
|
||||
consecutive_aphis: Optional[int] = Field(None, description="Consecutive APHIS")
|
||||
|
||||
# BOM/Commercial
|
||||
bom_version: Optional[int] = Field(None, description="BOM version")
|
||||
bill_version: Optional[int] = Field(None, description="Bill version")
|
||||
|
||||
# TLCAN value
|
||||
tlcan_value: Optional[Decimal] = Field(None, description="TLCAN value")
|
||||
|
||||
# Identifier
|
||||
identifier: Optional[str] = Field(None, max_length=2, description="Identifier")
|
||||
|
||||
# Validation fields
|
||||
validation_zero: Optional[int] = Field(None, description="Validation zero")
|
||||
validation_one: Optional[int] = Field(None, description="Validation one")
|
||||
|
||||
# Material type
|
||||
material_type: Optional[str] = Field(
|
||||
None, max_length=50, description="Material type"
|
||||
)
|
||||
|
||||
# Order concept
|
||||
order_type: Optional[str] = Field(None, max_length=50, description="Order type")
|
||||
line_concept: Optional[str] = Field(None, max_length=50, description="Line concept")
|
||||
|
||||
# Review dispatch
|
||||
review_dispatch: Optional[str] = Field(
|
||||
None, max_length=10, description="Review dispatch"
|
||||
)
|
||||
|
||||
# Take component from PT
|
||||
take_component_pt: Optional[int] = Field(None, description="Take component from PT")
|
||||
|
||||
# Pallet
|
||||
pallet2: Optional[int] = Field(None, description="Pallet 2")
|
||||
|
||||
# Wildcard field
|
||||
wildcard_field: Optional[str] = Field(
|
||||
None, max_length=100, description="Wildcard field"
|
||||
)
|
||||
reference_number: Optional[str] = Field(
|
||||
None, max_length=20, description="Reference number")
|
||||
None, max_length=20, description="Reference number"
|
||||
)
|
||||
order: Optional[str] = Field(None, max_length=50, description="Order")
|
||||
guide_number: Optional[str] = Field(
|
||||
None, max_length=50, description="Guide number")
|
||||
guide_number: Optional[str] = Field(None, max_length=50, description="Guide number")
|
||||
|
||||
# Dates
|
||||
depreciation_date: Optional[int] = Field(
|
||||
None, description="Depreciation date")
|
||||
depreciation_date: Optional[int] = Field(None, description="Depreciation date")
|
||||
|
||||
# Administrative fields
|
||||
rectification: Optional[int] = Field(None, description="Rectification")
|
||||
warehouse: Optional[str] = Field(
|
||||
None, max_length=30, description="Warehouse")
|
||||
location: Optional[str] = Field(
|
||||
None, max_length=200, description="Location")
|
||||
warehouse: Optional[str] = Field(None, max_length=30, description="Warehouse")
|
||||
location: Optional[str] = Field(None, max_length=200, description="Location")
|
||||
|
||||
|
||||
class ItemCreate(ItemBase):
|
||||
class LineItemCreate(LineItemBase):
|
||||
"""Schema for creating item with nested lines (one-to-many)"""
|
||||
lines: Optional[list[LineItemCreate]] = Field(
|
||||
default=[], description="List of line items")
|
||||
|
||||
# Override base fields - estos se asignan automáticamente en el service
|
||||
line_number: Optional[int] = Field(None, description="Line number (auto-assigned)")
|
||||
|
||||
financial: Optional[LineFinancialCreate] = Field(
|
||||
None, description="Financial data for this line"
|
||||
)
|
||||
quantity: Optional[LineQuantityCreate] = Field(
|
||||
None, description="Quantity data for this line"
|
||||
)
|
||||
customs: Optional[LineCustomCreate] = Field(
|
||||
None, description="Customs data for this line"
|
||||
)
|
||||
description: Optional[LineDescriptionCreate] = Field(
|
||||
None, description="Description data for this line"
|
||||
)
|
||||
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 ItemUpdate(ItemBase):
|
||||
class LineItemUpdate(LineItemBase):
|
||||
"""Schema for updating item"""
|
||||
|
||||
invoice_id: Optional[int] = Field(None, description="Invoice ID")
|
||||
lines: Optional[list[LineItemUpdate]] = Field(
|
||||
None, description="List of line items to update")
|
||||
# Override base fields - todos opcionales en updates
|
||||
line_number: Optional[int] = Field(None, description="Line number")
|
||||
financial: Optional[LineFinancialUpdate] = Field(
|
||||
None, description="Financial data for this line"
|
||||
)
|
||||
quantity: Optional[LineQuantityUpdate] = Field(
|
||||
None, description="Quantity data for this line"
|
||||
)
|
||||
customs: Optional[LineCustomUpdate] = Field(
|
||||
None, description="Customs data for this line"
|
||||
)
|
||||
description: Optional[LineDescriptionUpdate] = Field(
|
||||
None, description="Description data for this line"
|
||||
)
|
||||
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 ItemResponse(ItemBase):
|
||||
"""Schema for item response with nested data (one-to-many)"""
|
||||
class LineItemResponse(BaseModel):
|
||||
"""Schema for single item response"""
|
||||
|
||||
id: int
|
||||
lines: list[LineItemResponse] = Field(
|
||||
default=[], description="List of line items")
|
||||
invoice_id: int
|
||||
line_number: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
# Part identification
|
||||
part_number_id: Optional[int] = Field(
|
||||
None, alias="part_number", serialization_alias="part_number_id"
|
||||
)
|
||||
component_part_number_id: Optional[int] = Field(
|
||||
None,
|
||||
alias="component_part_number",
|
||||
serialization_alias="component_part_number_id",
|
||||
)
|
||||
class_id: Optional[int] = None
|
||||
|
||||
# Nested data
|
||||
financial: Optional[LineFinancialResponse] = None
|
||||
quantity: Optional[LineQuantityResponse] = None
|
||||
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
|
||||
|
||||
# Additional fields that might be present
|
||||
reference_number: Optional[str] = None
|
||||
order: Optional[str] = None
|
||||
warehouse: Optional[str] = None
|
||||
location: Optional[str] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True, populate_by_name=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)
|
||||
|
||||
# Map model field names to schema field names for aliased fields
|
||||
if hasattr(data, "part_number"):
|
||||
result["part_number_id"] = data.part_number
|
||||
if hasattr(data, "component_part_number"):
|
||||
result["component_part_number_id"] = data.component_part_number
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
class ItemListResponse(BaseModel):
|
||||
class LineItemListResponse(BaseModel):
|
||||
"""Schema for paginated item list"""
|
||||
|
||||
total: int = Field(..., description="Total number of items")
|
||||
items: list[ItemResponse] = Field(..., description="List of items")
|
||||
items: list[LineItemResponse] = Field(..., description="List of items")
|
||||
skip: int = Field(..., description="Number of skipped items")
|
||||
limit: int = Field(..., description="Maximum items per page")
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
"""
|
||||
Service layer for Items business logic
|
||||
Handles CRUD operations for Item with complete one-to-one relationships:
|
||||
Item -> LineItem -> LineFinancial
|
||||
-> LineQuantity
|
||||
-> LineCustoms
|
||||
-> LineDescription
|
||||
-> LineReference
|
||||
-> FaLineItem (Fixed Assets - a24)
|
||||
Handles CRUD operations for LineItem with complete one-to-one relationships:
|
||||
LineItem -> LineFinancial
|
||||
-> LineQuantity
|
||||
-> LineCustoms
|
||||
-> LineDescription
|
||||
-> LineReference
|
||||
-> FaLineItem (Fixed Assets - a24)
|
||||
|
||||
After refactoring: LineItem is the main entity, representing a single line item in an invoice.
|
||||
There is no intermediate Item entity anymore. Each LineItem belongs directly to an InvoiceHeader.
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -24,17 +27,14 @@ from core.exceptions import ErrorCollector
|
||||
from .imports.temporary.validators.create import validate_create
|
||||
from .imports.temporary.validators.update import validate_update
|
||||
|
||||
from api.v1.modules.a76.items.line_items.schemas import LineItemCreate, LineItemUpdate
|
||||
|
||||
from .schemas import ItemCreate, ItemUpdate
|
||||
from .line_items.models import LineItem
|
||||
from .schemas import LineItemCreate, LineItemUpdate
|
||||
from .line_financials.models import LineFinancial
|
||||
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
|
||||
from .models import LineItem
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -52,8 +52,7 @@ class ItemService:
|
||||
|
||||
max_line = (
|
||||
db.query(func.max(LineItem.line_number))
|
||||
.join(Item, LineItem.item_id == Item.id)
|
||||
.filter(Item.invoice_id == invoice_id)
|
||||
.filter(LineItem.invoice_id == invoice_id)
|
||||
.scalar()
|
||||
)
|
||||
|
||||
@@ -62,12 +61,15 @@ class ItemService:
|
||||
@staticmethod
|
||||
def _renumber_all_invoice_lines(db: Session, invoice_id: int) -> None:
|
||||
"""Renumber all line_items for a given invoice to be consecutive (1, 2, 3, ...)."""
|
||||
items = db.query(Item).filter(Item.invoice_id == invoice_id).all()
|
||||
all_lines = [line for item in items for line in item.lines]
|
||||
all_lines.sort(key=lambda x: x.line_number if x.line_number else 0)
|
||||
items = (
|
||||
db.query(LineItem)
|
||||
.filter(LineItem.invoice_id == invoice_id)
|
||||
.order_by(LineItem.line_number)
|
||||
.all()
|
||||
)
|
||||
|
||||
for idx, line in enumerate(all_lines, start=1):
|
||||
line.line_number = idx
|
||||
for idx, item in enumerate(items, start=1):
|
||||
item.line_number = idx
|
||||
|
||||
@staticmethod
|
||||
def _lock_invoice(
|
||||
@@ -143,24 +145,24 @@ class ItemService:
|
||||
@staticmethod
|
||||
def get_by_id(
|
||||
db: Session, item_id: int, tenant_id: int, company_id: int
|
||||
) -> Optional[Item]:
|
||||
) -> Optional[LineItem]:
|
||||
"""Get an item by ID with tenant/company validation"""
|
||||
return (
|
||||
db.query(Item)
|
||||
db.query(LineItem)
|
||||
.options(
|
||||
joinedload(Item.lines).joinedload(LineItem.financial),
|
||||
joinedload(Item.lines).joinedload(LineItem.quantity),
|
||||
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),
|
||||
joinedload(LineItem.financial),
|
||||
joinedload(LineItem.quantity),
|
||||
joinedload(LineItem.customs),
|
||||
joinedload(LineItem.description),
|
||||
joinedload(LineItem.reference),
|
||||
joinedload(LineItem.class_info),
|
||||
joinedload(LineItem.unit_of_measure_info),
|
||||
joinedload(LineItem.fa_data),
|
||||
)
|
||||
.filter(
|
||||
Item.id == item_id,
|
||||
Item.tenant_id == tenant_id,
|
||||
Item.company_id == company_id,
|
||||
LineItem.id == item_id,
|
||||
LineItem.tenant_id == tenant_id,
|
||||
LineItem.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
@@ -173,42 +175,42 @@ class ItemService:
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
filters: Optional[dict] = None,
|
||||
) -> Tuple[List[Item], int]:
|
||||
) -> Tuple[List[LineItem], int]:
|
||||
"""Get all items for a tenant/company with pagination and optional filters"""
|
||||
query = (
|
||||
db.query(Item)
|
||||
db.query(LineItem)
|
||||
.options(
|
||||
joinedload(Item.lines).joinedload(LineItem.financial),
|
||||
joinedload(Item.lines).joinedload(LineItem.quantity),
|
||||
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),
|
||||
joinedload(LineItem.financial),
|
||||
joinedload(LineItem.quantity),
|
||||
joinedload(LineItem.customs),
|
||||
joinedload(LineItem.description),
|
||||
joinedload(LineItem.reference),
|
||||
joinedload(LineItem.class_info),
|
||||
joinedload(LineItem.unit_of_measure_info),
|
||||
joinedload(LineItem.fa_data),
|
||||
)
|
||||
.filter(
|
||||
Item.tenant_id == tenant_id,
|
||||
Item.company_id == company_id,
|
||||
LineItem.tenant_id == tenant_id,
|
||||
LineItem.company_id == company_id,
|
||||
)
|
||||
)
|
||||
|
||||
# Apply filters if provided
|
||||
if filters:
|
||||
if filters.get("invoice_id"):
|
||||
query = query.filter(Item.invoice_id == filters["invoice_id"])
|
||||
query = query.filter(LineItem.invoice_id == filters["invoice_id"])
|
||||
if filters.get("item_type"):
|
||||
query = query.filter(Item.item_type == filters["item_type"])
|
||||
query = query.filter(LineItem.item_type == filters["item_type"])
|
||||
if filters.get("system_origin"):
|
||||
query = query.filter(Item.system_origin == filters["system_origin"])
|
||||
query = query.filter(LineItem.system_origin == filters["system_origin"])
|
||||
if filters.get("search"):
|
||||
search_term = f"%{filters['search']}%"
|
||||
query = query.filter(
|
||||
or_(
|
||||
Item.invoice_id.ilike(search_term),
|
||||
Item.reference_number.ilike(search_term),
|
||||
Item.order.ilike(search_term),
|
||||
Item.guide_number.ilike(search_term),
|
||||
LineItem.invoice_id.ilike(search_term),
|
||||
LineItem.reference_number.ilike(search_term),
|
||||
LineItem.order.ilike(search_term),
|
||||
LineItem.guide_number.ilike(search_term),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -224,22 +226,22 @@ class ItemService:
|
||||
company_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
) -> Tuple[List[Item], int]:
|
||||
) -> Tuple[List[LineItem], int]:
|
||||
"""Get all items for a specific invoice"""
|
||||
query = (
|
||||
db.query(Item)
|
||||
db.query(LineItem)
|
||||
.options(
|
||||
joinedload(Item.lines).joinedload(LineItem.financial),
|
||||
joinedload(Item.lines).joinedload(LineItem.quantity),
|
||||
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),
|
||||
joinedload(LineItem.financial),
|
||||
joinedload(LineItem.quantity),
|
||||
joinedload(LineItem.customs),
|
||||
joinedload(LineItem.description),
|
||||
joinedload(LineItem.reference),
|
||||
joinedload(LineItem.fa_data),
|
||||
)
|
||||
.filter(
|
||||
Item.invoice_id == invoice_id,
|
||||
Item.tenant_id == tenant_id,
|
||||
Item.company_id == company_id,
|
||||
LineItem.invoice_id == invoice_id,
|
||||
LineItem.tenant_id == tenant_id,
|
||||
LineItem.company_id == company_id,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -250,10 +252,10 @@ class ItemService:
|
||||
@staticmethod
|
||||
def create(
|
||||
db: Session,
|
||||
item_data: ItemCreate,
|
||||
item_data: LineItemCreate,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
) -> Item:
|
||||
) -> LineItem:
|
||||
"""Create a new item with all related nested data (multiple lines)"""
|
||||
|
||||
# Validaciones con ErrorCollector
|
||||
@@ -271,109 +273,74 @@ class ItemService:
|
||||
if not invoice_updated(db, item_data.invoice_id, tenant_id, company_id, errors):
|
||||
errors.raise_if_errors("Error al crear el item")
|
||||
|
||||
# Lock invoice and pre-calculate line_numbers
|
||||
# Lock invoice and calculate line number
|
||||
if not ItemService._lock_invoice(
|
||||
db, item_data.invoice_id, tenant_id, company_id, errors
|
||||
):
|
||||
errors.raise_if_errors("Error al crear el item")
|
||||
|
||||
line_numbers = []
|
||||
if item_data.lines:
|
||||
starting_line = ItemService._get_next_line_number(db, item_data.invoice_id)
|
||||
line_numbers = [starting_line + i for i in range(len(item_data.lines))]
|
||||
# Calculate the next line number for this single item
|
||||
line_number = ItemService._get_next_line_number(db, item_data.invoice_id)
|
||||
|
||||
# Validar cada line item que se va a crear
|
||||
if item_data.lines:
|
||||
for idx, line_data in enumerate(item_data.lines):
|
||||
line_number = line_numbers[idx] # Usar el line_number calculado
|
||||
# Validar el item
|
||||
validate_create(
|
||||
db,
|
||||
item_data, # Schema Pydantic completo
|
||||
tenant_id,
|
||||
company_id,
|
||||
errors,
|
||||
line_number,
|
||||
)
|
||||
|
||||
validate_create(
|
||||
db,
|
||||
line_data, # Schema Pydantic completo
|
||||
item_data.invoice_id, # invoice_id
|
||||
tenant_id,
|
||||
company_id,
|
||||
errors,
|
||||
line_number,
|
||||
)
|
||||
# Validaciones adicionales específicas del negocio
|
||||
if item_data.fa_data and item_data.fa_data.is_subitem is None:
|
||||
errors.add_required_error(field=f"fa_data.is_subitem")
|
||||
|
||||
# Validaciones adicionales específicas del negocio
|
||||
if line_data.fa_data and line_data.fa_data.is_subitem is None:
|
||||
errors.add_required_error(
|
||||
field=f"lines[{line_number}].fa_data.is_subitem"
|
||||
)
|
||||
if item_data.fa_data and item_data.fa_data.subitem_number is None:
|
||||
errors.add_required_error(field=f"fa_data.subitem_number")
|
||||
|
||||
if line_data.fa_data and line_data.fa_data.subitem_number is None:
|
||||
errors.add_required_error(
|
||||
field=f"lines[{line_number}].fa_data.subitem_number"
|
||||
)
|
||||
|
||||
# Validar apóstrofes en número de parte
|
||||
if line_data.part_number_id and "'" in str(line_data.part_number_id):
|
||||
errors.add_error(
|
||||
field=f"lines[{line_number}].part_number",
|
||||
message=f"Advertencia: El Número de Parte contiene apóstrofes y serán omitidos",
|
||||
code="WARNING_APOSTROPHE",
|
||||
)
|
||||
# Validar apóstrofes en número de parte
|
||||
if item_data.part_number_id and "'" in str(item_data.part_number_id):
|
||||
errors.add_error(
|
||||
field=f"part_number",
|
||||
message=f"Advertencia: El Número de Parte contiene apóstrofes y serán omitidos",
|
||||
code="WARNING_APOSTROPHE",
|
||||
)
|
||||
|
||||
# Si hay errores, lanzar excepción ANTES de intentar crear
|
||||
errors.raise_if_errors("Error al crear el item")
|
||||
|
||||
try:
|
||||
# Extract lines data
|
||||
lines_data = item_data.lines or []
|
||||
item_dict = item_data.model_dump(exclude={"lines"})
|
||||
# Prepare item data
|
||||
item_dict = item_data.model_dump(
|
||||
exclude={
|
||||
"financial",
|
||||
"quantity",
|
||||
"customs",
|
||||
"description",
|
||||
"reference",
|
||||
"fa_data",
|
||||
}
|
||||
)
|
||||
|
||||
# Add tenant and company
|
||||
item_dict["tenant_id"] = tenant_id
|
||||
item_dict["company_id"] = company_id
|
||||
# Add tenant, company and line number
|
||||
item_dict.update(
|
||||
{
|
||||
"tenant_id": tenant_id,
|
||||
"company_id": company_id,
|
||||
"line_number": line_number,
|
||||
}
|
||||
)
|
||||
|
||||
# Create the item
|
||||
db_item = Item(**item_dict)
|
||||
db_item = LineItem(**item_dict)
|
||||
db.add(db_item)
|
||||
db.flush() # Get the item ID
|
||||
|
||||
# Create line items if provided
|
||||
for idx, line_data in enumerate(lines_data):
|
||||
line_dict = line_data.model_dump(
|
||||
exclude={
|
||||
"financial",
|
||||
"quantity",
|
||||
"customs",
|
||||
"description",
|
||||
"reference",
|
||||
"fa_data",
|
||||
}
|
||||
)
|
||||
line_dict.update(
|
||||
{
|
||||
"item_id": db_item.id,
|
||||
"tenant_id": tenant_id,
|
||||
"company_id": company_id,
|
||||
"line_number": (
|
||||
line_numbers[idx]
|
||||
if line_numbers
|
||||
else ItemService._get_next_line_number(
|
||||
db, item_data.invoice_id
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
# Map schema field names to model field names
|
||||
line_dict["part_number"] = line_dict.pop("part_number_id", None)
|
||||
line_dict["component_part_number"] = line_dict.pop(
|
||||
"component_part_number_id", None
|
||||
)
|
||||
|
||||
db_line = LineItem(**line_dict)
|
||||
db.add(db_line)
|
||||
db.flush()
|
||||
|
||||
# Create all nested data
|
||||
ItemService._create_line_nested_data(
|
||||
db, db_line, line_data, tenant_id, company_id
|
||||
)
|
||||
# Create all nested data
|
||||
ItemService._create_line_nested_data(
|
||||
db, db_item, item_data, tenant_id, company_id
|
||||
)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_item)
|
||||
@@ -384,27 +351,27 @@ class ItemService:
|
||||
logger.error(f"Error creating item: {e}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Item creation failed - integrity constraint violated",
|
||||
detail="LineItem creation failed - integrity constraint violated",
|
||||
)
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Unexpected error creating item: {e}")
|
||||
raise HTTPException(status_code=500, detail="Error creating item")
|
||||
logger.error(f"Unexpected error creating LineItem: {e}")
|
||||
raise HTTPException(status_code=500, detail="Error creating LineItem")
|
||||
|
||||
@staticmethod
|
||||
def update(
|
||||
db: Session,
|
||||
item_id: int,
|
||||
item_data: ItemUpdate,
|
||||
item_data: LineItemUpdate,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
) -> Item:
|
||||
) -> LineItem:
|
||||
"""Update an item and optionally its nested data (multiple lines)"""
|
||||
|
||||
# Get existing item
|
||||
db_item = ItemService.get_by_id(db, item_id, tenant_id, company_id)
|
||||
if not db_item:
|
||||
raise HTTPException(status_code=404, detail="Item not found")
|
||||
raise HTTPException(status_code=404, detail="LineItem not found")
|
||||
|
||||
# Validaciones con ErrorCollector
|
||||
errors = ErrorCollector()
|
||||
@@ -418,143 +385,103 @@ class ItemService:
|
||||
):
|
||||
errors.raise_if_errors("Error al actualizar el item")
|
||||
|
||||
# Pre-calcular line_numbers para cada línea (en update, las líneas se renumeran desde 1)
|
||||
line_numbers = []
|
||||
if item_data.lines:
|
||||
line_numbers = [i + 1 for i in range(len(item_data.lines))]
|
||||
# Validar el item que se va a actualizar
|
||||
validate_update(
|
||||
db,
|
||||
item_data, # Schema de update
|
||||
db_item, # LineItem existente en DB
|
||||
tenant_id,
|
||||
company_id,
|
||||
errors,
|
||||
db_item.line_number,
|
||||
)
|
||||
|
||||
# Validar cada line item que se va a actualizar
|
||||
if item_data.lines:
|
||||
for idx, line_data in enumerate(item_data.lines):
|
||||
line_number = line_numbers[idx] # Usar el line_number calculado
|
||||
# Validaciones adicionales específicas del negocio
|
||||
|
||||
# Si el line tiene ID, es actualización; si no, es creación
|
||||
if hasattr(line_data, "id") and line_data.id:
|
||||
# Buscar el line item existente
|
||||
existing_line = next(
|
||||
(line for line in db_item.lines if line.id == line_data.id),
|
||||
None,
|
||||
)
|
||||
if existing_line:
|
||||
# Validar update con línea existente
|
||||
validate_update(
|
||||
db,
|
||||
line_data, # Schema de update
|
||||
existing_line, # LineItem existente en DB
|
||||
invoice_id_to_lock, # invoice_id
|
||||
tenant_id,
|
||||
company_id,
|
||||
errors,
|
||||
line_number,
|
||||
)
|
||||
else:
|
||||
# Es un nuevo line item, validar como creación
|
||||
validate_create(
|
||||
db,
|
||||
line_data, # Schema Pydantic completo
|
||||
invoice_id_to_lock, # invoice_id
|
||||
tenant_id,
|
||||
company_id,
|
||||
errors,
|
||||
line_number,
|
||||
)
|
||||
# Validar apóstrofes en número de parte
|
||||
if item_data.part_number_id and "'" in str(item_data.part_number_id):
|
||||
errors.add_error(
|
||||
field=f"part_number",
|
||||
message=f"Advertencia: El Número de Parte contiene apóstrofes y serán omitidos",
|
||||
solution=None,
|
||||
code="WARNING_APOSTROPHE",
|
||||
)
|
||||
|
||||
# Validaciones adicionales específicas del negocio
|
||||
# (Aplican tanto para crear como actualizar)
|
||||
# Validar tipo de partida
|
||||
if hasattr(item_data, "item_type") and item_data.item_type:
|
||||
tipo_partida = item_data.item_type
|
||||
if tipo_partida and tipo_partida not in ["N", "S"]:
|
||||
errors.add_error(
|
||||
field=f"item_type",
|
||||
message=f"Tipo de partida debe ser 'N' (Normal) o 'S' (Subpartida), recibido: '{tipo_partida}'",
|
||||
solution=None,
|
||||
code="INVALID_ITEM_TYPE",
|
||||
value=str(tipo_partida),
|
||||
)
|
||||
|
||||
# Validar apóstrofes en número de parte
|
||||
if line_data.part_number_id and "'" in str(line_data.part_number_id):
|
||||
# Si es subpartida (S), debe tener partida principal
|
||||
if tipo_partida == "S":
|
||||
if not hasattr(item_data, "main_line_id") or not item_data.main_line_id:
|
||||
errors.add_error(
|
||||
field=f"lines[{line_number}].part_number",
|
||||
message=f"Advertencia: El Número de Parte contiene apóstrofes y serán omitidos",
|
||||
field=f"main_line_id",
|
||||
message="Las subpartidas (tipo 'S') deben tener una partida principal",
|
||||
solution=None,
|
||||
code="WARNING_APOSTROPHE",
|
||||
code="MISSING_MAIN_LINE",
|
||||
)
|
||||
|
||||
# Validar tipo de partida
|
||||
if hasattr(line_data, "item_type"):
|
||||
tipo_partida = line_data.item_type
|
||||
if tipo_partida and tipo_partida not in ["N", "S"]:
|
||||
errors.add_error(
|
||||
field=f"lines[{line_number}].item_type",
|
||||
message=f"Tipo de partida debe ser 'N' (Normal) o 'S' (Subpartida), recibido: '{tipo_partida}'",
|
||||
solution=None,
|
||||
code="INVALID_ITEM_TYPE",
|
||||
value=str(tipo_partida),
|
||||
)
|
||||
|
||||
# Si es subpartida (S), debe tener partida principal
|
||||
if tipo_partida == "S":
|
||||
if (
|
||||
not hasattr(line_data, "main_line_id")
|
||||
or not line_data.main_line_id
|
||||
):
|
||||
errors.add_error(
|
||||
field=f"lines[{line_number}].main_line_id",
|
||||
message="Las subpartidas (tipo 'S') deben tener una partida principal",
|
||||
solution=None,
|
||||
code="MISSING_MAIN_LINE",
|
||||
)
|
||||
|
||||
# Si hay errores, lanzar excepción ANTES de actualizar
|
||||
errors.raise_if_errors("Error al actualizar el item")
|
||||
|
||||
try:
|
||||
# Get item data excluding nested objects
|
||||
item_dict = item_data.model_dump(
|
||||
exclude={
|
||||
"financial",
|
||||
"quantity",
|
||||
"customs",
|
||||
"description",
|
||||
"reference",
|
||||
"fa_data",
|
||||
},
|
||||
exclude_unset=True,
|
||||
)
|
||||
|
||||
# Extract lines data
|
||||
lines_data = item_data.lines
|
||||
item_dict = item_data.model_dump(exclude={"lines"}, exclude_unset=True)
|
||||
# Map schema field names to model field names
|
||||
if "part_number_id" in item_dict:
|
||||
item_dict["part_number"] = item_dict.pop("part_number_id")
|
||||
if "component_part_number_id" in item_dict:
|
||||
item_dict["component_part_number"] = item_dict.pop(
|
||||
"component_part_number_id"
|
||||
)
|
||||
|
||||
# Update item fields
|
||||
for key, value in item_dict.items():
|
||||
setattr(db_item, key, value)
|
||||
|
||||
# Update lines if provided (replace all lines)
|
||||
if lines_data is not None:
|
||||
# Delete existing lines (cascade will handle nested data)
|
||||
for existing_line in db_item.lines:
|
||||
db.delete(existing_line)
|
||||
db.flush()
|
||||
# Delete existing nested data
|
||||
db.query(LineFinancial).filter(
|
||||
LineFinancial.item_line_id == db_item.id
|
||||
).delete()
|
||||
db.query(LineQuantity).filter(
|
||||
LineQuantity.item_line_id == db_item.id
|
||||
).delete()
|
||||
db.query(LineCustom).filter(LineCustom.item_line_id == db_item.id).delete()
|
||||
db.query(LineDescription).filter(
|
||||
LineDescription.item_line_id == db_item.id
|
||||
).delete()
|
||||
db.query(LineReference).filter(
|
||||
LineReference.item_line_id == db_item.id
|
||||
).delete()
|
||||
db.query(FaLineItem).filter(FaLineItem.id == db_item.id).delete()
|
||||
db.flush()
|
||||
|
||||
# Create new lines
|
||||
for idx, line_data in enumerate(lines_data):
|
||||
line_dict = line_data.model_dump(
|
||||
exclude={
|
||||
"financial",
|
||||
"quantity",
|
||||
"customs",
|
||||
"description",
|
||||
"reference",
|
||||
"fa_data",
|
||||
},
|
||||
exclude_unset=True,
|
||||
)
|
||||
line_dict.update(
|
||||
{
|
||||
"item_id": db_item.id,
|
||||
"tenant_id": tenant_id,
|
||||
"company_id": company_id,
|
||||
"line_number": idx + 1,
|
||||
}
|
||||
)
|
||||
# Create new nested data
|
||||
ItemService._create_line_nested_data(
|
||||
db, db_item, item_data, tenant_id, company_id
|
||||
)
|
||||
|
||||
# Map schema field names to model field names
|
||||
line_dict["part_number"] = line_dict.pop("part_number_id", None)
|
||||
line_dict["component_part_number"] = line_dict.pop(
|
||||
"component_part_number_id", None
|
||||
)
|
||||
|
||||
db_line = LineItem(**line_dict)
|
||||
db.add(db_line)
|
||||
db.flush()
|
||||
|
||||
# Create all nested data
|
||||
ItemService._create_line_nested_data(
|
||||
db, db_line, line_data, tenant_id, company_id
|
||||
)
|
||||
|
||||
# Renumber all lines for this invoice to ensure consecutive numbering
|
||||
ItemService._renumber_all_invoice_lines(db, db_item.invoice_id)
|
||||
# Renumber all lines for this invoice to ensure consecutive numbering
|
||||
ItemService._renumber_all_invoice_lines(db, db_item.invoice_id)
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_item)
|
||||
|
||||
@@ -17,7 +17,7 @@ from api.v1.modules.a76.general_catalogs.company.models import Company
|
||||
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_validation import PedimentoValidation
|
||||
from api.v1.modules.a76.customs_brokers.models import CustomsBroker, CustomsBrokerPersonnel
|
||||
from api.v1.modules.a76.items.models import Item
|
||||
from api.v1.modules.a76.items.models import LineItem
|
||||
from api.v1.modules.a76.clients_and_providers.models import ClientProvider, ClientProviderAddress, ClientProviderPrograms
|
||||
|
||||
# --- SCHEMAS FOR TEMPLATE CONTEXT ---
|
||||
@@ -186,7 +186,7 @@ class AvisoConsolidadoExportacionService:
|
||||
logistics = header.logistics
|
||||
|
||||
# Fetch Items associated with this invoice (MOVED UP FOR WEIGHT CALCULATION)
|
||||
items = db.query(Item).filter(Item.invoice_id == invoice_id).all()
|
||||
items = db.query(LineItem).filter(LineItem.invoice_id == invoice_id).all()
|
||||
|
||||
# Peso Bruto
|
||||
peso_bruto_val = "0.0"
|
||||
@@ -194,14 +194,13 @@ class AvisoConsolidadoExportacionService:
|
||||
|
||||
# Calculate sum from items first
|
||||
if items:
|
||||
for item in items:
|
||||
if item.lines:
|
||||
for line in item.lines:
|
||||
if line.quantity and line.quantity.gross_weight:
|
||||
try:
|
||||
calculated_gross_weight += float(line.quantity.gross_weight)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
for item in items:
|
||||
for line in item:
|
||||
if line.quantity and line.quantity.gross_weight:
|
||||
try:
|
||||
calculated_gross_weight += float(line.quantity.gross_weight)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
if financials and financials.gross_weight and float(financials.gross_weight) > 0:
|
||||
peso_bruto_val = f"{financials.gross_weight:,.2f}"
|
||||
@@ -379,20 +378,19 @@ class AvisoConsolidadoExportacionService:
|
||||
cant_total = 0.0
|
||||
|
||||
if items:
|
||||
for item in items:
|
||||
if item.lines:
|
||||
for line in item.lines:
|
||||
# Priority: Quantity (UMA or Standard)
|
||||
q = 0.0
|
||||
if line.quantity:
|
||||
try:
|
||||
if line.quantity.quantity_uma is not None:
|
||||
q = float(line.quantity.quantity_uma)
|
||||
elif line.quantity.quantity is not None:
|
||||
q = float(line.quantity.quantity)
|
||||
except (ValueError, TypeError):
|
||||
q = 0.0
|
||||
cant_total += q
|
||||
for item in items:
|
||||
for line in item:
|
||||
# Priority: Quantity (UMA or Standard)
|
||||
q = 0.0
|
||||
if line.quantity:
|
||||
try:
|
||||
if line.quantity.quantity_uma is not None:
|
||||
q = float(line.quantity.quantity_uma)
|
||||
elif line.quantity.quantity is not None:
|
||||
q = float(line.quantity.quantity)
|
||||
except (ValueError, TypeError):
|
||||
q = 0.0
|
||||
cant_total += q
|
||||
|
||||
# Format: 15 chars, 3 decimals? Actually Clarion LINEPRINT usually just prints the text.
|
||||
# Clarion 'CLIP(FORMAT(Loc:CantTotal,@n015.3))' removes spaces.
|
||||
|
||||
@@ -15,10 +15,9 @@ from datetime import datetime
|
||||
# --- MODELOS (Imported from system for Header info) ---
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader, InvoiceComplianceMx
|
||||
from api.v1.modules.a76.general_catalogs.company.models import Company
|
||||
from api.v1.modules.a76.items.line_items.models import LineItem
|
||||
from api.v1.modules.a76.items.line_quantities.models import LineQuantity
|
||||
from api.v1.modules.a76.parts.models import Part
|
||||
from api.v1.modules.a76.items.models import Item
|
||||
from api.v1.modules.a76.items.models import LineItem
|
||||
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
|
||||
from api.v1.modules.a76.pedmientos.models.pedimento_dates import PedimentoDates
|
||||
from api.v1.modules.core.tenants.models import Tenant
|
||||
@@ -40,8 +39,8 @@ class FIFOAssignmentService:
|
||||
Returns a list of calculated discharges.
|
||||
"""
|
||||
# 1. Get Export Lines
|
||||
export_lines = db.query(LineItem).join(Item).filter(
|
||||
Item.invoice_id == invoice_id
|
||||
export_lines = db.query(LineItem).join(LineItem).filter(
|
||||
LineItem.invoice_id == invoice_id
|
||||
).options(
|
||||
joinedload(LineItem.quantity),
|
||||
joinedload(LineItem.description),
|
||||
@@ -58,7 +57,7 @@ class FIFOAssignmentService:
|
||||
if qty_needed <= 0:
|
||||
continue
|
||||
|
||||
part_number = exp_line.part_number
|
||||
part_number = exp_line.part_number_id
|
||||
if not part_number:
|
||||
self._log(f"Skipping line {exp_line.id}, no part number")
|
||||
continue
|
||||
@@ -79,10 +78,10 @@ class FIFOAssignmentService:
|
||||
|
||||
# 2. Find Import Candidates (FIFO order by payment date)
|
||||
# Use outerjoin for pedimento dates to avoid filtering out candidates with missing dates
|
||||
candidates = db.query(LineItem).join(Item).join(InvoiceHeader)\
|
||||
candidates = db.query(LineItem).join(InvoiceHeader)\
|
||||
.join(InvoiceComplianceMx).join(InvoiceComplianceMx.pedimento).outerjoin(Pedimentos.pedimento_dates)\
|
||||
.filter(
|
||||
LineItem.part_number == part_number,
|
||||
LineItem.part_number_id == part_number,
|
||||
InvoiceHeader.operation_type == 'imp', # Assuming 'imp' is the value for Import based on Enum
|
||||
).order_by(
|
||||
PedimentoDates.payment_date.asc()
|
||||
@@ -90,7 +89,7 @@ class FIFOAssignmentService:
|
||||
joinedload(LineItem.quantity),
|
||||
joinedload(LineItem.customs),
|
||||
joinedload(LineItem.financial),
|
||||
joinedload(LineItem.item).joinedload(Item.invoice).joinedload(InvoiceHeader.compliance_mx).joinedload(InvoiceComplianceMx.pedimento).joinedload(Pedimentos.pedimento_dates)
|
||||
joinedload(LineItem.item).joinedload(LineItem.invoice).joinedload(InvoiceHeader.compliance_mx).joinedload(InvoiceComplianceMx.pedimento).joinedload(Pedimentos.pedimento_dates)
|
||||
).all()
|
||||
|
||||
self._log(f"Found {len(candidates)} candidates for {part_number}")
|
||||
@@ -259,16 +258,15 @@ class DescargaReportService:
|
||||
# --- 2. Obtener Líneas de Exportación (Lo que necesitamos cubrir) ---
|
||||
if progress_callback: progress_callback(20, "Obteniendo items a exportar...")
|
||||
|
||||
export_lines = db.query(LineItem).filter(
|
||||
LineItem.item_id == Item.id,
|
||||
Item.invoice_id == invoice_id
|
||||
export_lines = db.query(LineItem).filter(
|
||||
LineItem.invoice_id == invoice_id
|
||||
).options(
|
||||
joinedload(LineItem.quantity),
|
||||
joinedload(LineItem.customs),
|
||||
joinedload(LineItem.description),
|
||||
joinedload(LineItem.unit_of_measure_info),
|
||||
joinedload(LineItem.part_info)
|
||||
).join(Item).all()
|
||||
).join(LineItem).all()
|
||||
|
||||
items_reporte = []
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ from api.v1.modules.a76.invoices.models import (
|
||||
)
|
||||
from api.v1.modules.a76.items.line_financials.models import LineFinancial
|
||||
from api.v1.modules.a76.items.line_quantities.models import LineQuantity
|
||||
from api.v1.modules.a76.items.line_items.models import LineItem
|
||||
from api.v1.modules.a76.items.models import LineItem
|
||||
from api.v1.modules.a76.clients_and_providers.models import (
|
||||
ClientProvider,
|
||||
ClientProviderAddress,
|
||||
@@ -27,7 +27,7 @@ from api.v1.modules.a76.parts.models import Part
|
||||
from api.v1.modules.a76.pedmientos.models import Pedimentos
|
||||
from api.v1.modules.a76.general_catalogs.company.models import Company
|
||||
from api.v1.modules.a76.customs_brokers.models import CustomsBroker
|
||||
from api.v1.modules.a76.items.models import Item
|
||||
from api.v1.modules.a76.items.models import LineItem
|
||||
|
||||
# --- TRANSPORTATION MODELS ---
|
||||
from api.v1.modules.a76.transportation.transporters.models import Transporter
|
||||
@@ -482,8 +482,7 @@ class ConsolidadoImportacionMexService:
|
||||
|
||||
lines = (
|
||||
db.query(LineItem)
|
||||
.join(Item, LineItem.item_id == Item.id)
|
||||
.filter(Item.invoice_id.in_(target_invoice_ids))
|
||||
.filter(LineItem.invoice_id.in_(target_invoice_ids))
|
||||
.all()
|
||||
)
|
||||
|
||||
@@ -532,7 +531,7 @@ class ConsolidadoImportacionMexService:
|
||||
.filter(LineFinancial.item_line_id == line.id)
|
||||
.first()
|
||||
)
|
||||
part_master = db.query(Part).filter(Part.id == line.part_number).first()
|
||||
part_master = db.query(Part).filter(Part.id == line.part_number_id).first()
|
||||
|
||||
# --- Resolver Identificadores (MOVED INSIDE MAIN LOOP) ---
|
||||
us_fraction_raw = ""
|
||||
|
||||
@@ -13,7 +13,6 @@ from sqlalchemy.orm import Session
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader, InvoiceLogistics, InvoiceComplianceMx
|
||||
from api.v1.modules.a76.items.line_financials.models import LineFinancial
|
||||
from api.v1.modules.a76.items.line_quantities.models import LineQuantity
|
||||
from api.v1.modules.a76.items.line_items.models import LineItem
|
||||
from api.v1.modules.a76.clients_and_providers.models import (
|
||||
ClientProvider, ClientProviderAddress, ClientProviderPrograms
|
||||
)
|
||||
@@ -21,7 +20,7 @@ from api.v1.modules.a76.parts.models import Part
|
||||
from api.v1.modules.a76.pedmientos.models import Pedimentos
|
||||
from api.v1.modules.a76.general_catalogs.company.models import Company
|
||||
from api.v1.modules.a76.customs_brokers.models import CustomsBroker
|
||||
from api.v1.modules.a76.items.models import Item
|
||||
from api.v1.modules.a76.items.models import LineItem
|
||||
|
||||
# --- TRANSPORTATION MODELS ---
|
||||
from api.v1.modules.a76.transportation.transporters.models import Transporter
|
||||
@@ -314,8 +313,8 @@ class ConsolidadoImportacionMexService:
|
||||
# NOT consolidating all invoices from the same Pedimento.
|
||||
target_invoice_ids = [header.id]
|
||||
|
||||
lines = db.query(LineItem).join(Item, LineItem.item_id == Item.id).filter(
|
||||
Item.invoice_id.in_(target_invoice_ids)
|
||||
lines = db.query(LineItem).filter(
|
||||
LineItem.invoice_id.in_(target_invoice_ids)
|
||||
).all()
|
||||
|
||||
partidas_list = []
|
||||
@@ -350,7 +349,7 @@ class ConsolidadoImportacionMexService:
|
||||
for line in lines:
|
||||
qty = db.query(LineQuantity).filter(LineQuantity.item_line_id == line.id).first()
|
||||
fin = db.query(LineFinancial).filter(LineFinancial.item_line_id == line.id).first()
|
||||
part_master = db.query(Part).filter(Part.id == line.part_number).first()
|
||||
part_master = db.query(Part).filter(Part.id == line.part_number_id).first()
|
||||
|
||||
# --- Resolver Identificadores (MOVED INSIDE MAIN LOOP) ---
|
||||
us_fraction_raw = ""
|
||||
|
||||
@@ -13,7 +13,6 @@ from sqlalchemy.orm import Session
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader, InvoiceLogistics
|
||||
from api.v1.modules.a76.items.line_financials.models import LineFinancial
|
||||
from api.v1.modules.a76.items.line_quantities.models import LineQuantity
|
||||
from api.v1.modules.a76.items.line_items.models import LineItem
|
||||
from api.v1.modules.a76.clients_and_providers.models import (
|
||||
ClientProvider,
|
||||
ClientProviderAddress,
|
||||
@@ -23,7 +22,7 @@ from api.v1.modules.a76.parts.models import Part
|
||||
from api.v1.modules.a76.pedmientos.models import Pedimentos
|
||||
from api.v1.modules.a76.general_catalogs.company.models import Company
|
||||
from api.v1.modules.a76.customs_brokers.models import CustomsBroker
|
||||
from api.v1.modules.a76.items.models import Item
|
||||
from api.v1.modules.a76.items.models import LineItem
|
||||
|
||||
# --- TRANSPORTATION MODELS ---
|
||||
from api.v1.modules.a76.transportation.transporters.models import Transporter
|
||||
@@ -422,9 +421,8 @@ class FacturaImportacionMexService:
|
||||
if progress_callback:
|
||||
progress_callback(50, "Procesando partidas...")
|
||||
lines = (
|
||||
db.query(LineItem)
|
||||
.join(Item, LineItem.item_id == Item.id)
|
||||
.filter(Item.invoice_id == header.id)
|
||||
db.query(LineItem)
|
||||
.filter(LineItem.invoice_id == header.id)
|
||||
.all()
|
||||
)
|
||||
partidas_list = []
|
||||
@@ -440,10 +438,10 @@ class FacturaImportacionMexService:
|
||||
.filter(LineFinancial.item_line_id == line.id)
|
||||
.first()
|
||||
)
|
||||
part_master = db.query(Part).filter(Part.id == line.part_number).first()
|
||||
part_master = db.query(Part).filter(Part.id == line.part_number_id).first()
|
||||
|
||||
desc_final = "S/D"
|
||||
num_parte_final = str(line.part_number or "S/N")
|
||||
num_parte_final = str(line.part_number_id or "S/N")
|
||||
fraccion_raw = ""
|
||||
origen_final = "MEX"
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ from sqlalchemy.orm import Session
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader, InvoiceLogistics
|
||||
from api.v1.modules.a76.items.line_financials.models import LineFinancial
|
||||
from api.v1.modules.a76.items.line_quantities.models import LineQuantity
|
||||
from api.v1.modules.a76.items.line_items.models import LineItem
|
||||
from api.v1.modules.a76.clients_and_providers.models import (
|
||||
ClientProvider, ClientProviderAddress, ClientProviderPrograms
|
||||
)
|
||||
@@ -21,7 +20,7 @@ from api.v1.modules.a76.parts.models import Part
|
||||
from api.v1.modules.a76.pedmientos.models import Pedimentos
|
||||
from api.v1.modules.a76.general_catalogs.company.models import Company
|
||||
from api.v1.modules.a76.customs_brokers.models import CustomsBroker
|
||||
from api.v1.modules.a76.items.models import Item
|
||||
from api.v1.modules.a76.items.models import LineItem
|
||||
|
||||
# --- TRANSPORTATION MODELS ---
|
||||
from api.v1.modules.a76.transportation.transporters.models import Transporter
|
||||
@@ -227,16 +226,16 @@ class FacturaImportacionMexService:
|
||||
)
|
||||
|
||||
if progress_callback: progress_callback(50, "Procesando partidas...")
|
||||
lines = db.query(LineItem).join(Item, LineItem.item_id == Item.id).filter(Item.invoice_id == header.id).all()
|
||||
lines = db.query(LineItem).filter(LineItem.invoice_id == header.id).all()
|
||||
partidas_list = []
|
||||
|
||||
for line in lines:
|
||||
qty = db.query(LineQuantity).filter(LineQuantity.item_line_id == line.id).first()
|
||||
fin = db.query(LineFinancial).filter(LineFinancial.item_line_id == line.id).first()
|
||||
part_master = db.query(Part).filter(Part.id == line.part_number).first()
|
||||
part_master = db.query(Part).filter(Part.id == line.part_number_id).first()
|
||||
|
||||
desc_final = "S/D"
|
||||
num_parte_final = str(line.part_number or "S/N")
|
||||
num_parte_final = str(line.part_number_id or "S/N")
|
||||
fraccion_raw = ""
|
||||
origen_final = "MEX"
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ from sqlalchemy.orm import Session
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader, InvoiceLogistics
|
||||
from api.v1.modules.a76.items.line_financials.models import LineFinancial
|
||||
from api.v1.modules.a76.items.line_quantities.models import LineQuantity
|
||||
from api.v1.modules.a76.items.line_items.models import LineItem
|
||||
from api.v1.modules.a76.clients_and_providers.models import (
|
||||
ClientProvider, ClientProviderAddress, ClientProviderPrograms
|
||||
)
|
||||
@@ -21,7 +20,7 @@ from api.v1.modules.a76.parts.models import Part
|
||||
from api.v1.modules.a76.pedmientos.models import Pedimentos
|
||||
from api.v1.modules.a76.general_catalogs.company.models import Company
|
||||
from api.v1.modules.a76.customs_brokers.models import CustomsBroker
|
||||
from api.v1.modules.a76.items.models import Item
|
||||
from api.v1.modules.a76.items.models import LineItem
|
||||
|
||||
# --- TRANSPORTATION MODELS ---
|
||||
from api.v1.modules.a76.transportation.transporters.models import Transporter
|
||||
@@ -278,16 +277,16 @@ class FacturaImportacionUsaService:
|
||||
)
|
||||
|
||||
if progress_callback: progress_callback(50, "Processing items...")
|
||||
lines = db.query(LineItem).join(Item, LineItem.item_id == Item.id).filter(Item.invoice_id == header.id).all()
|
||||
lines = db.query(LineItem).filter(LineItem.invoice_id == header.id).all()
|
||||
partidas_list = []
|
||||
|
||||
for line in lines:
|
||||
qty = db.query(LineQuantity).filter(LineQuantity.item_line_id == line.id).first()
|
||||
fin = db.query(LineFinancial).filter(LineFinancial.item_line_id == line.id).first()
|
||||
part_master = db.query(Part).filter(Part.id == line.part_number).first()
|
||||
part_master = db.query(Part).filter(Part.id == line.part_number_id).first()
|
||||
|
||||
desc_final = "N/D"
|
||||
num_parte_final = str(line.part_number or "N/A")
|
||||
num_parte_final = str(line.part_nupart_number_idmber or "N/A")
|
||||
fraccion_raw = ""
|
||||
origen_final = "MEX"
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ from sqlalchemy.orm import Session
|
||||
# --- MODELOS ---
|
||||
from api.v1.modules.a76.invoices.models import InvoiceHeader, InvoiceLogistics
|
||||
from api.v1.modules.a76.items.line_quantities.models import LineQuantity
|
||||
from api.v1.modules.a76.items.line_items.models import LineItem
|
||||
from api.v1.modules.a76.items.line_customs.models import LineCustom
|
||||
from api.v1.modules.a76.clients_and_providers.models import (
|
||||
ClientProvider, ClientProviderAddress, ClientProviderPrograms
|
||||
@@ -22,7 +21,7 @@ from api.v1.modules.a76.parts.models import Part
|
||||
from api.v1.modules.a76.pedmientos.models import Pedimentos
|
||||
from api.v1.modules.a76.general_catalogs.company.models import Company
|
||||
from api.v1.modules.a76.customs_brokers.models import CustomsBroker
|
||||
from api.v1.modules.a76.items.models import Item
|
||||
from api.v1.modules.a76.items.models import LineItem
|
||||
|
||||
# --- TRANSPORTATION MODELS ---
|
||||
from api.v1.modules.a76.transportation.transporters.models import Transporter
|
||||
@@ -238,7 +237,7 @@ class PackingListService:
|
||||
)
|
||||
|
||||
if progress_callback: progress_callback(50, "Procesando partidas...")
|
||||
lines = db.query(LineItem).join(Item, LineItem.item_id == Item.id).filter(Item.invoice_id == header.id).all()
|
||||
lines = db.query(LineItem).filter(LineItem.invoice_id == header.id).all()
|
||||
partidas_list = []
|
||||
|
||||
for line in lines:
|
||||
@@ -268,10 +267,10 @@ class PackingListService:
|
||||
# --------------------------------
|
||||
|
||||
custom_obj = db.query(LineCustom).filter(LineCustom.item_line_id == line.id).first()
|
||||
part_master = db.query(Part).filter(Part.id == line.part_number).first()
|
||||
part_master = db.query(Part).filter(Part.id == line.part_number_id).first()
|
||||
|
||||
desc_final = "S/D"
|
||||
num_parte_final = str(line.part_number or "S/N")
|
||||
num_parte_final = str(line.part_number_id or "S/N")
|
||||
fraccion_raw = ""
|
||||
origen_final = "MEX"
|
||||
uom_comercial = "PZA" # Default UOM
|
||||
|
||||
@@ -12,7 +12,7 @@ from api.v1.modules.a76.invoices.models import (
|
||||
InvoiceComplianceMx,
|
||||
OperationType,
|
||||
)
|
||||
from api.v1.modules.a76.items.models import Item
|
||||
from api.v1.modules.a76.items.models import LineItem
|
||||
from api.v1.modules.a76.clients_and_providers.models import (
|
||||
ClientProvider,
|
||||
ClientOrProviderEnum,
|
||||
@@ -244,9 +244,9 @@ class DashboardService:
|
||||
def get_items_metrics(self) -> KPIMetric:
|
||||
"""Obtiene métricas de items/productos"""
|
||||
total_items = (
|
||||
self.db.query(func.count(Item.id))
|
||||
self.db.query(func.count(LineItem.id))
|
||||
.filter(
|
||||
Item.tenant_id == self.tenant_id, Item.company_id == self.company_id
|
||||
LineItem.tenant_id == self.tenant_id, LineItem.company_id == self.company_id
|
||||
)
|
||||
.scalar()
|
||||
or 0
|
||||
|
||||
Reference in New Issue
Block a user