""" 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) ) ``` """