feat(crm,ops): proceso comercial (solicitudes/RFQ, tarifas, cotizaciones) + Operaciones (embarques)
Diagrama 1 (CRM comercial) y Diagrama 2 (Operaciones) del spec de agente de carga: - crm.service_requests (RFQ) + crm.rate_requests (solicitud de tarifas a proveedores) - crm.quotes + crm.quote_items: conceptos costo/venta/margen, totales automáticos, estados borrador→enviada→aceptada/rechazada - schema ops: ops.shipments (booking, Cut Off, ETD/ETA, naviera/agente aduanal/destino) y ops.shipment_documents (MBL/HBL, MAWB/HAWB, CMR…) - liberar-a-operaciones: crea el embarque desde la cotización aceptada - migración b3c4d5e6f7a8, routers/permisos, seed del flujo completo - 52 tests pytest en verde Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
102
backend/api/v1/modules/crm/quotes/dto.py
Normal file
102
backend/api/v1/modules/crm/quotes/dto.py
Normal file
@@ -0,0 +1,102 @@
|
||||
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)
|
||||
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)
|
||||
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
|
||||
status: str
|
||||
total_cost: Decimal
|
||||
total_sale: Decimal
|
||||
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))
|
||||
Reference in New Issue
Block a user