""" Supplychain module - Supplier and Reseller Management Handles suppliers, supply contracts, sub-resellers, distribution agreements, and sales ledger for accounting calculations. """ import json from datetime import datetime, date from appPublic.uniqueID import getID from appPublic.jsonConfig import getConfig from appPublic.dictObject import DictObject from sqlor.dbpools import DBPools, get_sor_context from ahserver.serverenv import ServerEnv from ahserver.filestorage import FileStorage from filemgr.filemgr import FileMgr, FileMgrResult class ContractAttachMgr(FileMgr): """Supplychain attachment manager — fiid from params_kw.""" def get_folder_ownerid(self, sor): """Contract folders are org-scoped, return owner for comparison.""" pass async def is_folder_ownerid(self, sor, orgid): """Allow any logined user to upload to contract folders.""" return True async def get_organization_quota(self, sor, orgid): """Unlimited quota for contract attachments.""" return 1000000000, '9999-12-31' async def file_uploaded(self, request, ns, userid): """Return success result after file uploaded.""" return FileMgrResult('success', 'file added') MODULE_NAME = "supplychain" def _get_dbname(): """Get module database name dynamically.""" env = ServerEnv() return env.get_module_dbname(MODULE_NAME) def _get_sor(): """Get a sqlor context for this module's database.""" config = getConfig() db = DBPools() db.databases = config.databases return db, _get_dbname() def _generate_supplier_code(resellerid): """Generate unique supplier code: SUP-{YYYYMMDD}-{seq}.""" env = ServerEnv() today = env.strdate(env.today()) prefix = f"SUP-{today.replace('-', '')}" db, dbname = _get_sor() with db.sqlorContext(dbname) as sor: sql = """SELECT COUNT(*) as cnt FROM suppliers WHERE resellerid = ${resellerid}$ AND supplier_code LIKE ${prefix}$""" recs = sor.sqlExe(sql, {"resellerid": resellerid, "prefix": prefix + "%"}) seq = (recs[0].cnt if recs else 0) + 1 return f"{prefix}-{seq:04d}" def _generate_contract_code(resellerid): """Generate unique contract code: SC-{YYYYMMDD}-{seq}.""" env = ServerEnv() today = env.strdate(env.today()) prefix = f"SC-{today.replace('-', '')}" db, dbname = _get_sor() with db.sqlorContext(dbname) as sor: sql = """SELECT COUNT(*) as cnt FROM supply_contracts WHERE resellerid = ${resellerid}$ AND contract_code LIKE ${prefix}$""" recs = sor.sqlExe(sql, {"resellerid": resellerid, "prefix": prefix + "%"}) seq = (recs[0].cnt if recs else 0) + 1 return f"{prefix}-{seq:04d}" def _generate_sub_reseller_code(resellerid): """Generate unique sub-reseller code: SR-{YYYYMMDD}-{seq}.""" today = datetime.now().strftime("%Y%m%d") prefix = f"SR-{today}" db, dbname = _get_sor() with db.sqlorContext(dbname) as sor: sql = """SELECT COUNT(*) as cnt FROM sub_resellers WHERE resellerid = ${resellerid}$ AND sub_reseller_code LIKE ${prefix}$""" recs = sor.sqlExe(sql, {"resellerid": resellerid, "prefix": prefix + "%"}) seq = (recs[0].cnt if recs else 0) + 1 return f"{prefix}-{seq:04d}" def _generate_agreement_code(resellerid): """Generate unique agreement code: DA-{YYYYMMDD}-{seq}.""" env = ServerEnv() today = env.strdate(env.today()) prefix = f"DA-{today.replace('-', '')}" db, dbname = _get_sor() with db.sqlorContext(dbname) as sor: sql = """SELECT COUNT(*) as cnt FROM distribution_agreements WHERE resellerid = ${resellerid}$ AND agreement_code LIKE ${prefix}$""" recs = sor.sqlExe(sql, {"resellerid": resellerid, "prefix": prefix + "%"}) seq = (recs[0].cnt if recs else 0) + 1 return f"{prefix}-{seq:04d}" # ============================================================ # Supplier APIs # ============================================================ async def create_supplier(request, params_kw): """Create a new supplier record (all external).""" env = request._run_ns user_id = await env.get_user() resellerid = await env.get_userorgid() data = params_kw db, dbname = _get_sor() orgid = data.get("orgid") if not orgid: # Create new organization for this supplier supplier_name = data.get("supplier_name") if not supplier_name: return json.dumps({"status": "error", "message": "供应商名称不能为空"}) new_orgid = getID() now_org = datetime.now().strftime("%Y-%m-%d %H:%M:%S") org_rec = { "id": new_orgid, "orgname": supplier_name, "orgtype": "supplier", "status": "1", "created_by": user_id, "created_at": now_org, "updated_at": now_org } async with db.sqlorContext("sage") as sor_sage: await sor_sage.C("organization", org_rec) orgid = new_orgid async with db.sqlorContext(dbname) as sor: supplier_code = data.get("supplier_code") or _generate_supplier_code(resellerid) now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") rec = { "id": getID(), "resellerid": resellerid, "supplier_code": supplier_code, "supplier_name": data.get("supplier_name"), "contact_person": data.get("contact_person"), "contact_phone": data.get("contact_phone"), "contact_email": data.get("contact_email"), "address": data.get("address"), "tax_number": data.get("tax_number"), "bank_name": data.get("bank_name"), "bank_account": data.get("bank_account"), "orgid": orgid, "is_external": "1", "settlement_cycle": data.get("settlement_cycle"), "settlement_day": data.get("settlement_day"), "payment_type": data.get("payment_type"), "status": data.get("status", "1"), "remark": data.get("remark"), "created_by": user_id, "created_at": now, "updated_at": now, } await sor.C("suppliers", rec) return json.dumps({"status": "ok", "data": rec, "message": "创建成功"}) async def update_supplier(request, params_kw): """Update a supplier record.""" data = params_kw db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") rec = {"id": data["id"], "updated_at": now} for key in ["supplier_name", "contact_person", "contact_phone", "contact_email", "address", "tax_number", "bank_name", "bank_account", "orgid", "settlement_cycle", "settlement_day", "payment_type", "status", "remark"]: if key in data: rec[key] = data[key] await sor.U("suppliers", rec) return json.dumps({"status": "ok", "message": "更新成功"}) async def delete_supplier(request, params_kw): """Delete a supplier record.""" data = params_kw db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: await sor.D("suppliers", {"id": data["id"]}) return json.dumps({"status": "ok", "message": "删除成功"}) # ============================================================ # Supply Contract APIs # ============================================================ async def create_supply_contract(request, params_kw): """Create a new supply contract.""" env = ServerEnv() user_id = await env.get_user() resellerid = await env.get_userorgid() data = params_kw db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: contract_code = data.get("contract_code") or _generate_contract_code(resellerid) now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") rec = { "id": getID(), "resellerid": resellerid, "supplier_id": data.get("supplier_id"), "contract_code": contract_code, "contract_name": data.get("contract_name"), "sign_date": data.get("sign_date"), "start_date": data.get("start_date"), "end_date": data.get("end_date"), "status": data.get("status", "1"), "default_discount": data.get("default_discount", 1.0), "remark": data.get("remark"), "created_by": user_id, "created_at": now, "updated_at": now, } await sor.C("supply_contracts", rec) return json.dumps({"status": "ok", "data": rec, "message": "创建成功"}) async def update_supply_contract(request, params_kw): """Update a supply contract.""" data = params_kw db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") rec = {"id": data["id"], "updated_at": now} for key in ["supplier_id", "contract_name", "sign_date", "start_date", "end_date", "status", "default_discount", "remark"]: if key in data: rec[key] = data[key] await sor.U("supply_contracts", rec) return json.dumps({"status": "ok", "message": "更新成功"}) async def delete_supply_contract(request, params_kw): """Delete a supply contract and its items.""" data = params_kw db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: await sor.D("supply_contract_items", {"contract_id": data["id"]}) await sor.D("supply_contracts", {"id": data["id"]}) return json.dumps({"status": "ok", "message": "删除成功"}) # ============================================================ # Supply Contract Items APIs # ============================================================ async def create_supply_contract_item(request, params_kw): """Create a supply contract product discount item.""" data = params_kw db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") rec = { "id": getID(), "contract_id": data["contract_id"], "resellerid": data.get("resellerid", ""), "prodtypeid": data.get("prodtypeid"), "productid": data.get("productid"), "discount": data.get("discount", 1.0), "settlement_price": data.get("settlement_price"), "remark": data.get("remark"), "created_at": now, } await sor.C("supply_contract_items", rec) return json.dumps({"status": "ok", "data": rec, "message": "创建成功"}) async def update_supply_contract_item(request, params_kw): """Update a supply contract item.""" data = params_kw db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: rec = {"id": data["id"]} for key in ["prodtypeid", "productid", "discount", "settlement_price", "remark"]: if key in data: rec[key] = data[key] await sor.U("supply_contract_items", rec) return json.dumps({"status": "ok", "message": "更新成功"}) async def delete_supply_contract_item(request, params_kw): """Delete a supply contract item.""" data = params_kw db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: await sor.D("supply_contract_items", {"id": data["id"]}) return json.dumps({"status": "ok", "message": "删除成功"}) # ============================================================ # Sub-Reseller APIs # ============================================================ async def create_sub_reseller(request, params_kw): """Create a new sub-reseller with orgid for discount/accounting.""" env = request._run_ns user_id = await env.get_user() resellerid = await env.get_userorgid() data = params_kw db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") # Create org entry for this sub_reseller new_orgid = getID() org_rec = { "id": new_orgid, "orgname": data.get("sub_dist_name"), "orgtype": "sub_reseller", "status": "1", "created_by": user_id, "created_at": now, "updated_at": now } async with db.sqlorContext("sage") as sor_sage: await sor_sage.C("organization", org_rec) rec = { "id": getID(), "resellerid": resellerid, "orgid": new_orgid, "sub_dist_name": data.get("sub_dist_name"), "contact_person": data.get("contact_person"), "contact_phone": data.get("contact_phone"), "contact_email": data.get("contact_email"), "address": data.get("address"), "tax_number": data.get("tax_number"), "bank_name": data.get("bank_name"), "bank_account": data.get("bank_account"), "status": data.get("status", "1"), "remark": data.get("remark"), "managed_by": user_id, "created_by": user_id, "created_at": now, "updated_at": now, } await sor.C("sub_distributors", rec) return json.dumps({"status": "ok", "data": rec, "message": "创建成功"}) async def update_sub_reseller(request, params_kw): """Update a sub-reseller.""" data = params_kw db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") rec = {"id": data["id"], "updated_at": now} for key in ["sub_reseller_name", "contact_person", "contact_phone", "contact_email", "address", "tax_number", "bank_name", "bank_account", "status", "remark"]: if key in data: rec[key] = data[key] await sor.U("sub_resellers", rec) return json.dumps({"status": "ok", "message": "更新成功"}) async def delete_sub_reseller(request, params_kw): """Delete a sub-reseller.""" data = params_kw db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: await sor.D("sub_resellers", {"id": data["id"]}) return json.dumps({"status": "ok", "message": "删除成功"}) # ============================================================ # Distribution Agreement APIs # ============================================================ async def create_distribution_agreement(request, params_kw): """Create a new distribution agreement.""" env = ServerEnv() user_id = await env.get_user() resellerid = await env.get_userorgid() data = params_kw db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: agreement_code = data.get("agreement_code") or _generate_agreement_code(resellerid) now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") rec = { "id": getID(), "resellerid": resellerid, "sub_reseller_id": data.get("sub_reseller_id"), "agreement_code": agreement_code, "agreement_name": data.get("agreement_name"), "sign_date": data.get("sign_date"), "start_date": data.get("start_date"), "end_date": data.get("end_date"), "status": data.get("status", "1"), "default_discount": data.get("default_discount", 1.0), "remark": data.get("remark"), "created_by": user_id, "created_at": now, "updated_at": now, } await sor.C("distribution_agreements", rec) return json.dumps({"status": "ok", "data": rec, "message": "创建成功"}) async def update_distribution_agreement(request, params_kw): """Update a distribution agreement.""" data = params_kw db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") rec = {"id": data["id"], "updated_at": now} for key in ["sub_reseller_id", "agreement_name", "sign_date", "start_date", "end_date", "status", "default_discount", "remark"]: if key in data: rec[key] = data[key] await sor.U("distribution_agreements", rec) return json.dumps({"status": "ok", "message": "更新成功"}) async def delete_distribution_agreement(request, params_kw): """Delete a distribution agreement and its items.""" data = params_kw db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: await sor.D("distribution_agreement_items", {"agreement_id": data["id"]}) await sor.D("distribution_agreements", {"id": data["id"]}) return json.dumps({"status": "ok", "message": "删除成功"}) # ============================================================ # Distribution Agreement Items APIs # ============================================================ async def create_distribution_agreement_item(request, params_kw): """Create a distribution agreement product discount item.""" data = params_kw db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") rec = { "id": getID(), "agreement_id": data["agreement_id"], "resellerid": data.get("resellerid", ""), "prodtypeid": data.get("prodtypeid"), "productid": data.get("productid"), "discount": data.get("discount", 1.0), "min_order_qty": data.get("min_order_qty"), "sale_price": data.get("sale_price"), "remark": data.get("remark"), "created_at": now, } await sor.C("distribution_agreement_items", rec) return json.dumps({"status": "ok", "data": rec, "message": "创建成功"}) async def update_distribution_agreement_item(request, params_kw): """Update a distribution agreement item.""" data = params_kw db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: rec = {"id": data["id"]} for key in ["prodtypeid", "productid", "discount", "min_order_qty", "sale_price", "remark"]: if key in data: rec[key] = data[key] await sor.U("distribution_agreement_items", rec) return json.dumps({"status": "ok", "message": "更新成功"}) async def delete_distribution_agreement_item(request, params_kw): """Delete a distribution agreement item and its product_org_auth.""" data = params_kw item_id = data["id"] db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: await sor.D("distribution_agreement_items", {"id": item_id}) # Also remove product_org_auth async with DBPools().sqlorContext(ServerEnv().get_module_dbname('sage')) as sor_sage: await sor_sage.sqlExe( "DELETE FROM product_org_auth WHERE auth_source_id = ${sid}$", {"sid": item_id} ) return json.dumps({"status": "ok", "message": "删除成功"}) # ============================================================ # Sales Ledger APIs # ============================================================ async def create_sales_ledger(request, params_kw): """Create a sales ledger entry.""" env = ServerEnv() user_id = await env.get_user() resellerid = await env.get_userorgid() data = params_kw db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") quantity = float(data.get("quantity", 0)) unit_price = float(data.get("unit_price", 0)) total_amount = quantity * unit_price supply_discount = float(data.get("supply_discount", 1.0)) supply_amount = total_amount * supply_discount distribution_discount = float(data.get("distribution_discount", 1.0)) distribution_amount = total_amount * distribution_discount profit_amount = distribution_amount - supply_amount rec = { "id": getID(), "resellerid": resellerid, "sub_reseller_id": data.get("sub_reseller_id"), "supplier_id": data.get("supplier_id"), "agreement_id": data.get("agreement_id"), "contract_id": data.get("contract_id"), "prodtypeid": data.get("prodtypeid"), "productid": data.get("productid"), "sale_date": data.get("sale_date"), "quantity": quantity, "unit_price": unit_price, "supply_discount": supply_discount, "supply_amount": round(supply_amount, 2), "distribution_discount": distribution_discount, "distribution_amount": round(distribution_amount, 2), "profit_amount": round(profit_amount, 2), "settlement_status": data.get("settlement_status", "0"), "remark": data.get("remark"), "created_by": user_id, "created_at": now, "updated_at": now, } await sor.C("sales_ledger", rec) return json.dumps({"status": "ok", "data": rec, "message": "记账成功"}) async def update_sales_ledger(request, params_kw): """Update a sales ledger entry.""" data = params_kw db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") rec = {"id": data["id"], "updated_at": now} for key in ["sub_reseller_id", "supplier_id", "agreement_id", "contract_id", "prodtypeid", "productid", "sale_date", "quantity", "unit_price", "supply_discount", "supply_amount", "distribution_discount", "distribution_amount", "profit_amount", "settlement_status", "remark"]: if key in data: rec[key] = data[key] await sor.U("sales_ledger", rec) return json.dumps({"status": "ok", "message": "更新成功"}) async def delete_sales_ledger(request, params_kw): """Delete a sales ledger entry.""" data = params_kw db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: await sor.D("sales_ledger", {"id": data["id"]}) return json.dumps({"status": "ok", "message": "删除成功"}) # ============================================================ # Distribution Chain Query # ============================================================ async def get_distribution_chain(product_ownerid, seller_orgid): """Get the distribution chain from product owner to seller. Chain is: product_owner → ... → sub_resellers recursively → seller Returns list of orgids from product_owner down to seller (inclusive). If seller_orgid == product_ownerid, returns [product_ownerid]. """ chain = [product_ownerid] if seller_orgid == product_ownerid: return chain env = ServerEnv() dbname = env.get_module_dbname('supplychain') db = DBPools() config = getConfig() db.databases = config.databases # Walk down the sub_reseller chain from product_owner to seller async with db.sqlorContext(dbname) as sor: current = product_ownerid visited = {product_ownerid} while current != seller_orgid: sql = """SELECT orgid FROM sub_resellers WHERE resellerid = ${resellerid}$ AND status = '1' LIMIT 100""" recs = await sor.sqlExe(sql, {'resellerid': current}) found = False for r in (recs or []): orgid = r.get('orgid') if hasattr(r, 'get') else getattr(r, 'orgid', None) if orgid and orgid == seller_orgid: chain.append(orgid) return chain if orgid and orgid not in visited: visited.add(orgid) chain.append(orgid) current = orgid found = True break if not found: # Can't reach seller from current, chain ends here break return chain # ============================================================ # Discount Calculation API (called by other modules during sales) # ============================================================ async def calculate_sale_amounts(request, params_kw): """ Calculate supply and distribution amounts for a sale. Called by accounting/other modules during product sales. Parameters: resellerid: 分销商机构ID sub_reseller_id: 二级分销商ID(可选) supplier_id: 供应商ID(可选) prodtypeid: 产品分类ID productid: 产品ID quantity: 销售数量 unit_price: 销售单价 Returns: supply_discount, supply_amount, distribution_discount, distribution_amount, profit_amount """ env = ServerEnv() data = params_kw resellerid = data.get("resellerid") sub_reseller_id = data.get("sub_reseller_id") supplier_id = data.get("supplier_id") prodtypeid = data.get("prodtypeid") productid = data.get("productid") quantity = float(data.get("quantity", 0)) unit_price = float(data.get("unit_price", 0)) total_amount = quantity * unit_price db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: biz_date = await env.get_business_date(sor) # Step 1: Find active supply contract for this supplier supply_discount = 1.0 contract_id = None if supplier_id: sql_sc = """SELECT id, default_discount FROM supply_contracts WHERE resellerid = ${resellerid}$ AND supplier_id = ${supplier_id}$ AND status = '1' AND start_date <= ${biz_date}$ AND (end_date IS NULL OR end_date > ${biz_date}$)""" recs_sc = await sor.sqlExe(sql_sc, { "resellerid": resellerid, "supplier_id": supplier_id, "biz_date": biz_date }) if recs_sc: contract_id = recs_sc[0].id supply_discount = float(recs_sc[0].default_discount) # Try to find product-specific discount in contract items # Priority: exact product > product type > NULL (default) sql_sci = """SELECT discount FROM supply_contract_items WHERE contract_id = ${contract_id}$ AND ((prodtypeid = ${prodtypeid}$ AND productid = ${productid}$) OR (prodtypeid = ${prodtypeid}$ AND productid IS NULL) OR (prodtypeid IS NULL AND productid IS NULL)) ORDER BY CASE WHEN prodtypeid IS NOT NULL AND productid IS NOT NULL THEN 1 WHEN prodtypeid IS NOT NULL AND productid IS NULL THEN 2 ELSE 3 END LIMIT 1""" recs_sci = await sor.sqlExe(sql_sci, { "contract_id": contract_id, "prodtypeid": prodtypeid, "productid": productid }) if recs_sci: supply_discount = float(recs_sci[0].discount) supply_amount = total_amount * supply_discount # Step 2: Find active distribution agreement for this sub-reseller distribution_discount = 1.0 agreement_id = None if sub_reseller_id: sql_da = """SELECT id, default_discount FROM distribution_agreements WHERE resellerid = ${resellerid}$ AND sub_reseller_id = ${sub_reseller_id}$ AND status = '1' AND start_date <= ${biz_date}$ AND (end_date IS NULL OR end_date > ${biz_date}$)""" recs_da = await sor.sqlExe(sql_da, { "resellerid": resellerid, "sub_reseller_id": sub_reseller_id, "biz_date": biz_date }) if recs_da: agreement_id = recs_da[0].id distribution_discount = float(recs_da[0].default_discount) # Try to find product-specific discount with priority ordering sql_dai = """SELECT discount FROM distribution_agreement_items WHERE agreement_id = ${agreement_id}$ AND ((prodtypeid = ${prodtypeid}$ AND productid = ${productid}$) OR (prodtypeid = ${prodtypeid}$ AND productid IS NULL) OR (prodtypeid IS NULL AND productid IS NULL)) ORDER BY CASE WHEN prodtypeid IS NOT NULL AND productid IS NOT NULL THEN 1 WHEN prodtypeid IS NOT NULL AND productid IS NULL THEN 2 ELSE 3 END LIMIT 1""" recs_dai = await sor.sqlExe(sql_dai, { "agreement_id": agreement_id, "prodtypeid": prodtypeid, "productid": productid }) if recs_dai: distribution_discount = float(recs_dai[0].discount) distribution_amount = total_amount * distribution_discount profit_amount = distribution_amount - supply_amount result = { "contract_id": contract_id, "agreement_id": agreement_id, "total_amount": round(total_amount, 2), "supply_discount": supply_discount, "supply_amount": round(supply_amount, 2), "distribution_discount": distribution_discount, "distribution_amount": round(distribution_amount, 2), "profit_amount": round(profit_amount, 2), } return json.dumps({"status": "ok", "data": result}) # ============================================================ # Platform Supply Relations APIs (P0) # ============================================================ def _generate_psr_code(supplier_org_id, buyer_org_id): """Generate unique relation code: PSR-{YYYYMMDD}-{seq}.""" env = ServerEnv() today = env.strdate(env.today()) prefix = f"PSR-{today.replace('-', '')}" db, dbname = _get_sor() with db.sqlorContext(dbname) as sor: sql = """SELECT COUNT(*) as cnt FROM platform_supply_relations WHERE supplier_org_id = ${sid}$ AND relation_code LIKE ${prefix}$""" recs = sor.sqlExe(sql, {"sid": supplier_org_id, "prefix": prefix + "%"}) seq = (recs[0].cnt if recs else 0) + 1 return f"{prefix}-{seq:04d}" async def create_platform_supply_relations(request, params_kw): """Create a platform supply relation.""" env = ServerEnv() user_id = await env.get_user() data = params_kw db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: supplier_org_id = data.get("supplier_org_id") buyer_org_id = data.get("buyer_org_id") relation_code = data.get("relation_code") or _generate_psr_code(supplier_org_id, buyer_org_id) now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") rec = { "id": getID(), "supplier_org_id": supplier_org_id, "buyer_org_id": buyer_org_id, "relation_code": relation_code, "relation_name": data.get("relation_name"), "relation_type": data.get("relation_type", "distribution"), "settlement_mode": data.get("settlement_mode", "discount"), "default_discount": data.get("default_discount", 1.0), "default_commission_rate": data.get("default_commission_rate", 0.0), "sign_date": data.get("sign_date"), "start_date": data.get("start_date"), "end_date": data.get("end_date"), "status": data.get("status", "1"), "remark": data.get("remark"), "created_by": user_id, "created_at": now, "updated_at": now, } await sor.C("platform_supply_relations", rec) return json.dumps({"status": "ok", "data": rec, "message": "创建成功"}) async def update_platform_supply_relations(request, params_kw): """Update a platform supply relation.""" data = params_kw db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") rec = {"id": data["id"], "updated_at": now} for key in ["relation_name", "relation_type", "settlement_mode", "default_discount", "default_commission_rate", "sign_date", "start_date", "end_date", "status", "remark"]: if key in data: rec[key] = data[key] await sor.U("platform_supply_relations", rec) return json.dumps({"status": "ok", "message": "更新成功"}) async def delete_platform_supply_relations(request, params_kw): """Delete a platform supply relation and its products.""" data = params_kw db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: await sor.D("platform_supply_products", {"relation_id": data["id"]}) await sor.D("platform_supply_relations", {"id": data["id"]}) return json.dumps({"status": "ok", "message": "删除成功"}) # ============================================================ # Platform Supply Products APIs (P0) # ============================================================ async def create_platform_supply_products(request, params_kw): """Create a platform supply product item.""" data = params_kw db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") rec = { "id": getID(), "relation_id": data.get("relation_id"), "supplier_org_id": data.get("supplier_org_id"), "buyer_org_id": data.get("buyer_org_id"), "source_product_id": data.get("source_product_id"), "supply_price": data.get("supply_price", 0), "suggested_retail_price": data.get("suggested_retail_price"), "discount": data.get("discount", 1.0), "commission_rate": data.get("commission_rate", 0.0), "min_order_qty": data.get("min_order_qty"), "status": data.get("status", "1"), "remark": data.get("remark"), "created_at": now, "updated_at": now, } await sor.C("platform_supply_products", rec) return json.dumps({"status": "ok", "data": rec, "message": "创建成功"}) async def update_platform_supply_products(request, params_kw): """Update a platform supply product item.""" data = params_kw db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") rec = {"id": data["id"], "updated_at": now} for key in ["supply_price", "suggested_retail_price", "discount", "commission_rate", "min_order_qty", "status", "remark"]: if key in data: rec[key] = data[key] await sor.U("platform_supply_products", rec) return json.dumps({"status": "ok", "message": "更新成功"}) async def delete_platform_supply_products(request, params_kw): """Delete a platform supply product item.""" data = params_kw db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: await sor.D("platform_supply_products", {"id": data["id"]}) return json.dumps({"status": "ok", "message": "删除成功"}) # ============================================================ # Product Supplier Mapping APIs (P0) # ============================================================ async def create_product_supplier_mapping(request, params_kw): """Create a product supplier mapping.""" data = params_kw db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") rec = { "id": getID(), "product_id": data.get("product_id"), "product_category_id": data.get("product_category_id"), "supplier_type": data.get("supplier_type", "internal"), "supplier_org_id": data.get("supplier_org_id"), "external_supplier_id": data.get("external_supplier_id"), "buyer_org_id": data.get("buyer_org_id"), "supply_price": data.get("supply_price"), "currency": data.get("currency", "CNY"), "min_order_qty": data.get("min_order_qty", 1), "lead_time_days": data.get("lead_time_days"), "is_preferred": data.get("is_preferred", "0"), "status": data.get("status", "1"), "relation_id": data.get("relation_id"), "contract_id": data.get("contract_id"), "created_at": now, "updated_at": now, } await sor.C("product_supplier_mapping", rec) return json.dumps({"status": "ok", "data": rec, "message": "创建成功"}) async def update_product_supplier_mapping(request, params_kw): """Update a product supplier mapping.""" data = params_kw db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") rec = {"id": data["id"], "updated_at": now} for key in ["supply_price", "currency", "min_order_qty", "lead_time_days", "is_preferred", "status", "remark", "relation_id", "contract_id"]: if key in data: rec[key] = data[key] await sor.U("product_supplier_mapping", rec) return json.dumps({"status": "ok", "message": "更新成功"}) async def delete_product_supplier_mapping(request, params_kw): """Delete a product supplier mapping.""" data = params_kw db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: await sor.D("product_supplier_mapping", {"id": data["id"]}) return json.dumps({"status": "ok", "message": "删除成功"}) async def get_agreement_discount_items(request): """Return products with discount status for a distribution agreement.""" params_kw = getattr(request._run_ns, 'params_kw', {}) or {} agreement_id = params_kw.get('agreement_id') or params_kw.get('id') if not agreement_id: return [] env = request._run_ns userorgid = await env.get_userorgid() or '0' # Get products from sage products = [] async with get_sor_context(env, 'sage') as sor: rows = await sor.sqlExe( """SELECT p.id as productid, p.product_name, p.product_code, p.price as sale_price, pc.id as category_id, pc.name as category_name FROM product p LEFT JOIN product_category pc ON p.category_id = pc.id WHERE p.status = '1' AND (p.org_id = ${oid}$ OR p.id IN (SELECT product_id FROM product_org_auth WHERE org_id = ${oid}$)) ORDER BY pc.sort_order, pc.name, p.sort_order, p.product_name""", {'oid': userorgid} ) for r in rows: products.append({ 'productid': r.productid, 'product_name': r.product_name, 'product_code': r.product_code, 'sale_price': str(r.sale_price) if r.sale_price else '', 'category_id': r.category_id or '', 'category_name': r.category_name or '', }) # Get existing agreement items from supplychain items = {} if products: async with get_sor_context(env, 'supplychain') as sor: rows = await sor.sqlExe( "SELECT id, productid, discount FROM distribution_agreement_items WHERE agreement_id = ${aid}$", {'aid': agreement_id} ) for r in rows: items[r.productid] = {'id': r.id, 'discount': r.discount} # Merge result = [] for p in products: p['item_id'] = '' p['discount'] = '1.0' if p['productid'] in items: p['item_id'] = items[p['productid']]['id'] p['discount'] = str(items[p['productid']]['discount']) result.append(p) return result async def get_contract_discount_items(request): """Return products with discount status for a supply contract. Filters products where product.providerid matches the contract's supplier_id. """ params_kw = getattr(request._run_ns, 'params_kw', {}) or {} contract_id = params_kw.get('contract_id') or params_kw.get('id') if not contract_id: return [] env = request._run_ns # Get supplier_id from contract supplier_id = None async with get_sor_context(env, 'supplychain') as sor: rows = await sor.sqlExe( "SELECT supplier_id FROM supply_contracts WHERE id = ${cid}$", {'cid': contract_id} ) if rows: supplier_id = rows[0].supplier_id if not supplier_id: return [] # Get products from sage — filter by providerid = supplier_id products = [] async with get_sor_context(env, 'sage') as sor: rows = await sor.sqlExe( "SELECT p.id as productid, p.product_name, p.product_code, " "p.price as sale_price, pc.id as category_id, pc.name as category_name " "FROM product p " "LEFT JOIN product_category pc ON p.category_id = pc.id " "WHERE p.status = '1' AND p.providerid = ${sid}$ " "ORDER BY pc.sort_order, pc.name, p.sort_order, p.product_name", {'sid': supplier_id} ) for r in rows: products.append({ 'productid': r.productid, 'product_name': r.product_name, 'product_code': r.product_code, 'sale_price': str(r.sale_price) if r.sale_price else '', 'category_id': r.category_id or '', 'category_name': r.category_name or '', }) # Get existing contract items from supplychain items = {} if products: async with get_sor_context(env, 'supplychain') as sor: rows = await sor.sqlExe( "SELECT id, productid, discount FROM supply_contract_items WHERE contract_id = ${cid}$", {'cid': contract_id} ) for r in rows: items[r.productid] = {'id': r.id, 'discount': r.discount} # Merge result = [] for p in products: p['item_id'] = '' p['discount'] = '1.0' if p['productid'] in items: p['item_id'] = items[p['productid']]['id'] p['discount'] = str(items[p['productid']]['discount']) result.append(p) return result # ============================================================ # Register functions with ServerEnv # ============================================================ def load_supplychain(): """Register all supplychain functions with ServerEnv.""" env = ServerEnv() # Supplier (register both singular and plural for backward compat) env.create_supplier = create_supplier env.create_suppliers = create_supplier env.update_supplier = update_supplier env.update_suppliers = update_supplier env.delete_supplier = delete_supplier env.delete_suppliers = delete_supplier # Supply Contract env.create_supply_contract = create_supply_contract env.create_supply_contracts = create_supply_contract env.update_supply_contract = update_supply_contract env.update_supply_contracts = update_supply_contract env.delete_supply_contract = delete_supply_contract env.delete_supply_contracts = delete_supply_contract # Supply Contract Items env.create_supply_contract_item = create_supply_contract_item env.create_supply_contract_items = create_supply_contract_item env.update_supply_contract_item = update_supply_contract_item env.update_supply_contract_items = update_supply_contract_item env.delete_supply_contract_item = delete_supply_contract_item env.delete_supply_contract_items = delete_supply_contract_item # Sub-Reseller env.create_sub_reseller = create_sub_reseller env.create_sub_resellers = create_sub_reseller env.update_sub_reseller = update_sub_reseller env.update_sub_resellers = update_sub_reseller env.delete_sub_reseller = delete_sub_reseller env.delete_sub_resellers = delete_sub_reseller # Sub-Distributor (not yet implemented) # Distribution Agreement env.create_distribution_agreement = create_distribution_agreement env.create_distribution_agreements = create_distribution_agreement env.update_distribution_agreement = update_distribution_agreement env.update_distribution_agreements = update_distribution_agreement env.delete_distribution_agreement = delete_distribution_agreement env.delete_distribution_agreements = delete_distribution_agreement # Distribution Agreement Items env.create_distribution_agreement_item = create_distribution_agreement_item env.create_distribution_agreement_items = create_distribution_agreement_item env.update_distribution_agreement_item = update_distribution_agreement_item env.update_distribution_agreement_items = update_distribution_agreement_item env.delete_distribution_agreement_item = delete_distribution_agreement_item env.delete_distribution_agreement_items = delete_distribution_agreement_item # Sales Ledger env.create_sales_ledger = create_sales_ledger env.create_sales_ledgers = create_sales_ledger env.update_sales_ledger = update_sales_ledger env.update_sales_ledgers = update_sales_ledger env.delete_sales_ledger = delete_sales_ledger env.delete_sales_ledgers = delete_sales_ledger # P0: Platform Supply Relations env.create_platform_supply_relations = create_platform_supply_relations env.update_platform_supply_relations = update_platform_supply_relations env.delete_platform_supply_relations = delete_platform_supply_relations # P0: Platform Supply Products env.create_platform_supply_products = create_platform_supply_products env.update_platform_supply_products = update_platform_supply_products env.delete_platform_supply_products = delete_platform_supply_products # Agreement discount setting UI env.get_agreement_discount_items = get_agreement_discount_items env.get_contract_discount_items = get_contract_discount_items # P0: Product Supplier Mapping env.create_product_supplier_mapping = create_product_supplier_mapping env.update_product_supplier_mapping = update_product_supplier_mapping env.delete_product_supplier_mapping = delete_product_supplier_mapping # Calculation API env.calculate_sale_amounts = calculate_sale_amounts # Distribution chain query env.get_distribution_chain = get_distribution_chain # Attachment folder helper env.ensure_contract_folder = ensure_contract_folder env.ContractAttachMgr = ContractAttachMgr return True async def ensure_contract_folder(request, contract_id, fiid): """Ensure a filemgr folder exists for a contract attachment. Returns the folderid (uses contract_id as folder id).""" db, dbname = _get_sor() async with db.sqlorContext(dbname) as sor: recs = await sor.sqlExe( "SELECT id FROM folder WHERE fiid=${fiid}$ AND name=${name}$", {'fiid': fiid, 'name': contract_id} ) if recs and len(recs) > 0: return recs[0].id # Create new folder fid = getID() await sor.sqlExe( "INSERT INTO folder (id, parentid, fiid, name) VALUES (${id}$, ${pid}$, ${fiid}$, ${name}$)", {'id': fid, 'pid': '0', 'fiid': fiid, 'name': contract_id} ) return fid