feat: add items tab form and save invoice functionality

- Implemented the items tab form in Svelte for editing invoices, displaying item quantities and import values.
- Created saveInvoice function to handle both creation and updating of invoices, including validation of required fields and building a unified payload for API requests.
- Added support for compliance, financials, and logistics data in the invoice payload.
This commit is contained in:
AlexeerCT
2025-12-29 18:00:46 -06:00
parent e4b2a8f8df
commit 304f7b07d4
28 changed files with 2710 additions and 854 deletions

View File

@@ -0,0 +1,25 @@
"""
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
__all__ = [
"Item",
"LineItem",
"LineFinancial",
"LineQuantity",
"LineCustom",
"LineDescription",
"LineReference",
"CTMReceipt",
"SubassemblyEntry",
]

View File

@@ -0,0 +1,16 @@
"""Line customs module"""
from .models import LineCustom
from .schemas import (
LineCustomBase,
LineCustomCreate,
LineCustomUpdate,
LineCustomResponse,
)
__all__ = [
"LineCustom",
"LineCustomBase",
"LineCustomCreate",
"LineCustomUpdate",
"LineCustomResponse",
]

View File

@@ -0,0 +1,55 @@
from decimal import Decimal
from typing import Optional, TYPE_CHECKING
from sqlalchemy import String, Numeric, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from core.database import Base
if TYPE_CHECKING:
from ..line_items.models import LineItem
class LineCustom(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 (one-to-one)
line: Mapped["LineItem"] = relationship(back_populates="customs")

View File

@@ -0,0 +1,58 @@
from decimal import Decimal
from typing import Optional
from pydantic import BaseModel, Field, ConfigDict
# ============================================================================
# LINE CUSTOMS SCHEMAS
# ============================================================================
class LineCustomBase(BaseModel):
"""Base schema for line customs"""
# Tariff/Customs Classifications
fraction: Optional[str] = Field(None, max_length=10, description="Fraction (FRACCION/FRACCIONIMPO/FRACCIONEXPO)")
fraction_type: Optional[str] = Field(None, max_length=7, description="Fraction type (TIPOFRACCION/TIPOFRACCIONIMPO/TIPOFRACCIONEXPO)")
american_fraction: Optional[str] = Field(None, max_length=16, description="American fraction (FRACCIONAMERICANA)")
alternate_fraction: Optional[str] = Field(None, max_length=10, description="Alternate fraction (FRACCIONALTERNA)")
reference_fraction: Optional[str] = Field(None, max_length=10, description="Reference fraction (FRACCIONREFERENCIA)")
octave_fraction: Optional[str] = Field(None, max_length=10, description="Octave fraction (FRACCIONROCTAVA)")
tlcan_fraction: Optional[str] = Field(None, max_length=13, description="TLCAN fraction (FRACCIONTLCAN)")
extra_american_fraction: Optional[str] = Field(None, max_length=16, description="Extra American fraction (FRACAMESELEXTRA)")
garment_fraction: Optional[str] = Field(None, max_length=19, description="Garment fraction (FRACCIONDELAPRENDA/FRACCIONDELAPARTIDA)")
# Ad Valorem
advalorem: Optional[str] = Field(None, max_length=10, description="Ad valorem (ADVIMPO/ADVEXPO)")
advalorem_numeric: Optional[Decimal] = Field(None, description="Ad valorem numeric (ADVIMPONUM/ADVEXPONUM)")
advalorem_american: Optional[Decimal] = Field(None, description="Ad valorem American (ADVAME)")
advalorem_tlcan: Optional[Decimal] = Field(None, description="Ad valorem TLCAN (ADVTLCAN)")
# Rates
rate: Optional[str] = Field(None, max_length=10, description="Rate (TASAIM/TASAEX)")
depreciation_rate: Optional[Decimal] = Field(None, description="Depreciation rate (TASADEPRECIA)")
# Origin/Destination
origin_country: Optional[str] = Field(None, max_length=3, description="Origin country (PAISORIGEN)")
destination_country: Optional[str] = Field(None, max_length=3, description="Destination country (PAISDESTINO)")
optional_country: Optional[str] = Field(None, max_length=3, description="Optional country (PAISOPCIONAL)")
origin_procedure: Optional[str] = Field(None, max_length=3, description="Origin procedure (PROCEDENCIA)")
scrap_procedure: Optional[str] = Field(None, max_length=3, description="Scrap procedure (PROCSCRAP)")
# Sector
sector: Optional[str] = Field(None, max_length=8, description="Sector (SECTOR)")
class LineCustomCreate(LineCustomBase):
"""Schema for creating line customs"""
pass
class LineCustomUpdate(LineCustomBase):
"""Schema for updating line customs"""
pass
class LineCustomResponse(LineCustomBase):
"""Schema for line customs response"""
id: int
item_line_id: int
model_config = ConfigDict(from_attributes=True)

View File

@@ -0,0 +1,16 @@
"""Line descriptions module"""
from .models import LineDescription
from .schemas import (
LineDescriptionBase,
LineDescriptionCreate,
LineDescriptionUpdate,
LineDescriptionResponse,
)
__all__ = [
"LineDescription",
"LineDescriptionBase",
"LineDescriptionCreate",
"LineDescriptionUpdate",
"LineDescriptionResponse",
]

View File

@@ -0,0 +1,43 @@
from typing import Optional, TYPE_CHECKING
from sqlalchemy import Boolean, String, Text, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from core.database import Base
if TYPE_CHECKING:
from ..line_items.models import LineItem
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 (one-to-one)
line: Mapped["LineItem"] = relationship(back_populates="description")

View File

@@ -0,0 +1,46 @@
from typing import Optional
from pydantic import BaseModel, Field, ConfigDict
# ============================================================================
# LINE DESCRIPTION SCHEMAS
# ============================================================================
class LineDescriptionBase(BaseModel):
"""Base schema for line descriptions"""
# Descriptions
description_spanish: Optional[str] = Field(None, max_length=4999, description="Description in Spanish (DESCRIPCIONE)")
description_english: Optional[str] = Field(None, max_length=4999, description="Description in English (DESCRIPCIONI)")
extra_description: Optional[str] = Field(None, description="Extra description (DESCRIPCIONEEXTRA)")
part_description: Optional[str] = Field(None, max_length=500, description="Part description (DESCRIPCIONPARTE)")
class_description: Optional[str] = Field(None, max_length=500, description="Class description (DESCRIPCIONCLASE)")
# Product attributes
brand: Optional[str] = Field(None, max_length=50, description="Brand (MARCA)")
model: Optional[str] = Field(None, max_length=50, description="Model (MODELO)")
has_serial: Optional[bool] = Field(None, description="Has serial (LLEVASERIE)")
# Additional information
additional_info_spanish: Optional[str] = Field(None, max_length=1000, description="Additional info in Spanish (INFOADICIONESP)")
additional_info_english: Optional[str] = Field(None, max_length=1000, description="Additional info in English (INFOADICIONING)")
# Lot and entry tracking
lot: Optional[str] = Field(None, max_length=254, description="Lot (LOTE)")
entry_number: Optional[str] = Field(None, max_length=50, description="Entry number (NUMENTRADA/NUMERODEENTRADA)")
class LineDescriptionCreate(LineDescriptionBase):
"""Schema for creating line description"""
pass
class LineDescriptionUpdate(LineDescriptionBase):
"""Schema for updating line description"""
pass
class LineDescriptionResponse(LineDescriptionBase):
"""Schema for line description response"""
id: int
item_line_id: int
model_config = ConfigDict(from_attributes=True)

View File

@@ -0,0 +1,16 @@
"""Line financials module"""
from .models import LineFinancial
from .schemas import (
LineFinancialBase,
LineFinancialCreate,
LineFinancialUpdate,
LineFinancialResponse,
)
__all__ = [
"LineFinancial",
"LineFinancialBase",
"LineFinancialCreate",
"LineFinancialUpdate",
"LineFinancialResponse",
]

View File

@@ -0,0 +1,101 @@
from decimal import Decimal
from typing import Optional, TYPE_CHECKING
from sqlalchemy import Numeric, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from core.database import Base
if TYPE_CHECKING:
from ..line_items.models import LineItem
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 (one-to-one)
line: Mapped["LineItem"] = relationship(back_populates="financial")

View File

@@ -0,0 +1,104 @@
from decimal import Decimal
from typing import Optional
from pydantic import BaseModel, Field, ConfigDict
# ============================================================================
# LINE FINANCIAL SCHEMAS
# ============================================================================
class LineFinancialBase(BaseModel):
"""Base schema for line financials"""
# Costs - Capture
unit_cost_capture: Optional[Decimal] = Field(None, description="Unit cost in capture currency (COSTOUNITARIOCAPTURA)")
# Costs - USD
unit_cost_usd: Optional[Decimal] = Field(None, description="Unit cost in USD (COSTOUNITARIODLLS/COSTOUNITARIOME)")
unit_cost_commercial_usd: Optional[Decimal] = Field(None, description="Commercial unit cost in USD (COSTOUCOMDLLS)")
unit_cost_current_usd: Optional[Decimal] = Field(None, description="Current unit cost in USD (COSTOACTUALDLLS)")
unit_cost_depreciated_usd: Optional[Decimal] = Field(None, description="Depreciated unit cost in USD (COSTODEPRECME)")
unit_cost_subitem_usd: Optional[Decimal] = Field(None, description="Subitem unit cost in USD (COSTOUNITARIOSUBPDLLS)")
unit_cost_auxiliary_usd: Optional[Decimal] = Field(None, description="Auxiliary unit cost in USD (COSTOUAUXILIARME)")
sales_cost_usd: Optional[Decimal] = Field(None, description="Sales cost in USD (COSTOVENTAME)")
commercial_unit_cost: Optional[Decimal] = Field(None, description="Commercial unit cost (COSTOUNICOMERCIAL)")
# Costs - MXN
unit_cost_mxn: Optional[Decimal] = Field(None, description="Unit cost in MXN (COSTOUNITARIOPESOS/COSTOUNITARIOMN)")
unit_cost_commercial_mxn: Optional[Decimal] = Field(None, description="Commercial unit cost in MXN (COSTOUCOMPESOS)")
unit_cost_current_mxn: Optional[Decimal] = Field(None, description="Current unit cost in MXN (COSTOACTUALMN)")
unit_cost_depreciated_mxn: Optional[Decimal] = Field(None, description="Depreciated unit cost in MXN (COSTODEPRECMN)")
unit_cost_subitem_mxn: Optional[Decimal] = Field(None, description="Subitem unit cost in MXN (COSTOUNITARIOSUBPPESOS)")
sales_cost_mxn: Optional[Decimal] = Field(None, description="Sales cost in MXN (COSTOVENTAMN)")
# Costs - MC (Custom Currency)
unit_cost_mc: Optional[Decimal] = Field(None, description="Unit cost in MC (COSTOUNITARIOMC)")
# Values - MXN
value_mxn: Optional[Decimal] = Field(None, description="Value in MXN (VALORMN/VALORIMPOMN/VALOREXPOMN)")
value_commercial_mxn: Optional[Decimal] = Field(None, description="Commercial value in MXN (VALOREXPOCOMMN)")
value_updated_mxn: Optional[Decimal] = Field(None, description="Updated value in MXN (VALORACTUALIZADOMN)")
value_subitem_mxn: Optional[Decimal] = Field(None, description="Subitem value in MXN (VALORSUBPMN)")
sub_import_value_mxn: Optional[Decimal] = Field(None, description="Sub-import value in MXN (SUBVALORIMPOMN)")
value_returned_mxn: Optional[Decimal] = Field(None, description="Returned value in MXN (VALORRETORNADOMN)")
value_depreciated_mxn: Optional[Decimal] = Field(None, description="Depreciated value in MXN (VALORDEPRECIADOMN)")
customs_value_mxn: Optional[Decimal] = Field(None, description="Customs value in MXN (VALORADUANASMN)")
value_total_mxn: Optional[Decimal] = Field(None, description="Total value in MXN (VALORTOTALMN)")
value_temp_material_mxn: Optional[Decimal] = Field(None, description="Temporary material value in MXN (VALORMPTEMPMN)")
value_def_material_mxn: Optional[Decimal] = Field(None, description="Definitive material value in MXN (VALORMPDEFMN)")
value_added_mxn: Optional[Decimal] = Field(None, description="Added value in MXN (VALORAGREMN)")
value_national_packing_mxn: Optional[Decimal] = Field(None, description="National packing value in MXN (VALEMPAQUENACMN)")
vat_mxn: Optional[Decimal] = Field(None, description="VAT in MXN (IVAIMPOMN/IVAEXPOMN/VALORIVAMN)")
vat_used_mxn: Optional[Decimal] = Field(None, description="VAT used in MXN (VALORIVAMNUSADO)")
advalorem_line_mxn: Optional[Decimal] = Field(None, description="Ad valorem line in MXN (ADVALORMNLINEAPED)")
# Values - USD
value_usd: Optional[Decimal] = Field(None, description="Value in USD (VALORME/VALORIMPOME/VALOREXPOME)")
value_commercial_usd: Optional[Decimal] = Field(None, description="Commercial value in USD (VALOREXPOCOMME)")
value_updated_usd: Optional[Decimal] = Field(None, description="Updated value in USD (VALORACTUALIZADOME)")
value_subitem_usd: Optional[Decimal] = Field(None, description="Subitem value in USD (VALORSUBPME)")
sub_import_value_usd: Optional[Decimal] = Field(None, description="Sub-import value in USD (SUBVALORIMPOME)")
value_returned_usd: Optional[Decimal] = Field(None, description="Returned value in USD (VALORRETORNADOME)")
value_depreciated_usd: Optional[Decimal] = Field(None, description="Depreciated value in USD (VALORDEPRECIADOME)")
customs_value_usd: Optional[Decimal] = Field(None, description="Customs value in USD (VALORADUANASME)")
value_auxiliary_usd: Optional[Decimal] = Field(None, description="Auxiliary value in USD (VALORIMPOAUXILIARME/VALOREXPORAUXILIARME)")
value_total_usd: Optional[Decimal] = Field(None, description="Total value in USD (VALORTOTALME)")
value_temp_material_usd: Optional[Decimal] = Field(None, description="Temporary material value in USD (VALORMPTEMPME)")
value_def_material_usd: Optional[Decimal] = Field(None, description="Definitive material value in USD (VALORMPDEFME)")
value_added_usd: Optional[Decimal] = Field(None, description="Added value in USD (VALORAGREME)")
value_national_packing_usd: Optional[Decimal] = Field(None, description="National packing value in USD (VALEMPAQUENACME)")
value_us_packing_usd: Optional[Decimal] = Field(None, description="US packing value in USD (VALEMPAQUEUSME)")
vat_usd: Optional[Decimal] = Field(None, description="VAT in USD (IVAIMPOME/IVAEXPOME/VALORIVAME)")
vat_used_usd: Optional[Decimal] = Field(None, description="VAT used in USD (VALORIVAMEUSADO)")
value_non_originating_usd: Optional[Decimal] = Field(None, description="Non-originating value in USD (VALORNOORIGINARIOME)")
value_originating_usd: Optional[Decimal] = Field(None, description="Originating value in USD (VALORORIGINARIOME)")
igi_amount_usd: Optional[Decimal] = Field(None, description="IGI amount in USD (MONTOIGIME)")
exempt_amount_usd: Optional[Decimal] = Field(None, description="Exempt amount in USD (MONTOEXCENTOME)")
total_commercial_value: Optional[Decimal] = Field(None, description="Total commercial value (VALORTOTALCOMERCIAL)")
advalorem_line_usd: Optional[Decimal] = Field(None, description="Ad valorem line in USD (ADVALORMELINEAPED)")
# Values - MC
value_mc: Optional[Decimal] = Field(None, description="Value in MC (VALORIMPOMC/VALOREXPOMC)")
sub_import_value_mc: Optional[Decimal] = Field(None, description="Sub-import value in MC (SUBVALORIMPOMC)")
vat_mc: Optional[Decimal] = Field(None, description="VAT in MC (IVAIMPOMC/IVAEXPOMC)")
value_added_mc: Optional[Decimal] = Field(None, description="Added value in MC (VALORAGREMC)")
value_national_packing_mc: Optional[Decimal] = Field(None, description="National packing value in MC (VALEMPAQUENACMC)")
value_total_mc: Optional[Decimal] = Field(None, description="Total value in MC (VALORTOTALMC)")
value_temp_material_mc: Optional[Decimal] = Field(None, description="Temporary material value in MC (VALORMPTEMPMC)")
value_def_material_mc: Optional[Decimal] = Field(None, description="Definitive material value in MC (VALORMPDEFMC)")
class LineFinancialCreate(LineFinancialBase):
"""Schema for creating line financial"""
pass
class LineFinancialUpdate(LineFinancialBase):
"""Schema for updating line financial"""
pass
class LineFinancialResponse(LineFinancialBase):
"""Schema for line financial response"""
id: int
item_line_id: int
model_config = ConfigDict(from_attributes=True)

View File

@@ -0,0 +1,16 @@
"""Line items module"""
from .models import LineItem
from .schemas import (
LineItemBase,
LineItemCreate,
LineItemUpdate,
LineItemResponse,
)
__all__ = [
"LineItem",
"LineItemBase",
"LineItemCreate",
"LineItemUpdate",
"LineItemResponse",
]

View File

@@ -0,0 +1,179 @@
from decimal import Decimal
from typing import Optional, TYPE_CHECKING
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 ..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
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[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")
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)

View File

@@ -0,0 +1,165 @@
from decimal import Decimal
from typing import Optional
from pydantic import BaseModel, Field, ConfigDict
# Import nested schemas
from ..line_customs.schemas import (
LineCustomsCreate,
LineCustomsUpdate,
LineCustomsResponse
)
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
)
# ============================================================================
# LINE ITEM SCHEMAS
# ============================================================================
class LineItemBase(BaseModel):
"""Base schema for line items"""
line_number: int = Field(..., description="Line number")
# Part identification
part_number: Optional[str] = Field(None, max_length=50, description="Part number")
component_part_number: Optional[str] = Field(None, max_length=50, description="Component part number")
class_code: Optional[str] = Field(None, max_length=20, description="Class code")
# Unit of measure
unit_of_measure: Optional[str] = Field(None, max_length=10, description="Unit of measure")
alternate_unit: Optional[str] = Field(None, max_length=10, 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")
# Subitem flags
is_subitem: Optional[bool] = Field(None, description="Is subitem")
contains_subitems: Optional[bool] = Field(None, description="Contains subitems")
includes_subitems: Optional[bool] = Field(None, description="Includes subitems")
subitem_number: Optional[bool] = Field(None, description="Subitem number")
# Special flags
is_military_mcia: Optional[bool] = Field(None, description="Is military merchandise")
# 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"""
financial: Optional[LineFinancialCreate] = Field(None, description="Financial data for this line")
quantity: Optional[LineQuantityCreate] = Field(None, description="Quantity data for this line")
customs: Optional[LineCustomsCreate] = 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")
class LineItemUpdate(LineItemBase):
"""Schema for updating line item with all nested data"""
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[LineCustomsUpdate] = 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")
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[LineCustomsResponse] = None
description: Optional[LineDescriptionResponse] = None
reference: Optional[LineReferenceResponse] = None
model_config = ConfigDict(from_attributes=True)

View File

@@ -0,0 +1,16 @@
"""Line quantities module"""
from .models import LineQuantity
from .schemas import (
LineQuantityBase,
LineQuantityCreate,
LineQuantityUpdate,
LineQuantityResponse,
)
__all__ = [
"LineQuantity",
"LineQuantityBase",
"LineQuantityCreate",
"LineQuantityUpdate",
"LineQuantityResponse",
]

View File

@@ -0,0 +1,50 @@
from decimal import Decimal
from typing import Optional, TYPE_CHECKING
from sqlalchemy import String, Integer, Numeric, SmallInteger, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from core.database import Base
if TYPE_CHECKING:
from ..line_items.models import LineItem
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 (one-to-one)
line: Mapped["LineItem"] = relationship(back_populates="quantity")

View File

@@ -0,0 +1,53 @@
from decimal import Decimal
from typing import Optional
from pydantic import BaseModel, Field, ConfigDict
# ============================================================================
# LINE QUANTITY SCHEMAS
# ============================================================================
class LineQuantityBase(BaseModel):
"""Base schema for line quantities"""
# Quantities
quantity: Optional[Decimal] = Field(None, description="Main quantity (CANTIMPO/CANTEXPO/CANTIMPODEF)")
alternate_quantity: Optional[Decimal] = Field(None, description="Alternate quantity (CANTALTERNA)")
quantity_uma: Optional[Decimal] = Field(None, description="UMA quantity (CANTIMPOUMA/CANTEXPOUMA)")
auxiliary_quantity: Optional[Decimal] = Field(None, description="Auxiliary quantity (CANTIMPOAUXILIAR/CANTEXPOAUXILIAR)")
# Quantities - Special (SCAF specific)
quantity_temp_export: Optional[Decimal] = Field(None, description="Temporary export quantity (CANTEXPOTEMP)")
quantity_existence: Optional[Decimal] = Field(None, description="Existence quantity (CANTEXISTENCIA)")
quantity_returned: Optional[Decimal] = Field(None, description="Returned quantity (CANTRETORNADA)")
quantity_returned_temp: Optional[Decimal] = Field(None, description="Returned temporary quantity (CANTRETORNADATEMP)")
serial_count: Optional[int] = Field(None, description="Serial count (CANT_SERIES/CANT_SERIESDEF)")
# Weight
weight_unit: Optional[str] = Field(None, max_length=3, description="Weight unit ('KG' or 'LB')")
net_weight: Optional[Decimal] = Field(None, description="Net weight (PESONETO)")
gross_weight: Optional[Decimal] = Field(None, description="Gross weight (PESOBRUTO)")
# Packaging
package_key: Optional[str] = Field(None, max_length=5, description="Package key (CLAVEBULTOS)")
package_quantity: Optional[int] = Field(None, description="Package quantity (CANTBULTOS)")
package_description: Optional[str] = Field(None, max_length=40, description="Package description (DESCBULTOS)")
container_quantity: Optional[int] = Field(None, description="Container quantity (CANTBULCONT)")
container_description: Optional[str] = Field(None, max_length=40, description="Container description (DESCCONTENEDOR)")
box_count: Optional[str] = Field(None, max_length=30, description="Box count (NOCAJAS)")
class LineQuantityCreate(LineQuantityBase):
"""Schema for creating line quantity"""
pass
class LineQuantityUpdate(LineQuantityBase):
"""Schema for updating line quantity"""
pass
class LineQuantityResponse(LineQuantityBase):
"""Schema for line quantity response"""
id: int
item_line_id: int
model_config = ConfigDict(from_attributes=True)

View File

@@ -0,0 +1,16 @@
"""Line references module"""
from .models import LineReference
from .schemas import (
LineReferenceBase,
LineReferenceCreate,
LineReferenceUpdate,
LineReferenceResponse,
)
__all__ = [
"LineReference",
"LineReferenceBase",
"LineReferenceCreate",
"LineReferenceUpdate",
"LineReferenceResponse",
]

View File

@@ -0,0 +1,36 @@
from typing import Optional, TYPE_CHECKING
from sqlalchemy import Integer, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from core.database import Base
if TYPE_CHECKING:
from ..line_items.models import LineItem
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 (one-to-one)
line: Mapped["LineItem"] = relationship(back_populates="reference")

View File

@@ -0,0 +1,39 @@
from typing import Optional
from pydantic import BaseModel, Field, ConfigDict
# ============================================================================
# LINE REFERENCE SCHEMAS
# ============================================================================
class LineReferenceBase(BaseModel):
"""Base schema for line references"""
serie_id: Optional[int] = Field(None, description="Serie ID (SERIEPARTIDA)")
# Customer/Vendor
customer_invoice: Optional[str] = Field(None, description="Customer invoice (CLIENTEFACTURAR)")
assigned_client: Optional[str] = Field(None, description="Assigned client (CLIENTEASIGNADO)")
supplier: Optional[str] = Field(None, description="Supplier (PROVEEDOR)")
requisitioner: Optional[str] = Field(None, description="Requisitioner (REQUISITOR)")
sent_to: Optional[str] = Field(None, description="Sent to (ENVIADOA)")
# PED line reference
ped_line: Optional[int] = Field(None, description="PED line (LINEAPED)")
ro_line: Optional[int] = Field(None, description="RO line (LINEARO)")
class LineReferenceCreate(LineReferenceBase):
"""Schema for creating line reference"""
pass
class LineReferenceUpdate(LineReferenceBase):
"""Schema for updating line reference"""
pass
class LineReferenceResponse(LineReferenceBase):
"""Schema for line reference response"""
id: int
item_line_id: int
model_config = ConfigDict(from_attributes=True)

View File

@@ -3,22 +3,21 @@ 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 typing import Optional, TYPE_CHECKING, List
from sqlalchemy import Boolean, String, Integer, 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 .series.models import Serie
from .line_items.models import LineItem
# ============================================================================
# CORE ENTITIES
# ============================================================================
class Item(Base):
class Item(Base, TenantScopedMixin, TimestampMixin):
"""
Unified item header table for all import/export operations
Consolidates headers from both SCAF and SCAII systems
@@ -27,397 +26,47 @@ class Item(Base):
__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.
invoice_id: Mapped[int] = mapped_column(
ForeignKey("a76.invoice_header.id")) # CONSECUTIVO
# Type: IMPORT_TEMP, IMPORT_DEF, EXPORT, REPAIR, etc.
item_type: Mapped[str] = mapped_column(String(20))
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
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
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
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
# Relationships
lines: Mapped[list["LineItem"]] = relationship(back_populates="item", cascade="all, delete-orphan")
location: Mapped[Optional[str]] = mapped_column(
String(200)) # LOCALIZACION
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()
# Relationships (one-to-many)
lines: Mapped[List["LineItem"]] = relationship(
"LineItem", back_populates="item", cascade="all, delete-orphan")
# ============================================================================
# SUPPORTING TABLES
# ============================================================================
class PackingList(Base):
class PackingList(Base, TenantScopedMixin, TimestampMixin):
"""
Packing list items
From: SPartidasPackingList
@@ -426,44 +75,14 @@ class PackingList(Base):
__table_args__ = {
"schema": "a76",
}
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
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
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):
class CTMReceipt(Base, TenantScopedMixin, TimestampMixin):
"""
CTM Receipt lines (temporary manufacturing)
From: SPartidasReciboCTM
@@ -472,15 +91,17 @@ class CTMReceipt(Base):
__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
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
exit_invoice: Mapped[Optional[str]] = mapped_column(
String(19)) # FACTURASALIDA
class SubassemblyEntry(Base):
class SubassemblyEntry(Base, TenantScopedMixin, TimestampMixin):
"""
Subassembly/Submanufacturing Entry lines
From: SPartidasEntradaSM
@@ -489,46 +110,18 @@ class SubassemblyEntry(Base):
__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
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:
@@ -617,4 +210,4 @@ MIGRATION STRATEGY FROM ORIGINAL TABLES TO NORMALIZED SCHEMA:
LineItem.value_depreciated_usd.isnot(None)
)
```
"""
"""

View File

@@ -0,0 +1,234 @@
"""
API Endpoints for Items management
Handles CRUD operations for Item with one-to-many relationships to LineItems
"""
from typing import Dict, Any, Optional
from fastapi import APIRouter, Depends, HTTPException, Query, Path, status
from sqlalchemy.orm import Session
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,
)
from .service import ItemService
router = APIRouter(prefix="/items", tags=["Items"])
# ============================================================================
# ITEM CRUD ENDPOINTS
# ============================================================================
@router.post("/", response_model=ItemResponse, status_code=status.HTTP_201_CREATED)
async def create_item(
item_data: ItemCreate,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""
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
- Each LineItem has one LineDescription
- Each LineItem has one LineReference
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
service = ItemService()
return service.create(db, item_data, tenant_id, company_id)
@router.get("/{item_id}", response_model=ItemResponse)
async def get_item(
item_id: int = Path(..., description="Item ID"),
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""
Get a specific item by ID with all nested data
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
service = ItemService()
item = service.get_by_id(db, item_id, tenant_id, company_id)
if not item:
raise HTTPException(status_code=404, detail="Item not found")
return item
@router.get("/", response_model=ItemListResponse)
async def list_items(
company_id: int = Query(..., description="Company ID"),
skip: int = Query(0, ge=0, description="Number of records to skip"),
limit: int = Query(100, ge=1, le=1000,
description="Maximum records to return"),
invoice_id: Optional[int] = Query(
None, description="Filter by invoice ID"),
item_type: Optional[str] = Query(None, description="Filter by item type"),
system_origin: Optional[str] = Query(
None, description="Filter by system origin (SCAF/SCAII)"),
search: Optional[str] = Query(
None, description="Search term for invoice number, reference, order, or guide"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""
List items with optional filtering and pagination
Filters:
- invoice_id: Filter by specific invoice
- item_type: Filter by item type (IMPORT_TEMP, IMPORT_DEF, EXPORT, REPAIR)
- system_origin: Filter by system (SCAF, SCAII)
- search: Search across multiple fields
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
service = ItemService()
filters = {
"invoice_id": invoice_id,
"item_type": item_type,
"system_origin": system_origin,
"search": search,
}
items, total = service.get_all(
db, tenant_id, company_id, skip, limit, filters)
return ItemListResponse(
total=total,
items=items,
skip=skip,
limit=limit
)
@router.put("/{item_id}", response_model=ItemResponse)
async def update_item(
item_id: int = Path(..., description="Item ID"),
item_data: ItemUpdate = ...,
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""
Update an item and optionally its nested line data
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
# Verify the item exists and belongs to the tenant/company
service = ItemService()
existing_item = service.get_by_id(db, item_id, tenant_id, company_id)
if not existing_item:
raise HTTPException(status_code=404, detail="Item not found")
return service.update(db, item_id, item_data, tenant_id, company_id)
@router.delete("/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_item(
item_id: int = Path(..., description="Item ID"),
company_id: int = Query(..., description="Company ID"),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""
Delete an item and all its related data (cascade delete)
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
service = ItemService()
# Verify the item exists and belongs to the tenant/company
existing_item = service.get_by_id(db, item_id, tenant_id, company_id)
if not existing_item:
raise HTTPException(status_code=404, detail="Item not found")
success = service.delete(db, item_id, tenant_id, company_id)
if not success:
raise HTTPException(status_code=404, detail="Item not found")
return None
# ============================================================================
# ADDITIONAL ENDPOINTS FOR INVOICE
# ============================================================================
@router.get("/invoice/{invoice_id}/items", response_model=ItemListResponse)
async def get_items_by_invoice(
invoice_id: int = Path(..., description="Invoice ID"),
company_id: int = Query(..., description="Company ID"),
skip: int = Query(0, ge=0),
limit: int = Query(100, ge=1, le=1000),
db: Session = Depends(get_core_db),
current_user: Dict[str, Any] = Depends(get_current_user),
):
"""
Get all items for a specific invoice
"""
tenant_id = validate_access_to_resource(db, company_id, current_user)
service = ItemService()
items, total = service.get_by_invoice(
db, invoice_id, tenant_id, company_id, skip, limit)
return ItemListResponse(
total=total,
items=items,
skip=skip,
limit=limit
)
# STATISTICS & UTILITIES
# ============================================================================
@router.get("/stats/summary")
async def get_items_summary(
invoice_id: Optional[int] = Query(
None, description="Filter by invoice ID"),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""
Get summary statistics for items
"""
service = ItemService(db)
items, total = service.list_items(
skip=0,
limit=10000, # Get all for stats
invoice_id=invoice_id
)
# Calculate stats
stats = {
"total_items": total,
"by_type": {},
"by_system": {},
}
for item in items:
# Count by type
if item.item_type:
stats["by_type"][item.item_type] = stats["by_type"].get(
item.item_type, 0) + 1
# Count by system
if item.system_origin:
stats["by_system"][item.system_origin] = stats["by_system"].get(
item.system_origin, 0) + 1
return stats

View File

@@ -0,0 +1,87 @@
"""
Schemas for Items and related entities
Complete nested one-to-one structure:
Item -> LineItem -> LineFinancial -> LineQuantity -> LineCustoms -> LineDescription -> LineReference
"""
from typing import Optional
from datetime import datetime
from decimal import Decimal
from pydantic import BaseModel, Field, ConfigDict
# Import schemas from individual modules
from .line_items.schemas import (
LineItemCreate,
LineItemUpdate,
LineItemResponse
)
# ============================================================================
# ITEM SCHEMAS
# ============================================================================
class ItemBase(BaseModel):
"""Base schema for items"""
invoice_id: int = Field(..., description="Invoice ID")
item_type: str = Field(..., max_length=20,
description="Item type: IMPORT_TEMP, IMPORT_DEF, EXPORT, REPAIR")
system_origin: str = Field(..., max_length=10,
description="System origin: SCAF or SCAII")
# Item references
invoice_number: Optional[str] = Field(
None, max_length=15, description="Invoice number")
reference_number: Optional[str] = Field(
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")
# Dates
invoice_date: Optional[int] = Field(None, description="Invoice 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")
class ItemCreate(ItemBase):
"""Schema for creating item with nested lines (one-to-many)"""
lines: Optional[list[LineItemCreate]] = Field(
default=[], description="List of line items")
class ItemUpdate(ItemBase):
"""Schema for updating item"""
invoice_id: Optional[int] = Field(None, description="Invoice ID")
item_type: Optional[str] = Field(
None, max_length=20, description="Item type")
system_origin: Optional[str] = Field(
None, max_length=10, description="System origin")
lines: Optional[list[LineItemUpdate]] = Field(
None, description="List of line items to update")
class ItemResponse(ItemBase):
"""Schema for item response with nested data (one-to-many)"""
id: int
lines: list[LineItemResponse] = Field(
default=[], description="List of line items")
model_config = ConfigDict(from_attributes=True)
class ItemListResponse(BaseModel):
"""Schema for paginated item list"""
total: int = Field(..., description="Total number of items")
items: list[ItemResponse] = Field(..., description="List of items")
skip: int = Field(..., description="Number of skipped items")
limit: int = Field(..., description="Maximum items per page")
model_config = ConfigDict(from_attributes=True)

View File

@@ -1,9 +1,10 @@
from typing import Optional
from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, Numeric, String
from sqlalchemy import ForeignKey, Integer, String
from sqlalchemy.orm import Mapped, mapped_column
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
class Serie(Base):
class Serie(Base, TenantScopedMixin, TimestampMixin):
__tablename__ = "item_line_series"
__table_args__ = {
"schema": "a76",

View File

@@ -0,0 +1,800 @@
"""
Service layer for Items business logic
Handles CRUD operations for Item with complete one-to-one relationships:
Item -> LineItem -> LineFinancial
-> LineQuantity
-> LineCustoms
-> LineDescription
-> LineReference
"""
import logging
from typing import Optional, List, Tuple
from fastapi import HTTPException
from sqlalchemy import and_, or_
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session, joinedload
from api.v1.modules.a76.items.line_items.schemas import LineItemCreate, LineItemUpdate
from .schemas import ItemCreate, ItemUpdate
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
logger = logging.getLogger(__name__)
class ItemService:
"""
Service for managing Items and related entities with tenant/company isolation
"""
@staticmethod
def get_by_id(
db: Session,
item_id: int,
tenant_id: int,
company_id: int
) -> Optional[Item]:
"""Get an item by ID with tenant/company validation"""
return (
db.query(Item)
.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),
)
.filter(
Item.id == item_id,
Item.tenant_id == tenant_id,
Item.company_id == company_id,
)
.first()
)
@staticmethod
def get_all(
db: Session,
tenant_id: int,
company_id: int,
skip: int = 0,
limit: int = 100,
filters: Optional[dict] = None,
) -> Tuple[List[Item], int]:
"""Get all items for a tenant/company with pagination and optional filters"""
query = (
db.query(Item)
.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),
)
.filter(
Item.tenant_id == tenant_id,
Item.company_id == company_id,
)
)
# Apply filters if provided
if filters:
if filters.get("invoice_id"):
query = query.filter(Item.invoice_id == filters["invoice_id"])
if filters.get("item_type"):
query = query.filter(Item.item_type == filters["item_type"])
if filters.get("system_origin"):
query = query.filter(Item.system_origin ==
filters["system_origin"])
if filters.get("search"):
search_term = f"%{filters['search']}%"
query = query.filter(
or_(
Item.invoice_number.ilike(search_term),
Item.reference_number.ilike(search_term),
Item.order.ilike(search_term),
Item.guide_number.ilike(search_term),
)
)
total = query.count()
items = query.offset(skip).limit(limit).all()
return items, total
@staticmethod
def get_by_invoice(
db: Session,
invoice_id: int,
tenant_id: int,
company_id: int,
skip: int = 0,
limit: int = 100,
) -> Tuple[List[Item], int]:
"""Get all items for a specific invoice"""
query = (
db.query(Item)
.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),
)
.filter(
Item.invoice_id == invoice_id,
Item.tenant_id == tenant_id,
Item.company_id == company_id,
)
)
total = query.count()
items = query.offset(skip).limit(limit).all()
return items, total
@staticmethod
def create(
db: Session,
item_data: ItemCreate,
tenant_id: int,
company_id: int,
) -> Item:
"""Create a new item with all related nested data (multiple lines)"""
try:
# Extract lines data
lines_data = item_data.lines or []
item_dict = item_data.model_dump(exclude={"lines"})
# Add tenant and company
item_dict["tenant_id"] = tenant_id
item_dict["company_id"] = company_id
# Create the item
db_item = Item(**item_dict)
db.add(db_item)
db.flush() # Get the item ID
# Create line items if provided
for line_data in lines_data:
# Extract nested data from line
financial_data = line_data.financial
quantity_data = line_data.quantity
customs_data = line_data.customs
description_data = line_data.description
reference_data = line_data.reference
line_dict = line_data.model_dump(
exclude={"financial", "quantity",
"customs", "description", "reference"}
)
line_dict["item_id"] = db_item.id
# Create line item
db_line = LineItem(**line_dict)
db.add(db_line)
db.flush() # Get the line ID
# Create financial data if provided
if financial_data:
financial_dict = financial_data.model_dump()
financial_dict["item_line_id"] = db_line.id
db_financial = LineFinancial(**financial_dict)
db.add(db_financial)
# Create quantity data if provided
if quantity_data:
quantity_dict = quantity_data.model_dump()
quantity_dict["item_line_id"] = db_line.id
db_quantity = LineQuantity(**quantity_dict)
db.add(db_quantity)
# Create customs data if provided
if customs_data:
customs_dict = customs_data.model_dump()
customs_dict["item_line_id"] = db_line.id
db_customs = LineCustom(**customs_dict)
db.add(db_customs)
# Create description data if provided
if description_data:
description_dict = description_data.model_dump()
description_dict["item_line_id"] = db_line.id
db_description = LineDescription(**description_dict)
db.add(db_description)
# Create reference data if provided
if reference_data:
reference_dict = reference_data.model_dump()
reference_dict["item_line_id"] = db_line.id
db_reference = LineReference(**reference_dict)
db.add(db_reference)
db.commit()
db.refresh(db_item)
return db_item
except IntegrityError as e:
db.rollback()
logger.error(f"Error creating item: {e}")
raise HTTPException(
status_code=400,
detail="Item 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")
@staticmethod
def update(
db: Session,
item_id: int,
item_data: ItemUpdate,
tenant_id: int,
company_id: int,
) -> Item:
"""Update an item and optionally its nested data (multiple lines)"""
try:
# 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")
# Extract lines data
lines_data = item_data.lines
item_dict = item_data.model_dump(
exclude={"lines"}, exclude_unset=True)
# 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()
# Create new lines
for line_data in lines_data:
# Extract nested data from line
financial_data = line_data.financial
quantity_data = line_data.quantity
customs_data = line_data.customs
description_data = line_data.description
reference_data = line_data.reference
line_dict = line_data.model_dump(
exclude={"financial", "quantity",
"customs", "description", "reference"},
exclude_unset=True
)
line_dict["item_id"] = db_item.id
db_line = LineItem(**line_dict)
db.add(db_line)
db.flush()
# Create nested data if provided
if financial_data is not None:
financial_dict = financial_data.model_dump(
exclude_unset=True)
financial_dict["item_line_id"] = db_line.id
db.add(LineFinancial(**financial_dict))
if quantity_data is not None:
quantity_dict = quantity_data.model_dump(
exclude_unset=True)
quantity_dict["item_line_id"] = db_line.id
db.add(LineQuantity(**quantity_dict))
if customs_data is not None:
customs_dict = customs_data.model_dump(
exclude_unset=True)
customs_dict["item_line_id"] = db_line.id
db.add(LineCustom(**customs_dict))
if description_data is not None:
description_dict = description_data.model_dump(
exclude_unset=True)
description_dict["item_line_id"] = db_line.id
db.add(LineDescription(**description_dict))
if reference_data is not None:
reference_dict = reference_data.model_dump(
exclude_unset=True)
reference_dict["item_line_id"] = db_line.id
db.add(LineReference(**reference_dict))
db.commit()
db.refresh(db_item)
return db_item
except HTTPException:
raise
except Exception as e:
db.rollback()
logger.error(f"Unexpected error updating item: {e}")
raise HTTPException(status_code=500, detail="Error updating item")
@staticmethod
def delete(
db: Session,
item_id: int,
tenant_id: int,
company_id: int,
) -> bool:
"""Delete an item and all its related data (cascade delete)"""
try:
db_item = ItemService.get_by_id(db, item_id, tenant_id, company_id)
if not db_item:
return False
db.delete(db_item)
db.commit()
return True
except Exception as e:
db.rollback()
logger.error(f"Error deleting item: {e}")
raise HTTPException(status_code=500, detail="Error deleting item")
def __init__(self, db: Session):
self.db = db
def create_item(self, item_data: ItemCreate) -> Item:
"""
Create a new item with its nested line item (one-to-one)
Also creates the line's financial and quantity data
"""
try:
# Extract line data before creating the item
line_data = item_data.line
item_dict = item_data.model_dump(exclude={'line'})
# Create the item
db_item = Item(**item_dict)
self.db.add(db_item)
self.db.flush() # Get the item ID without committing
# Create line item with nested data if provided
if line_data:
self._create_line_item(db_item.id, line_data)
self.db.commit()
self.db.refresh(db_item)
return db_item
except IntegrityError as e:
self.db.rollback()
logger.error(f"Integrity error creating item: {e}")
raise HTTPException(
status_code=400,
detail="Item creation failed due to data integrity constraint"
)
except Exception as e:
self.db.rollback()
logger.error(f"Unexpected error creating item: {e}")
raise HTTPException(
status_code=500,
detail=f"Error creating item: {str(e)}"
)
def _create_line_item(self, item_id: int, line_data: LineItemCreate) -> LineItem:
"""
Create a line item with all its nested data (financial, quantity, customs, description, reference)
"""
# Extract nested data
financial_data = line_data.financial
quantity_data = line_data.quantity
customs_data = line_data.customs
description_data = line_data.description
reference_data = line_data.reference
line_dict = line_data.model_dump(exclude={
'financial', 'quantity', 'customs', 'description', 'reference'
})
# Create line item
db_line = LineItem(item_id=item_id, **line_dict)
self.db.add(db_line)
self.db.flush() # Get the line ID
# Create financial data if provided
if financial_data:
db_financial = LineFinancial(
item_line_id=db_line.id,
**financial_data.model_dump()
)
self.db.add(db_financial)
# Create quantity data if provided
if quantity_data:
db_quantity = LineQuantity(
item_line_id=db_line.id,
**quantity_data.model_dump()
)
self.db.add(db_quantity)
# Create customs data if provided
if customs_data:
db_customs = LineCustom(
item_line_id=db_line.id,
**customs_data.model_dump()
)
self.db.add(db_customs)
# Create description data if provided
if description_data:
db_description = LineDescription(
item_line_id=db_line.id,
**description_data.model_dump()
)
self.db.add(db_description)
# Create reference data if provided
if reference_data:
db_reference = LineReference(
item_line_id=db_line.id,
**reference_data.model_dump()
)
self.db.add(db_reference)
return db_line
def get_item(self, item_id: int) -> Optional[Item]:
"""
Get an item by ID with all nested data loaded
"""
item = (
self.db.query(Item)
.options(joinedload(Item.lines))
.filter(Item.id == item_id)
.first()
)
if not item:
raise HTTPException(
status_code=404,
detail=f"Item with id {item_id} not found"
)
return item
def list_items(
self,
skip: int = 0,
limit: int = 100,
invoice_id: Optional[int] = None,
item_type: Optional[str] = None,
system_origin: Optional[str] = None,
) -> tuple[list[Item], int]:
"""
List items with optional filters and pagination
Returns tuple of (items, total_count)
"""
query = self.db.query(Item).options(joinedload(Item.lines))
# Apply filters
if invoice_id:
query = query.filter(Item.invoice_id == invoice_id)
if item_type:
query = query.filter(Item.item_type == item_type)
if system_origin:
query = query.filter(Item.system_origin == system_origin)
# Get total count
total = query.count()
# Apply pagination
items = query.offset(skip).limit(limit).all()
return items, total
def update_item(self, item_id: int, item_data: ItemUpdate) -> Item:
"""
Update an item and optionally its line item (one-to-one)
"""
try:
db_item = self.get_item(item_id)
# Extract line data
line_data = item_data.line
update_dict = item_data.model_dump(
exclude={'line'}, exclude_unset=True)
# Update item fields
for field, value in update_dict.items():
setattr(db_item, field, value)
# Update line if provided
if line_data is not None:
# Get the existing line or create new one
db_line = (
self.db.query(LineItem)
.filter(LineItem.item_id == item_id)
.first()
)
if db_line:
self._update_line_item(db_line, line_data)
else:
# Create new line if it doesn't exist
self._create_line_item(item_id, line_data)
self.db.commit()
self.db.refresh(db_item)
return db_item
except HTTPException:
raise
except IntegrityError as e:
self.db.rollback()
logger.error(f"Integrity error updating item: {e}")
raise HTTPException(
status_code=400,
detail="Item update failed due to data integrity constraint"
)
except Exception as e:
self.db.rollback()
logger.error(f"Unexpected error updating item: {e}")
raise HTTPException(
status_code=500,
detail=f"Error updating item: {str(e)}"
)
def _update_line_item(self, db_line: LineItem, line_data: LineItemUpdate):
"""
Update a line item and all its nested data
"""
# Extract nested data
financial_data = line_data.financial
quantity_data = line_data.quantity
customs_data = line_data.customs
description_data = line_data.description
reference_data = line_data.reference
line_dict = line_data.model_dump(
exclude={'financial', 'quantity',
'customs', 'description', 'reference'},
exclude_unset=True
)
# Update line fields
for field, value in line_dict.items():
setattr(db_line, field, value)
# Update financial data
if financial_data:
db_financial = (
self.db.query(LineFinancial)
.filter(LineFinancial.item_line_id == db_line.id)
.first()
)
if db_financial:
# Update existing
for field, value in financial_data.model_dump(exclude_unset=True).items():
setattr(db_financial, field, value)
else:
# Create new
db_financial = LineFinancial(
item_line_id=db_line.id,
**financial_data.model_dump(exclude_unset=True)
)
self.db.add(db_financial)
# Update quantity data
if quantity_data:
db_quantity = (
self.db.query(LineQuantity)
.filter(LineQuantity.item_line_id == db_line.id)
.first()
)
if db_quantity:
# Update existing
for field, value in quantity_data.model_dump(exclude_unset=True).items():
setattr(db_quantity, field, value)
else:
# Create new
db_quantity = LineQuantity(
item_line_id=db_line.id,
**quantity_data.model_dump(exclude_unset=True)
)
self.db.add(db_quantity)
# Update customs data
if customs_data:
db_customs = (
self.db.query(LineCustom)
.filter(LineCustom.item_line_id == db_line.id)
.first()
)
if db_customs:
# Update existing
for field, value in customs_data.model_dump(exclude_unset=True).items():
setattr(db_customs, field, value)
else:
# Create new
db_customs = LineCustom(
item_line_id=db_line.id,
**customs_data.model_dump(exclude_unset=True)
)
self.db.add(db_customs)
# Update description data
if description_data:
db_description = (
self.db.query(LineDescription)
.filter(LineDescription.item_line_id == db_line.id)
.first()
)
if db_description:
# Update existing
for field, value in description_data.model_dump(exclude_unset=True).items():
setattr(db_description, field, value)
else:
# Create new
db_description = LineDescription(
item_line_id=db_line.id,
**description_data.model_dump(exclude_unset=True)
)
self.db.add(db_description)
# Update reference data
if reference_data:
db_reference = (
self.db.query(LineReference)
.filter(LineReference.item_line_id == db_line.id)
.first()
)
if db_reference:
# Update existing
for field, value in reference_data.model_dump(exclude_unset=True).items():
setattr(db_reference, field, value)
else:
# Create new
db_reference = LineReference(
item_line_id=db_line.id,
**reference_data.model_dump(exclude_unset=True)
)
self.db.add(db_reference)
def delete_item(self, item_id: int) -> bool:
"""
Delete an item and all its related data (cascade)
"""
try:
db_item = self.get_item(item_id)
self.db.delete(db_item)
self.db.commit()
return True
except HTTPException:
raise
except Exception as e:
self.db.rollback()
logger.error(f"Error deleting item: {e}")
raise HTTPException(
status_code=500,
detail=f"Error deleting item: {str(e)}"
)
def search_items(
self,
search_term: Optional[str] = None,
skip: int = 0,
limit: int = 100
) -> tuple[list[Item], int]:
"""
Search items by various fields
"""
query = self.db.query(Item).options(joinedload(Item.lines))
if search_term:
search_filter = or_(
Item.invoice_number.ilike(f"%{search_term}%"),
Item.reference_number.ilike(f"%{search_term}%"),
Item.order.ilike(f"%{search_term}%"),
Item.guide_number.ilike(f"%{search_term}%"),
)
query = query.filter(search_filter)
total = query.count()
items = query.offset(skip).limit(limit).all()
return items, total
# ========================================================================
# LINE ITEM SPECIFIC OPERATIONS (one-to-one)
# ========================================================================
def get_line_for_item(self, item_id: int) -> Optional[LineItem]:
"""
Get the line item for a specific item
"""
db_line = (
self.db.query(LineItem)
.filter(LineItem.item_id == item_id)
.first()
)
return db_line
def create_or_replace_line(self, item_id: int, line_data: LineItemCreate) -> LineItem:
"""
Create or replace the line for an item (one-to-one relationship)
"""
try:
# Verify item exists
db_item = self.get_item(item_id)
# Check if line already exists
existing_line = (
self.db.query(LineItem)
.filter(LineItem.item_id == item_id)
.first()
)
if existing_line:
# Delete existing line (cascade will delete financials and quantities)
self.db.delete(existing_line)
self.db.flush()
# Create new line
db_line = self._create_line_item(item_id, line_data)
self.db.commit()
self.db.refresh(db_line)
return db_line
except HTTPException:
raise
except Exception as e:
self.db.rollback()
logger.error(f"Error creating/replacing line for item: {e}")
raise HTTPException(
status_code=500,
detail=f"Error creating/replacing line for item: {str(e)}"
)
def delete_line_from_item(self, item_id: int) -> bool:
"""
Delete the line from an item
"""
try:
db_line = (
self.db.query(LineItem)
.filter(LineItem.item_id == item_id)
.first()
)
if not db_line:
raise HTTPException(
status_code=404,
detail=f"No line found for item {item_id}"
)
self.db.delete(db_line)
self.db.commit()
return True
except HTTPException:
raise
except Exception as e:
self.db.rollback()
logger.error(f"Error deleting line from item: {e}")
raise HTTPException(
status_code=500,
detail=f"Error deleting line from item: {str(e)}"
)

View File

@@ -5,6 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite dev",
"i18n:compile": "paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide",
"build": "vite build",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",

View File

@@ -0,0 +1,60 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Button } from '$lib/components/ui/button';
import { Checkbox } from '$lib/components/ui/checkbox';
import { RadioGroup, RadioGroupItem } from '$lib/components/ui/radio-group';
import * as Select from '$lib/components/ui/select';
import { Textarea } from '$lib/components/ui/textarea';
import { Plus, Upload } from 'lucide-svelte';
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
let {
invoice,
formData = $bindable(),
exists = $bindable()
}: {
invoice: Invoice | null;
formData?: any;
exists?: boolean;
} = $props();
let imported = 0;
let net_weight = 0;
let gross_weight = 0;
</script>
<div class="grid grid-cols-4 grid-rows-1 gap-3">
<div class="border rounded-md p-3 space-y-3 col-span-3">
1
</div>
<div class="border rounded-md p-3 space-y-3 col-start-4">
<div>
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Cantidades:</h4>
<div>
<div class="grid grid-cols-2 gap-3">
<div>
Partidas: <span class="text-blue-400">{invoice?.items?.length || 0}</span>
</div>
<div>
Bultos: <span class="text-blue-400">{invoice?.packages || 0}</span>
</div>
</div>
</div>
Importada: <span class="text-blue-400">{imported || 0}</span> <br>
Peso neto: <span class="text-blue-400">{net_weight || 0}</span><br>
Peso bruto: <span class="text-blue-400">{gross_weight || 0}</span> <br>
</div>
<h4 class="text-xs font-semibold text-muted-foreground uppercase col-span-2">Valores de importacion:</h4>
Dolares: <span class="text-blue-400">{invoice?.items || 0}</span> <span class="text-red-400">USD</span> <br>
Pesos: <span class="text-blue-400">{invoice?.items || 0}</span> <span class="text-red-400">MXN</span><br>
De Captura: <span class="text-blue-400">{invoice?.items || 0}</span> <span class="text-red-400">USD</span>
<h4 class="text-xs font-semibold text-muted-foreground uppercase col-span-2 opacity-0">spacer</h4>
Aduana: <span class="text-blue-400">{invoice?.items || 0}</span> <span class="text-red-400">USD</span><br>
Aduana: <span class="text-blue-400">{invoice?.items || 0}</span> <span class="text-red-400">MXN</span><br>
</div>
</div>

View File

@@ -0,0 +1,406 @@
import { goto } from '$app/navigation';
import { invoicesApi, type CreateInvoiceData, type UpdateInvoiceData, type OperationType } from '$lib/api/dashboard/a76/invoices';
interface FormDataSet {
generalFormData: any;
observationFormData: any;
itemsFormData: any;
othersFormData: any;
}
interface SaveInvoiceOptions {
invoiceId: number | null;
isCreate: boolean;
companyId: number;
formData: FormDataSet;
}
export async function saveInvoice(options: SaveInvoiceOptions): Promise<{ success: boolean; error?: string; newInvoiceId?: number }> {
const { invoiceId, isCreate, companyId, formData } = options;
const { generalFormData, observationFormData, itemsFormData, othersFormData } = formData;
try {
// Validar campos requeridos para creación
if (isCreate && generalFormData) {
const requiredFields = {
operation_type: 'Tipo de Operación',
invoice_type: 'Tipo de Factura',
invoice_number: 'Número de Factura'
};
const missingFields: string[] = [];
for (const [field, label] of Object.entries(requiredFields)) {
const value = generalFormData[field];
if (value === null || value === undefined || value === '') {
missingFields.push(label);
}
}
if (missingFields.length > 0) {
throw new Error(`Los siguientes campos son obligatorios: ${missingFields.join(', ')}`);
}
}
// Construir el payload unificado
const payload = buildInvoicePayload(formData);
let newInvoiceId = invoiceId;
if (isCreate) {
// Crear nueva factura con todos sus sub-recursos
const response = await invoicesApi.create(companyId, payload as CreateInvoiceData);
if (response.error) {
const errorMsg = typeof response.error === 'string' ? response.error : 'Error al crear la factura';
throw new Error(errorMsg);
}
if (!response.data?.id) throw new Error('No se recibió el ID de la factura creada');
newInvoiceId = response.data.id;
// Redirigir a la página de edición
await goto(`/dashboard/invoices/edit/${newInvoiceId}`);
} else {
// Actualizar factura existente con todos sus sub-recursos
const response = await invoicesApi.update(invoiceId!, companyId, payload as UpdateInvoiceData);
if (response.error) throw new Error(response.error);
}
return { success: true, newInvoiceId: newInvoiceId ?? undefined };
} catch (e) {
const error = e instanceof Error ? e.message : 'Error al guardar los cambios';
return { success: false, error };
}
}
function buildInvoicePayload(formData: FormDataSet): CreateInvoiceData | UpdateInvoiceData {
const { generalFormData, observationFormData, itemsFormData, othersFormData } = formData;
const payload: CreateInvoiceData | UpdateInvoiceData = {
// Datos generales desde el formulario general
operation_type: generalFormData?.operation_type !== null && generalFormData?.operation_type !== undefined
? (generalFormData.operation_type === 1 ? 'exp' : 'imp') as OperationType
: undefined,
invoice_type: generalFormData?.invoice_type || undefined,
invoice_number: generalFormData?.invoice_number || undefined,
invoice_date: generalFormData?.invoice_date || undefined,
emission_date: generalFormData?.emission_date || undefined,
// Observation fields from observationFormData
observation_es: observationFormData?.observation_es || undefined,
observation_en: observationFormData?.observation_en || undefined,
alternate_invoice: observationFormData?.alternate_invoice || undefined,
};
// Solo agregar sub-recursos si tienen valores reales
// Compliance MX
const hasComplianceValue = generalFormData?.pedimento || generalFormData?.remesa || generalFormData?.aduana ||
generalFormData?.provider_id || generalFormData?.sold_to_id ||
generalFormData?.shipped_to_id || generalFormData?.customs_broker_id ||
observationFormData?.pedimento || observationFormData?.pedimento_code ||
observationFormData?.remesa || observationFormData?.aduana ||
observationFormData?.provider_id || observationFormData?.sold_to_id ||
observationFormData?.shipped_to_id || observationFormData?.shipped_by_id ||
observationFormData?.customs_broker_id || observationFormData?.is_mixed ||
observationFormData?.waste_type || observationFormData?.appendix_17 ||
observationFormData?.edocument || observationFormData?.electronic_signature ||
observationFormData?.sem_id || observationFormData?.enclosure ||
observationFormData?.incoterm;
if (hasComplianceValue) {
payload.compliance_mx = buildComplianceMxData(generalFormData, observationFormData);
}
// Financials
const hasFinancialsValue = generalFormData?.currency_type ||
generalFormData?.iva_factor ||
itemsFormData?.currency || itemsFormData?.exchange_rate ||
itemsFormData?.value_mn || itemsFormData?.value_me ||
itemsFormData?.customs_value_mn || itemsFormData?.freight ||
itemsFormData?.insurance;
if (hasFinancialsValue) {
payload.financials = buildFinancialsData(generalFormData, itemsFormData, observationFormData);
}
// Logistics
const hasLogisticsFromGeneral = generalFormData?.carrier_id ||
generalFormData?.driver_name || generalFormData?.transport_type || generalFormData?.transport_num;
if (hasLogisticsFromGeneral || (othersFormData && Array.isArray(othersFormData) && othersFormData.length > 0)) {
payload.logistics = buildLogisticsData(generalFormData, othersFormData, observationFormData);
}
// Eliminar campos undefined para no enviarlos
Object.keys(payload).forEach(key => {
if (payload[key as keyof typeof payload] === undefined) {
delete payload[key as keyof typeof payload];
}
});
return payload;
}
function buildComplianceMxData(generalFormData: any, observationFormData: any) {
return {
// Pedimento fields
pedimento: generalFormData?.pedimento || observationFormData?.pedimento || null,
pedimento_code: observationFormData?.pedimento_code || null,
pedimento_k1: observationFormData?.pedimento_k1 || null,
remesa: generalFormData?.remesa || observationFormData?.remesa || null,
aduana: generalFormData?.aduana || observationFormData?.aduana || null,
port_of_entry: observationFormData?.port_of_entry || null,
destination: observationFormData?.destination || null,
manifest_number: observationFormData?.manifest_number || null,
// Client/Provider fields
provider_header: generalFormData?.provider_header || observationFormData?.provider_header || null,
provider_id: generalFormData?.provider_id || observationFormData?.provider_id || null,
sold_to_header: generalFormData?.sold_to_header || observationFormData?.sold_to_header || null,
sold_to_id: generalFormData?.sold_to_id || observationFormData?.sold_to_id || null,
shipped_to_header: generalFormData?.shipped_to_header || observationFormData?.shipped_to_header || null,
shipped_to_id: generalFormData?.shipped_to_id || observationFormData?.shipped_to_id || null,
shipped_by_header: observationFormData?.shipped_by_header || null,
shipped_by_id: observationFormData?.shipped_by_id || null,
// Customs broker fields
customs_broker_id: generalFormData?.customs_broker_id || observationFormData?.customs_broker_id || null,
customs_broker_us_id: observationFormData?.customs_broker_us_id || null,
broker_invoice_num: observationFormData?.broker_invoice_num || null,
broker_invoice_date: observationFormData?.broker_invoice_date || null,
// Flags & regimes
is_mixed: observationFormData?.is_mixed || null,
waste_type: observationFormData?.waste_type || null,
scrap_type: observationFormData?.scrap_type || null,
appendix_17: observationFormData?.appendix_17 || null,
is_regime_change: observationFormData?.is_regime_change || null,
which_exchange_rate: observationFormData?.which_exchange_rate || null,
value_method: observationFormData?.value_method || null,
act_value: observationFormData?.act_value || null,
is_pedimento_pending: observationFormData?.is_pedimento_pending || null,
// Ownership & balances
is_owner_of_goods: observationFormData?.is_owner_of_goods || null,
generate_balances: observationFormData?.generate_balances || null,
was_reviewed_by_company: observationFormData?.was_reviewed_by_company || null,
// VUCEM / Digital
edocument: observationFormData?.edocument || null,
electronic_signature: observationFormData?.electronic_signature || null,
certificate_number: observationFormData?.certificate_number || null,
niu_number: observationFormData?.niu_number || null,
bill_of_lading_count: observationFormData?.bill_of_lading_count || null,
addendum_vu: observationFormData?.addendum_vu || null,
origin_destination_cove: observationFormData?.origin_destination_cove || null,
vucem_operation_num: observationFormData?.vucem_operation_num || null,
customs_person_line: observationFormData?.customs_person_line || null,
// Additional control
contingency_mode: observationFormData?.contingency_mode || null,
enclosure: observationFormData?.enclosure || null,
guide_type_to_identify: observationFormData?.guide_type_to_identify || null,
location: observationFormData?.location || null,
// DOT & official
dot_code: observationFormData?.dot_code || null,
subdivision: observationFormData?.subdivision || null,
acts_as: observationFormData?.acts_as || null,
movement_type: observationFormData?.movement_type || null,
office_document: observationFormData?.office_document || null,
reason_export: observationFormData?.reason_export || null,
signature_key: observationFormData?.signature_key || null,
// SM specific
sem_id: observationFormData?.sem_id || null,
};
}
function buildFinancialsData(generalFormData: any, itemsFormData: any, observationFormData: any) {
return {
// Currency
currency: itemsFormData?.currency || null,
currency_type: generalFormData?.currency_type || itemsFormData?.currency_type || null,
exchange_rate: itemsFormData?.exchange_rate || null,
exchange_rate_mm: itemsFormData?.exchange_rate_mm || null,
// Merchandise values
value_mn: itemsFormData?.value_mn || null,
value_me: itemsFormData?.value_me || null,
value_mc: itemsFormData?.value_mc || null,
// Customs value
customs_value_mn: itemsFormData?.customs_value_mn || null,
customs_value_me: itemsFormData?.customs_value_me || null,
// Raw materials
raw_material_value_mn: itemsFormData?.raw_material_value_mn || null,
raw_material_value_me: itemsFormData?.raw_material_value_me || null,
// Aggregate value
aggregate_value_mn: itemsFormData?.aggregate_value_mn || null,
aggregate_value_me: itemsFormData?.aggregate_value_me || null,
aggregate_value_mc: itemsFormData?.aggregate_value_mc || null,
// Mexican merchandise value
mexican_value_mn: itemsFormData?.mexican_value_mn || null,
mexican_value_me: itemsFormData?.mexican_value_me || null,
mexican_value_mc: itemsFormData?.mexican_value_mc || null,
// National packaging
national_packaging_mn: itemsFormData?.national_packaging_mn || null,
national_packaging_me: itemsFormData?.national_packaging_me || null,
national_packaging_mc: itemsFormData?.national_packaging_mc || null,
// Costs & increments
freight: itemsFormData?.freight || observationFormData?.freight || null,
insurance: itemsFormData?.insurance || observationFormData?.insurance || null,
insurance_value: itemsFormData?.insurance_value || observationFormData?.insurance_value || null,
packaging: itemsFormData?.packaging || observationFormData?.packaging || null,
other_increments: itemsFormData?.other_increments || observationFormData?.other_increments || null,
total_increments_mn: itemsFormData?.total_increments_mn || observationFormData?.total_increments_mn || null,
total_increments_me: itemsFormData?.total_increments_me || observationFormData?.total_increments_me || null,
// Taxes
iva_mn: itemsFormData?.iva_mn || null,
iva_me: itemsFormData?.iva_me || null,
iva_mc: itemsFormData?.iva_mc || null,
iva_factor: generalFormData?.iva_factor || itemsFormData?.iva_factor || null,
tax_value_me: itemsFormData?.tax_value_me || null,
seal_value_2500: itemsFormData?.seal_value_2500 || null,
// Weights & quantities
total_quantity: itemsFormData?.total_quantity || null,
gross_weight: itemsFormData?.gross_weight || null,
net_weight: itemsFormData?.net_weight || null,
bundle_count: itemsFormData?.bundle_count || null,
weight_factor: itemsFormData?.weight_factor || null,
};
}
function buildLogisticsData(generalFormData: any, othersFormData: any, observationFormData: any) {
const hasLogisticsFromGeneral = generalFormData?.carrier_id ||
generalFormData?.driver_name || generalFormData?.transport_type || generalFormData?.transport_num;
if (hasLogisticsFromGeneral) {
const logisticsEntry = buildLogisticsEntry(generalFormData, observationFormData);
// Si también hay datos del formulario de others, combinarlos
if (othersFormData && Array.isArray(othersFormData) && othersFormData.length > 0) {
// Actualizar el primer elemento con datos del general
return [
mergeLogisticsEntries(generalFormData, othersFormData[0], observationFormData),
// Agregar los demás elementos si existen
...othersFormData.slice(1).map((item: any) => buildLogisticsEntryFromOther(item, observationFormData))
];
} else {
// Solo datos del general
return [logisticsEntry];
}
} else {
// Solo datos del formulario others
return othersFormData.map((item: any) => buildLogisticsEntryFromOther(item, observationFormData));
}
}
function buildLogisticsEntry(generalFormData: any, observationFormData: any) {
return {
carrier_id: generalFormData?.carrier_id || null,
transport_type: generalFormData?.transport_type || null,
transport_mode: null,
driver_name: generalFormData?.driver_name || null,
is_rail: null,
rail_id: null,
vehicle_num: generalFormData?.transport_num || null,
license_plate: null,
seal_number: null,
guide_number: null,
entry_exit_date: null,
incoterm: observationFormData?.incoterm || null,
};
}
function mergeLogisticsEntries(generalFormData: any, otherData: any, observationFormData: any) {
return {
// Carrier info
carrier_id: generalFormData?.carrier_id || otherData.carrier_id || null,
transport_id: otherData.transport_id || null,
transport_us_id: otherData.transport_us_id || null,
transport_type: generalFormData?.transport_type || otherData.transport_type || null,
transport_num: otherData.transport_num || null,
transport_mode: otherData.transport_mode || null,
driver_name: generalFormData?.driver_name || otherData.driver_name || null,
is_rail: otherData.is_rail || null,
rail_id: otherData.rail_id || null,
// Vehicle & tracking
vehicle_num: generalFormData?.transport_num || otherData.vehicle_num || null,
license_plate: otherData.license_plate || null,
license_plate_complete: otherData.license_plate_complete || null,
trailer_num: otherData.trailer_num || null,
seal_number: otherData.seal_number || null,
guide_number: otherData.guide_number || null,
bill_number: otherData.bill_number || null,
reference_number: otherData.reference_number || null,
shipment_number: otherData.shipment_number || null,
// Incoterms
incoterm: otherData.incoterm || observationFormData?.incoterm || null,
// Identifiers & complements
identifier_1: otherData.identifier_1 || null,
complement_1: otherData.complement_1 || null,
identifier_2: otherData.identifier_2 || null,
complement_2: otherData.complement_2 || null,
// Weight & container info
weight_type: otherData.weight_type || null,
container_types: otherData.container_types || null,
vehicle_data: otherData.vehicle_data || null,
// Locations & routes
origin_location: otherData.origin_location || null,
destination_location: otherData.destination_location || null,
transport_itinerary: otherData.transport_itinerary || null,
destination_goods: otherData.destination_goods || null,
// Logistics dates
entry_exit_date: otherData.entry_exit_date || null,
delivery_date: otherData.delivery_date || null,
// Delivery control
delivered_status: otherData.delivered_status || null,
received_by: otherData.received_by || null,
// Payment info
payment_date: otherData.payment_date || null,
payment_receipt_num: otherData.payment_receipt_num || null,
// CTM process
is_ctm_process: otherData.is_ctm_process || null,
};
}
function buildLogisticsEntryFromOther(item: any, observationFormData: any) {
return {
// Carrier info
carrier_id: item.carrier_id || null,
transport_id: item.transport_id || null,
transport_us_id: item.transport_us_id || null,
transport_type: item.transport_type || null,
transport_num: item.transport_num || null,
transport_mode: item.transport_mode || null,
driver_name: item.driver_name || null,
is_rail: item.is_rail || null,
rail_id: item.rail_id || null,
// Vehicle & tracking
vehicle_num: item.vehicle_num || null,
license_plate: item.license_plate || null,
license_plate_complete: item.license_plate_complete || null,
trailer_num: item.trailer_num || null,
seal_number: item.seal_number || null,
guide_number: item.guide_number || null,
bill_number: item.bill_number || null,
reference_number: item.reference_number || null,
shipment_number: item.shipment_number || null,
// Incoterms
incoterm: item.incoterm || observationFormData?.incoterm || null,
// Identifiers & complements
identifier_1: item.identifier_1 || null,
complement_1: item.complement_1 || null,
identifier_2: item.identifier_2 || null,
complement_2: item.complement_2 || null,
// Weight & container info
weight_type: item.weight_type || null,
container_types: item.container_types || null,
vehicle_data: item.vehicle_data || null,
// Locations & routes
origin_location: item.origin_location || null,
destination_location: item.destination_location || null,
transport_itinerary: item.transport_itinerary || null,
destination_goods: item.destination_goods || null,
// Logistics dates
entry_exit_date: item.entry_exit_date || null,
delivery_date: item.delivery_date || null,
// Delivery control
delivered_status: item.delivered_status || null,
received_by: item.received_by || null,
// Payment info
payment_date: item.payment_date || null,
payment_receipt_num: item.payment_receipt_num || null,
// CTM process
is_ctm_process: item.is_ctm_process || null,
};
}

View File

@@ -23,16 +23,16 @@
// Importar los componentes de cada pestaña
import GeneralTabForm from '$lib/components/dashboard/invoices/edit/general-tab-form.svelte';
import ObservationsTabForm from '$lib/components/dashboard/invoices/edit/observations-tab-form.svelte';
import ItemsTabForm from '$lib/components/dashboard/invoices/edit/continuation-tab-form.svelte';
import ItemsTabForm from '$lib/components/dashboard/invoices/edit/items-tab-form.svelte';
import OthersTabForm from '$lib/components/dashboard/invoices/edit/others-tab-form.svelte';
import InvoiceTopFields from '$lib/components/dashboard/invoices/edit/invoice-top-fields.svelte';
import ContinuationTabForm from '$lib/components/dashboard/invoices/edit/continuation-tab-form.svelte';
// Importar la API de facturas
import { invoicesApi, type CreateInvoiceData, type UpdateInvoiceData, type OperationType } from '$lib/api/dashboard/a76/invoices';
import type { InvoiceType } from '$lib/api/dashboard/refrence_data/invoice_types';
import type { CustomsBroker } from '$lib/api/dashboard/a76/customs-brokers';
import type { ClientProvider } from '$lib/api/dashboard/a76/clients-providers';
import { saveInvoice } from '$lib/components/dashboard/invoices/edit/save-invoice';
// Cargar companyStore solo en el cliente - no usamos sidebar en esta página
let companyStore: any = $state(undefined);
@@ -108,398 +108,22 @@
success = false;
try {
// Validar campos requeridos para creación
if (data.isCreate && generalFormData) {
const requiredFields = {
operation_type: 'Tipo de Operación',
invoice_type: 'Tipo de Factura',
invoice_number: 'Número de Factura'
};
const missingFields: string[] = [];
for (const [field, label] of Object.entries(requiredFields)) {
const value = (generalFormData as any)[field];
if (value === null || value === undefined || value === '') {
missingFields.push(label);
}
}
if (missingFields.length > 0) {
throw new Error(`Los siguientes campos son obligatorios: ${missingFields.join(', ')}`);
}
}
// Construir el payload unificado
const payload: CreateInvoiceData | UpdateInvoiceData = {
// Datos generales desde el formulario general
operation_type: generalFormData?.operation_type !== null && generalFormData?.operation_type !== undefined
? (generalFormData.operation_type === 1 ? 'exp' : 'imp') as OperationType
: undefined,
invoice_type: generalFormData?.invoice_type || undefined,
invoice_number: generalFormData?.invoice_number || undefined,
invoice_date: generalFormData?.invoice_date || undefined,
emission_date: generalFormData?.emission_date || undefined,
// Observation fields from observationFormData
observation_es: observationFormData?.observation_es || undefined,
observation_en: observationFormData?.observation_en || undefined,
alternate_invoice: observationFormData?.alternate_invoice || undefined,
};
// Solo agregar sub-recursos si tienen valores reales
// Compliance MX - combinar datos del formulario general y observations
const hasComplianceValue = generalFormData?.pedimento || generalFormData?.remesa || generalFormData?.aduana ||
generalFormData?.provider_id || generalFormData?.sold_to_id ||
generalFormData?.shipped_to_id || generalFormData?.customs_broker_id ||
observationFormData?.pedimento || observationFormData?.pedimento_code ||
observationFormData?.remesa || observationFormData?.aduana ||
observationFormData?.provider_id || observationFormData?.sold_to_id ||
observationFormData?.shipped_to_id || observationFormData?.shipped_by_id ||
observationFormData?.customs_broker_id || observationFormData?.is_mixed ||
observationFormData?.waste_type || observationFormData?.appendix_17 ||
observationFormData?.edocument || observationFormData?.electronic_signature ||
observationFormData?.sem_id || observationFormData?.enclosure ||
observationFormData?.incoterm;
if (hasComplianceValue) {
payload.compliance_mx = {
// Pedimento fields
pedimento: generalFormData?.pedimento || observationFormData?.pedimento || null,
pedimento_code: observationFormData?.pedimento_code || null,
pedimento_k1: observationFormData?.pedimento_k1 || null,
remesa: generalFormData?.remesa || observationFormData?.remesa || null,
aduana: generalFormData?.aduana || observationFormData?.aduana || null,
port_of_entry: observationFormData?.port_of_entry || null,
destination: observationFormData?.destination || null,
manifest_number: observationFormData?.manifest_number || null,
// Client/Provider fields
provider_header: generalFormData?.provider_header || observationFormData?.provider_header || null,
provider_id: generalFormData?.provider_id || observationFormData?.provider_id || null,
sold_to_header: generalFormData?.sold_to_header || observationFormData?.sold_to_header || null,
sold_to_id: generalFormData?.sold_to_id || observationFormData?.sold_to_id || null,
shipped_to_header: generalFormData?.shipped_to_header || observationFormData?.shipped_to_header || null,
shipped_to_id: generalFormData?.shipped_to_id || observationFormData?.shipped_to_id || null,
shipped_by_header: observationFormData?.shipped_by_header || null,
shipped_by_id: observationFormData?.shipped_by_id || null,
// Customs broker fields
customs_broker_id: generalFormData?.customs_broker_id || observationFormData?.customs_broker_id || null,
customs_broker_us_id: observationFormData?.customs_broker_us_id || null,
broker_invoice_num: observationFormData?.broker_invoice_num || null,
broker_invoice_date: observationFormData?.broker_invoice_date || null,
// Flags & regimes
is_mixed: observationFormData?.is_mixed || null,
waste_type: observationFormData?.waste_type || null,
scrap_type: observationFormData?.scrap_type || null,
appendix_17: observationFormData?.appendix_17 || null,
is_regime_change: observationFormData?.is_regime_change || null,
which_exchange_rate: observationFormData?.which_exchange_rate || null,
value_method: observationFormData?.value_method || null,
act_value: observationFormData?.act_value || null,
is_pedimento_pending: observationFormData?.is_pedimento_pending || null,
// Ownership & balances
is_owner_of_goods: observationFormData?.is_owner_of_goods || null,
generate_balances: observationFormData?.generate_balances || null,
was_reviewed_by_company: observationFormData?.was_reviewed_by_company || null,
// VUCEM / Digital
edocument: observationFormData?.edocument || null,
electronic_signature: observationFormData?.electronic_signature || null,
certificate_number: observationFormData?.certificate_number || null,
niu_number: observationFormData?.niu_number || null,
bill_of_lading_count: observationFormData?.bill_of_lading_count || null,
addendum_vu: observationFormData?.addendum_vu || null,
origin_destination_cove: observationFormData?.origin_destination_cove || null,
vucem_operation_num: observationFormData?.vucem_operation_num || null,
customs_person_line: observationFormData?.customs_person_line || null,
// Additional control
contingency_mode: observationFormData?.contingency_mode || null,
enclosure: observationFormData?.enclosure || null,
guide_type_to_identify: observationFormData?.guide_type_to_identify || null,
location: observationFormData?.location || null,
// DOT & official
dot_code: observationFormData?.dot_code || null,
subdivision: observationFormData?.subdivision || null,
acts_as: observationFormData?.acts_as || null,
movement_type: observationFormData?.movement_type || null,
office_document: observationFormData?.office_document || null,
reason_export: observationFormData?.reason_export || null,
signature_key: observationFormData?.signature_key || null,
// SM specific
sem_id: observationFormData?.sem_id || null,
};
}
// Financials - combinar datos del formulario general y items
const hasFinancialsValue = generalFormData?.currency_type ||
generalFormData?.iva_factor ||
itemsFormData?.currency || itemsFormData?.exchange_rate ||
itemsFormData?.value_mn || itemsFormData?.value_me ||
itemsFormData?.customs_value_mn || itemsFormData?.freight ||
itemsFormData?.insurance;
if (hasFinancialsValue) {
payload.financials = {
// Currency
currency: itemsFormData?.currency || null,
currency_type: generalFormData?.currency_type || itemsFormData?.currency_type || null,
exchange_rate: itemsFormData?.exchange_rate || null,
exchange_rate_mm: itemsFormData?.exchange_rate_mm || null,
// Merchandise values
value_mn: itemsFormData?.value_mn || null,
value_me: itemsFormData?.value_me || null,
value_mc: itemsFormData?.value_mc || null,
// Customs value
customs_value_mn: itemsFormData?.customs_value_mn || null,
customs_value_me: itemsFormData?.customs_value_me || null,
// Raw materials
raw_material_value_mn: itemsFormData?.raw_material_value_mn || null,
raw_material_value_me: itemsFormData?.raw_material_value_me || null,
// Aggregate value
aggregate_value_mn: itemsFormData?.aggregate_value_mn || null,
aggregate_value_me: itemsFormData?.aggregate_value_me || null,
aggregate_value_mc: itemsFormData?.aggregate_value_mc || null,
// Mexican merchandise value
mexican_value_mn: itemsFormData?.mexican_value_mn || null,
mexican_value_me: itemsFormData?.mexican_value_me || null,
mexican_value_mc: itemsFormData?.mexican_value_mc || null,
// National packaging
national_packaging_mn: itemsFormData?.national_packaging_mn || null,
national_packaging_me: itemsFormData?.national_packaging_me || null,
national_packaging_mc: itemsFormData?.national_packaging_mc || null,
// Costs & increments
freight: itemsFormData?.freight || observationFormData?.freight || null,
insurance: itemsFormData?.insurance || observationFormData?.insurance || null,
insurance_value: itemsFormData?.insurance_value || observationFormData?.insurance_value || null,
packaging: itemsFormData?.packaging || observationFormData?.packaging || null,
other_increments: itemsFormData?.other_increments || observationFormData?.other_increments || null,
total_increments_mn: itemsFormData?.total_increments_mn || observationFormData?.total_increments_mn || null,
total_increments_me: itemsFormData?.total_increments_me || observationFormData?.total_increments_me || null,
// Taxes
iva_mn: itemsFormData?.iva_mn || null,
iva_me: itemsFormData?.iva_me || null,
iva_mc: itemsFormData?.iva_mc || null,
iva_factor: generalFormData?.iva_factor || itemsFormData?.iva_factor || null,
tax_value_me: itemsFormData?.tax_value_me || null,
seal_value_2500: itemsFormData?.seal_value_2500 || null,
// Weights & quantities
total_quantity: itemsFormData?.total_quantity || null,
gross_weight: itemsFormData?.gross_weight || null,
net_weight: itemsFormData?.net_weight || null,
bundle_count: itemsFormData?.bundle_count || null,
weight_factor: itemsFormData?.weight_factor || null,
};
}
// Logistics - combinar datos del formulario general con othersFormData
const hasLogisticsFromGeneral = generalFormData?.carrier_id ||
generalFormData?.driver_name || generalFormData?.transport_type || generalFormData?.transport_num;
if (hasLogisticsFromGeneral || (othersFormData && Array.isArray(othersFormData) && othersFormData.length > 0)) {
// Si hay datos en el formulario general, crear/actualizar el primer elemento
if (hasLogisticsFromGeneral) {
const logisticsEntry = {
carrier_id: generalFormData?.carrier_id || null,
transport_type: generalFormData?.transport_type || null,
transport_mode: null,
driver_name: generalFormData?.driver_name || null,
is_rail: null,
rail_id: null,
vehicle_num: generalFormData?.transport_num || null,
license_plate: null,
seal_number: null,
guide_number: null,
entry_exit_date: null,
};
// Si también hay datos del formulario de others, combinarlos
if (othersFormData && Array.isArray(othersFormData) && othersFormData.length > 0) {
// Actualizar el primer elemento con datos del general
payload.logistics = [
{
// Carrier info
carrier_id: generalFormData?.carrier_id || othersFormData[0].carrier_id || null,
transport_id: othersFormData[0].transport_id || null,
transport_us_id: othersFormData[0].transport_us_id || null,
transport_type: generalFormData?.transport_type || othersFormData[0].transport_type || null,
transport_num: othersFormData[0].transport_num || null,
transport_mode: othersFormData[0].transport_mode || null,
driver_name: generalFormData?.driver_name || othersFormData[0].driver_name || null,
is_rail: othersFormData[0].is_rail || null,
rail_id: othersFormData[0].rail_id || null,
// Vehicle & tracking
vehicle_num: generalFormData?.transport_num || othersFormData[0].vehicle_num || null,
license_plate: othersFormData[0].license_plate || null,
license_plate_complete: othersFormData[0].license_plate_complete || null,
trailer_num: othersFormData[0].trailer_num || null,
seal_number: othersFormData[0].seal_number || null,
guide_number: othersFormData[0].guide_number || null,
bill_number: othersFormData[0].bill_number || null,
reference_number: othersFormData[0].reference_number || null,
shipment_number: othersFormData[0].shipment_number || null,
// Incoterms
incoterm: othersFormData[0].incoterm || observationFormData?.incoterm || null,
// Identifiers & complements
identifier_1: othersFormData[0].identifier_1 || null,
complement_1: othersFormData[0].complement_1 || null,
identifier_2: othersFormData[0].identifier_2 || null,
complement_2: othersFormData[0].complement_2 || null,
// Weight & container info
weight_type: othersFormData[0].weight_type || null,
container_types: othersFormData[0].container_types || null,
vehicle_data: othersFormData[0].vehicle_data || null,
// Locations & routes
origin_location: othersFormData[0].origin_location || null,
destination_location: othersFormData[0].destination_location || null,
transport_itinerary: othersFormData[0].transport_itinerary || null,
destination_goods: othersFormData[0].destination_goods || null,
// Logistics dates
entry_exit_date: othersFormData[0].entry_exit_date || null,
delivery_date: othersFormData[0].delivery_date || null,
// Delivery control
delivered_status: othersFormData[0].delivered_status || null,
received_by: othersFormData[0].received_by || null,
// Payment info
payment_date: othersFormData[0].payment_date || null,
payment_receipt_num: othersFormData[0].payment_receipt_num || null,
// CTM process
is_ctm_process: othersFormData[0].is_ctm_process || null,
},
// Agregar los demás elementos si existen
...othersFormData.slice(1).map((item: any) => ({
// Carrier info
carrier_id: item.carrier_id || null,
transport_id: item.transport_id || null,
transport_us_id: item.transport_us_id || null,
transport_type: item.transport_type || null,
transport_num: item.transport_num || null,
transport_mode: item.transport_mode || null,
driver_name: item.driver_name || null,
is_rail: item.is_rail || null,
rail_id: item.rail_id || null,
// Vehicle & tracking
vehicle_num: item.vehicle_num || null,
license_plate: item.license_plate || null,
license_plate_complete: item.license_plate_complete || null,
trailer_num: item.trailer_num || null,
seal_number: item.seal_number || null,
guide_number: item.guide_number || null,
bill_number: item.bill_number || null,
reference_number: item.reference_number || null,
shipment_number: item.shipment_number || null,
// Incoterms
incoterm: item.incoterm || null,
// Identifiers & complements
identifier_1: item.identifier_1 || null,
complement_1: item.complement_1 || null,
identifier_2: item.identifier_2 || null,
complement_2: item.complement_2 || null,
// Weight & container info
weight_type: item.weight_type || null,
container_types: item.container_types || null,
vehicle_data: item.vehicle_data || null,
// Locations & routes
origin_location: item.origin_location || null,
destination_location: item.destination_location || null,
transport_itinerary: item.transport_itinerary || null,
destination_goods: item.destination_goods || null,
// Logistics dates
entry_exit_date: item.entry_exit_date || null,
delivery_date: item.delivery_date || null,
// Delivery control
delivered_status: item.delivered_status || null,
received_by: item.received_by || null,
// Payment info
payment_date: item.payment_date || null,
payment_receipt_num: item.payment_receipt_num || null,
// CTM process
is_ctm_process: item.is_ctm_process || null,
}))
];
} else {
// Solo datos del general
payload.logistics = [logisticsEntry];
}
} else {
// Solo datos del formulario others
payload.logistics = othersFormData.map((item: any) => ({
// Carrier info
carrier_id: item.carrier_id || null,
transport_id: item.transport_id || null,
transport_us_id: item.transport_us_id || null,
transport_type: item.transport_type || null,
transport_num: item.transport_num || null,
transport_mode: item.transport_mode || null,
driver_name: item.driver_name || null,
is_rail: item.is_rail || null,
rail_id: item.rail_id || null,
// Vehicle & tracking
vehicle_num: item.vehicle_num || null,
license_plate: item.license_plate || null,
license_plate_complete: item.license_plate_complete || null,
trailer_num: item.trailer_num || null,
seal_number: item.seal_number || null,
guide_number: item.guide_number || null,
bill_number: item.bill_number || null,
reference_number: item.reference_number || null,
shipment_number: item.shipment_number || null,
// Incoterms
incoterm: item.incoterm || observationFormData?.incoterm || null,
// Identifiers & complements
identifier_1: item.identifier_1 || null,
complement_1: item.complement_1 || null,
identifier_2: item.identifier_2 || null,
complement_2: item.complement_2 || null,
// Weight & container info
weight_type: item.weight_type || null,
container_types: item.container_types || null,
vehicle_data: item.vehicle_data || null,
// Locations & routes
origin_location: item.origin_location || null,
destination_location: item.destination_location || null,
transport_itinerary: item.transport_itinerary || null,
destination_goods: item.destination_goods || null,
// Logistics dates
entry_exit_date: item.entry_exit_date || null,
delivery_date: item.delivery_date || null,
// Delivery control
delivered_status: item.delivered_status || null,
received_by: item.received_by || null,
// Payment info
payment_date: item.payment_date || null,
payment_receipt_num: item.payment_receipt_num || null,
// CTM process
is_ctm_process: item.is_ctm_process || null,
}));
}
}
// Eliminar campos undefined para no enviarlos
Object.keys(payload).forEach(key => {
if (payload[key as keyof typeof payload] === undefined) {
delete payload[key as keyof typeof payload];
const result = await saveInvoice({
invoiceId,
isCreate: data.isCreate || false,
companyId: companyStore?.activeCompany?.id || 0,
formData: {
generalFormData,
observationFormData,
itemsFormData,
othersFormData
}
});
let newInvoiceId = invoiceId;
if (data.isCreate) {
// Crear nueva factura con todos sus sub-recursos
const response = await invoicesApi.create(companyStore?.activeCompany?.id || 0, payload as CreateInvoiceData);
if (response.error) {
const errorMsg = typeof response.error === 'string' ? response.error : 'Error al crear la factura';
throw new Error(errorMsg);
}
if (!response.data?.id) throw new Error('No se recibió el ID de la factura creada');
newInvoiceId = response.data.id;
// Redirigir a la página de edición
await goto(`/dashboard/invoices/edit/${newInvoiceId}`);
return;
} else {
// Actualizar factura existente con todos sus sub-recursos
const response = await invoicesApi.update(invoiceId!, companyStore?.activeCompany?.id || 0, payload as UpdateInvoiceData);
if (response.error) throw new Error(response.error);
if (!result.success) {
throw new Error(result.error || 'Error al guardar la factura');
}
success = true;
setTimeout(() => {
success = false;
@@ -647,7 +271,7 @@
<!-- Footer fijo en la parte inferior -->
<div
class="fixed bottom-0 left-0 right-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 border-t shadow-lg z-[5]"
class="fixed bottom-0 left-0 right-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 border-t shadow-lg z-[5] group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] ml-[calc(var(--sidebar-width))]"
>
<div class="px-4 py-4 space-y-4 max-w-[1400px] mx-auto">
<!-- Tabs Navigation -->