173 lines
6.0 KiB
Python
173 lines
6.0 KiB
Python
|
|
"""Idempotent seed data for the B2B ordering portal.
|
||
|
|
|
||
|
|
Creates a small demo catalogue, a demo ordering customer + buyer user, a price
|
||
|
|
list, and a customer-specific price so the acceptance-criteria flow works
|
||
|
|
out of the box. Safe to run on every startup — it no-ops once seeded.
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import logging
|
||
|
|
|
||
|
|
from sqlalchemy import select
|
||
|
|
from sqlalchemy.orm import Session
|
||
|
|
|
||
|
|
from app.core.config import settings
|
||
|
|
from app.models.client_access import ClientAccount, ClientFeatureAccess, ClientUser
|
||
|
|
from app.models.ordering import (
|
||
|
|
CatalogueProduct,
|
||
|
|
CustomerPriceAssignment,
|
||
|
|
CustomerProductPrice,
|
||
|
|
NotificationSetting,
|
||
|
|
PriceList,
|
||
|
|
PriceListItem,
|
||
|
|
ProductCategory,
|
||
|
|
)
|
||
|
|
from app.services.client_access_service import (
|
||
|
|
MODULE_INDEX,
|
||
|
|
ensure_user_module_permissions,
|
||
|
|
)
|
||
|
|
|
||
|
|
logger = logging.getLogger("data_entry_app.seed")
|
||
|
|
|
||
|
|
ORDERING_TENANT = settings.client_tenant_id
|
||
|
|
|
||
|
|
_CATEGORIES = [
|
||
|
|
("grains", "Grains", 10),
|
||
|
|
("premixed", "Premixed Products", 20),
|
||
|
|
("bags", "Bags", 30),
|
||
|
|
("bulk_loads", "Bulk Loads", 40),
|
||
|
|
("custom_blends", "Custom Blends", 50),
|
||
|
|
("services", "Services & Delivery", 60),
|
||
|
|
]
|
||
|
|
|
||
|
|
_PRODUCTS = [
|
||
|
|
# (name, sku, category, uom, unit_size, moq, base_price, stock, requires_quote)
|
||
|
|
("Cracked Maize", "GRN-MAIZE-20", "grains", "20kg bag", "20kg", 1, 24.50, "in_stock", False),
|
||
|
|
("Whole Barley", "GRN-BARLEY-20", "grains", "20kg bag", "20kg", 1, 21.00, "in_stock", False),
|
||
|
|
("Layer Premix", "PMX-LAYER-20", "premixed", "20kg bag", "20kg", 1, 32.75, "in_stock", False),
|
||
|
|
("Calf Starter Premix", "PMX-CALF-20", "premixed", "20kg bag", "20kg", 5, 38.40, "low_stock", False),
|
||
|
|
("Bulka Bag (1T)", "BAG-BULKA-1T", "bags", "each", "1 tonne", 1, 9.50, "in_stock", False),
|
||
|
|
("Bulk Maize Load", "BLK-MAIZE-T", "bulk_loads", "tonne", "per tonne", 1, 685.00, "made_to_order", False),
|
||
|
|
("Custom Horse Blend", "CST-HORSE-20", "custom_blends", "20kg bag", "20kg", 10, None, "made_to_order", True),
|
||
|
|
("Delivery (per pallet)", "SVC-DELIVERY", "services", "pallet", "1 pallet", 1, 45.00, "in_stock", False),
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
def seed_ordering(db: Session) -> dict[str, int]:
|
||
|
|
created = {"categories": 0, "products": 0, "customer": 0, "pricing": 0}
|
||
|
|
|
||
|
|
# Categories
|
||
|
|
existing_categories = {
|
||
|
|
c.slug for c in db.scalars(select(ProductCategory).where(ProductCategory.tenant_id == ORDERING_TENANT)).all()
|
||
|
|
}
|
||
|
|
for slug, name, sort_order in _CATEGORIES:
|
||
|
|
if slug in existing_categories:
|
||
|
|
continue
|
||
|
|
db.add(ProductCategory(tenant_id=ORDERING_TENANT, slug=slug, name=name, sort_order=sort_order))
|
||
|
|
created["categories"] += 1
|
||
|
|
|
||
|
|
# Catalogue products
|
||
|
|
existing_skus = {
|
||
|
|
p.sku for p in db.scalars(select(CatalogueProduct).where(CatalogueProduct.tenant_id == ORDERING_TENANT)).all()
|
||
|
|
}
|
||
|
|
product_by_sku: dict[str, CatalogueProduct] = {}
|
||
|
|
for name, sku, category, uom, unit_size, moq, base_price, stock, requires_quote in _PRODUCTS:
|
||
|
|
if sku in existing_skus:
|
||
|
|
continue
|
||
|
|
product = CatalogueProduct(
|
||
|
|
tenant_id=ORDERING_TENANT,
|
||
|
|
name=name,
|
||
|
|
sku=sku,
|
||
|
|
category=category,
|
||
|
|
unit_of_measure=uom,
|
||
|
|
unit_size=unit_size,
|
||
|
|
min_order_quantity=moq,
|
||
|
|
base_price=base_price,
|
||
|
|
stock_status=stock,
|
||
|
|
requires_quote=requires_quote,
|
||
|
|
)
|
||
|
|
db.add(product)
|
||
|
|
product_by_sku[sku] = product
|
||
|
|
created["products"] += 1
|
||
|
|
db.flush()
|
||
|
|
|
||
|
|
# Notification settings row
|
||
|
|
if db.scalar(select(NotificationSetting).where(NotificationSetting.tenant_id == ORDERING_TENANT)) is None:
|
||
|
|
db.add(
|
||
|
|
NotificationSetting(
|
||
|
|
tenant_id=ORDERING_TENANT,
|
||
|
|
internal_recipients=settings.admin_email,
|
||
|
|
send_customer_confirmation=True,
|
||
|
|
require_po_number=False,
|
||
|
|
from_email=settings.admin_email,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
|
||
|
|
# Demo ordering customer + buyer user
|
||
|
|
demo = db.scalar(select(ClientAccount).where(ClientAccount.client_code == "RIVERSIDE"))
|
||
|
|
if demo is None:
|
||
|
|
demo = ClientAccount(
|
||
|
|
tenant_id="riverside-stockfeeds",
|
||
|
|
name="Riverside Stockfeeds",
|
||
|
|
client_code="RIVERSIDE",
|
||
|
|
status="active",
|
||
|
|
notes="Demo B2B ordering customer",
|
||
|
|
)
|
||
|
|
db.add(demo)
|
||
|
|
db.flush()
|
||
|
|
created["customer"] += 1
|
||
|
|
|
||
|
|
info = MODULE_INDEX["ordering"]
|
||
|
|
db.add(
|
||
|
|
ClientFeatureAccess(
|
||
|
|
tenant_id=demo.tenant_id,
|
||
|
|
client_account_id=demo.id,
|
||
|
|
feature_key="ordering",
|
||
|
|
feature_name=info["module_name"],
|
||
|
|
feature_group=info["module_group"],
|
||
|
|
description=info["description"],
|
||
|
|
enabled=True,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
buyer = ClientUser(
|
||
|
|
tenant_id=demo.tenant_id,
|
||
|
|
client_account_id=demo.id,
|
||
|
|
full_name="Riverside Buyer",
|
||
|
|
email="buyer@riverside.example",
|
||
|
|
role="buyer",
|
||
|
|
status="active",
|
||
|
|
is_new_user=False,
|
||
|
|
)
|
||
|
|
db.add(buyer)
|
||
|
|
db.flush()
|
||
|
|
ensure_user_module_permissions(db, buyer)
|
||
|
|
|
||
|
|
# A customer-specific contract price + a small discount on the rest.
|
||
|
|
maize = db.scalar(
|
||
|
|
select(CatalogueProduct).where(
|
||
|
|
CatalogueProduct.tenant_id == ORDERING_TENANT, CatalogueProduct.sku == "GRN-MAIZE-20"
|
||
|
|
)
|
||
|
|
)
|
||
|
|
if maize is not None:
|
||
|
|
db.add(
|
||
|
|
CustomerProductPrice(
|
||
|
|
tenant_id=ORDERING_TENANT,
|
||
|
|
client_account_id=demo.id,
|
||
|
|
product_id=maize.id,
|
||
|
|
unit_price=22.00,
|
||
|
|
rule_type="contract",
|
||
|
|
contract_reference="2026 supply agreement",
|
||
|
|
)
|
||
|
|
)
|
||
|
|
db.add(
|
||
|
|
CustomerPriceAssignment(
|
||
|
|
tenant_id=ORDERING_TENANT,
|
||
|
|
client_account_id=demo.id,
|
||
|
|
price_list_id=None,
|
||
|
|
discount_percent=5.0,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
created["pricing"] += 1
|
||
|
|
|
||
|
|
return created
|