From 0107eebdcb6937edac4a6cf669dfdbf9402665e5 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Sun, 12 Jul 2026 10:51:18 +0800 Subject: [PATCH] feat: discount setting UI for agreement/contract items (like discount module pattern) - agreement_discount_setting.ui: product list with UiFloat discount inputs - contract_discount_setting.ui: same for supply contracts - get_agreement_discount_items.dspy + Python function - get_contract_discount_items.dspy + Python function - save_agreement_discount.dspy: create/update/delete items on discount change - save_contract_discount.dspy: same for supply contracts - Updated distribution_agreements_list and supply_contracts_list subtables --- json/distribution_agreements_list.json | 2 +- json/supply_contracts_list.json | 2 +- supplychain/init.py | 33 +++++++++++ wwwroot/agreement_discount_setting.ui | 57 +++++++++++++++++++ wwwroot/api/get_agreement_discount_items.dspy | 34 +++++++++++ wwwroot/api/get_contract_discount_items.dspy | 27 +++++++++ wwwroot/api/save_agreement_discount.dspy | 51 +++++++++++++++++ wwwroot/api/save_contract_discount.dspy | 30 ++++++++++ wwwroot/contract_discount_setting.ui | 57 +++++++++++++++++++ wwwroot/distribution_agreements_list/index.ui | 2 +- wwwroot/supply_contracts_list/index.ui | 2 +- 11 files changed, 293 insertions(+), 4 deletions(-) create mode 100644 wwwroot/agreement_discount_setting.ui create mode 100644 wwwroot/api/get_agreement_discount_items.dspy create mode 100644 wwwroot/api/get_contract_discount_items.dspy create mode 100644 wwwroot/api/save_agreement_discount.dspy create mode 100644 wwwroot/api/save_contract_discount.dspy create mode 100644 wwwroot/contract_discount_setting.ui diff --git a/json/distribution_agreements_list.json b/json/distribution_agreements_list.json index 253a0ab..5af13b3 100644 --- a/json/distribution_agreements_list.json +++ b/json/distribution_agreements_list.json @@ -81,7 +81,7 @@ { "field": "agreement_id", "title": "产品分销折扣", - "url": "{{entire_url('/supplychain/distribution_agreement_items_list')}}", + "url": "{{entire_url('/supplychain/agreement_discount_setting.ui')}}", "subtable": "distribution_agreement_items" }, { diff --git a/json/supply_contracts_list.json b/json/supply_contracts_list.json index d6e4000..714ca54 100644 --- a/json/supply_contracts_list.json +++ b/json/supply_contracts_list.json @@ -86,7 +86,7 @@ { "field": "contract_id", "title": "产品折扣明细", - "url": "{{entire_url('/supplychain/supply_contract_items_list')}}", + "url": "{{entire_url('/supplychain/contract_discount_setting.ui')}}", "subtable": "supply_contract_items" }, { diff --git a/supplychain/init.py b/supplychain/init.py index 1e401d5..283b217 100644 --- a/supplychain/init.py +++ b/supplychain/init.py @@ -943,6 +943,36 @@ async def delete_product_supplier_mapping(request, params_kw): return json.dumps({"status": "ok", "message": "删除成功"}) +async def get_agreement_discount_items(request, params_kw): + """Return products with discount status for a distribution agreement.""" + agreement_id = params_kw.get('agreement_id') or params_kw.get('id') + if not agreement_id: + return [] + userorgid = getattr(ServerEnv(), 'orgid', None) or '0' + result = [] + async with DBPools().sqlorContext('sage') as sor: + rows = await sor.sqlExe( + """SELECT p.id as productid, p.product_name, p.product_code, p.price as sale_price, + pc.id as category_id, pc.name as category_name, + dai.id as item_id, dai.discount + FROM product p + LEFT JOIN product_category pc ON p.category_id = pc.id + LEFT JOIN supplychain.distribution_agreement_items dai ON dai.productid = p.id AND dai.agreement_id = ${aid}$ + WHERE p.status = '1' + AND (p.org_id = ${oid}$ OR p.id IN (SELECT product_id FROM product_org_auth WHERE org_id = ${oid}$)) + ORDER BY pc.sort_order, pc.name, p.sort_order, p.product_name""", + {'aid': agreement_id, 'oid': userorgid} + ) + for r in rows: + result.append({ + 'productid': r.productid, 'product_name': r.product_name, + 'product_code': r.product_code, 'sale_price': str(r.sale_price) if r.sale_price else '', + 'category_id': r.category_id or '', 'category_name': r.category_name or '', + 'item_id': r.item_id or '', 'discount': str(r.discount) if r.discount else '1.0', + }) + return result + + # ============================================================ # Register functions with ServerEnv # ============================================================ @@ -1008,6 +1038,9 @@ def load_supplychain(): env.create_platform_supply_products = create_platform_supply_products env.update_platform_supply_products = update_platform_supply_products env.delete_platform_supply_products = delete_platform_supply_products + # Agreement discount setting UI + env.get_agreement_discount_items = get_agreement_discount_items + env.get_contract_discount_items = get_contract_discount_items # P0: Product Supplier Mapping env.create_product_supplier_mapping = create_product_supplier_mapping env.update_product_supplier_mapping = update_product_supplier_mapping diff --git a/wwwroot/agreement_discount_setting.ui b/wwwroot/agreement_discount_setting.ui new file mode 100644 index 0000000..3d36d6f --- /dev/null +++ b/wwwroot/agreement_discount_setting.ui @@ -0,0 +1,57 @@ +{% set items = get_agreement_discount_items(request) %} +{% set agreement_id = params_kw.agreement_id or params_kw.id %} +{ + "widgettype": "VBox", + "options": {"width": "100%", "height": "100%", "css": "filler"}, + "subwidgets": [ + { + "widgettype": "HBox", + "options": {"bgcolor": "#0F172A", "padding": "8px 0", "borderRadius": "6px 6px 0 0", "alignItems": "center", "width": "100%"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "产品分类", "width": "22%", "padding": "0 8px", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8"}}, + {"widgettype": "Text", "options": {"text": "产品名称", "width": "28%", "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": "14%", "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 items %} + { + "widgettype": "HBox", + "options": {"padding": "6px 0", "border": "0 0 1px 0", "borderColor": "#334155", "alignItems": "center", "width": "100%"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": {{json.dumps(p.category_name or '')}}, "width": "22%", "padding": "0 8px", "fontSize": "12px", "color": "#E2E8F0"}}, + {"widgettype": "Text", "options": {"text": {{json.dumps(p.product_name or '')}}, "width": "28%", "padding": "0 8px", "fontSize": "12px", "color": "#F1F5F9"}}, + { + "widgettype": "UiFloat", + "options": {"width": "12%", "padding": "0 8px", "name": "discount", "value": {{json.dumps(p.discount or '1.0')}}, "dec_len": 2, "fontSize": "12px"}, + "binds": [ + {"wid": "self", "event": "blur", "actiontype": "urlwidget", "datawidget": "self", "target": "self", + "options": {"url": "{{entire_url('/supplychain/api/save_agreement_discount.dspy')}}", "params": { + "agreement_id": {{json.dumps(agreement_id)}}, + "productid": {{json.dumps(p.productid)}}, + "prodtypeid": {{json.dumps(p.category_id or '')}}, + "old_discount": {{json.dumps(p.discount or '1.0')}}, + "item_id": {{json.dumps(p.item_id or '')}} + }}} + ] + }, + {"widgettype": "Text", "options": {"text": {{json.dumps(p.sale_price or '')}}, "width": "14%", "padding": "0 8px", "fontSize": "12px", "color": "#E2E8F0"}}, + {"widgettype": "Text", "options": {"text": {% if p.item_id %}"✓已设置"{% else %}"-未设置"{% endif %}, "width": "20%", "padding": "0 8px", "fontSize": "11px", "color": {% if p.item_id %}"#34D399"{% else %}"#64748B"{% endif %}}} + ] + }{% if not loop.last %},{% endif %} +{% endfor %} + ] + } + ] + } + ] +} diff --git a/wwwroot/api/get_agreement_discount_items.dspy b/wwwroot/api/get_agreement_discount_items.dspy new file mode 100644 index 0000000..cad75ef --- /dev/null +++ b/wwwroot/api/get_agreement_discount_items.dspy @@ -0,0 +1,34 @@ +"""Return all products with their discount status in this distribution agreement.""" +agreement_id = params_kw.get('agreement_id') or params_kw.get('id') +if not agreement_id: + return [] + +userorgid = (await get_userorgid()) or '0' + +# Get products available to this org (own + authorized via product_org_auth) +async with DBPools().sqlorContext('sage') as sor: + rows = await sor.sqlExe( + """SELECT p.id as productid, p.product_name, p.product_code, p.price as sale_price, + pc.id as category_id, pc.name as category_name, + dai.id as item_id, dai.discount + FROM product p + LEFT JOIN product_category pc ON p.category_id = pc.id + LEFT JOIN distribution_agreement_items dai ON dai.productid = p.id AND dai.agreement_id = ${aid}$ + WHERE p.status = '1' + AND (p.org_id = ${oid}$ OR p.id IN (SELECT product_id FROM product_org_auth WHERE org_id = ${oid}$)) + ORDER BY pc.sort_order, pc.name, p.sort_order, p.product_name""", + {'aid': agreement_id, 'oid': userorgid} + ) + result = [] + for r in rows: + result.append({ + 'productid': r.productid, + 'product_name': r.product_name, + 'product_code': r.product_code, + 'sale_price': str(r.sale_price) if r.sale_price else '', + 'category_id': r.category_id or '', + 'category_name': r.category_name or '', + 'item_id': r.item_id or '', + 'discount': str(r.discount) if r.discount else '1.0', + }) + return result diff --git a/wwwroot/api/get_contract_discount_items.dspy b/wwwroot/api/get_contract_discount_items.dspy new file mode 100644 index 0000000..10ea741 --- /dev/null +++ b/wwwroot/api/get_contract_discount_items.dspy @@ -0,0 +1,27 @@ +"""Return products with discount status for a supply contract.""" +contract_id = params_kw.get('contract_id') or params_kw.get('id') +if not contract_id: + return [] +userorgid = (await get_userorgid()) or '0' + +async with DBPools().sqlorContext('sage') as sor: + rows = await sor.sqlExe( + """SELECT p.id as productid, p.product_name, p.product_code, p.price as sale_price, + pc.id as category_id, pc.name as category_name, + sci.id as item_id, sci.discount + FROM product p + LEFT JOIN product_category pc ON p.category_id = pc.id + LEFT JOIN supplychain.supply_contract_items sci ON sci.productid = p.id AND sci.contract_id = ${cid}$ + WHERE p.status = '1' AND p.org_id = ${oid}$ + ORDER BY pc.sort_order, pc.name, p.sort_order, p.product_name""", + {'cid': contract_id, 'oid': userorgid} + ) + result = [] + for r in rows: + result.append({ + 'productid': r.productid, 'product_name': r.product_name, + 'product_code': r.product_code, 'sale_price': str(r.sale_price) if r.sale_price else '', + 'category_id': r.category_id or '', 'category_name': r.category_name or '', + 'item_id': r.item_id or '', 'discount': str(r.discount) if r.discount else '1.0', + }) + return result diff --git a/wwwroot/api/save_agreement_discount.dspy b/wwwroot/api/save_agreement_discount.dspy new file mode 100644 index 0000000..21907b7 --- /dev/null +++ b/wwwroot/api/save_agreement_discount.dspy @@ -0,0 +1,51 @@ +"""Save or update a distribution agreement product discount.""" +agreement_id = params_kw.get('agreement_id') +productid = params_kw.get('productid') +prodtypeid = params_kw.get('prodtypeid') or '' +new_discount = params_kw.get('discount') or '1.0' +old_discount = params_kw.get('old_discount') or '1.0' +item_id = params_kw.get('item_id') + +userorgid = (await get_userorgid()) or '0' +dbname = get_module_dbname('supplychain') + +async with DBPools().sqlorContext(dbname) as sor: + if item_id: + # Existing item — update or delete + if new_discount == old_discount: + # No change, skip + pass + elif float(new_discount) <= 0: + # Delete if discount set to 0 + await sor.D('distribution_agreement_items', {'id': item_id}) + # Also remove product_org_auth + async with DBPools().sqlorContext('sage') as s2: + await s2.sqlExe("DELETE FROM product_org_auth WHERE auth_source_id = ${sid}$", {'sid': item_id}) + else: + await sor.U('distribution_agreement_items', { + 'id': item_id, 'discount': new_discount, 'updated_at': timestampstr() + }) + elif float(new_discount) > 0 and new_discount != old_discount: + # New item — create + new_id = getID() + now = timestampstr() + await sor.C('distribution_agreement_items', { + 'id': new_id, 'agreement_id': agreement_id, 'resellerid': userorgid, + 'productid': productid, 'prodtypeid': prodtypeid, + 'discount': new_discount, 'created_at': now, + }) + # Sync product_org_auth + async with DBPools().sqlorContext(dbname) as s2: + ag = await s2.sqlExe( + "SELECT sub_reseller_id FROM distribution_agreements WHERE id = ${aid}$", + {'aid': agreement_id} + ) + if ag: + async with DBPools().sqlorContext('sage') as s3: + await s3.C('product_org_auth', { + 'id': getID(), 'product_id': productid, + 'org_id': ag[0].sub_reseller_id, + 'auth_source': 'distribution_agreement', 'auth_source_id': new_id, + }) + +return {'status': 'ok'} diff --git a/wwwroot/api/save_contract_discount.dspy b/wwwroot/api/save_contract_discount.dspy new file mode 100644 index 0000000..e9260b1 --- /dev/null +++ b/wwwroot/api/save_contract_discount.dspy @@ -0,0 +1,30 @@ +"""Save or update a supply contract product discount.""" +contract_id = params_kw.get('contract_id') +productid = params_kw.get('productid') +prodtypeid = params_kw.get('prodtypeid') or '' +new_discount = params_kw.get('discount') or '1.0' +old_discount = params_kw.get('old_discount') or '1.0' +item_id = params_kw.get('item_id') + +userorgid = (await get_userorgid()) or '0' +dbname = get_module_dbname('supplychain') + +async with DBPools().sqlorContext(dbname) as sor: + if item_id: + if new_discount == old_discount: + pass + elif float(new_discount) <= 0: + await sor.D('supply_contract_items', {'id': item_id}) + else: + await sor.U('supply_contract_items', { + 'id': item_id, 'discount': new_discount, 'updated_at': timestampstr() + }) + elif float(new_discount) > 0 and new_discount != old_discount: + now = timestampstr() + await sor.C('supply_contract_items', { + 'id': getID(), 'contract_id': contract_id, 'resellerid': userorgid, + 'productid': productid, 'prodtypeid': prodtypeid, + 'discount': new_discount, 'created_at': now, + }) + +return {'status': 'ok'} diff --git a/wwwroot/contract_discount_setting.ui b/wwwroot/contract_discount_setting.ui new file mode 100644 index 0000000..904832b --- /dev/null +++ b/wwwroot/contract_discount_setting.ui @@ -0,0 +1,57 @@ +{% set items = get_contract_discount_items(request) %} +{% set contract_id = params_kw.contract_id or params_kw.id %} +{ + "widgettype": "VBox", + "options": {"width": "100%", "height": "100%", "css": "filler"}, + "subwidgets": [ + { + "widgettype": "HBox", + "options": {"bgcolor": "#0F172A", "padding": "8px 0", "borderRadius": "6px 6px 0 0", "alignItems": "center", "width": "100%"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": "产品分类", "width": "22%", "padding": "0 8px", "fontSize": "12px", "fontWeight": "600", "color": "#94A3B8"}}, + {"widgettype": "Text", "options": {"text": "产品名称", "width": "28%", "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": "14%", "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 items %} + { + "widgettype": "HBox", + "options": {"padding": "6px 0", "border": "0 0 1px 0", "borderColor": "#334155", "alignItems": "center", "width": "100%"}, + "subwidgets": [ + {"widgettype": "Text", "options": {"text": {{json.dumps(p.category_name or '')}}, "width": "22%", "padding": "0 8px", "fontSize": "12px", "color": "#E2E8F0"}}, + {"widgettype": "Text", "options": {"text": {{json.dumps(p.product_name or '')}}, "width": "28%", "padding": "0 8px", "fontSize": "12px", "color": "#F1F5F9"}}, + { + "widgettype": "UiFloat", + "options": {"width": "12%", "padding": "0 8px", "name": "discount", "value": {{json.dumps(p.discount or '1.0')}}, "dec_len": 2, "fontSize": "12px"}, + "binds": [ + {"wid": "self", "event": "blur", "actiontype": "urlwidget", "datawidget": "self", "target": "self", + "options": {"url": "{{entire_url('/supplychain/api/save_contract_discount.dspy')}}", "params": { + "contract_id": {{json.dumps(contract_id)}}, + "productid": {{json.dumps(p.productid)}}, + "prodtypeid": {{json.dumps(p.category_id or '')}}, + "old_discount": {{json.dumps(p.discount or '1.0')}}, + "item_id": {{json.dumps(p.item_id or '')}} + }}} + ] + }, + {"widgettype": "Text", "options": {"text": {{json.dumps(p.sale_price or '')}}, "width": "14%", "padding": "0 8px", "fontSize": "12px", "color": "#E2E8F0"}}, + {"widgettype": "Text", "options": {"text": {% if p.item_id %}"✓已设置"{% else %}"-未设置"{% endif %}, "width": "20%", "padding": "0 8px", "fontSize": "11px", "color": {% if p.item_id %}"#34D399"{% else %}"#64748B"{% endif %}}} + ] + }{% if not loop.last %},{% endif %} +{% endfor %} + ] + } + ] + } + ] +} diff --git a/wwwroot/distribution_agreements_list/index.ui b/wwwroot/distribution_agreements_list/index.ui index 15882f1..c900917 100644 --- a/wwwroot/distribution_agreements_list/index.ui +++ b/wwwroot/distribution_agreements_list/index.ui @@ -338,7 +338,7 @@ "options": { "method": "POST", "params": {}, - "url": "{{entire_url('/supplychain/distribution_agreement_items_list')}}" + "url": "{{entire_url('/supplychain/agreement_discount_setting.ui')}}" } } ] diff --git a/wwwroot/supply_contracts_list/index.ui b/wwwroot/supply_contracts_list/index.ui index 88499a9..eeeb872 100644 --- a/wwwroot/supply_contracts_list/index.ui +++ b/wwwroot/supply_contracts_list/index.ui @@ -338,7 +338,7 @@ "options": { "method": "POST", "params": {}, - "url": "{{entire_url('/supplychain/supply_contract_items_list')}}" + "url": "{{entire_url('/supplychain/contract_discount_setting.ui')}}" } } ]