- Added `company_id` to `ClientProviderAddress` and `ClientProviderPrograms` for better association. - Updated `_process_with_discharge` to simplify invoice review by removing redundant parameters. - Improved `pre_validators` to ensure address validation checks for existence before accessing country. - Enhanced balance comparison logic in `compare_balances` by tracking consumed quantities. - Modified `fill_available_balances` to reflect invoice status changes from 'UNPROCESSED' to 'PENDING'. - Introduced `get_lines_with_balance` method in `ItemService` to fetch invoice lines with available balance. - Added new API endpoint to retrieve import invoice lines with balance information. - Updated frontend components to display available balances and improve user experience in invoice selection.
392 lines
15 KiB
Python
392 lines
15 KiB
Python
"""
|
|
Normalized Database Schema for SCAF (Fixed Assets) and SCAII (Parts Inventory)
|
|
SQLAlchemy v2 - Annex 24 Compliance
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from typing import Optional, List, TYPE_CHECKING
|
|
from core.database import Base
|
|
from decimal import Decimal
|
|
from sqlalchemy import Boolean, Date, 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
|
|
|
|
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
|
|
|
|
# Imported at runtime so SQLAlchemy's mapper registry can resolve the class name
|
|
# used in the relationship string below.
|
|
from api.v1.modules.a76.general_catalogs.identifiers.models import IdentifierDetail
|
|
|
|
# ============================================================================
|
|
# CORE ENTITIES
|
|
# ============================================================================
|
|
|
|
|
|
class LineItem(Base, TenantScopedMixin, TimestampMixin):
|
|
"""
|
|
Unified item header table for all import/export operations
|
|
Consolidates headers from both SCAF and SCAII systems
|
|
"""
|
|
|
|
__tablename__ = "item_lines"
|
|
__table_args__ = {
|
|
"schema": "a76",
|
|
}
|
|
|
|
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
|
|
order: Mapped[Optional[str]] = mapped_column(String(50)) # ORDENCOMPRA / ORDENVENTA
|
|
guide_number: Mapped[Optional[str]] = mapped_column(
|
|
String(50)
|
|
) # NUMEROGUIA/NUMERODEGUIA
|
|
|
|
# Dates
|
|
depreciation_date: Mapped[Optional[datetime]] = mapped_column(
|
|
Date
|
|
) # FECHADEPRECIACION
|
|
|
|
# Administrative fields
|
|
rectification: Mapped[Optional[bool]] = mapped_column(Boolean) # RECTIFICACION
|
|
warehouse: Mapped[Optional[str]] = mapped_column(String(30)) # BODEGA
|
|
location: Mapped[Optional[str]] = mapped_column(String(200)) # LOCALIZACION
|
|
|
|
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",
|
|
cascade="all, delete-orphan",
|
|
uselist=False,
|
|
)
|
|
identifiers: Mapped[List["IdentifierDetail"]] = relationship(
|
|
"IdentifierDetail",
|
|
back_populates="line",
|
|
cascade="all, delete-orphan",
|
|
)
|
|
part_info: Mapped[Optional["Part"]] = relationship(
|
|
"Part",
|
|
foreign_keys=[part_number_id],
|
|
viewonly=True,
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# SUPPORTING TABLES
|
|
# ============================================================================
|
|
|
|
|
|
class PackingList(Base, TenantScopedMixin, TimestampMixin):
|
|
"""
|
|
Packing list items
|
|
From: SPartidasPackingList
|
|
"""
|
|
|
|
__tablename__ = "packing_lists"
|
|
__table_args__ = {
|
|
"schema": "a76",
|
|
}
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
|
item_line_id: Mapped[int] = mapped_column(Integer) # LINEA
|
|
packing_list_number: Mapped[Optional[str]] = mapped_column(
|
|
String(100)
|
|
) # NUMPACKINGLIST
|
|
|
|
|
|
class CTMReceipt(Base, TenantScopedMixin, TimestampMixin):
|
|
"""
|
|
CTM Receipt lines (temporary manufacturing)
|
|
From: SPartidasReciboCTM
|
|
"""
|
|
|
|
__tablename__ = "ctm_receipts"
|
|
__table_args__ = {
|
|
"schema": "a76",
|
|
}
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
|
receipt_line: Mapped[int] = mapped_column(
|
|
ForeignKey("a76.item_lines.id")
|
|
) # LINEARECIBO
|
|
|
|
option: Mapped[Optional[str]] = mapped_column(String(3)) # OPCION
|
|
exit_invoice: Mapped[Optional[str]] = mapped_column(String(19)) # FACTURASALIDA
|
|
|
|
|
|
class SubassemblyEntry(Base, TenantScopedMixin, TimestampMixin):
|
|
"""
|
|
Subassembly/Submanufacturing Entry lines
|
|
From: SPartidasEntradaSM
|
|
"""
|
|
|
|
__tablename__ = "subassembly_entries"
|
|
__table_args__ = {
|
|
"schema": "a76",
|
|
}
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
|
remission_line: Mapped[int] = mapped_column(Integer) # LINEAREMISION
|
|
exit_invoice: Mapped[Optional[str]] = mapped_column(String(15)) # FACTURASALIDA
|
|
exit_line: Mapped[Optional[int]] = mapped_column(Integer) # LINEASALIDA
|
|
|
|
|
|
# ============================================================================
|
|
# INDEXES AND CONSTRAINTS
|
|
# ============================================================================
|
|
|
|
|
|
"""
|
|
Recommended indexes for optimal query performance:
|
|
|
|
CREATE INDEX idx_items_consecutive ON items(consecutive);
|
|
CREATE INDEX idx_items_invoice ON items(invoice_number);
|
|
CREATE INDEX idx_items_type_system ON items(item_type, system_origin);
|
|
CREATE INDEX idx_items_dates ON items(invoice_date, depreciation_date);
|
|
|
|
CREATE INDEX idx_lines_item ON item_lines(item_id);
|
|
CREATE INDEX idx_lines_part ON item_lines(part_number);
|
|
CREATE INDEX idx_lines_class ON item_lines(class_code);
|
|
CREATE INDEX idx_lines_invoice_refs ON item_lines(import_invoice, export_invoice);
|
|
CREATE INDEX idx_lines_fractions ON item_lines(import_fraction, export_fraction);
|
|
|
|
CREATE INDEX idx_packing_consecutive ON packing_lists(consecutive);
|
|
CREATE INDEX idx_packing_part ON packing_lists(part_number);
|
|
|
|
CREATE INDEX idx_repair_invoice ON repair_parts(import_invoice, import_line);
|
|
CREATE INDEX idx_ctm_ship_consec ON ctm_shipments(consecutive);
|
|
CREATE INDEX idx_ctm_rcpt_consec ON ctm_receipts(consecutive);
|
|
CREATE INDEX idx_sub_entry_consec ON subassembly_entries(consecutive);
|
|
CREATE INDEX idx_sub_exit_consec ON subassembly_exits(consecutive);
|
|
CREATE INDEX idx_imposition_consec ON imposition_parts(consecutive);
|
|
"""
|
|
|
|
|
|
# ============================================================================
|
|
# MIGRATION NOTES
|
|
# ============================================================================
|
|
|
|
"""
|
|
MIGRATION STRATEGY FROM ORIGINAL TABLES TO NORMALIZED SCHEMA:
|
|
|
|
1. DOCUMENT MAPPING:
|
|
- QEqeMaq (Equipment Import Temp) → items (type='EQUIPMENT_IMPORT_TEMP', system='SCAF')
|
|
- QEqeMaqRep (Equipment Repair Export) → items (type='EQUIPMENT_REPAIR_EXPORT', system='SCAF')
|
|
- QEqiDef (Equipment Import Definitive) → items (type='EQUIPMENT_IMPORT_DEF', system='SCAF')
|
|
- QEqiMaq (Equipment Machinery) → items (type='EQUIPMENT_MACHINERY', system='SCAF')
|
|
- QEqiMaqRep (Equipment Machinery Repair) → items (type='EQUIPMENT_REPAIR_IMPORT', system='SCAF')
|
|
- SPartidasCM (Common Commerce) → items (type='COMMON_COMMERCE', system='SCAII')
|
|
- SPartidasExpo (Export) → items (type='EXPORT', system='SCAII')
|
|
- SPartidasImpo (Import) → items (type='IMPORT', system='SCAII')
|
|
|
|
2. LINE MAPPING:
|
|
All line items from Q* and SPartidas* tables map to item_lines with appropriate
|
|
field mapping based on the original column names (preserved as comments).
|
|
|
|
3. FIELD CONSOLIDATION RULES:
|
|
- Costs: Unified under unit_cost_* with currency suffix (usd/mxn/mc)
|
|
- Values: Unified under value_* with currency suffix
|
|
- Quantities: Unified under quantity_* with specific purpose suffixes
|
|
- Descriptions: Consolidated into description_spanish/english/extra
|
|
- Fractions: All fraction fields preserved with clear naming
|
|
|
|
4. DATA INTEGRITY:
|
|
- Original CONSECUTIVO + LINE number preserved for traceability
|
|
- Foreign key relationships established via item_id
|
|
- All original fields retained to prevent data loss
|
|
|
|
5. SPECIAL TABLES:
|
|
- PackingList, RepairPart, CTM*, Subassembly*, ImpositionPart remain separate
|
|
as they serve specific purposes and don't fit the main item/line pattern
|
|
|
|
6. BENEFITS:
|
|
- Eliminates redundancy across 13 original tables
|
|
- Unified query interface for all operations
|
|
- Maintains full audit trail with original field names
|
|
- Enables cross-system reporting (SCAF + SCAII)
|
|
- Simplifies maintenance with single schema
|
|
|
|
7. QUERYING EXAMPLES:
|
|
```python
|
|
# Get all imports (both systems)
|
|
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
|
|
session.query(LineItem).filter(
|
|
LineItem.part_number == 'ABC123'
|
|
)
|
|
|
|
# Get SCAF equipment with depreciation
|
|
session.query(LineItem).join(LineItem).filter(
|
|
LineItem.system_origin == 'SCAF',
|
|
LineItem.value_depreciated_usd.isnot(None)
|
|
)
|
|
```
|
|
"""
|