- 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>
94 lines
4.7 KiB
Python
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)
|