402 lines
15 KiB
Python
402 lines
15 KiB
Python
"""
|
|
Schemas for Items and related entities
|
|
Complete nested one-to-one structure:
|
|
LineItem -> LineFinancial -> LineQuantity -> LineCustoms -> LineDescription -> LineReference
|
|
"""
|
|
|
|
from typing import Any, Optional, Union
|
|
from datetime import datetime
|
|
from decimal import Decimal
|
|
from pydantic import BaseModel, Field, ConfigDict, model_validator
|
|
|
|
# Import schemas from individual modules
|
|
# 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,
|
|
)
|
|
|
|
from api.v1.modules.a76.general_catalogs.identifiers.dto import (
|
|
IdentifierDetailCreate,
|
|
IdentifierDetailUpdate,
|
|
IdentifierDetailResponse,
|
|
)
|
|
from api.v1.modules.a76.classes.models import Class
|
|
from api.v1.modules.a24.fa.fa_item_lines.dto import (
|
|
FaLineItemCreateDTO,
|
|
FaLineItemUpdateDTO,
|
|
FaLineItemResponseDTO,
|
|
)
|
|
from .series.schemas import (
|
|
SerieCreate,
|
|
SerieUpdate,
|
|
SerieResponse,
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# ITEM SCHEMAS
|
|
# ============================================================================
|
|
|
|
|
|
class LineItemBase(BaseModel):
|
|
"""Base schema for items"""
|
|
|
|
model_config = ConfigDict(populate_by_name=True)
|
|
|
|
invoice_id: int = Field(..., description="Invoice ID")
|
|
line_number: int = Field(..., description="Line number")
|
|
|
|
# Part identification
|
|
part_number_id: Union[int, str, None] = Field(
|
|
None,
|
|
description="Part number",
|
|
alias="part_number",
|
|
serialization_alias="part_number_id",
|
|
)
|
|
component_part_number_id: Union[int, str, None] = Field(
|
|
None,
|
|
description="Component part number",
|
|
alias="component_part_number",
|
|
serialization_alias="component_part_number_id",
|
|
)
|
|
class_id: Optional[int] = Field(None, description="Class code")
|
|
|
|
# Unit of measure
|
|
unit_of_measure: Optional[int] = Field(None, description="Unit of measure")
|
|
alternate_unit: Optional[int] = Field(None, 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")
|
|
|
|
# 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"
|
|
)
|
|
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[datetime] = 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 LineItemCreate(LineItemBase):
|
|
"""Schema for creating item with nested lines (one-to-many)"""
|
|
|
|
# Override base fields - estos se asignan automáticamente en el service
|
|
line_number: Optional[int] = Field(None, description="Line number (auto-assigned)")
|
|
|
|
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"
|
|
)
|
|
fa_data: Optional[FaLineItemCreateDTO] = Field(
|
|
None, description="Fixed Asset data for this line"
|
|
)
|
|
series: Optional[list[SerieCreate]] = Field(
|
|
None, description="Series data for this line (multiple per line)"
|
|
)
|
|
identifiers: Optional[list[IdentifierDetailCreate]] = Field(
|
|
None, description="Identifiers for this line"
|
|
)
|
|
|
|
|
|
class LineItemUpdate(LineItemBase):
|
|
"""Schema for updating item"""
|
|
|
|
invoice_id: Optional[int] = Field(None, description="Invoice ID")
|
|
# Override base fields - todos opcionales en updates
|
|
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"
|
|
)
|
|
fa_data: Optional[FaLineItemUpdateDTO] = Field(
|
|
None, description="Fixed Asset data for this line"
|
|
)
|
|
series: Optional[list[SerieUpdate]] = Field(
|
|
None, description="Series data for this line (replace all)"
|
|
)
|
|
identifiers: Optional[list[IdentifierDetailUpdate]] = Field(
|
|
None, description="Identifiers for this line"
|
|
)
|
|
|
|
|
|
class LineItemResponse(LineItemBase):
|
|
"""Schema for single item response"""
|
|
|
|
id: int
|
|
invoice_id: int
|
|
line_number: int
|
|
|
|
# Part identification
|
|
part_number_id: Optional[int] = Field(
|
|
None, alias="part_number_id_input", serialization_alias="part_number_id"
|
|
)
|
|
part_number: Optional[str] = None
|
|
component_part_number_id: Optional[int] = Field(
|
|
None,
|
|
alias="component_part_number_id_input",
|
|
serialization_alias="component_part_number_id",
|
|
)
|
|
component_part_number: Optional[str] = None
|
|
class_id: Optional[int] = None
|
|
|
|
# Nested data
|
|
financial: Optional[LineFinancialResponse] = None
|
|
quantity: Optional[LineQuantityResponse] = None
|
|
customs: Optional[LineCustomResponse] = None
|
|
description: Optional[LineDescriptionResponse] = None
|
|
reference: Optional[LineReferenceResponse] = None
|
|
fa_data: Optional[FaLineItemResponseDTO] = None
|
|
series: Optional[list[SerieResponse]] = None
|
|
identifiers: Optional[list[IdentifierDetailResponse]] = None
|
|
|
|
# Fields populated from relationships
|
|
class_code: Optional[str] = None
|
|
class_description: Optional[str] = None
|
|
unit_of_measure_code: Optional[str] = None
|
|
|
|
# Additional fields that might be present
|
|
reference_number: Optional[str] = None
|
|
order: Optional[str] = None
|
|
warehouse: Optional[str] = None
|
|
location: Optional[str] = None
|
|
|
|
model_config = ConfigDict(from_attributes=True, populate_by_name=True)
|
|
|
|
@model_validator(mode="before")
|
|
@classmethod
|
|
def extract_relationship_info(cls, data: Any) -> Any:
|
|
"""Extract information from joined relationships to provide flat mapping for UI."""
|
|
if isinstance(data, dict):
|
|
# If already a dict, ensure description syncs to top-level if missing
|
|
desc = data.get("description", {})
|
|
if isinstance(desc, dict):
|
|
if not data.get("part_description_es"):
|
|
data["part_description_es"] = desc.get("description_spanish")
|
|
if not data.get("part_description_en"):
|
|
data["part_description_en"] = desc.get("description_english")
|
|
return data
|
|
|
|
# It's an ORM object
|
|
result = {}
|
|
|
|
# 1. Start with model attributes (columns)
|
|
if hasattr(data, "__table__"):
|
|
for k in data.__table__.columns.keys():
|
|
result[k] = getattr(data, k, None)
|
|
else:
|
|
# Fallback for non-table objects if any
|
|
for k, v in data.__dict__.items():
|
|
if not k.startswith("_"):
|
|
result[k] = v
|
|
|
|
# Alias mapping for part numbers
|
|
if hasattr(data, "part_number_id") and "part_number_id" not in result:
|
|
result["part_number_id"] = data.part_number_id
|
|
if hasattr(data, "component_part_number_id") and "component_part_number_id" not in result:
|
|
result["component_part_number_id"] = data.component_part_number_id
|
|
|
|
# Extract part info (string part numbers) from relationship objects
|
|
if hasattr(data, "part_info") and data.part_info is not None:
|
|
result["part_number"] = getattr(data.part_info, "part_number", None)
|
|
if hasattr(data, "component_part_info") and data.component_part_info is not None:
|
|
result["component_part_number"] = getattr(data.component_part_info, "part_number", None)
|
|
|
|
# Extract class info
|
|
if hasattr(data, "class_info") and data.class_info is not None:
|
|
result["class_code"] = getattr(data.class_info, "class_code", None)
|
|
result["class_description"] = getattr(data.class_info, "description_es", None)
|
|
|
|
# Extract unit of measure code
|
|
if hasattr(data, "unit_of_measure_info") and data.unit_of_measure_info is not None:
|
|
result["unit_of_measure_code"] = getattr(data.unit_of_measure_info, "code", None)
|
|
|
|
# 2. Extract nested objects and populate redundant descriptions
|
|
# We MUST use explicit getattr for relationships to ensure SQLAlchemy loads/uses joined-loaded ones
|
|
for key in ["financial", "quantity", "customs", "description", "reference", "fa_data", "series", "identifiers"]:
|
|
val = getattr(data, key, None)
|
|
if val is not None:
|
|
result[key] = val
|
|
# Sync to top-level for description redundancy (huge boost for UI stability)
|
|
if key == "description":
|
|
result["part_description_es"] = getattr(val, "description_spanish", None)
|
|
result["part_description_en"] = getattr(val, "description_english", None)
|
|
else:
|
|
# Provide default empty dict for core containers to help frontend
|
|
if key in ["financial", "quantity", "customs", "description"]:
|
|
result[key] = {}
|
|
|
|
return result
|
|
|
|
|
|
class LineItemListResponse(BaseModel):
|
|
"""Schema for paginated item list"""
|
|
|
|
total: int = Field(..., description="Total number of items")
|
|
items: list[LineItemResponse] = 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)
|