v0.1.14 - b2b portal
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user