diff --git a/backend/api/v1/modules/a24/fa/fa_item_lines/models.py b/backend/api/v1/modules/a24/fa/fa_item_lines/models.py new file mode 100644 index 00000000..5aa2d038 --- /dev/null +++ b/backend/api/v1/modules/a24/fa/fa_item_lines/models.py @@ -0,0 +1,34 @@ +from typing import Optional +from sqlalchemy import Boolean, ForeignKey, Integer, String +from sqlalchemy.orm import Mapped, mapped_column +from core.database import Base + +class LineItem(Base): + __tablename__ = "line_items" + __table_args__ = ( + {"schema": "a24"} + ) + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + item_id: Mapped[int] = mapped_column(ForeignKey("a76.items.id")) + + # Asset information (SCAF specific) + asset_number: Mapped[Optional[str]] = mapped_column(String(25)) # ASSETNUMBER + asset_photo: Mapped[Optional[str]] = mapped_column(String(255)) # FOTOACTIVOFIJO + equipment_message: Mapped[Optional[str]] = mapped_column(String(40)) # EQI_MENSAJE + invoice_type_asset: Mapped[Optional[str]] = mapped_column(String(6)) # TIPOFACTURAASSET + return_import_invoice: Mapped[Optional[str]] = mapped_column(String(15)) # FACTURAIMPORET + return_import_date: Mapped[Optional[int]] = mapped_column(Integer) # FECHAFACIMPORET + movement_type_import: Mapped[Optional[str]] = mapped_column(String(3)) # TIPOMOVIMPO + + # Cross-references IN CASE OF IMPORT REPAIR + search_invoice: Mapped[Optional[str]] = mapped_column(String(15)) # FACTURAEXPO (when line is import) / FACTURAIMPO (when line is export) + search_line: Mapped[Optional[int]] = mapped_column(Integer) # LINEAEXPO (when line is import) / LINEAIMPO (when line is export) + + # Search type + search_type: Mapped[Optional[str]] = mapped_column(String(10)) # TIPOBUSQUEDA + + # Special flags + download: Mapped[Optional[bool]] = mapped_column(Boolean) # DESCARGA + own_equipment: Mapped[Optional[bool]] = mapped_column(Boolean) # EQUIPOPROPIO + omit_annex31: Mapped[Optional[bool]] = mapped_column(Boolean) # OMITITENANEXO31 \ No newline at end of file diff --git a/backend/api/v1/modules/a76/items/models.py b/backend/api/v1/modules/a76/items/models.py new file mode 100644 index 00000000..1439f6a0 --- /dev/null +++ b/backend/api/v1/modules/a76/items/models.py @@ -0,0 +1,620 @@ +""" +Normalized Database Schema for SCAF (Fixed Assets) and SCAII (Parts Inventory) +SQLAlchemy v2 - Annex 24 Compliance +""" + +from datetime import datetime +from decimal import Decimal +from typing import Optional, TYPE_CHECKING +from enum import Enum +from sqlalchemy import Boolean, String, Integer, Numeric, Text, SmallInteger, ForeignKey +from sqlalchemy.orm import Mapped, mapped_column, relationship +from core.database import Base + +if TYPE_CHECKING: + from .series.models import Serie + +# ============================================================================ +# CORE ENTITIES +# ============================================================================ + +class Item(Base): + """ + Unified item header table for all import/export operations + Consolidates headers from both SCAF and SCAII systems + """ + __tablename__ = "items" + __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 + item_type: Mapped[str] = mapped_column(String(20)) # Type: IMPORT_TEMP, IMPORT_DEF, EXPORT, REPAIR, etc. + system_origin: Mapped[str] = mapped_column(String(10)) # SCAF or SCAII + + # Item references + invoice_number: Mapped[Optional[str]] = mapped_column(String(15)) # FACTURAIMPO/FACTURAEXPO + 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 + invoice_date: Mapped[Optional[int]] = mapped_column(Integer) # FECHAFACTURA + depreciation_date: Mapped[Optional[int]] = mapped_column(Integer) # FECHADEPRECIACION + + # Administrative fields + rectification: Mapped[Optional[int]] = mapped_column(SmallInteger) # RECTIFICACION + warehouse: Mapped[Optional[str]] = mapped_column(String(30)) # BODEGA + location: Mapped[Optional[str]] = mapped_column(String(200)) # LOCALIZACION + + # Relationships + lines: Mapped[list["LineItem"]] = relationship(back_populates="item", cascade="all, delete-orphan") + + +class LineItem(Base): + """ + 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[str]] = mapped_column(ForeignKey("a76.parts.id")) # NUMPARTE + component_part_number: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.parts.id")) # NUMPARTECOM + class_code: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.classes.id")) # CLASE + + # Unit of measure + unit_of_measure: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.units_of_measure_general.id")) # UNIDADMEDIDA/UNIMED + alternate_unit: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.units_of_measure_general.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 + + # Subitem flags + is_subitem: Mapped[Optional[bool]] = mapped_column(Boolean) # ESSUBPARTIDA + contains_subitems: Mapped[Optional[bool]] = mapped_column(Boolean) # CONTIENESUBP + includes_subitems: Mapped[Optional[bool]] = mapped_column(Boolean) # INCUYESUBPARTIDAS + subitem_number: Mapped[Optional[bool]] = mapped_column(Boolean) # SUBPARTIDA + + # 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") + +class LineFinancial(Base): + """ + Financial details for line items + Consolidates all line-level data from Q and S tables + """ + __tablename__ = "line_financials" + __table_args__ = { + "schema": "a76", + } + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + item_line_id: Mapped[int] = mapped_column(ForeignKey("a76.item_lines.id")) + + # Costs - Capture + unit_cost_capture: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTOUNITARIOCAPTURA + + # Costs - USD + unit_cost_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTOUNITARIODLLS/COSTOUNITARIOME + unit_cost_commercial_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTOUCOMDLLS + unit_cost_current_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTOACTUALDLLS + unit_cost_depreciated_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTODEPRECME + unit_cost_subitem_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTOUNITARIOSUBPDLLS + unit_cost_auxiliary_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTOUAUXILIARME + sales_cost_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTOVENTAME + commercial_unit_cost: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTOUNICOMERCIAL + + # Costs - MXN + unit_cost_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTOUNITARIOPESOS/COSTOUNITARIOMN + unit_cost_commercial_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTOUCOMPESOS + unit_cost_current_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTOACTUALMN + unit_cost_depreciated_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTODEPRECMN + unit_cost_subitem_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTOUNITARIOSUBPPESOS + sales_cost_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTOVENTAMN + + # Costs - MC (Custom Currency) + unit_cost_mc: Mapped[Optional[Decimal]] = mapped_column(Numeric(29, 8)) # COSTOUNITARIOMC + + # Values - MXN + value_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORMN/VALORIMPOMN/VALOREXPOMN + value_commercial_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALOREXPOCOMMN + value_updated_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORACTUALIZADOMN + value_subitem_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORSUBPMN + sub_import_value_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # SUBVALORIMPOMN + value_returned_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORRETORNADOMN + value_depreciated_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORDEPRECIADOMN + customs_value_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORADUANASMN + value_total_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORTOTALMN + value_temp_material_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORMPTEMPMN + value_def_material_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORMPDEFMN + value_added_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORAGREMN + value_national_packing_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALEMPAQUENACMN + vat_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # IVAIMPOMN/IVAEXPOMN/VALORIVAMN + vat_used_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORIVAMNUSADO + advalorem_line_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # ADVALORMNLINEAPED + + # Values - USD + value_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(29, 8)) # VALORME/VALORIMPOME/VALOREXPOME + value_commercial_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALOREXPOCOMME + value_updated_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORACTUALIZADOME + value_subitem_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORSUBPME + sub_import_value_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # SUBVALORIMPOME + value_returned_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORRETORNADOME + value_depreciated_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORDEPRECIADOME + customs_value_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORADUANASME + value_auxiliary_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORIMPOAUXILIARME / VALOREXPORAUXILIARME + value_total_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORTOTALME + value_temp_material_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORMPTEMPME + value_def_material_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORMPDEFME + value_added_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORAGREME + value_national_packing_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALEMPAQUENACME + value_us_packing_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALEMPAQUEUSME + vat_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # IVAIMPOME/IVAEXPOME/VALORIVAME + vat_used_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORIVAMEUSADO + value_non_originating_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORNOORIGINARIOME + value_originating_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORORIGINARIOME + igi_amount_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # MONTOIGIME + exempt_amount_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # MONTOEXCENTOME + total_commercial_value: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORTOTALCOMERCIAL + advalorem_line_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # ADVALORMELINEAPED + + # Values - MC + value_mc: Mapped[Optional[Decimal]] = mapped_column(Numeric(29, 8)) # VALORIMPOMC/VALOREXPOMC + sub_import_value_mc: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # SUBVALORIMPOMC + vat_mc: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # IVAIMPOMC/IVAEXPOMC + value_added_mc: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORAGREMC + value_national_packing_mc: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALEMPAQUENACMC + value_total_mc: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORTOTALMC + value_temp_material_mc: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORMPTEMPMC + value_def_material_mc: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORMPDEFMC + + # Relationship + item_line: Mapped["LineItem"] = relationship() + +class LineQuantity(Base): + """ + Quantity details for line items + Consolidates all line-level data from Q and S tables + """ + __tablename__ = "line_quantities" + __table_args__ = { + "schema": "a76", + } + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + item_line_id: Mapped[int] = mapped_column(ForeignKey("a76.item_lines.id")) + + # Quantities + quantity: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8)) # CANTIMPO / CANTEXPO / CANTIMPODEF + alternate_quantity: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8)) # CANTALTERNA + quantity_uma: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8)) # CANTIMPOUMA / CANTEXPOUMA + auxiliary_quantity: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8)) # CANTIMPOAUXILIAR / CANTEXPOAUXILIAR + + # Quantities - Special (SCAF specific) + quantity_temp_export: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8)) # CANTEXPOTEMP + quantity_existence: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8)) # CANTEXISTENCIA + quantity_returned: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8)) # CANTRETORNADA + quantity_returned_temp: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8)) # CANTRETORNADATEMP + serial_count: Mapped[Optional[int]] = mapped_column(Integer) # CANT_SERIES/CANT_SERIESDEF + + # Weight + weight_unit: Mapped[Optional[str]] = mapped_column(String(3)) # 'KG' o 'LB' + net_weight: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8)) # PESONETO + gross_weight: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8)) # PESOBRUTO + + # Packaging + package_key: Mapped[Optional[str]] = mapped_column(String(5)) # CLAVEBULTOS + package_quantity: Mapped[Optional[int]] = mapped_column(Integer) # CANTBULTOS + package_description: Mapped[Optional[str]] = mapped_column(String(40)) # DESCBULTOS + container_quantity: Mapped[Optional[int]] = mapped_column(SmallInteger) # CANTBULCONT + container_description: Mapped[Optional[str]] = mapped_column(String(40)) # DESCCONTENEDOR + box_count: Mapped[Optional[str]] = mapped_column(String(30)) # NOCAJAS + + # Relationship + item_line: Mapped["LineItem"] = relationship() + +class LineCustoms(Base): + """ + Customs details for line items + Consolidates all line-level data from Q and S tables + """ + __tablename__ = "line_customs" + __table_args__ = { + "schema": "a76", + } + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + item_line_id: Mapped[int] = mapped_column(ForeignKey("a76.item_lines.id")) + + # Tariff/Customs Classifications + fraction: Mapped[Optional[str]] = mapped_column(String(10)) # FRACCION / FRACCIONIMPO / FRACCIONEXPO + fraction_type: Mapped[Optional[str]] = mapped_column(String(7)) # TIPOFRACCION / TIPOFRACCIONIMPO / TIPOFRACCIONEXPO + american_fraction: Mapped[Optional[str]] = mapped_column(String(16)) # FRACCIONAMERICANA + alternate_fraction: Mapped[Optional[str]] = mapped_column(String(10)) # FRACCIONALTERNA + reference_fraction: Mapped[Optional[str]] = mapped_column(String(10)) # FRACCIONREFERENCIA + octave_fraction: Mapped[Optional[str]] = mapped_column(String(10)) # FRACCIONROCTAVA + tlcan_fraction: Mapped[Optional[str]] = mapped_column(String(13)) # FRACCIONTLCAN + extra_american_fraction: Mapped[Optional[str]] = mapped_column(String(16)) # FRACAMESELEXTRA + garment_fraction: Mapped[Optional[str]] = mapped_column(String(19)) # FRACCIONDELAPRENDA/FRACCIONDELAPARTIDA + + # Ad Valorem + advalorem: Mapped[Optional[str]] = mapped_column(String(10)) # ADVIMPO / ADVEXPO + advalorem_numeric: Mapped[Optional[Decimal]] = mapped_column(Numeric(7, 2)) # ADVIMPONUM / ADVEXPONUM + advalorem_american: Mapped[Optional[Decimal]] = mapped_column(Numeric(5, 2)) # ADVAME + advalorem_tlcan: Mapped[Optional[Decimal]] = mapped_column(Numeric(5, 2)) # ADVTLCAN + + # Rates + rate: Mapped[Optional[str]] = mapped_column(String(10)) # TASAIM / TASAEX + depreciation_rate: Mapped[Optional[Decimal]] = mapped_column(Numeric(5, 2)) # TASADEPRECIA + + # Origin/Destination + origin_country: Mapped[Optional[str]] = mapped_column(String(3)) # PAISORIGEN + destination_country: Mapped[Optional[str]] = mapped_column(String(3)) # PAISDESTINO + optional_country: Mapped[Optional[str]] = mapped_column(String(3)) # PAISOPCIONAL + origin_procedure: Mapped[Optional[str]] = mapped_column(String(3)) # PROCEDENCIA + scrap_procedure: Mapped[Optional[str]] = mapped_column(String(3)) # PROCSCRAP + + # Sector + sector: Mapped[Optional[str]] = mapped_column(String(8)) # SECTOR + + # Relationship + item_line: Mapped["LineItem"] = relationship() + +class LineDescription(Base): + """ + Description details for line items + Consolidates all line-level data from Q and S tables + """ + __tablename__ = "line_descriptions" + __table_args__ = { + "schema": "a76", + } + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + item_line_id: Mapped[int] = mapped_column(ForeignKey("a76.item_lines.id")) + + # Descriptions + description_spanish: Mapped[Optional[str]] = mapped_column(String(4999)) # DESCRIPCIONE + description_english: Mapped[Optional[str]] = mapped_column(String(4999)) # DESCRIPCIONI + extra_description: Mapped[Optional[str]] = mapped_column(Text) # DESCRIPCIONEEXTRA + part_description: Mapped[Optional[str]] = mapped_column(String(500)) # DESCRIPCIONPARTE + class_description: Mapped[Optional[str]] = mapped_column(String(500)) # DESCRIPCIONCLASE + + # Product attributes + brand: Mapped[Optional[str]] = mapped_column(String(50)) # MARCA + model: Mapped[Optional[str]] = mapped_column(String(50)) # MODELO + has_serial: Mapped[Optional[bool]] = mapped_column(Boolean) # LLEVASERIE + + # Additional information + additional_info_spanish: Mapped[Optional[str]] = mapped_column(String(1000)) # INFOADICIONESP + additional_info_english: Mapped[Optional[str]] = mapped_column(String(1000)) # INFOADICIONING + + # Lot and entry tracking + lot: Mapped[Optional[str]] = mapped_column(String(254)) # LOTE + entry_number: Mapped[Optional[str]] = mapped_column(String(50)) # NUMENTRADA/NUMERODEENTRADA + + # Relationship + item_line: Mapped["LineItem"] = relationship() + +class LineReference(Base): + """ + Reference details for line items + Consolidates all line-level data from Q and S tables + """ + __tablename__ = "line_references" + __table_args__ = { + "schema": "a76", + } + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + item_line_id: Mapped[int] = mapped_column(ForeignKey("a76.item_lines.id")) + + serie_id: Mapped[Optional[int]] = mapped_column(ForeignKey("a76.item_line_series.id")) # SERIEPARTIDA + + # Customer/Vendor + customer_invoice: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # CLIENTEFACTURAR + assigned_client: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # CLIENTEASIGNADO + supplier: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # PROVEEDOR + requisitioner: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # REQUISITOR + sent_to: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # ENVIADOA + + # PED line reference + ped_line: Mapped[Optional[int]] = mapped_column(Integer) # LINEAPED + ro_line: Mapped[Optional[int]] = mapped_column(Integer) # LINEARO + + # Relationship + item_line: Mapped["LineItem"] = relationship() + +# ============================================================================ +# SUPPORTING TABLES +# ============================================================================ + +class PackingList(Base): + """ + 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 RepairPart(Base): + """ + Repair parts (orphan table without primary key in original) + From: SPartidasRep + """ + __tablename__ = "repair_parts" + __table_args__ = { + "schema": "a76", + } + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + import_line: Mapped[int] = mapped_column(Integer) # LINEAIMPO + + +class CTMShipment(Base): + """ + CTM Shipment lines (temporary manufacturing) + From: SPartidasEnviaCTM + """ + __tablename__ = "ctm_shipments" + __table_args__ = { + "schema": "a76", + } + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + shipment_line: Mapped[int] = mapped_column(ForeignKey("a76.item_lines.id")) # LINEAENVIO + brand: Mapped[Optional[str]] = mapped_column(String(50)) # MARCA + model: Mapped[Optional[str]] = mapped_column(String(50)) # MODELO + serial_numbers: Mapped[Optional[str]] = mapped_column(String(50)) # SERIES + + +class CTMReceipt(Base): + """ + 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): + """ + 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 + + +class SubassemblyExit(Base): + """ + Subassembly/Submanufacturing Exit lines + From: SPartidasSalidaSM + """ + __tablename__ = "subassembly_exits" + __table_args__ = { + "schema": "a76", + } + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + exit_line: Mapped[int] = mapped_column(ForeignKey("a76.item_lines.id")) # LINEASALIDA + + +class ImpositionPart(Base): + """ + Imposition parts (orphan table without primary key constraint) + From: SPartidasImposion + """ + __tablename__ = "imposition_parts" + __table_args__ = { + "schema": "a76", + } + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + import_line: Mapped[int] = mapped_column(Integer) # LINEAIMPO + + + +# ============================================================================ +# 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(Item).filter( + Item.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(Item).join(LineItem).filter( + Item.system_origin == 'SCAF', + LineItem.value_depreciated_usd.isnot(None) + ) + ``` +""" \ No newline at end of file diff --git a/backend/api/v1/modules/a76/items/series/models.py b/backend/api/v1/modules/a76/items/series/models.py new file mode 100644 index 00000000..a944b080 --- /dev/null +++ b/backend/api/v1/modules/a76/items/series/models.py @@ -0,0 +1,23 @@ +from typing import Optional +from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, Numeric, String +from sqlalchemy.orm import Mapped, mapped_column +from core.database import Base + +class Serie(Base): + __tablename__ = "item_line_series" + __table_args__ = { + "schema": "a76", + } + + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + line_item_id: Mapped[int] = mapped_column(ForeignKey("a76.item_lines.id")) # LINEAIMPO / LINEAEXPO + row: Mapped[int] = mapped_column(Integer) # RENGLON + serial_numbers: Mapped[Optional[str]] = mapped_column(String(50)) # SERIEEXPO + model: Mapped[Optional[str]] = mapped_column(String(50)) # MODELOEXPO + sub_model: Mapped[Optional[str]] = mapped_column(String(50)) # SUBMODELOEXPO + brand: Mapped[Optional[str]] = mapped_column(String(50)) # MARCA + expo_brad: Mapped[Optional[str]] = mapped_column(String(50)) # MARCAEXPO + number_id: Mapped[Optional[str]] = mapped_column(String(25)) # NUMIDEXPO + + + \ No newline at end of file diff --git a/backend/api/v1/modules/a76/parts/models.py b/backend/api/v1/modules/a76/parts/models.py index a536d897..12750dca 100644 --- a/backend/api/v1/modules/a76/parts/models.py +++ b/backend/api/v1/modules/a76/parts/models.py @@ -9,12 +9,10 @@ from typing import TYPE_CHECKING, Optional from api.v1.common.base_models import TenantScopedMixin, TimestampMixin from core.database import Base from sqlalchemy import ( - ForeignKey, ForeignKeyConstraint, Integer, Numeric, PrimaryKeyConstraint, - SmallInteger, String, UniqueConstraint, Boolean, @@ -57,7 +55,7 @@ class Part(Base, TenantScopedMixin, TimestampMixin): # Unique constraint compuesta client_id: Mapped[int] = mapped_column(Integer) - part_number: Mapped[str] = mapped_column(String(49)) + part_number: Mapped[str] = mapped_column(String(50)) # Basic information fraction: Mapped[Optional[str]] = mapped_column(String(10)) diff --git a/backend/main.py b/backend/main.py index 197a111b..0959a83d 100644 --- a/backend/main.py +++ b/backend/main.py @@ -16,6 +16,9 @@ from core.middleware import ( from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware +from api.v1.modules.a76.items.models import Item # Importar rutas para registrar con el router +from api.v1.modules.a76.items.series.models import Serie # Importar modelos para registrar con SQLAlchemy + # Configurar logging logging.basicConfig( level=logging.INFO if not settings.DEBUG else logging.DEBUG, diff --git a/docker-compose.yml b/docker-compose.yml index 6d4d1ffb..18215907 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,13 @@ services: # PostgreSQL - Base de datos core (app) postgres-a76: - image: postgres:16-alpine + image: postgres:18-alpine container_name: anexo76-postgres-a76 environment: POSTGRES_DB: anexo76_core POSTGRES_USER: postgres POSTGRES_PASSWORD: ${POSTGRES_APP_PASSWORD:-postgres} POSTGRES_INITDB_ARGS: "--encoding=UTF8" - PGDATA: /var/lib/postgresql/data/pgdata ports: - "5432:5432" volumes: @@ -38,14 +37,13 @@ services: # PostgreSQL - Base de datos Keycloak postgres-keycloak: - image: postgres:16-alpine + image: postgres:18-alpine container_name: anexo76-postgres-keycloak environment: POSTGRES_DB: keycloak POSTGRES_USER: postgres POSTGRES_PASSWORD: ${POSTGRES_KEYCLOAK_PASSWORD:-postgres} POSTGRES_INITDB_ARGS: "--encoding=UTF8" - PGDATA: /var/lib/postgresql/data/pgdata ports: - "5433:5432" volumes: