from typing import Literal, Optional, List from datetime import datetime, date from decimal import Decimal from pydantic import BaseModel, Field, field_validator from .models import ( DestinationOriginCove, InvoiceStatus, OperationType, Currency, TransportType, WeightUnit, ) # --- Base Schemas --- class InvoiceHeaderBase(BaseModel): """Base fields for Invoice Header""" system: Optional[str] = Field(None, max_length=12, description="System of origin") operation_type: Optional[OperationType] = Field( ..., description="Operation type: imp/exp/sm/ctm" ) @field_validator("operation_type", mode="before") @classmethod def normalize_operation_type(cls, v): """Accept DB string (e.g. 'IMP') and coerce to enum value ('imp').""" if isinstance(v, str): return v.lower() if v else v return v invoice_type: Optional[str] = Field( None, max_length=5, description="Invoice type key" ) document_type: Optional[str] = Field( None, max_length=3, description="Document type (Regimen Aduanero)" ) invoice_number: Optional[str] = Field( None, max_length=100, description="Invoice number" ) project_number: Optional[str] = Field( None, max_length=14, description="Project number" ) purchase_order: Optional[str] = Field( None, max_length=50, description="Purchase order" ) related_doc_id: Optional[int] = Field( None, description="Related document ID for rectifications" ) alternate_invoice: Optional[str] = Field( None, max_length=99, description="Alternate invoice" ) invoice_ref: Optional[str] = Field( None, max_length=19, description="Invoice reference" ) proforma_number: Optional[str] = Field( None, max_length=20, description="Proforma number" ) invoice_date: date = Field(..., description="Invoice date") emission_date: Optional[date] = Field(None, description="Emission date") status: Optional[InvoiceStatus] = Field(None, description="Status: pending, processed, reversed") @field_validator("status", mode="before") @classmethod def normalize_status(cls, v): """Coerce legacy boolean strings ('false'/'true') to InvoiceStatus.""" if v is None: return v if isinstance(v, str): v_lower = v.lower() if v_lower == "false": return InvoiceStatus.PENDING.value if v_lower == "true": return InvoiceStatus.PROCESSED.value return v processed_date: Optional[datetime] = Field(None, description="Update date") who_processed: Optional[str] = Field(None, max_length=20, description="Who processed") capture_user: Optional[str] = Field(None, max_length=20, description="Capture user") traffic_light_status: Optional[str] = Field( None, max_length=50, description="Traffic light status" ) process_log: Optional[str] = Field( None, max_length=300, description="Processing log" ) status_rec: Optional[int] = Field(None, description="Reception status") status_rep: Optional[str] = Field( None, max_length=2, description="Report status" ) observation_es: Optional[str] = Field(None, description="Observations in Spanish") observation_en: Optional[str] = Field(None, description="Observations in English") comments_status: Optional[str] = Field(None, description="Comments status") vu_observations: Optional[str] = Field( None, max_length=500, description="VUCEM observations" ) cfdi_uuid: Optional[str] = Field(None, max_length=100, description="CFDI UUID") path_pdf: Optional[str] = Field( None, max_length=500, description="Path to PDF file" ) path_xml: Optional[str] = Field( None, max_length=500, description="Path to XML file" ) subcompany: Optional[str] = Field(None, max_length=5, description="Subcompany") party_count: Optional[int] = Field(None, description="Quantity of parties") generate_id: Optional[bool] = Field(False, description="Generate ID") generate_desc_parties: Optional[str] = Field( None, max_length=12, description="Generate description of parties" ) apply_manual_discount: Optional[bool] = Field( False, description="Apply manual discount" ) is_bulk: Optional[bool] = Field(None, description="Is bulk") download_substance: Optional[bool] = Field(None, description="Download substance") download_class: Optional[bool] = Field(None, description="Download class") download_def: Optional[bool] = Field(None, description="Definitive download") payment_terms: Optional[str] = Field( None, max_length=200, description="Payment terms" ) handling_fees: Optional[Decimal] = Field(None, description="Handling fees") option_iv18: Optional[str] = Field(None, max_length=50, description="Option IV18") enajenation_goods: Optional[bool] = Field(None, description="Enajenation of goods") class InvoiceComplianceMxBase(BaseModel): """Base fields for Compliance MX""" pedimento_id: Optional[int] = Field(None, description="Pedimento id") pedimento_r1: Optional[int] = Field(None, description="Pedimento id (R1)") pedimento_k1: Optional[int] = Field(None, description="Pedimento id (K1)") remesa: Optional[int] = Field(None, description="Remesa") aduana: Optional[str] = Field(None, max_length=5, description="Customs office") port_of_entry: Optional[str] = Field( None, max_length=6, description="Port of entry" ) destination: Optional[str] = Field( None, max_length=3, description="Destination code" ) manifest_number: Optional[str] = Field( None, max_length=15, description="Manifest number" ) provider_header: Optional[str] = Field(None, max_length=20, description="Provider header") provider_id: Optional[int] = Field(None, description="Provider ID") sold_to_header: Optional[str] = Field(None, max_length=20, description="Sold to header") sold_to_id: Optional[int] = Field(None, description="Sold to ID") shipped_to_header: Optional[str] = Field(None, max_length=20, description="Shipped to header") shipped_to_id: Optional[int] = Field(None, description="Shipped to ID") shipped_by_header: Optional[str] = Field( None, max_length=20, description="Shipped by header" ) shipped_by_id: Optional[int] = Field(None, description="Shipped by ID") customs_broker_id: Optional[int] = Field(None, description="Customs broker ID (null for MEX)") customs_broker_us_id: Optional[int] = Field( None, description="US customs broker ID" ) broker_invoice_num: Optional[str] = Field( None, max_length=20, description="Broker invoice number" ) broker_invoice_date: Optional[date] = Field(None, description="Broker invoice date") is_mixed: Optional[bool] = Field(False, description="Is mixed operation") waste_type: Optional[str] = Field(None, max_length=1, description="Waste type") scrap_type: Optional[str] = Field(None, max_length=1, description="Scrap type") appendix_17: Optional[int] = Field(None, description="Appendix 17") is_regime_change: Optional[bool] = Field(False, description="Is regime change") which_exchange_rate: Optional[str] = Field( None, max_length=5, description="Which exchange rate" ) rule_3121_parties_ii: Optional[bool] = Field( False, description="Is Rule 3.1.21 Parties II" ) value_method: Optional[str] = Field(None, max_length=2, description="Value method") act_value: Optional[str] = Field(None, max_length=5, description="Act value") is_pedimento_pending: Optional[bool] = Field(False, description="Is pedimento pending") is_owner_of_goods: Optional[bool] = Field(False, description="Is owner of goods") generate_balances: Optional[bool] = Field(False, description="Generate balances") was_reviewed_by_company: Optional[bool] = Field( None, description="Was reviewed by company" ) edocument: Optional[str] = Field(None, max_length=50, description="E-document") electronic_signature: Optional[str] = Field( None, max_length=999, description="Electronic signature" ) certificate_number: Optional[str] = Field( None, max_length=99, description="Certificate number" ) niu_number: Optional[str] = Field(None, max_length=19, description="NIU number") bill_of_lading_count: Optional[str] = Field( None, max_length=12, description="Bill of lading count" ) addendum_vu: Optional[str] = Field( None, max_length=204, description="VUCEM addendum" ) origin_destination_cove: Optional[DestinationOriginCove] = Field( "franja_front_norte", max_length=20, description="Origin/Destination COVE" ) vucem_operation_num: Optional[str] = Field( None, max_length=19, description="VUCEM operation number" ) customs_person_line: Optional[int] = Field(None, description="Customs person line") contingency_mode: Optional[bool] = Field(None, description="Contingency mode") enclosure: Optional[str] = Field(None, max_length=4, description="Enclosure") guide_type_to_identify: Optional[str] = Field( None, max_length=1, description="Guide type to identify" ) location: Optional[str] = Field(None, max_length=200, description="Location") dot_code: Optional[str] = Field(None, max_length=20, description="DOT code") subdivision: Optional[str] = Field(None, max_length=20, description="Subdivision") acts_as: Optional[str] = Field(None, max_length=20, description="Acts as") movement_type: Optional[str] = Field( None, max_length=31, description="Movement type" ) office_document: Optional[str] = Field( None, max_length=30, description="Office document" ) reason_export: Optional[str] = Field( None, max_length=1, description="Reason for export" ) signature_key: Optional[str] = Field( None, max_length=100, description="Signature key" ) sem_id: Optional[int] = Field(None, description="SEM ID") class InvoiceFinancialsBase(BaseModel): """Base fields for Financials""" currency: Optional[Currency] = Field(None, max_length=7, description="Currency code") currency_type: Optional[str] = Field("USD", description="Currency type") exchange_rate: Optional[Decimal] = Field(0.00, description="Exchange rate") exchange_rate_mm: Optional[Decimal] = Field( 0.00, description="Exchange rate currency to currency" ) value_mn: Optional[Decimal] = Field(0.00, description="Value in MXN") value_me: Optional[Decimal] = Field(0.00, description="Value in foreign currency") value_mc: Optional[Decimal] = Field(0.00, description="Value in third currency") customs_value_mn: Optional[Decimal] = Field( 0.00, description="Customs value in MXN" ) customs_value_me: Optional[Decimal] = Field( 0.00, description="Customs value in foreign currency" ) raw_material_value_mn: Optional[Decimal] = Field( 0.00, description="Raw material value in MXN" ) raw_material_value_me: Optional[Decimal] = Field( 0.00, description="Raw material value in foreign currency" ) aggregate_value_mn: Optional[Decimal] = Field( 0.00, description="Aggregate value in MXN" ) aggregate_value_me: Optional[Decimal] = Field( 0.00, description="Aggregate value in foreign currency" ) aggregate_value_mc: Optional[Decimal] = Field( 0.00, description="Aggregate value in third currency" ) mexican_value_mn: Optional[Decimal] = Field( 0.00, description="Mexican merchandise value in MXN" ) mexican_value_me: Optional[Decimal] = Field( 0.00, description="Mexican merchandise value in foreign currency" ) mexican_value_mc: Optional[Decimal] = Field( 0.00, description="Mexican merchandise value in third currency" ) national_packaging_mn: Optional[Decimal] = Field( 0.00, description="National packaging in MXN" ) national_packaging_me: Optional[Decimal] = Field( 0.00, description="National packaging in foreign currency" ) national_packaging_mc: Optional[Decimal] = Field( 0.00, description="National packaging in third currency" ) freight: Optional[Decimal] = Field(0.00, description="Freight cost") insurance: Optional[Decimal] = Field(0.00, description="Insurance cost") insurance_value: Optional[Decimal] = Field(0.00, description="Insurance value") packaging: Optional[Decimal] = Field(0.00, description="Packaging") other_increments: Optional[Decimal] = Field(0.00, description="Other increments") other_deductibles: Optional[Decimal] = Field(0.00, description="Other deductibles") total_increments_mn: Optional[Decimal] = Field( 0.00, description="Total increments in MXN" ) total_increments_me: Optional[Decimal] = Field( 0.00, description="Total increments in foreign currency" ) iva_mn: Optional[Decimal] = Field(0.00, description="IVA in MXN") iva_me: Optional[Decimal] = Field(0.00, description="IVA in foreign currency") iva_mc: Optional[Decimal] = Field(0.00, description="IVA in third currency") iva_factor: Optional[Decimal] = Field(0.00, description="IVA factor") tax_value_me: Optional[Decimal] = Field( 0.00, description="Tax value in foreign currency" ) seal_value_2500: Optional[bool] = Field(None, description="Seal value 2500") total_quantity: Optional[Decimal] = Field(0.00, description="Total quantity") total_packages: Optional[int] = Field(0, description="Total packages") gross_weight: Optional[Decimal] = Field(0.00, description="Gross weight") net_weight: Optional[Decimal] = Field(0.00, description="Net weight") bundle_count: Optional[int] = Field(0, description="Bundle count") weight_factor: Optional[Decimal] = Field(0.00, description="Weight factor") class InvoiceLogisticsBase(BaseModel): """Base fields for Logistics""" carrier_id: Optional[str] = Field(None, max_length=10, description="Carrier code (front-end key)") carrier_int_id: Optional[int] = Field( None, description="Internal carrier integer ID (mapped from carrier_id key)" ) transport_id: Optional[str] = Field(None, max_length=10, description="Transport ID") transport_int_id: Optional[int] = Field( None, description="Internal vehicle integer ID (mapped from transport_id key)" ) transport_us_id: Optional[str] = Field( None, max_length=10, description="US transport ID" ) transport_type: TransportType = Field( "none", max_length=15, description="Transport type" ) transport_num: Optional[str] = Field( None, max_length=20, description="Transport number" ) transport_mode: Optional[str] = Field( 30, max_length=15, description="Transport mode" ) driver_name: Optional[str] = Field(None, max_length=80, description="Driver name") is_rail: Optional[bool] = Field(False, description="Is rail transport") rail_id: Optional[str] = Field(None, max_length=31, description="Rail ID") vehicle_num: Optional[str] = Field( None, max_length=20, description="Vehicle number" ) license_plate: Optional[str] = Field( None, max_length=20, description="License plate" ) license_plate_complete: Optional[str] = Field( None, max_length=40, description="Complete license plate" ) trailer_num: Optional[str] = Field( None, max_length=20, description="Trailer number" ) trailer_int_id: Optional[int] = Field( None, description="Internal trailer integer ID (mapped from trailer_num key)" ) seal_number: Optional[str] = Field(None, max_length=15, description="Seal number") guide_number: Optional[str] = Field(None, max_length=20, description="Guide number") bill_number: Optional[str] = Field(None, max_length=15, description="Bill number") reference_number: Optional[str] = Field( None, max_length=14, description="Reference number" ) shipment_number: Optional[str] = Field( None, max_length=19, description="Shipment number" ) incoterm: Optional[str] = Field(None, max_length=5, description="Incoterm") identifier_1: Optional[str] = Field(None, max_length=2, description="Identifier 1") complement_1: Optional[str] = Field(None, max_length=30, description="Complement 1") identifier_2: Optional[str] = Field(None, max_length=2, description="Identifier 2") complement_2: Optional[str] = Field(None, max_length=30, description="Complement 2") weight_type: WeightUnit = Field( default="kgs", max_length=3, description="Weight type" ) @field_validator("weight_type", mode="before") @classmethod def normalize_weight_type(cls, v): if isinstance(v, str): return v.lower() return v container_types: Optional[str] = Field( None, max_length=500, description="Container types" ) vehicle_data: Optional[str] = Field( None, max_length=500, description="Vehicle data" ) origin_location: Optional[str] = Field( None, max_length=200, description="Origin location" ) destination_location: Optional[str] = Field( None, max_length=200, description="Destination location" ) transport_itinerary: Optional[str] = Field( None, max_length=1000, description="Transport itinerary" ) destination_goods: Optional[str] = Field( None, max_length=50, description="Destination of goods" ) entry_exit_date: Optional[date] = Field(None, description="Entry/Exit date") delivery_date: Optional[date] = Field(None, description="Delivery date") delivered_status: Optional[bool] = Field(False, description="Delivered status") received_by: Optional[str] = Field(None, max_length=50, description="Received by") payment_date: Optional[date] = Field(None, description="Payment date") payment_receipt_num: Optional[str] = Field( None, max_length=20, description="Payment receipt number" ) is_ctm_process: Optional[bool] = Field(False, description="Is CTM process") # Continuation Tab Fields equipment_reviewed: Optional[bool] = Field(False, description="Equipment reviewed") is_subdivision: Optional[bool] = Field(False, description="Is subdivision") acts_as_cd: Optional[bool] = Field(False, description="Acts as CD") pedimento_arrived: Optional[bool] = Field(False, description="Pedimento arrived") green_light_mx: Optional[bool] = Field(False, description="Green light Mexico") green_light_us: Optional[bool] = Field(False, description="Green light USA") red_light_mx: Optional[bool] = Field(False, description="Red light Mexico") red_light_us: Optional[bool] = Field(False, description="Red light USA") class InvoiceSalesDetailsBase(BaseModel): """Base fields for Sales Details""" line_number: int = Field(..., description="Line number") sales_order: Optional[str] = Field(None, max_length=20, description="Sales order") colors_description: Optional[str] = Field( None, max_length=49, description="Colors description" ) square_color_code: Optional[str] = Field( None, max_length=1, description="Square color code" ) line_bundles: Optional[int] = Field(None, description="Line bundles count") class InvoiceCollectionsBase(BaseModel): """Base fields for Collections""" line_number: int = Field(..., description="Line number") invoice_number: Optional[str] = Field( None, max_length=15, description="Invoice number" ) concept: Optional[str] = Field(None, max_length=100, description="Concept") # --- Create Schemas --- class InvoiceComplianceMxCreate(InvoiceComplianceMxBase): """Schema for creating Compliance MX""" pass class InvoiceFinancialsCreate(InvoiceFinancialsBase): """Schema for creating Financials""" pass class InvoiceLogisticsCreate(InvoiceLogisticsBase): """Schema for creating Logistics""" pass class InvoiceSalesDetailsCreate(InvoiceSalesDetailsBase): """Schema for creating Sales Details""" pass class InvoiceCollectionsCreate(InvoiceCollectionsBase): """Schema for creating Collections""" pass class InvoiceHeaderCreate(InvoiceHeaderBase): """Schema for creating Invoice Header with nested relations""" status: Optional[InvoiceStatus] = Field(InvoiceStatus.PENDING, description="Status: pending, processed, reversed") compliance_mx: Optional[InvoiceComplianceMxCreate] = None financials: Optional[InvoiceFinancialsCreate] = None logistics: Optional[InvoiceLogisticsCreate] = None details: Optional[List[InvoiceSalesDetailsCreate]] = None collections: Optional[List[InvoiceCollectionsCreate]] = None # --- Update Schemas --- class InvoiceComplianceMxUpdate(InvoiceComplianceMxBase): """Schema for updating Compliance MX""" is_pedimento_pending: Optional[bool] = None class InvoiceFinancialsUpdate(InvoiceFinancialsBase): """Schema for updating Financials""" pass class InvoiceLogisticsUpdate(InvoiceLogisticsBase): """Schema for updating Logistics""" pass class InvoiceSalesDetailsUpdate(InvoiceSalesDetailsBase): """Schema for updating Sales Details""" line_number: Optional[int] = None class InvoiceCollectionsUpdate(InvoiceCollectionsBase): """Schema for updating Collections""" line_number: Optional[int] = None class InvoiceHeaderUpdate(InvoiceHeaderBase): """Schema for updating Invoice Header with nested relations""" id: int document_type: Optional[str] = None invoice_date: Optional[date] = None operation_type: Optional[OperationType] = None compliance_mx: Optional[InvoiceComplianceMxUpdate] = None financials: Optional[InvoiceFinancialsUpdate] logistics: Optional[InvoiceLogisticsUpdate] = None details: Optional[List[InvoiceSalesDetailsUpdate]] = None collections: Optional[List[InvoiceCollectionsUpdate]] = None # --- Response Schemas --- class PedimentoBasicInfo(BaseModel): """Basic Pedimento information for Invoice response""" pedimento_number: Optional[str] = None pedimento_code: Optional[str] = None customs_office: Optional[str] = None license: Optional[str] = None year: Optional[str] = None class Config: from_attributes = True class InvoiceComplianceMxResponse(InvoiceComplianceMxBase): """Schema for Compliance MX response""" invoice_id: int pedimento: Optional[PedimentoBasicInfo] = None class Config: from_attributes = True class InvoiceFinancialsResponse(InvoiceFinancialsBase): """Schema for Financials response""" id: int invoice_id: int class Config: from_attributes = True class InvoiceLogisticsResponse(InvoiceLogisticsBase): """Schema for Logistics response""" id: int invoice_id: int class Config: from_attributes = True class InvoiceSalesDetailsResponse(InvoiceSalesDetailsBase): """Schema for Sales Details response""" id: int invoice_id: int class Config: from_attributes = True class InvoiceCollectionsResponse(InvoiceCollectionsBase): """Schema for Collections response""" id: int invoice_id: int class Config: from_attributes = True class InvoiceHeaderResponse(InvoiceHeaderBase): """Schema for Invoice Header response with nested relations""" id: int capture_date: datetime compliance_mx: Optional[InvoiceComplianceMxResponse] = None financials: Optional[InvoiceFinancialsResponse] = None logistics: Optional[InvoiceLogisticsResponse] = None details: Optional[List[InvoiceSalesDetailsResponse]] = [] collections: Optional[List[InvoiceCollectionsResponse]] = [] class Config: from_attributes = True class InvoiceHeaderListResponse(BaseModel): items: List[InvoiceHeaderResponse] total: int page: int # --- Consolidated Response Schemas --- # Import necessary DTOs from other modules from api.v1.modules.public.reference_data.invoice_types.dto import InvoiceTypeDTO from api.v1.modules.a76.customs_brokers.dto import CustomsBrokerResponseDTO from api.v1.modules.a76.clients_and_providers.dto import ClientProviderResponseDTO from api.v1.modules.public.reference_data.currency_types.dto import CurrencyTypeDTO from api.v1.modules.public.reference_data.transport_types.dto import TransportTypeDTO from api.v1.modules.public.reference_data.customs_sections.dto import CustomsSectionDTO from api.v1.modules.public.reference_data.incoterms.dto import IncotermDTO from api.v1.modules.public.reference_data.valuation_methods.dto import ValuationMethodDTO from api.v1.modules.public.reference_data.transport_modes.dto import TransportModeDTO from api.v1.modules.public.reference_data.code_pedimento_regimens.dto import CodePedimentoRegimenDTO # TODO: Check if these paths are correct or need adjustment based on actual file locations # Using Any for now for potentially complex or unverified paths to avoid immediate ImportErrors # Detailed verification is needed for: # - TransporterDTO # - VehicleDTO # - DriverDTO # - TrailerDTO # - SealResponseDTO # - PedimentoDTO class InvoiceCatalogsResponse(BaseModel): """Consolidated response for all catalogs needed in Invoice Create/Edit views""" invoice_types: List[InvoiceTypeDTO] = [] customs_brokers: List[CustomsBrokerResponseDTO] = [] clients: List[ClientProviderResponseDTO] = [] providers: List[ClientProviderResponseDTO] = [] currency_types: List[CurrencyTypeDTO] = [] transport_types: List[TransportTypeDTO] = [] transporters: List[dict] = [] # Placeholder, refine with actual DTO vehicles: List[dict] = [] # Placeholder, refine with actual DTO drivers: List[dict] = [] # Placeholder, refine with actual DTO trailers: List[dict] = [] # Placeholder, refine with actual DTO customs_sections: List[CustomsSectionDTO] = [] code_pedimento_regimens: List[CodePedimentoRegimenDTO] = [] seals: List[dict] = [] # Placeholder, refine with actual DTO incoterms: List[IncotermDTO] = [] valuation_methods: List[ValuationMethodDTO] = [] pedimentos: List[dict] = [] # Placeholder, refine with actual DTO transport_modes: List[TransportModeDTO] = [] default_settings: Optional[dict] = None class InvoiceCreationResponse(InvoiceCatalogsResponse): """Response for Invoice Creation View""" is_create: bool = True invoice: Optional[dict] = None # Should be null for creation invoice_id: Optional[int] = None filters: Optional[dict] = None # Pre-filled filters if any class InvoiceEditionResponse(InvoiceCatalogsResponse): """Response for Invoice Edition View""" is_create: bool = False invoice: InvoiceHeaderResponse invoice_id: int filters: Optional[dict] = None