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
This commit is contained in:
parent
85ae9802a2
commit
0107eebdcb
@ -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"
|
||||
},
|
||||
{
|
||||
|
||||
@ -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"
|
||||
},
|
||||
{
|
||||
|
||||
@ -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
|
||||
|
||||
57
wwwroot/agreement_discount_setting.ui
Normal file
57
wwwroot/agreement_discount_setting.ui
Normal file
@ -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 %}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
34
wwwroot/api/get_agreement_discount_items.dspy
Normal file
34
wwwroot/api/get_agreement_discount_items.dspy
Normal file
@ -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
|
||||
27
wwwroot/api/get_contract_discount_items.dspy
Normal file
27
wwwroot/api/get_contract_discount_items.dspy
Normal file
@ -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
|
||||
51
wwwroot/api/save_agreement_discount.dspy
Normal file
51
wwwroot/api/save_agreement_discount.dspy
Normal file
@ -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'}
|
||||
30
wwwroot/api/save_contract_discount.dspy
Normal file
30
wwwroot/api/save_contract_discount.dspy
Normal file
@ -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'}
|
||||
57
wwwroot/contract_discount_setting.ui
Normal file
57
wwwroot/contract_discount_setting.ui
Normal file
@ -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 %}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -338,7 +338,7 @@
|
||||
"options": {
|
||||
"method": "POST",
|
||||
"params": {},
|
||||
"url": "{{entire_url('/supplychain/distribution_agreement_items_list')}}"
|
||||
"url": "{{entire_url('/supplychain/agreement_discount_setting.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')}}"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user