Files
ERP/bot/tools/seed_mock.py
T
whqreza a61da4a378 feat(bot): phase-6 calibration — mock seed, A/B harness, low-trust fallback
- tools/seed_mock.py: idempotent additive mock data (uuid5 ids) covering all
  tables (units, brands, specs, suppliers, materials + EAV/brand/unit links,
  projects, warehouses, inventory, quotes with termsheets/line items/decisions,
  receipts) so phases 6-7 can be exercised end-to-end
- tools/calibrate_matching.py: A/B difflib vs Jev on 35 scenarios (31 positive
  + 4 negative); collects RAW picks with widened bands, classifies against the
  configured thresholds, sweeps EXACT 0.55-0.95 (negatives count as binds),
  recommends FLOOR strictly above the highest wrong pick with margin for
  ±0.05 run-to-run confidence jitter; markdown report + real traces
- matching.py: below-FLOOR picks no longer suppress difflib — they log a
  routing.decision event (source=jev-lowtrust) and leave no result so the
  existing difflib path keeps its chance (0.54 «پردیس» was lost to «new»)
- config.py: JEV_CONF_FLOOR 0.60 -> 0.65 (removes the جعبه→بسته @0.61 wrong
  candidate offer; correct sub-floor picks باق اسپارت/کیلو are difflib-covered)
- test_matching.py: +2 low-trust cases (13/13 mock, 16/16 live)
- docker-compose.yml: mount bot/tools into the bot container
2026-10-12 21:42:32 +03:30

534 lines
28 KiB
Python

"""Mock-data seeder (ADDITIVE + idempotent) for JeV testing and phase-6 calibration.
docker compose exec bot python tools/seed_mock.py
Rules:
- Never deletes or modifies existing user data; existing rows (by name/key/id)
are skipped, so re-runs are safe and the team's own test data is untouched.
- Deterministic ids (uuid5) for every created entity → stable cross-references.
- Covers: units, brands, attribute definitions + material EAV values, materials,
brand/material links, material unit conversions, suppliers (+categories),
supplier↔material edges, projects, warehouses, inventory + ledger, and three
full quotes: offered / accepted multi-project / arrived (receipt + allocation
+ PURCHASE ledger row + decisions + events).
- All numeric parameters use Decimal (asyncpg is strict about numeric columns).
"""
import asyncio
import sys
import uuid
from datetime import date, datetime, timedelta, timezone
from decimal import Decimal
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from sqlalchemy import text
from app.db import SessionLocal
NS = uuid.UUID("6d3ed6a0-9e11-4b2a-8f7e-000000000000")
def uid(*parts: str) -> uuid.UUID:
return uuid.uuid5(NS, "|".join(parts))
def D(v) -> Decimal:
return Decimal(str(v))
STATS: dict[str, list[int]] = {}
def _bump(kind: str, created: int, skipped: int) -> None:
c, s = STATS.get(kind, [0, 0])
STATS[kind] = [c + created, s + skipped]
UNITS_EXTRA = [
("sh", "شاخه", "count"),
("m2", "متر مربع", "area"),
("pack", "بسته", "count"),
("roll", "رول", "count"),
("sheet", "ورق", "count"),
]
BRANDS = [
"فولاد مبارکه", "ذوب آهن اصفهان", "سیمان تهران", "سیمان سپاهان",
"پلیکا", "البرز", "سرامیک آسیا",
]
ATTR_DEFS = [
("spec_قطر", "قطر"),
("spec_سایز", "سایز"),
("spec_گرید", "گرید"),
("spec_ضخامت", "ضخامت"),
("spec_بسته‌بندی", "بسته‌بندی"),
]
# name, canonical unit, supplier, brand|None, specs[(key,value)], conversions[(unit, factor)]
MATERIALS = [
("میلگرد آجدار", "kg", "نمایندگی فولاد مبارکه", "فولاد مبارکه",
[("spec_سایز", "سایز ۱۴"), ("spec_گرید", "آجدار A3")], [("شاخه", 12), ("تن", 1000)]),
("تیرآهن", "kg", "نمایندگی فولاد مبارکه", "فولاد مبارکه",
[("spec_سایز", "سایز ۱۸")], [("شاخه", 226)]),
("ورق سیاه", "kg", "نمایندگی فولاد مبارکه", "فولاد مبارکه",
[("spec_ضخامت", "۲ میل")], [("ورق", "24.6")]),
("پروفیل قوطی", "kg", "نمایندگی فولاد مبارکه", "فولاد مبارکه",
[("spec_سایز", "۴۰×۴۰"), ("spec_ضخامت", "۲ میل")], []),
("سیمان پرتلند", "kg", "بازرگانی مصطفوی", "سیمان تهران",
[("spec_بسته‌بندی", "۵۰ کیلویی")], [("کیسه", 50), ("تن", 1000)]),
("گچ و خاک", "kg", "بازرگانی مصطفوی", None,
[("spec_بسته‌بندی", "۳۰ کیلویی")], [("کیسه", 30)]),
("گچ ساختمانی", "kg", "بازرگانی مصطفوی", None, [], [("کیسه", 30)]),
("شن و ماسه", "kg", "بازرگانی مصطفوی", None, [], []),
("لوله پلیکا", "m", "پلیکا فروش البرز", "پلیکا",
[("spec_قطر", "۶ اینچ")], [("شاخه", 6)]),
("لوله مانیسمان", "m", "تجهیزات آبی پارس", None,
[("spec_قطر", "۲ اینچ")], []),
("سه راهی پلیکا", "piece", "پلیکا فروش البرز", "پلیکا",
[("spec_قطر", "۶ اینچ")], []),
("شیر فلکه", "piece", "تجهیزات آبی پارس", None,
[("spec_سایز", "۲ اینچ")], []),
("کاشی سرامیک", "m2", "سرامیک آسیا", "سرامیک آسیا",
[("spec_سایز", "۶۰×۶۰")], []),
("رنگ روغنی", "kg", "شیمی صنعت سپاهان", None,
[("spec_بسته‌بندی", "گالن ۳.۶ کیلویی")], []),
("الکترود جوشکاری", "kg", "ابزار سازان اصفهان", None,
[("spec_سایز", "۳.۲")], []),
("نایلون شیرینک", "kg", "شیمی صنعت سپاهان", None, [], []),
]
SUPPLIERS = [
("بازرگانی مصطفوی", "construction-materials", "اصفهان، خیابان امام", "031-32223344", "حاج آقا مصطفوی"),
("نمایندگی فولاد مبارکه", "metals-steel-sections", "اصفهان، شهرک صنعتی", "031-33445566", "مهندس کریمی"),
("پلیکا فروش البرز", "mechanical-installations-pipes", "کرج، جاده مخصوص", "026-34567890", "آقای رضایی"),
("تجهیزات آبی پارس", "mechanical-installations-pipes", "تهران، بازار آهن", "021-55667788", "آقای شریفی"),
("سرامیک آسیا", "finishing-interior-decoration", "یزد، میبد", "035-32345678", "آقای دهقان"),
("ابزار سازان اصفهان", "construction-tools-equipment", "اصفهان، بازار", "031-32112233", "آقای نوری"),
("شیمی صنعت سپاهان", "construction-chemicals", "اصفهان، شهرک علمی", "031-33889900", "خانم احمدی"),
]
PROJECTS = [
("دفتر", "دفتر مرکزی شرکت"),
("منشانه", "پروژه منشانه"),
("باغ اسپارت", "پروژه باغ اسپارت"),
("پردیس کاشان", "پروژه پردیس کاشان"),
("بابک", "پروژه بابک"),
]
async def _name_id(s, table: str, name: str):
row = (await s.execute(
text(f"SELECT id FROM {table} WHERE name = :n LIMIT 1"), {"n": name}
)).scalar()
return str(row) if row else None
async def seed() -> None:
now = datetime.now(timezone.utc)
async with SessionLocal() as s:
# ---------------- units ----------------
created = skipped = 0
for code, name_fa, dim in UNITS_EXTRA:
r = await s.execute(text(
"INSERT INTO units (code, name_fa, dimension, si_factor, is_base) "
"VALUES (:c, :n, :d, 1, false) ON CONFLICT (code) DO NOTHING"
), {"c": code, "n": name_fa, "d": dim})
created += r.rowcount
skipped += 1 - r.rowcount
_bump("units", created, skipped)
_unit_rows = (await s.execute(text("SELECT id, code, name_fa FROM units"))).all()
unit_map = {r.code: r.id for r in _unit_rows}
unit_fa = {r.name_fa: r.id for r in _unit_rows}
def unit_of(ref) -> int:
return unit_map.get(ref) or unit_fa[ref]
# ---------------- brands ----------------
created = skipped = 0
brand_id: dict[str, str] = {}
for b in BRANDS:
existing = await _name_id(s, "brands", b)
if existing:
brand_id[b] = existing
skipped += 1
continue
bid = uid("brand", b)
await s.execute(text(
"INSERT INTO brands (id, name, country) VALUES (:id, :name, 'ایران') ON CONFLICT (id) DO NOTHING"
), {"id": str(bid), "name": b})
brand_id[b] = str(bid)
created += 1
_bump("brands", created, skipped)
# ---------------- attribute definitions ----------------
created = skipped = 0
attr_id: dict[str, str] = {}
for key, label in ATTR_DEFS:
row = (await s.execute(
text("SELECT id FROM attribute_definitions WHERE attribute_key=:k"), {"k": key}
)).scalar()
if row:
attr_id[key] = str(row)
skipped += 1
continue
aid = uid("attr", key)
await s.execute(text(
"INSERT INTO attribute_definitions (id, attribute_key, label, data_type, enum_values) "
"VALUES (:id, :k, :l, 'string', NULL) ON CONFLICT (id) DO NOTHING"
), {"id": str(aid), "k": key, "l": label})
attr_id[key] = str(aid)
created += 1
_bump("attribute_definitions", created, skipped)
# ---------------- suppliers ----------------
created = skipped = 0
cat_map = {r.slug: str(r.id) for r in
(await s.execute(text("SELECT id, slug FROM material_categories"))).all()}
supplier_id: dict[str, str] = {}
for name, slug, location, phone, contact in SUPPLIERS:
existing = await _name_id(s, "suppliers", name)
if existing:
supplier_id[name] = existing
skipped += 1
continue
sid = uid("supplier", name)
await s.execute(text(
"INSERT INTO suppliers (id, name, category_id, location, phone, contact_person) "
"VALUES (:id, :name, :cat, :loc, :phone, :contact) ON CONFLICT (id) DO NOTHING"
), {"id": str(sid), "name": name, "cat": cat_map.get(slug), "loc": location,
"phone": phone, "contact": contact})
supplier_id[name] = str(sid)
created += 1
_bump("suppliers", created, skipped)
# ---------------- materials (+ links, EAV, conversions) ----------------
created = skipped = 0
material_id: dict[str, str] = {}
brand_material_of: dict[str, str | None] = {}
brand_of: dict[str, str | None] = {}
for name, unit_code, sup_name, brand, specs, convs in MATERIALS:
mid = await _name_id(s, "materials", name)
if mid is None:
mid = str(uid("material", name))
await s.execute(text(
"INSERT INTO materials (id, name, canonical_unit_id, status) "
"VALUES (:id, :name, :unit, 'active') ON CONFLICT (id) DO NOTHING"
), {"id": mid, "name": name, "unit": unit_map[unit_code]})
created += 1
else:
skipped += 1
material_id[name] = mid
for key, value in specs:
await s.execute(text(
"INSERT INTO material_attribute_values "
"(id, material_id, attribute_definition_id, value_string) "
"VALUES (:id, :m, :a, :v) "
"ON CONFLICT (material_id, attribute_definition_id) DO NOTHING"
), {"id": str(uid("mav", name, key)), "m": mid, "a": attr_id[key], "v": value})
if brand:
bmid = str(uid("bm", brand, name))
await s.execute(text(
"INSERT INTO brand_materials (id, brand_id, material_id, is_active) "
"VALUES (:id, :b, :m, true) ON CONFLICT (brand_id, material_id) DO NOTHING"
), {"id": bmid, "b": brand_id[brand], "m": mid})
brand_material_of[name] = bmid
brand_of[name] = brand_id[brand]
else:
brand_material_of[name] = None
brand_of[name] = None
for conv_code, factor in convs:
exists = (await s.execute(text(
"SELECT 1 FROM material_units WHERE material_id=:m AND brand_material_id IS NULL "
"AND unit_id=:u LIMIT 1"
), {"m": mid, "u": unit_of(conv_code)})).scalar()
if exists:
continue
await s.execute(text(
"INSERT INTO material_units (id, material_id, brand_material_id, unit_id, "
"conversion_factor, is_default) VALUES (:id, :m, NULL, :u, :f, false)"
), {"id": str(uid("mu", name, conv_code)), "m": mid,
"u": unit_of(conv_code), "f": D(factor)})
await s.execute(text(
"INSERT INTO supplier_materials (id, supplier_id, material_id, is_active, "
"last_quoted_price, last_quoted_at) VALUES (:id, :sup, :m, true, :price, :at) "
"ON CONFLICT (supplier_id, material_id) DO NOTHING"
), {"id": str(uid("sm", sup_name, name)), "sup": supplier_id[sup_name],
"m": mid, "price": D("100000"), "at": now})
_bump("materials", created, skipped)
# ---------------- projects + warehouses ----------------
created = skipped = 0
project_id: dict[str, str] = {}
for name, desc in PROJECTS:
pid = await _name_id(s, "projects", name)
if pid is None:
pid = str(uid("project", name))
await s.execute(text(
"INSERT INTO projects (id, name, description) VALUES (:id, :name, :desc) "
"ON CONFLICT (id) DO NOTHING"
), {"id": pid, "name": name, "desc": desc})
created += 1
else:
skipped += 1
project_id[name] = pid
_bump("projects", created, skipped)
created = skipped = 0
warehouse_id: dict[tuple[str, str], str] = {}
for pname, pid in project_id.items():
for wname in ("انبار مرکزی", "انبار کارگاه"):
wid = str(uid("wh", pname, wname))
exists = (await s.execute(text(
"SELECT 1 FROM warehouses WHERE id=:id LIMIT 1"), {"id": wid})).scalar()
if exists:
skipped += 1
else:
await s.execute(text(
"INSERT INTO warehouses (id, project_id, name, description) "
"VALUES (:id, :p, :n, :d)"
), {"id": wid, "p": pid, "n": wname, "d": f"{wname} پروژه {pname}"})
created += 1
warehouse_id[(pname, wname)] = wid
_bump("warehouses", created, skipped)
# ---------------- inventory + ledger ----------------
inv_plan = [
("منشانه", "انبار مرکزی", "گچ و خاک", None, "200", "kg"),
("منشانه", "انبار کارگاه", "سیمان پرتلند", "سیمان تهران", "150", "kg"),
("بابک", "انبار مرکزی", "میلگرد آجدار", "فولاد مبارکه", "800", "kg"),
("پردیس کاشان", "انبار مرکزی", "لوله پلیکا", "پلیکا", "60", "m"),
]
created = skipped = 0
for pname, wname, mname, bname, qty, unit_code in inv_plan:
iid = str(uid("inv", pname, wname, mname))
exists = (await s.execute(text(
"SELECT 1 FROM inventory WHERE id=:id LIMIT 1"), {"id": iid})).scalar()
if exists:
skipped += 1
continue
await s.execute(text(
"INSERT INTO inventory (id, warehouse_id, material_id, brand_id, quantity, unit_id, "
"specs, specs_hash) VALUES (:id, :w, :m, :b, :q, :u, '{}'::jsonb, 'default')"
), {"id": iid, "w": warehouse_id[(pname, wname)], "m": material_id[mname],
"b": brand_id.get(bname) if bname else None, "q": D(qty), "u": unit_map[unit_code]})
await s.execute(text(
"INSERT INTO inventory_transactions (id, inventory_id, warehouse_id, transaction_type, "
"quantity, performed_by, transaction_date, note) "
"VALUES (:id, :inv, :w, 'PURCHASE', :q, 'کاربر آزمایشی', :at, 'موجودی اولیه (seed)')"
), {"id": str(uid("tx", iid, "purchase")), "inv": iid,
"w": warehouse_id[(pname, wname)], "q": D(qty), "at": now - timedelta(days=20)})
created += 1
_bump("inventory", created, skipped)
# ---------------- quotes ----------------
created = skipped = 0
admin_uid = (await s.execute(text(
"SELECT id FROM users WHERE role='admin' ORDER BY created_at LIMIT 1"
))).scalar()
admin_uid = str(admin_uid) if admin_uid else None
async def insert_quote_block(qkey, supplier, projects, status, quote_date, expiry,
accepted, arrived, is_official, notes, items, termsheets,
decisions_map, events, receipts=None, legacy_project=None):
qid = str(uid("quote", qkey))
exists = (await s.execute(text("SELECT 1 FROM quotes WHERE id=:id"), {"id": qid})).scalar()
if exists:
return False
await s.execute(text(
"INSERT INTO quotes (id, supplier_id, project_id, quote_date, expiry_date, "
"accepted_date, arrived_date, status, is_official, notes) VALUES "
"(:id, :sup, :proj, :qd, :ed, :ad, :ar, CAST(:st AS quote_status), :off, :notes)"
), {"id": qid, "sup": supplier_id[supplier],
"proj": project_id.get(legacy_project) if legacy_project else None,
"qd": quote_date, "ed": expiry, "ad": accepted, "ar": arrived,
"st": status, "off": is_official, "notes": notes})
for p in projects:
await s.execute(text(
"INSERT INTO quote_projects (quote_id, project_id) VALUES (:q, :p) "
"ON CONFLICT DO NOTHING"), {"q": qid, "p": project_id[p]})
ts_ids: dict[str, str] = {}
for ts in termsheets:
tid = str(uid("ts", qkey, ts["label"]))
ts_ids[ts["label"]] = tid
await s.execute(text(
"INSERT INTO termsheets (id, quote_id, label, status, discount_type, discount_value) "
"VALUES (:id, :q, :l, CAST(:st AS termsheet_status), CAST(:dt AS discount_type), :dv)"
), {"id": tid, "q": qid, "l": ts["label"], "st": ts["status"],
"dt": ts.get("discount_type", "none"), "dv": D(ts.get("discount_value", 0))})
for i, (due, amt, note) in enumerate(ts["steps"], start=1):
await s.execute(text(
"INSERT INTO termsheet_schedule_steps (id, termsheet_id, step_no, due_date, "
"amount, note) VALUES (:id, :ts, :no, :due, :amt, :note)"
), {"id": str(uid("step", qkey, ts["label"], str(i))), "ts": tid, "no": i,
"due": due, "amt": D(amt), "note": note})
line_ids: dict[str, str] = {}
its_ids: dict[tuple[str, str], str] = {}
for it in items:
lid = str(uid("li", qkey, it["material"]))
line_ids[it["material"]] = lid
await s.execute(text(
"INSERT INTO line_items (id, quote_id, material_id, brand_material_id, unit_id, "
"quantity, notes) VALUES (:id, :q, :m, :bm, :u, :qty, NULL)"
), {"id": lid, "q": qid, "m": material_id[it["material"]],
"bm": brand_material_of.get(it["material"]), "u": unit_map[it["unit"]],
"qty": D(it["qty"])})
for p in it.get("projects", projects):
await s.execute(text(
"INSERT INTO line_item_projects (line_item_id, project_id) VALUES (:li, :p) "
"ON CONFLICT DO NOTHING"), {"li": lid, "p": project_id[p]})
if it.get("price") is not None:
tlabel = it.get("termsheet", termsheets[0]["label"])
iid = str(uid("it", qkey, it["material"], tlabel))
its_ids[(it["material"], tlabel)] = iid
await s.execute(text(
"INSERT INTO item_termsheets (id, line_item_id, termsheet_id, unit_price, "
"min_order_qty) VALUES (:id, :li, :ts, :price, NULL)"
), {"id": iid, "li": lid, "ts": ts_ids[tlabel], "price": D(it["price"])})
dec_ids: dict[str, str] = {}
for mname, dec in decisions_map.items():
did = str(uid("dec", qkey, mname))
chosen_its = (its_ids.get((mname, dec.get("termsheet")))
if dec.get("status") == "chosen" else None)
await s.execute(text(
"INSERT INTO item_decisions (id, line_item_id, chosen_item_termsheet_id, status, "
"project_id, adjusted_unit_price, decided_at) VALUES "
"(:id, :li, :its, CAST(:st AS item_decision_status), :proj, :price, :at)"
), {"id": did, "li": line_ids[mname], "its": chosen_its, "st": dec["status"],
"proj": project_id.get(dec.get("project")) if dec.get("project") else None,
"price": D(dec["price"]) if dec.get("price") is not None else None,
"at": accepted if dec.get("status") == "chosen" else None})
dec_ids[mname] = did
for ev in events:
at = {"offered": quote_date, "accepted": accepted, "arrived": arrived}.get(ev) or quote_date
await s.execute(text(
"INSERT INTO quote_events (id, quote_id, event_type, payload, occurred_at, actor) "
"VALUES (:id, :q, CAST(:ev AS quote_event_type), '{}'::jsonb, :at, 'کاربر آزمایشی (seed)')"
), {"id": str(uid("ev", qkey, ev)), "q": qid, "ev": ev, "at": at})
for rc in receipts or []:
rid = str(uid("rc", qkey, rc["material"]))
await s.execute(text(
"INSERT INTO item_receipts (id, item_decision_id, confirmed_by, received_at, "
"received_qty, on_time, quality_note) VALUES (:id, :d, :by, :at, :qty, true, NULL)"
), {"id": rid, "d": dec_ids[rc["material"]], "by": admin_uid,
"at": arrived, "qty": D(rc["qty"])})
for wname, alloc_qty in rc["allocations"]:
aid = str(uid("alloc", qkey, rc["material"], wname))
wid = warehouse_id[(rc["project"], wname)]
await s.execute(text(
"INSERT INTO item_receipt_allocations (id, receipt_id, warehouse_id, quantity) "
"VALUES (:id, :r, :w, :q)"
), {"id": aid, "r": rid, "w": wid, "q": D(alloc_qty)})
inv_row = (await s.execute(text(
"SELECT id FROM inventory WHERE warehouse_id=:w AND material_id=:m "
"AND COALESCE(brand_id, '00000000-0000-0000-0000-000000000000') = "
"COALESCE(CAST(:b AS uuid), '00000000-0000-0000-0000-000000000000') "
"AND specs_hash='default' LIMIT 1"
), {"w": wid, "m": material_id[rc["material"]],
"b": brand_of.get(rc["material"])})).scalar()
qty_canonical = D(alloc_qty) * D(rc.get("factor", 1))
if inv_row is None:
inv_id = str(uid("inv", rc["project"], wname, rc["material"]))
await s.execute(text(
"INSERT INTO inventory (id, warehouse_id, material_id, brand_id, quantity, "
"unit_id, specs, specs_hash) "
"VALUES (:id, :w, :m, :b, :q, :u, '{}'::jsonb, 'default')"
), {"id": inv_id, "w": wid, "m": material_id[rc["material"]],
"b": brand_of.get(rc["material"]), "q": qty_canonical,
"u": unit_map[rc.get("canonical", "kg")]})
else:
inv_id = str(inv_row)
await s.execute(text(
"UPDATE inventory SET quantity = quantity + :q WHERE id=:id"
), {"q": qty_canonical, "id": inv_id})
await s.execute(text(
"INSERT INTO inventory_transactions (id, inventory_id, warehouse_id, "
"receipt_allocation_id, transaction_type, quantity, performed_by, "
"transaction_date, note) VALUES (:id, :inv, :w, :alloc, 'PURCHASE', :q, "
"'کاربر آزمایشی', :at, :note)"
), {"id": str(uid("tx", qkey, rc["material"], wname)), "inv": inv_id,
"w": wid, "alloc": aid, "q": qty_canonical, "at": arrived,
"note": f"ورود از رسید خرید {rc['material']}"})
return True
d_today = date(2026, 10, 12)
ok1 = await insert_quote_block(
"q1", "پلیکا فروش البرز", ["پردیس کاشان"], "offered",
now - timedelta(days=2), d_today + timedelta(days=12), None, None, True,
"[mock] استعلام لوله و اتصالات پردیس",
items=[
{"material": "لوله پلیکا", "unit": "sh", "qty": "120", "price": "450000"},
{"material": "سه راهی پلیکا", "unit": "piece", "qty": "40", "price": "85000"},
],
termsheets=[{"label": "نقد امروز (۵٪ تخفیف)", "status": "offered",
"discount_type": "percent", "discount_value": "5",
"steps": [(d_today, "60000000", "تسویه نقدی یکباره")]}],
decisions_map={"لوله پلیکا": {"status": "pending"}, "سه راهی پلیکا": {"status": "pending"}},
events=["offered"],
legacy_project="پردیس کاشان",
)
ok2 = await insert_quote_block(
"q2", "بازرگانی مصطفوی", ["منشانه", "دفتر"], "accepted",
now - timedelta(days=9), d_today + timedelta(days=5),
now - timedelta(days=6), None, False,
"[mock] خرید گچ و سیمان پروژه‌های منشانه و دفتر",
items=[
{"material": "گچ و خاک", "unit": "bag", "qty": "100", "price": "85000",
"termsheet": "سه قسط ماهانه", "projects": ["منشانه"]},
{"material": "سیمان پرتلند", "unit": "bag", "qty": "50", "price": "185000",
"termsheet": "سه قسط ماهانه", "projects": ["دفتر"]},
],
termsheets=[{"label": "سه قسط ماهانه", "status": "accepted",
"steps": [(d_today, "5875000", "قسط اول"),
(d_today + timedelta(days=30), "5875000", "قسط دوم"),
(d_today + timedelta(days=60), "5875000", "قسط سوم")]}],
decisions_map={
"گچ و خاک": {"status": "chosen", "project": "منشانه",
"termsheet": "سه قسط ماهانه", "price": "85000"},
"سیمان پرتلند": {"status": "chosen", "project": "دفتر",
"termsheet": "سه قسط ماهانه", "price": "185000"},
},
events=["offered", "accepted"],
legacy_project="منشانه",
)
ok3 = await insert_quote_block(
"q3", "نمایندگی فولاد مبارکه", ["بابک"], "arrived",
now - timedelta(days=15), d_today - timedelta(days=1),
now - timedelta(days=12), now - timedelta(days=3), True,
"[mock] میلگرد پروژه بابک (رسید کامل)",
items=[{"material": "میلگرد آجدار", "unit": "ton", "qty": "2", "price": "42000000"}],
termsheets=[{"label": "نقد امروز", "status": "accepted",
"steps": [(d_today, "84000000", "تسویه")]}],
decisions_map={"میلگرد آجدار": {"status": "chosen", "project": "بابک",
"termsheet": "نقد امروز", "price": "42000000"}},
events=["offered", "accepted", "arrived"],
receipts=[{"material": "میلگرد آجدار", "qty": "2", "project": "بابک",
"allocations": [("انبار مرکزی", "2")], "factor": 1000,
"canonical": "kg"}],
legacy_project="بابک",
)
_bump("quotes", sum(1 for x in (ok1, ok2, ok3) if x),
sum(1 for x in (ok1, ok2, ok3) if not x))
await s.commit()
print("\n=== seed summary (created / skipped) ===")
for kind, (c, sk) in STATS.items():
print(f" {kind:22} created={c:3} skipped={sk:3}")
print("done. Nothing was deleted or modified.")
if __name__ == "__main__":
asyncio.run(seed())