feat: 营销方案产品折扣设置(独立于普通折扣方案)

index.ui: 显示所有产品 + 基准折扣(全部用户) + 方案折扣, 标志'已加入/未加入'
get_products.dspy: 左联全部用户基准折扣 + 营销方案专属折扣
save_discount.dspy: 验证 0<新<基准, upsert 到 discount_detail
init.py: get_marketing_discount_products + get_marketing_info
marketing_list.json: toolbar 指向新页面
This commit is contained in:
yumoqing 2026-07-03 16:05:21 +08:00
parent ae6c547aff
commit 33cd1d287d
5 changed files with 274 additions and 2 deletions

View File

@ -733,6 +733,9 @@ def load_discount():
# Discount setting products list
env.get_discount_setting_products = get_discount_setting_products
env.get_discount_info = get_discount_info
# Marketing plan discount setting
env.get_marketing_discount_products = get_marketing_discount_products
env.get_marketing_info = get_marketing_info
async def get_discount_setting_products(request):
@ -795,3 +798,64 @@ async def get_discount_info(request):
if recs:
return {'name': recs[0].name or '', 'customer': recs[0].orgname or ''}
return {}
async def get_marketing_discount_products(request):
"""Get products with base (all-user) discount and marketing-specific discount.
Used in Jinja2 template: {% set products = get_marketing_discount_products(request) %}
"""
env = request._run_ns
discountid = (request._run_ns.params_kw or {}).get('discountid', '')
user_orgid = await env.get_userorgid()
if not discountid or not user_orgid:
return []
dbname = env.get_module_dbname('discount')
db = DBPools()
config = getConfig()
db.databases = config.databases
sql = """
SELECT
p.id as productid,
p.product_code,
p.product_name,
p.category_id,
pc.name as category_name,
base.discount as base_discount,
mkt.id as detail_id,
mkt.discount as marketing_discount,
CASE WHEN mkt.id IS NOT NULL THEN '1' ELSE '0' END as in_marketing
FROM product p
LEFT JOIN product_category pc ON p.category_id = pc.id
LEFT JOIN (
SELECT dd.productid, MAX(dd.discount) as discount
FROM discount_detail dd
JOIN discount d ON dd.discountid = d.id
WHERE d.resellerid = ${org_id}$ AND (d.customerid IS NULL OR d.customerid = '*')
GROUP BY dd.productid
) base ON base.productid = p.id
LEFT JOIN discount_detail mkt ON mkt.productid = p.id AND mkt.discountid = ${discountid}$
WHERE p.org_id = ${org_id}$ AND p.status = '1'
ORDER BY pc.name, p.product_name
"""
ns = {'discountid': discountid, 'org_id': user_orgid}
async with db.sqlorContext(dbname) as sor:
recs = await sor.sqlExe(sql, ns)
return recs if recs else []
async def get_marketing_info(request):
"""Get marketing plan name for display header."""
env = request._run_ns
discountid = (request._run_ns.params_kw or {}).get('discountid', '')
if not discountid:
return {}
dbname = env.get_module_dbname('discount')
db = DBPools()
config = getConfig()
db.databases = config.databases
sql = "SELECT name FROM discount_marketing WHERE id = ${discountid}$"
async with db.sqlorContext(dbname) as sor:
recs = await sor.sqlExe(sql, {'discountid': discountid})
if recs:
return {'name': recs[0].name or ''}
return {}

View File

@ -50,7 +50,7 @@
"need_other": false
},
"options": {
"url": "{{entire_url('../discount_setting')}}"
"url": "{{entire_url('../marketing_discount_setting')}}"
}
}
],

View File

@ -0,0 +1,49 @@
# 查询产品及折扣: 全部用户基准折扣 + 营销方案专属折扣
# 参数: discountid (营销方案id)
discountid = params_kw.get('discountid', '')
if not discountid:
return {"total": 0, "rows": []}
user_orgid = await get_userorgid()
if not user_orgid:
return {"total": 0, "rows": []}
ns = params_kw.copy()
if not ns.get('page'):
ns['page'] = 1
if not ns.get('sort'):
ns['sort'] = 'category_name, product_name'
sql = '''
SELECT
p.id as productid,
p.product_code,
p.product_name,
p.category_id,
pc.name as category_name,
base.discount as base_discount,
mkt.id as detail_id,
mkt.discount as marketing_discount,
CASE WHEN mkt.id IS NOT NULL THEN 1 ELSE 0 END as in_marketing
FROM product p
LEFT JOIN product_category pc ON p.category_id = pc.id
LEFT JOIN (
SELECT dd.productid, MAX(dd.discount) as discount
FROM discount_detail dd
JOIN discount d ON dd.discountid = d.id
WHERE d.resellerid = ${org_id}$ AND (d.customerid IS NULL OR d.customerid = '*')
GROUP BY dd.productid
) base ON base.productid = p.id
LEFT JOIN discount_detail mkt ON mkt.productid = p.id AND mkt.discountid = ${discountid}$
WHERE p.org_id = ${org_id}$ AND p.status = '1'
ORDER BY pc.name, p.product_name
'''
ns['org_id'] = user_orgid
db = DBPools()
dbname = get_module_dbname('discount')
async with db.sqlorContext(dbname) as sor:
r = await sor.sqlPaging(sql, ns.copy())
return r

View File

@ -0,0 +1,82 @@
{% set products = get_marketing_discount_products(request) %}
{% set info = get_marketing_info(request) %}
{
"widgettype": "VBox",
"options": {
"width": "100%",
"height": "100%",
"css": "filler"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"text": "营销方案产品折扣 - {{info.name or ''}}",
"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": "20%", "padding": "0 8px", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8"}},
{"widgettype": "Text", "options": {"text": "产品名称", "width": "26%", "padding": "0 8px", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8"}},
{"widgettype": "Text", "options": {"text": "基准折扣", "width": "12%", "padding": "0 8px", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8"}},
{"widgettype": "Text", "options": {"text": "方案折扣", "width": "12%", "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": "Text", "options": {"text": "操作", "width": "20%", "padding": "0 8px", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8"}}
]
},
{
"widgettype": "VScrollPanel",
"options": {
"width": "100%",
"flex": "1"
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {"width": "100%"},
"subwidgets": [
{% for p in products %}
{
"widgettype": "HBox",
"options": {
"padding": "6px 0",
"border": "0 0 1px 0",
"borderColor": "#334155",
"alignItems": "center",
"width": "100%"
},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "{{p.category_name or ''}}", "width": "20%", "padding": "0 8px", "fontSize": "12px", "color": "#E2E8F0"}},
{"widgettype": "Text", "options": {"text": "{{p.product_name or ''}}", "width": "26%", "padding": "0 8px", "fontSize": "12px", "color": "#F1F5F9"}},
{"widgettype": "Text", "options": {"text": "{{p.base_discount or '-'}}", "width": "12%", "padding": "0 8px", "fontSize": "12px", "color": "#22C55E"}},
{"widgettype": "UiFloat", "options": {"name": "discount", "value": "{{p.marketing_discount or ''}}", "placeholder": "{{p.base_discount or ''}}", "dec_len": 2, "width": "12%", "padding": "0 8px", "fontSize": "12px"},
"binds": [{"wid": "self", "event": "blur", "actiontype": "urlwidget", "datawidget": "self", "target": "self",
"options": {"url": "{{entire_url('./save_discount.dspy')}}",
"params": {"discountid": "{{params_kw.discountid}}", "base_discount": "{{p.base_discount or ''}}", "productid": "{{p.productid}}", "detail_id": "{{p.detail_id or ''}}"}}}]},
{"widgettype": "Text", "options": {"text": "{{'已加入' if p.in_marketing == '1' else '未加入'}}", "width": "10%", "padding": "0 8px", "fontSize": "11px", "color": "{{'#F59E0B' if p.in_marketing == '1' else '#475569'}}"}},
{"widgettype": "Button", "options": {"label": "{{'移出' if p.in_marketing == '1' else '加入'}}", "padding": "2px 8px", "fontSize": "11px", "borderRadius": "4px",
"bgcolor": "{{'#991B1B' if p.in_marketing == '1' else '#166534'}}", "color": "#FFFFFF", "border": "none"},
"binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "self",
"options": {"url": "{{entire_url('./save_discount.dspy')}}",
"params": {"discountid": "{{params_kw.discountid}}", "base_discount": "{{p.base_discount or '1.0'}}", "productid": "{{p.productid}}", "detail_id": "{{p.detail_id or ''}}", "discount": "{{'0' if p.in_marketing == '1' else p.base_discount or '1.0'}}"}}}]}
]
}{% if not loop.last %},{% endif %}
{% endfor %}
]
}
]
}
]
}

View File

@ -0,0 +1,77 @@
# 营销方案产品折扣保存
# 规则: 0 < 新折扣 < 全部用户基准折扣
# 自动 upsert 到 discount_detail
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid', 'type': 'error', 'timeout': 3}}
try:
discountid = params_kw.get('discountid')
productid = params_kw.get('productid')
discount_val = params_kw.get('discount')
base_discount = params_kw.get('base_discount')
detail_id = params_kw.get('detail_id')
if not discountid or not productid:
raise Exception('缺少必要参数')
# 过滤 NaN/空值
if discount_val is not None:
s = str(discount_val).strip().lower()
if s in ('', 'nan', 'none', 'null'):
discount_val = None
user_orgid = await get_userorgid()
dbname = get_module_dbname('discount')
# discount为0或空 → 删除明细
if not discount_val or float(discount_val) == 0:
if detail_id:
async with DBPools().sqlorContext(dbname) as sor:
await sor.D('discount_detail', {'id': detail_id})
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '已移出营销方案', 'type': 'success', 'timeout': 3}}
else:
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': 'ok', 'type': 'success', 'timeout': 3}}
return result
new_val = float(discount_val)
# 验证: 必须大于0
if new_val <= 0:
result = {'widgettype': 'Error', 'options': {'title': '折扣无效', 'message': '折扣必须大于0', 'timeout': 5}}
return result
# 验证: 不能超过基准折扣
if base_discount:
base_val = float(base_discount)
if new_val >= base_val:
result = {'widgettype': 'Error', 'options': {'title': '折扣无效', 'message': f'营销折扣({new_val})必须小于基准折扣({base_val})', 'timeout': 5}}
return result
# 验证: 不能超过100%1.0
if new_val > 1.0:
result = {'widgettype': 'Error', 'options': {'title': '折扣无效', 'message': '折扣不能大于1.0', 'timeout': 5}}
return result
async with DBPools().sqlorContext(dbname) as sor:
if detail_id:
await sor.U('discount_detail', {
'id': detail_id,
'discount': new_val
})
else:
await sor.C('discount_detail', {
'id': getID(),
'discountid': discountid,
'resellerid': user_orgid,
'prodtypeid': None,
'productid': productid,
'discount': new_val
})
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': f'折扣保存成功({new_val})', 'type': 'success', 'timeout': 3}}
except Exception as e:
debug(f'save_marketing_discount error: {format_exc()}')
result['options'] = {'title': 'Error', 'message': f'保存失败: {str(e)}', 'type': 'error', 'timeout': 5}
return result