diff --git a/README.md b/README.md index 6eee838..f473449 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,61 @@ # discount ## 功能 -支持商户为客户设置折扣,一旦客户设置折扣,商户的所有商品此客户均享受商户为其设置的折扣 +支持商户为客户设置折扣,可以为不同产品设置不同的折扣值。 -## +## 数据模型 +### discount (折扣表) +主折扣记录表,定义商户-客户关系的折扣。 +- id: 折扣id +- name: 折扣名称 +- resellerid: 商户id +- customerid: 客户id (可为NULL表示商户级默认折扣) +- enabled_date: 启用日期 +- expired_date: 失效日期 + +### discount_detail (折扣产品明细表) +存储每个产品的具体折扣值。 +- id: 明细id +- discountid: 关联折扣id +- resellerid: 商户id +- prodtypeid: 产品类型id (可为NULL表示所有类型) +- productid: 产品id (可为NULL表示该类型下所有产品) +- discount: 折扣值 (0 < discount < 1) + +### discount_qr (折扣二维码表) +促销二维码记录。 +- id: 二维码id +- resellerid: 商户id +- discount: 折扣值 +- valid_term: 有效期 (如 30D, 3M, 1Y) +- expired_date: 促销失效日期 +- qr_webpath: 二维码路径 + +## 折扣查询 + +### 获取产品折扣 +```python +from discount.init import get_product_discount + +discount = await get_product_discount(resellerid, customerid, prodtypeid, productid) +# 返回折扣值 (float),1.0表示无折扣 +``` + +查询优先级: +1. 精确匹配:产品类型 + 产品id +2. 类型级别匹配:产品类型 (productid为NULL) +3. 默认匹配:所有产品 (prodtypeid和productid均为NULL) + +### 获取客户默认折扣 (兼容旧接口) +```python +from discount.init import get_customer_discount + +discount = await get_customer_discount(resellerid, customerid) +# 返回客户默认折扣值 +``` + +## 维护界面 +- 折扣管理: discount_list.ui - 创建/编辑/删除折扣记录 +- 折扣产品明细: discount_detail_list.ui?discountid=xxx - 为指定折扣添加产品明细,设置各产品折扣值 +- 生成促销码: promote.ui - 生成促销二维码 diff --git a/discount/__init__.py b/discount/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/discount/__init__.py @@ -0,0 +1 @@ + diff --git a/discount/init.py b/discount/init.py index 4f6cd8e..96f8309 100644 --- a/discount/init.py +++ b/discount/init.py @@ -1,144 +1,340 @@ +from datetime import datetime, timedelta from appPublic.qr import gen_qr_withlogo from appPublic.uniqueID import getID from appPublic.jsonConfig import getConfig from appPublic.dictObject import DictObject -from sqlor.dbpools import get_sor_context +from sqlor.dbpools import DBPools from ahserver.serverenv import ServerEnv from ahserver.filestorage import FileStorage -async def discount_qrcode(request, params_kw): - """ - discount less then 1 and greate then 0 - valid_term is digit + one of ["D", "M", "Y"] - expired_date is a date after it the promote qrcode invalidable - """ - discount = params_kw.discount - valid_term = params_kw.valid_term - expired_date = params_kw.expired_date - env = request._run_ns - resellerid = await env.get_userorgid() - id = getID() - url = env.entire_url('./promote') + f'?id={id}' - fs = FileStorage() - p = fs._name2path(f'{getID()}.png') - config = getConfig() - gen_qr_withlogo(url, p, logopath=config.logopath, logoloc='cc') - webp = fs.webpath(p) - async with get_sor_context(env, 'discount') as sor: - biz_date = await env.get_business_date(sor) - if biz_date >= expired_date: - raise Exception('Promote QRCODE is out of time') - ret = { - 'id': id, - 'resellerid': resellerid, - 'discount': discount, - 'valid_term': valid_term, - 'expired_date': expired_date, - 'qr_webpath': webp - } - await sor.C('discount_qr', ret.copy()) - return DictObject(**ret) - return None +async def discount_qrcode(request, params_kw): + """ + Generate a promotional QR code for discount + discount less then 1 and greate then 0 + valid_term is digit + one of ["D", "M", "Y"] + expired_date is a date after it the promote qrcode invalidable + """ + discount = params_kw.discount + valid_term = params_kw.valid_term + expired_date = params_kw.expired_date + env = ServerEnv() + dbname = env.get_module_dbname('discount') + config = getConfig() + db = DBPools() + db.databases = config.databases + resellerid = await env.get_userorgid() + qr_id = getID() + url = env.entire_url('./promote') + f'?id={qr_id}' + fs = FileStorage() + p = fs._name2path(f'{getID()}.png') + gen_qr_withlogo(url, p, logopath=config.logopath, logoloc='cc') + webp = fs.webpath(p) + async with db.sqlorContext(dbname) as sor: + biz_date = await env.get_business_date(sor) + if biz_date >= expired_date: + raise Exception('Promote QRCODE is out of time') + + ret = { + 'id': qr_id, + 'resellerid': resellerid, + 'discount': discount, + 'valid_term': valid_term, + 'expired_date': expired_date, + 'qr_webpath': webp + } + await sor.C('discount_qr', ret.copy()) + return DictObject(**ret) + return None + async def set_promote_discount(request, params_kw): - env = request._run_ns - id = params_kw.id - customerid = await env.get_userorgid() - async with get_sor_context(env, 'discount') as sor: - recs = await sor.R('discount_qr', {'id': id}) - if not recs: - raise Exception(f'promote id({id}) not exists') - biz_date = await env.get_business_date(sor) - if recs[0].expired_date <= biz_date: - raise Exception('Promote QRCODE is out of time') - cnt = int(recs[0].valid_term[:-1]) - unit = recs[0].valid_term[-1] - enabled_date = biz_date - expired_date = '' - if unit == 'D': - expired_date = env.strdate_add(enabled_date, days=cnt) - elif unit == 'M': - expired_date = env.strdate_add(enabled_date, months=cnt) - elif unit == 'Y': - expired_date = env.strdate_add(enabled_date, years=cnt) - else: - raise Exception(f'Invalid valid_term({recs[0].valid_term})') - need_new_discount = await disable_old_discount(sor, recs[0].resellerid, - customerid, biz_date, recs[0].discount) - if not need_new_discount: - return await sor_get_customer_discount(sor, recs[0].resellerid, customerid) - ret = { - 'id': getID(), + env = ServerEnv() + dbname = env.get_module_dbname('discount') + config = getConfig() + db = DBPools() + db.databases = config.databases + id = params_kw.id + customerid = await env.get_userorgid() + async with db.sqlorContext(dbname) as sor: + recs = await sor.R('discount_qr', {'id': id}) + if not recs: + raise Exception(f'promote id({id}) not exists') + biz_date = await env.get_business_date(sor) + if recs[0].expired_date <= biz_date: + raise Exception('Promote QRCODE is out of time') + cnt = int(recs[0].valid_term[:-1]) + unit = recs[0].valid_term[-1] + enabled_date = biz_date + expired_date = '' + if unit == 'D': + expired_date = env.strdate_add(enabled_date, days=cnt) + elif unit == 'M': + expired_date = env.strdate_add(enabled_date, months=cnt) + elif unit == 'Y': + expired_date = env.strdate_add(enabled_date, years=cnt) + else: + raise Exception(f'Invalid valid_term({recs[0].valid_term})') + + # Disable old active discount for this customer + await disable_old_discount(sor, recs[0].resellerid, customerid, biz_date) + + # Create new discount record (no longer stores discount value directly) + discountid = getID() + ret = { + 'id': discountid, + 'name': f'促销折扣-{biz_date}', 'resellerid': recs[0].resellerid, 'customerid': customerid, - 'discount': recs[0].discount, 'enabled_date': enabled_date, 'expired_date': expired_date } - await sor.C('discount', ret.copy()) - return recs[0].discount - return None - -async def disable_old_discount(sor, resellerid, customerid, biz_date, new_discount): - sql = """select * from discount -where resellerid = ${resellerid}$ - and customerid=${customerid}$ - and enabled_date <= ${biz_date}$ - and expired_date > ${biz_date}$ for update""" - recs = await sor.sqlExe(sql, {'resellerid': resellerid, 'customerid': customerid, 'biz_date': biz_date}) - if not recs: - return True - if new_discount > recs[0].discount: - return False - await sor.U('discount', {'id': recs[0].id, 'expired_date': biz_date}) - return True + await sor.C('discount', ret.copy()) + + # Create discount_detail record with the discount value from QR code + # prodtypeid=None, productid=None means applies to all products + detail_ret = { + 'id': getID(), + 'discountid': discountid, + 'resellerid': recs[0].resellerid, + 'prodtypeid': None, + 'productid': None, + 'discount': recs[0].discount, + } + await sor.C('discount_detail', detail_ret.copy()) + + return recs[0].discount + return None + + +async def disable_old_discount(sor, resellerid, customerid, biz_date): + """Disable any active discount record for the given reseller+customer pair.""" + # Use sor.R with sort instead of raw SQL + FOR UPDATE (DB-agnostic) + recs = await sor.R('discount', { + 'resellerid': resellerid, + 'customerid': customerid, + 'sort': 'enabled_date desc' + }) + if not recs: + return + # Find the active one in Python (DB-agnostic date comparison) + for rec in recs: + if rec.get('enabled_date', '') <= biz_date and rec.get('expired_date', '') > biz_date: + await sor.U('discount', {'id': rec['id'], 'expired_date': biz_date}) + return + async def sor_get_star_discount(sor, resellerid, biz_date): - env = ServerEnv() - sql = """select * from discount -where resellerid = ${resellerid}$ - and enabled_date <= ${biz_date}$ - and expired_date > ${biz_date}$""" - ns = { - "resellerid": resellerid, - "biz_date": biz_date - } - recs = await sor.sqlExe(sql, ns) - if not recs: - return 1 - - return recs[0].discount + """Get default discount for a reseller (no specific customer).""" + sql = """select d.id from discount d +where d.resellerid = ${resellerid}$ + and d.customerid is NULL + and d.enabled_date <= ${biz_date}$ + and d.expired_date > ${biz_date}$""" + ns = { + "resellerid": resellerid, + "biz_date": biz_date + } + recs = await sor.sqlExe(sql, ns) + if not recs: + return 1 + + # Look up default discount detail (prodtypeid=None, productid=None) + sql2 = """select discount from discount_detail +where discountid = ${discountid}$ + and prodtypeid is NULL + and productid is NULL""" + recs2 = await sor.sqlExe(sql2, {'discountid': recs[0].id}) + if not recs2: + return 1 + return recs2[0].discount async def sor_get_customer_discount(sor, resellerid, customerid): - env = ServerEnv() - biz_date = await env.get_business_date(sor) - sql = """select * from discount + """Get discount record for a customer (legacy, returns record not value). + Use sor_get_product_discount for product-specific discount.""" + env = ServerEnv() + biz_date = await env.get_business_date(sor) + sql = """select * from discount where resellerid = ${resellerid}$ - and customerid = ${customerid}$ - and enabled_date <= ${biz_date}$ - and expired_date > ${biz_date}$""" - ns = { - "resellerid": resellerid, - "customerid": customerid, - "biz_date": biz_date - } - recs = await sor.sqlExe(sql, ns) - if not recs: - return await sor_get_star_discount(sor, resellerid, biz_date) - - return recs[0].discount + and customerid = ${customerid}$ + and enabled_date <= ${biz_date}$ + and expired_date > ${biz_date}$""" + ns = { + "resellerid": resellerid, + "customerid": customerid, + "biz_date": biz_date + } + recs = await sor.sqlExe(sql, ns) + if not recs: + return await sor_get_star_discount(sor, resellerid, biz_date) + + # Return default discount for this customer (prodtypeid=None, productid=None) + sql2 = """select discount from discount_detail +where discountid = ${discountid}$ + and prodtypeid is NULL + and productid is NULL""" + recs2 = await sor.sqlExe(sql2, {'discountid': recs[0].id}) + if not recs2: + return 1 + return recs2[0].discount + + +async def sor_get_product_discount(sor, resellerid, customerid, prodtypeid, productid): + """ + Get product-specific discount. + Lookup priority: + 1. Exact match: discountid -> (prodtypeid, productid) + 2. Type-level match: discountid -> (prodtypeid, NULL) + 3. Default match: discountid -> (NULL, NULL) + + Returns discount value (float), default 1.0 (no discount). + """ + env = ServerEnv() + biz_date = await env.get_business_date(sor) + + # Step 1: Find active discount record for reseller+customer + sql = """select id from discount +where resellerid = ${resellerid}$ + and customerid = ${customerid}$ + and enabled_date <= ${biz_date}$ + and expired_date > ${biz_date}$""" + ns = { + "resellerid": resellerid, + "customerid": customerid, + "biz_date": biz_date + } + recs = await sor.sqlExe(sql, ns) + if not recs: + # Try reseller-level default (customerid is NULL) + sql = """select id from discount +where resellerid = ${resellerid}$ + and customerid is NULL + and enabled_date <= ${biz_date}$ + and expired_date > ${biz_date}$""" + recs = await sor.sqlExe(sql, ns) + if not recs: + return 1.0 + + discountid = recs[0].id + + # Step 2: Try exact product match + sql2 = """select discount from discount_detail +where discountid = ${discountid}$ + and prodtypeid = ${prodtypeid}$ + and productid = ${productid}$""" + recs2 = await sor.sqlExe(sql2, { + 'discountid': discountid, + 'prodtypeid': prodtypeid, + 'productid': productid + }) + if recs2: + return recs2[0].discount + + # Step 3: Try product type level match (productid is NULL) + sql3 = """select discount from discount_detail +where discountid = ${discountid}$ + and prodtypeid = ${prodtypeid}$ + and productid is NULL""" + recs3 = await sor.sqlExe(sql3, { + 'discountid': discountid, + 'prodtypeid': prodtypeid, + }) + if recs3: + return recs3[0].discount + + # Step 4: Try default match (both NULL) + sql4 = """select discount from discount_detail +where discountid = ${discountid}$ + and prodtypeid is NULL + and productid is NULL""" + recs4 = await sor.sqlExe(sql4, {'discountid': discountid}) + if recs4: + return recs4[0].discount + + return 1.0 + async def get_customer_discount(resellerid, customerid): - env = ServerEnv() - async with get_sor_context(env, 'discount') as sor: - return await sor_get_customer_discount(sor, resellerid, customerid) - return 1 + """Legacy: get default discount for a customer (all products).""" + env = ServerEnv() + dbname = env.get_module_dbname('discount') + config = getConfig() + db = DBPools() + db.databases = config.databases + async with db.sqlorContext(dbname) as sor: + return await sor_get_customer_discount(sor, resellerid, customerid) + return 1 + + +async def get_product_discount(resellerid, customerid, prodtypeid, productid): + """ + Get product-specific discount. + Parameters: + resellerid: merchant ID + customerid: customer ID + prodtypeid: product type ID + productid: product ID + Returns: discount value (float), 1.0 means no discount. + """ + env = ServerEnv() + dbname = env.get_module_dbname('discount') + config = getConfig() + db = DBPools() + db.databases = config.databases + async with db.sqlorContext(dbname) as sor: + return await sor_get_product_discount(sor, resellerid, customerid, prodtypeid, productid) + return 1.0 + + +async def get_discount_details(sor, discountid): + """Get all product detail records for a given discount.""" + sql = """select * from discount_detail +where discountid = ${discountid}$ +order by prodtypeid, productid""" + recs = await sor.sqlExe(sql, {'discountid': discountid}) + return recs + + +async def add_discount_detail(sor, discountid, resellerid, prodtypeid, productid, discount): + """Add a product-specific discount detail.""" + if discount <= 0 or discount >= 1: + raise Exception(f'discount({discount}) invalid, must be between 0 and 1') + + ret = { + 'id': getID(), + 'discountid': discountid, + 'resellerid': resellerid, + 'prodtypeid': prodtypeid if prodtypeid else None, + 'productid': productid if productid else None, + 'discount': discount, + } + await sor.C('discount_detail', ret.copy()) + return ret + + +async def update_discount_detail(sor, detail_id, discount): + """Update a discount detail record.""" + if discount <= 0 or discount >= 1: + raise Exception(f'discount({discount}) invalid, must be between 0 and 1') + + await sor.U('discount_detail', {'id': detail_id, 'discount': discount}) + + +async def delete_discount_detail(sor, detail_id): + """Delete a discount detail record.""" + await sor.D('discount_detail', {'id': detail_id}) + def load_discount(): - env = ServerEnv() - env.get_customer_discount = get_customer_discount - env.sor_get_customer_discount = sor_get_customer_discount - env.discount_qrcode = discount_qrcode - env.set_promote_discount = set_promote_discount + env = ServerEnv() + env.get_customer_discount = get_customer_discount + env.sor_get_customer_discount = sor_get_customer_discount + env.get_product_discount = get_product_discount + env.sor_get_product_discount = sor_get_product_discount + env.discount_qrcode = discount_qrcode + env.set_promote_discount = set_promote_discount + env.get_discount_details = get_discount_details + env.add_discount_detail = add_discount_detail + env.update_discount_detail = update_discount_detail + env.delete_discount_detail = delete_discount_detail diff --git a/json/discount_detail_list.json b/json/discount_detail_list.json new file mode 100644 index 0000000..75beead --- /dev/null +++ b/json/discount_detail_list.json @@ -0,0 +1,52 @@ +{ + "tblname": "discount_detail", + "alias": "discount_detail_list", + "title": "折扣产品明细", + "params": { + "sortby": ["prodtypeid", "productid"], + "browserfields": { + "exclouded": ["id", "resellerid"], + "alters": { + "discountid": { + "uitype": "code", + "dataurl": "/get_code.dspy", + "datamethod": "GET", + "dataparams": { + "dbname": "sage", + "table": "discount", + "tblvalue": "id", + "tbltext": "name" + } + }, + "prodtypeid": { + "uitype": "code", + "dataurl": "/get_code.dspy", + "datamethod": "GET", + "dataparams": { + "dbname": "sage", + "table": "prodtype", + "tblvalue": "id", + "tbltext": "name" + } + }, + "productid": { + "uitype": "code", + "dataurl": "/get_code.dspy", + "datamethod": "GET", + "dataparams": { + "dbname": "sage", + "table": "product", + "tblvalue": "id", + "tbltext": "name" + } + } + } + }, + "editexclouded": ["id", "discountid", "resellerid"], + "editable": { + "new_data_url": "{{entire_url('../api/discount_detail_create.dspy')}}", + "update_data_url": "{{entire_url('../api/discount_detail_update.dspy')}}", + "delete_data_url": "{{entire_url('../api/discount_detail_delete.dspy')}}" + } + } +} diff --git a/json/discount_list.json b/json/discount_list.json new file mode 100644 index 0000000..8f6ecd5 --- /dev/null +++ b/json/discount_list.json @@ -0,0 +1,41 @@ +{ + "tblname": "discount", + "alias": "discount_list", + "title": "折扣管理", + "params": { + "sortby": ["enabled_date desc"], + "browserfields": { + "exclouded": ["id"], + "alters": { + "resellerid": { + "uitype": "code", + "dataurl": "/get_code.dspy", + "datamethod": "GET", + "dataparams": { + "dbname": "sage", + "table": "organization", + "tblvalue": "id", + "tbltext": "orgname" + } + }, + "customerid": { + "uitype": "code", + "dataurl": "/get_code.dspy", + "datamethod": "GET", + "dataparams": { + "dbname": "sage", + "table": "organization", + "tblvalue": "id", + "tbltext": "orgname" + } + } + } + }, + "editexclouded": ["id"], + "editable": { + "new_data_url": "{{entire_url('../api/discount_create.dspy')}}", + "update_data_url": "{{entire_url('../api/discount_update.dspy')}}", + "delete_data_url": "{{entire_url('../api/discount_delete.dspy')}}" + } + } +} diff --git a/models/discount.xlsx b/models/discount.xlsx index 38aaca9..769b715 100644 Binary files a/models/discount.xlsx and b/models/discount.xlsx differ diff --git a/models/discount_detail.xlsx b/models/discount_detail.xlsx new file mode 100644 index 0000000..4bb6b19 Binary files /dev/null and b/models/discount_detail.xlsx differ diff --git a/pyproject.toml b/pyproject.toml index 95efe10..ebd79be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,9 +12,8 @@ authors = [ readme = "README.md" requires-python = ">=3.10" dependencies = [ - "apppublic", - "sqlor", - "ahserver" + "sqlor", + "bricks_for_python" ] [tool.setuptools] diff --git a/wwwroot/generate_qr.dspy b/wwwroot/generate_qr.dspy index 6e1b647..e64760d 100644 --- a/wwwroot/generate_qr.dspy +++ b/wwwroot/generate_qr.dspy @@ -1,58 +1,63 @@ +from ahserver.serverenv import ServerEnv + params_kw.discount = float(params_kw.discount) if params_kw.discount <= 0 or params_kw.discount >= 1: - e = Exception(f'discount({params_kw.discount}) invalid') - exception(f'{e}') - raise e + e = Exception(f'discount({params_kw.discount}) invalid') + exception(f'{e}') + raise e if params_kw.valid_term[-1] not in ['D', 'M', 'Y']: - e = Exception(f'valid_term must ends with "D", "M" or "Y"') - exception(f'{e}') - raise e + e = Exception(f'valid_term must ends with "D", "M" or "Y"') + exception(f'{e}') + raise e cnt = int(params_kw.valid_term[:-1]) if cnt < 0: - e = Exception(f'valid_term({params_kw.valid_term}) invalid') - exception(f'{e}') - raise e + e = Exception(f'valid_term({params_kw.valid_term}) invalid') + exception(f'{e}') + raise e if not params_kw.expired_date: - params_kw.expired_date = '9999-12-31' + params_kw.expired_date = '9999-12-31' x = await discount_qrcode(request, params_kw) -return { - "widgettype": "VBox", - "options": { - "width": "100%", - "height": "100%" - }, - "subwidgets": [ - { - "widgettype": "Image", - "options": { - "width": "340px", - "height": "340px", - "url": entire_url('/idfile') + f'?path={x.qr_webpath}' - } - }, { - "widgettype": "HBox", - "options": { - "width": "100%", - "cheight": 2 - }, - "subwidgets": [ - { - "widgettype": "Text", - "options": { - "otext": "折扣:", - "i18n": True, - "cwidth": 3 - } - },{ - "widgettype": "Text", - "options":{ - "text": x.discount - } - } - ] - } - ] -} +env = ServerEnv() +qr_url = env.entire_url('/idfile') + f'?path={x.qr_webpath}' + +return { + "widgettype": "VBox", + "options": { + "width": "100%", + "height": "100%" + }, + "subwidgets": [ + { + "widgettype": "Image", + "options": { + "width": "340px", + "height": "340px", + "url": qr_url + } + }, { + "widgettype": "HBox", + "options": { + "width": "100%", + "cheight": 2 + }, + "subwidgets": [ + { + "widgettype": "Text", + "options": { + "otext": "折扣:", + "i18n": True, + "cwidth": 3 + } + },{ + "widgettype": "Text", + "options":{ + "text": x.discount + } + } + ] + } + ] +} diff --git a/wwwroot/menu.ui b/wwwroot/menu.ui index 39ec706..548c069 100644 --- a/wwwroot/menu.ui +++ b/wwwroot/menu.ui @@ -5,11 +5,21 @@ "target":"root.page_center", "cwidth":10, "items":[ - { - "name":"promotecode", - "label": "生成促销码", - "url": "{{entire_url('promote.ui')}}", - } - ] - } + { + "name":"discount_list", + "label": "折扣管理", + "url": "{{entire_url('discount_list.ui')}}" + }, + { + "name":"discount_detail_list", + "label": "折扣产品明细", + "url": "{{entire_url('discount_detail_list.ui')}}" + }, + { + "name":"promotecode", + "label": "生成促销码", + "url": "{{entire_url('promote.ui')}}" + } + ] + } } diff --git a/wwwroot/promote.ui b/wwwroot/promote.ui index 6d91df8..7c2a0e8 100644 --- a/wwwroot/promote.ui +++ b/wwwroot/promote.ui @@ -3,6 +3,8 @@ "options": { "width": "100%", "height": "100%", + "url": "{{entire_url('generate_qr.dspy')}}", + "method": "POST", "fields": [ { "name": "valid_term", @@ -16,7 +18,7 @@ "tip": "促销二维码需在此日期前使用" },{ "name": "discount", - "lable": "折扣", + "label": "折扣", "uitype": "float", "tip": "折扣>0, < 1, 最终价格=产品价格*折扣, 折扣越小,折扣力度越大" } @@ -25,13 +27,10 @@ "binds":[ { "wid": "self", - "event": "submit", - "actiontype": "urlwidget", + "event": "submited", + "actiontype": "script", "target": "self", - "options": { - "url": "{{entire_url('generate_qr.dspy')}}", - "params": {} - } + "script": "await bricks.show_resp_message_or_error(event.params)" } ] }