Merge pull request 'feature/items' (#37) from feature/items into development
Reviewed-on: ADUANASOFT/anexo76#37
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
25
backend/api/v1/modules/a76/items/__init__.py
Normal file
25
backend/api/v1/modules/a76/items/__init__.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""
|
||||
Items module - Annex 76 Compliance
|
||||
"""
|
||||
|
||||
# Import models in correct order to avoid circular dependencies
|
||||
# LineItem must be imported before models that reference it
|
||||
from .line_items.models import LineItem
|
||||
from .line_financials.models import LineFinancial
|
||||
from .line_quantities.models import LineQuantity
|
||||
from .line_customs.models import LineCustom
|
||||
from .line_descriptions.models import LineDescription
|
||||
from .line_references.models import LineReference
|
||||
from .models import Item, CTMReceipt, SubassemblyEntry
|
||||
|
||||
__all__ = [
|
||||
"Item",
|
||||
"LineItem",
|
||||
"LineFinancial",
|
||||
"LineQuantity",
|
||||
"LineCustom",
|
||||
"LineDescription",
|
||||
"LineReference",
|
||||
"CTMReceipt",
|
||||
"SubassemblyEntry",
|
||||
]
|
||||
16
backend/api/v1/modules/a76/items/line_customs/__init__.py
Normal file
16
backend/api/v1/modules/a76/items/line_customs/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""Line customs module"""
|
||||
from .models import LineCustom
|
||||
from .schemas import (
|
||||
LineCustomBase,
|
||||
LineCustomCreate,
|
||||
LineCustomUpdate,
|
||||
LineCustomResponse,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"LineCustom",
|
||||
"LineCustomBase",
|
||||
"LineCustomCreate",
|
||||
"LineCustomUpdate",
|
||||
"LineCustomResponse",
|
||||
]
|
||||
55
backend/api/v1/modules/a76/items/line_customs/models.py
Normal file
55
backend/api/v1/modules/a76/items/line_customs/models.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from decimal import Decimal
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from sqlalchemy import String, Numeric, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..line_items.models import LineItem
|
||||
|
||||
class LineCustom(Base):
|
||||
"""
|
||||
Customs details for line items
|
||||
Consolidates all line-level data from Q and S tables
|
||||
"""
|
||||
__tablename__ = "line_customs"
|
||||
__table_args__ = {
|
||||
"schema": "a76",
|
||||
}
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
item_line_id: Mapped[int] = mapped_column(ForeignKey("a76.item_lines.id"))
|
||||
|
||||
# Tariff/Customs Classifications
|
||||
fraction: Mapped[Optional[str]] = mapped_column(String(10)) # FRACCION / FRACCIONIMPO / FRACCIONEXPO
|
||||
fraction_type: Mapped[Optional[str]] = mapped_column(String(7)) # TIPOFRACCION / TIPOFRACCIONIMPO / TIPOFRACCIONEXPO
|
||||
american_fraction: Mapped[Optional[str]] = mapped_column(String(16)) # FRACCIONAMERICANA
|
||||
alternate_fraction: Mapped[Optional[str]] = mapped_column(String(10)) # FRACCIONALTERNA
|
||||
reference_fraction: Mapped[Optional[str]] = mapped_column(String(10)) # FRACCIONREFERENCIA
|
||||
octave_fraction: Mapped[Optional[str]] = mapped_column(String(10)) # FRACCIONROCTAVA
|
||||
tlcan_fraction: Mapped[Optional[str]] = mapped_column(String(13)) # FRACCIONTLCAN
|
||||
extra_american_fraction: Mapped[Optional[str]] = mapped_column(String(16)) # FRACAMESELEXTRA
|
||||
garment_fraction: Mapped[Optional[str]] = mapped_column(String(19)) # FRACCIONDELAPRENDA/FRACCIONDELAPARTIDA
|
||||
|
||||
# Ad Valorem
|
||||
advalorem: Mapped[Optional[str]] = mapped_column(String(10)) # ADVIMPO / ADVEXPO
|
||||
advalorem_numeric: Mapped[Optional[Decimal]] = mapped_column(Numeric(7, 2)) # ADVIMPONUM / ADVEXPONUM
|
||||
advalorem_american: Mapped[Optional[Decimal]] = mapped_column(Numeric(5, 2)) # ADVAME
|
||||
advalorem_tlcan: Mapped[Optional[Decimal]] = mapped_column(Numeric(5, 2)) # ADVTLCAN
|
||||
|
||||
# Rates
|
||||
rate: Mapped[Optional[str]] = mapped_column(String(10)) # TASAIM / TASAEX
|
||||
depreciation_rate: Mapped[Optional[Decimal]] = mapped_column(Numeric(5, 2)) # TASADEPRECIA
|
||||
|
||||
# Origin/Destination
|
||||
origin_country: Mapped[Optional[str]] = mapped_column(String(3)) # PAISORIGEN
|
||||
destination_country: Mapped[Optional[str]] = mapped_column(String(3)) # PAISDESTINO
|
||||
optional_country: Mapped[Optional[str]] = mapped_column(String(3)) # PAISOPCIONAL
|
||||
origin_procedure: Mapped[Optional[str]] = mapped_column(String(3)) # PROCEDENCIA
|
||||
scrap_procedure: Mapped[Optional[str]] = mapped_column(String(3)) # PROCSCRAP
|
||||
|
||||
# Sector
|
||||
sector: Mapped[Optional[str]] = mapped_column(String(8)) # SECTOR
|
||||
|
||||
# Relationship (one-to-one)
|
||||
line: Mapped["LineItem"] = relationship(back_populates="customs")
|
||||
58
backend/api/v1/modules/a76/items/line_customs/schemas.py
Normal file
58
backend/api/v1/modules/a76/items/line_customs/schemas.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from decimal import Decimal
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
|
||||
# ============================================================================
|
||||
# LINE CUSTOMS SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
class LineCustomBase(BaseModel):
|
||||
"""Base schema for line customs"""
|
||||
# Tariff/Customs Classifications
|
||||
fraction: Optional[str] = Field(None, max_length=10, description="Fraction (FRACCION/FRACCIONIMPO/FRACCIONEXPO)")
|
||||
fraction_type: Optional[str] = Field(None, max_length=7, description="Fraction type (TIPOFRACCION/TIPOFRACCIONIMPO/TIPOFRACCIONEXPO)")
|
||||
american_fraction: Optional[str] = Field(None, max_length=16, description="American fraction (FRACCIONAMERICANA)")
|
||||
alternate_fraction: Optional[str] = Field(None, max_length=10, description="Alternate fraction (FRACCIONALTERNA)")
|
||||
reference_fraction: Optional[str] = Field(None, max_length=10, description="Reference fraction (FRACCIONREFERENCIA)")
|
||||
octave_fraction: Optional[str] = Field(None, max_length=10, description="Octave fraction (FRACCIONROCTAVA)")
|
||||
tlcan_fraction: Optional[str] = Field(None, max_length=13, description="TLCAN fraction (FRACCIONTLCAN)")
|
||||
extra_american_fraction: Optional[str] = Field(None, max_length=16, description="Extra American fraction (FRACAMESELEXTRA)")
|
||||
garment_fraction: Optional[str] = Field(None, max_length=19, description="Garment fraction (FRACCIONDELAPRENDA/FRACCIONDELAPARTIDA)")
|
||||
|
||||
# Ad Valorem
|
||||
advalorem: Optional[str] = Field(None, max_length=10, description="Ad valorem (ADVIMPO/ADVEXPO)")
|
||||
advalorem_numeric: Optional[Decimal] = Field(None, description="Ad valorem numeric (ADVIMPONUM/ADVEXPONUM)")
|
||||
advalorem_american: Optional[Decimal] = Field(None, description="Ad valorem American (ADVAME)")
|
||||
advalorem_tlcan: Optional[Decimal] = Field(None, description="Ad valorem TLCAN (ADVTLCAN)")
|
||||
|
||||
# Rates
|
||||
rate: Optional[str] = Field(None, max_length=10, description="Rate (TASAIM/TASAEX)")
|
||||
depreciation_rate: Optional[Decimal] = Field(None, description="Depreciation rate (TASADEPRECIA)")
|
||||
|
||||
# Origin/Destination
|
||||
origin_country: Optional[str] = Field(None, max_length=3, description="Origin country (PAISORIGEN)")
|
||||
destination_country: Optional[str] = Field(None, max_length=3, description="Destination country (PAISDESTINO)")
|
||||
optional_country: Optional[str] = Field(None, max_length=3, description="Optional country (PAISOPCIONAL)")
|
||||
origin_procedure: Optional[str] = Field(None, max_length=3, description="Origin procedure (PROCEDENCIA)")
|
||||
scrap_procedure: Optional[str] = Field(None, max_length=3, description="Scrap procedure (PROCSCRAP)")
|
||||
|
||||
# Sector
|
||||
sector: Optional[str] = Field(None, max_length=8, description="Sector (SECTOR)")
|
||||
|
||||
|
||||
class LineCustomCreate(LineCustomBase):
|
||||
"""Schema for creating line customs"""
|
||||
pass
|
||||
|
||||
|
||||
class LineCustomUpdate(LineCustomBase):
|
||||
"""Schema for updating line customs"""
|
||||
pass
|
||||
|
||||
|
||||
class LineCustomResponse(LineCustomBase):
|
||||
"""Schema for line customs response"""
|
||||
id: int
|
||||
item_line_id: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -0,0 +1,16 @@
|
||||
"""Line descriptions module"""
|
||||
from .models import LineDescription
|
||||
from .schemas import (
|
||||
LineDescriptionBase,
|
||||
LineDescriptionCreate,
|
||||
LineDescriptionUpdate,
|
||||
LineDescriptionResponse,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"LineDescription",
|
||||
"LineDescriptionBase",
|
||||
"LineDescriptionCreate",
|
||||
"LineDescriptionUpdate",
|
||||
"LineDescriptionResponse",
|
||||
]
|
||||
43
backend/api/v1/modules/a76/items/line_descriptions/models.py
Normal file
43
backend/api/v1/modules/a76/items/line_descriptions/models.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from sqlalchemy import Boolean, String, Text, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..line_items.models import LineItem
|
||||
|
||||
class LineDescription(Base):
|
||||
"""
|
||||
Description details for line items
|
||||
Consolidates all line-level data from Q and S tables
|
||||
"""
|
||||
__tablename__ = "line_descriptions"
|
||||
__table_args__ = {
|
||||
"schema": "a76",
|
||||
}
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
item_line_id: Mapped[int] = mapped_column(ForeignKey("a76.item_lines.id"))
|
||||
|
||||
# Descriptions
|
||||
description_spanish: Mapped[Optional[str]] = mapped_column(String(4999)) # DESCRIPCIONE
|
||||
description_english: Mapped[Optional[str]] = mapped_column(String(4999)) # DESCRIPCIONI
|
||||
extra_description: Mapped[Optional[str]] = mapped_column(Text) # DESCRIPCIONEEXTRA
|
||||
part_description: Mapped[Optional[str]] = mapped_column(String(500)) # DESCRIPCIONPARTE
|
||||
class_description: Mapped[Optional[str]] = mapped_column(String(500)) # DESCRIPCIONCLASE
|
||||
|
||||
# Product attributes
|
||||
brand: Mapped[Optional[str]] = mapped_column(String(50)) # MARCA
|
||||
model: Mapped[Optional[str]] = mapped_column(String(50)) # MODELO
|
||||
has_serial: Mapped[Optional[bool]] = mapped_column(Boolean) # LLEVASERIE
|
||||
|
||||
# Additional information
|
||||
additional_info_spanish: Mapped[Optional[str]] = mapped_column(String(1000)) # INFOADICIONESP
|
||||
additional_info_english: Mapped[Optional[str]] = mapped_column(String(1000)) # INFOADICIONING
|
||||
|
||||
# Lot and entry tracking
|
||||
lot: Mapped[Optional[str]] = mapped_column(String(254)) # LOTE
|
||||
entry_number: Mapped[Optional[str]] = mapped_column(String(50)) # NUMENTRADA/NUMERODEENTRADA
|
||||
|
||||
# Relationship (one-to-one)
|
||||
line: Mapped["LineItem"] = relationship(back_populates="description")
|
||||
@@ -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)
|
||||
16
backend/api/v1/modules/a76/items/line_financials/__init__.py
Normal file
16
backend/api/v1/modules/a76/items/line_financials/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""Line financials module"""
|
||||
from .models import LineFinancial
|
||||
from .schemas import (
|
||||
LineFinancialBase,
|
||||
LineFinancialCreate,
|
||||
LineFinancialUpdate,
|
||||
LineFinancialResponse,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"LineFinancial",
|
||||
"LineFinancialBase",
|
||||
"LineFinancialCreate",
|
||||
"LineFinancialUpdate",
|
||||
"LineFinancialResponse",
|
||||
]
|
||||
101
backend/api/v1/modules/a76/items/line_financials/models.py
Normal file
101
backend/api/v1/modules/a76/items/line_financials/models.py
Normal file
@@ -0,0 +1,101 @@
|
||||
from decimal import Decimal
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from sqlalchemy import Numeric, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..line_items.models import LineItem
|
||||
|
||||
class LineFinancial(Base):
|
||||
"""
|
||||
Financial details for line items
|
||||
Consolidates all line-level data from Q and S tables
|
||||
"""
|
||||
__tablename__ = "line_financials"
|
||||
__table_args__ = {
|
||||
"schema": "a76",
|
||||
}
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
item_line_id: Mapped[int] = mapped_column(ForeignKey("a76.item_lines.id"))
|
||||
|
||||
# Costs - Capture
|
||||
unit_cost_capture: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTOUNITARIOCAPTURA
|
||||
|
||||
# Costs - USD
|
||||
unit_cost_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTOUNITARIODLLS/COSTOUNITARIOME
|
||||
unit_cost_commercial_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTOUCOMDLLS
|
||||
unit_cost_current_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTOACTUALDLLS
|
||||
unit_cost_depreciated_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTODEPRECME
|
||||
unit_cost_subitem_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTOUNITARIOSUBPDLLS
|
||||
unit_cost_auxiliary_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTOUAUXILIARME
|
||||
sales_cost_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTOVENTAME
|
||||
commercial_unit_cost: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTOUNICOMERCIAL
|
||||
|
||||
# Costs - MXN
|
||||
unit_cost_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTOUNITARIOPESOS/COSTOUNITARIOMN
|
||||
unit_cost_commercial_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTOUCOMPESOS
|
||||
unit_cost_current_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTOACTUALMN
|
||||
unit_cost_depreciated_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTODEPRECMN
|
||||
unit_cost_subitem_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTOUNITARIOSUBPPESOS
|
||||
sales_cost_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # COSTOVENTAMN
|
||||
|
||||
# Costs - MC (Custom Currency)
|
||||
unit_cost_mc: Mapped[Optional[Decimal]] = mapped_column(Numeric(29, 8)) # COSTOUNITARIOMC
|
||||
|
||||
# Values - MXN
|
||||
value_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORMN/VALORIMPOMN/VALOREXPOMN
|
||||
value_commercial_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALOREXPOCOMMN
|
||||
value_updated_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORACTUALIZADOMN
|
||||
value_subitem_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORSUBPMN
|
||||
sub_import_value_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # SUBVALORIMPOMN
|
||||
value_returned_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORRETORNADOMN
|
||||
value_depreciated_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORDEPRECIADOMN
|
||||
customs_value_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORADUANASMN
|
||||
value_total_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORTOTALMN
|
||||
value_temp_material_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORMPTEMPMN
|
||||
value_def_material_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORMPDEFMN
|
||||
value_added_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORAGREMN
|
||||
value_national_packing_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALEMPAQUENACMN
|
||||
vat_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # IVAIMPOMN/IVAEXPOMN/VALORIVAMN
|
||||
vat_used_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORIVAMNUSADO
|
||||
advalorem_line_mxn: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # ADVALORMNLINEAPED
|
||||
|
||||
# Values - USD
|
||||
value_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(29, 8)) # VALORME/VALORIMPOME/VALOREXPOME
|
||||
value_commercial_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALOREXPOCOMME
|
||||
value_updated_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORACTUALIZADOME
|
||||
value_subitem_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORSUBPME
|
||||
sub_import_value_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # SUBVALORIMPOME
|
||||
value_returned_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORRETORNADOME
|
||||
value_depreciated_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORDEPRECIADOME
|
||||
customs_value_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORADUANASME
|
||||
value_auxiliary_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORIMPOAUXILIARME / VALOREXPORAUXILIARME
|
||||
value_total_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORTOTALME
|
||||
value_temp_material_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORMPTEMPME
|
||||
value_def_material_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORMPDEFME
|
||||
value_added_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORAGREME
|
||||
value_national_packing_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALEMPAQUENACME
|
||||
value_us_packing_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALEMPAQUEUSME
|
||||
vat_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # IVAIMPOME/IVAEXPOME/VALORIVAME
|
||||
vat_used_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORIVAMEUSADO
|
||||
value_non_originating_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORNOORIGINARIOME
|
||||
value_originating_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORORIGINARIOME
|
||||
igi_amount_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # MONTOIGIME
|
||||
exempt_amount_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # MONTOEXCENTOME
|
||||
total_commercial_value: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORTOTALCOMERCIAL
|
||||
advalorem_line_usd: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # ADVALORMELINEAPED
|
||||
|
||||
# Values - MC
|
||||
value_mc: Mapped[Optional[Decimal]] = mapped_column(Numeric(29, 8)) # VALORIMPOMC/VALOREXPOMC
|
||||
sub_import_value_mc: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # SUBVALORIMPOMC
|
||||
vat_mc: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # IVAIMPOMC/IVAEXPOMC
|
||||
value_added_mc: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORAGREMC
|
||||
value_national_packing_mc: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALEMPAQUENACMC
|
||||
value_total_mc: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORTOTALMC
|
||||
value_temp_material_mc: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORMPTEMPMC
|
||||
value_def_material_mc: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) # VALORMPDEFMC
|
||||
|
||||
# Relationship (one-to-one)
|
||||
line: Mapped["LineItem"] = relationship(back_populates="financial")
|
||||
104
backend/api/v1/modules/a76/items/line_financials/schemas.py
Normal file
104
backend/api/v1/modules/a76/items/line_financials/schemas.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from decimal import Decimal
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
|
||||
# ============================================================================
|
||||
# LINE FINANCIAL SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
class LineFinancialBase(BaseModel):
|
||||
"""Base schema for line financials"""
|
||||
# Costs - Capture
|
||||
unit_cost_capture: Optional[Decimal] = Field(None, description="Unit cost in capture currency (COSTOUNITARIOCAPTURA)")
|
||||
|
||||
# Costs - USD
|
||||
unit_cost_usd: Optional[Decimal] = Field(None, description="Unit cost in USD (COSTOUNITARIODLLS/COSTOUNITARIOME)")
|
||||
unit_cost_commercial_usd: Optional[Decimal] = Field(None, description="Commercial unit cost in USD (COSTOUCOMDLLS)")
|
||||
unit_cost_current_usd: Optional[Decimal] = Field(None, description="Current unit cost in USD (COSTOACTUALDLLS)")
|
||||
unit_cost_depreciated_usd: Optional[Decimal] = Field(None, description="Depreciated unit cost in USD (COSTODEPRECME)")
|
||||
unit_cost_subitem_usd: Optional[Decimal] = Field(None, description="Subitem unit cost in USD (COSTOUNITARIOSUBPDLLS)")
|
||||
unit_cost_auxiliary_usd: Optional[Decimal] = Field(None, description="Auxiliary unit cost in USD (COSTOUAUXILIARME)")
|
||||
sales_cost_usd: Optional[Decimal] = Field(None, description="Sales cost in USD (COSTOVENTAME)")
|
||||
commercial_unit_cost: Optional[Decimal] = Field(None, description="Commercial unit cost (COSTOUNICOMERCIAL)")
|
||||
|
||||
# Costs - MXN
|
||||
unit_cost_mxn: Optional[Decimal] = Field(None, description="Unit cost in MXN (COSTOUNITARIOPESOS/COSTOUNITARIOMN)")
|
||||
unit_cost_commercial_mxn: Optional[Decimal] = Field(None, description="Commercial unit cost in MXN (COSTOUCOMPESOS)")
|
||||
unit_cost_current_mxn: Optional[Decimal] = Field(None, description="Current unit cost in MXN (COSTOACTUALMN)")
|
||||
unit_cost_depreciated_mxn: Optional[Decimal] = Field(None, description="Depreciated unit cost in MXN (COSTODEPRECMN)")
|
||||
unit_cost_subitem_mxn: Optional[Decimal] = Field(None, description="Subitem unit cost in MXN (COSTOUNITARIOSUBPPESOS)")
|
||||
sales_cost_mxn: Optional[Decimal] = Field(None, description="Sales cost in MXN (COSTOVENTAMN)")
|
||||
|
||||
# Costs - MC (Custom Currency)
|
||||
unit_cost_mc: Optional[Decimal] = Field(None, description="Unit cost in MC (COSTOUNITARIOMC)")
|
||||
|
||||
# Values - MXN
|
||||
value_mxn: Optional[Decimal] = Field(None, description="Value in MXN (VALORMN/VALORIMPOMN/VALOREXPOMN)")
|
||||
value_commercial_mxn: Optional[Decimal] = Field(None, description="Commercial value in MXN (VALOREXPOCOMMN)")
|
||||
value_updated_mxn: Optional[Decimal] = Field(None, description="Updated value in MXN (VALORACTUALIZADOMN)")
|
||||
value_subitem_mxn: Optional[Decimal] = Field(None, description="Subitem value in MXN (VALORSUBPMN)")
|
||||
sub_import_value_mxn: Optional[Decimal] = Field(None, description="Sub-import value in MXN (SUBVALORIMPOMN)")
|
||||
value_returned_mxn: Optional[Decimal] = Field(None, description="Returned value in MXN (VALORRETORNADOMN)")
|
||||
value_depreciated_mxn: Optional[Decimal] = Field(None, description="Depreciated value in MXN (VALORDEPRECIADOMN)")
|
||||
customs_value_mxn: Optional[Decimal] = Field(None, description="Customs value in MXN (VALORADUANASMN)")
|
||||
value_total_mxn: Optional[Decimal] = Field(None, description="Total value in MXN (VALORTOTALMN)")
|
||||
value_temp_material_mxn: Optional[Decimal] = Field(None, description="Temporary material value in MXN (VALORMPTEMPMN)")
|
||||
value_def_material_mxn: Optional[Decimal] = Field(None, description="Definitive material value in MXN (VALORMPDEFMN)")
|
||||
value_added_mxn: Optional[Decimal] = Field(None, description="Added value in MXN (VALORAGREMN)")
|
||||
value_national_packing_mxn: Optional[Decimal] = Field(None, description="National packing value in MXN (VALEMPAQUENACMN)")
|
||||
vat_mxn: Optional[Decimal] = Field(None, description="VAT in MXN (IVAIMPOMN/IVAEXPOMN/VALORIVAMN)")
|
||||
vat_used_mxn: Optional[Decimal] = Field(None, description="VAT used in MXN (VALORIVAMNUSADO)")
|
||||
advalorem_line_mxn: Optional[Decimal] = Field(None, description="Ad valorem line in MXN (ADVALORMNLINEAPED)")
|
||||
|
||||
# Values - USD
|
||||
value_usd: Optional[Decimal] = Field(None, description="Value in USD (VALORME/VALORIMPOME/VALOREXPOME)")
|
||||
value_commercial_usd: Optional[Decimal] = Field(None, description="Commercial value in USD (VALOREXPOCOMME)")
|
||||
value_updated_usd: Optional[Decimal] = Field(None, description="Updated value in USD (VALORACTUALIZADOME)")
|
||||
value_subitem_usd: Optional[Decimal] = Field(None, description="Subitem value in USD (VALORSUBPME)")
|
||||
sub_import_value_usd: Optional[Decimal] = Field(None, description="Sub-import value in USD (SUBVALORIMPOME)")
|
||||
value_returned_usd: Optional[Decimal] = Field(None, description="Returned value in USD (VALORRETORNADOME)")
|
||||
value_depreciated_usd: Optional[Decimal] = Field(None, description="Depreciated value in USD (VALORDEPRECIADOME)")
|
||||
customs_value_usd: Optional[Decimal] = Field(None, description="Customs value in USD (VALORADUANASME)")
|
||||
value_auxiliary_usd: Optional[Decimal] = Field(None, description="Auxiliary value in USD (VALORIMPOAUXILIARME/VALOREXPORAUXILIARME)")
|
||||
value_total_usd: Optional[Decimal] = Field(None, description="Total value in USD (VALORTOTALME)")
|
||||
value_temp_material_usd: Optional[Decimal] = Field(None, description="Temporary material value in USD (VALORMPTEMPME)")
|
||||
value_def_material_usd: Optional[Decimal] = Field(None, description="Definitive material value in USD (VALORMPDEFME)")
|
||||
value_added_usd: Optional[Decimal] = Field(None, description="Added value in USD (VALORAGREME)")
|
||||
value_national_packing_usd: Optional[Decimal] = Field(None, description="National packing value in USD (VALEMPAQUENACME)")
|
||||
value_us_packing_usd: Optional[Decimal] = Field(None, description="US packing value in USD (VALEMPAQUEUSME)")
|
||||
vat_usd: Optional[Decimal] = Field(None, description="VAT in USD (IVAIMPOME/IVAEXPOME/VALORIVAME)")
|
||||
vat_used_usd: Optional[Decimal] = Field(None, description="VAT used in USD (VALORIVAMEUSADO)")
|
||||
value_non_originating_usd: Optional[Decimal] = Field(None, description="Non-originating value in USD (VALORNOORIGINARIOME)")
|
||||
value_originating_usd: Optional[Decimal] = Field(None, description="Originating value in USD (VALORORIGINARIOME)")
|
||||
igi_amount_usd: Optional[Decimal] = Field(None, description="IGI amount in USD (MONTOIGIME)")
|
||||
exempt_amount_usd: Optional[Decimal] = Field(None, description="Exempt amount in USD (MONTOEXCENTOME)")
|
||||
total_commercial_value: Optional[Decimal] = Field(None, description="Total commercial value (VALORTOTALCOMERCIAL)")
|
||||
advalorem_line_usd: Optional[Decimal] = Field(None, description="Ad valorem line in USD (ADVALORMELINEAPED)")
|
||||
|
||||
# Values - MC
|
||||
value_mc: Optional[Decimal] = Field(None, description="Value in MC (VALORIMPOMC/VALOREXPOMC)")
|
||||
sub_import_value_mc: Optional[Decimal] = Field(None, description="Sub-import value in MC (SUBVALORIMPOMC)")
|
||||
vat_mc: Optional[Decimal] = Field(None, description="VAT in MC (IVAIMPOMC/IVAEXPOMC)")
|
||||
value_added_mc: Optional[Decimal] = Field(None, description="Added value in MC (VALORAGREMC)")
|
||||
value_national_packing_mc: Optional[Decimal] = Field(None, description="National packing value in MC (VALEMPAQUENACMC)")
|
||||
value_total_mc: Optional[Decimal] = Field(None, description="Total value in MC (VALORTOTALMC)")
|
||||
value_temp_material_mc: Optional[Decimal] = Field(None, description="Temporary material value in MC (VALORMPTEMPMC)")
|
||||
value_def_material_mc: Optional[Decimal] = Field(None, description="Definitive material value in MC (VALORMPDEFMC)")
|
||||
|
||||
|
||||
class LineFinancialCreate(LineFinancialBase):
|
||||
"""Schema for creating line financial"""
|
||||
pass
|
||||
|
||||
|
||||
class LineFinancialUpdate(LineFinancialBase):
|
||||
"""Schema for updating line financial"""
|
||||
pass
|
||||
|
||||
|
||||
class LineFinancialResponse(LineFinancialBase):
|
||||
"""Schema for line financial response"""
|
||||
id: int
|
||||
item_line_id: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
16
backend/api/v1/modules/a76/items/line_items/__init__.py
Normal file
16
backend/api/v1/modules/a76/items/line_items/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""Line items module"""
|
||||
from .models import LineItem
|
||||
from .schemas import (
|
||||
LineItemBase,
|
||||
LineItemCreate,
|
||||
LineItemUpdate,
|
||||
LineItemResponse,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"LineItem",
|
||||
"LineItemBase",
|
||||
"LineItemCreate",
|
||||
"LineItemUpdate",
|
||||
"LineItemResponse",
|
||||
]
|
||||
179
backend/api/v1/modules/a76/items/line_items/models.py
Normal file
179
backend/api/v1/modules/a76/items/line_items/models.py
Normal file
@@ -0,0 +1,179 @@
|
||||
from decimal import Decimal
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from sqlalchemy import Boolean, String, Integer, Numeric, SmallInteger, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..models import Item
|
||||
from ..line_financials.models import LineFinancial
|
||||
from ..line_quantities.models import LineQuantity
|
||||
from ..line_customs.models import LineCustom
|
||||
from ..line_descriptions.models import LineDescription
|
||||
from ..line_references.models import LineReference
|
||||
|
||||
|
||||
class LineItem(Base, TenantScopedMixin, TimestampMixin):
|
||||
"""
|
||||
Unified line items for all items
|
||||
Consolidates all line-level data from Q and S tables
|
||||
"""
|
||||
__tablename__ = "item_lines"
|
||||
__table_args__ = {
|
||||
"schema": "a76",
|
||||
}
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
item_id: Mapped[int] = mapped_column(ForeignKey("a76.items.id"))
|
||||
line_number: Mapped[int] = mapped_column(
|
||||
Integer) # LINEAIMPO/LINEAEXPO/LINEA
|
||||
|
||||
# Part identification
|
||||
part_number: Mapped[Optional[str]] = mapped_column(
|
||||
ForeignKey("a76.parts.id")) # NUMPARTE
|
||||
component_part_number: Mapped[Optional[str]] = mapped_column(
|
||||
ForeignKey("a76.parts.id")) # NUMPARTECOM
|
||||
class_code: Mapped[Optional[str]] = mapped_column(
|
||||
ForeignKey("a76.classes.id")) # CLASE
|
||||
|
||||
# Unit of measure
|
||||
unit_of_measure: Mapped[Optional[str]] = mapped_column(
|
||||
ForeignKey("a76.units_of_measure_general.id")) # UNIDADMEDIDA/UNIMED
|
||||
alternate_unit: Mapped[Optional[str]] = mapped_column(
|
||||
ForeignKey("a76.units_of_measure_general.id")) # UNIMEDALTERNA
|
||||
uma_key: Mapped[Optional[str]] = mapped_column(String(2)) # CLAVEUMA
|
||||
auxiliary_unit: Mapped[Optional[str]] = mapped_column(
|
||||
String(5)) # UNIMEDAUXILIAR
|
||||
|
||||
# Permits and certificates
|
||||
permit_number: Mapped[Optional[str]] = mapped_column(
|
||||
String(20)) # NUMPERMISO
|
||||
page_line: Mapped[Optional[str]] = mapped_column(String(10)) # PAGRENGLON
|
||||
has_certificate: Mapped[Optional[bool]] = mapped_column(
|
||||
Boolean) # TIENECO/CERTORIGEN
|
||||
certificate_number: Mapped[Optional[str]] = mapped_column(
|
||||
String(10)) # NOCERTIFICADO
|
||||
octave_permit: Mapped[Optional[str]] = mapped_column(
|
||||
String(20)) # PERMISOROCTAVA
|
||||
permits_ped: Mapped[Optional[str]] = mapped_column(
|
||||
String(500)) # PERMISOSPED
|
||||
|
||||
# FDA
|
||||
has_fda_code: Mapped[Optional[bool]] = mapped_column(
|
||||
Boolean) # LLEVACODFDA
|
||||
fda_key: Mapped[Optional[str]] = mapped_column(String(10)) # CLAVEFDA
|
||||
|
||||
# Subitem flags
|
||||
is_subitem: Mapped[Optional[bool]] = mapped_column(Boolean) # ESSUBPARTIDA
|
||||
contains_subitems: Mapped[Optional[bool]
|
||||
] = mapped_column(Boolean) # CONTIENESUBP
|
||||
includes_subitems: Mapped[Optional[bool]] = mapped_column(
|
||||
Boolean) # INCUYESUBPARTIDAS
|
||||
subitem_number: Mapped[Optional[bool]
|
||||
] = mapped_column(Boolean) # SUBPARTIDA
|
||||
|
||||
# Special flags
|
||||
is_military_mcia: Mapped[Optional[bool]
|
||||
] = mapped_column(Boolean) # ESMCIAMILITAR
|
||||
|
||||
# IV32 (Tax identification)
|
||||
iv32_type_key: Mapped[Optional[str]] = mapped_column(
|
||||
String(5)) # CLAVETIPOIV32
|
||||
iv32_number: Mapped[Optional[str]] = mapped_column(
|
||||
String(35)) # NUMEROIV32
|
||||
|
||||
# IN CASE OF EXPO
|
||||
scrap_invoice: Mapped[Optional[str]] = mapped_column(
|
||||
String(15)) # FACTURASCRAP
|
||||
consecutive_destination: Mapped[Optional[int]
|
||||
] = mapped_column(Integer) # CONSECUTIVODES
|
||||
ctm_section: Mapped[Optional[str]] = mapped_column(
|
||||
String(3)) # APARTADOCTM
|
||||
|
||||
# Tax payment
|
||||
tax_payment: Mapped[Optional[bool]] = mapped_column(
|
||||
Boolean) # PAGOIMPUESTO
|
||||
payment_method: Mapped[Optional[str]] = mapped_column(
|
||||
String(9)) # FORMAPAGO/FORMAPAGOTIGI
|
||||
igi_amount: Mapped[Optional[Decimal]] = mapped_column(
|
||||
Numeric(23, 8)) # MONTOIGI
|
||||
igi_payment_method: Mapped[Optional[str]] = mapped_column(
|
||||
String(9)) # FORMAPAGOTIGI
|
||||
|
||||
# FCC
|
||||
fcc_key: Mapped[Optional[str]] = mapped_column(String(30)) # CLAVEFCC
|
||||
|
||||
# Valuation method
|
||||
valuation_method: Mapped[Optional[str]
|
||||
] = mapped_column(String(2)) # METVALOR
|
||||
valuation_determined_value: Mapped[Optional[Decimal]] = mapped_column(
|
||||
Numeric(29, 8)) # METVALORVALORDETERMINADO/METVALORACIONVALORDETERMINADO
|
||||
valuation_reason: Mapped[Optional[str]] = mapped_column(
|
||||
String(500)) # METVALORMOTIVODEUSO/METVALORACIONMOTIVODEUSO
|
||||
|
||||
# Container rules
|
||||
container_rule: Mapped[Optional[str]] = mapped_column(
|
||||
String(50)) # CONTENEDORREGLA
|
||||
container_parts_ii: Mapped[Optional[str]] = mapped_column(
|
||||
String(50)) # CONTENEDORPARTESII
|
||||
|
||||
# APHIS
|
||||
consecutive_aphis: Mapped[Optional[int]] = mapped_column(
|
||||
Integer) # CONSECUTIVOAPHIS
|
||||
|
||||
# BOM/Commercial
|
||||
bom_version: Mapped[Optional[int]] = mapped_column(Integer) # VERSIONBOM
|
||||
bill_version: Mapped[Optional[int]] = mapped_column(Integer) # VERSIONBILL
|
||||
|
||||
# TLCAN value
|
||||
tlcan_value: Mapped[Optional[Decimal]] = mapped_column(
|
||||
Numeric(23, 8)) # VALORTLCAN
|
||||
|
||||
# Identifier
|
||||
identifier: Mapped[Optional[str]] = mapped_column(
|
||||
String(2)) # IDENTIFICADOR
|
||||
|
||||
# Validation fields
|
||||
validation_zero: Mapped[Optional[int]] = mapped_column(
|
||||
Integer) # VALIDACIONZERO
|
||||
validation_one: Mapped[Optional[int]] = mapped_column(
|
||||
Integer) # VALIDACIONUNO
|
||||
|
||||
# Material type
|
||||
material_type: Mapped[Optional[str]] = mapped_column(
|
||||
String(50)) # TIPOMAT/TIPODENUMPARTE
|
||||
|
||||
# Order concept
|
||||
order_type: Mapped[Optional[str]] = mapped_column(
|
||||
String(50)) # TIPODEORDEN
|
||||
line_concept: Mapped[Optional[str]] = mapped_column(
|
||||
String(50)) # CONCEPTODELAPARTIDA
|
||||
|
||||
# Review dispatch
|
||||
review_dispatch: Mapped[Optional[str]] = mapped_column(
|
||||
String(10)) # REVISARDESP
|
||||
|
||||
# Take component from PT
|
||||
take_component_pt: Mapped[Optional[int]
|
||||
] = mapped_column(Integer) # TOMARCOMOPT
|
||||
|
||||
# Pallet
|
||||
pallet2: Mapped[Optional[int]] = mapped_column(SmallInteger) # PALLET2
|
||||
|
||||
# Wildcard field
|
||||
wildcard_field: Mapped[Optional[str]] = mapped_column(
|
||||
String(100)) # CAMPOCOMODIN
|
||||
|
||||
# Relationships
|
||||
item: Mapped["Item"] = relationship(back_populates="lines")
|
||||
financial: Mapped[Optional["LineFinancial"]] = relationship(
|
||||
back_populates="line", cascade="all, delete-orphan", uselist=False)
|
||||
quantity: Mapped[Optional["LineQuantity"]] = relationship(
|
||||
back_populates="line", cascade="all, delete-orphan", uselist=False)
|
||||
customs: Mapped[Optional["LineCustom"]] = relationship(
|
||||
back_populates="line", cascade="all, delete-orphan", uselist=False)
|
||||
description: Mapped[Optional["LineDescription"]] = relationship(
|
||||
back_populates="line", cascade="all, delete-orphan", uselist=False)
|
||||
reference: Mapped[Optional["LineReference"]] = relationship(
|
||||
back_populates="line", cascade="all, delete-orphan", uselist=False)
|
||||
165
backend/api/v1/modules/a76/items/line_items/schemas.py
Normal file
165
backend/api/v1/modules/a76/items/line_items/schemas.py
Normal file
@@ -0,0 +1,165 @@
|
||||
from decimal import Decimal
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
|
||||
# Import nested schemas
|
||||
from ..line_customs.schemas import (
|
||||
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)
|
||||
16
backend/api/v1/modules/a76/items/line_quantities/__init__.py
Normal file
16
backend/api/v1/modules/a76/items/line_quantities/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""Line quantities module"""
|
||||
from .models import LineQuantity
|
||||
from .schemas import (
|
||||
LineQuantityBase,
|
||||
LineQuantityCreate,
|
||||
LineQuantityUpdate,
|
||||
LineQuantityResponse,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"LineQuantity",
|
||||
"LineQuantityBase",
|
||||
"LineQuantityCreate",
|
||||
"LineQuantityUpdate",
|
||||
"LineQuantityResponse",
|
||||
]
|
||||
50
backend/api/v1/modules/a76/items/line_quantities/models.py
Normal file
50
backend/api/v1/modules/a76/items/line_quantities/models.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from decimal import Decimal
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from sqlalchemy import String, Integer, Numeric, SmallInteger, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..line_items.models import LineItem
|
||||
|
||||
class LineQuantity(Base):
|
||||
"""
|
||||
Quantity details for line items
|
||||
Consolidates all line-level data from Q and S tables
|
||||
"""
|
||||
__tablename__ = "line_quantities"
|
||||
__table_args__ = {
|
||||
"schema": "a76",
|
||||
}
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
item_line_id: Mapped[int] = mapped_column(ForeignKey("a76.item_lines.id"))
|
||||
|
||||
# Quantities
|
||||
quantity: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8)) # CANTIMPO / CANTEXPO / CANTIMPODEF
|
||||
alternate_quantity: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8)) # CANTALTERNA
|
||||
quantity_uma: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8)) # CANTIMPOUMA / CANTEXPOUMA
|
||||
auxiliary_quantity: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8)) # CANTIMPOAUXILIAR / CANTEXPOAUXILIAR
|
||||
|
||||
# Quantities - Special (SCAF specific)
|
||||
quantity_temp_export: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8)) # CANTEXPOTEMP
|
||||
quantity_existence: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8)) # CANTEXISTENCIA
|
||||
quantity_returned: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8)) # CANTRETORNADA
|
||||
quantity_returned_temp: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8)) # CANTRETORNADATEMP
|
||||
serial_count: Mapped[Optional[int]] = mapped_column(Integer) # CANT_SERIES/CANT_SERIESDEF
|
||||
|
||||
# Weight
|
||||
weight_unit: Mapped[Optional[str]] = mapped_column(String(3)) # 'KG' o 'LB'
|
||||
net_weight: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8)) # PESONETO
|
||||
gross_weight: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8)) # PESOBRUTO
|
||||
|
||||
# Packaging
|
||||
package_key: Mapped[Optional[str]] = mapped_column(String(5)) # CLAVEBULTOS
|
||||
package_quantity: Mapped[Optional[int]] = mapped_column(Integer) # CANTBULTOS
|
||||
package_description: Mapped[Optional[str]] = mapped_column(String(40)) # DESCBULTOS
|
||||
container_quantity: Mapped[Optional[int]] = mapped_column(SmallInteger) # CANTBULCONT
|
||||
container_description: Mapped[Optional[str]] = mapped_column(String(40)) # DESCCONTENEDOR
|
||||
box_count: Mapped[Optional[str]] = mapped_column(String(30)) # NOCAJAS
|
||||
|
||||
# Relationship (one-to-one)
|
||||
line: Mapped["LineItem"] = relationship(back_populates="quantity")
|
||||
53
backend/api/v1/modules/a76/items/line_quantities/schemas.py
Normal file
53
backend/api/v1/modules/a76/items/line_quantities/schemas.py
Normal file
@@ -0,0 +1,53 @@
|
||||
from decimal import Decimal
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
|
||||
# ============================================================================
|
||||
# LINE QUANTITY SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
class LineQuantityBase(BaseModel):
|
||||
"""Base schema for line quantities"""
|
||||
# Quantities
|
||||
quantity: Optional[Decimal] = Field(None, description="Main quantity (CANTIMPO/CANTEXPO/CANTIMPODEF)")
|
||||
alternate_quantity: Optional[Decimal] = Field(None, description="Alternate quantity (CANTALTERNA)")
|
||||
quantity_uma: Optional[Decimal] = Field(None, description="UMA quantity (CANTIMPOUMA/CANTEXPOUMA)")
|
||||
auxiliary_quantity: Optional[Decimal] = Field(None, description="Auxiliary quantity (CANTIMPOAUXILIAR/CANTEXPOAUXILIAR)")
|
||||
|
||||
# Quantities - Special (SCAF specific)
|
||||
quantity_temp_export: Optional[Decimal] = Field(None, description="Temporary export quantity (CANTEXPOTEMP)")
|
||||
quantity_existence: Optional[Decimal] = Field(None, description="Existence quantity (CANTEXISTENCIA)")
|
||||
quantity_returned: Optional[Decimal] = Field(None, description="Returned quantity (CANTRETORNADA)")
|
||||
quantity_returned_temp: Optional[Decimal] = Field(None, description="Returned temporary quantity (CANTRETORNADATEMP)")
|
||||
serial_count: Optional[int] = Field(None, description="Serial count (CANT_SERIES/CANT_SERIESDEF)")
|
||||
|
||||
# Weight
|
||||
weight_unit: Optional[str] = Field(None, max_length=3, description="Weight unit ('KG' or 'LB')")
|
||||
net_weight: Optional[Decimal] = Field(None, description="Net weight (PESONETO)")
|
||||
gross_weight: Optional[Decimal] = Field(None, description="Gross weight (PESOBRUTO)")
|
||||
|
||||
# Packaging
|
||||
package_key: Optional[str] = Field(None, max_length=5, description="Package key (CLAVEBULTOS)")
|
||||
package_quantity: Optional[int] = Field(None, description="Package quantity (CANTBULTOS)")
|
||||
package_description: Optional[str] = Field(None, max_length=40, description="Package description (DESCBULTOS)")
|
||||
container_quantity: Optional[int] = Field(None, description="Container quantity (CANTBULCONT)")
|
||||
container_description: Optional[str] = Field(None, max_length=40, description="Container description (DESCCONTENEDOR)")
|
||||
box_count: Optional[str] = Field(None, max_length=30, description="Box count (NOCAJAS)")
|
||||
|
||||
|
||||
class LineQuantityCreate(LineQuantityBase):
|
||||
"""Schema for creating line quantity"""
|
||||
pass
|
||||
|
||||
|
||||
class LineQuantityUpdate(LineQuantityBase):
|
||||
"""Schema for updating line quantity"""
|
||||
pass
|
||||
|
||||
|
||||
class LineQuantityResponse(LineQuantityBase):
|
||||
"""Schema for line quantity response"""
|
||||
id: int
|
||||
item_line_id: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
16
backend/api/v1/modules/a76/items/line_references/__init__.py
Normal file
16
backend/api/v1/modules/a76/items/line_references/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""Line references module"""
|
||||
from .models import LineReference
|
||||
from .schemas import (
|
||||
LineReferenceBase,
|
||||
LineReferenceCreate,
|
||||
LineReferenceUpdate,
|
||||
LineReferenceResponse,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"LineReference",
|
||||
"LineReferenceBase",
|
||||
"LineReferenceCreate",
|
||||
"LineReferenceUpdate",
|
||||
"LineReferenceResponse",
|
||||
]
|
||||
36
backend/api/v1/modules/a76/items/line_references/models.py
Normal file
36
backend/api/v1/modules/a76/items/line_references/models.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from sqlalchemy import Integer, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from core.database import Base
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..line_items.models import LineItem
|
||||
|
||||
class LineReference(Base):
|
||||
"""
|
||||
Reference details for line items
|
||||
Consolidates all line-level data from Q and S tables
|
||||
"""
|
||||
__tablename__ = "line_references"
|
||||
__table_args__ = {
|
||||
"schema": "a76",
|
||||
}
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
item_line_id: Mapped[int] = mapped_column(ForeignKey("a76.item_lines.id"))
|
||||
|
||||
serie_id: Mapped[Optional[int]] = mapped_column(ForeignKey("a76.item_line_series.id")) # SERIEPARTIDA
|
||||
|
||||
# Customer/Vendor
|
||||
customer_invoice: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # CLIENTEFACTURAR
|
||||
assigned_client: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # CLIENTEASIGNADO
|
||||
supplier: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # PROVEEDOR
|
||||
requisitioner: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # REQUISITOR
|
||||
sent_to: Mapped[Optional[str]] = mapped_column(ForeignKey("a76.clients_and_providers.id")) # ENVIADOA
|
||||
|
||||
# PED line reference
|
||||
ped_line: Mapped[Optional[int]] = mapped_column(Integer) # LINEAPED
|
||||
ro_line: Mapped[Optional[int]] = mapped_column(Integer) # LINEARO
|
||||
|
||||
# Relationship (one-to-one)
|
||||
line: Mapped["LineItem"] = relationship(back_populates="reference")
|
||||
39
backend/api/v1/modules/a76/items/line_references/schemas.py
Normal file
39
backend/api/v1/modules/a76/items/line_references/schemas.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field, ConfigDict
|
||||
|
||||
# ============================================================================
|
||||
# LINE REFERENCE SCHEMAS
|
||||
# ============================================================================
|
||||
|
||||
class LineReferenceBase(BaseModel):
|
||||
"""Base schema for line references"""
|
||||
serie_id: Optional[int] = Field(None, description="Serie ID (SERIEPARTIDA)")
|
||||
|
||||
# Customer/Vendor
|
||||
customer_invoice: Optional[str] = Field(None, description="Customer invoice (CLIENTEFACTURAR)")
|
||||
assigned_client: Optional[str] = Field(None, description="Assigned client (CLIENTEASIGNADO)")
|
||||
supplier: Optional[str] = Field(None, description="Supplier (PROVEEDOR)")
|
||||
requisitioner: Optional[str] = Field(None, description="Requisitioner (REQUISITOR)")
|
||||
sent_to: Optional[str] = Field(None, description="Sent to (ENVIADOA)")
|
||||
|
||||
# PED line reference
|
||||
ped_line: Optional[int] = Field(None, description="PED line (LINEAPED)")
|
||||
ro_line: Optional[int] = Field(None, description="RO line (LINEARO)")
|
||||
|
||||
|
||||
class LineReferenceCreate(LineReferenceBase):
|
||||
"""Schema for creating line reference"""
|
||||
pass
|
||||
|
||||
|
||||
class LineReferenceUpdate(LineReferenceBase):
|
||||
"""Schema for updating line reference"""
|
||||
pass
|
||||
|
||||
|
||||
class LineReferenceResponse(LineReferenceBase):
|
||||
"""Schema for line reference response"""
|
||||
id: int
|
||||
item_line_id: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -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)
|
||||
)
|
||||
```
|
||||
"""
|
||||
"""
|
||||
|
||||
240
backend/api/v1/modules/a76/items/routes.py
Normal file
240
backend/api/v1/modules/a76/items/routes.py
Normal file
@@ -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
|
||||
74
backend/api/v1/modules/a76/items/schemas.py
Normal file
74
backend/api/v1/modules/a76/items/schemas.py
Normal file
@@ -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)
|
||||
@@ -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",
|
||||
|
||||
346
backend/api/v1/modules/a76/items/service.py
Normal file
346
backend/api/v1/modules/a76/items/service.py
Normal file
@@ -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")
|
||||
@@ -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")
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 ''",
|
||||
|
||||
@@ -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;
|
||||
|
||||
121
frontend/src/lib/api/dashboard/a76/items.ts
Normal file
121
frontend/src/lib/api/dashboard/a76/items.ts
Normal file
@@ -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<ItemListResponse>(`/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<ItemListResponse>(`/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<Item>(`/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<Item>(`/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<Item>(`/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()}`);
|
||||
}
|
||||
};
|
||||
@@ -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)
|
||||
)
|
||||
);
|
||||
</script>
|
||||
|
||||
<!-- Layout de 2 columnas compacto -->
|
||||
@@ -203,20 +222,17 @@
|
||||
</div>
|
||||
|
||||
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Clientes - Proveedores - Agente Aduanal</h4>
|
||||
<div class="space-y-1.5">
|
||||
<Label for="provider_id" class="text-xs">Proveedor:</Label>
|
||||
<div class="grid grid-cols-4 gap-3 space-y-1.5">
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={formData.provider_header || ''}
|
||||
value={formData.provider_header || providerHeaderOptions[0]?.value || ''}
|
||||
onValueChange={(v) => {
|
||||
formData.provider_header = v ?? '';
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="provider_header" class="h-7 text-xs">
|
||||
<Select.Trigger id="provider_header" class="h-7 text-xs min-w-[125px] max-w-[250px]">
|
||||
<span class="truncate">
|
||||
{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...'}
|
||||
</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
@@ -234,7 +250,7 @@
|
||||
formData.provider_id = v ? parseInt(v) : null;
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="provider_id" class="h-7 text-xs">
|
||||
<Select.Trigger id="provider_id" class="h-7 text-xs min-w-[120px] max-w-[250px]">
|
||||
<span class="truncate">
|
||||
{formData.provider_id
|
||||
? providers.find(p => p.id === formData.provider_id)?.name || 'Selecciona...'
|
||||
@@ -248,23 +264,20 @@
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</Select.Root>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<Label for="sold_to_id" class="text-xs">Consignado a:</Label>
|
||||
<div class="grid grid-cols-4 gap-3 space-y-1.5">
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={formData.sold_to_header || ''}
|
||||
value={formData.sold_to_header || soldToHeaderOptions[0]?.value || ''}
|
||||
onValueChange={(v) => {
|
||||
formData.sold_to_header = v ?? '';
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="sold_to_header" class="h-7 text-xs">
|
||||
<Select.Trigger id="sold_to_header" class="h-7 text-xs min-w-[125px] max-w-[250px]">
|
||||
<span class="truncate">
|
||||
{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...'}
|
||||
</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
@@ -282,7 +295,7 @@
|
||||
formData.sold_to_id = v ? parseInt(v) : null;
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="sold_to_id" class="h-7 text-xs">
|
||||
<Select.Trigger id="sold_to_id" class="h-7 text-xs min-w-[120px] max-w-[250px]">
|
||||
<span class="truncate">
|
||||
{formData.sold_to_id
|
||||
? clients.find(c => c.id === formData.sold_to_id)?.name || 'Selecciona...'
|
||||
@@ -299,20 +312,17 @@
|
||||
</Select.Root>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<Label for="shipped_to_id" class="text-xs">Enviado a:</Label>
|
||||
<div class="grid grid-cols-4 gap-3 space-y-1.5">
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={formData.shipped_to_header || ''}
|
||||
value={formData.shipped_to_header || shippedToHeaderOptions[0]?.value || ''}
|
||||
onValueChange={(v) => {
|
||||
formData.shipped_to_header = v ?? '';
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="shipped_to_header" class="h-7 text-xs">
|
||||
<Select.Trigger id="shipped_to_header" class="h-7 text-xs min-w-[125px] max-w-[250px]">
|
||||
<span class="truncate">
|
||||
{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...'}
|
||||
</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content class="max-h-[300px]">
|
||||
@@ -330,7 +340,7 @@
|
||||
formData.shipped_to_id = v ? parseInt(v) : null;
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="shipped_to_id" class="h-7 text-xs">
|
||||
<Select.Trigger id="shipped_to_id" class="h-7 text-xs min-w-[120px] max-w-[250px]">
|
||||
<span class="truncate">
|
||||
{formData.shipped_to_id
|
||||
? allClientsProviders.find(cp => cp.id === formData.shipped_to_id)?.name || 'Selecciona...'
|
||||
@@ -347,66 +357,66 @@
|
||||
</Select.Root>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="space-y-1.5">
|
||||
<Label for="customs_broker_id" class="text-xs">Agente Aduanal Mex:</Label>
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={formData.customs_broker_id || ''}
|
||||
onValueChange={(v) => {
|
||||
formData.customs_broker_id = v || null;
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="customs_broker_id" class="h-7 text-xs">
|
||||
<span class="truncate">
|
||||
{formData.customs_broker_id
|
||||
? customsBrokers.find(cb => cb.broker_key === formData.customs_broker_id)?.name || '...'
|
||||
: '...'}
|
||||
</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content class="max-h-[300px]">
|
||||
{#each customsBrokers as broker}
|
||||
<Select.Item value={broker.broker_key}>
|
||||
{broker.name}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<Label for="customs_broker_id" class="text-xs">Agente Aduanal Mex:</Label>
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={formData.customs_broker_id || customsBrokers[0]?.broker_key || ''}
|
||||
onValueChange={(v) => {
|
||||
formData.customs_broker_id = v || null;
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="customs_broker_id" class="h-7 text-xs min-w-[150px] max-w-[300px]">
|
||||
<span class="truncate">
|
||||
{customsBrokers.find(cb => cb.broker_key === (formData.customs_broker_id || customsBrokers[0]?.broker_key))?.name || '...'}
|
||||
</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content class="max-h-[300px]">
|
||||
{#each customsBrokers as broker}
|
||||
<Select.Item value={broker.broker_key}>
|
||||
{broker.name}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<Label for="customs_broker_us_id" class="text-xs">Agente Aduanal Ame:</Label>
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={formData.customs_broker_us_id || ''}
|
||||
onValueChange={(v) => {
|
||||
formData.customs_broker_us_id = v || null;
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="customs_broker_us_id" class="h-7 text-xs">
|
||||
<span class="truncate">
|
||||
{formData.customs_broker_us_id
|
||||
? customsBrokers.find(cb => cb.broker_key === formData.customs_broker_us_id)?.name || '...'
|
||||
: '...'}
|
||||
</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content class="max-h-[300px]">
|
||||
{#each customsBrokers as broker}
|
||||
<Select.Item value={broker.broker_key}>
|
||||
{broker.name}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1.5">
|
||||
<Label for="customs_broker_us_id" class="text-xs">Agente Aduanal Ame:</Label>
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={formData.customs_broker_us_id || ''}
|
||||
onValueChange={(v) => {
|
||||
formData.customs_broker_us_id = v || null;
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="customs_broker_us_id" class=" min-w-[150px] h-7 text-xs">
|
||||
<span class="truncate">
|
||||
{formData.customs_broker_us_id
|
||||
? customsBrokers.find(cb => cb.broker_key === formData.customs_broker_us_id)?.name || '...'
|
||||
: '...'}
|
||||
</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content class="max-h-[300px]">
|
||||
{#each customsBrokers as broker}
|
||||
<Select.Item value={broker.broker_key}>
|
||||
{broker.name}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Columna Derecha: Tipo de Moneda y Transportista -->
|
||||
<div class="space-y-3">
|
||||
<!-- Tipo de Moneda - Pesos Netos y Brutos -->
|
||||
<div class="border rounded-md p-3 space-y-2">
|
||||
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Tipo de Moneda - Pesos Netos y Brutos</h4>
|
||||
<div class="flex justify-between">
|
||||
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Tipo de Moneda - Pesos Netos y Brutos</h4>
|
||||
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Tipo de cambio: </h4>
|
||||
</div>
|
||||
|
||||
<!-- Radio buttons para tipo de moneda -->
|
||||
<div class="space-y-1.5">
|
||||
@@ -425,8 +435,7 @@
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
{#if formData.currency_mode === 'captura'}
|
||||
<div class="space-y-1.5">
|
||||
<Label for="currency_type" class="text-xs">Moneda:</Label>
|
||||
<Select.Root
|
||||
@@ -436,12 +445,12 @@
|
||||
formData.currency_type = v ?? '';
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="currency_type" class="h-7 text-xs">
|
||||
<Select.Trigger id="currency_type" class="h-7 text-xs min-w-[80px]">
|
||||
<span class="truncate">
|
||||
{formData.currency_type || '...'}
|
||||
</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content class="max-h-[300px]">
|
||||
<Select.Content class="min-w-[80px] max-h-[300px]">
|
||||
{#each currencyTypes as currencyType}
|
||||
<Select.Item value={currencyType.code}>
|
||||
{currencyType.code}
|
||||
@@ -450,7 +459,8 @@
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
|
||||
{/if}
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div class="space-y-1.5">
|
||||
<Label for="weight_type" class="text-xs">Tipo Peso:</Label>
|
||||
<Select.Root
|
||||
@@ -460,15 +470,15 @@
|
||||
formData.weight_type = v ?? '';
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="weight_type" class="h-7 text-xs">
|
||||
<Select.Trigger id="weight_type" class="min-w-[150px] h-7 text-xs">
|
||||
<span class="truncate">
|
||||
{formData.weight_type || '...'}
|
||||
</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each weightTypeOptions as weightType}
|
||||
<Select.Item value={weightType.value}>
|
||||
{weightType.value}
|
||||
<Select.Item value={weightType.label}>
|
||||
{weightType.label}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
@@ -479,33 +489,7 @@
|
||||
<Label for="iva_factor" class="text-xs">IVA:</Label>
|
||||
<Input id="iva_factor" type="number" step="0.0001" bind:value={formData.iva_factor} placeholder="0.16" class="h-7 text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<Label for="invoice_type" class="text-xs">Tipo de Cambio:</Label>
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={formData.invoice_type || ''}
|
||||
onValueChange={(v) => {
|
||||
formData.invoice_type = v ?? '';
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="invoice_type" class="h-7 text-xs">
|
||||
<span class="truncate">
|
||||
{formData.invoice_type
|
||||
? `${formData.invoice_type}`
|
||||
: '...'}
|
||||
</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content class="max-h-[300px]">
|
||||
{#each invoiceTypes as type}
|
||||
<Select.Item value={type.key}>
|
||||
{type.key} - {type.description}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Transportista -->
|
||||
@@ -514,8 +498,33 @@
|
||||
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div class="space-y-1.5">
|
||||
<Label for="carrier_id" class="text-xs">Clave:</Label>
|
||||
<Input id="carrier_id" type="number" bind:value={formData.carrier_id} class="h-7 text-xs" />
|
||||
<Label for="carrier_id" class="text-xs">Transportista:</Label>
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={formData.carrier_id ? String(formData.carrier_id) : ''}
|
||||
onValueChange={(v) => {
|
||||
formData.carrier_id = v || null;
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="carrier_id" class="h-7 text-xs min-w-[120px] max-w-[250px]">
|
||||
<span class="truncate">
|
||||
{#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}
|
||||
</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content class="max-h-[300px]">
|
||||
{#each transporters as transporter}
|
||||
<Select.Item value={String(transporter.transporter_key)}>
|
||||
{transporter.transporter_key}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 space-y-1.5">
|
||||
@@ -527,15 +536,113 @@
|
||||
formData.transport_type = v ?? '';
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="transport_type" class="h-7 text-xs">
|
||||
<Select.Trigger id="transport_type" class="h-7 text-xs min-w-[120px] max-w-[250px]">
|
||||
<span class="truncate">
|
||||
{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}
|
||||
</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content class="max-h-[300px]">
|
||||
{#each transportTypes as transportType}
|
||||
<Select.Item value={transportType.transport_code}>
|
||||
{transportType.transport_code}
|
||||
{#each vehicles as vehicle}
|
||||
<Select.Item value={vehicle.vehicle_key}>
|
||||
{vehicle.vehicle_key} {vehicle.plate_number ? `- ${vehicle.plate_number}` : ''}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<Label for="driver_name" class="text-xs">Conductor:</Label>
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={formData.driver_name || ''}
|
||||
onValueChange={(v) => {
|
||||
formData.driver_name = v ?? '';
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="driver_name" class="h-7 text-xs w-full">
|
||||
<span class="truncate">
|
||||
{#if formData.driver_name}
|
||||
{formData.driver_name}
|
||||
{:else if drivers.length > 0}
|
||||
Selecciona conductor...
|
||||
{:else}
|
||||
Sin datos
|
||||
{/if}
|
||||
</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content class="max-h-[300px]">
|
||||
{#each drivers as driver}
|
||||
<Select.Item value={driver.driver_name}>
|
||||
{driver.driver_name}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
<div class="space-y-1.5">
|
||||
<Label for="transport_type" class="text-xs">Tipo Transporte:</Label>
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={formData.transport_id || 'Ninguno'}
|
||||
onValueChange={(v) => {
|
||||
formData.transport_id = v ?? 'Ninguno';
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="transport_id" class="h-7 text-xs min-w-[120px] max-w-[250px]">
|
||||
<span class="truncate">
|
||||
{formData.transport_id || 'Ninguno'}
|
||||
</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="Ninguno">Ninguno</Select.Item>
|
||||
<Select.Item value="Transporte">Transporte</Select.Item>
|
||||
<Select.Item value="Caja">Caja</Select.Item>
|
||||
<Select.Item value="Placas">Placas</Select.Item>
|
||||
<Select.Item value="Camión">Camión</Select.Item>
|
||||
<Select.Item value="Buque">Buque</Select.Item>
|
||||
<Select.Item value="Ferrobarcaza">Ferrobarcaza</Select.Item>
|
||||
<Select.Item value="Contenedor">Contenedor</Select.Item>
|
||||
<Select.Item value="Avion">Avion</Select.Item>
|
||||
<Select.Item value="Gondola">Gondola</Select.Item>
|
||||
<Select.Item value="Plataforma">Plataforma</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5 col-span-3">
|
||||
<Label for="transport_num" class="text-xs">Placas:</Label>
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={formData.transport_num || ''}
|
||||
onValueChange={(v) => {
|
||||
formData.transport_num = v ?? '';
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="transport_num" class="h-7 text-xs w-full">
|
||||
<span class="truncate">
|
||||
{#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}
|
||||
</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content class="max-h-[300px]">
|
||||
{#each trailers as trailer}
|
||||
<Select.Item value={trailer.trailer_number}>
|
||||
{trailer.plate_number || trailer.trailer_number}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
@@ -543,31 +650,66 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<Label for="driver_name" class="text-xs">Conductor:</Label>
|
||||
<Input id="driver_name" bind:value={formData.driver_name} class="h-7 text-xs" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="space-y-1.5">
|
||||
<Label for="transport_type" class="text-xs">Tipo Transporte:</Label>
|
||||
<Input id="transport_id" bind:value={formData.transport_id} class="h-7 text-xs" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<Label for="transport_num" class="text-xs">Placas:</Label>
|
||||
<Input id="transport_num" bind:value={formData.transport_num} class="h-7 text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<Label for="aduana" class="text-xs">Aduana y Sección de Despacho:</Label>
|
||||
<Input id="aduana" bind:value={formData.aduana} class="h-7 text-xs" />
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={formData.aduana || ''}
|
||||
onValueChange={(v) => {
|
||||
formData.aduana = v ?? '';
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="aduana" class="h-7 text-xs w-full">
|
||||
<span class="truncate">
|
||||
{#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}
|
||||
</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content class="max-h-[300px]">
|
||||
{#each customsSections as section}
|
||||
<Select.Item value={section.customs_code}>
|
||||
{section.customs_code} - {section.section_name}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<Label for="clave_regimen_aduanero" class="text-xs">Clave de Régimen Aduanero:</Label>
|
||||
<Input id="clave_regimen_aduanero" bind:value={formData.clave_regimen_aduanero} class="h-7 text-xs" />
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={formData.clave_regimen_aduanero || ''}
|
||||
onValueChange={(v) => {
|
||||
formData.clave_regimen_aduanero = v ?? '';
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="clave_regimen_aduanero" class="h-7 text-xs w-full">
|
||||
<span class="truncate">
|
||||
{#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}
|
||||
</span>
|
||||
</Select.Trigger>
|
||||
<Select.Content class="max-h-[300px]">
|
||||
{#each filteredRegimens as regimen}
|
||||
<Select.Item value={regimen.regimen_code}>
|
||||
{regimen.regimen_code}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<script lang="ts">
|
||||
import * as RadioGroup from '$lib/components/ui/radio-group';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
|
||||
let {
|
||||
isSubPartida = $bindable(),
|
||||
continueSubPartidas = $bindable()
|
||||
}: {
|
||||
isSubPartida: string;
|
||||
continueSubPartidas: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div class="lg:col-span-5 space-y-3">
|
||||
<!-- Is Item/Subitem and Continue Sub-Items -->
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<fieldset class="border rounded-md p-2 space-y-2">
|
||||
<legend class="text-xs font-semibold px-2 bg-gray-200 dark:bg-gray-700">Is</legend>
|
||||
<RadioGroup.Root bind:value={isSubPartida} class="flex gap-3">
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="partida" id="partida" />
|
||||
<Label for="partida" class="text-xs font-normal">Item</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="subpartida" id="subpartida" />
|
||||
<Label for="subpartida" class="text-xs font-normal">Subitem</Label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="border rounded-md p-2 space-y-2">
|
||||
<legend class="text-xs font-semibold px-2 bg-gray-200 dark:bg-gray-700">Continue Sub-Items</legend>
|
||||
<RadioGroup.Root bind:value={continueSubPartidas} class="flex gap-3">
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="si" id="continue_si" />
|
||||
<Label for="continue_si" class="text-xs font-normal">Yes</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="no" id="continue_no" />
|
||||
<Label for="continue_no" class="text-xs font-normal">No</Label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<!-- Descriptions -->
|
||||
<fieldset class="border rounded-md p-2 space-y-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="num_parte" class="text-xs">Part Number:</Label>
|
||||
<div class="flex gap-1">
|
||||
<Input id="num_parte" class="h-7 text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
<Label for="desc_espanol" class="text-xs">Description in Spanish:</Label>
|
||||
<textarea
|
||||
id="desc_espanol"
|
||||
class="flex min-h-[60px] w-full rounded-md border border-input bg-background px-2 py-1 text-xs ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
<Label for="desc_ingles" class="text-xs">Description in English:</Label>
|
||||
<textarea
|
||||
id="desc_ingles"
|
||||
class="flex min-h-[60px] w-full rounded-md border border-input bg-background px-2 py-1 text-xs ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
></textarea>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
@@ -0,0 +1,124 @@
|
||||
<script lang="ts">
|
||||
import * as Sheet from '$lib/components/ui/sheet';
|
||||
import * as Tabs from '$lib/components/ui/tabs';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Loader2 } from 'lucide-svelte';
|
||||
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
|
||||
import type { Item } from '$lib/api/dashboard/a76/items';
|
||||
|
||||
// Import child components
|
||||
import MainData from './main-data.svelte';
|
||||
import ItemConfiguration from './item-configuration.svelte';
|
||||
import PackagesSection from './packages-section.svelte';
|
||||
import SummarySection from './summary-section.svelte';
|
||||
import TabContinuation from './tab-continuation.svelte';
|
||||
import TabSeries from './tab-series.svelte';
|
||||
import TabLabeling from './tab-labeling.svelte';
|
||||
import TabIdentifiers from './tab-identifiers.svelte';
|
||||
|
||||
let {
|
||||
open = $bindable(),
|
||||
isEditMode = false,
|
||||
editingItem = $bindable(),
|
||||
invoice,
|
||||
onSave,
|
||||
isSaving = false
|
||||
}: {
|
||||
open: boolean;
|
||||
isEditMode?: boolean;
|
||||
editingItem: Partial<Item>;
|
||||
invoice: Invoice | null;
|
||||
onSave: () => void;
|
||||
isSaving?: boolean;
|
||||
} = $props();
|
||||
|
||||
let isSubPartida = $state('partida');
|
||||
let continueSubPartidas = $state('no');
|
||||
</script>
|
||||
|
||||
<style>
|
||||
:global([data-tabs-trigger]) {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<Sheet.Root bind:open={open}>
|
||||
<Sheet.Content side="right" class="w-full sm:max-w-[95vw] lg:max-w-[80vw] xl:max-w-[70vw] overflow-y-auto overflow-x-hidden">
|
||||
<Sheet.Header class="text-white -mx-4 -mt-3 px-6">
|
||||
<Sheet.Title class="text-base font-semibold text-white">
|
||||
Temporary Import Item
|
||||
</Sheet.Title>
|
||||
<Sheet.Description class="text-xs text-purple-100">
|
||||
Order Number: {invoice?.invoice_number || 'N/A'} | Line: 1
|
||||
</Sheet.Description>
|
||||
</Sheet.Header>
|
||||
|
||||
<div class="w-full max-w-full overflow-x-hidden px-1">
|
||||
<!-- Always visible section: Main data and right column -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-4 mb-6">
|
||||
<!-- Left column (7 columns) -->
|
||||
<div class="lg:col-span-7 space-y-3">
|
||||
<MainData />
|
||||
</div>
|
||||
|
||||
<!-- Right column (5 columns) -->
|
||||
<ItemConfiguration bind:isSubPartida={isSubPartida} bind:continueSubPartidas={continueSubPartidas} />
|
||||
</div>
|
||||
|
||||
<!-- Tabs with additional content -->
|
||||
<Tabs.Root value="generales" class="mt-6">
|
||||
<Tabs.List class="grid w-full grid-cols-5 mb-4 h-9 gap-1">
|
||||
<Tabs.Trigger value="generales" class="text-[10px] sm:text-xs px-1 sm:px-2 py-1">1) General</Tabs.Trigger>
|
||||
<Tabs.Trigger value="continuacion" class="text-[10px] sm:text-xs px-1 sm:px-2 py-1">2) Continuation</Tabs.Trigger>
|
||||
<Tabs.Trigger value="series" class="text-[10px] sm:text-xs px-1 sm:px-2 py-1">3) Series</Tabs.Trigger>
|
||||
<Tabs.Trigger value="etiquetado" class="text-[10px] sm:text-xs px-1 sm:px-2 py-1">4) Labeling</Tabs.Trigger>
|
||||
<Tabs.Trigger value="identificadores" class="text-[10px] sm:text-xs px-1 sm:px-2 py-1">5) Identifiers</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
<!-- Tab: General -->
|
||||
<Tabs.Content value="generales" class="space-y-3 mt-0">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<PackagesSection />
|
||||
<SummarySection />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- Tab: Continuation -->
|
||||
<Tabs.Content value="continuacion" class="space-y-3 mt-0">
|
||||
<TabContinuation />
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- Tab: Series -->
|
||||
<Tabs.Content value="series" class="space-y-3 mt-0">
|
||||
<TabSeries />
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- Tab: Labeling -->
|
||||
<Tabs.Content value="etiquetado" class="space-y-3 mt-0">
|
||||
<TabLabeling />
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- Tab: Identifiers -->
|
||||
<Tabs.Content value="identificadores" class="space-y-3 mt-0">
|
||||
<TabIdentifiers />
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
</div>
|
||||
|
||||
<Sheet.Footer>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<Button variant="outline" onclick={() => open = false} disabled={isSaving}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onclick={onSave} disabled={isSaving}>
|
||||
{#if isSaving}
|
||||
<Loader2 class="w-4 h-4 mr-2 animate-spin" />
|
||||
Saving...
|
||||
{:else}
|
||||
{isEditMode ? 'Save Changes' : 'Add Item'}
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</Sheet.Footer>
|
||||
</Sheet.Content>
|
||||
</Sheet.Root>
|
||||
@@ -0,0 +1,74 @@
|
||||
<script lang="ts">
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
</script>
|
||||
|
||||
<fieldset class="border rounded-md p-3 space-y-3">
|
||||
<legend class="text-xs font-semibold px-2 bg-gray-200 dark:bg-gray-700">Main Data</legend>
|
||||
|
||||
<!-- Class -->
|
||||
<div class="grid grid-cols-12 gap-2">
|
||||
<div class="col-span-4 space-y-1">
|
||||
<Label for="clase" class="text-xs font-medium">* Class:</Label>
|
||||
<div class="flex gap-1">
|
||||
<Input id="clase" class="h-8 text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quantity -->
|
||||
<div class="grid grid-cols-12 gap-2">
|
||||
<div class="col-span-4 space-y-1">
|
||||
<Label for="cantidad" class="text-xs font-medium">* Quantity:</Label>
|
||||
<Input id="cantidad" type="number" min="0" value="0.00000000" class="h-8 text-xs text-right" />
|
||||
</div>
|
||||
<div class="col-span-4 space-y-1">
|
||||
<Label class="text-xs font-medium">U.M.:</Label>
|
||||
<div class="flex gap-1">
|
||||
<div class="h-8 flex items-center flex-1">
|
||||
<span class="text-xs text-muted-foreground">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Unit Cost and Fraction -->
|
||||
<div class="grid grid-cols-12 gap-2">
|
||||
<div class="col-span-4 space-y-1">
|
||||
<Label for="costo_unitario" class="text-xs font-medium">* Unit Cost:</Label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Input id="costo_unitario" type="number" min="0" value="0.00000000" class="h-8 text-xs text-right flex-1" />
|
||||
<span class="text-xs text-blue-600 font-semibold">USD</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-5 space-y-1">
|
||||
<Label for="fraccion" class="text-xs font-medium">Fraction:</Label>
|
||||
<div class="flex gap-1">
|
||||
<Input id="fraccion" value="0000 00 00" class="h-8 text-xs text-center" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Origin Country and Tariff Type -->
|
||||
<div class="grid grid-cols-12 gap-2">
|
||||
<div class="col-span-4 space-y-1">
|
||||
<Label for="pais_origen" class="text-xs font-medium">* Origin Country:</Label>
|
||||
<div class="flex gap-1">
|
||||
<Input id="pais_origen" class="h-8 text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-4 space-y-1">
|
||||
<Label for="tipo_tarifa" class="text-xs font-medium">* Tariff Type:</Label>
|
||||
<select id="tipo_tarifa" class="flex h-8 w-full rounded-md border border-input bg-background px-2 py-1 text-xs ring-offset-background">
|
||||
<option>GENERAL</option>
|
||||
<option>PREFERENCIAL</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-span-4 space-y-1">
|
||||
<Label class="text-xs font-medium">Advalorem:</Label>
|
||||
<div class="h-8 flex items-center">
|
||||
<span class="text-xs text-muted-foreground">0.00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -0,0 +1,86 @@
|
||||
<script lang="ts">
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
</script>
|
||||
|
||||
<fieldset class="border rounded-md p-2 space-y-2">
|
||||
<legend class="text-xs font-semibold px-2 bg-gray-200 dark:bg-gray-700">PACKAGES</legend>
|
||||
|
||||
<div class="grid grid-cols-12 gap-2 items-end">
|
||||
<div class="col-span-2 space-y-1">
|
||||
<Label for="cantidad_bultos" class="text-xs">Quantity:</Label>
|
||||
<Input id="cantidad_bultos" type="number" min="0" value="0" class="h-7 text-xs text-right" />
|
||||
</div>
|
||||
<div class="col-span-3 space-y-1">
|
||||
<Label for="clave_bultos" class="text-xs">Package Code:</Label>
|
||||
<Input id="clave_bultos" class="h-7 text-xs" />
|
||||
</div>
|
||||
<div class="col-span-1 flex items-end">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-12 gap-2 items-end">
|
||||
<div class="col-span-2 space-y-1">
|
||||
<Label for="peso_bultos" class="text-xs">Weight: 0</Label>
|
||||
</div>
|
||||
<div class="col-span-4 space-y-1">
|
||||
<Label for="descripcion_bultos" class="text-xs">Description:</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WEIGHTS subsection -->
|
||||
<div class="border-t pt-2">
|
||||
<div class="text-xs font-semibold mb-2">WEIGHTS</div>
|
||||
<div class="grid grid-cols-6 gap-2 items-end">
|
||||
<div class="col-span-2 space-y-1">
|
||||
<Label for="peso_neto" class="text-xs">Net:</Label>
|
||||
<Input id="peso_neto" type="number" min="0" value="0.00000000" class="h-7 text-xs text-right" />
|
||||
</div>
|
||||
<div class="col-span-2 space-y-1">
|
||||
<Label for="peso_bruto" class="text-xs">Gross:</Label>
|
||||
<Input id="peso_bruto" type="number" min="0" value="0.00000000" class="h-7 text-xs text-right" />
|
||||
</div>
|
||||
<div class="col-span-2 space-y-1">
|
||||
<Label class="text-xs invisible">Space</Label>
|
||||
<span class="text-xs text-red-600 font-semibold">KILOS</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-12 gap-2 items-end">
|
||||
<div class="col-span-2 space-y-1">
|
||||
<Label for="no_permiso" class="text-xs">Permit No.:</Label>
|
||||
<Input id="no_permiso" class="h-7 text-xs" />
|
||||
</div>
|
||||
<div class="col-span-3 space-y-1">
|
||||
<Label for="pag_region" class="text-xs">Page/Region:</Label>
|
||||
<Input id="pag_region" class="h-7 text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-12 gap-2 items-end">
|
||||
<div class="col-span-4 space-y-1">
|
||||
<Label for="fraccion_americana" class="text-xs">American Fraction:</Label>
|
||||
<Input id="fraccion_americana" class="h-7 text-xs" />
|
||||
</div>
|
||||
<div class="col-span-3 space-y-1">
|
||||
<Label class="text-xs invisible">Space</Label>
|
||||
<span class="text-xs">Advalorem: 0.00</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-9 gap-2 items-end">
|
||||
<div class="col-span-3 space-y-1">
|
||||
<Label for="marca" class="text-xs">Brand:</Label>
|
||||
<Input id="marca" class="h-7 text-xs" />
|
||||
</div>
|
||||
<div class="col-span-3 space-y-1">
|
||||
<Label for="modelo" class="text-xs">Model:</Label>
|
||||
<Input id="modelo" class="h-7 text-xs" />
|
||||
</div>
|
||||
<div class="col-span-3 space-y-1">
|
||||
<Label for="orden_compra" class="text-xs">Purchase Order:</Label>
|
||||
<Input id="orden_compra" class="h-7 text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<fieldset class="border rounded-md p-2 space-y-1 bg-red-50 dark:bg-red-950/20">
|
||||
<legend class="text-xs font-semibold px-2 bg-red-700 text-white">GENERAL DATA</legend>
|
||||
|
||||
<div class="text-xs font-semibold">RETURN QUANTITY SUB-ITEMS</div>
|
||||
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||
<div>Temporary: <span class="text-blue-600">0.00000000</span></div>
|
||||
<div>Replacement or Change: <span class="text-blue-600">0.00000000</span></div>
|
||||
<div>Definitive: <span class="text-blue-600">0.00000000</span></div>
|
||||
<div>Returned Values: <span class="text-blue-600">0.00000000</span></div>
|
||||
<div class="col-span-2">Returned Values: <span class="text-blue-600">0.00000000</span></div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2 text-xs pt-2 border-t">
|
||||
<div class="font-semibold">WEIGHTS (KILOS)</div>
|
||||
<div class="font-semibold">WEIGHTS (Pounds)</div>
|
||||
<div>Net: <span class="text-blue-600">0.00000000</span></div>
|
||||
<div><span class="text-blue-600">0.00000000</span></div>
|
||||
<div>Whole: <span class="text-blue-600">0.00000000</span></div>
|
||||
<div><span class="text-blue-600">0.00000000</span></div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<!-- COSTS AND VALUES -->
|
||||
<fieldset class="border rounded-md p-2 space-y-1 bg-amber-50 dark:bg-amber-950/20">
|
||||
<legend class="text-xs font-semibold px-2 bg-amber-700 text-white">COSTS AND VALUES</legend>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||
<div class="font-semibold">(Dollars)</div>
|
||||
<div class="font-semibold">(Pesos)</div>
|
||||
<div>Cost: <span class="text-blue-600">0.00000000</span></div>
|
||||
<div><span class="text-blue-600">0.00000000</span></div>
|
||||
<div>Value: <span class="text-blue-600">0.00000000</span></div>
|
||||
<div><span class="text-blue-600">0.00000000</span></div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1 pt-2 border-t">
|
||||
<div class="text-xs">Capture Cost: <span class="text-blue-600">0.00000000</span> <span class="text-blue-600">USD</span></div>
|
||||
<div class="text-xs">Capture Value: <span class="text-blue-600">0.00000000</span> <span class="text-blue-600">USD</span></div>
|
||||
<div class="text-xs">Customs Value: <span class="text-blue-600">0.00000000</span> <span class="text-blue-600">USD</span></div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
@@ -0,0 +1,133 @@
|
||||
<script lang="ts">
|
||||
import * as RadioGroup from '$lib/components/ui/radio-group';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Checkbox } from '$lib/components/ui/checkbox';
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<!-- Left Column -->
|
||||
<div class="space-y-3">
|
||||
<!-- TAX PAID -->
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<fieldset class="border rounded-md p-2 space-y-2">
|
||||
<legend class="text-xs font-semibold px-2 bg-gray-200 dark:bg-gray-700">TAX PAID</legend>
|
||||
<RadioGroup.Root value="no" class="flex gap-3">
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="si" id="pago_impuesto_si" />
|
||||
<Label for="pago_impuesto_si" class="text-xs font-normal">Yes</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="no" id="pago_impuesto_no" />
|
||||
<Label for="pago_impuesto_no" class="text-xs font-normal">No</Label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
</fieldset>
|
||||
<div class="space-y-1">
|
||||
<div class="space-y-1">
|
||||
<Label for="forma_pago" class="text-xs">Payment Method:</Label>
|
||||
<div class="flex gap-1">
|
||||
<Input id="forma_pago" value="21" class="h-7 text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
<Label for="credito_iva" class="text-xs">VAT AND EXCISE TAX CREDITS.</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- IGI Amount -->
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="monto_igi" class="text-xs">IGI Amount: 0 <span class="text-xs">DOLLARS</span></Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Certificate of Origin -->
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<fieldset class="border rounded-md p-2 space-y-2">
|
||||
<legend class="text-xs font-semibold px-2 bg-gray-200 dark:bg-gray-700">Has Certificate of Origin?</legend>
|
||||
<RadioGroup.Root value="no" class="flex gap-3">
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="si" id="cert_origen_si" />
|
||||
<Label for="cert_origen_si" class="text-xs font-normal">Yes</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="no" id="cert_origen_no" />
|
||||
<Label for="cert_origen_no" class="text-xs font-normal">No</Label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
</fieldset>
|
||||
<div class="space-y-1 col-span-3">
|
||||
<Label for="num_cert_origen" class="text-xs">Certificate of Origin No.:</Label>
|
||||
<Input id="num_cert_origen" class="h-7 text-xs" />
|
||||
<Label for="num_cert_origen" class="text-xs">End Date:</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Location -->
|
||||
<div class="space-y-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="localizacion_maquinaria" class="text-xs">Machinery and equipment location:</Label>
|
||||
<div class="flex gap-1">
|
||||
<Input id="localizacion_maquinaria" class="h-7 text-xs" />
|
||||
</div>
|
||||
<Label for="localizacion_maquinaria" class="text-xs">Location variable</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Military Equipment -->
|
||||
<div class="flex items-center space-x-2 ">
|
||||
<Checkbox id="equipo_militar" />
|
||||
<Label for="equipo_militar" class="text-xs font-normal">Enable if Item Contains Military Equipment</Label>
|
||||
</div>
|
||||
|
||||
<!-- Lot and Entry Number -->
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="lote" class="text-xs">Lot:</Label>
|
||||
<Input id="lote" class="h-7 text-xs" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="num_entrada" class="text-xs">Entry No.:</Label>
|
||||
<Input id="num_entrada" class="h-7 text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column -->
|
||||
<div class="space-y-3">
|
||||
<!-- Permit and Eighth Rule Fraction -->
|
||||
<div class="space-y-2">
|
||||
<div class="space-y-1">
|
||||
<Label for="permiso_regla_octava" class="text-xs">Eighth Rule Permit:</Label>
|
||||
<div class="flex gap-1">
|
||||
<Input id="permiso_regla_octava" class="h-7 text-xs" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-2 items-end">
|
||||
<div class="space-y-1 col-span-3">
|
||||
<Label for="fraccion_regla_octava" class="text-xs">Eighth Rule Fraction:</Label>
|
||||
<Input id="fraccion_regla_octava" value="0000.00.00" class="h-7 text-xs" />
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<Label for="linea_regla" class="text-xs">Line:</Label>
|
||||
<Input id="linea_regla" type="number" min="0" value="0" class="h-7 text-xs text-right" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Consider in a31 -->
|
||||
<div class="flex items-center space-x-2 ">
|
||||
<Checkbox id="a31" />
|
||||
<Label for="a31" class="text-xs font-normal">Consider in A31</Label>
|
||||
</div>
|
||||
|
||||
<!-- Extra Description -->
|
||||
<div class="space-y-1">
|
||||
<Label for="desc_extra_espanol" class="text-xs">Extra Description in Spanish:</Label>
|
||||
<textarea
|
||||
id="desc_extra_espanol"
|
||||
class="flex min-h-[80px] w-full rounded-md border border-input bg-background px-2 py-1 text-xs ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
</script>
|
||||
|
||||
<fieldset class="border rounded-md p-3 space-y-3">
|
||||
<legend class="text-xs font-semibold px-2 uppercase">Identifiers</legend>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="identificador1" class="text-xs">Identifier 1:</Label>
|
||||
<Input id="identificador1" class="h-8 text-sm" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="identificador2" class="text-xs">Identifier 2:</Label>
|
||||
<Input id="identificador2" class="h-8 text-sm" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="identificador3" class="text-xs">Identifier 3:</Label>
|
||||
<Input id="identificador3" class="h-8 text-sm" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="identificador4" class="text-xs">Identifier 4:</Label>
|
||||
<Input id="identificador4" class="h-8 text-sm" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="notas_identificadores" class="text-xs">Notes:</Label>
|
||||
<textarea
|
||||
id="notas_identificadores"
|
||||
class="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
placeholder="Notes about identifiers..."
|
||||
></textarea>
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
</script>
|
||||
|
||||
<fieldset class="border rounded-md p-3 space-y-3">
|
||||
<legend class="text-xs font-semibold px-2 uppercase">Labeling</legend>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="numero_etiqueta" class="text-xs">Label Number:</Label>
|
||||
<Input id="numero_etiqueta" class="h-8 text-sm" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="tipo_etiqueta" class="text-xs">Label Type:</Label>
|
||||
<Input id="tipo_etiqueta" class="h-8 text-sm" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="observaciones_etiqueta" class="text-xs">Observations:</Label>
|
||||
<textarea
|
||||
id="observaciones_etiqueta"
|
||||
class="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
placeholder="Labeling observations..."
|
||||
></textarea>
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
</script>
|
||||
|
||||
<fieldset class="border rounded-md p-3 space-y-3">
|
||||
<legend class="text-xs font-semibold px-2 uppercase">Serial Numbers</legend>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="series" class="text-xs">Serial Numbers:</Label>
|
||||
<textarea
|
||||
id="series"
|
||||
class="flex min-h-[100px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
||||
placeholder="Enter serial numbers, one per line..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-muted-foreground">
|
||||
You can enter multiple serial numbers, one per line
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -0,0 +1,274 @@
|
||||
<script lang="ts">
|
||||
import * as Sheet from '$lib/components/ui/sheet';
|
||||
import * as Tabs from '$lib/components/ui/tabs';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Loader2 } from 'lucide-svelte';
|
||||
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
|
||||
import type { Item } from '$lib/api/dashboard/a76/items';
|
||||
|
||||
let {
|
||||
open = $bindable(),
|
||||
isEditMode = false,
|
||||
editingItem = $bindable(),
|
||||
invoice,
|
||||
onSave,
|
||||
isSaving = false
|
||||
}: {
|
||||
open: boolean;
|
||||
isEditMode?: boolean;
|
||||
editingItem: Partial<Item>;
|
||||
invoice: Invoice | null;
|
||||
onSave: () => void;
|
||||
isSaving?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<Sheet.Root bind:open={open}>
|
||||
<Sheet.Content side="right" class="w-full sm:max-w-2xl overflow-y-auto">
|
||||
<Sheet.Header>
|
||||
<Sheet.Title>{isEditMode ? 'Editar Item' : 'Agregar Nuevo Item'} - SCAII (Inventario)</Sheet.Title>
|
||||
<Sheet.Description>
|
||||
{isEditMode ? 'Modifica los campos del inventario y guarda los cambios.' : 'Completa la información del nuevo item de inventario.'}
|
||||
</Sheet.Description>
|
||||
</Sheet.Header>
|
||||
|
||||
<Tabs.Root value="general" class="mt-6">
|
||||
<Tabs.List class="grid w-full grid-cols-4">
|
||||
<Tabs.Trigger value="general">General</Tabs.Trigger>
|
||||
<Tabs.Trigger value="clasificacion">Clasificación</Tabs.Trigger>
|
||||
<Tabs.Trigger value="cantidades">Cantidades</Tabs.Trigger>
|
||||
<Tabs.Trigger value="otros">Otros</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
<!-- Tab: General -->
|
||||
<Tabs.Content value="general" class="space-y-4 mt-4">
|
||||
<!-- Información de la Factura (Solo lectura) -->
|
||||
<div class="rounded-lg border bg-muted/50 p-4 space-y-3">
|
||||
<h4 class="text-sm font-medium">Información de la Factura (SCAII - Inventario)</h4>
|
||||
{#if !invoice?.id}
|
||||
<div class="text-sm text-amber-600 bg-amber-50 dark:bg-amber-950/20 p-3 rounded">
|
||||
⚠️ Esta factura aún no se ha guardado. Los items se asociarán cuando guardes la factura.
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<span class="text-muted-foreground">ID Factura:</span>
|
||||
<span class="ml-2 font-medium">{invoice.id}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-muted-foreground">Tipo Operación:</span>
|
||||
<span class="ml-2 font-medium uppercase">{invoice.operation_type || 'N/A'}</span>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<span class="text-muted-foreground">Número de Factura:</span>
|
||||
<span class="ml-2 font-medium">{invoice.invoice_number || 'Pendiente'}</span>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<span class="text-muted-foreground">Sistema:</span>
|
||||
<span class="ml-2 font-medium bg-blue-100 dark:bg-blue-900/30 px-2 py-1 rounded">SCAII (Inventory)</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="reference_number">Número de Referencia</Label>
|
||||
<Input id="reference_number" bind:value={editingItem.reference_number} />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="order">Orden de Compra/Venta</Label>
|
||||
<Input
|
||||
id="order"
|
||||
bind:value={editingItem.order}
|
||||
placeholder={invoice?.purchase_order || ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="warehouse">Almacén</Label>
|
||||
<Input id="warehouse" bind:value={editingItem.warehouse} />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="location">Ubicación</Label>
|
||||
<Input id="location" bind:value={editingItem.location} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Campos específicos de SCAII -->
|
||||
<div class="space-y-2">
|
||||
<Label for="product_description">Descripción del Producto</Label>
|
||||
<Input id="product_description" placeholder="Descripción detallada del producto" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="sku">SKU</Label>
|
||||
<Input id="sku" placeholder="Código SKU del producto" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="batch">Lote</Label>
|
||||
<Input id="batch" placeholder="Número de lote" />
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- Tab: Clasificación -->
|
||||
<Tabs.Content value="clasificacion" class="space-y-4 mt-4">
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="tariff_fraction">Fracción Arancelaria</Label>
|
||||
<Input id="tariff_fraction" placeholder="8 dígitos" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="product_type">Tipo de Producto</Label>
|
||||
<Input id="product_type" placeholder="Materia prima, producto terminado, etc." />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="material_type">Tipo de Material</Label>
|
||||
<Input id="material_type" placeholder="Metal, plástico, etc." />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="product_code">Código de Producto</Label>
|
||||
<Input id="product_code" placeholder="Código interno" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="country_origin">País de Origen</Label>
|
||||
<Input id="country_origin" placeholder="Código del país" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="merchandise_category">Categoría de Mercancía</Label>
|
||||
<Input id="merchandise_category" placeholder="Categoría" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- Tab: Cantidades -->
|
||||
<Tabs.Content value="cantidades" class="space-y-4 mt-4">
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="quantity">Cantidad</Label>
|
||||
<Input id="quantity" type="number" placeholder="0" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="unit">Unidad de Medida</Label>
|
||||
<Input id="unit" placeholder="PZA, KG, M, etc." />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="net_weight">Peso Neto (KG)</Label>
|
||||
<Input id="net_weight" type="number" step="0.01" placeholder="0.00" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="gross_weight">Peso Bruto (KG)</Label>
|
||||
<Input id="gross_weight" type="number" step="0.01" placeholder="0.00" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="unit_value">Valor Unitario (USD)</Label>
|
||||
<Input id="unit_value" type="number" step="0.01" placeholder="0.00" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="total_value">Valor Total (USD)</Label>
|
||||
<Input id="total_value" type="number" step="0.01" placeholder="0.00" disabled />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Campos específicos de SCAII -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="packages">Número de Bultos</Label>
|
||||
<Input id="packages" type="number" placeholder="0" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="package_type">Tipo de Empaque</Label>
|
||||
<Input id="package_type" placeholder="Caja, pallet, etc." />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="imported_quantity">Cantidad Importada</Label>
|
||||
<Input id="imported_quantity" type="number" placeholder="0" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="remaining_quantity">Cantidad Remanente</Label>
|
||||
<Input id="remaining_quantity" type="number" placeholder="0" disabled />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- Tab: Otros -->
|
||||
<Tabs.Content value="otros" class="space-y-4 mt-4">
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="brand">Marca</Label>
|
||||
<Input id="brand" placeholder="Marca del producto" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="expiration_date">Fecha de Caducidad</Label>
|
||||
<Input id="expiration_date" type="date" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="production_date">Fecha de Producción</Label>
|
||||
<Input id="production_date" type="date" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="min_stock">Stock Mínimo</Label>
|
||||
<Input id="min_stock" type="number" placeholder="0" />
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="max_stock">Stock Máximo</Label>
|
||||
<Input id="max_stock" type="number" placeholder="0" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="observations">Observaciones</Label>
|
||||
<textarea
|
||||
id="observations"
|
||||
class="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
placeholder="Notas adicionales sobre el inventario..."
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
|
||||
<Sheet.Footer class="mt-6 gap-2">
|
||||
<Button variant="outline" onclick={() => open = false} disabled={isSaving}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button onclick={onSave} disabled={isSaving}>
|
||||
{#if isSaving}
|
||||
<Loader2 class="w-4 h-4 mr-2 animate-spin" />
|
||||
Guardando...
|
||||
{:else}
|
||||
{isEditMode ? 'Guardar Cambios' : 'Agregar Item'}
|
||||
{/if}
|
||||
</Button>
|
||||
</Sheet.Footer>
|
||||
</Sheet.Content>
|
||||
</Sheet.Root>
|
||||
@@ -0,0 +1,377 @@
|
||||
<script lang="ts">
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import { Plus, Pencil, Trash2, Loader2 } from 'lucide-svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import type { Invoice } from '$lib/api/dashboard/a76/invoices';
|
||||
import { itemsApi, type Item } from '$lib/api/dashboard/a76/items';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import ItemSheetFa from './fa/item-sheet-fa.svelte';
|
||||
import ItemSheetInv from './inv/item-sheet-inv.svelte';
|
||||
|
||||
let {
|
||||
invoice,
|
||||
formData = $bindable(),
|
||||
exists = $bindable()
|
||||
}: {
|
||||
invoice: Invoice | null;
|
||||
formData?: any;
|
||||
exists?: boolean;
|
||||
} = $props();
|
||||
|
||||
let imported = 0;
|
||||
let net_weight = 0;
|
||||
let gross_weight = 0;
|
||||
|
||||
let items = $state<Item[]>([]);
|
||||
let displayedItems = $state<Item[]>([]);
|
||||
let itemsPerPage = 20;
|
||||
let currentPage = $state(1);
|
||||
let tableContainer: HTMLDivElement | undefined = $state();
|
||||
let isLoadingMore = $state(false);
|
||||
let isLoadingItems = $state(false);
|
||||
let isSaving = $state(false);
|
||||
|
||||
// Sheet states
|
||||
let showItemSheet = $state(false);
|
||||
let isEditMode = $state(false);
|
||||
let showDeleteDialog = $state(false);
|
||||
let selectedItem = $state<Item | null>(null);
|
||||
let editingItem = $state<Partial<Item>>({
|
||||
invoice_id: undefined,
|
||||
reference_number: '',
|
||||
order: '',
|
||||
warehouse: '',
|
||||
location: ''
|
||||
});
|
||||
|
||||
// Determinar el tipo de sistema (SCAF o SCAII)
|
||||
const invoiceSystem = $derived(invoice?.system || 'scaii'); // Por defecto SCAII si no se especifica
|
||||
|
||||
// Derived value para company ID
|
||||
const activeCompanyId = $derived(companyStore.activeCompany?.id);
|
||||
|
||||
// Cargar items cuando la factura tenga ID
|
||||
$effect(() => {
|
||||
if (invoice?.id && activeCompanyId) {
|
||||
loadItems();
|
||||
}
|
||||
});
|
||||
|
||||
async function loadItems() {
|
||||
if (!invoice?.id || !activeCompanyId) return;
|
||||
|
||||
isLoadingItems = true;
|
||||
try {
|
||||
const response = await itemsApi.listByInvoice(invoice.id, activeCompanyId);
|
||||
if (response.data) {
|
||||
items = response.data.items || [];
|
||||
currentPage = 1;
|
||||
loadMoreItems();
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error loading items:', error);
|
||||
const errorMessage = error?.response?.data?.detail || 'No se pudieron cargar los items de la factura.';
|
||||
toast.error('Error al cargar items', {
|
||||
description: errorMessage
|
||||
});
|
||||
} finally {
|
||||
isLoadingItems = false;
|
||||
}
|
||||
}
|
||||
|
||||
function loadMoreItems() {
|
||||
const start = 0;
|
||||
const end = currentPage * itemsPerPage;
|
||||
displayedItems = items.slice(start, end);
|
||||
isLoadingMore = false;
|
||||
}
|
||||
|
||||
function handleScroll(e: Event) {
|
||||
const target = e.target as HTMLDivElement;
|
||||
const threshold = 100;
|
||||
const scrolledToBottom = target.scrollHeight - target.scrollTop - target.clientHeight < threshold;
|
||||
|
||||
if (scrolledToBottom && !isLoadingMore && displayedItems.length < items.length) {
|
||||
isLoadingMore = true;
|
||||
currentPage++;
|
||||
loadMoreItems();
|
||||
}
|
||||
}
|
||||
|
||||
function handleAdd() {
|
||||
// Validar que la factura esté guardada (tiene ID)
|
||||
if (!invoice?.id) {
|
||||
toast.warning('Factura no guardada', {
|
||||
description: 'Debes guardar la factura primero antes de agregar partidas.',
|
||||
duration: 5000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
isEditMode = false;
|
||||
showItemSheet = true;
|
||||
// Auto-asignar valores desde la factura
|
||||
editingItem = {
|
||||
invoice_id: invoice.id,
|
||||
reference_number: '',
|
||||
order: invoice.purchase_order || '',
|
||||
warehouse: '',
|
||||
location: ''
|
||||
};
|
||||
}
|
||||
|
||||
function handleEdit(item: Item) {
|
||||
isEditMode = true;
|
||||
selectedItem = item;
|
||||
editingItem = { ...item };
|
||||
showItemSheet = true;
|
||||
}
|
||||
|
||||
function handleDelete(item: Item) {
|
||||
selectedItem = item;
|
||||
showDeleteDialog = true;
|
||||
}
|
||||
|
||||
async function saveNewItem() {
|
||||
if (!invoice?.id || !activeCompanyId) return;
|
||||
|
||||
isSaving = true;
|
||||
try {
|
||||
const response = await itemsApi.create(activeCompanyId, {
|
||||
invoice_id: invoice.id,
|
||||
reference_number: editingItem.reference_number,
|
||||
order: editingItem.order,
|
||||
warehouse: editingItem.warehouse,
|
||||
location: editingItem.location
|
||||
});
|
||||
|
||||
// Recargar items
|
||||
await loadItems();
|
||||
|
||||
showItemSheet = false;
|
||||
toast.success('Item creado', {
|
||||
description: 'El item se ha creado correctamente.'
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Error creating item:', error);
|
||||
const errorMessage = error?.response?.data?.detail || 'No se pudo crear el item. Intenta de nuevo.';
|
||||
toast.error('Error al crear item', {
|
||||
description: errorMessage
|
||||
});
|
||||
} finally {
|
||||
isSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveEditedItem() {
|
||||
if (!selectedItem?.id || !activeCompanyId) return;
|
||||
|
||||
isSaving = true;
|
||||
try {
|
||||
await itemsApi.update(selectedItem.id, activeCompanyId, {
|
||||
reference_number: editingItem.reference_number,
|
||||
order: editingItem.order,
|
||||
warehouse: editingItem.warehouse,
|
||||
location: editingItem.location
|
||||
});
|
||||
|
||||
// Recargar items
|
||||
await loadItems();
|
||||
|
||||
showItemSheet = false;
|
||||
toast.success('Item actualizado', {
|
||||
description: 'El item se ha actualizado correctamente.'
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Error updating item:', error);
|
||||
const errorMessage = error?.response?.data?.detail || 'No se pudo actualizar el item. Intenta de nuevo.';
|
||||
toast.error('Error al actualizar item', {
|
||||
description: errorMessage
|
||||
});
|
||||
} finally {
|
||||
isSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
function saveItem() {
|
||||
if (isEditMode) {
|
||||
saveEditedItem();
|
||||
} else {
|
||||
saveNewItem();
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmDelete() {
|
||||
if (!selectedItem?.id || !activeCompanyId) return;
|
||||
|
||||
isSaving = true;
|
||||
try {
|
||||
await itemsApi.delete(selectedItem.id, activeCompanyId);
|
||||
|
||||
// Recargar items
|
||||
await loadItems();
|
||||
|
||||
showDeleteDialog = false;
|
||||
toast.success('Item eliminado', {
|
||||
description: 'El item se ha eliminado correctamente.'
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Error deleting item:', error);
|
||||
const errorMessage = error?.response?.data?.detail || 'No se pudo eliminar el item. Intenta de nuevo.';
|
||||
toast.error('Error al eliminar item', {
|
||||
description: errorMessage
|
||||
});
|
||||
} finally {
|
||||
isSaving = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-4 grid-rows-1 gap-3">
|
||||
<div class="border rounded-md p-3 space-y-3 col-span-3">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<h3 class="text-sm font-semibold">Items de la Factura</h3>
|
||||
<Button size="sm" onclick={handleAdd}>
|
||||
<Plus class="w-4 h-4 mr-1" />
|
||||
Agregar Item
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
bind:this={tableContainer}
|
||||
onscroll={handleScroll}
|
||||
class="max-h-[500px] overflow-auto border rounded-md"
|
||||
>
|
||||
<Table.Root>
|
||||
<Table.Header class="sticky top-0 bg-background z-10">
|
||||
<Table.Row>
|
||||
<Table.Head>Referencia</Table.Head>
|
||||
<Table.Head>Orden</Table.Head>
|
||||
<Table.Head>Almacén</Table.Head>
|
||||
<Table.Head>Ubicación</Table.Head>
|
||||
<Table.Head class="text-right w-[120px]">Acciones</Table.Head>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{#if displayedItems.length === 0}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={7} class="text-center text-muted-foreground py-8">
|
||||
No hay items disponibles
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{:else}
|
||||
{#each displayedItems as item (item.id)}
|
||||
<Table.Row>
|
||||
<Table.Cell>{item.reference_number || '-'}</Table.Cell>
|
||||
<Table.Cell>{item.order || '-'}</Table.Cell>
|
||||
<Table.Cell>{item.warehouse || '-'}</Table.Cell>
|
||||
<Table.Cell>{item.location || '-'}</Table.Cell>
|
||||
<Table.Cell class="text-right">
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button size="icon" variant="ghost" onclick={() => handleEdit(item)}>
|
||||
<Pencil class="w-4 h-4" />
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" onclick={() => handleDelete(item)}>
|
||||
<Trash2 class="w-4 h-4 text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/each}
|
||||
{#if isLoadingMore}
|
||||
<Table.Row>
|
||||
<Table.Cell colspan={7} class="text-center py-4">
|
||||
<span class="text-sm text-muted-foreground">Cargando más items...</span>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
{/if}
|
||||
{/if}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
|
||||
{#if items.length > 0}
|
||||
<div class="text-xs text-muted-foreground text-right">
|
||||
Mostrando {displayedItems.length} de {items.length} items
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="border rounded-md p-3 space-y-3 col-start-4">
|
||||
<div>
|
||||
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Cantidades:</h4>
|
||||
<div>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
Partidas: <span class="text-blue-400">{items.length || 0}</span>
|
||||
</div>
|
||||
<div>
|
||||
Bultos: <span class="text-blue-400">0</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Importada: <span class="text-blue-400">{imported || 0}</span> <br>
|
||||
Peso neto: <span class="text-blue-400">{net_weight || 0}</span><br>
|
||||
Peso bruto: <span class="text-blue-400">{gross_weight || 0}</span> <br>
|
||||
</div>
|
||||
|
||||
<h4 class="text-xs font-semibold text-muted-foreground uppercase col-span-2">Valores de importacion:</h4>
|
||||
Dolares: <span class="text-blue-400">0</span> <span class="text-red-400">USD</span> <br>
|
||||
Pesos: <span class="text-blue-400">0</span> <span class="text-red-400">MXN</span><br>
|
||||
De Captura: <span class="text-blue-400">0</span> <span class="text-red-400">USD</span>
|
||||
|
||||
<h4 class="text-xs font-semibold text-muted-foreground uppercase col-span-2 opacity-0">spacer</h4>
|
||||
|
||||
Aduana: <span class="text-blue-400">0</span> <span class="text-red-400">USD</span><br>
|
||||
Aduana: <span class="text-blue-400">0</span> <span class="text-red-400">MXN</span><br>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Item Sheet (Panel lateral para agregar/editar) -->
|
||||
<!-- Renderizar el componente apropiado según el sistema -->
|
||||
{#if invoiceSystem === 'fixed_asset'}
|
||||
<ItemSheetFa
|
||||
bind:open={showItemSheet}
|
||||
{isEditMode}
|
||||
bind:editingItem={editingItem}
|
||||
{invoice}
|
||||
onSave={saveItem}
|
||||
{isSaving}
|
||||
/>
|
||||
{:else}
|
||||
<ItemSheetInv
|
||||
bind:open={showItemSheet}
|
||||
{isEditMode}
|
||||
bind:editingItem={editingItem}
|
||||
{invoice}
|
||||
onSave={saveItem}
|
||||
{isSaving}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Delete Confirmation Dialog -->
|
||||
<Dialog.Root bind:open={showDeleteDialog}>
|
||||
<Dialog.Content>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Confirmar Eliminación</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
¿Está seguro que desea eliminar este item? Esta acción no se puede deshacer.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<Dialog.Footer>
|
||||
<Button variant="outline" onclick={() => showDeleteDialog = false} disabled={isSaving}>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button variant="destructive" onclick={confirmDelete} disabled={isSaving}>
|
||||
{#if isSaving}
|
||||
<Loader2 class="w-4 h-4 mr-2 animate-spin" />
|
||||
Eliminando...
|
||||
{:else}
|
||||
Eliminar
|
||||
{/if}
|
||||
</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
import { Toaster } from 'svelte-sonner';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
@@ -9,4 +10,5 @@
|
||||
<link rel="icon" href={favicon} />
|
||||
</svelte:head>
|
||||
|
||||
<Toaster richColors position="top-right" />
|
||||
{@render children?.()}
|
||||
|
||||
@@ -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 || []
|
||||
|
||||
@@ -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 || []
|
||||
|
||||
@@ -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 || [],
|
||||
|
||||
@@ -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 @@
|
||||
|
||||
<!-- Footer fijo en la parte inferior -->
|
||||
<div
|
||||
class="fixed bottom-0 left-0 right-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 border-t shadow-lg z-[5]"
|
||||
class="fixed bottom-0 left-0 right-0 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 border-t shadow-lg z-[5] group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] ml-[calc(var(--sidebar-width))]"
|
||||
>
|
||||
<div class="px-4 py-4 space-y-4 max-w-[1400px] mx-auto">
|
||||
<!-- Tabs Navigation -->
|
||||
|
||||
@@ -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 || [],
|
||||
|
||||
Reference in New Issue
Block a user