v1.3 - client and admin scaffolding

This commit is contained in:
2026-04-25 22:51:36 +12:00
parent bc211ffcc8
commit 8cf9bfb441
54 changed files with 8882 additions and 1248 deletions
+156 -58
View File
@@ -1,76 +1,174 @@
from datetime import date
from datetime import date, datetime
from sqlalchemy import select
from app.db.session import Base, SessionLocal, engine
from app.models.assumption import FreightCostRule, PackagingCostRule, ProcessCostRule
from app.models.client_access import ClientAccount, ClientFeatureAccess, ClientUser
from app.models.mix import Mix, MixIngredient
from app.models.product import Product
from app.models.raw_material import RawMaterial, RawMaterialPriceVersion
CLIENT_FEATURES = [
("dashboard", "Dashboard", "workspace", "Top-level operational dashboard"),
("raw_materials", "Raw Materials", "costing", "Maintain live material costs and versions"),
("mix_master", "Mix Master", "costing", "Create and maintain mix worksheets"),
("products", "Products", "pricing", "Review finished product pricing"),
("scenarios", "Scenarios", "planning", "Run scenario overrides and comparisons"),
("powerbi_export", "Power BI Export", "reporting", "Expose client access data to BI consumers"),
]
def seed_client_access(db):
existing = db.scalar(select(ClientAccount.id))
if existing is not None:
return
specialty = ClientAccount(
tenant_id="hunter-premium-produce",
name="Hunter Premium Produce",
client_code="HPP",
status="active",
powerbi_workspace="hunter-premium-produce-prod",
notes="Primary production client for the Lean 101 admin and access workflows",
)
loft = ClientAccount(
tenant_id="loft-grains",
name="Loft Grains",
client_code="LOFT",
status="onboarding",
powerbi_workspace="farm-ops-sandbox",
notes="Onboarding workspace used to test staged user enablement",
)
db.add_all([specialty, loft])
db.flush()
specialty.users.extend(
[
ClientUser(
tenant_id=specialty.tenant_id,
full_name="Amelia Hart",
email="operator@example.com",
role="admin",
status="active",
is_new_user=False,
last_login_at=datetime(2026, 4, 24, 11, 30),
),
ClientUser(
tenant_id=specialty.tenant_id,
full_name="Ethan Cole",
email="ethan.cole@hunterpremiumproduce.example",
role="operator",
status="invited",
is_new_user=True,
),
]
)
loft.users.extend(
[
ClientUser(
tenant_id=loft.tenant_id,
full_name="Ruby Singh",
email="ruby.singh@loftgrains.example",
role="viewer",
status="active",
is_new_user=False,
last_login_at=datetime(2026, 4, 22, 9, 10),
)
]
)
enabled_feature_map = {
"hunter-premium-produce": {"dashboard", "raw_materials", "mix_master", "products", "scenarios", "powerbi_export"},
"loft-grains": {"dashboard", "products", "powerbi_export"},
}
for client in (specialty, loft):
enabled_keys = enabled_feature_map[client.tenant_id]
for feature_key, feature_name, feature_group, description in CLIENT_FEATURES:
client.features.append(
ClientFeatureAccess(
tenant_id=client.tenant_id,
feature_key=feature_key,
feature_name=feature_name,
feature_group=feature_group,
description=description,
enabled=feature_key in enabled_keys,
)
)
def seed_costing_workspace(db):
existing = db.scalar(select(RawMaterial.id))
if existing is not None:
return
tenant_id = "hunter-premium-produce"
maize = RawMaterial(tenant_id=tenant_id, name="Maize", supplier="Example Supplier", unit_of_measure="tonne", kg_per_unit=1000, status="active")
barley = RawMaterial(tenant_id=tenant_id, name="Barley", supplier="Example Supplier", unit_of_measure="tonne", kg_per_unit=1000, status="active")
acid_buf = RawMaterial(tenant_id=tenant_id, name="Acid Buf", supplier="Example Supplier", unit_of_measure="bag", kg_per_unit=25, status="active")
maize.price_versions.append(RawMaterialPriceVersion(tenant_id=tenant_id, market_value=520, waste_percentage=0.02, effective_date=date(2026, 4, 1)))
barley.price_versions.append(RawMaterialPriceVersion(tenant_id=tenant_id, market_value=470, waste_percentage=0.015, effective_date=date(2026, 4, 1)))
acid_buf.price_versions.append(RawMaterialPriceVersion(tenant_id=tenant_id, market_value=39, waste_percentage=0.0, effective_date=date(2026, 4, 1)))
db.add_all([maize, barley, acid_buf])
db.flush()
db.add_all(
[
ProcessCostRule(tenant_id=tenant_id, process_name="standard_bagging", grading_cost=0.055, bagging_cost=0.04, cracking_cost=0.0),
ProcessCostRule(tenant_id=tenant_id, process_name="bulk_loadout", grading_cost=0.03, bagging_cost=0.0, cracking_cost=0.0),
PackagingCostRule(tenant_id=tenant_id, sale_type="standard", unit_of_measure="20kg bag", own_bag=False, bag_cost=0.63),
PackagingCostRule(tenant_id=tenant_id, sale_type="bulka", unit_of_measure="550kg bulka", own_bag=False, bag_cost=7.5),
FreightCostRule(tenant_id=tenant_id, sale_type="standard", unit_of_measure="20kg bag", cost_per_unit=1.45),
FreightCostRule(tenant_id=tenant_id, sale_type="bulka", unit_of_measure="550kg bulka", cost_per_unit=18.0),
]
)
db.flush()
mix = Mix(tenant_id=tenant_id, client_name="Hunter Premium Produce", name="Hunter Orchard Blend", status="active", version=1, notes="Seed recipe for MVP")
db.add(mix)
db.flush()
db.add_all(
[
MixIngredient(tenant_id=tenant_id, mix_id=mix.id, raw_material_id=maize.id, quantity_kg=180),
MixIngredient(tenant_id=tenant_id, mix_id=mix.id, raw_material_id=barley.id, quantity_kg=95),
MixIngredient(tenant_id=tenant_id, mix_id=mix.id, raw_material_id=acid_buf.id, quantity_kg=5),
]
)
db.flush()
db.add(
Product(
tenant_id=tenant_id,
client_name="Hunter Premium Produce",
item_id="SKU-001",
name="Hunter Orchard Blend 20kg",
mix_id=mix.id,
sale_type="standard",
own_bag=False,
unit_of_measure="20kg bag",
items_per_pallet=50,
bagging_process="standard_bagging",
distributor_margin=0.225,
wholesale_margin=0.18,
notes="Reference product for formula parity work",
)
)
def seed_if_empty():
Base.metadata.create_all(bind=engine)
with SessionLocal() as db:
existing = db.scalar(select(RawMaterial.id))
if existing is not None:
return
maize = RawMaterial(name="Maize", supplier="Example Supplier", unit_of_measure="tonne", kg_per_unit=1000, status="active")
barley = RawMaterial(name="Barley", supplier="Example Supplier", unit_of_measure="tonne", kg_per_unit=1000, status="active")
acid_buf = RawMaterial(name="Acid Buf", supplier="Example Supplier", unit_of_measure="bag", kg_per_unit=25, status="active")
maize.price_versions.append(RawMaterialPriceVersion(market_value=520, waste_percentage=0.02, effective_date=date(2026, 4, 1)))
barley.price_versions.append(RawMaterialPriceVersion(market_value=470, waste_percentage=0.015, effective_date=date(2026, 4, 1)))
acid_buf.price_versions.append(RawMaterialPriceVersion(market_value=39, waste_percentage=0.0, effective_date=date(2026, 4, 1)))
db.add_all([maize, barley, acid_buf])
db.flush()
db.add_all(
[
ProcessCostRule(process_name="standard_bagging", grading_cost=0.055, bagging_cost=0.04, cracking_cost=0.0),
ProcessCostRule(process_name="bulk_loadout", grading_cost=0.03, bagging_cost=0.0, cracking_cost=0.0),
PackagingCostRule(sale_type="standard", unit_of_measure="20kg bag", own_bag=False, bag_cost=0.63),
PackagingCostRule(sale_type="bulka", unit_of_measure="550kg bulka", own_bag=False, bag_cost=7.5),
FreightCostRule(sale_type="standard", unit_of_measure="20kg bag", cost_per_unit=1.45),
FreightCostRule(sale_type="bulka", unit_of_measure="550kg bulka", cost_per_unit=18.0),
]
)
db.flush()
mix = Mix(client_name="Specialty Feeds", name="Pigeon Mix", status="active", version=1, notes="Seed recipe for MVP")
db.add(mix)
db.flush()
db.add_all(
[
MixIngredient(mix_id=mix.id, raw_material_id=maize.id, quantity_kg=180),
MixIngredient(mix_id=mix.id, raw_material_id=barley.id, quantity_kg=95),
MixIngredient(mix_id=mix.id, raw_material_id=acid_buf.id, quantity_kg=5),
]
)
db.flush()
db.add(
Product(
client_name="Specialty Feeds",
item_id="SKU-001",
name="Specialty Pigeon Breeder 20kg",
mix_id=mix.id,
sale_type="standard",
own_bag=False,
unit_of_measure="20kg bag",
items_per_pallet=50,
bagging_process="standard_bagging",
distributor_margin=0.225,
wholesale_margin=0.18,
notes="Reference product for formula parity work",
)
)
seed_costing_workspace(db)
seed_client_access(db)
db.commit()
if __name__ == "__main__":
seed_if_empty()