feat: customer assignment - free_customer_assign + customer_reassign pages, register hook bind_type=3, sales list filter

This commit is contained in:
yumoqing 2026-07-13 17:56:05 +08:00
parent f801507961
commit 029fd91d79
6 changed files with 282 additions and 6 deletions

View File

@ -881,6 +881,11 @@ def load_discount():
# Marketing plan discount setting
env.get_marketing_discount_products = get_marketing_discount_products
env.get_marketing_info = get_marketing_info
# Customer assignment
env.get_free_customers = get_free_customers
env.get_bound_customers = get_bound_customers
env.save_customer_sale = save_customer_sale
env.get_reseller_sales = get_reseller_sales
async def get_discount_setting_products(request):
@ -1020,3 +1025,109 @@ async def get_marketing_info(request):
if recs:
return {'name': recs[0].name or ''}
return {}
async def get_free_customers(request):
"""Return unbound customers for the current reseller.
Used in Jinja2 template: {% set customers = get_free_customers(request) %}
"""
env = request._run_ns
userorgid = await env.get_userorgid()
if not userorgid or userorgid == '0':
return []
dbname = env.get_module_dbname('discount')
async with DBPools().sqlorContext(dbname) as sor:
# Find customers of this reseller that are unbound (no sale_id)
rows = await sor.sqlExe(
"""SELECT dcb.id as bind_id, dcb.customerid,
o.orgname as customer_name, dcb.sale_id
FROM discount_customer_bind dcb
JOIN organization o ON o.id = dcb.customerid
WHERE dcb.resellerid = ${rid}$ AND dcb.sale_id IS NULL
ORDER BY o.orgname""",
{'rid': userorgid}
)
return rows or []
async def get_bound_customers(request):
"""Return bound customers with their current sale for the current reseller.
Used in Jinja2 template: {% set customers = get_bound_customers(request) %}
"""
env = request._run_ns
userorgid = await env.get_userorgid()
if not userorgid or userorgid == '0':
return []
dbname = env.get_module_dbname('discount')
async with DBPools().sqlorContext(dbname) as sor:
rows = await sor.sqlExe(
"""SELECT dcb.id as bind_id, dcb.customerid,
o.orgname as customer_name, dcb.sale_id,
u.nick_name as sale_name
FROM discount_customer_bind dcb
JOIN organization o ON o.id = dcb.customerid
LEFT JOIN users u ON u.id = dcb.sale_id
WHERE dcb.resellerid = ${rid}$ AND dcb.sale_id IS NOT NULL
ORDER BY o.orgname""",
{'rid': userorgid}
)
return rows or []
async def save_customer_sale(request, params_kw):
"""Save/update sale assignment for a customer (upsert on discount_customer_bind).
Called from free_customer_assign and customer_reassign pages on blur.
"""
env = request._run_ns
userorgid = await env.get_userorgid()
bind_id = params_kw.get('bind_id')
customerid = params_kw.get('customerid')
sale_id = params_kw.get('sale_id')
if not customerid:
return {'status': 'error', 'message': 'Missing customerid'}
dbname = env.get_module_dbname('discount')
async with DBPools().sqlorContext(dbname) as sor:
if bind_id:
# Update existing binding
await sor.U('discount_customer_bind', {
'id': bind_id,
'sale_id': sale_id or None,
})
else:
# Create new binding for free customer
new_id = getID()
await sor.C('discount_customer_bind', {
'id': new_id,
'customerid': customerid,
'resellerid': userorgid,
'sale_id': sale_id or None,
'promo_code_id': None,
'bind_type': '2', # manual assignment
})
bind_id = new_id
return {'status': 'success', 'bind_id': bind_id}
async def get_reseller_sales(request):
"""Return sales users list for the current reseller (for dropdown).
Used in Jinja2 template: {% set sales = get_reseller_sales(request) %}
"""
env = request._run_ns
userorgid = await env.get_userorgid()
if not userorgid or userorgid == '0':
return []
async with DBPools().sqlorContext('sage') as sor:
rows = await sor.sqlExe(
"""SELECT DISTINCT u.id as sale_id, u.nick_name as sale_name
FROM users u
JOIN userrole ur ON ur.userid = u.id
JOIN role r ON r.id = ur.roleid
WHERE r.name = 'sale' AND u.orgid = ${oid}$
ORDER BY u.nick_name""",
{'oid': userorgid}
)
return [{'value': r.sale_id, 'text': r.sale_name} for r in rows]

View File

@ -177,9 +177,17 @@ for tblname in SALE_TABLES:
# sale 相关 API
for api_path in get_api_files():
basename = os.path.basename(api_path)
if any(k in basename for k in ["promo_code", "customer_bind"]):
if any(k in basename for k in ["promo_code", "customer_bind", "customer_sale"]):
PATHS_SALE.append(api_path)
# sale 客户分配页面
PATHS_SALE.extend([
f"/{MOD}/free_customer_assign",
f"/{MOD}/free_customer_assign/index.ui",
f"/{MOD}/customer_reassign",
f"/{MOD}/customer_reassign/index.ui",
])
# ============================================================
# 去重
# ============================================================

View File

@ -1,15 +1,19 @@
# 获取机构下所有客户及销售归属
# 获取当前机构下的客户及销售归属
userorgid = await get_userorgid()
if not userorgid:
return []
db = DBPools()
resellerid = params_kw.get('resellerid') or '0'
resellerid = userorgid
sql = """
select o.id as customerid, o.orgname as customer_name,
dcb.id as bind_id, dcb.sale_id,
u.name as sale_name
u.nick_name as sale_name
from organization o
left join discount_customer_bind dcb on dcb.customerid = o.id and dcb.resellerid = ${resellerid}$
left join user u on u.id = dcb.sale_id
where 1=1
left join users u on u.id = dcb.sale_id
where o.org_type = 'customer' and o.parentid = ${resellerid}$
order by o.orgname
"""

View File

@ -0,0 +1,17 @@
# Save/update customer-sale assignment (called on blur from customer assign pages)
from discount.init import save_customer_sale
ret = await save_customer_sale(request, params_kw)
if ret.get('status') == 'success':
return {
'widgettype': 'Message',
'options': {
'title': '成功', 'message': '已更新', 'type': 'success', 'timeout': 2
}
}
return {
'widgettype': 'Error',
'options': {
'title': '失败', 'message': ret.get('message', '未知错误'), 'timeout': 3
}
}

View File

@ -0,0 +1,69 @@
{% set customers = get_bound_customers(request) %}
{% set sales = get_reseller_sales(request) %}
{
"widgettype": "VBox",
"options": {"width": "100%", "height": "100%", "css": "filler"},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "客户再分配 - 更换销售",
"fontSize": "16px", "fontWeight": "600", "color": "#F1F5F9", "padding": "8px 12px"
}
},
{
"widgettype": "HBox",
"options": {"bgcolor": "#0F172A", "padding": "8px 0", "borderRadius": "6px 6px 0 0", "alignItems": "center", "width": "100%"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "客户名称", "width": "30%", "padding": "0 8px", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8"}},
{"widgettype": "Text", "options": {"text": "当前销售", "width": "25%", "padding": "0 8px", "fontSize": "12px", "fontWeight": "600", "color": "#22C55E"}},
{"widgettype": "Text", "options": {"text": "更换为", "width": "35%", "padding": "0 8px", "fontSize": "12px", "fontWeight": "600", "color": "#F59E0B"}},
{"widgettype": "Text", "options": {"text": "", "width": "10%", "padding": "0 8px"}}
]
},
{
"widgettype": "VScrollPanel",
"options": {"width": "100%", "flex": "1"},
"subwidgets": [
{
"widgettype": "VBox",
"options": {"width": "100%"},
"subwidgets": [
{% for c in customers %}
{
"widgettype": "HBox",
"options": {"padding": "6px 0", "border": "0 0 1px 0", "borderColor": "#334155", "alignItems": "center", "width": "100%"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": {{json.dumps(c.customer_name or '')}}, "width": "30%", "padding": "0 8px", "fontSize": "12px", "color": "#E2E8F0"}},
{"widgettype": "Text", "options": {"text": {{json.dumps(c.sale_name or '-')}}, "width": "25%", "padding": "0 8px", "fontSize": "12px", "color": "#22C55E"}},
{
"widgettype": "UiCode",
"options": {
"width": "35%", "padding": "0 8px",
"name": "sale_id",
"value": "",
"placeholder": "选择新销售...",
"data": {{json.dumps(sales or [], ensure_ascii=False)}}
},
"binds": [{
"wid": "self", "event": "blur",
"actiontype": "urlwidget", "datawidget": "self", "target": "self",
"options": {
"url": "{{entire_url('/discount/api/save_customer_sale.dspy')}}",
"params": {
"bind_id": {{json.dumps(c.bind_id or '')}},
"customerid": {{json.dumps(c.customerid)}}
}
}
}]
},
{"widgettype": "Text", "options": {"text": "", "width": "10%", "padding": "0 8px"}}
]
}{% if not loop.last %},{% endif %}
{% endfor %}
]
}
]
}
]
}

View File

@ -0,0 +1,67 @@
{% set customers = get_free_customers(request) %}
{% set sales = get_reseller_sales(request) %}
{
"widgettype": "VBox",
"options": {"width": "100%", "height": "100%", "css": "filler"},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "自由客户分配 - 选择销售",
"fontSize": "16px", "fontWeight": "600", "color": "#F1F5F9", "padding": "8px 12px"
}
},
{
"widgettype": "HBox",
"options": {"bgcolor": "#0F172A", "padding": "8px 0", "borderRadius": "6px 6px 0 0", "alignItems": "center", "width": "100%"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "客户名称", "width": "45%", "padding": "0 8px", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8"}},
{"widgettype": "Text", "options": {"text": "分配销售", "width": "45%", "padding": "0 8px", "fontSize": "12px", "fontWeight": "600", "color": "#F59E0B"}},
{"widgettype": "Text", "options": {"text": "状态", "width": "10%", "padding": "0 8px", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8"}}
]
},
{
"widgettype": "VScrollPanel",
"options": {"width": "100%", "flex": "1"},
"subwidgets": [
{
"widgettype": "VBox",
"options": {"width": "100%"},
"subwidgets": [
{% for c in customers %}
{
"widgettype": "HBox",
"options": {"padding": "6px 0", "border": "0 0 1px 0", "borderColor": "#334155", "alignItems": "center", "width": "100%"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": {{json.dumps(c.customer_name or '')}}, "width": "45%", "padding": "0 8px", "fontSize": "12px", "color": "#E2E8F0"}},
{
"widgettype": "UiCode",
"options": {
"width": "45%", "padding": "0 8px",
"name": "sale_id",
"value": "",
"placeholder": "选择销售...",
"data": {{json.dumps(sales or [], ensure_ascii=False)}}
},
"binds": [{
"wid": "self", "event": "blur",
"actiontype": "urlwidget", "datawidget": "self", "target": "self",
"options": {
"url": "{{entire_url('/discount/api/save_customer_sale.dspy')}}",
"params": {
"bind_id": {{json.dumps(c.bind_id or '')}},
"customerid": {{json.dumps(c.customerid)}}
}
}
}]
},
{"widgettype": "Text", "options": {"text": "未分配", "width": "10%", "padding": "0 8px", "fontSize": "11px", "color": "#475569"}}
]
}{% if not loop.last %},{% endif %}
{% endfor %}
]
}
]
}
]
}