# recharge_reverse_list.dspy — 充值错帐处理:充值账单列表(2026-09-08) # 归属过滤:reseller 财务角色只看自己名下客户;owner 财务角色看全部。 # 每条账单一个「冲正」按钮(conform 确认),已冲正的显示标记禁用。 # 返回 widget 描述(VBox 卡片列表),由 recharge_reverse.ui 的 rev_list 容器加载。 dbname = get_module_dbname('accounting') uid = await get_user() if not uid: return {"widgettype": "Text", "options": {"text": "请先登录", "color": "#EF4444"}} userorgid = await get_userorgid() or '' db = DBPools() async with db.sqlorContext(dbname) as sor: # 操作者是否 owner 财务角色(不受归属限制) roles_recs = await sor.sqlExe( "SELECT r.orgtypeid FROM userrole ur JOIN role r ON ur.roleid=r.id WHERE ur.userid=${u}$", {'u': uid}) await sor.sqlExe("COMMIT", {}) is_owner = any((getattr(r, 'orgtypeid', '') or '') == 'owner' for r in (roles_recs or [])) # 充值账单(RECHARGE + 冲正 RECHARGE_REVERSE 一起查,冲正行用于判「已冲正」) if is_owner: bills = await sor.sqlExe( "SELECT b.id, b.customerid, b.orderid, b.business_op, b.amount, b.bill_date, " "COALESCE(o.orgname,'') AS orgname FROM bill b " "LEFT JOIN organization o ON o.id=b.customerid " "WHERE b.business_op IN ('RECHARGE','RECHARGE_REVERSE') " "ORDER BY b.bill_date DESC, b.bill_timestamp DESC LIMIT 100", {}) else: # reseller:只看自己名下客户(organization.parentid 或 discount_customer_bind) bills = await sor.sqlExe( "SELECT b.id, b.customerid, b.orderid, b.business_op, b.amount, b.bill_date, " "COALESCE(o.orgname,'') AS orgname FROM bill b " "LEFT JOIN organization o ON o.id=b.customerid " "WHERE b.business_op IN ('RECHARGE','RECHARGE_REVERSE') " "AND (o.parentid=${r}$ OR EXISTS (" " SELECT 1 FROM discount_customer_bind dcb " " WHERE dcb.customerid=b.customerid AND dcb.resellerid=${r}$)) " "ORDER BY b.bill_date DESC, b.bill_timestamp DESC LIMIT 100", {'r': userorgid}) await sor.sqlExe("COMMIT", {}) # 已冲正集合:冲正单的 orderid='REV:<原bill.id>' reversed_ids = set() reverses = [b for b in (bills or []) if getattr(b, 'business_op', '') == 'RECHARGE_REVERSE'] for rv in reverses: oid = getattr(rv, 'orderid', '') or '' if oid.startswith('REV:'): reversed_ids.add(oid[4:]) submit_url = entire_url('/accounting/recharge_reverse_submit.dspy') list_url = entire_url('/accounting/recharge_reverse_list.dspy') cards = [] for b in (bills or []): bop = getattr(b, 'business_op', '') bid = getattr(b, 'id', '') if bop == 'RECHARGE_REVERSE': # 冲正单本身只展示不再冲正 cards.append({ "widgettype": "HBox", "options": {"bgcolor": "#F0FDF4", "border": "1px solid #BBF7D0", "borderRadius": "8px", "padding": "10px 14px", "gap": "12px", "alignItems": "center"}, "subwidgets": [ {"widgettype": "Text", "options": {"text": "冲正单", "bgcolor": "#16A34A", "color": "#FFFFFF", "borderRadius": "6px", "padding": "2px 8px", "cfontsize": 0.85}}, {"widgettype": "Text", "options": {"text": str(getattr(b, 'orgname', '') or getattr(b, 'customerid', '')), "halign": "left", "css": "filler"}}, {"widgettype": "Text", "options": {"text": "¥" + str(getattr(b, 'amount', 0)), "halign": "left"}}, {"widgettype": "Text", "options": {"text": str(getattr(b, 'bill_date', '')), "color": "#94A3B8", "cfontsize": 0.85, "halign": "left"}}, ]}) continue is_rev = bid in reversed_ids row = [ {"widgettype": "Text", "options": {"text": "充值", "bgcolor": "#2563EB" if not is_rev else "#94A3B8", "color": "#FFFFFF", "borderRadius": "6px", "padding": "2px 8px", "cfontsize": 0.85}}, {"widgettype": "Text", "options": {"text": str(getattr(b, 'orgname', '') or getattr(b, 'customerid', '')), "halign": "left", "css": "filler"}}, {"widgettype": "Text", "options": {"text": "¥" + str(getattr(b, 'amount', 0)), "halign": "left"}}, {"widgettype": "Text", "options": {"text": str(getattr(b, 'bill_date', '')), "color": "#94A3B8", "cfontsize": 0.85, "halign": "left"}}, ] if is_rev: row.append({"widgettype": "Text", "options": {"text": "已冲正", "color": "#16A34A", "cfontsize": 0.85, "halign": "left"}}) else: # 冲正按钮:conform 确认 → script fetch POST → 成功后刷新列表 script = ( "var body=new URLSearchParams();" "body.append('bill_id'," + json.dumps(bid) + ");" "body.append('reason',(window.prompt&&window.prompt('冲正原因(记入审计)',''))||'');" "var resp=await fetch(" + json.dumps(submit_url) + ",{method:'POST',body:body});" "var rj=await resp.json();" "if(rj.success){" " var lc=bricks.getWidgetById('rev_list',bricks.app);" " if(lc){lc.clear_widgets();var lr=await fetch(" + json.dumps(list_url) + ");var ld=await lr.json();" " var lw=await bricks.widgetBuild(ld,lc);if(lw)lc.add_widget(lw);}" " new bricks.Message({title:'冲正成功',message:rj.message||'已冲正'});" "}else{new bricks.Message({title:'冲正失败',message:rj.error||'失败'});}") row.append({ "widgettype": "Button", "options": {"label": "冲正", "css": "small danger"}, "binds": [{ "wid": "self", "event": "click", "actiontype": "script", "target": "self", "conform": {"title": "冲正确认", "message": "确认冲正该笔充值 ¥" + str(getattr(b, 'amount', 0)) + "?冲正将翻转原记账方向(客户余额相应扣回),操作记入审计且不可自动撤销。", "conform": {"label": "确认冲正"}, "discard": {"label": "取消"}}, "script": script}]}) cards.append({ "widgettype": "HBox", "options": {"bgcolor": "#1E293B" if not is_rev else "#26313F", "border": "1px solid #334155", "borderRadius": "8px", "padding": "10px 14px", "gap": "12px", "alignItems": "center"}, "subwidgets": row}) if not cards: cards = [{"widgettype": "Text", "options": {"text": "暂无充值账单", "color": "#94A3B8", "padding": "20px"}}] return { "widgettype": "VBox", "options": {"width": "100%", "gap": "8px"}, "subwidgets": cards}