diff --git a/backend/api/v1/modules/a76/items/__init__.py b/backend/api/v1/modules/a76/items/__init__.py new file mode 100644 index 00000000..a66bfe89 --- /dev/null +++ b/backend/api/v1/modules/a76/items/__init__.py @@ -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", +] diff --git a/backend/api/v1/modules/a76/items/line_customs/__init__.py b/backend/api/v1/modules/a76/items/line_customs/__init__.py new file mode 100644 index 00000000..6d676bd7 --- /dev/null +++ b/backend/api/v1/modules/a76/items/line_customs/__init__.py @@ -0,0 +1,16 @@ +"""Line customs module""" +from .models import LineCustom +from .schemas import ( + LineCustomBase, + LineCustomCreate, + LineCustomUpdate, + LineCustomResponse, +) + +__all__ = [ + "LineCustom", + "LineCustomBase", + "LineCustomCreate", + "LineCustomUpdate", + "LineCustomResponse", +] diff --git a/backend/api/v1/modules/a76/items/line_customs/models.py b/backend/api/v1/modules/a76/items/line_customs/models.py new file mode 100644 index 00000000..1c40448f --- /dev/null +++ b/backend/api/v1/modules/a76/items/line_customs/models.py @@ -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") \ No newline at end of file diff --git a/backend/api/v1/modules/a76/items/line_customs/schemas.py b/backend/api/v1/modules/a76/items/line_customs/schemas.py new file mode 100644 index 00000000..92f2fde4 --- /dev/null +++ b/backend/api/v1/modules/a76/items/line_customs/schemas.py @@ -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) diff --git a/backend/api/v1/modules/a76/items/line_descriptions/__init__.py b/backend/api/v1/modules/a76/items/line_descriptions/__init__.py new file mode 100644 index 00000000..227064f6 --- /dev/null +++ b/backend/api/v1/modules/a76/items/line_descriptions/__init__.py @@ -0,0 +1,16 @@ +"""Line descriptions module""" +from .models import LineDescription +from .schemas import ( + LineDescriptionBase, + LineDescriptionCreate, + LineDescriptionUpdate, + LineDescriptionResponse, +) + +__all__ = [ + "LineDescription", + "LineDescriptionBase", + "LineDescriptionCreate", + "LineDescriptionUpdate", + "LineDescriptionResponse", +] diff --git a/backend/api/v1/modules/a76/items/line_descriptions/models.py b/backend/api/v1/modules/a76/items/line_descriptions/models.py new file mode 100644 index 00000000..dd78b780 --- /dev/null +++ b/backend/api/v1/modules/a76/items/line_descriptions/models.py @@ -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") \ No newline at end of file diff --git a/backend/api/v1/modules/a76/items/line_descriptions/schemas.py b/backend/api/v1/modules/a76/items/line_descriptions/schemas.py new file mode 100644 index 00000000..2f0d1317 --- /dev/null +++ b/backend/api/v1/modules/a76/items/line_descriptions/schemas.py @@ -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) diff --git a/backend/api/v1/modules/a76/items/line_financials/__init__.py b/backend/api/v1/modules/a76/items/line_financials/__init__.py new file mode 100644 index 00000000..a090aee0 --- /dev/null +++ b/backend/api/v1/modules/a76/items/line_financials/__init__.py @@ -0,0 +1,16 @@ +"""Line financials module""" +from .models import LineFinancial +from .schemas import ( + LineFinancialBase, + LineFinancialCreate, + LineFinancialUpdate, + LineFinancialResponse, +) + +__all__ = [ + "LineFinancial", + "LineFinancialBase", + "LineFinancialCreate", + "LineFinancialUpdate", + "LineFinancialResponse", +] diff --git a/backend/api/v1/modules/a76/items/line_financials/models.py b/backend/api/v1/modules/a76/items/line_financials/models.py new file mode 100644 index 00000000..04b3002d --- /dev/null +++ b/backend/api/v1/modules/a76/items/line_financials/models.py @@ -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") \ No newline at end of file diff --git a/backend/api/v1/modules/a76/items/line_financials/schemas.py b/backend/api/v1/modules/a76/items/line_financials/schemas.py new file mode 100644 index 00000000..0779a481 --- /dev/null +++ b/backend/api/v1/modules/a76/items/line_financials/schemas.py @@ -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) diff --git a/backend/api/v1/modules/a76/items/line_items/__init__.py b/backend/api/v1/modules/a76/items/line_items/__init__.py new file mode 100644 index 00000000..7f7ae971 --- /dev/null +++ b/backend/api/v1/modules/a76/items/line_items/__init__.py @@ -0,0 +1,16 @@ +"""Line items module""" +from .models import LineItem +from .schemas import ( + LineItemBase, + LineItemCreate, + LineItemUpdate, + LineItemResponse, +) + +__all__ = [ + "LineItem", + "LineItemBase", + "LineItemCreate", + "LineItemUpdate", + "LineItemResponse", +] diff --git a/backend/api/v1/modules/a76/items/line_items/models.py b/backend/api/v1/modules/a76/items/line_items/models.py new file mode 100644 index 00000000..bb78348a --- /dev/null +++ b/backend/api/v1/modules/a76/items/line_items/models.py @@ -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) diff --git a/backend/api/v1/modules/a76/items/line_items/schemas.py b/backend/api/v1/modules/a76/items/line_items/schemas.py new file mode 100644 index 00000000..e86c5a62 --- /dev/null +++ b/backend/api/v1/modules/a76/items/line_items/schemas.py @@ -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) diff --git a/backend/api/v1/modules/a76/items/line_quantities/__init__.py b/backend/api/v1/modules/a76/items/line_quantities/__init__.py new file mode 100644 index 00000000..8e5f8d41 --- /dev/null +++ b/backend/api/v1/modules/a76/items/line_quantities/__init__.py @@ -0,0 +1,16 @@ +"""Line quantities module""" +from .models import LineQuantity +from .schemas import ( + LineQuantityBase, + LineQuantityCreate, + LineQuantityUpdate, + LineQuantityResponse, +) + +__all__ = [ + "LineQuantity", + "LineQuantityBase", + "LineQuantityCreate", + "LineQuantityUpdate", + "LineQuantityResponse", +] diff --git a/backend/api/v1/modules/a76/items/line_quantities/models.py b/backend/api/v1/modules/a76/items/line_quantities/models.py new file mode 100644 index 00000000..1fa41c16 --- /dev/null +++ b/backend/api/v1/modules/a76/items/line_quantities/models.py @@ -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") \ No newline at end of file diff --git a/backend/api/v1/modules/a76/items/line_quantities/schemas.py b/backend/api/v1/modules/a76/items/line_quantities/schemas.py new file mode 100644 index 00000000..71552342 --- /dev/null +++ b/backend/api/v1/modules/a76/items/line_quantities/schemas.py @@ -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) \ No newline at end of file diff --git a/backend/api/v1/modules/a76/items/line_references/__init__.py b/backend/api/v1/modules/a76/items/line_references/__init__.py new file mode 100644 index 00000000..872e095e --- /dev/null +++ b/backend/api/v1/modules/a76/items/line_references/__init__.py @@ -0,0 +1,16 @@ +"""Line references module""" +from .models import LineReference +from .schemas import ( + LineReferenceBase, + LineReferenceCreate, + LineReferenceUpdate, + LineReferenceResponse, +) + +__all__ = [ + "LineReference", + "LineReferenceBase", + "LineReferenceCreate", + "LineReferenceUpdate", + "LineReferenceResponse", +] diff --git a/backend/api/v1/modules/a76/items/line_references/models.py b/backend/api/v1/modules/a76/items/line_references/models.py new file mode 100644 index 00000000..11e2459c --- /dev/null +++ b/backend/api/v1/modules/a76/items/line_references/models.py @@ -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") \ No newline at end of file diff --git a/backend/api/v1/modules/a76/items/line_references/schemas.py b/backend/api/v1/modules/a76/items/line_references/schemas.py new file mode 100644 index 00000000..7a9677dd --- /dev/null +++ b/backend/api/v1/modules/a76/items/line_references/schemas.py @@ -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) diff --git a/backend/api/v1/modules/a76/items/models.py b/backend/api/v1/modules/a76/items/models.py index 1439f6a0..5e6ece83 100644 --- a/backend/api/v1/modules/a76/items/models.py +++ b/backend/api/v1/modules/a76/items/models.py @@ -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) ) ``` -""" \ No newline at end of file +""" diff --git a/backend/api/v1/modules/a76/items/routes.py b/backend/api/v1/modules/a76/items/routes.py new file mode 100644 index 00000000..8d4b924a --- /dev/null +++ b/backend/api/v1/modules/a76/items/routes.py @@ -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 diff --git a/backend/api/v1/modules/a76/items/schemas.py b/backend/api/v1/modules/a76/items/schemas.py new file mode 100644 index 00000000..85fe6c2f --- /dev/null +++ b/backend/api/v1/modules/a76/items/schemas.py @@ -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) diff --git a/backend/api/v1/modules/a76/items/series/models.py b/backend/api/v1/modules/a76/items/series/models.py index a944b080..6801b43a 100644 --- a/backend/api/v1/modules/a76/items/series/models.py +++ b/backend/api/v1/modules/a76/items/series/models.py @@ -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", diff --git a/backend/api/v1/modules/a76/items/service.py b/backend/api/v1/modules/a76/items/service.py new file mode 100644 index 00000000..c697d5b0 --- /dev/null +++ b/backend/api/v1/modules/a76/items/service.py @@ -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)}" + ) diff --git a/frontend/package.json b/frontend/package.json index af6b62ab..8deac80f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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 ''", diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items-tab-form.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items-tab-form.svelte new file mode 100644 index 00000000..c7af8c0f --- /dev/null +++ b/frontend/src/lib/components/dashboard/invoices/edit/items-tab-form.svelte @@ -0,0 +1,60 @@ + + +