diff --git a/backend/api/v1/modules/a76/invoices/models.py b/backend/api/v1/modules/a76/invoices/models.py index a5337664..10d22c0c 100644 --- a/backend/api/v1/modules/a76/invoices/models.py +++ b/backend/api/v1/modules/a76/invoices/models.py @@ -38,7 +38,7 @@ class InvoiceHeader(Base, TenantScopedMixin, TimestampMixin): id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True) # Identifiers - system: Mapped[Optional[str]] = mapped_column(String(10)) # SISTEMA / Sistema de origen <-- no tiene campo en la antigua base de datos, sera para fixed-asset(scaf), inventory(scaii) + system: Mapped[Optional[str]] = mapped_column(String(12)) # SISTEMA / Sistema de origen <-- no tiene campo en la antigua base de datos, sera para fixed_asset(scaf), inventory(scaii) operation_type: Mapped[OperationType] = mapped_column(String(10)) # TIPOMOVIMIENTO / Clasifica imp/exp/sm/ctm invoice_type: Mapped[Optional[str]] = mapped_column(ForeignKey("public.invoice_types.key")) # TIPOFACTURA / TIPODOC invoice_number: Mapped[Optional[str]] = mapped_column(String(20)) # FACTURAIMPO/FACTURAEXPO/FACTURAREMISION/FACTURAENVIO/FACTURASALIDA diff --git a/backend/api/v1/modules/a76/invoices/schemas.py b/backend/api/v1/modules/a76/invoices/schemas.py index 2afaec50..16c77e70 100644 --- a/backend/api/v1/modules/a76/invoices/schemas.py +++ b/backend/api/v1/modules/a76/invoices/schemas.py @@ -9,7 +9,7 @@ from .models import OperationType class InvoiceHeaderBase(BaseModel): """Base fields for Invoice Header""" system: Optional[str] = Field( - None, max_length=10, description="System of origin") + None, max_length=12, description="System of origin") operation_type: Optional[OperationType] = Field( None, max_length=10, description="Operation type: imp/exp/sm/ctm") invoice_type: Optional[str] = Field( 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..c490352f --- /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 ( + LineCustomCreate, + LineCustomUpdate, + LineCustomResponse +) +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[LineCustomCreate] = 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[LineCustomUpdate] = 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[LineCustomResponse] = 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..eb9c57a4 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,39 @@ 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. - system_origin: Mapped[str] = mapped_column(String(10)) # SCAF or SCAII - + invoice_id: Mapped[int] = mapped_column(ForeignKey("a76.invoice_header.id")) # CONSECUTIVO + # 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 - + 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 - + 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 +67,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 +83,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 +102,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 +202,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..4bf677e3 --- /dev/null +++ b/backend/api/v1/modules/a76/items/routes.py @@ -0,0 +1,240 @@ +""" +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( + company_id: int = Query(..., description="Company ID"), + 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 + """ + tenant_id = validate_access_to_resource(db, company_id, current_user) + + filters = {"invoice_id": invoice_id} if invoice_id else None + items, total = ItemService.get_all( + db=db, + tenant_id=tenant_id, + company_id=company_id, + skip=0, + limit=10000, # Get all for stats + filters=filters + ) + + # 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..3503bab1 --- /dev/null +++ b/backend/api/v1/modules/a76/items/schemas.py @@ -0,0 +1,74 @@ +""" +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") + 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 + 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") + 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..d8a30b99 --- /dev/null +++ b/backend/api/v1/modules/a76/items/service.py @@ -0,0 +1,346 @@ +""" +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") diff --git a/backend/api/v1/modules/a76/router.py b/backend/api/v1/modules/a76/router.py index 8bfee681..6129f616 100644 --- a/backend/api/v1/modules/a76/router.py +++ b/backend/api/v1/modules/a76/router.py @@ -9,6 +9,7 @@ from .customs_brokers.routes import router as customs_broker_router # Importar routers de módulos from .invoices.routes import router as invoices_router +from .items.routes import router as items_router from .classes import router as classes_router from .clients_and_providers import router as client_and_provider_router from .general_catalogs.company import router as company_router @@ -46,6 +47,7 @@ router = APIRouter() # Registrar módulos router.include_router(invoices_router, prefix="/a76", tags=["a76 / invoices"]) +router.include_router(items_router, prefix="/a76", tags=["a76 / items"]) router.include_router(pedimentos_router, prefix="/a76") router.include_router( client_and_provider_router, prefix="/a76", tags=["a76 / clients_and_providers"] @@ -69,14 +71,14 @@ router.include_router( ) router.include_router(exchange_rate_router, prefix="/a76", tags=["a76 / exchange_rate"]) -router.include_router(trailers_router, prefix="/a76", tags=["a76 / trailers"]) +router.include_router(trailers_router, prefix="/a76/transportation", tags=["a76 / trailers"]) router.include_router( customs_broker_router, prefix="/a76", tags=["a76 / customs_broker"] ) -router.include_router(drivers_router, prefix="/a76", tags=["a76 / drivers"]) -router.include_router(transporters_router, prefix="/a76", +router.include_router(drivers_router, prefix="/a76/transportation", tags=["a76 / drivers"]) +router.include_router(transporters_router, prefix="/a76/transportation", tags=["a76 / transporters"]) -router.include_router(vehicles_router, prefix="/a76", tags=["a76 / vehicles"]) +router.include_router(vehicles_router, prefix="/a76/transportation", tags=["a76 / vehicles"]) # Registrar catálogos generales adicionales router.include_router(concepts_router, prefix="/a76") diff --git a/backend/api/v1/modules/a76/transportation/drivers/dto.py b/backend/api/v1/modules/a76/transportation/drivers/dto.py index 014459a6..f07e4b4c 100644 --- a/backend/api/v1/modules/a76/transportation/drivers/dto.py +++ b/backend/api/v1/modules/a76/transportation/drivers/dto.py @@ -28,8 +28,8 @@ class DriverBaseDTO(BaseModel): badge_number: Optional[str] class_type: Optional[str] unique_badge_number: Optional[str] - company_id: str - tenant_id: str + company_id: int + tenant_id: int class DriverCreateDTO(DriverBaseDTO): diff --git a/backend/api/v1/modules/a76/transportation/drivers/routes.py b/backend/api/v1/modules/a76/transportation/drivers/routes.py index ebab5154..981ab353 100644 --- a/backend/api/v1/modules/a76/transportation/drivers/routes.py +++ b/backend/api/v1/modules/a76/transportation/drivers/routes.py @@ -1,31 +1,53 @@ -from typing import List +from typing import Any, Dict, List from core.database import get_core_db -from core.security import get_current_user -from fastapi import APIRouter, Depends, HTTPException, status +from core.security import get_current_user, validate_access_to_resource +from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy.orm import Session from .dto import DriverCreateDTO, DriverResponseDTO +from .models import Driver from .services import DriverService router = APIRouter(prefix="/drivers") -@router.get("/", response_model=List[DriverResponseDTO]) +@router.get("/", response_model=Dict[str, Any]) async def list_drivers( - db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) + company_id: int = Query(..., description="Company ID for filtering"), + page: int = Query(1, ge=1, description="Page number"), + page_size: int = Query(50, ge=1, le=100, description="Page size"), + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), ): - return db.query(DriverService).all() + tenant_id = validate_access_to_resource(db, company_id, current_user) + drivers = DriverService.list_drivers(db, str(company_id), tenant_id) + total = len(drivers) + + # Aplicar paginación manualmente + skip = (page - 1) * page_size + paginated_drivers = drivers[skip : skip + page_size] + + return { + "items": [DriverResponseDTO.model_validate(driver) for driver in paginated_drivers], + "total": total, + "page": page, + "page_size": page_size, + } @router.get("/{transporter_key}/{line}", response_model=DriverResponseDTO) async def read_driver( transporter_key: str, line: int, + company_id: int = Query(..., description="Company ID for filtering"), db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user), ): - driver = DriverService.get_driver_by_key_and_line(db, transporter_key, line) + tenant_id = validate_access_to_resource(db, company_id, current_user) + driver = DriverService.get_driver_by_key_and_line( + db, transporter_key, line, str(company_id), tenant_id + ) if not driver: raise HTTPException(status_code=404, detail="Driver not found") return driver @@ -44,9 +66,13 @@ async def create_driver( async def delete_driver( transporter_key: str, line: int, + company_id: int = Query(..., description="Company ID for filtering"), db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user), ): - driver = DriverService.delete_driver(db, transporter_key, line) + tenant_id = validate_access_to_resource(db, company_id, current_user) + driver = DriverService.delete_driver( + db, transporter_key, line, str(company_id), tenant_id + ) if not driver: raise HTTPException(status_code=404, detail="Driver not found") diff --git a/backend/api/v1/modules/a76/transportation/drivers/services.py b/backend/api/v1/modules/a76/transportation/drivers/services.py index 9800b62f..c22a49c0 100644 --- a/backend/api/v1/modules/a76/transportation/drivers/services.py +++ b/backend/api/v1/modules/a76/transportation/drivers/services.py @@ -1,3 +1,5 @@ +from typing import List, Optional + from sqlalchemy.orm import Session from . import dto, models @@ -5,15 +7,30 @@ from . import dto, models class DriverService: @staticmethod - def get_driver_by_key_and_line(db: Session, transporter_key: str, line: int): - return ( - db.query(models.Driver) - .filter( - models.Driver.transporter_key == transporter_key, - models.Driver.line == line, - ) - .first() + def list_drivers( + db: Session, company_id: str, tenant_id: Optional[str] = None + ) -> List[models.Driver]: + query = db.query(models.Driver).filter(models.Driver.company_id == company_id) + if tenant_id: + query = query.filter(models.Driver.tenant_id == tenant_id) + return query.all() + + @staticmethod + def get_driver_by_key_and_line( + db: Session, + transporter_key: str, + line: int, + company_id: str, + tenant_id: Optional[str] = None, + ) -> Optional[models.Driver]: + query = db.query(models.Driver).filter( + models.Driver.transporter_key == transporter_key, + models.Driver.line == line, + models.Driver.company_id == company_id, ) + if tenant_id: + query = query.filter(models.Driver.tenant_id == tenant_id) + return query.first() @staticmethod def create_driver(db: Session, driver_data: dto.DriverCreateDTO): @@ -24,8 +41,16 @@ class DriverService: return new_driver @staticmethod - def delete_driver(db: Session, transporter_key: str, line: int): - driver = DriverService.get_driver_by_key_and_line(db, transporter_key, line) + def delete_driver( + db: Session, + transporter_key: str, + line: int, + company_id: str, + tenant_id: Optional[str] = None, + ) -> Optional[models.Driver]: + driver = DriverService.get_driver_by_key_and_line( + db, transporter_key, line, company_id, tenant_id + ) if driver: db.delete(driver) db.commit() diff --git a/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/routes.py b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/routes.py index 76533e83..6d421350 100644 --- a/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/routes.py +++ b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/routes.py @@ -14,7 +14,7 @@ router = APIRouter(prefix="/code-pedimento-regimens") @router.get("/", response_model=Dict[str, Any]) def list_code_pedimento_regimens( page: int = Query(1, ge=1, description="Número de página"), - page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), + page_size: int = Query(50, ge=1, le=1000, description="Tamaño de página"), code: str = Query(None, description="Filter by code"), regime: str = Query(None, description="Filter by regime"), type: str = Query(None, description="Filter by type"), @@ -27,9 +27,9 @@ def list_code_pedimento_regimens( if code is not None: query = query.filter(CodePedimentoRegimen.pedimento_code == code) if regime is not None: - query = query.filter(CodePedimentoRegimen.regime == regime) + query = query.filter(CodePedimentoRegimen.regimen_code == regime) if type is not None: - query = query.filter(CodePedimentoRegimen.type == type) + query = query.filter(CodePedimentoRegimen.type_code == type) items = query.offset(skip).limit(page_size).all() total = query.count() 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/api/dashboard/a76/invoices.ts b/frontend/src/lib/api/dashboard/a76/invoices.ts index e245f335..7dbb1c0d 100644 --- a/frontend/src/lib/api/dashboard/a76/invoices.ts +++ b/frontend/src/lib/api/dashboard/a76/invoices.ts @@ -232,10 +232,10 @@ export interface InvoiceListResponse { } export interface CreateInvoiceData { - system?: string | null; - operation_type?: OperationType | null; - invoice_type?: string | null; - invoice_number?: string | null; + system: string; + operation_type: OperationType; + invoice_type: string; + invoice_number: string; project_number?: string | null; purchase_order?: string | null; related_doc_id?: number | null; diff --git a/frontend/src/lib/api/dashboard/a76/items.ts b/frontend/src/lib/api/dashboard/a76/items.ts new file mode 100644 index 00000000..2dc3e26e --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/items.ts @@ -0,0 +1,121 @@ +/** + * API Client para Items + * Gestiona las operaciones CRUD para items de facturas + */ +import { api } from '$lib/api'; + +// --- Interfaces --- + +export interface Item { + id?: number; + invoice_id: number; + reference_number?: string; + order?: string; + guide_number?: string; + depreciation_date?: number; + rectification?: number; + warehouse?: string; + location?: string; + created_at?: string; + updated_at?: string; +} + +export interface ItemListResponse { + items: Item[]; + total: number; + skip: number; + limit: number; +} + +export interface CreateItemData { + invoice_id: number; + reference_number?: string; + order?: string; + guide_number?: string; + depreciation_date?: number; + rectification?: number; + warehouse?: string; + location?: string; +} + +export interface UpdateItemData { + reference_number?: string; + order?: string; + guide_number?: string; + depreciation_date?: number; + rectification?: boolean; + warehouse?: string; + location?: string; +} + +/** + * API para Items + */ +export const itemsApi = { + /** + * Lista todos los items con paginación + */ + list: (companyId: number, skip = 0, limit = 100, invoiceId?: number) => { + const params = new URLSearchParams({ + company_id: companyId.toString(), + skip: skip.toString(), + limit: limit.toString() + }); + + if (invoiceId) { + params.append('invoice_id', invoiceId.toString()); + } + + return api.get(`/v1/a76/items/?${params.toString()}`); + }, + + /** + * Lista items por invoice ID + */ + listByInvoice: (invoiceId: number, companyId: number) => { + const params = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.get(`/v1/a76/items/invoice/${invoiceId}/items?${params.toString()}`); + }, + + /** + * Obtiene un item por ID + */ + get: (itemId: number, companyId: number) => { + const params = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.get(`/v1/a76/items/${itemId}?${params.toString()}`); + }, + + /** + * Crea un nuevo item + */ + create: (companyId: number, data: CreateItemData) => { + const params = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.post(`/v1/a76/items/?${params.toString()}`, data); + }, + + /** + * Actualiza un item existente + */ + update: (itemId: number, companyId: number, data: UpdateItemData) => { + const params = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.put(`/v1/a76/items/${itemId}?${params.toString()}`, data); + }, + + /** + * Elimina un item + */ + delete: (itemId: number, companyId: number) => { + const params = new URLSearchParams({ + company_id: companyId.toString() + }); + return api.delete(`/v1/a76/items/${itemId}?${params.toString()}`); + } +}; diff --git a/frontend/src/lib/components/dashboard/invoices/edit/general-tab-form.svelte b/frontend/src/lib/components/dashboard/invoices/edit/general-tab-form.svelte index 5f9e640d..73c5fd42 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/general-tab-form.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/general-tab-form.svelte @@ -17,6 +17,12 @@ providers = [], currencyTypes = [], transportTypes = [], + transporters = [], + vehicles = [], + drivers = [], + trailers = [], + customsSections = [], + codePedimentoRegimens = [], defaultOperationType = undefined, defaultInvoiceType = undefined }: { @@ -28,9 +34,15 @@ providers?: any[]; currencyTypes?: any[]; transportTypes?: any[]; + transporters?: any[]; + vehicles?: any[]; + drivers?: any[]; + trailers?: any[]; + customsSections?: any[]; + codePedimentoRegimens?: any[]; defaultOperationType?: number | null; defaultInvoiceType?: string | null; - } = $props(); + } = $props(); if (!formData) { if (invoice) { @@ -174,6 +186,13 @@ // Combinar clientes y proveedores para shipped_to const allClientsProviders = [...clients, ...providers]; + + // Filtrar regímenes por tipo de operación (1='1' exp, 2='2' imp) + const filteredRegimens = $derived( + codePedimentoRegimens.filter(r => + r.type_code === String(formData.operation_type) + ) + ); @@ -203,20 +222,17 @@

Clientes - Proveedores - Agente Aduanal

-
- +
{ formData.provider_header = v ?? ''; }} > - + - {formData.provider_header - ? providerHeaderOptions.find(o => o.value === formData.provider_header)?.label || formData.provider_header - : 'Selecciona encabezado...'} + {providerHeaderOptions.find(o => o.value === (formData.provider_header || providerHeaderOptions[0]?.value))?.label || 'Selecciona encabezado...'} @@ -234,7 +250,7 @@ formData.provider_id = v ? parseInt(v) : null; }} > - + {formData.provider_id ? providers.find(p => p.id === formData.provider_id)?.name || 'Selecciona...' @@ -248,23 +264,20 @@ {/each} - +
-
- +
{ formData.sold_to_header = v ?? ''; }} > - + - {formData.sold_to_header - ? soldToHeaderOptions.find(o => o.value === formData.sold_to_header)?.label || formData.sold_to_header - : 'Selecciona encabezado...'} + {soldToHeaderOptions.find(o => o.value === (formData.sold_to_header || soldToHeaderOptions[0]?.value))?.label || 'Selecciona encabezado...'} @@ -282,7 +295,7 @@ formData.sold_to_id = v ? parseInt(v) : null; }} > - + {formData.sold_to_id ? clients.find(c => c.id === formData.sold_to_id)?.name || 'Selecciona...' @@ -299,20 +312,17 @@
-
- +
{ formData.shipped_to_header = v ?? ''; }} > - + - {formData.shipped_to_header - ? shippedToHeaderOptions.find(o => o.value === formData.shipped_to_header)?.label || formData.shipped_to_header - : 'Selecciona encabezado...'} + {shippedToHeaderOptions.find(o => o.value === (formData.shipped_to_header || shippedToHeaderOptions[0]?.value))?.label || 'Selecciona encabezado...'} @@ -330,7 +340,7 @@ formData.shipped_to_id = v ? parseInt(v) : null; }} > - + {formData.shipped_to_id ? allClientsProviders.find(cp => cp.id === formData.shipped_to_id)?.name || 'Selecciona...' @@ -347,66 +357,66 @@
-
-
- - { - formData.customs_broker_id = v || null; - }} - > - - - {formData.customs_broker_id - ? customsBrokers.find(cb => cb.broker_key === formData.customs_broker_id)?.name || '...' - : '...'} - - - - {#each customsBrokers as broker} - - {broker.name} - - {/each} - - -
+ +
+ + { + formData.customs_broker_id = v || null; + }} + > + + + {customsBrokers.find(cb => cb.broker_key === (formData.customs_broker_id || customsBrokers[0]?.broker_key))?.name || '...'} + + + + {#each customsBrokers as broker} + + {broker.name} + + {/each} + + +
-
- - { - formData.customs_broker_us_id = v || null; - }} - > - - - {formData.customs_broker_us_id - ? customsBrokers.find(cb => cb.broker_key === formData.customs_broker_us_id)?.name || '...' - : '...'} - - - - {#each customsBrokers as broker} - - {broker.name} - - {/each} - - -
-
+
+ + { + formData.customs_broker_us_id = v || null; + }} + > + + + {formData.customs_broker_us_id + ? customsBrokers.find(cb => cb.broker_key === formData.customs_broker_us_id)?.name || '...' + : '...'} + + + + {#each customsBrokers as broker} + + {broker.name} + + {/each} + + +
-

Tipo de Moneda - Pesos Netos y Brutos

+
+

Tipo de Moneda - Pesos Netos y Brutos

+

Tipo de cambio:

+
@@ -425,8 +435,7 @@
- -
+ {#if formData.currency_mode === 'captura'}
- + {formData.currency_type || '...'} - + {#each currencyTypes as currencyType} {currencyType.code} @@ -450,7 +459,8 @@
- + {/if} +
- + {formData.weight_type || '...'} {#each weightTypeOptions as weightType} - - {weightType.value} + + {weightType.label} {/each} @@ -479,33 +489,7 @@
-
- -
- - { - formData.invoice_type = v ?? ''; - }} - > - - - {formData.invoice_type - ? `${formData.invoice_type}` - : '...'} - - - - {#each invoiceTypes as type} - - {type.key} - {type.description} - - {/each} - - -
+
@@ -514,8 +498,33 @@
- - + + { + formData.carrier_id = v || null; + }} + > + + + {#if formData.carrier_id} + {transporters.find(t => String(t.transporter_key) === String(formData.carrier_id))?.name || formData.carrier_id} + {:else if transporters.length > 0} + Selecciona transportista... + {:else} + Sin datos + {/if} + + + + {#each transporters as transporter} + + {transporter.transporter_key} + + {/each} + +
@@ -527,15 +536,113 @@ formData.transport_type = v ?? ''; }} > - + - {formData.transport_type || '...'} + {#if formData.transport_type} + {vehicles.find(v => v.vehicle_key === formData.transport_type)?.vehicle_key || formData.transport_type} + {:else if vehicles.length > 0} + Selecciona vehículo... + {:else} + Sin datos + {/if} - {#each transportTypes as transportType} - - {transportType.transport_code} + {#each vehicles as vehicle} + + {vehicle.vehicle_key} {vehicle.plate_number ? `- ${vehicle.plate_number}` : ''} + + {/each} + + +
+
+ +
+ + { + formData.driver_name = v ?? ''; + }} + > + + + {#if formData.driver_name} + {formData.driver_name} + {:else if drivers.length > 0} + Selecciona conductor... + {:else} + Sin datos + {/if} + + + + {#each drivers as driver} + + {driver.driver_name} + + {/each} + + +
+ +
+
+ + { + formData.transport_id = v ?? 'Ninguno'; + }} + > + + + {formData.transport_id || 'Ninguno'} + + + + Ninguno + Transporte + Caja + Placas + Camión + Buque + Ferrobarcaza + Contenedor + Avion + Gondola + Plataforma + + +
+ +
+ + { + formData.transport_num = v ?? ''; + }} + > + + + {#if formData.transport_num} + {trailers.find(t => t.trailer_number === formData.transport_num)?.plate_number || formData.transport_num} + {:else if trailers.length > 0} + Selecciona remolque... + {:else} + Sin datos + {/if} + + + + {#each trailers as trailer} + + {trailer.plate_number || trailer.trailer_number} {/each} @@ -543,31 +650,66 @@
-
- - -
- -
-
- - -
- -
- - -
-
-
- + { + formData.aduana = v ?? ''; + }} + > + + + {#if formData.aduana} + {customsSections.find(cs => cs.customs_code === formData.aduana)?.section_name || formData.aduana} + {:else if customsSections.length > 0} + Selecciona aduana... + {:else} + Sin datos + {/if} + + + + {#each customsSections as section} + + {section.customs_code} - {section.section_name} + + {/each} + +
- + { + formData.clave_regimen_aduanero = v ?? ''; + }} + > + + + {#if formData.clave_regimen_aduanero} + {formData.clave_regimen_aduanero} + {:else if filteredRegimens.length > 0} + Selecciona régimen... + {:else if formData.operation_type} + Sin regímenes para tipo {formData.operation_type} + {:else} + Selecciona tipo de operación primero + {/if} + + + + {#each filteredRegimens as regimen} + + {regimen.regimen_code} + + {/each} + +
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-configuration.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-configuration.svelte new file mode 100644 index 00000000..374d4067 --- /dev/null +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-configuration.svelte @@ -0,0 +1,72 @@ + + +
+ +
+
+ Is + +
+ + +
+
+ + +
+
+
+ +
+ Continue Sub-Items + +
+ + +
+
+ + +
+
+
+
+ + +
+
+ +
+ +
+
+ +
+ + +
+ +
+ + +
+
+
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-sheet-fa.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-sheet-fa.svelte new file mode 100644 index 00000000..ae9cdac2 --- /dev/null +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-sheet-fa.svelte @@ -0,0 +1,124 @@ + + + + + + + + + Temporary Import Item + + + Order Number: {invoice?.invoice_number || 'N/A'} | Line: 1 + + + +
+ +
+ +
+ +
+ + + +
+ + + + + 1) General + 2) Continuation + 3) Series + 4) Labeling + 5) Identifiers + + + + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + +
+
+
+
\ No newline at end of file diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/main-data.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/main-data.svelte new file mode 100644 index 00000000..6230db8e --- /dev/null +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/main-data.svelte @@ -0,0 +1,74 @@ + + +
+ Main Data + + +
+
+ +
+ +
+
+
+ + +
+
+ + +
+
+ +
+
+ - +
+
+
+
+ + +
+
+ +
+ + USD +
+
+
+ +
+ +
+
+
+ + +
+
+ +
+ +
+
+
+ + +
+
+ +
+ 0.00 +
+
+
+
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/packages-section.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/packages-section.svelte new file mode 100644 index 00000000..dfc5fd75 --- /dev/null +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/packages-section.svelte @@ -0,0 +1,86 @@ + + +
+ PACKAGES + +
+
+ + +
+
+ + +
+
+
+
+ +
+
+ +
+
+ +
+
+ + +
+
WEIGHTS
+
+
+ + +
+
+ + +
+
+ + KILOS +
+
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + Advalorem: 0.00 +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/summary-section.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/summary-section.svelte new file mode 100644 index 00000000..186aedfe --- /dev/null +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/summary-section.svelte @@ -0,0 +1,46 @@ + + +
+
+ GENERAL DATA + +
RETURN QUANTITY SUB-ITEMS
+
+
Temporary: 0.00000000
+
Replacement or Change: 0.00000000
+
Definitive: 0.00000000
+
Returned Values: 0.00000000
+
Returned Values: 0.00000000
+
+ +
+
WEIGHTS (KILOS)
+
WEIGHTS (Pounds)
+
Net: 0.00000000
+
0.00000000
+
Whole: 0.00000000
+
0.00000000
+
+
+ + +
+ COSTS AND VALUES + +
+
(Dollars)
+
(Pesos)
+
Cost: 0.00000000
+
0.00000000
+
Value: 0.00000000
+
0.00000000
+
+ +
+
Capture Cost: 0.00000000 USD
+
Capture Value: 0.00000000 USD
+
Customs Value: 0.00000000 USD
+
+
+
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-continuation.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-continuation.svelte new file mode 100644 index 00000000..cd4ff233 --- /dev/null +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-continuation.svelte @@ -0,0 +1,133 @@ + + +
+ +
+ +
+
+ TAX PAID + +
+ + +
+
+ + +
+
+
+
+
+ +
+ +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ Has Certificate of Origin? + +
+ + +
+
+ + +
+
+
+
+ + + +
+
+ + +
+
+ +
+ +
+ +
+
+ + +
+ + +
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+ +
+
+ +
+ +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+ + +
+ + +
+ + +
+
+
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-identifiers.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-identifiers.svelte new file mode 100644 index 00000000..63974265 --- /dev/null +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-identifiers.svelte @@ -0,0 +1,39 @@ + + +
+ Identifiers + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-labeling.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-labeling.svelte new file mode 100644 index 00000000..3dd58e63 --- /dev/null +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-labeling.svelte @@ -0,0 +1,28 @@ + + +
+ Labeling + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-series.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-series.svelte new file mode 100644 index 00000000..d5f242f6 --- /dev/null +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/tab-series.svelte @@ -0,0 +1,20 @@ + + +
+ Serial Numbers + +
+ + +
+ +
+ You can enter multiple serial numbers, one per line +
+
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/inv/item-sheet-inv.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/inv/item-sheet-inv.svelte new file mode 100644 index 00000000..4b697771 --- /dev/null +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/inv/item-sheet-inv.svelte @@ -0,0 +1,274 @@ + + + + + + {isEditMode ? 'Editar Item' : 'Agregar Nuevo Item'} - SCAII (Inventario) + + {isEditMode ? 'Modifica los campos del inventario y guarda los cambios.' : 'Completa la información del nuevo item de inventario.'} + + + + + + General + Clasificación + Cantidades + Otros + + + + + +
+

Información de la Factura (SCAII - Inventario)

+ {#if !invoice?.id} +
+ ⚠️ Esta factura aún no se ha guardado. Los items se asociarán cuando guardes la factura. +
+ {:else} +
+
+ ID Factura: + {invoice.id} +
+
+ Tipo Operación: + {invoice.operation_type || 'N/A'} +
+
+ Número de Factura: + {invoice.invoice_number || 'Pendiente'} +
+
+ Sistema: + SCAII (Inventory) +
+
+ {/if} +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ +
+
+ + +
+
+ + +
+
+
+ + + +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+
+
+ + + +
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+
+
+ + + +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+
+ + + + + +
+
diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte new file mode 100644 index 00000000..e30f3a3d --- /dev/null +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte @@ -0,0 +1,377 @@ + + +
+
+
+

Items de la Factura

+ +
+ +
+ + + + Referencia + Orden + Almacén + Ubicación + Acciones + + + + {#if displayedItems.length === 0} + + + No hay items disponibles + + + {:else} + {#each displayedItems as item (item.id)} + + {item.reference_number || '-'} + {item.order || '-'} + {item.warehouse || '-'} + {item.location || '-'} + +
+ + +
+
+
+ {/each} + {#if isLoadingMore} + + + Cargando más items... + + + {/if} + {/if} +
+
+
+ + {#if items.length > 0} +
+ Mostrando {displayedItems.length} de {items.length} items +
+ {/if} +
+ +
+
+

Cantidades:

+
+
+
+ Partidas: {items.length || 0} +
+
+ Bultos: 0 +
+
+
+ Importada: {imported || 0}
+ Peso neto: {net_weight || 0}
+ Peso bruto: {gross_weight || 0}
+
+ +

Valores de importacion:

+ Dolares: 0 USD
+ Pesos: 0 MXN
+ De Captura: 0 USD + +

spacer

+ + Aduana: 0 USD
+ Aduana: 0 MXN
+
+
+ + + +{#if invoiceSystem === 'fixed_asset'} + +{:else} + +{/if} + + + + + + Confirmar Eliminación + + ¿Está seguro que desea eliminar este item? Esta acción no se puede deshacer. + + + + + + + + diff --git a/frontend/src/lib/components/dashboard/invoices/edit/save-invoice.ts b/frontend/src/lib/components/dashboard/invoices/edit/save-invoice.ts new file mode 100644 index 00000000..de34c42e --- /dev/null +++ b/frontend/src/lib/components/dashboard/invoices/edit/save-invoice.ts @@ -0,0 +1,407 @@ +import { goto } from '$app/navigation'; +import { invoicesApi, type CreateInvoiceData, type UpdateInvoiceData, type OperationType } from '$lib/api/dashboard/a76/invoices'; + +interface FormDataSet { + generalFormData: any; + observationFormData: any; + itemsFormData: any; + othersFormData: any; +} + +interface SaveInvoiceOptions { + invoiceId: number | null; + isCreate: boolean; + companyId: number; + formData: FormDataSet; +} + +export async function saveInvoice(options: SaveInvoiceOptions): Promise<{ success: boolean; error?: string; newInvoiceId?: number }> { + const { invoiceId, isCreate, companyId, formData } = options; + const { generalFormData, observationFormData, itemsFormData, othersFormData } = formData; + + try { + // Validar campos requeridos para creación + if (isCreate && generalFormData) { + const requiredFields = { + operation_type: 'Tipo de Operación', + invoice_type: 'Tipo de Factura', + invoice_number: 'Número de Factura' + }; + + const missingFields: string[] = []; + for (const [field, label] of Object.entries(requiredFields)) { + const value = generalFormData[field]; + if (value === null || value === undefined || value === '') { + missingFields.push(label); + } + } + + if (missingFields.length > 0) { + throw new Error(`Los siguientes campos son obligatorios: ${missingFields.join(', ')}`); + } + } + + // Construir el payload unificado + const payload = buildInvoicePayload(formData); + + let newInvoiceId = invoiceId; + + if (isCreate) { + // Crear nueva factura con todos sus sub-recursos + const response = await invoicesApi.create(companyId, payload as CreateInvoiceData); + if (response.error) { + const errorMsg = typeof response.error === 'string' ? response.error : 'Error al crear la factura'; + throw new Error(errorMsg); + } + if (!response.data?.id) throw new Error('No se recibió el ID de la factura creada'); + newInvoiceId = response.data.id; + + // Redirigir a la página de edición + await goto(`/dashboard/invoices/edit/${newInvoiceId}`); + } else { + // Actualizar factura existente con todos sus sub-recursos + const response = await invoicesApi.update(invoiceId!, companyId, payload as UpdateInvoiceData); + if (response.error) throw new Error(response.error); + } + + return { success: true, newInvoiceId: newInvoiceId ?? undefined }; + } catch (e) { + const error = e instanceof Error ? e.message : 'Error al guardar los cambios'; + return { success: false, error }; + } +} + +function buildInvoicePayload(formData: FormDataSet): CreateInvoiceData | UpdateInvoiceData { + const { generalFormData, observationFormData, itemsFormData, othersFormData } = formData; + + const payload: CreateInvoiceData | UpdateInvoiceData = { + // Datos generales desde el formulario general + system: 'fixed_asset', + operation_type: generalFormData?.operation_type !== null && generalFormData?.operation_type !== undefined + ? (generalFormData.operation_type === 1 ? 'exp' : 'imp') as OperationType + : undefined, + invoice_type: generalFormData?.invoice_type || undefined, + invoice_number: generalFormData?.invoice_number || undefined, + invoice_date: generalFormData?.invoice_date || undefined, + emission_date: generalFormData?.emission_date || undefined, + // Observation fields from observationFormData + observation_es: observationFormData?.observation_es || undefined, + observation_en: observationFormData?.observation_en || undefined, + alternate_invoice: observationFormData?.alternate_invoice || undefined, + }; + + // Solo agregar sub-recursos si tienen valores reales + + // Compliance MX + const hasComplianceValue = generalFormData?.pedimento || generalFormData?.remesa || generalFormData?.aduana || + generalFormData?.provider_id || generalFormData?.sold_to_id || + generalFormData?.shipped_to_id || generalFormData?.customs_broker_id || + observationFormData?.pedimento || observationFormData?.pedimento_code || + observationFormData?.remesa || observationFormData?.aduana || + observationFormData?.provider_id || observationFormData?.sold_to_id || + observationFormData?.shipped_to_id || observationFormData?.shipped_by_id || + observationFormData?.customs_broker_id || observationFormData?.is_mixed || + observationFormData?.waste_type || observationFormData?.appendix_17 || + observationFormData?.edocument || observationFormData?.electronic_signature || + observationFormData?.sem_id || observationFormData?.enclosure || + observationFormData?.incoterm; + + if (hasComplianceValue) { + payload.compliance_mx = buildComplianceMxData(generalFormData, observationFormData); + } + + // Financials + const hasFinancialsValue = generalFormData?.currency_type || + generalFormData?.iva_factor || + itemsFormData?.currency || itemsFormData?.exchange_rate || + itemsFormData?.value_mn || itemsFormData?.value_me || + itemsFormData?.customs_value_mn || itemsFormData?.freight || + itemsFormData?.insurance; + + if (hasFinancialsValue) { + payload.financials = buildFinancialsData(generalFormData, itemsFormData, observationFormData); + } + + // Logistics + const hasLogisticsFromGeneral = generalFormData?.carrier_id || + generalFormData?.driver_name || generalFormData?.transport_type || generalFormData?.transport_num; + + if (hasLogisticsFromGeneral || (othersFormData && Array.isArray(othersFormData) && othersFormData.length > 0)) { + payload.logistics = buildLogisticsData(generalFormData, othersFormData, observationFormData); + } + + // Eliminar campos undefined para no enviarlos + Object.keys(payload).forEach(key => { + if (payload[key as keyof typeof payload] === undefined) { + delete payload[key as keyof typeof payload]; + } + }); + + return payload; +} + +function buildComplianceMxData(generalFormData: any, observationFormData: any) { + return { + // Pedimento fields + pedimento: generalFormData?.pedimento || observationFormData?.pedimento || null, + pedimento_code: observationFormData?.pedimento_code || null, + pedimento_k1: observationFormData?.pedimento_k1 || null, + remesa: generalFormData?.remesa || observationFormData?.remesa || null, + aduana: generalFormData?.aduana || observationFormData?.aduana || null, + port_of_entry: observationFormData?.port_of_entry || null, + destination: observationFormData?.destination || null, + manifest_number: observationFormData?.manifest_number || null, + // Client/Provider fields + provider_header: generalFormData?.provider_header || observationFormData?.provider_header || null, + provider_id: generalFormData?.provider_id || observationFormData?.provider_id || null, + sold_to_header: generalFormData?.sold_to_header || observationFormData?.sold_to_header || null, + sold_to_id: generalFormData?.sold_to_id || observationFormData?.sold_to_id || null, + shipped_to_header: generalFormData?.shipped_to_header || observationFormData?.shipped_to_header || null, + shipped_to_id: generalFormData?.shipped_to_id || observationFormData?.shipped_to_id || null, + shipped_by_header: observationFormData?.shipped_by_header || null, + shipped_by_id: observationFormData?.shipped_by_id || null, + // Customs broker fields + customs_broker_id: generalFormData?.customs_broker_id || observationFormData?.customs_broker_id || null, + customs_broker_us_id: observationFormData?.customs_broker_us_id || null, + broker_invoice_num: observationFormData?.broker_invoice_num || null, + broker_invoice_date: observationFormData?.broker_invoice_date || null, + // Flags & regimes + is_mixed: observationFormData?.is_mixed || null, + waste_type: observationFormData?.waste_type || null, + scrap_type: observationFormData?.scrap_type || null, + appendix_17: observationFormData?.appendix_17 || null, + is_regime_change: observationFormData?.is_regime_change || null, + which_exchange_rate: observationFormData?.which_exchange_rate || null, + value_method: observationFormData?.value_method || null, + act_value: observationFormData?.act_value || null, + is_pedimento_pending: observationFormData?.is_pedimento_pending || null, + // Ownership & balances + is_owner_of_goods: observationFormData?.is_owner_of_goods || null, + generate_balances: observationFormData?.generate_balances || null, + was_reviewed_by_company: observationFormData?.was_reviewed_by_company || null, + // VUCEM / Digital + edocument: observationFormData?.edocument || null, + electronic_signature: observationFormData?.electronic_signature || null, + certificate_number: observationFormData?.certificate_number || null, + niu_number: observationFormData?.niu_number || null, + bill_of_lading_count: observationFormData?.bill_of_lading_count || null, + addendum_vu: observationFormData?.addendum_vu || null, + origin_destination_cove: observationFormData?.origin_destination_cove || null, + vucem_operation_num: observationFormData?.vucem_operation_num || null, + customs_person_line: observationFormData?.customs_person_line || null, + // Additional control + contingency_mode: observationFormData?.contingency_mode || null, + enclosure: observationFormData?.enclosure || null, + guide_type_to_identify: observationFormData?.guide_type_to_identify || null, + location: observationFormData?.location || null, + // DOT & official + dot_code: observationFormData?.dot_code || null, + subdivision: observationFormData?.subdivision || null, + acts_as: observationFormData?.acts_as || null, + movement_type: observationFormData?.movement_type || null, + office_document: observationFormData?.office_document || null, + reason_export: observationFormData?.reason_export || null, + signature_key: observationFormData?.signature_key || null, + // SM specific + sem_id: observationFormData?.sem_id || null, + }; +} + +function buildFinancialsData(generalFormData: any, itemsFormData: any, observationFormData: any) { + return { + // Currency + currency: itemsFormData?.currency || null, + currency_type: generalFormData?.currency_type || itemsFormData?.currency_type || null, + exchange_rate: itemsFormData?.exchange_rate || null, + exchange_rate_mm: itemsFormData?.exchange_rate_mm || null, + // Merchandise values + value_mn: itemsFormData?.value_mn || null, + value_me: itemsFormData?.value_me || null, + value_mc: itemsFormData?.value_mc || null, + // Customs value + customs_value_mn: itemsFormData?.customs_value_mn || null, + customs_value_me: itemsFormData?.customs_value_me || null, + // Raw materials + raw_material_value_mn: itemsFormData?.raw_material_value_mn || null, + raw_material_value_me: itemsFormData?.raw_material_value_me || null, + // Aggregate value + aggregate_value_mn: itemsFormData?.aggregate_value_mn || null, + aggregate_value_me: itemsFormData?.aggregate_value_me || null, + aggregate_value_mc: itemsFormData?.aggregate_value_mc || null, + // Mexican merchandise value + mexican_value_mn: itemsFormData?.mexican_value_mn || null, + mexican_value_me: itemsFormData?.mexican_value_me || null, + mexican_value_mc: itemsFormData?.mexican_value_mc || null, + // National packaging + national_packaging_mn: itemsFormData?.national_packaging_mn || null, + national_packaging_me: itemsFormData?.national_packaging_me || null, + national_packaging_mc: itemsFormData?.national_packaging_mc || null, + // Costs & increments + freight: itemsFormData?.freight || observationFormData?.freight || null, + insurance: itemsFormData?.insurance || observationFormData?.insurance || null, + insurance_value: itemsFormData?.insurance_value || observationFormData?.insurance_value || null, + packaging: itemsFormData?.packaging || observationFormData?.packaging || null, + other_increments: itemsFormData?.other_increments || observationFormData?.other_increments || null, + total_increments_mn: itemsFormData?.total_increments_mn || observationFormData?.total_increments_mn || null, + total_increments_me: itemsFormData?.total_increments_me || observationFormData?.total_increments_me || null, + // Taxes + iva_mn: itemsFormData?.iva_mn || null, + iva_me: itemsFormData?.iva_me || null, + iva_mc: itemsFormData?.iva_mc || null, + iva_factor: generalFormData?.iva_factor || itemsFormData?.iva_factor || null, + tax_value_me: itemsFormData?.tax_value_me || null, + seal_value_2500: itemsFormData?.seal_value_2500 || null, + // Weights & quantities + total_quantity: itemsFormData?.total_quantity || null, + gross_weight: itemsFormData?.gross_weight || null, + net_weight: itemsFormData?.net_weight || null, + bundle_count: itemsFormData?.bundle_count || null, + weight_factor: itemsFormData?.weight_factor || null, + }; +} + +function buildLogisticsData(generalFormData: any, othersFormData: any, observationFormData: any) { + const hasLogisticsFromGeneral = generalFormData?.carrier_id || + generalFormData?.driver_name || generalFormData?.transport_type || generalFormData?.transport_num; + + if (hasLogisticsFromGeneral) { + const logisticsEntry = buildLogisticsEntry(generalFormData, observationFormData); + + // Si también hay datos del formulario de others, combinarlos + if (othersFormData && Array.isArray(othersFormData) && othersFormData.length > 0) { + // Actualizar el primer elemento con datos del general + return [ + mergeLogisticsEntries(generalFormData, othersFormData[0], observationFormData), + // Agregar los demás elementos si existen + ...othersFormData.slice(1).map((item: any) => buildLogisticsEntryFromOther(item, observationFormData)) + ]; + } else { + // Solo datos del general + return [logisticsEntry]; + } + } else { + // Solo datos del formulario others + return othersFormData.map((item: any) => buildLogisticsEntryFromOther(item, observationFormData)); + } +} + +function buildLogisticsEntry(generalFormData: any, observationFormData: any) { + return { + carrier_id: generalFormData?.carrier_id || null, + transport_type: generalFormData?.transport_type || null, + transport_mode: null, + driver_name: generalFormData?.driver_name || null, + is_rail: null, + rail_id: null, + vehicle_num: generalFormData?.transport_num || null, + license_plate: null, + seal_number: null, + guide_number: null, + entry_exit_date: null, + incoterm: observationFormData?.incoterm || null, + }; +} + +function mergeLogisticsEntries(generalFormData: any, otherData: any, observationFormData: any) { + return { + // Carrier info + carrier_id: generalFormData?.carrier_id || otherData.carrier_id || null, + transport_id: otherData.transport_id || null, + transport_us_id: otherData.transport_us_id || null, + transport_type: generalFormData?.transport_type || otherData.transport_type || null, + transport_num: otherData.transport_num || null, + transport_mode: otherData.transport_mode || null, + driver_name: generalFormData?.driver_name || otherData.driver_name || null, + is_rail: otherData.is_rail || null, + rail_id: otherData.rail_id || null, + // Vehicle & tracking + vehicle_num: generalFormData?.transport_num || otherData.vehicle_num || null, + license_plate: otherData.license_plate || null, + license_plate_complete: otherData.license_plate_complete || null, + trailer_num: otherData.trailer_num || null, + seal_number: otherData.seal_number || null, + guide_number: otherData.guide_number || null, + bill_number: otherData.bill_number || null, + reference_number: otherData.reference_number || null, + shipment_number: otherData.shipment_number || null, + // Incoterms + incoterm: otherData.incoterm || observationFormData?.incoterm || null, + // Identifiers & complements + identifier_1: otherData.identifier_1 || null, + complement_1: otherData.complement_1 || null, + identifier_2: otherData.identifier_2 || null, + complement_2: otherData.complement_2 || null, + // Weight & container info + weight_type: otherData.weight_type || null, + container_types: otherData.container_types || null, + vehicle_data: otherData.vehicle_data || null, + // Locations & routes + origin_location: otherData.origin_location || null, + destination_location: otherData.destination_location || null, + transport_itinerary: otherData.transport_itinerary || null, + destination_goods: otherData.destination_goods || null, + // Logistics dates + entry_exit_date: otherData.entry_exit_date || null, + delivery_date: otherData.delivery_date || null, + // Delivery control + delivered_status: otherData.delivered_status || null, + received_by: otherData.received_by || null, + // Payment info + payment_date: otherData.payment_date || null, + payment_receipt_num: otherData.payment_receipt_num || null, + // CTM process + is_ctm_process: otherData.is_ctm_process || null, + }; +} + +function buildLogisticsEntryFromOther(item: any, observationFormData: any) { + return { + // Carrier info + carrier_id: item.carrier_id || null, + transport_id: item.transport_id || null, + transport_us_id: item.transport_us_id || null, + transport_type: item.transport_type || null, + transport_num: item.transport_num || null, + transport_mode: item.transport_mode || null, + driver_name: item.driver_name || null, + is_rail: item.is_rail || null, + rail_id: item.rail_id || null, + // Vehicle & tracking + vehicle_num: item.vehicle_num || null, + license_plate: item.license_plate || null, + license_plate_complete: item.license_plate_complete || null, + trailer_num: item.trailer_num || null, + seal_number: item.seal_number || null, + guide_number: item.guide_number || null, + bill_number: item.bill_number || null, + reference_number: item.reference_number || null, + shipment_number: item.shipment_number || null, + // Incoterms + incoterm: item.incoterm || observationFormData?.incoterm || null, + // Identifiers & complements + identifier_1: item.identifier_1 || null, + complement_1: item.complement_1 || null, + identifier_2: item.identifier_2 || null, + complement_2: item.complement_2 || null, + // Weight & container info + weight_type: item.weight_type || null, + container_types: item.container_types || null, + vehicle_data: item.vehicle_data || null, + // Locations & routes + origin_location: item.origin_location || null, + destination_location: item.destination_location || null, + transport_itinerary: item.transport_itinerary || null, + destination_goods: item.destination_goods || null, + // Logistics dates + entry_exit_date: item.entry_exit_date || null, + delivery_date: item.delivery_date || null, + // Delivery control + delivered_status: item.delivered_status || null, + received_by: item.received_by || null, + // Payment info + payment_date: item.payment_date || null, + payment_receipt_num: item.payment_receipt_num || null, + // CTM process + is_ctm_process: item.is_ctm_process || null, + }; +} diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index 8c56a3c6..5c8a7830 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -1,6 +1,7 @@ @@ -9,4 +10,5 @@ + {@render children?.()} diff --git a/frontend/src/routes/api-sveltekit/invoices/[id]/edit-data/+server.ts b/frontend/src/routes/api-sveltekit/invoices/[id]/edit-data/+server.ts index 48c5cd3f..feb116b4 100644 --- a/frontend/src/routes/api-sveltekit/invoices/[id]/edit-data/+server.ts +++ b/frontend/src/routes/api-sveltekit/invoices/[id]/edit-data/+server.ts @@ -30,6 +30,12 @@ export const GET: RequestHandler = async ({ params, cookies, fetch }) => { providersResponse, currencyTypesResponse, transportTypesResponse, + transportersResponse, + vehiclesResponse, + driversResponse, + trailersResponse, + customsSectionsResponse, + codePedimentoRegimensResponse, sealsResponse, incotermsResponse, pedimentosResponse @@ -41,6 +47,12 @@ export const GET: RequestHandler = async ({ params, cookies, fetch }) => { authenticatedFetch(`v1/a76/clients-providers/?company_id=${companyId}&type=provider&page=1&page_size=1000`, {}, cookies, fetch), authenticatedFetch('v1/public/refrence_data/currency-types/?page=1&page_size=100', {}, cookies, fetch), authenticatedFetch('v1/public/refrence_data/transport-types/?page=1&page_size=100', {}, cookies, fetch), + authenticatedFetch(`v1/a76/transportation/transporters/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch), + authenticatedFetch(`v1/a76/transportation/vehicles/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch), + authenticatedFetch(`v1/a76/transportation/drivers/?company_id=${companyId}`, {}, cookies, fetch), + authenticatedFetch(`v1/a76/transportation/trailers/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch), + authenticatedFetch('v1/public/refrence_data/customs-sections/?page=1&page_size=100', {}, cookies, fetch), + authenticatedFetch('v1/public/refrence_data/code-pedimento-regimens/?page=1&page_size=1000', {}, cookies, fetch), authenticatedFetch(`v1/a76/seals/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch), authenticatedFetch('v1/public/refrence_data/incoterms/?page=1&page_size=100', {}, cookies, fetch), authenticatedFetch(`v1/a76/pedimentos/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch) @@ -57,6 +69,12 @@ export const GET: RequestHandler = async ({ params, cookies, fetch }) => { const providers = providersResponse.ok ? await providersResponse.json() : { items: [] }; const currencyTypes = currencyTypesResponse.ok ? await currencyTypesResponse.json() : { items: [] }; const transportTypes = transportTypesResponse.ok ? await transportTypesResponse.json() : { items: [] }; + const transporters = transportersResponse.ok ? await transportersResponse.json() : { items: [] }; + const vehicles = vehiclesResponse.ok ? await vehiclesResponse.json() : { items: [] }; + const drivers = driversResponse.ok ? await driversResponse.json() : { items: [] }; + const trailers = trailersResponse.ok ? await trailersResponse.json() : { items: [] }; + const customsSections = customsSectionsResponse.ok ? await customsSectionsResponse.json() : { items: [] }; + const codePedimentoRegimens = codePedimentoRegimensResponse.ok ? await codePedimentoRegimensResponse.json() : { items: [] }; const seals = sealsResponse.ok ? await sealsResponse.json() : { items: [] }; const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] }; const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] }; @@ -69,6 +87,12 @@ export const GET: RequestHandler = async ({ params, cookies, fetch }) => { providers: providers.items || [], currencyTypes: currencyTypes.items || [], transportTypes: transportTypes.items || [], + transporters: transporters.items || [], + vehicles: vehicles.items || [], + drivers: drivers.items || [], + trailers: trailers.items || [], + customsSections: customsSections.items || [], + codePedimentoRegimens: codePedimentoRegimens.items || [], seals: seals.items || [], incoterms: incoterms.items || [], pedimentos: pedimentos.items || [] diff --git a/frontend/src/routes/api-sveltekit/invoices/reference-data/+server.ts b/frontend/src/routes/api-sveltekit/invoices/reference-data/+server.ts index 97c0e9ab..85ad19e1 100644 --- a/frontend/src/routes/api-sveltekit/invoices/reference-data/+server.ts +++ b/frontend/src/routes/api-sveltekit/invoices/reference-data/+server.ts @@ -23,8 +23,12 @@ export const GET: RequestHandler = async ({ cookies, fetch }) => { clientsResponse, providersResponse, currencyTypesResponse, - transportTypesResponse, - sealsResponse, + transportTypesResponse, transportersResponse, + vehiclesResponse, + driversResponse, + trailersResponse, + customsSectionsResponse, + codePedimentoRegimensResponse, sealsResponse, incotermsResponse, pedimentosResponse ] = await Promise.all([ @@ -34,6 +38,12 @@ export const GET: RequestHandler = async ({ cookies, fetch }) => { authenticatedFetch(`v1/a76/clients-providers/?company_id=${companyId}&type=provider&page=1&page_size=1000`, {}, cookies, fetch), authenticatedFetch('v1/public/refrence_data/currency-types/?page=1&page_size=100', {}, cookies, fetch), authenticatedFetch('v1/public/refrence_data/transport-types/?page=1&page_size=100', {}, cookies, fetch), + authenticatedFetch(`v1/a76/transportation/transporters/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch), + authenticatedFetch(`v1/a76/transportation/vehicles/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch), + authenticatedFetch(`v1/a76/transportation/drivers/?company_id=${companyId}`, {}, cookies, fetch), + authenticatedFetch(`v1/a76/transportation/trailers/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch), + authenticatedFetch('v1/public/refrence_data/customs-sections/?page=1&page_size=100', {}, cookies, fetch), + authenticatedFetch('v1/public/refrence_data/code-pedimento-regimens/?page=1&page_size=1000', {}, cookies, fetch), authenticatedFetch(`v1/a76/seals/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch), authenticatedFetch('v1/public/refrence_data/incoterms/?page=1&page_size=100', {}, cookies, fetch), authenticatedFetch(`v1/a76/pedimentos/?company_id=${companyId}&page=1&page_size=100`, {}, cookies, fetch) @@ -45,6 +55,12 @@ export const GET: RequestHandler = async ({ cookies, fetch }) => { const providers = providersResponse.ok ? await providersResponse.json() : { items: [] }; const currencyTypes = currencyTypesResponse.ok ? await currencyTypesResponse.json() : { items: [] }; const transportTypes = transportTypesResponse.ok ? await transportTypesResponse.json() : { items: [] }; + const transporters = transportersResponse.ok ? await transportersResponse.json() : { items: [] }; + const vehicles = vehiclesResponse.ok ? await vehiclesResponse.json() : { items: [] }; + const drivers = driversResponse.ok ? await driversResponse.json() : { items: [] }; + const trailers = trailersResponse.ok ? await trailersResponse.json() : { items: [] }; + const customsSections = customsSectionsResponse.ok ? await customsSectionsResponse.json() : { items: [] }; + const codePedimentoRegimens = codePedimentoRegimensResponse.ok ? await codePedimentoRegimensResponse.json() : { items: [] }; const seals = sealsResponse.ok ? await sealsResponse.json() : { items: [] }; const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] }; const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] }; @@ -56,6 +72,12 @@ export const GET: RequestHandler = async ({ cookies, fetch }) => { providers: providers.items || [], currencyTypes: currencyTypes.items || [], transportTypes: transportTypes.items || [], + transporters: transporters.items || [], + vehicles: vehicles.items || [], + drivers: drivers.items || [], + trailers: trailers.items || [], + customsSections: customsSections.items || [], + codePedimentoRegimens: codePedimentoRegimens.items || [], seals: seals.items || [], incoterms: incoterms.items || [], pedimentos: pedimentos.items || [] diff --git a/frontend/src/routes/dashboard/invoices/edit/[id]/+page.server.ts b/frontend/src/routes/dashboard/invoices/edit/[id]/+page.server.ts index 4321a3c7..77e08746 100644 --- a/frontend/src/routes/dashboard/invoices/edit/[id]/+page.server.ts +++ b/frontend/src/routes/dashboard/invoices/edit/[id]/+page.server.ts @@ -74,6 +74,48 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => { fetch ); + const transportersPromise = authenticatedFetch( + `v1/a76/transportation/transporters/?company_id=${companyId}&page=1&page_size=100`, + {}, + cookies, + fetch + ); + + const vehiclesPromise = authenticatedFetch( + `v1/a76/transportation/vehicles/?company_id=${companyId}&page=1&page_size=100`, + {}, + cookies, + fetch + ); + + const driversPromise = authenticatedFetch( + `v1/a76/transportation/drivers/?company_id=${companyId}`, + {}, + cookies, + fetch + ); + + const trailersPromise = authenticatedFetch( + `v1/a76/transportation/trailers/?company_id=${companyId}&page=1&page_size=100`, + {}, + cookies, + fetch + ); + + const customsSectionsPromise = authenticatedFetch( + 'v1/public/refrence_data/customs-sections/?page=1&page_size=100', + {}, + cookies, + fetch + ); + + const codePedimentoRegimensPromise = authenticatedFetch( + 'v1/public/refrence_data/code-pedimento-regimens/?page=1&page_size=1000', + {}, + cookies, + fetch + ); + const sealsPromise = authenticatedFetch( `v1/a76/seals/?company_id=${companyId}&page=1&page_size=100`, {}, @@ -105,6 +147,12 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => { providersResponse, currencyTypesResponse, transportTypesResponse, + transportersResponse, + vehiclesResponse, + driversResponse, + trailersResponse, + customsSectionsResponse, + codePedimentoRegimensResponse, sealsResponse, incotermsResponse, pedimentosResponse @@ -115,6 +163,12 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => { providersPromise, currencyTypesPromise, transportTypesPromise, + transportersPromise, + vehiclesPromise, + driversPromise, + trailersPromise, + customsSectionsPromise, + codePedimentoRegimensPromise, sealsPromise, incotermsPromise, pedimentosPromise @@ -126,6 +180,12 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => { const providers = providersResponse.ok ? await providersResponse.json() : { items: [] }; const currencyTypes = currencyTypesResponse.ok ? await currencyTypesResponse.json() : { items: [] }; const transportTypes = transportTypesResponse.ok ? await transportTypesResponse.json() : { items: [] }; + const transporters = transportersResponse.ok ? await transportersResponse.json() : { items: [] }; + const vehicles = vehiclesResponse.ok ? await vehiclesResponse.json() : { items: [] }; + const drivers = driversResponse.ok ? await driversResponse.json() : { items: [] }; + const trailers = trailersResponse.ok ? await trailersResponse.json() : { items: [] }; + const customsSections = customsSectionsResponse.ok ? await customsSectionsResponse.json() : { items: [] }; + const codePedimentoRegimens = codePedimentoRegimensResponse.ok ? await codePedimentoRegimensResponse.json() : { items: [] }; const seals = sealsResponse.ok ? await sealsResponse.json() : { items: [] }; const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] }; const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] }; @@ -140,6 +200,12 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => { providers: providers.items || [], currencyTypes: currencyTypes.items || [], transportTypes: transportTypes.items || [], + transporters: transporters.items || [], + vehicles: vehicles.items || [], + drivers: drivers.items || [], + trailers: trailers.items || [], + customsSections: customsSections.items || [], + codePedimentoRegimens: codePedimentoRegimens.items || [], seals: seals.items || [], incoterms: incoterms.items || [], pedimentos: pedimentos.items || [], @@ -162,6 +228,12 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => { providers: [], currencyTypes: [], transportTypes: [], + transporters: [], + vehicles: [], + drivers: [], + trailers: [], + customsSections: [], + codePedimentoRegimens: [], seals: [], incoterms: [], pedimentos: [], @@ -201,6 +273,12 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => { providersResponse, currencyTypesResponse, transportTypesResponse, + transportersResponse, + vehiclesResponse, + driversResponse, + trailersResponse, + customsSectionsResponse, + codePedimentoRegimensResponse, sealsResponse, incotermsResponse, pedimentosResponse @@ -211,6 +289,12 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => { providersPromise, currencyTypesPromise, transportTypesPromise, + transportersPromise, + vehiclesPromise, + driversPromise, + trailersPromise, + customsSectionsPromise, + codePedimentoRegimensPromise, sealsPromise, incotermsPromise, pedimentosPromise @@ -222,6 +306,12 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => { const providers = providersResponse.ok ? await providersResponse.json() : { items: [] }; const currencyTypes = currencyTypesResponse.ok ? await currencyTypesResponse.json() : { items: [] }; const transportTypes = transportTypesResponse.ok ? await transportTypesResponse.json() : { items: [] }; + const transporters = transportersResponse.ok ? await transportersResponse.json() : { items: [] }; + const vehicles = vehiclesResponse.ok ? await vehiclesResponse.json() : { items: [] }; + const drivers = driversResponse.ok ? await driversResponse.json() : { items: [] }; + const trailers = trailersResponse.ok ? await trailersResponse.json() : { items: [] }; + const customsSections = customsSectionsResponse.ok ? await customsSectionsResponse.json() : { items: [] }; + const codePedimentoRegimens = codePedimentoRegimensResponse.ok ? await codePedimentoRegimensResponse.json() : { items: [] }; const seals = sealsResponse.ok ? await sealsResponse.json() : { items: [] }; const incoterms = incotermsResponse.ok ? await incotermsResponse.json() : { items: [] }; const pedimentos = pedimentosResponse.ok ? await pedimentosResponse.json() : { items: [] }; @@ -236,6 +326,12 @@ export const load: PageServerLoad = async ({ params, cookies, fetch, url }) => { providers: providers.items || [], currencyTypes: currencyTypes.items || [], transportTypes: transportTypes.items || [], + transporters: transporters.items || [], + vehicles: vehicles.items || [], + drivers: drivers.items || [], + trailers: trailers.items || [], + customsSections: customsSections.items || [], + codePedimentoRegimens: codePedimentoRegimens.items || [], seals: seals.items || [], incoterms: incoterms.items || [], pedimentos: pedimentos.items || [], diff --git a/frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte b/frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte index 3a35472a..afac04dd 100644 --- a/frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte +++ b/frontend/src/routes/dashboard/invoices/edit/[id]/+page.svelte @@ -23,16 +23,16 @@ // Importar los componentes de cada pestaña import GeneralTabForm from '$lib/components/dashboard/invoices/edit/general-tab-form.svelte'; import ObservationsTabForm from '$lib/components/dashboard/invoices/edit/observations-tab-form.svelte'; - import ItemsTabForm from '$lib/components/dashboard/invoices/edit/continuation-tab-form.svelte'; + import ItemsTabForm from '$lib/components/dashboard/invoices/edit/items/items-tab-form.svelte'; import OthersTabForm from '$lib/components/dashboard/invoices/edit/others-tab-form.svelte'; import InvoiceTopFields from '$lib/components/dashboard/invoices/edit/invoice-top-fields.svelte'; import ContinuationTabForm from '$lib/components/dashboard/invoices/edit/continuation-tab-form.svelte'; // Importar la API de facturas - import { invoicesApi, type CreateInvoiceData, type UpdateInvoiceData, type OperationType } from '$lib/api/dashboard/a76/invoices'; import type { InvoiceType } from '$lib/api/dashboard/refrence_data/invoice_types'; import type { CustomsBroker } from '$lib/api/dashboard/a76/customs-brokers'; import type { ClientProvider } from '$lib/api/dashboard/a76/clients-providers'; + import { saveInvoice } from '$lib/components/dashboard/invoices/edit/save-invoice'; // Cargar companyStore solo en el cliente - no usamos sidebar en esta página let companyStore: any = $state(undefined); @@ -62,6 +62,12 @@ enclosure?: any[]; currencyTypes?: any[]; transportTypes?: any[]; + transporters?: any[]; + vehicles?: any[]; + drivers?: any[]; + trailers?: any[]; + customsSections?: any[]; + codePedimentoRegimens?: any[]; user?: any; companies?: any[]; authenticated?: boolean; @@ -108,398 +114,22 @@ success = false; try { - // Validar campos requeridos para creación - if (data.isCreate && generalFormData) { - const requiredFields = { - operation_type: 'Tipo de Operación', - invoice_type: 'Tipo de Factura', - invoice_number: 'Número de Factura' - }; - - const missingFields: string[] = []; - for (const [field, label] of Object.entries(requiredFields)) { - const value = (generalFormData as any)[field]; - if (value === null || value === undefined || value === '') { - missingFields.push(label); - } - } - - if (missingFields.length > 0) { - throw new Error(`Los siguientes campos son obligatorios: ${missingFields.join(', ')}`); - } - } - - // Construir el payload unificado - const payload: CreateInvoiceData | UpdateInvoiceData = { - // Datos generales desde el formulario general - operation_type: generalFormData?.operation_type !== null && generalFormData?.operation_type !== undefined - ? (generalFormData.operation_type === 1 ? 'exp' : 'imp') as OperationType - : undefined, - invoice_type: generalFormData?.invoice_type || undefined, - invoice_number: generalFormData?.invoice_number || undefined, - invoice_date: generalFormData?.invoice_date || undefined, - emission_date: generalFormData?.emission_date || undefined, - // Observation fields from observationFormData - observation_es: observationFormData?.observation_es || undefined, - observation_en: observationFormData?.observation_en || undefined, - alternate_invoice: observationFormData?.alternate_invoice || undefined, - }; - - // Solo agregar sub-recursos si tienen valores reales - - // Compliance MX - combinar datos del formulario general y observations - const hasComplianceValue = generalFormData?.pedimento || generalFormData?.remesa || generalFormData?.aduana || - generalFormData?.provider_id || generalFormData?.sold_to_id || - generalFormData?.shipped_to_id || generalFormData?.customs_broker_id || - observationFormData?.pedimento || observationFormData?.pedimento_code || - observationFormData?.remesa || observationFormData?.aduana || - observationFormData?.provider_id || observationFormData?.sold_to_id || - observationFormData?.shipped_to_id || observationFormData?.shipped_by_id || - observationFormData?.customs_broker_id || observationFormData?.is_mixed || - observationFormData?.waste_type || observationFormData?.appendix_17 || - observationFormData?.edocument || observationFormData?.electronic_signature || - observationFormData?.sem_id || observationFormData?.enclosure || - observationFormData?.incoterm; - - if (hasComplianceValue) { - payload.compliance_mx = { - // Pedimento fields - pedimento: generalFormData?.pedimento || observationFormData?.pedimento || null, - pedimento_code: observationFormData?.pedimento_code || null, - pedimento_k1: observationFormData?.pedimento_k1 || null, - remesa: generalFormData?.remesa || observationFormData?.remesa || null, - aduana: generalFormData?.aduana || observationFormData?.aduana || null, - port_of_entry: observationFormData?.port_of_entry || null, - destination: observationFormData?.destination || null, - manifest_number: observationFormData?.manifest_number || null, - // Client/Provider fields - provider_header: generalFormData?.provider_header || observationFormData?.provider_header || null, - provider_id: generalFormData?.provider_id || observationFormData?.provider_id || null, - sold_to_header: generalFormData?.sold_to_header || observationFormData?.sold_to_header || null, - sold_to_id: generalFormData?.sold_to_id || observationFormData?.sold_to_id || null, - shipped_to_header: generalFormData?.shipped_to_header || observationFormData?.shipped_to_header || null, - shipped_to_id: generalFormData?.shipped_to_id || observationFormData?.shipped_to_id || null, - shipped_by_header: observationFormData?.shipped_by_header || null, - shipped_by_id: observationFormData?.shipped_by_id || null, - // Customs broker fields - customs_broker_id: generalFormData?.customs_broker_id || observationFormData?.customs_broker_id || null, - customs_broker_us_id: observationFormData?.customs_broker_us_id || null, - broker_invoice_num: observationFormData?.broker_invoice_num || null, - broker_invoice_date: observationFormData?.broker_invoice_date || null, - // Flags & regimes - is_mixed: observationFormData?.is_mixed || null, - waste_type: observationFormData?.waste_type || null, - scrap_type: observationFormData?.scrap_type || null, - appendix_17: observationFormData?.appendix_17 || null, - is_regime_change: observationFormData?.is_regime_change || null, - which_exchange_rate: observationFormData?.which_exchange_rate || null, - value_method: observationFormData?.value_method || null, - act_value: observationFormData?.act_value || null, - is_pedimento_pending: observationFormData?.is_pedimento_pending || null, - // Ownership & balances - is_owner_of_goods: observationFormData?.is_owner_of_goods || null, - generate_balances: observationFormData?.generate_balances || null, - was_reviewed_by_company: observationFormData?.was_reviewed_by_company || null, - // VUCEM / Digital - edocument: observationFormData?.edocument || null, - electronic_signature: observationFormData?.electronic_signature || null, - certificate_number: observationFormData?.certificate_number || null, - niu_number: observationFormData?.niu_number || null, - bill_of_lading_count: observationFormData?.bill_of_lading_count || null, - addendum_vu: observationFormData?.addendum_vu || null, - origin_destination_cove: observationFormData?.origin_destination_cove || null, - vucem_operation_num: observationFormData?.vucem_operation_num || null, - customs_person_line: observationFormData?.customs_person_line || null, - // Additional control - contingency_mode: observationFormData?.contingency_mode || null, - enclosure: observationFormData?.enclosure || null, - guide_type_to_identify: observationFormData?.guide_type_to_identify || null, - location: observationFormData?.location || null, - // DOT & official - dot_code: observationFormData?.dot_code || null, - subdivision: observationFormData?.subdivision || null, - acts_as: observationFormData?.acts_as || null, - movement_type: observationFormData?.movement_type || null, - office_document: observationFormData?.office_document || null, - reason_export: observationFormData?.reason_export || null, - signature_key: observationFormData?.signature_key || null, - // SM specific - sem_id: observationFormData?.sem_id || null, - }; - } - - // Financials - combinar datos del formulario general y items - const hasFinancialsValue = generalFormData?.currency_type || - generalFormData?.iva_factor || - itemsFormData?.currency || itemsFormData?.exchange_rate || - itemsFormData?.value_mn || itemsFormData?.value_me || - itemsFormData?.customs_value_mn || itemsFormData?.freight || - itemsFormData?.insurance; - - if (hasFinancialsValue) { - payload.financials = { - // Currency - currency: itemsFormData?.currency || null, - currency_type: generalFormData?.currency_type || itemsFormData?.currency_type || null, - exchange_rate: itemsFormData?.exchange_rate || null, - exchange_rate_mm: itemsFormData?.exchange_rate_mm || null, - // Merchandise values - value_mn: itemsFormData?.value_mn || null, - value_me: itemsFormData?.value_me || null, - value_mc: itemsFormData?.value_mc || null, - // Customs value - customs_value_mn: itemsFormData?.customs_value_mn || null, - customs_value_me: itemsFormData?.customs_value_me || null, - // Raw materials - raw_material_value_mn: itemsFormData?.raw_material_value_mn || null, - raw_material_value_me: itemsFormData?.raw_material_value_me || null, - // Aggregate value - aggregate_value_mn: itemsFormData?.aggregate_value_mn || null, - aggregate_value_me: itemsFormData?.aggregate_value_me || null, - aggregate_value_mc: itemsFormData?.aggregate_value_mc || null, - // Mexican merchandise value - mexican_value_mn: itemsFormData?.mexican_value_mn || null, - mexican_value_me: itemsFormData?.mexican_value_me || null, - mexican_value_mc: itemsFormData?.mexican_value_mc || null, - // National packaging - national_packaging_mn: itemsFormData?.national_packaging_mn || null, - national_packaging_me: itemsFormData?.national_packaging_me || null, - national_packaging_mc: itemsFormData?.national_packaging_mc || null, - // Costs & increments - freight: itemsFormData?.freight || observationFormData?.freight || null, - insurance: itemsFormData?.insurance || observationFormData?.insurance || null, - insurance_value: itemsFormData?.insurance_value || observationFormData?.insurance_value || null, - packaging: itemsFormData?.packaging || observationFormData?.packaging || null, - other_increments: itemsFormData?.other_increments || observationFormData?.other_increments || null, - total_increments_mn: itemsFormData?.total_increments_mn || observationFormData?.total_increments_mn || null, - total_increments_me: itemsFormData?.total_increments_me || observationFormData?.total_increments_me || null, - // Taxes - iva_mn: itemsFormData?.iva_mn || null, - iva_me: itemsFormData?.iva_me || null, - iva_mc: itemsFormData?.iva_mc || null, - iva_factor: generalFormData?.iva_factor || itemsFormData?.iva_factor || null, - tax_value_me: itemsFormData?.tax_value_me || null, - seal_value_2500: itemsFormData?.seal_value_2500 || null, - // Weights & quantities - total_quantity: itemsFormData?.total_quantity || null, - gross_weight: itemsFormData?.gross_weight || null, - net_weight: itemsFormData?.net_weight || null, - bundle_count: itemsFormData?.bundle_count || null, - weight_factor: itemsFormData?.weight_factor || null, - }; - } - - // Logistics - combinar datos del formulario general con othersFormData - const hasLogisticsFromGeneral = generalFormData?.carrier_id || - generalFormData?.driver_name || generalFormData?.transport_type || generalFormData?.transport_num; - - if (hasLogisticsFromGeneral || (othersFormData && Array.isArray(othersFormData) && othersFormData.length > 0)) { - // Si hay datos en el formulario general, crear/actualizar el primer elemento - if (hasLogisticsFromGeneral) { - const logisticsEntry = { - carrier_id: generalFormData?.carrier_id || null, - transport_type: generalFormData?.transport_type || null, - transport_mode: null, - driver_name: generalFormData?.driver_name || null, - is_rail: null, - rail_id: null, - vehicle_num: generalFormData?.transport_num || null, - license_plate: null, - seal_number: null, - guide_number: null, - entry_exit_date: null, - }; - - // Si también hay datos del formulario de others, combinarlos - if (othersFormData && Array.isArray(othersFormData) && othersFormData.length > 0) { - // Actualizar el primer elemento con datos del general - payload.logistics = [ - { - // Carrier info - carrier_id: generalFormData?.carrier_id || othersFormData[0].carrier_id || null, - transport_id: othersFormData[0].transport_id || null, - transport_us_id: othersFormData[0].transport_us_id || null, - transport_type: generalFormData?.transport_type || othersFormData[0].transport_type || null, - transport_num: othersFormData[0].transport_num || null, - transport_mode: othersFormData[0].transport_mode || null, - driver_name: generalFormData?.driver_name || othersFormData[0].driver_name || null, - is_rail: othersFormData[0].is_rail || null, - rail_id: othersFormData[0].rail_id || null, - // Vehicle & tracking - vehicle_num: generalFormData?.transport_num || othersFormData[0].vehicle_num || null, - license_plate: othersFormData[0].license_plate || null, - license_plate_complete: othersFormData[0].license_plate_complete || null, - trailer_num: othersFormData[0].trailer_num || null, - seal_number: othersFormData[0].seal_number || null, - guide_number: othersFormData[0].guide_number || null, - bill_number: othersFormData[0].bill_number || null, - reference_number: othersFormData[0].reference_number || null, - shipment_number: othersFormData[0].shipment_number || null, - // Incoterms - incoterm: othersFormData[0].incoterm || observationFormData?.incoterm || null, - // Identifiers & complements - identifier_1: othersFormData[0].identifier_1 || null, - complement_1: othersFormData[0].complement_1 || null, - identifier_2: othersFormData[0].identifier_2 || null, - complement_2: othersFormData[0].complement_2 || null, - // Weight & container info - weight_type: othersFormData[0].weight_type || null, - container_types: othersFormData[0].container_types || null, - vehicle_data: othersFormData[0].vehicle_data || null, - // Locations & routes - origin_location: othersFormData[0].origin_location || null, - destination_location: othersFormData[0].destination_location || null, - transport_itinerary: othersFormData[0].transport_itinerary || null, - destination_goods: othersFormData[0].destination_goods || null, - // Logistics dates - entry_exit_date: othersFormData[0].entry_exit_date || null, - delivery_date: othersFormData[0].delivery_date || null, - // Delivery control - delivered_status: othersFormData[0].delivered_status || null, - received_by: othersFormData[0].received_by || null, - // Payment info - payment_date: othersFormData[0].payment_date || null, - payment_receipt_num: othersFormData[0].payment_receipt_num || null, - // CTM process - is_ctm_process: othersFormData[0].is_ctm_process || null, - }, - // Agregar los demás elementos si existen - ...othersFormData.slice(1).map((item: any) => ({ - // Carrier info - carrier_id: item.carrier_id || null, - transport_id: item.transport_id || null, - transport_us_id: item.transport_us_id || null, - transport_type: item.transport_type || null, - transport_num: item.transport_num || null, - transport_mode: item.transport_mode || null, - driver_name: item.driver_name || null, - is_rail: item.is_rail || null, - rail_id: item.rail_id || null, - // Vehicle & tracking - vehicle_num: item.vehicle_num || null, - license_plate: item.license_plate || null, - license_plate_complete: item.license_plate_complete || null, - trailer_num: item.trailer_num || null, - seal_number: item.seal_number || null, - guide_number: item.guide_number || null, - bill_number: item.bill_number || null, - reference_number: item.reference_number || null, - shipment_number: item.shipment_number || null, - // Incoterms - incoterm: item.incoterm || null, - // Identifiers & complements - identifier_1: item.identifier_1 || null, - complement_1: item.complement_1 || null, - identifier_2: item.identifier_2 || null, - complement_2: item.complement_2 || null, - // Weight & container info - weight_type: item.weight_type || null, - container_types: item.container_types || null, - vehicle_data: item.vehicle_data || null, - // Locations & routes - origin_location: item.origin_location || null, - destination_location: item.destination_location || null, - transport_itinerary: item.transport_itinerary || null, - destination_goods: item.destination_goods || null, - // Logistics dates - entry_exit_date: item.entry_exit_date || null, - delivery_date: item.delivery_date || null, - // Delivery control - delivered_status: item.delivered_status || null, - received_by: item.received_by || null, - // Payment info - payment_date: item.payment_date || null, - payment_receipt_num: item.payment_receipt_num || null, - // CTM process - is_ctm_process: item.is_ctm_process || null, - })) - ]; - } else { - // Solo datos del general - payload.logistics = [logisticsEntry]; - } - } else { - // Solo datos del formulario others - payload.logistics = othersFormData.map((item: any) => ({ - // Carrier info - carrier_id: item.carrier_id || null, - transport_id: item.transport_id || null, - transport_us_id: item.transport_us_id || null, - transport_type: item.transport_type || null, - transport_num: item.transport_num || null, - transport_mode: item.transport_mode || null, - driver_name: item.driver_name || null, - is_rail: item.is_rail || null, - rail_id: item.rail_id || null, - // Vehicle & tracking - vehicle_num: item.vehicle_num || null, - license_plate: item.license_plate || null, - license_plate_complete: item.license_plate_complete || null, - trailer_num: item.trailer_num || null, - seal_number: item.seal_number || null, - guide_number: item.guide_number || null, - bill_number: item.bill_number || null, - reference_number: item.reference_number || null, - shipment_number: item.shipment_number || null, - // Incoterms - incoterm: item.incoterm || observationFormData?.incoterm || null, - // Identifiers & complements - identifier_1: item.identifier_1 || null, - complement_1: item.complement_1 || null, - identifier_2: item.identifier_2 || null, - complement_2: item.complement_2 || null, - // Weight & container info - weight_type: item.weight_type || null, - container_types: item.container_types || null, - vehicle_data: item.vehicle_data || null, - // Locations & routes - origin_location: item.origin_location || null, - destination_location: item.destination_location || null, - transport_itinerary: item.transport_itinerary || null, - destination_goods: item.destination_goods || null, - // Logistics dates - entry_exit_date: item.entry_exit_date || null, - delivery_date: item.delivery_date || null, - // Delivery control - delivered_status: item.delivered_status || null, - received_by: item.received_by || null, - // Payment info - payment_date: item.payment_date || null, - payment_receipt_num: item.payment_receipt_num || null, - // CTM process - is_ctm_process: item.is_ctm_process || null, - })); - } - } - - // Eliminar campos undefined para no enviarlos - Object.keys(payload).forEach(key => { - if (payload[key as keyof typeof payload] === undefined) { - delete payload[key as keyof typeof payload]; + const result = await saveInvoice({ + invoiceId, + isCreate: data.isCreate || false, + companyId: companyStore?.activeCompany?.id || 0, + formData: { + generalFormData, + observationFormData, + itemsFormData, + othersFormData } }); - let newInvoiceId = invoiceId; - - if (data.isCreate) { - // Crear nueva factura con todos sus sub-recursos - const response = await invoicesApi.create(companyStore?.activeCompany?.id || 0, payload as CreateInvoiceData); - if (response.error) { - const errorMsg = typeof response.error === 'string' ? response.error : 'Error al crear la factura'; - throw new Error(errorMsg); - } - if (!response.data?.id) throw new Error('No se recibió el ID de la factura creada'); - newInvoiceId = response.data.id; - - // Redirigir a la página de edición - await goto(`/dashboard/invoices/edit/${newInvoiceId}`); - return; - } else { - // Actualizar factura existente con todos sus sub-recursos - const response = await invoicesApi.update(invoiceId!, companyStore?.activeCompany?.id || 0, payload as UpdateInvoiceData); - if (response.error) throw new Error(response.error); + if (!result.success) { + throw new Error(result.error || 'Error al guardar la factura'); } - + success = true; setTimeout(() => { success = false; @@ -601,6 +231,12 @@ providers={data.providers || []} currencyTypes={data.currencyTypes || []} transportTypes={data.transportTypes || []} + transporters={data.transporters || []} + vehicles={data.vehicles || []} + drivers={data.drivers || []} + trailers={data.trailers || []} + customsSections={data.customsSections || []} + codePedimentoRegimens={data.codePedimentoRegimens || []} defaultOperationType={data.filters?.operation_type ?? undefined} defaultInvoiceType={data.filters?.invoice_type ?? undefined} /> @@ -647,7 +283,7 @@
diff --git a/frontend/src/routes/dashboard/invoices/edit/[id]/+page.ts b/frontend/src/routes/dashboard/invoices/edit/[id]/+page.ts index 9a83f0b6..4058c7ad 100644 --- a/frontend/src/routes/dashboard/invoices/edit/[id]/+page.ts +++ b/frontend/src/routes/dashboard/invoices/edit/[id]/+page.ts @@ -60,6 +60,12 @@ export const load: PageLoad = async ({ params, url, parent }) => { providers: data.providers || [], currencyTypes: data.currencyTypes || [], transportTypes: data.transportTypes || [], + transporters: data.transporters || [], + vehicles: data.vehicles || [], + drivers: data.drivers || [], + trailers: data.trailers || [], + customsSections: data.customsSections || [], + codePedimentoRegimens: data.codePedimentoRegimens || [], seals: data.seals || [], incoterms: data.incoterms || [], pedimentos: data.pedimentos || [], @@ -80,6 +86,12 @@ export const load: PageLoad = async ({ params, url, parent }) => { providers: [], currencyTypes: [], transportTypes: [], + transporters: [], + vehicles: [], + drivers: [], + trailers: [], + customsSections: [], + codePedimentoRegimens: [], seals: [], incoterms: [], pedimentos: [], @@ -116,6 +128,12 @@ export const load: PageLoad = async ({ params, url, parent }) => { providers: data.providers || [], currencyTypes: data.currencyTypes || [], transportTypes: data.transportTypes || [], + transporters: data.transporters || [], + vehicles: data.vehicles || [], + drivers: data.drivers || [], + trailers: data.trailers || [], + customsSections: data.customsSections || [], + codePedimentoRegimens: data.codePedimentoRegimens || [], seals: data.seals || [], incoterms: data.incoterms || [], pedimentos: data.pedimentos || [],