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:
93
backend/api/v1/modules/crm/rates/models.py
Normal file
93
backend/api/v1/modules/crm/rates/models.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""Modelos del módulo Tarifario (base de costos para Cotizaciones).
|
||||
|
||||
Un ``RateSheet`` (tarifario) pertenece a un proveedor y agrupa muchas
|
||||
``RateLane`` (rutas origen→destino). Cada ruta tiene, según el modo:
|
||||
- Aéreo / LCL: varios ``RateBreak`` (quiebres de peso/volumen con su tarifa).
|
||||
- FCL / terrestre: una tarifa plana por contenedor/unidad (``flat_rate``).
|
||||
Los ``RateCharge`` son cargos adicionales a nivel tarifario o ruta.
|
||||
"""
|
||||
|
||||
from datetime import date
|
||||
from decimal import Decimal
|
||||
|
||||
from sqlalchemy import Date, ForeignKey, Integer, Numeric, String, Text, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
|
||||
|
||||
class RateSheet(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "rate_sheets"
|
||||
__table_args__ = {"schema": "crm"}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
supplier_id: Mapped[int | None] = mapped_column(
|
||||
Integer, ForeignKey("crm.suppliers.id"), nullable=True, index=True
|
||||
)
|
||||
# aereo | maritimo_fcl | maritimo_lcl | terrestre
|
||||
mode: Mapped[str] = mapped_column(String(20), nullable=False, index=True)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
currency: Mapped[str | None] = mapped_column(String(3), nullable=True, server_default=text("'USD'"))
|
||||
valid_from: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||
valid_to: Mapped[date | None] = mapped_column(Date, nullable=True)
|
||||
default_origin: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
||||
# borrador | activo | vencido | reemplazado
|
||||
status: Mapped[str] = mapped_column(String(20), nullable=False, server_default=text("'borrador'"))
|
||||
source_file: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
source_url: Mapped[str | None] = mapped_column(String(1024), nullable=True)
|
||||
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
created_by: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
updated_by: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
||||
|
||||
|
||||
class RateLane(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "rate_lanes"
|
||||
__table_args__ = {"schema": "crm"}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
rate_sheet_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("crm.rate_sheets.id"), nullable=False, index=True
|
||||
)
|
||||
origin: Mapped[str | None] = mapped_column(String(20), nullable=True, index=True)
|
||||
destination: Mapped[str | None] = mapped_column(String(20), nullable=True, index=True)
|
||||
region: Mapped[str | None] = mapped_column(String(60), nullable=True)
|
||||
# Solo FCL/terrestre (código del catálogo tipo_equipo). Nulo en aéreo/LCL.
|
||||
equipment_type: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
||||
# per_kg | per_wm | per_container | flat
|
||||
rate_unit: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
||||
min_charge: Mapped[Decimal | None] = mapped_column(Numeric(14, 4), nullable=True)
|
||||
flat_rate: Mapped[Decimal | None] = mapped_column(Numeric(14, 4), nullable=True)
|
||||
transit_days: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
||||
notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
|
||||
|
||||
class RateBreak(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "rate_breaks"
|
||||
__table_args__ = {"schema": "crm"}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
rate_lane_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("crm.rate_lanes.id"), nullable=False, index=True
|
||||
)
|
||||
# Umbral del quiebre (kg en aéreo; W/M en LCL)
|
||||
from_qty: Mapped[Decimal] = mapped_column(Numeric(12, 3), nullable=False, server_default=text("0"))
|
||||
rate: Mapped[Decimal] = mapped_column(Numeric(14, 4), nullable=False, server_default=text("0"))
|
||||
|
||||
|
||||
class RateCharge(Base, TenantScopedMixin, TimestampMixin):
|
||||
__tablename__ = "rate_charges"
|
||||
__table_args__ = {"schema": "crm"}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
rate_sheet_id: Mapped[int | None] = mapped_column(
|
||||
Integer, ForeignKey("crm.rate_sheets.id"), nullable=True, index=True
|
||||
)
|
||||
rate_lane_id: Mapped[int | None] = mapped_column(
|
||||
Integer, ForeignKey("crm.rate_lanes.id"), nullable=True, index=True
|
||||
)
|
||||
concept: Mapped[str] = mapped_column(String(60), nullable=False)
|
||||
# fijo | por_kg | por_guia | por_contenedor | porcentaje
|
||||
charge_type: Mapped[str] = mapped_column(String(20), nullable=False, server_default=text("'fijo'"))
|
||||
value: Mapped[Decimal | None] = mapped_column(Numeric(14, 4), nullable=True)
|
||||
condition: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
Reference in New Issue
Block a user