feat: 完善分销商管理CRUD — sub_distributors/distribution_agreements/distribution_agreement_items

- 增强3个CRUD JSON配置:过滤器、子表关联、下拉选择alters
- 修复9个API dspy文件:移除违规import,改用init.py函数直接调用
- 新增2个搜索API:get_search_sub_reseller_id、get_search_agreement_id
- 自动生成分销商编号(SD-YYYYMMDD-NNNN)和协议编号(DA-YYYYMMDD-NNNN)
- 级联删除:删除分销商时级联删除协议及明细,删除协议时级联删除明细
- 更新load_path.py注册新API路径
This commit is contained in:
Hermes Agent 2026-06-17 15:18:27 +08:00
parent 669c491f93
commit 047ec1800a
24 changed files with 398 additions and 159 deletions

View File

@ -1,13 +1,45 @@
{
"tblname": "distribution_agreement_items",
"alias": "distribution_agreement_items_list",
"title": "分销协议产品折扣",
"title": "分销协议产品折扣管理",
"params": {
"sortby": ["prodtypeid", "productid"],
"sortby": [
"prodtypeid",
"productid"
],
"logined_userorgid": "resellerid",
"browserfields": {
"exclouded": ["id", "agreement_id", "resellerid"]
"exclouded": [
"id",
"agreement_id",
"resellerid"
],
"alters": {
"agreement_id": {
"uitype": "code",
"dataurl": "{{entire_url('../api/get_search_agreement_id.dspy')}}",
"valueField": "agreement_id",
"textField": "agreement_id_text"
},
"prodtypeid": {
"uitype": "code",
"dataurl": "{{entire_url('../api/get_search_prodtypeid.dspy')}}",
"valueField": "prodtypeid",
"textField": "prodtypeid_text"
},
"productid": {
"uitype": "code",
"dataurl": "{{entire_url('../api/get_search_productid.dspy')}}",
"valueField": "productid",
"textField": "productid_text"
}
}
},
"editexclouded": [
"id",
"resellerid",
"created_at"
],
"editable": {
"new_data_url": "{{entire_url('../api/distribution_agreement_items_create.dspy')}}",
"update_data_url": "{{entire_url('../api/distribution_agreement_items_update.dspy')}}",

View File

@ -3,33 +3,79 @@
"alias": "distribution_agreements_list",
"title": "分销协议管理",
"params": {
"sortby": ["created_at desc"],
"sortby": [
"created_at desc"
],
"logined_userorgid": "resellerid",
"data_filter": {
"AND": [
{"field": "agreement_name", "op": "LIKE", "var": "agreement_name"},
{"field": "agreement_code", "op": "LIKE", "var": "agreement_code"},
{"field": "status", "op": "=", "var": "status"}
{
"field": "agreement_name",
"op": "LIKE",
"var": "agreement_name"
},
{
"field": "agreement_code",
"op": "LIKE",
"var": "agreement_code"
},
{
"field": "sub_reseller_id",
"op": "=",
"var": "sub_reseller_id"
},
{
"field": "status",
"op": "=",
"var": "status"
}
]
},
"filter_labels": {
"agreement_name": "协议名称",
"agreement_code": "协议编号",
"sub_reseller_id": "二级分销商",
"status": "状态"
},
"browserfields": {
"exclouded": ["id"],
"exclouded": [
"id",
"resellerid"
],
"alters": {
"sub_reseller_id": {
"uitype": "code",
"dataurl": "{{entire_url('../api/get_search_sub_reseller_id.dspy')}}",
"valueField": "sub_reseller_id",
"textField": "sub_reseller_id_text"
},
"status": {
"uitype": "code",
"data": [
{"value": "1", "text": "生效中"},
{"value": "2", "text": "已到期"},
{"value": "0", "text": "已终止"}
{
"value": "1",
"text": "生效中"
},
{
"value": "2",
"text": "已到期"
},
{
"value": "0",
"text": "已终止"
}
]
}
}
},
"editexclouded": [
"id",
"resellerid",
"agreement_code",
"created_by",
"created_at",
"updated_at"
],
"subtables": [
{
"field": "agreement_id",

View File

@ -1,22 +1,64 @@
{
"tblname": "sub_distributors",
"alias": "sub_distributors_list",
"title": "二级分销商管理",
"title": "分销商管理",
"params": {
"sortby": [
"created_at desc"
],
"logined_userorgid": "resellerid",
"data_filter": {
"AND": [
{
"field": "sub_dist_name",
"op": "LIKE",
"var": "sub_dist_name"
},
{
"field": "sub_dist_code",
"op": "LIKE",
"var": "sub_dist_code"
},
{
"field": "status",
"op": "=",
"var": "status"
}
]
},
"filter_labels": {
"sub_dist_name": "分销商名称",
"sub_dist_code": "分销商编号",
"status": "状态"
},
"browserfields": {
"exclouded": [
"id",
"resellerid",
"created_by",
"created_at",
"updated_at"
],
"alters": {
"status": {
"uitype": "code",
"data": [
{
"value": "1",
"text": "正常"
},
{
"value": "0",
"text": "停用"
}
]
}
}
},
"editexclouded": [
"id",
"resellerid",
"sub_dist_code",
"created_by",
"created_at",
"updated_at"
@ -25,6 +67,14 @@
"new_data_url": "{{entire_url('../api/sub_distributors_create.dspy')}}",
"update_data_url": "{{entire_url('../api/sub_distributors_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/sub_distributors_delete.dspy')}}"
},
"subtables": [
{
"field": "sub_reseller_id",
"title": "分销协议",
"url": "{{entire_url('../distribution_agreements_list')}}",
"subtable": "distribution_agreements"
}
]
}
}

View File

@ -99,6 +99,8 @@ PATHS_LOGINED = [
"/supplychain/api/get_search_contract_id.dspy",
"/supplychain/api/get_search_prodtypeid.dspy",
"/supplychain/api/get_search_productid.dspy",
"/supplychain/api/get_search_sub_reseller_id.dspy",
"/supplychain/api/get_search_agreement_id.dspy",
# CRUD API — sub_distributors
"/supplychain/api/sub_distributors_create.dspy",
"/supplychain/api/sub_distributors_update.dspy",

View File

@ -1,9 +1,2 @@
import json
from ahserver.serverenv import ServerEnv
env = ServerEnv()
create_func = getattr(env, 'create_distribution_agreement_items', None)
if create_func is None:
print(json.dumps({"status": "error", "message": "create_distribution_agreement_items function not found"}))
else:
result = await create_func(request, params_kw)
print(result)
result = await create_distribution_agreement_items(request, params_kw)
return json.loads(result) if isinstance(result, str) else result

View File

@ -1,9 +1,2 @@
import json
from ahserver.serverenv import ServerEnv
env = ServerEnv()
delete_func = getattr(env, 'delete_distribution_agreement_items', None)
if delete_func is None:
print(json.dumps({"status": "error", "message": "delete_distribution_agreement_items function not found"}))
else:
result = await delete_func(request, params_kw)
print(result)
result = await delete_distribution_agreement_items(request, params_kw)
return json.loads(result) if isinstance(result, str) else result

View File

@ -1,9 +1,2 @@
import json
from ahserver.serverenv import ServerEnv
env = ServerEnv()
update_func = getattr(env, 'update_distribution_agreement_items', None)
if update_func is None:
print(json.dumps({"status": "error", "message": "update_distribution_agreement_items function not found"}))
else:
result = await update_func(request, params_kw)
print(result)
result = await update_distribution_agreement_items(request, params_kw)
return json.loads(result) if isinstance(result, str) else result

View File

@ -1,9 +1,2 @@
import json
from ahserver.serverenv import ServerEnv
env = ServerEnv()
create_func = getattr(env, 'create_distribution_agreements', None)
if create_func is None:
print(json.dumps({"status": "error", "message": "create_distribution_agreements function not found"}))
else:
result = await create_func(request, params_kw)
print(result)
result = await create_distribution_agreements(request, params_kw)
return json.loads(result) if isinstance(result, str) else result

View File

@ -1,9 +1,2 @@
import json
from ahserver.serverenv import ServerEnv
env = ServerEnv()
delete_func = getattr(env, 'delete_distribution_agreements', None)
if delete_func is None:
print(json.dumps({"status": "error", "message": "delete_distribution_agreements function not found"}))
else:
result = await delete_func(request, params_kw)
print(result)
result = await delete_distribution_agreements(request, params_kw)
return json.loads(result) if isinstance(result, str) else result

View File

@ -1,9 +1,2 @@
import json
from ahserver.serverenv import ServerEnv
env = ServerEnv()
update_func = getattr(env, 'update_distribution_agreements', None)
if update_func is None:
print(json.dumps({"status": "error", "message": "update_distribution_agreements function not found"}))
else:
result = await update_func(request, params_kw)
print(result)
result = await update_distribution_agreements(request, params_kw)
return json.loads(result) if isinstance(result, str) else result

View File

@ -0,0 +1,16 @@
result = [{'agreement_id': '', 'agreement_id_text': '全部'}]
try:
userorgid = await get_userorgid()
if not userorgid:
return json.dumps(result, ensure_ascii=False)
db = DBPools()
dbname = get_module_dbname('supplychain')
async with db.sqlorContext(dbname) as sor:
rows = await sor.sqlExe(
"select id as agreement_id, agreement_name as agreement_id_text from distribution_agreements where resellerid = ${userorgid}$ and status = '1' order by agreement_name",
{"userorgid": userorgid}
)
return json.dumps(result + list(rows), ensure_ascii=False)
except Exception as e:
debug(f'get_search_agreement_id error: {e}')
return json.dumps(result, ensure_ascii=False)

View File

@ -0,0 +1,16 @@
result = [{'sub_reseller_id': '', 'sub_reseller_id_text': '全部'}]
try:
userorgid = await get_userorgid()
if not userorgid:
return json.dumps(result, ensure_ascii=False)
db = DBPools()
dbname = get_module_dbname('supplychain')
async with db.sqlorContext(dbname) as sor:
rows = await sor.sqlExe(
"select id as sub_reseller_id, sub_reseller_name as sub_reseller_id_text from sub_resellers where resellerid = ${userorgid}$ and status = '1' order by sub_reseller_name",
{"userorgid": userorgid}
)
return json.dumps(result + list(rows), ensure_ascii=False)
except Exception as e:
debug(f'get_search_sub_reseller_id error: {e}')
return json.dumps(result, ensure_ascii=False)

View File

@ -1,28 +1,2 @@
import json
from appPublic.uniqueID import getID
from datetime import datetime
async def main(request, params_kw):
"""Create a new sub_distributors record."""
user_id = await get_user()
user_orgid = await get_userorgid()
dbname = get_module_dbname('supplychain')
data = params_kw.get("data", "{}")
if isinstance(data, str):
data = json.loads(data)
data["id"] = getID()
data["resellerid"] = user_orgid
data["created_by"] = user_id
data["created_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Auto-generate sub distributor code
if not data.get("sub_dist_code"):
data["sub_dist_code"] = f"SUB-{datetime.now().strftime('%Y%m%d')}-{getID()[:4].upper()}"
config = getConfig(".")
DBPools(config.databases)
async with db.sqlorContext(dbname) as sor:
await sor.C("sub_distributors", data)
return json.dumps({"status": "ok", "data": data})
result = await create_sub_distributors(request, params_kw)
return json.loads(result) if isinstance(result, str) else result

View File

@ -1,19 +1,2 @@
import json
async def main(request, params_kw):
"""Delete a sub_distributors record."""
dbname = get_module_dbname('supplychain')
data = params_kw.get("data", "{}")
if isinstance(data, str):
data = json.loads(data)
record_id = data.get("id")
if not record_id:
return json.dumps({"status": "error", "message": "Missing record id"})
config = getConfig(".")
DBPools(config.databases)
async with db.sqlorContext(dbname) as sor:
await sor.D("sub_distributors", {"id": record_id})
return json.dumps({"status": "ok", "message": "Deleted successfully"})
result = await delete_sub_distributors(request, params_kw)
return json.loads(result) if isinstance(result, str) else result

View File

@ -1,27 +1,2 @@
import json
from datetime import datetime
async def main(request, params_kw):
"""Update a sub_distributors record."""
user_id = await get_user()
dbname = get_module_dbname('supplychain')
data = params_kw.get("data", "{}")
if isinstance(data, str):
data = json.loads(data)
record_id = data.get("id")
if not record_id:
return json.dumps({"status": "error", "message": "Missing record id"})
data["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Remove fields that should not be updated
for key in ["id", "resellerid", "created_by", "created_at"]:
data.pop(key, None)
config = getConfig(".")
DBPools(config.databases)
async with db.sqlorContext(dbname) as sor:
await sor.U("sub_distributors", data)
return json.dumps({"status": "ok", "data": data})
result = await update_sub_distributors(request, params_kw)
return json.loads(result) if isinstance(result, str) else result

View File

@ -23,6 +23,7 @@ if not userorgid:
}
}
ns['resellerid'] = userorgid
ns['created_at'] = timestampstr()
db = DBPools()
dbname = get_module_dbname('supplychain')

View File

@ -23,7 +23,7 @@ if not ns.get('page'):
if not ns.get('sort'):
ns['sort'] = ["prodtypeid","productid"]
ns['sort'] = ["prodtypeid", "productid"]

View File

@ -44,10 +44,36 @@
"id",
"agreement_id",
"resellerid"
]
],
"alters": {
"agreement_id": {
"uitype": "code",
"dataurl": "{{entire_url('../api/get_search_agreement_id.dspy')}}",
"valueField": "agreement_id",
"textField": "agreement_id_text"
},
"prodtypeid": {
"uitype": "code",
"dataurl": "{{entire_url('../api/get_search_prodtypeid.dspy')}}",
"valueField": "prodtypeid",
"textField": "prodtypeid_text"
},
"productid": {
"uitype": "code",
"dataurl": "{{entire_url('../api/get_search_productid.dspy')}}",
"valueField": "productid",
"textField": "productid_text"
}
}
},
"editexclouded":[
"id",
"resellerid",
"created_at"
],
"fields":[
{
"name": "id",
@ -78,7 +104,7 @@
"valueField": "agreement_id",
"textField": "agreement_id_text"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
"dataurl": "{{entire_url('../api/get_search_agreement_id.dspy')}}"
},
{
"name": "resellerid",
@ -97,9 +123,12 @@
"type": "str",
"length": 32,
"cwidth": 18,
"uitype": "str",
"uitype": "code",
"datatype": "str",
"label": "产品分类ID"
"label": "产品分类ID",
"dataurl": "{{entire_url('../api/get_search_prodtypeid.dspy')}}",
"valueField": "prodtypeid",
"textField": "prodtypeid_text"
},
{
"name": "productid",
@ -107,9 +136,12 @@
"type": "str",
"length": 32,
"cwidth": 18,
"uitype": "str",
"uitype": "code",
"datatype": "str",
"label": "产品ID"
"label": "产品ID",
"dataurl": "{{entire_url('../api/get_search_productid.dspy')}}",
"valueField": "productid",
"textField": "productid_text"
},
{
"name": "discount",

View File

@ -24,6 +24,21 @@ if not userorgid:
}
ns['resellerid'] = userorgid
# Auto-generate agreement_code if not provided
if not ns.get('agreement_code'):
from datetime import datetime as _dt
_prefix = f"DA-{_dt.now().strftime('%Y%m%d')}"
async with DBPools().sqlorContext(get_module_dbname('supplychain')) as _sor:
_recs = await _sor.sqlExe(
"SELECT COUNT(*) as cnt FROM distribution_agreements WHERE resellerid = ${rid}$ AND agreement_code LIKE ${p}$",
{"rid": userorgid, "p": _prefix + "%"}
)
_seq = (_recs[0].cnt if _recs else 0) + 1
ns['agreement_code'] = f"{_prefix}-{_seq:04d}"
ns['created_by'] = userorgid
ns['created_at'] = timestampstr()
ns['updated_at'] = timestampstr()
db = DBPools()
dbname = get_module_dbname('supplychain')
async with db.sqlorContext(dbname) as sor:

View File

@ -21,6 +21,8 @@ ns['resellerid'] = userorgid
db = DBPools()
dbname = get_module_dbname('supplychain')
async with db.sqlorContext(dbname) as sor:
# Cascade delete agreement items first
await sor.D('distribution_agreement_items', {'agreement_id': ns['id']})
r = await sor.D('distribution_agreements', ns)
debug('delete success');
return {

View File

@ -52,9 +52,16 @@
"browserfields": {
"exclouded": [
"id"
"id",
"resellerid"
],
"alters": {
"sub_reseller_id": {
"uitype": "code",
"dataurl": "{{entire_url('../api/get_search_sub_reseller_id.dspy')}}",
"valueField": "sub_reseller_id",
"textField": "sub_reseller_id_text"
},
"status": {
"uitype": "code",
"data": [
@ -76,6 +83,15 @@
},
"editexclouded":[
"id",
"resellerid",
"agreement_code",
"created_by",
"created_at",
"updated_at"
],
"fields":[
{
"name": "id",
@ -126,7 +142,7 @@
"valueField": "sub_reseller_id",
"textField": "sub_reseller_id_text"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
"dataurl": "{{entire_url('../api/get_search_sub_reseller_id.dspy')}}"
},
{
"name": "agreement_code",
@ -271,6 +287,11 @@
"op": "LIKE",
"var": "agreement_code"
},
{
"field": "sub_reseller_id",
"op": "=",
"var": "sub_reseller_id"
},
{
"field": "status",
"op": "=",
@ -283,6 +304,7 @@
"filter_labels":{
"agreement_name": "协议名称",
"agreement_code": "协议编号",
"sub_reseller_id": "二级分销商",
"status": "状态"
},

View File

@ -24,6 +24,20 @@ if not userorgid:
}
ns['resellerid'] = userorgid
# Auto-generate sub_dist_code if not provided
if not ns.get('sub_dist_code'):
from datetime import datetime as _dt
_prefix = f"SD-{_dt.now().strftime('%Y%m%d')}"
async with DBPools().sqlorContext(get_module_dbname('supplychain')) as _sor:
_recs = await _sor.sqlExe(
"SELECT COUNT(*) as cnt FROM sub_distributors WHERE resellerid = ${rid}$ AND sub_dist_code LIKE ${p}$",
{"rid": userorgid, "p": _prefix + "%"}
)
_seq = (_recs[0].cnt if _recs else 0) + 1
ns['sub_dist_code'] = f"{_prefix}-{_seq:04d}"
ns['created_by'] = userorgid
ns['created_at'] = timestampstr()
db = DBPools()
dbname = get_module_dbname('supplychain')
async with db.sqlorContext(dbname) as sor:

View File

@ -21,6 +21,16 @@ ns['resellerid'] = userorgid
db = DBPools()
dbname = get_module_dbname('supplychain')
async with db.sqlorContext(dbname) as sor:
# Cascade delete distribution_agreement_items first (by agreement_id)
_agreements = await sor.sqlExe(
"SELECT id FROM distribution_agreements WHERE sub_reseller_id = ${sid}$",
{"sid": ns['id']}
)
for _ag in (_agreements or []):
await sor.D('distribution_agreement_items', {'agreement_id': _ag.id})
# Cascade delete distribution_agreements
await sor.D('distribution_agreements', {'sub_reseller_id': ns['id']})
# Delete the sub_distributor
r = await sor.D('sub_distributors', ns)
debug('delete success');
return {

View File

@ -15,6 +15,17 @@
"toolbar":{
"tools": [
{
"selected_row": true,
"name": "distribution_agreements",
"icon": "{{entire_url('/imgs/distribution_agreements.svg')}}",
"label": "分销协议"
}
]
},
"css":"card",
@ -41,16 +52,34 @@
"browserfields": {
"exclouded": [
"id",
"resellerid",
"created_by",
"created_at",
"updated_at"
],
"alters": {
"status": {
"uitype": "code",
"data": [
{
"value": "1",
"text": "正常"
},
{
"value": "0",
"text": "停用"
}
]
}
}
},
"editexclouded":[
"id",
"resellerid",
"sub_dist_code",
"created_by",
"created_at",
"updated_at"
@ -198,9 +227,19 @@
"nullable": "no",
"default": "1",
"cwidth": 4,
"uitype": "str",
"uitype": "code",
"datatype": "char",
"label": "状态"
"label": "状态",
"data": [
{
"value": "1",
"text": "正常"
},
{
"value": "0",
"text": "停用"
}
]
},
{
"name": "remark",
@ -245,6 +284,32 @@
"data_filter":{
"AND": [
{
"field": "sub_dist_name",
"op": "LIKE",
"var": "sub_dist_name"
},
{
"field": "sub_dist_code",
"op": "LIKE",
"var": "sub_dist_code"
},
{
"field": "status",
"op": "=",
"var": "status"
}
]
},
"filter_labels":{
"sub_dist_name": "分销商名称",
"sub_dist_code": "分销商编号",
"status": "状态"
},
@ -253,7 +318,33 @@
"cache_limit":5
}
,"binds":[]
,"binds":[
{
"wid": "self",
"event": "distribution_agreements",
"actiontype": "urlwidget",
"target": "PopupWindow",
"popup_options": {
"title": "分销协议",
"icon": "{{entire_url('/appbase/get_icon.dspy')}}?id=distribution_agreements",
"resizable": true,
"height": "70%",
"width": "70%"
},
"params_mapping": {
"mapping": {
"id": "sub_reseller_id",
"referer_widget": "referer_widget"
},
"need_other": false
},
"options": {
"method": "POST",
"params": {},
"url": "{{entire_url('../distribution_agreements_list')}}"
}
}
]
}]
}