Files
plantillas-proyectos/backend/api/v1/modules/a76/items/schemas.py

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)