feat: customer assignment - free_customer_assign + customer_reassign pages, register hook bind_type=3, sales list filter
This commit is contained in:
parent
f801507961
commit
029fd91d79
111
discount/init.py
111
discount/init.py
@ -881,6 +881,11 @@ def load_discount():
|
|||||||
# Marketing plan discount setting
|
# Marketing plan discount setting
|
||||||
env.get_marketing_discount_products = get_marketing_discount_products
|
env.get_marketing_discount_products = get_marketing_discount_products
|
||||||
env.get_marketing_info = get_marketing_info
|
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):
|
async def get_discount_setting_products(request):
|
||||||
@ -1020,3 +1025,109 @@ async def get_marketing_info(request):
|
|||||||
if recs:
|
if recs:
|
||||||
return {'name': recs[0].name or ''}
|
return {'name': recs[0].name or ''}
|
||||||
return {}
|
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]
|
||||||
|
|
||||||
|
|||||||
@ -177,9 +177,17 @@ for tblname in SALE_TABLES:
|
|||||||
# sale 相关 API
|
# sale 相关 API
|
||||||
for api_path in get_api_files():
|
for api_path in get_api_files():
|
||||||
basename = os.path.basename(api_path)
|
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)
|
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",
|
||||||
|
])
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 去重
|
# 去重
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|||||||
@ -1,15 +1,19 @@
|
|||||||
# 获取机构下所有客户及销售归属
|
# 获取当前机构下的客户及销售归属
|
||||||
|
userorgid = await get_userorgid()
|
||||||
|
if not userorgid:
|
||||||
|
return []
|
||||||
|
|
||||||
db = DBPools()
|
db = DBPools()
|
||||||
resellerid = params_kw.get('resellerid') or '0'
|
resellerid = userorgid
|
||||||
|
|
||||||
sql = """
|
sql = """
|
||||||
select o.id as customerid, o.orgname as customer_name,
|
select o.id as customerid, o.orgname as customer_name,
|
||||||
dcb.id as bind_id, dcb.sale_id,
|
dcb.id as bind_id, dcb.sale_id,
|
||||||
u.name as sale_name
|
u.nick_name as sale_name
|
||||||
from organization o
|
from organization o
|
||||||
left join discount_customer_bind dcb on dcb.customerid = o.id and dcb.resellerid = ${resellerid}$
|
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
|
left join users u on u.id = dcb.sale_id
|
||||||
where 1=1
|
where o.org_type = 'customer' and o.parentid = ${resellerid}$
|
||||||
order by o.orgname
|
order by o.orgname
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
17
wwwroot/api/save_customer_sale.dspy
Normal file
17
wwwroot/api/save_customer_sale.dspy
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
69
wwwroot/customer_reassign/index.ui
Normal file
69
wwwroot/customer_reassign/index.ui
Normal 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 %}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
67
wwwroot/free_customer_assign/index.ui
Normal file
67
wwwroot/free_customer_assign/index.ui
Normal 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 %}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user