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:
@@ -22,7 +22,10 @@ from api.v1.modules.crm.documents.models import Document
|
||||
from api.v1.modules.crm.leads.models import Lead
|
||||
from api.v1.modules.crm.opportunities.models import Opportunity
|
||||
from api.v1.modules.crm.pipelines.models import Pipeline, PipelineStage
|
||||
from api.v1.modules.crm.quotes.models import Quote, QuoteItem
|
||||
from api.v1.modules.crm.service_requests.models import ServiceRequest
|
||||
from api.v1.modules.crm.suppliers.models import Supplier
|
||||
from api.v1.modules.ops.shipments.models import Shipment, ShipmentDocument
|
||||
from core.database import CoreSessionLocal
|
||||
|
||||
# Deben coincidir con DEV_LOCAL_AUTH_TENANT_ID / DEV_LOCAL_AUTH_COMPANY_ID
|
||||
@@ -254,6 +257,72 @@ def seed_suppliers_and_related(db) -> None:
|
||||
print("✓ 2 proveedores, 3 direcciones, 2 documentos y 1 contacto de proveedor")
|
||||
|
||||
|
||||
def seed_commercial_and_ops(db) -> None:
|
||||
"""Demo del flujo comercial: Solicitud → Cotización (aceptada) → Embarque liberado."""
|
||||
if db.query(ServiceRequest).filter(
|
||||
ServiceRequest.tenant_id == TENANT_ID, ServiceRequest.company_id == COMPANY_ID,
|
||||
ServiceRequest.deleted_at.is_(None),
|
||||
).first():
|
||||
print("• Ya existe flujo comercial; se omite")
|
||||
return
|
||||
|
||||
account = (
|
||||
db.query(Account)
|
||||
.filter(Account.tenant_id == TENANT_ID, Account.company_id == COMPANY_ID, Account.deleted_at.is_(None))
|
||||
.order_by(Account.id.asc())
|
||||
.first()
|
||||
)
|
||||
account_id = account.id if account else None
|
||||
|
||||
# 1. Solicitud de servicio (RFQ)
|
||||
sr = ServiceRequest(
|
||||
reference="SOL-0001", account_id=account_id, operation_type="exportacion",
|
||||
transport_mode="maritimo", service_type="puerta_puerta", incoterm="FOB",
|
||||
origin="Manzanillo, MX", destination="Long Beach, US", cargo_type="Carga general",
|
||||
load_type="FCL", container_equipment="1x40'HC", status="cotizada",
|
||||
tenant_id=TENANT_ID, company_id=COMPANY_ID,
|
||||
)
|
||||
db.add(sr)
|
||||
db.flush()
|
||||
|
||||
# 2. Cotización aceptada con conceptos
|
||||
quote = Quote(
|
||||
reference="COT-0001", service_request_id=sr.id, account_id=account_id, currency="USD",
|
||||
status="aceptada", tenant_id=TENANT_ID, company_id=COMPANY_ID,
|
||||
)
|
||||
db.add(quote)
|
||||
db.flush()
|
||||
items = [
|
||||
("flete_internacional", 1, 1800, 2200),
|
||||
("transporte_terrestre", 1, 350, 500),
|
||||
("despacho_aduanal", 1, 200, 320),
|
||||
]
|
||||
total_cost = total_sale = 0
|
||||
for concept, qty, cost, sale in items:
|
||||
db.add(QuoteItem(quote_id=quote.id, concept=concept, quantity=qty, unit_cost=cost,
|
||||
unit_sale=sale, currency="USD", tenant_id=TENANT_ID, company_id=COMPANY_ID))
|
||||
total_cost += qty * cost
|
||||
total_sale += qty * sale
|
||||
quote.total_cost = total_cost
|
||||
quote.total_sale = total_sale
|
||||
sr.status = "liberada"
|
||||
|
||||
# 3. Embarque liberado a Operaciones
|
||||
shipment = Shipment(
|
||||
reference="EMB-0001", quote_id=quote.id, service_request_id=sr.id, account_id=account_id,
|
||||
operation_type=sr.operation_type, transport_mode=sr.transport_mode, service_type=sr.service_type,
|
||||
incoterm=sr.incoterm, origin=sr.origin, destination=sr.destination,
|
||||
status="booking", booking_number="BKG-778812", tenant_id=TENANT_ID, company_id=COMPANY_ID,
|
||||
)
|
||||
db.add(shipment)
|
||||
db.flush()
|
||||
db.add(ShipmentDocument(shipment_id=shipment.id, doc_kind="master", doc_type="MBL",
|
||||
number="MBLU12345678", tenant_id=TENANT_ID, company_id=COMPANY_ID))
|
||||
|
||||
db.commit()
|
||||
print("✓ Flujo comercial: 1 solicitud, 1 cotización aceptada (3 conceptos), 1 embarque + documento MBL")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
db = CoreSessionLocal()
|
||||
try:
|
||||
@@ -261,6 +330,7 @@ def main() -> None:
|
||||
pipeline, stages = ensure_pipeline(db)
|
||||
seed_sample_data(db, pipeline, stages)
|
||||
seed_suppliers_and_related(db)
|
||||
seed_commercial_and_ops(db)
|
||||
print("\nSeed CRM completado.")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
Reference in New Issue
Block a user