Files
CRM_AGENTES_CARGA/backend/api/v1/modules/crm/rates/models.py
Ernesto Herrera 2ae6901b6a 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>
2026-07-27 09:28:49 -06:00

94 lines
4.7 KiB
Python

"""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)