1151 lines
47 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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