544 lines
25 KiB
Python
544 lines
25 KiB
Python
"""Seed the procurement DB with realistic demo data (v1.3).
|
|
|
|
Wipes the app tables and inserts a clean, relational dataset:
|
|
8 topic categories (already bootstrapped at API startup), base units,
|
|
realistic brands/materials/suppliers, 3 projects with 2 warehouses each,
|
|
4 quotes covering every lifecycle state (offered / accepted-in-transit /
|
|
partially-received / arrived-completed) with termsheets, decisions,
|
|
partial receipts + warehouse allocations, and matching inventory rows.
|
|
|
|
Run AFTER the stack is up (api image has this script + the ORM):
|
|
|
|
docker compose run --rm api python tools/seed.py
|
|
|
|
User roles are linked to the Casdoor users by username (razaei = admin,
|
|
mousavi = مدیر تدارکات, mohaghegh/tester = مدیر پروژه). The Casdoor org
|
|
must be seeded first (python backend/tools/seed_casdoor.py); if a user is
|
|
missing there, the row is created with a pending casdoor_id and the role
|
|
can be assigned later from the Organization panel.
|
|
|
|
The existing junk/test rows (random names) are replaced — the dumps in
|
|
backups/ should be refreshed after running (see README).
|
|
"""
|
|
import asyncio
|
|
import sys
|
|
from datetime import date, datetime, timedelta, timezone
|
|
from decimal import Decimal
|
|
|
|
from sqlalchemy import select, text
|
|
|
|
from app.db.engine import AsyncSessionLocal
|
|
from app.db.init_db import BASE_UNITS, TOPIC_CATEGORIES
|
|
from app.models.catalog import (
|
|
Brand,
|
|
BrandMaterial,
|
|
Material,
|
|
MaterialCategory,
|
|
Unit,
|
|
)
|
|
from app.models.inventory import InventoryItem, InventoryTransaction, Warehouse
|
|
from app.models.procurement import (
|
|
ItemDecision,
|
|
ItemReceipt,
|
|
ItemReceiptAllocation,
|
|
ItemTermsheet,
|
|
LineItem,
|
|
LineItemProjects,
|
|
Project,
|
|
Quote,
|
|
QuoteEvent,
|
|
QuoteProjects,
|
|
Supplier,
|
|
SupplierMaterial,
|
|
Termsheet,
|
|
TermsheetScheduleStep,
|
|
User,
|
|
)
|
|
|
|
NOW = datetime.now(timezone.utc)
|
|
|
|
|
|
def d(days_ago: int) -> datetime:
|
|
return NOW - timedelta(days=days_ago)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Casdoor user ids (username -> id) — used to bind roles to real identities
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def casdoor_user_ids() -> dict[str, str]:
|
|
import httpx
|
|
from app.config import settings
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=15) as client:
|
|
login = await client.post(
|
|
f"{settings.CASDOOR_ENDPOINT}/api/login",
|
|
json={
|
|
"type": "login",
|
|
"organization": "built-in",
|
|
"username": settings.CASDOOR_ADMIN_USER,
|
|
"password": settings.CASDOOR_ADMIN_PASSWORD,
|
|
"application": "app-built-in",
|
|
},
|
|
)
|
|
if login.status_code != 200 or (login.json() or {}).get("status") != "ok":
|
|
print("!! Casdoor admin login failed — roles will use pending ids")
|
|
return {}
|
|
resp = await client.get(
|
|
f"{settings.CASDOOR_ENDPOINT}/api/get-users?owner={settings.CASDOOR_ORGANIZATION}"
|
|
)
|
|
body = resp.json()
|
|
if body.get("status") != "ok":
|
|
return {}
|
|
return {str(u.get("name")): str(u.get("id")) for u in (body.get("data") or []) if u.get("id")}
|
|
except Exception as exc: # noqa: BLE001 — best effort
|
|
print(f"!! Casdoor unreachable ({exc}) — roles will use pending ids")
|
|
return {}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def get_or_create_unit(db, code: str, name_fa: str, dimension: str, si_factor=1, is_base=False) -> int:
|
|
unit = await db.scalar(select(Unit).where(Unit.code == code))
|
|
if unit:
|
|
return unit.id
|
|
unit = Unit(code=code, name_fa=name_fa, dimension=dimension, si_factor=si_factor, is_base=is_base)
|
|
db.add(unit)
|
|
await db.flush()
|
|
return unit.id
|
|
|
|
|
|
async def get_category(db, slug: str) -> MaterialCategory:
|
|
cat = await db.scalar(select(MaterialCategory).where(MaterialCategory.slug == slug))
|
|
if cat:
|
|
return cat
|
|
meta = next(c for c in TOPIC_CATEGORIES if c["slug"] == slug)
|
|
cat = MaterialCategory(slug=slug, name=meta["name"])
|
|
db.add(cat)
|
|
await db.flush()
|
|
return cat
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main
|
|
# ---------------------------------------------------------------------------
|
|
|
|
USER_PLAN = [
|
|
# (casdoor username, first, last, role)
|
|
("ali.mohaghegh", "علی", "محقق", "admin"),
|
|
("razaei", "علی", "رضایی", "admin"),
|
|
("mousavi", "حسین", "موسوی", "procurement_manager"),
|
|
("mohaghegh", "محمد", "محقق", "pm"),
|
|
("tester", "کاربر", "تست", "pm"),
|
|
]
|
|
|
|
MATERIALS = [
|
|
# (name, category_slug, unit_code, brand_name)
|
|
("کابل مسی افشان ۲×۲٫۵", "electrical-equipment-supplies", "m", "کابل البرز"),
|
|
("کلید و پریز آرین مدل سفید", "electrical-equipment-supplies", "piece", None),
|
|
("میلگرد آجدار A3 سایز ۱۴", "metals-steel-sections", "ton", "ذوب آهن اصفهان"),
|
|
("تیرآهن IPE ۱۴", "metals-steel-sections", "ton", "ذوب آهن اصفهان"),
|
|
("سیمان تیپ ۲", "construction-materials", "bag", "سیمان تهران"),
|
|
("بلوک سیمانی ۲۰×۲۰×۴۰", "construction-materials", "piece", None),
|
|
("لوله پلیاتیلن PE100 سایز ۱۶", "mechanical-installations-pipes", "m", "پارس لوله"),
|
|
("لوله پلیاتیلن PE100 سایز ۲۵", "mechanical-installations-pipes", "m", "پارس لوله"),
|
|
("چسب بتن", "construction-chemicals", "kg", "شیمی ساختمان"),
|
|
("پنجره UPVC دو جداره", "doors-windows-glass", "piece", None),
|
|
("سرامیک پرسلان ۶۰×۶۰", "finishing-interior-decoration", "piece", None),
|
|
("ویبراتور بتن", "construction-tools-equipment", "piece", None),
|
|
]
|
|
|
|
SUPPLIERS = [
|
|
# (name, category_slug, location, phone, contact_person, tax_id)
|
|
("شرکت صنایع کابل البرز", "electrical-equipment-supplies", "تهران — شهرک صنعتی", "۰۲۱-۳۳۹۹۱۲۳۴", "مهندس کریمی", "10102345678"),
|
|
("فولاد ناب آریا", "metals-steel-sections", "اصفهان — ذوبآهن", "۰۳۱-۳۴۵۶۷۸۹۰", "آقای صادقی", "10203456789"),
|
|
("پخش لوله و اتصالات پارس", "mechanical-installations-pipes", "تهران — بازار آهن", "۰۲۱-۵۵۹۹۸۷۶۵", "آقای نادری", "10304567890"),
|
|
("بازرگانی مصالح تهران", "construction-materials", "تهران — خیابان شوش", "۰۲۱-۶۶۷۷۸۸۹۹", "خانم احمدی", "10405678901"),
|
|
]
|
|
|
|
PROJECTS = [
|
|
# (name, description, warehouse names)
|
|
("مجتمع مسکونی آرام", "ساخت ۲۴۰ واحد مسکونی — منطقه ۲۲ تهران", ["انبار مرکزی آرام", "انبار مصالح آرام"]),
|
|
("برج اداری سپهر", "برج اداری ۱۸ طبقه — سعادتآباد", ["انبار سپهر یک", "انبار سپهر دو"]),
|
|
("پروژه زیرساخت شرق تهران", "شبکه معابر و زیرساخت — منطقه ۱۳", ["انبار شرق اصلی", "انبار سیار شرق"]),
|
|
]
|
|
|
|
# quote index -> (supplier idx, project idx, status, days since quote_date,
|
|
# days since accepted, [(material idx, brand?, qty, unit_price), ...])
|
|
QUOTES = [
|
|
{
|
|
"supplier": 0, "project": 0, "status": "offered", "quote_days": 12, "accepted_days": None,
|
|
"items": [(0, 500, "120000"), (1, 300, "65000")],
|
|
},
|
|
{
|
|
"supplier": 1, "project": 0, "status": "accepted", "quote_days": 20, "accepted_days": 13,
|
|
"items": [(2, 20, "38000000"), (3, 15, "52000000")],
|
|
},
|
|
{
|
|
"supplier": 2, "project": 1, "status": "accepted", "quote_days": 15, "accepted_days": 9,
|
|
"items": [(6, 1000, "45000"), (7, 600, "145000")],
|
|
# partial receipts: item0 1000/1000 (400→wh0, 600→wh1), item1 200/600 (→wh0)
|
|
"receipts": [
|
|
{"item": 0, "qty": 400, "wh": 0, "days": 7},
|
|
{"item": 0, "qty": 600, "wh": 1, "days": 4},
|
|
{"item": 1, "qty": 200, "wh": 0, "days": 2},
|
|
],
|
|
},
|
|
{
|
|
"supplier": 3, "project": 2, "status": "arrived", "quote_days": 30, "accepted_days": 24,
|
|
"items": [(4, 400, "220000"), (5, 2000, "28000")],
|
|
"receipts": [
|
|
{"item": 0, "qty": 400, "wh": 0, "days": 3},
|
|
{"item": 1, "qty": 2000, "wh": 1, "days": 3},
|
|
],
|
|
},
|
|
]
|
|
|
|
UNITS_BY_CODE = {u["code"]: u for u in BASE_UNITS}
|
|
|
|
|
|
async def run() -> None:
|
|
casdoor_ids = await casdoor_user_ids()
|
|
async with AsyncSessionLocal() as db:
|
|
# ---- wipe previous (junk) data, keep categories/units bootstrap ----
|
|
await db.execute(text(
|
|
"TRUNCATE TABLE quote_attachments, quote_events, quote_projects, line_item_projects, "
|
|
"item_receipt_allocations, item_receipts, inventory_transactions, inventory, "
|
|
"item_decisions, item_termsheets, termsheet_schedule_steps, termsheets, line_item_specs, "
|
|
"line_items, quotes, supplier_materials, suppliers, brand_materials, "
|
|
"material_attribute_values, materials, brands, warehouses, projects, users, "
|
|
"standard_conversions "
|
|
"RESTART IDENTITY CASCADE"
|
|
))
|
|
# Only the 8 canonical topic categories may exist — any leftover slug
|
|
# (e.g. «سیم», «کابل» from earlier testing) is removed so the topic
|
|
# grids keep showing exactly the same 8 categories as always.
|
|
await db.execute(
|
|
text(
|
|
"DELETE FROM material_categories WHERE slug NOT IN ("
|
|
"'mechanical-installations-pipes','electrical-equipment-supplies',"
|
|
"'doors-windows-glass','metals-steel-sections','construction-materials',"
|
|
"'construction-chemicals','finishing-interior-decoration',"
|
|
"'construction-tools-equipment')"
|
|
)
|
|
)
|
|
|
|
# ---- users (roles bound to Casdoor identities by username) ----
|
|
users: dict[str, User] = {}
|
|
for name, first, last, role in USER_PLAN:
|
|
cid = casdoor_ids.get(name)
|
|
if not cid:
|
|
print(f"!! Casdoor user '{name}' not found — created with pending id (run seed_casdoor.py)")
|
|
user = User(
|
|
casdoor_id=cid or f"pending-{name}",
|
|
first_name=first,
|
|
last_name=last,
|
|
role=role,
|
|
)
|
|
db.add(user)
|
|
users[name] = user
|
|
await db.flush()
|
|
|
|
# ---- units (base set; fetch-or-create) ----
|
|
unit_ids: dict[str, int] = {}
|
|
for meta in BASE_UNITS:
|
|
unit_ids[meta["code"]] = await get_or_create_unit(
|
|
db, meta["code"], meta["name_fa"], meta["dimension"], meta["si_factor"], meta["is_base"]
|
|
)
|
|
|
|
# ---- brands ----
|
|
brand_ids: dict[str, object] = {}
|
|
brand_names = sorted({b for _, _, _, b in MATERIALS if b})
|
|
for bname in brand_names:
|
|
brand = Brand(name=bname, country="ایران")
|
|
db.add(brand)
|
|
await db.flush()
|
|
brand_ids[bname] = brand.id
|
|
|
|
# ---- materials (one per category at least) + brand links ----
|
|
material_by_name: dict[str, Material] = {}
|
|
brand_material_by_material: dict[str, object] = {}
|
|
for name, cat_slug, unit_code, brand_name in MATERIALS:
|
|
cat = await get_category(db, cat_slug)
|
|
material = Material(
|
|
name=name,
|
|
topic=cat.name,
|
|
category_id=cat.id, # v2.8: dashboard Category→Material cascade
|
|
canonical_unit_id=unit_ids[unit_code],
|
|
status="active",
|
|
)
|
|
db.add(material)
|
|
await db.flush()
|
|
material_by_name[name] = material
|
|
if brand_name:
|
|
bm = BrandMaterial(brand_id=brand_ids[brand_name], material_id=material.id, is_active=True)
|
|
db.add(bm)
|
|
await db.flush()
|
|
brand_material_by_material[name] = bm.id
|
|
|
|
# ---- suppliers + supplier-material links ----
|
|
suppliers: list[Supplier] = []
|
|
for name, cat_slug, location, phone, contact, tax in SUPPLIERS:
|
|
cat = await get_category(db, cat_slug)
|
|
sup = Supplier(
|
|
name=name, category_id=cat.id, location=location,
|
|
phone=phone, contact_person=contact, tax_id=tax,
|
|
)
|
|
db.add(sup)
|
|
await db.flush()
|
|
suppliers.append(sup)
|
|
# last-quoted prices: link each supplier to 2 materials of its category
|
|
sup_cat_slug = {name: cat_slug for name, cat_slug, *_ in SUPPLIERS}
|
|
for sup in suppliers:
|
|
target_slug = sup_cat_slug.get(sup.name)
|
|
cat_materials = [material_by_name[mat_name] for mat_name, c_slug, *_ in MATERIALS if c_slug == target_slug]
|
|
for m in cat_materials[:2]:
|
|
db.add(SupplierMaterial(
|
|
supplier_id=sup.id, material_id=m.id, is_active=True,
|
|
last_quoted_price=Decimal("1000000"), last_quoted_at=d(6),
|
|
))
|
|
await db.flush()
|
|
|
|
# ---- projects + warehouses ----
|
|
project_rows: list[Project] = []
|
|
warehouse_rows: list[list[Warehouse]] = []
|
|
pm_names = {"مجتمع مسکونی آرام": "tester", "برج اداری سپهر": "mohaghegh", "پروژه زیرساخت شرق تهران": None}
|
|
for name, desc, wh_names in PROJECTS:
|
|
proj = Project(name=name, description=desc, start_date=date.today().replace(year=date.today().year))
|
|
pm = pm_names.get(name)
|
|
if pm:
|
|
proj.pm_user_id = users[pm].id
|
|
db.add(proj)
|
|
await db.flush()
|
|
project_rows.append(proj)
|
|
whs: list[Warehouse] = []
|
|
for wn in wh_names:
|
|
wh = Warehouse(project_id=proj.id, name=wn)
|
|
db.add(wh)
|
|
await db.flush()
|
|
whs.append(wh)
|
|
warehouse_rows.append(whs)
|
|
await db.flush()
|
|
|
|
# ---- quotes ----
|
|
for qidx, qdef in enumerate(QUOTES):
|
|
sup = suppliers[qdef["supplier"]]
|
|
proj = project_rows[qdef["project"]]
|
|
accepted_at = d(qdef["accepted_days"]) if qdef["accepted_days"] is not None else None
|
|
quote = Quote(
|
|
supplier_id=sup.id,
|
|
project_id=proj.id,
|
|
quote_date=d(qdef["quote_days"]),
|
|
expiry_date=(d(qdef["quote_days"]) + timedelta(days=30)).date(),
|
|
accepted_date=accepted_at,
|
|
arrived_date=d(1) if qdef["status"] == "arrived" else None,
|
|
status=qdef["status"],
|
|
is_official=True,
|
|
notes="پیشنهاد نمونه — داده دمو",
|
|
)
|
|
db.add(quote)
|
|
await db.flush()
|
|
db.add(QuoteProjects(quote_id=quote.id, project_id=proj.id))
|
|
|
|
db.add(QuoteEvent(quote_id=quote.id, event_type="offered", occurred_at=d(qdef["quote_days"]), actor=sup.name))
|
|
if accepted_at:
|
|
db.add(QuoteEvent(quote_id=quote.id, event_type="accepted", occurred_at=accepted_at, actor=users["razaei"].first_name))
|
|
if qdef["status"] == "arrived":
|
|
db.add(QuoteEvent(quote_id=quote.id, event_type="arrived", occurred_at=d(1), actor=users["razaei"].first_name))
|
|
|
|
ts = Termsheet(quote_id=quote.id, label="گزینه نقدی — دمو", status="accepted" if accepted_at else "offered")
|
|
db.add(ts)
|
|
await db.flush()
|
|
total_all = Decimal("0")
|
|
|
|
line_items: list[LineItem] = []
|
|
for item_idx, (mat_idx, qty, price) in enumerate(qdef["items"]):
|
|
mat_name = MATERIALS[mat_idx][0]
|
|
mat = material_by_name[mat_name]
|
|
li = LineItem(
|
|
quote_id=quote.id,
|
|
material_id=mat.id,
|
|
brand_material_id=brand_material_by_material.get(mat_name),
|
|
unit_id=mat.canonical_unit_id,
|
|
quantity=Decimal(str(qty)),
|
|
)
|
|
db.add(li)
|
|
await db.flush()
|
|
line_items.append(li)
|
|
db.add(LineItemProjects(line_item_id=li.id, project_id=proj.id))
|
|
its = ItemTermsheet(line_item_id=li.id, termsheet_id=ts.id, unit_price=Decimal(price))
|
|
db.add(its)
|
|
await db.flush()
|
|
total_all += Decimal(str(qty)) * Decimal(price)
|
|
|
|
if accepted_at:
|
|
decision = ItemDecision(
|
|
line_item_id=li.id,
|
|
status="chosen",
|
|
project_id=proj.id,
|
|
chosen_item_termsheet_id=its.id,
|
|
adjusted_unit_price=Decimal(price),
|
|
adjusted_notes="تأیید قیمت — دمو",
|
|
decided_at=accepted_at,
|
|
)
|
|
db.add(decision)
|
|
|
|
# schedule steps: ۳۰٪ پیش، ۴۰٪ حین تحویل، ۳۰٪ پس از تحویل
|
|
p1 = (total_all * Decimal("30") / 100).quantize(Decimal("1"))
|
|
p2 = (total_all * Decimal("40") / 100).quantize(Decimal("1"))
|
|
p3 = total_all - p1 - p2
|
|
for step_no, amount in enumerate([p1, p2, p3], start=1):
|
|
db.add(TermsheetScheduleStep(
|
|
termsheet_id=ts.id, step_no=step_no,
|
|
due_date=(d(qdef["quote_days"]) + timedelta(days=step_no * 20)).date(),
|
|
amount=amount,
|
|
))
|
|
|
|
# ---- receipts + allocations + inventory (only for quotes with receipts) ----
|
|
for rdef in qdef.get("receipts", []):
|
|
li = line_items[rdef["item"]]
|
|
mat = material_by_name[MATERIALS[qdef["items"][rdef["item"]][0]][0]]
|
|
wh = warehouse_rows[qdef["project"]][rdef["wh"]]
|
|
decision = await db.scalar(select(ItemDecision).where(ItemDecision.line_item_id == li.id))
|
|
confirmer = users["mohaghegh"] if qdef["project"] == 1 else users["tester"] if qdef["project"] == 0 else users["razaei"]
|
|
receipt = ItemReceipt(
|
|
item_decision_id=decision.id,
|
|
confirmed_by=confirmer.id,
|
|
received_at=d(rdef["days"]),
|
|
received_qty=Decimal(str(rdef["qty"])),
|
|
on_time=True,
|
|
)
|
|
db.add(receipt)
|
|
await db.flush()
|
|
alloc = ItemReceiptAllocation(receipt_id=receipt.id, warehouse_id=wh.id, quantity=Decimal(str(rdef["qty"])))
|
|
db.add(alloc)
|
|
await db.flush()
|
|
|
|
inv = await db.scalar(select(InventoryItem).where(
|
|
InventoryItem.warehouse_id == wh.id,
|
|
InventoryItem.material_id == mat.id,
|
|
InventoryItem.specs_hash == "default",
|
|
))
|
|
if inv is None:
|
|
inv = InventoryItem(
|
|
warehouse_id=wh.id, material_id=mat.id, brand_id=None,
|
|
quantity=Decimal("0"), unit_id=mat.canonical_unit_id,
|
|
specs={}, specs_hash="default",
|
|
)
|
|
db.add(inv)
|
|
await db.flush()
|
|
inv.quantity += Decimal(str(rdef["qty"]))
|
|
|
|
db.add(InventoryTransaction(
|
|
inventory_id=inv.id,
|
|
warehouse_id=wh.id,
|
|
receipt_allocation_id=alloc.id,
|
|
transaction_type="PURCHASE",
|
|
quantity=Decimal(str(rdef["qty"])),
|
|
performed_by=f"{confirmer.first_name} {confirmer.last_name}",
|
|
note=f"رسید {str(receipt.id)[:8]} — {qdef['status']}",
|
|
transaction_date=d(rdef["days"]),
|
|
))
|
|
await db.flush()
|
|
|
|
# ---- MVP dashboard demo ledger -----------------------------------
|
|
# Multi-day history so every widget has real data to show:
|
|
# - two old PURCHASEs 70/85 days ago → dead stock rows (60-90d)
|
|
# - withdrawals/returns/waste over the last ~12 days → flow series
|
|
# - tickets dated TODAY (performed_by names) → daily control
|
|
# Quantities are hand-checked to keep every row >= 0.
|
|
|
|
async def ensure_inv(mat_idx: int, project_idx: int, wh_idx: int) -> InventoryItem:
|
|
mat = material_by_name[MATERIALS[mat_idx][0]]
|
|
wh = warehouse_rows[project_idx][wh_idx]
|
|
inv = await db.scalar(select(InventoryItem).where(
|
|
InventoryItem.warehouse_id == wh.id,
|
|
InventoryItem.material_id == mat.id,
|
|
InventoryItem.specs_hash == "default",
|
|
))
|
|
if inv is None:
|
|
inv = InventoryItem(
|
|
warehouse_id=wh.id, material_id=mat.id, brand_id=None,
|
|
quantity=Decimal("0"), unit_id=mat.canonical_unit_id,
|
|
specs={}, specs_hash="default",
|
|
)
|
|
db.add(inv)
|
|
await db.flush()
|
|
return inv
|
|
|
|
async def add_tx(inv: InventoryItem, wh: Warehouse, tx_type: str, qty: int,
|
|
days_ago: int, performed_by: str, note: str | None = None) -> None:
|
|
inv.quantity += Decimal(str(qty))
|
|
db.add(InventoryTransaction(
|
|
inventory_id=inv.id,
|
|
warehouse_id=wh.id,
|
|
transaction_type=tx_type,
|
|
quantity=Decimal(str(qty)),
|
|
performed_by=performed_by,
|
|
note=note or "تاریخچه دمو داشبورد",
|
|
transaction_date=d(days_ago),
|
|
))
|
|
|
|
wh_aram, wh_sepehr, wh_shargh = warehouse_rows[0], warehouse_rows[1], warehouse_rows[2]
|
|
|
|
# dead stock: cable + switches bought long ago, untouched since
|
|
cable = await ensure_inv(0, 0, 0)
|
|
await add_tx(cable, wh_aram[0], "PURCHASE", 800, 70, "حسین موسوی", "خرید اولیه — دمو")
|
|
keys = await ensure_inv(1, 0, 0)
|
|
await add_tx(keys, wh_aram[0], "PURCHASE", 200, 85, "حسین موسوی", "خرید اولیه — دمو")
|
|
|
|
# cement (انبار مرکزی آرام is project0 — cement lives in شرق اصلی)
|
|
cement = await ensure_inv(4, 2, 0)
|
|
await add_tx(cement, wh_shargh[0], "WITHDRAWAL", -80, 10, "رضا محمدی")
|
|
await add_tx(cement, wh_shargh[0], "WITHDRAWAL", -60, 8, "رضا محمدی")
|
|
await add_tx(cement, wh_shargh[0], "RETURN", 20, 7, "علی رضایی", "برگشت از سایت — مازاد")
|
|
await add_tx(cement, wh_shargh[0], "DAMAGE", -10, 5, "کاربر تست")
|
|
await add_tx(cement, wh_shargh[0], "WITHDRAWAL", -50, 1, "رضا محمدی")
|
|
await add_tx(cement, wh_shargh[0], "WITHDRAWAL", -30, 0, "رضا محمدی")
|
|
await add_tx(cement, wh_shargh[0], "RETURN", 12, 0, "علی رضایی", "برگشت از سایت — امروز")
|
|
|
|
blocks = await ensure_inv(5, 2, 1)
|
|
await add_tx(blocks, wh_shargh[1], "WITHDRAWAL", -400, 12, "رضا محمدی")
|
|
await add_tx(blocks, wh_shargh[1], "WITHDRAWAL", -300, 9, "کاربر تست")
|
|
await add_tx(blocks, wh_shargh[1], "LOSS", -15, 6, "کاربر تست")
|
|
await add_tx(blocks, wh_shargh[1], "WASTE", -25, 4, "رضا محمدی")
|
|
await add_tx(blocks, wh_shargh[1], "WITHDRAWAL", -250, 0, "حسین موسوی")
|
|
|
|
pe16_a = await ensure_inv(6, 1, 0)
|
|
await add_tx(pe16_a, wh_sepehr[0], "WITHDRAWAL", -100, 6, "محمد محقق")
|
|
await add_tx(pe16_a, wh_sepehr[0], "RETURN", 25, 3, "محمد محقق", "برگشت — برش اضافه")
|
|
await add_tx(pe16_a, wh_sepehr[0], "WITHDRAWAL", -80, 0, "محمد محقق")
|
|
|
|
pe16_b = await ensure_inv(6, 1, 1)
|
|
await add_tx(pe16_b, wh_sepehr[1], "WITHDRAWAL", -150, 11, "محمد محقق")
|
|
await add_tx(pe16_b, wh_sepehr[1], "WASTE", -20, 7, "محمد محقق")
|
|
await add_tx(pe16_b, wh_sepehr[1], "WITHDRAWAL", -120, 2, "محمد محقق")
|
|
|
|
pe25 = await ensure_inv(7, 1, 0)
|
|
await add_tx(pe25, wh_sepehr[0], "WITHDRAWAL", -60, 9, "محمد محقق")
|
|
await add_tx(pe25, wh_sepehr[0], "RETURN", 10, 5, "محمد محقق")
|
|
await add_tx(pe25, wh_sepehr[0], "DAMAGE", -8, 3, "محمد محقق")
|
|
|
|
await db.flush()
|
|
|
|
await db.commit()
|
|
|
|
# summary
|
|
print("\nSeed complete ✓")
|
|
print("Projects : 3 (2 warehouses each)")
|
|
print("Materials :", len(MATERIALS), "across all 8 categories")
|
|
print("Suppliers :", len(SUPPLIERS))
|
|
print("Quotes :", len(QUOTES), "(offered / accepted-in-transit / partial / arrived)")
|
|
print("Roles : razaei=admin · mousavi=مدیر تدارکات · mohaghegh/tester=مدیر پروژه")
|
|
print("\nLogin: http://localhost — dev-mode admin: razaei / 123456")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
asyncio.run(run())
|
|
except KeyboardInterrupt:
|
|
sys.exit(130)
|