from datetime import date, datetime from decimal import Decimal from typing import Literal from pydantic import BaseModel, ConfigDict, Field class ShipmentBase(BaseModel): reference: str | None = Field(None, max_length=40) quote_id: int | None = None service_request_id: int | None = None account_id: int | None = None operation_type: str | None = Field(None, max_length=20) transport_mode: str | None = Field(None, max_length=20) service_type: str | None = Field(None, max_length=20) incoterm: str | None = Field(None, max_length=10) origin: str | None = Field(None, max_length=160) destination: str | None = Field(None, max_length=160) status: str = Field("abierta", max_length=20) booking_number: str | None = Field(None, max_length=60) carrier_supplier_id: int | None = None ground_carrier_supplier_id: int | None = None customs_agent_id: int | None = None destination_agent_id: int | None = None cutoff_date: datetime | None = None pickup_at: datetime | None = None etd: date | None = None previous_etd: date | None = None eta: date | None = None vessel_flight: str | None = Field(None, max_length=120) container_number: str | None = Field(None, max_length=60) notes: str | None = None actual_cost_total: Decimal | None = None cost_currency: str | None = Field(None, max_length=3) owner_user_id: str | None = Field(None, max_length=64) class ShipmentCreate(ShipmentBase): pass class ShipmentUpdate(BaseModel): reference: str | None = Field(None, max_length=40) account_id: int | None = None operation_type: str | None = Field(None, max_length=20) transport_mode: str | None = Field(None, max_length=20) service_type: str | None = Field(None, max_length=20) incoterm: str | None = Field(None, max_length=10) origin: str | None = Field(None, max_length=160) destination: str | None = Field(None, max_length=160) status: str | None = Field(None, max_length=20) booking_number: str | None = Field(None, max_length=60) carrier_supplier_id: int | None = None ground_carrier_supplier_id: int | None = None customs_agent_id: int | None = None destination_agent_id: int | None = None cutoff_date: datetime | None = None pickup_at: datetime | None = None etd: date | None = None eta: date | None = None vessel_flight: str | None = Field(None, max_length=120) container_number: str | None = Field(None, max_length=60) notes: str | None = None owner_user_id: str | None = Field(None, max_length=64) class ShipmentRescheduleInput(BaseModel): """Reprogramación de salida cuando no se alcanza el Cut Off (R-E-06).""" etd: date | None = None cutoff_date: datetime | None = None reason: str | None = None class ShipmentCloseInput(BaseModel): """Cierre operativo del embarque con costos finales (R-E-22).""" actual_cost_total: Decimal = Field(..., ge=0) cost_currency: str = Field("MXN", max_length=3) notes: str | None = None class ShipmentResponse(ShipmentBase): model_config = ConfigDict(from_attributes=True) id: int closed_at: datetime | None = None closed_by: str | None = None created_by: str | None = None updated_by: str | None = None tenant_id: int company_id: int created_at: datetime updated_at: datetime class ShipmentEventBase(BaseModel): shipment_id: int event_type: str | None = Field(None, max_length=60) title: str = Field(..., min_length=1, max_length=160) kind: str = Field("hito", max_length=20) # hito | decision status: str = Field("pendiente", max_length=20) outcome: str | None = Field(None, max_length=20) # autorizado | rechazado parent_event_id: int | None = None attempt: int = Field(1, ge=1) position: int = Field(0, ge=0) planned_date: datetime | None = None actual_date: datetime | None = None notes: str | None = None class ShipmentEventCreate(ShipmentEventBase): pass class ShipmentEventUpdate(BaseModel): event_type: str | None = Field(None, max_length=60) title: str | None = Field(None, min_length=1, max_length=160) kind: str | None = Field(None, max_length=20) status: str | None = Field(None, max_length=20) position: int | None = Field(None, ge=0) planned_date: datetime | None = None actual_date: datetime | None = None notes: str | None = None class ShipmentEventDecisionInput(BaseModel): """Resultado de un punto de decisión del flujo (R-E-13, R-E-05, R-I-06).""" outcome: Literal["autorizado", "rechazado"] notes: str | None = None class ShipmentEventResponse(ShipmentEventBase): model_config = ConfigDict(from_attributes=True) id: int tenant_id: int company_id: int created_at: datetime updated_at: datetime class ShipmentDocumentBase(BaseModel): shipment_id: int doc_kind: str = Field("otro", max_length=10) doc_type: str = Field(..., max_length=30) number: str | None = Field(None, max_length=80) issue_date: date | None = None file_url: str | None = Field(None, max_length=1024) file_key: str | None = Field(None, max_length=512) notes: str | None = None class ShipmentDocumentCreate(ShipmentDocumentBase): pass class ShipmentDocumentUpdate(BaseModel): doc_kind: str | None = Field(None, max_length=10) doc_type: str | None = Field(None, max_length=30) number: str | None = Field(None, max_length=80) issue_date: date | None = None file_url: str | None = Field(None, max_length=1024) file_key: str | None = Field(None, max_length=512) notes: str | None = None class ShipmentDocumentResponse(ShipmentDocumentBase): model_config = ConfigDict(from_attributes=True) id: int tenant_id: int company_id: int created_at: datetime updated_at: datetime