- create_sub_reseller auto-creates organization record with orgid - get_distribution_chain walks from product_owner down to seller via sub_resellers
1038 lines
42 KiB
Python
1038 lines
42 KiB
Python
"""
|
||
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
|
||
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}."""
|
||
env = ServerEnv()
|
||
today = env.strdate(env.today())
|
||
prefix = f"SR-{today.replace('-', '')}"
|
||
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."""
|
||
env = ServerEnv()
|
||
user_id = await env.get_user()
|
||
resellerid = await env.get_userorgid()
|
||
data = params_kw
|
||
db, dbname = _get_sor()
|
||
|
||
# Handle external supplier: create new org
|
||
is_external = data.get("is_external", "1")
|
||
orgid = data.get("orgid")
|
||
|
||
if is_external == "1" and not orgid:
|
||
# External supplier: create new organization
|
||
supplier_name = data.get("supplier_name")
|
||
if not supplier_name:
|
||
return json.dumps({"status": "error", "message": "供应商名称不能为空"})
|
||
|
||
# Create org in sage database
|
||
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
|
||
elif is_external == "0" and not orgid:
|
||
return json.dumps({"status": "error", "message": "内部供应商必须选择所属机构"})
|
||
|
||
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": is_external,
|
||
"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", "is_external", "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 = 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:
|
||
sub_reseller_code = data.get("sub_reseller_code") or _generate_sub_reseller_code(resellerid)
|
||
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_reseller_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_reseller_code": sub_reseller_code,
|
||
"sub_reseller_name": data.get("sub_reseller_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"),
|
||
"created_by": user_id,
|
||
"created_at": now,
|
||
"updated_at": now,
|
||
}
|
||
await sor.C("sub_resellers", 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."""
|
||
data = params_kw
|
||
db, dbname = _get_sor()
|
||
async with db.sqlorContext(dbname) as sor:
|
||
await sor.D("distribution_agreement_items", {"id": data["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": "删除成功"})
|
||
|
||
|
||
# ============================================================
|
||
# 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
|
||
# 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
|