feat(crm): módulo Tarifario — modelos, CRUD, import Excel y motor de costeo
- Tablas rate_sheets/lanes/breaks/charges (esquema crm) + migración con down(). - Catálogos nuevos: modo_tarifario, unidad_tarifa, concepto_cargo. - CRUD de tarifarios y rutas; descarga de plantilla Excel por modo; import con vista previa y validación; alta directa desde Excel. - Motor de costeo /rate-quote: aéreo (peso facturable + quiebres + optimización), marítimo FCL (por contenedor), LCL (W/M) y terrestre; suma cargos adicionales. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
148
backend/api/v1/modules/crm/rates/dto.py
Normal file
148
backend/api/v1/modules/crm/rates/dto.py
Normal file
@@ -0,0 +1,148 @@
|
||||
"""Schemas del módulo Tarifario."""
|
||||
|
||||
from datetime import date, datetime
|
||||
from decimal import Decimal
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
# ---------- Quiebres y cargos ----------
|
||||
class RateBreakDTO(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
from_qty: Decimal = Field(0)
|
||||
rate: Decimal = Field(0)
|
||||
|
||||
|
||||
class RateChargeDTO(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
concept: str = Field(..., max_length=60)
|
||||
charge_type: str = Field("fijo", max_length=20)
|
||||
value: Decimal | None = None
|
||||
condition: str | None = None
|
||||
|
||||
|
||||
# ---------- Rutas ----------
|
||||
class RateLaneBase(BaseModel):
|
||||
origin: str | None = Field(None, max_length=20)
|
||||
destination: str | None = Field(None, max_length=20)
|
||||
region: str | None = Field(None, max_length=60)
|
||||
equipment_type: str | None = Field(None, max_length=20)
|
||||
rate_unit: str | None = Field(None, max_length=20)
|
||||
min_charge: Decimal | None = None
|
||||
flat_rate: Decimal | None = None
|
||||
transit_days: int | None = None
|
||||
notes: str | None = None
|
||||
|
||||
|
||||
class RateLaneCreate(RateLaneBase):
|
||||
breaks: list[RateBreakDTO] = Field(default_factory=list)
|
||||
|
||||
|
||||
class RateLaneUpdate(RateLaneBase):
|
||||
breaks: list[RateBreakDTO] | None = None
|
||||
|
||||
|
||||
class RateLaneResponse(RateLaneBase):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: int
|
||||
rate_sheet_id: int
|
||||
breaks: list[RateBreakDTO] = Field(default_factory=list)
|
||||
|
||||
|
||||
# ---------- Tarifario (cabecera) ----------
|
||||
class RateSheetBase(BaseModel):
|
||||
supplier_id: int | None = None
|
||||
mode: str = Field(..., max_length=20)
|
||||
name: str = Field(..., min_length=1, max_length=255)
|
||||
currency: str | None = Field("USD", max_length=3)
|
||||
valid_from: date | None = None
|
||||
valid_to: date | None = None
|
||||
default_origin: str | None = Field(None, max_length=20)
|
||||
status: str = Field("borrador", max_length=20)
|
||||
notes: str | None = None
|
||||
|
||||
|
||||
class RateSheetCreate(RateSheetBase):
|
||||
pass
|
||||
|
||||
|
||||
class RateSheetUpdate(BaseModel):
|
||||
supplier_id: int | None = None
|
||||
mode: str | None = Field(None, max_length=20)
|
||||
name: str | None = Field(None, max_length=255)
|
||||
currency: str | None = Field(None, max_length=3)
|
||||
valid_from: date | None = None
|
||||
valid_to: date | None = None
|
||||
default_origin: str | None = Field(None, max_length=20)
|
||||
status: str | None = Field(None, max_length=20)
|
||||
notes: str | None = None
|
||||
|
||||
|
||||
class RateSheetResponse(RateSheetBase):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: int
|
||||
tenant_id: int
|
||||
company_id: int
|
||||
source_file: str | None = None
|
||||
created_by: str | None = None
|
||||
updated_by: str | None = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
lane_count: int | None = None
|
||||
|
||||
|
||||
# ---------- Importación ----------
|
||||
class ImportPreviewRow(BaseModel):
|
||||
row: int
|
||||
data: dict
|
||||
ok: bool
|
||||
warnings: list[str] = Field(default_factory=list)
|
||||
errors: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ImportPreview(BaseModel):
|
||||
mode: str
|
||||
total: int
|
||||
valid: int
|
||||
rows: list[ImportPreviewRow]
|
||||
columns: list[str]
|
||||
|
||||
|
||||
class ImportConfirm(RateSheetCreate):
|
||||
lanes: list[RateLaneCreate]
|
||||
|
||||
|
||||
# ---------- Costeo ----------
|
||||
class CostRequest(BaseModel):
|
||||
mode: str
|
||||
origin: str | None = None
|
||||
destination: str | None = None
|
||||
on_date: date | None = None
|
||||
gross_weight_kg: Decimal | None = None
|
||||
volume_m3: Decimal | None = None
|
||||
equipment_type: str | None = None
|
||||
quantity: int = 1
|
||||
dangerous: bool = False
|
||||
|
||||
|
||||
class CostChargeLine(BaseModel):
|
||||
concept: str
|
||||
amount: Decimal
|
||||
|
||||
|
||||
class CostOption(BaseModel):
|
||||
rate_sheet_id: int
|
||||
rate_sheet_name: str
|
||||
supplier_id: int | None
|
||||
currency: str | None
|
||||
chargeable: Decimal | None = None # peso/wm facturable usado
|
||||
base_cost: Decimal
|
||||
charges: list[CostChargeLine] = Field(default_factory=list)
|
||||
total_cost: Decimal
|
||||
transit_days: int | None = None
|
||||
detail: str | None = None
|
||||
|
||||
|
||||
class CostResult(BaseModel):
|
||||
request: CostRequest
|
||||
options: list[CostOption]
|
||||
Reference in New Issue
Block a user