- Crea el módulo faltante permissions/seed_v2.py (register_core_permissions), que sync_permissions y bootstrap_super_admin importan; sin él ambos fallaban con ImportError y el auto-bootstrap dev quedaba inoperante. - seed: ensure_company (a76.company id=1, requerida por la FK de company_roles) y seed_carril_roles (Ventas, Operaciones, Facturación, Consulta) con sus permisos; pobla el PermissionRegistry importando los permisos de cada dominio. - Verificado end-to-end: con enforcement activo el usuario dev auto-bootstrapea a super_admin y crm/ops/fin responden 200 (no 403). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
471 lines
20 KiB
Python
471 lines
20 KiB
Python
"""Seed de datos para el CRM en desarrollo.
|
|
|
|
Idempotente: se puede correr varias veces sin duplicar. Puebla:
|
|
1. Tenant dev (core.tenants id=1) — requerido por la FK tenant_id de las tablas crm.
|
|
2. Embudo por defecto "Ventas" + 6 etapas (catálogo del pipeline).
|
|
3. Datos de ejemplo: cuentas, contactos, prospectos, oportunidades y actividades.
|
|
|
|
Ejecutar dentro del contenedor backend:
|
|
docker compose exec backend python seed_crm.py
|
|
|
|
Los datos usan valores dummy (RFC XAXX010101000, etc.); no incluir datos reales.
|
|
"""
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
from api.v1.modules.core.tenants.models import Tenant, TenantType
|
|
from api.v1.modules.crm.accounts.models import Account
|
|
from api.v1.modules.crm.activities.models import Activity
|
|
from api.v1.modules.crm.addresses.models import Address
|
|
from api.v1.modules.crm.contacts.models import Contact
|
|
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, ShipmentEvent
|
|
from api.v1.modules.ops.shipments import service as shipments_service
|
|
from api.v1.modules.ops.shipments.dto import ShipmentCloseInput, ShipmentEventDecisionInput
|
|
from api.v1.modules.fin.invoices.models import Invoice
|
|
from api.v1.modules.fin.invoices import service as invoices_service
|
|
from api.v1.modules.fin.invoices.dto import PaymentCreate
|
|
from api.v1.modules.core.permissions.models import CompanyRole, Permission, RolePermission
|
|
from api.v1.modules.core.permissions.service import PermissionService
|
|
# Efecto secundario: poblar el PermissionRegistry con los permisos de cada dominio
|
|
import api.v1.modules.crm.permissions # noqa: F401
|
|
import api.v1.modules.ops.permissions # noqa: F401
|
|
import api.v1.modules.fin.permissions # noqa: F401
|
|
from core.database import CoreSessionLocal
|
|
|
|
# Deben coincidir con DEV_LOCAL_AUTH_TENANT_ID / DEV_LOCAL_AUTH_COMPANY_ID
|
|
TENANT_ID = 1
|
|
COMPANY_ID = 1
|
|
|
|
RFC_DUMMY = "XAXX010101000" # RFC genérico dummy (política de datos)
|
|
|
|
STAGE_DEFS = [
|
|
# (nombre, probabilidad, is_won, is_lost)
|
|
("Prospecto", 10, False, False),
|
|
("Contactado", 25, False, False),
|
|
("Propuesta", 50, False, False),
|
|
("Negociación", 75, False, False),
|
|
("Ganada", 100, True, False),
|
|
("Perdida", 0, False, True),
|
|
]
|
|
|
|
|
|
def ensure_tenant(db) -> Tenant:
|
|
tenant = db.query(Tenant).filter(Tenant.id == TENANT_ID).first()
|
|
if tenant:
|
|
print(f"• Tenant id={TENANT_ID} ya existe ({tenant.name})")
|
|
return tenant
|
|
tenant = Tenant(
|
|
id=TENANT_ID,
|
|
name="Aduanasoft (dev)",
|
|
slug="dev",
|
|
keycloak_realm="master",
|
|
type=TenantType.SHARED,
|
|
is_active=True,
|
|
)
|
|
db.add(tenant)
|
|
db.commit()
|
|
# Alinear la secuencia para que futuros inserts no colisionen con el id explícito
|
|
db.execute(
|
|
__import__("sqlalchemy").text(
|
|
"SELECT setval('core.tenants_id_seq', (SELECT MAX(id) FROM core.tenants))"
|
|
)
|
|
)
|
|
db.commit()
|
|
print(f"✓ Tenant dev creado (id={TENANT_ID})")
|
|
return tenant
|
|
|
|
|
|
def ensure_pipeline(db):
|
|
pipeline = (
|
|
db.query(Pipeline)
|
|
.filter(
|
|
Pipeline.tenant_id == TENANT_ID,
|
|
Pipeline.company_id == COMPANY_ID,
|
|
Pipeline.deleted_at.is_(None),
|
|
)
|
|
.first()
|
|
)
|
|
if pipeline:
|
|
stages = (
|
|
db.query(PipelineStage)
|
|
.filter(
|
|
PipelineStage.pipeline_id == pipeline.id,
|
|
PipelineStage.deleted_at.is_(None),
|
|
)
|
|
.order_by(PipelineStage.position.asc())
|
|
.all()
|
|
)
|
|
print(f"• Embudo '{pipeline.name}' ya existe ({len(stages)} etapas)")
|
|
return pipeline, stages
|
|
|
|
pipeline = Pipeline(name="Ventas", is_default=True, tenant_id=TENANT_ID, company_id=COMPANY_ID)
|
|
db.add(pipeline)
|
|
db.flush()
|
|
stages = []
|
|
for position, (name, probability, is_won, is_lost) in enumerate(STAGE_DEFS):
|
|
stage = PipelineStage(
|
|
pipeline_id=pipeline.id,
|
|
name=name,
|
|
position=position,
|
|
probability=probability,
|
|
is_won=is_won,
|
|
is_lost=is_lost,
|
|
tenant_id=TENANT_ID,
|
|
company_id=COMPANY_ID,
|
|
)
|
|
db.add(stage)
|
|
stages.append(stage)
|
|
db.commit()
|
|
for stage in stages:
|
|
db.refresh(stage)
|
|
print(f"✓ Embudo 'Ventas' + {len(stages)} etapas creados")
|
|
return pipeline, stages
|
|
|
|
|
|
def seed_sample_data(db, pipeline, stages) -> None:
|
|
has_accounts = (
|
|
db.query(Account)
|
|
.filter(
|
|
Account.tenant_id == TENANT_ID,
|
|
Account.company_id == COMPANY_ID,
|
|
Account.deleted_at.is_(None),
|
|
)
|
|
.first()
|
|
)
|
|
if has_accounts:
|
|
print("• Ya existen cuentas; se omiten los datos de ejemplo")
|
|
return
|
|
|
|
accounts_data = [
|
|
dict(name="Maquiladora del Norte SA de CV", trade_name="MaqNorte", rfc=RFC_DUMMY,
|
|
account_type="immex", status="active", city="Tijuana", state="Baja California",
|
|
email="contacto@ejemplo.mx", phone="6640000000"),
|
|
dict(name="Importadora Pacífico SA de CV", trade_name="ImpPacífico", rfc=RFC_DUMMY,
|
|
account_type="importador", status="active", city="Manzanillo", state="Colima"),
|
|
dict(name="Agencia Aduanal López y Asociados", account_type="agencia_aduanal",
|
|
patente_aduanal="0000", status="active", city="Nuevo Laredo", state="Tamaulipas"),
|
|
dict(name="Transportes Frontera", account_type="transportista", status="prospect",
|
|
city="Ciudad Juárez", state="Chihuahua"),
|
|
]
|
|
accounts = [Account(**d, tenant_id=TENANT_ID, company_id=COMPANY_ID) for d in accounts_data]
|
|
db.add_all(accounts)
|
|
db.flush()
|
|
|
|
contacts_data = [
|
|
dict(account=accounts[0], first_name="María", last_name="Pérez",
|
|
job_title="Gerente de Comercio Exterior", email="maria@ejemplo.mx",
|
|
phone="6640000001", is_primary=True),
|
|
dict(account=accounts[1], first_name="Jorge", last_name="Ramírez",
|
|
job_title="Director de Logística", email="jorge@ejemplo.mx", is_primary=True),
|
|
dict(account=accounts[2], first_name="Luis", last_name="López",
|
|
job_title="Agente Aduanal", email="luis@ejemplo.mx", is_primary=True),
|
|
]
|
|
for d in contacts_data:
|
|
account = d.pop("account")
|
|
db.add(Contact(**d, account_id=account.id, tenant_id=TENANT_ID, company_id=COMPANY_ID))
|
|
|
|
leads_data = [
|
|
dict(name="Interés en módulo de pedimentos", company_name="Comercializadora Bajío",
|
|
contact_name="Ana Torres", email="ana@ejemplo.mx", source="web", status="new",
|
|
estimated_value=45000),
|
|
dict(name="Demo solicitada Anexo 24", company_name="Ensambles del Golfo",
|
|
contact_name="Pedro Gómez", source="evento", status="contacted",
|
|
estimated_value=80000),
|
|
]
|
|
for d in leads_data:
|
|
db.add(Lead(**d, tenant_id=TENANT_ID, company_id=COMPANY_ID))
|
|
|
|
open_stages = [s for s in stages if not s.is_won and not s.is_lost] or stages
|
|
opportunities_data = [
|
|
dict(name="Licencia Anexo 76 - MaqNorte", account=accounts[0], amount=150000, stage=open_stages[0]),
|
|
dict(name="Suite completa - Importadora Pacífico", account=accounts[1], amount=320000, stage=open_stages[min(1, len(open_stages) - 1)]),
|
|
dict(name="Módulo de saldos - López y Asociados", account=accounts[2], amount=90000, stage=open_stages[min(2, len(open_stages) - 1)]),
|
|
dict(name="Renovación anual - MaqNorte", account=accounts[0], amount=60000, stage=open_stages[min(3, len(open_stages) - 1)]),
|
|
]
|
|
for d in opportunities_data:
|
|
account = d.pop("account")
|
|
stage = d.pop("stage")
|
|
db.add(Opportunity(
|
|
**d, account_id=account.id, pipeline_id=pipeline.id, stage_id=stage.id,
|
|
probability=stage.probability, currency="MXN", status="open",
|
|
tenant_id=TENANT_ID, company_id=COMPANY_ID,
|
|
))
|
|
|
|
now = datetime.now(timezone.utc)
|
|
activities_data = [
|
|
dict(activity_type="call", subject="Llamada de seguimiento MaqNorte", status="pending",
|
|
due_date=now + timedelta(days=1), account=accounts[0]),
|
|
dict(activity_type="meeting", subject="Demo Importadora Pacífico", status="pending",
|
|
due_date=now + timedelta(days=3), account=accounts[1]),
|
|
dict(activity_type="task", subject="Enviar cotización a López y Asociados",
|
|
status="completed", completed_at=now, account=accounts[2]),
|
|
]
|
|
for d in activities_data:
|
|
account = d.pop("account")
|
|
db.add(Activity(**d, account_id=account.id, tenant_id=TENANT_ID, company_id=COMPANY_ID))
|
|
|
|
db.commit()
|
|
print("✓ Datos de ejemplo: 4 cuentas, 3 contactos, 2 prospectos, 4 oportunidades, 3 actividades")
|
|
|
|
|
|
def seed_suppliers_and_related(db) -> None:
|
|
"""Proveedores + direcciones/documentos/contactos (clientes y proveedores)."""
|
|
if db.query(Supplier).filter(Supplier.tenant_id == TENANT_ID, Supplier.company_id == COMPANY_ID, Supplier.deleted_at.is_(None)).first():
|
|
print("• Ya existen proveedores; se omite el seed de catálogos")
|
|
return
|
|
|
|
suppliers_data = [
|
|
dict(name="Naviera del Golfo SA de CV", trade_name="NavGolfo", rfc=RFC_DUMMY,
|
|
classifications=["naviera", "agente_carga"], coverage="internacional",
|
|
countries=["MX", "US", "PA"], ports=["Veracruz", "Manzanillo"],
|
|
quote_currency="USD", status="active"),
|
|
dict(name="Agencia Aduanal Reyes y Asociados", rfc=RFC_DUMMY,
|
|
classifications=["agente_aduanal"], coverage="nacional",
|
|
customs=["Nuevo Laredo", "Colombia"], status="active"),
|
|
]
|
|
suppliers = [Supplier(**d, tenant_id=TENANT_ID, company_id=COMPANY_ID) for d in suppliers_data]
|
|
db.add_all(suppliers)
|
|
db.flush()
|
|
|
|
first_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()
|
|
)
|
|
|
|
# Direcciones (múltiples): proveedor + cliente
|
|
db.add(Address(supplier_id=suppliers[0].id, address_type="oficina", street="Malecón 100",
|
|
neighborhood="Centro", postal_code="91700", city="Veracruz", state="Veracruz",
|
|
is_primary=True, tenant_id=TENANT_ID, company_id=COMPANY_ID))
|
|
if first_account:
|
|
db.add(Address(account_id=first_account.id, address_type="fiscal", street="Blvd. Industrial 500",
|
|
neighborhood="Otay", postal_code="22000", city="Tijuana", state="Baja California",
|
|
is_primary=True, tenant_id=TENANT_ID, company_id=COMPANY_ID))
|
|
db.add(Address(account_id=first_account.id, address_type="bodega", street="Camino a la Presa 12",
|
|
city="Tijuana", state="Baja California", tenant_id=TENANT_ID, company_id=COMPANY_ID))
|
|
|
|
# Documentos
|
|
db.add(Document(supplier_id=suppliers[0].id, doc_type="constancia_fiscal",
|
|
name="Constancia de Situación Fiscal - NavGolfo", tenant_id=TENANT_ID, company_id=COMPANY_ID))
|
|
if first_account:
|
|
db.add(Document(account_id=first_account.id, doc_type="acta_constitutiva",
|
|
name="Acta Constitutiva - MaqNorte", tenant_id=TENANT_ID, company_id=COMPANY_ID))
|
|
|
|
# Contacto de proveedor (con área y flags)
|
|
db.add(Contact(supplier_id=suppliers[0].id, first_name="Rosa", last_name="Díaz", area="Ventas",
|
|
job_title="Ejecutiva de cuenta", email="rosa@ejemplo.mx", phone="2290000000",
|
|
is_primary=True, receives_quotes=True, tenant_id=TENANT_ID, company_id=COMPANY_ID))
|
|
|
|
db.commit()
|
|
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 seed_invoicing_and_events(db) -> None:
|
|
"""Factura desde el embarque (con pago parcial) + hitos de la bitácora."""
|
|
from decimal import Decimal
|
|
|
|
if db.query(Invoice).filter(
|
|
Invoice.tenant_id == TENANT_ID, Invoice.company_id == COMPANY_ID, Invoice.deleted_at.is_(None)
|
|
).first():
|
|
print("• Ya existe facturación; se omite")
|
|
return
|
|
|
|
shipment = (
|
|
db.query(Shipment)
|
|
.filter(Shipment.tenant_id == TENANT_ID, Shipment.company_id == COMPANY_ID, Shipment.deleted_at.is_(None))
|
|
.order_by(Shipment.id.asc())
|
|
.first()
|
|
)
|
|
if not shipment:
|
|
print("• Sin embarque; se omite facturación")
|
|
return
|
|
|
|
# Bitácora de hitos (según tipo de operación, incluye puntos de decisión)
|
|
events = shipments_service.seed_default_milestones(db, shipment.id, TENANT_ID, COMPANY_ID)
|
|
|
|
# Resolver los puntos de decisión como autorizados y completar los hitos simples,
|
|
# para poder cerrar operativamente el embarque.
|
|
for ev in events:
|
|
if ev.kind == "decision":
|
|
shipments_service.decide_shipment_event(
|
|
db, ev.id, ShipmentEventDecisionInput(outcome="autorizado"), TENANT_ID, COMPANY_ID
|
|
)
|
|
else:
|
|
shipments_service.complete_shipment_event(db, ev.id, TENANT_ID, COMPANY_ID)
|
|
|
|
# Cierre operativo con costos finales (dispara la facturación — R-F-01/R-E-22)
|
|
shipments_service.close_shipment(
|
|
db, shipment.id,
|
|
ShipmentCloseInput(actual_cost_total=Decimal("2350"), cost_currency="USD", notes="Cierre demo"),
|
|
TENANT_ID, COMPANY_ID,
|
|
)
|
|
|
|
# Factura generada desde el embarque cerrado (toma conceptos de la cotización)
|
|
invoice = invoices_service.generate_from_shipment(db, shipment.id, TENANT_ID, COMPANY_ID)
|
|
invoices_service.emit_invoice(db, invoice.id, TENANT_ID, COMPANY_ID)
|
|
invoices_service.create_payment(
|
|
db, PaymentCreate(invoice_id=invoice.id, amount=Decimal("1000"), method="transferencia", reference="SPEI-001"),
|
|
TENANT_ID, COMPANY_ID,
|
|
)
|
|
print("✓ Embarque cerrado, factura emitida con pago parcial + bitácora con decisiones resueltas")
|
|
|
|
|
|
# Carriles del proceso (R-T-07): a qué módulos/acciones puede acceder cada rol.
|
|
# clave = (code, nombre); valor = función que decide si un permiso pertenece al rol.
|
|
def _carril_roles() -> dict:
|
|
# cada función recibe el CÓDIGO del permiso (str) y decide si pertenece al carril
|
|
return {
|
|
("ventas", "Ventas"): lambda c: c.startswith("crm.") or c in {"ops.access", "ops.shipment.view"},
|
|
("operaciones", "Operaciones"): lambda c: c.startswith("ops.")
|
|
or c in {"crm.access", "crm.account.view", "crm.quote.view", "crm.service_request.view", "fin.access", "fin.invoice.view"},
|
|
("facturacion", "Facturación"): lambda c: c.startswith("fin.")
|
|
or c in {"ops.access", "ops.shipment.view", "crm.access", "crm.account.view"},
|
|
("consulta", "Consulta"): lambda c: c.endswith(".access") or c.endswith(".view"),
|
|
}
|
|
|
|
|
|
def ensure_company(db) -> None:
|
|
"""Garantiza la empresa dev (a76.company id=1), requerida por la FK company_id de
|
|
los roles/permisos (core.company_roles → a76.company). La plantilla no la crea."""
|
|
from sqlalchemy import text
|
|
|
|
exists = db.execute(text("SELECT 1 FROM a76.company WHERE id = :id"), {"id": COMPANY_ID}).first()
|
|
if exists:
|
|
print(f"• Empresa id={COMPANY_ID} ya existe (a76.company)")
|
|
return
|
|
db.execute(
|
|
text("INSERT INTO a76.company (id, tenant_id) VALUES (:id, :tid)"),
|
|
{"id": COMPANY_ID, "tid": TENANT_ID},
|
|
)
|
|
db.execute(text("SELECT setval('a76.company_id_seq', (SELECT MAX(id) FROM a76.company))"))
|
|
db.commit()
|
|
print(f"✓ Empresa demo id={COMPANY_ID} creada (a76.company)")
|
|
|
|
|
|
def seed_carril_roles(db) -> None:
|
|
"""Crea los roles por carril del proceso con sus permisos (R-T-07). Idempotente."""
|
|
# Poblar el catálogo de permisos desde el registry de código
|
|
PermissionService(db).sync_permissions()
|
|
all_perms = db.query(Permission).filter(Permission.is_active == True).all() # noqa: E712
|
|
|
|
created = 0
|
|
for (code, name), belongs in _carril_roles().items():
|
|
role = (
|
|
db.query(CompanyRole)
|
|
.filter(CompanyRole.company_id == COMPANY_ID, CompanyRole.tenant_id == TENANT_ID, CompanyRole.code == code)
|
|
.first()
|
|
)
|
|
if not role:
|
|
role = CompanyRole(
|
|
company_id=COMPANY_ID, tenant_id=TENANT_ID, name=name, code=code,
|
|
description=f"Rol de carril: {name}", is_active=True,
|
|
)
|
|
db.add(role)
|
|
db.flush()
|
|
created += 1
|
|
existing_perm_ids = {
|
|
pid for (pid,) in db.query(RolePermission.permission_id).filter(RolePermission.company_role_id == role.id)
|
|
}
|
|
for perm in all_perms:
|
|
if belongs(perm.code) and perm.id not in existing_perm_ids:
|
|
db.add(RolePermission(
|
|
company_role_id=role.id, permission_id=perm.id,
|
|
tenant_id=TENANT_ID, company_id=COMPANY_ID,
|
|
))
|
|
db.commit()
|
|
print(f"✓ Roles por carril sembrados (nuevos: {created}) — Ventas, Operaciones, Facturación, Consulta")
|
|
|
|
|
|
def main() -> None:
|
|
db = CoreSessionLocal()
|
|
try:
|
|
ensure_tenant(db)
|
|
pipeline, stages = ensure_pipeline(db)
|
|
seed_sample_data(db, pipeline, stages)
|
|
seed_suppliers_and_related(db)
|
|
seed_commercial_and_ops(db)
|
|
seed_invoicing_and_events(db)
|
|
ensure_company(db)
|
|
seed_carril_roles(db)
|
|
print("\nSeed CRM completado.")
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|