diff --git a/discount/init.py b/discount/init.py index 4904d67..078531c 100644 --- a/discount/init.py +++ b/discount/init.py @@ -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 {} diff --git a/json/discount_marketing_list.json b/json/discount_marketing_list.json index 009928c..ce382ef 100644 --- a/json/discount_marketing_list.json +++ b/json/discount_marketing_list.json @@ -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" } -} +} \ No newline at end of file diff --git a/wwwroot/marketing_discount_setting/get_products.dspy b/wwwroot/marketing_discount_setting/get_products.dspy new file mode 100644 index 0000000..5cd2d8a --- /dev/null +++ b/wwwroot/marketing_discount_setting/get_products.dspy @@ -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 diff --git a/wwwroot/marketing_discount_setting/index.ui b/wwwroot/marketing_discount_setting/index.ui new file mode 100644 index 0000000..44a7d50 --- /dev/null +++ b/wwwroot/marketing_discount_setting/index.ui @@ -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 %} + ] + } + ] + } + ] +} diff --git a/wwwroot/marketing_discount_setting/save_discount.dspy b/wwwroot/marketing_discount_setting/save_discount.dspy new file mode 100644 index 0000000..07eb0ea --- /dev/null +++ b/wwwroot/marketing_discount_setting/save_discount.dspy @@ -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