from datetime import date, datetime from decimal import Decimal from pydantic import BaseModel, ConfigDict, Field, computed_field # ----- Quote items ----- class QuoteItemBase(BaseModel): concept: str = Field(..., max_length=60) description: str | None = Field(None, max_length=255) supplier_id: int | None = None quantity: Decimal = Field(Decimal(1), ge=0, max_digits=12, decimal_places=2) unit_cost: Decimal = Field(Decimal(0), ge=0, max_digits=14, decimal_places=2) unit_sale: Decimal = Field(Decimal(0), ge=0, max_digits=14, decimal_places=2) currency: str | None = Field(None, max_length=3) class QuoteItemCreate(QuoteItemBase): quote_id: int class QuoteItemUpdate(BaseModel): concept: str | None = Field(None, max_length=60) description: str | None = Field(None, max_length=255) supplier_id: int | None = None quantity: Decimal | None = Field(None, ge=0, max_digits=12, decimal_places=2) unit_cost: Decimal | None = Field(None, ge=0, max_digits=14, decimal_places=2) unit_sale: Decimal | None = Field(None, ge=0, max_digits=14, decimal_places=2) currency: str | None = Field(None, max_length=3) class QuoteItemResponse(QuoteItemBase): model_config = ConfigDict(from_attributes=True) id: int quote_id: int tenant_id: int company_id: int @computed_field @property def line_cost(self) -> Decimal: return (self.quantity or Decimal(0)) * (self.unit_cost or Decimal(0)) @computed_field @property def line_sale(self) -> Decimal: return (self.quantity or Decimal(0)) * (self.unit_sale or Decimal(0)) # ----- Quotes ----- class QuoteBase(BaseModel): reference: str | None = Field(None, max_length=40) service_request_id: int | None = None account_id: int | None = None currency: str = Field("USD", max_length=3) load_type: str | None = Field(None, max_length=10) # FCL | LCL (variante de la comparación "Ambas") issue_date: date | None = None valid_until: date | None = None notes: str | None = None terms: str | None = None owner_user_id: str | None = Field(None, max_length=64) class QuoteCreate(QuoteBase): pass class QuoteUpdate(BaseModel): reference: str | None = Field(None, max_length=40) service_request_id: int | None = None account_id: int | None = None currency: str | None = Field(None, max_length=3) load_type: str | None = Field(None, max_length=10) issue_date: date | None = None valid_until: date | None = None notes: str | None = None terms: str | None = None owner_user_id: str | None = Field(None, max_length=64) class QuoteResponse(QuoteBase): model_config = ConfigDict(from_attributes=True) id: int service_request_reference: str | None = None # folio de la solicitud referenciada status: str total_cost: Decimal total_sale: Decimal pdf_file_key: str | None = None sent_at: datetime | None = None accepted_at: datetime | None = None rejected_at: datetime | None = None created_by: str | None = None updated_by: str | None = None tenant_id: int company_id: int created_at: datetime updated_at: datetime @computed_field @property def margin(self) -> Decimal: return (self.total_sale or Decimal(0)) - (self.total_cost or Decimal(0)) # ----- Configuración de marca del formato de cotización ----- class QuoteSettingsInput(BaseModel): emitter_name: str | None = Field(None, max_length=255) emitter_rfc: str | None = Field(None, max_length=13) emitter_address: str | None = None emitter_phone: str | None = Field(None, max_length=60) emitter_email: str | None = Field(None, max_length=255) emitter_website: str | None = Field(None, max_length=255) accent_color: str | None = Field(None, max_length=9) quote_prefix: str | None = Field(None, max_length=12) default_terms: str | None = None footer_note: str | None = None class QuoteSettingsResponse(QuoteSettingsInput): model_config = ConfigDict(from_attributes=True) id: int | None = None logo_file_key: str | None = None class SendQuoteEmailRequest(BaseModel): to: str | None = None subject: str | None = None message: str | None = None