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:
parent
ae6c547aff
commit
33cd1d287d
@ -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 {}
|
||||
|
||||
@ -50,7 +50,7 @@
|
||||
"need_other": false
|
||||
},
|
||||
"options": {
|
||||
"url": "{{entire_url('../discount_setting')}}"
|
||||
"url": "{{entire_url('../marketing_discount_setting')}}"
|
||||
}
|
||||
}
|
||||
],
|
||||
@ -61,4 +61,4 @@
|
||||
},
|
||||
"logined_userorgid": "resellerid"
|
||||
}
|
||||
}
|
||||
}
|
||||
49
wwwroot/marketing_discount_setting/get_products.dspy
Normal file
49
wwwroot/marketing_discount_setting/get_products.dspy
Normal 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
|
||||
82
wwwroot/marketing_discount_setting/index.ui
Normal file
82
wwwroot/marketing_discount_setting/index.ui
Normal 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 %}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
77
wwwroot/marketing_discount_setting/save_discount.dspy
Normal file
77
wwwroot/marketing_discount_setting/save_discount.dspy
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user