Compare commits

..

No commits in common. "main" and "feat/modern-ui-pricing" have entirely different histories.

34 changed files with 490 additions and 2171 deletions

1
.gitignore vendored
View File

@ -1 +0,0 @@
__pycache__/

252
README.md
View File

@ -331,258 +331,6 @@ llmage (模型管理) ──→ llm.ppid ──→ pricing_program.id
---
## HTTP API 接口
### 获取定价展示数据
```
GET /pricing/api/get_pricing_display.dspy?ppid={ppid}
Authorization: Bearer {token}
```
**说明:** 返回指定定价项目的可读定价数据,供前端展示。自动解析 pricing_data 字段,转换为人类可读的价格表。
**返回示例Token 定价):**
```json
{
"status": "ok",
"data": {
"ppid": "5i1JIpqERgCWqKQ4DCegD",
"name": "通义千问 qwen3.7-max",
"pricing_type": "per_use",
"items": [
{
"filters": {"model": "qwen3.7-max"},
"filter_labels": {"模型": "qwen3.7-max"},
"price_factors": [
{
"factor": "uncache_tokens",
"label": "非缓存tokens",
"unit_price": 6.0,
"unit": "百万",
"unit_label": "元/百万"
}
]
},
{
"filters": {"model": "qwen3.7-max"},
"filter_labels": {"模型": "qwen3.7-max"},
"price_factors": [
{
"factor": "cached_tokens",
"label": "缓存tokens",
"unit_price": 1.2,
"unit": "百万",
"unit_label": "元/百万"
}
]
}
],
"display_text": "【通义千问 qwen3.7-max】定价:\n - 非缓存tokens: 6.0 元/百万\n - 缓存tokens: 1.2 元/百万"
}
}
```
**返回示例(视频生成定价):**
```json
{
"status": "ok",
"data": {
"ppid": "vidu_video_pricing",
"name": "viduq3视频定价",
"pricing_type": "per_use",
"items": [
{
"filters": {"model": "viduq3-turbo", "resolution": "1080p", "off_peak": "0"},
"filter_labels": {"模型": "viduq3-turbo", "分辨率": "1080p", "错峰": "0"},
"price_factors": [
{
"factor": "duration",
"label": "时长",
"unit_price": 0.56,
"unit": "秒",
"unit_label": "元/秒"
}
]
}
],
"display_text": "【viduq3视频定价】定价:\n - 时长: 0.56 元/秒 [model=viduq3-turbo, resolution=1080p, off_peak=0]"
}
}
```
**返回字段说明:**
- `ppid`: 定价项目 ID
- `name`: 定价项目名称
- `pricing_type`: 定价类型(`per_use` 按次计费)
- `items`: 定价项列表
- `filters`: 过滤条件(如适用模型、分辨率等)
- `filter_labels`: 过滤条件的中文标签
- `price_factors`: 计价因子列表
- `factor`: 因子字段名
- `label`: 因子中文名称
- `unit_price`: 单位价格(已转换为展示价,无需再乘)
- `unit`: 单位(百万、秒、次等)
- `unit_label`: 单位标签(元/百万、元/秒等)
- `tiered`: 阶梯定价(仅当价格不同时展示)
- `display_text`: 人类可读的价格表文本
---
## pricing_data 使用指南
### 两种格式
pricing_data 支持两种格式:
#### 1. 新格式推荐price_factors + unit_prices + unit
适用于简单定价场景,直接指定计价因子和单位价格。
**示例 1Token 定价(文本生成)**
```yaml
unit_values:
百万: 1000000
fields:
model:
type: str
role: filter
label: 模型
uncache_tokens:
type: float
role: factor
label: 非缓存tokens
cached_tokens:
type: float
role: factor
label: 缓存tokens
completion_tokens:
type: float
role: factor
label: 输出tokens
pricings:
# 非缓存输入定价
- price_factors: uncache_tokens
unit_prices: 6.0
unit: 百万
filters:
- model: qwen3.7-max
# 缓存输入定价
- price_factors: cached_tokens
unit_prices: 1.2
unit: 百万
filters:
- model: qwen3.7-max
# 输出定价
- price_factors: completion_tokens
unit_prices: 18.0
unit: 百万
filters:
- model: qwen3.7-max
```
**示例 2视频生成定价多维度过滤**
```yaml
unit_values:
次: 1
fields:
model:
type: str
role: filter
label: 模型
resolution:
type: str
role: filter
label: 分辨率
duration:
type: int
role: filter
label: 时长(秒)
value_mode: between
off_peak:
type: str
role: filter
label: 错峰执行
flat:
type: float
role: factor
label: 固定费用
pricings:
- price_factors: flat
unit_prices: 85.0
unit: 次
filters:
- model: viduq2-pro
- resolution: 1080p
- duration: '1'
- off_peak: false
- price_factors: flat
unit_prices: 43.0
unit: 次
filters:
- model: viduq2-pro
- resolution: 1080p
- duration: '1'
- off_peak: true
- price_factors: duration
unit_prices: 0.56
unit: 秒
filters:
- model: viduq3-turbo
- resolution: 1080p
- off_peak: 0
```
#### 2. 旧格式formula公式计算
适用于复杂定价场景,使用 Python 表达式计算金额。
**示例:按 Token 数量计算**
```yaml
fields:
model:
type: str
role: filter
label: 模型
prompt_tokens:
type: float
role: factor
label: 输入tokens
completion_tokens:
type: float
role: factor
label: 输出tokens
pricings:
- model: gpt-4
formula: (3.2 * prompt_tokens + 16 * completion_tokens) / 1000000.0
- model: gpt-3.5
formula: (0.5 * prompt_tokens + 1.5 * completion_tokens) / 1000000.0
```
### 关键规则
1. **unit_values 必须定义**:在 pricing_data 顶部定义 `unit_values`,如 `百万: 1000000`,用于单位换算
2. **unit_prices 存储展示价**:新格式中 `unit_prices` 直接存储展示价格(如 `6.0` 表示 6.0 元/百万),不需要再乘以单位值
3. **filters 支持两种格式**
- 列表格式:`filters: [{model: xxx}, {resolution: yyy}]`
- 字典格式:直接作为字段写在 pricing 条目中
4. **value_mode 默认精确匹配**:不指定时做 `=` 精确匹配,可指定 `between``in``>``<`
5. **formula 可引用任意字段**:旧格式中 formula 可引用 config_data 中的任意字段名
6. **多条件匹配**:如果多条定价规则都匹配,全部返回(不是只返回第一条)
---
## 关键设计要点
1. **YAML 驱动定价规则**:所有定价逻辑用 YAML 描述,支持字段定义、选项、匹配模式、计算公式

View File

@ -1,49 +0,0 @@
Add Error: Add Error
Add Success: Add Success
Authorization Error: Authorization Error
Cancel: Cancel
Conform: Confirm
Delete Error: Delete Error
Delete Success: Delete Success
Discard: Discard
Please login: Please login
Record no exist or with wrong ownership: Record no exist or with wrong ownership
Reset: Reset
Submit: Submit
Update Error: Update Error
Update Success: Update Success
Validation Failed: Validation Failed
failed: failed
id: id
ok: ok
ppid parameter required: ppid parameter required
上传定价数据: Upload Pricing Data
下载定价数据: Download Pricing Data
价格: Price
供应商: Supplier
供应商折扣: Supplier Discount
名称: Name
启用日期: Effective Date
失效日期: Expiry Date
定价属于: Pricing Belongs To
定价数据: Pricing Data
定价文件: Pricing File
定价时序数据没找到: Pricing time-series data not found
定价模版: Pricing Template
定价测试: Pricing Test
定价管理: Pricing Management
定价细项: Pricing Line Items
定价项目: Pricing Item
定价项目id: Pricing Item ID
定价项目时序: Pricing Item Time Series
所属机构: Organization
折扣不能为空: Discount cannot be empty
折扣不能大于1: Discount cannot be greater than 1
折扣不能小于0: Discount cannot be less than 0
折扣必须是数字: Discount must be a number
描述: Description
测试: Test
规格明细: Specification Details
计费数据(json): Billing Data (JSON)
项目名称: Project Name
验证定价: Validate Pricing

View File

@ -1,49 +0,0 @@
Add Error: 追加エラー
Add Success: 追加成功
Authorization Error: 認証エラー
Cancel: キャンセル
Conform: 確認
Delete Error: 削除エラー
Delete Success: 削除成功
Discard: 破棄
Please login: ログインしてください
Record no exist or with wrong ownership: レコードが存在しないか、所有権が不正です
Reset: リセット
Submit: 送信
Update Error: 更新エラー
Update Success: 更新成功
Validation Failed: バリデーション失敗
failed: 失敗
id: id
ok: ok
ppid parameter required: ppidパラメーターが必要です
上传定价数据: 価格データアップロード
下载定价数据: 価格データダウンロード
价格: 価格
供应商: サプライヤー
供应商折扣: サプライヤー割引
名称: 名称
启用日期: 有効開始日
失效日期: 有効期限
定价属于: 価格所属
定价数据: 価格データ
定价文件: 価格ファイル
定价时序数据没找到: 価格時系列データが見つかりません
定价模版: 価格テンプレート
定价测试: 価格テスト
定价管理: 価格管理
定价细项: 価格明細
定价项目: 価格項目
定价项目id: 価格項目ID
定价项目时序: 価格項目時系列
所属机构: 所属組織
折扣不能为空: 割引は必須です
折扣不能大于1: 割引は1より大きくできません
折扣不能小于0: 割引は0より小さくできません
折扣必须是数字: 割引は数値である必要があります
描述: 説明
测试: テスト
规格明细: 仕様明細
计费数据(json): 課金データ(JSON)
项目名称: プロジェクト名
验证定价: 価格検証

View File

@ -1,49 +0,0 @@
Add Error: 추가 오류
Add Success: 추가 성공
Authorization Error: 인증 오류
Cancel: 취소
Conform: 확인
Delete Error: 삭제 오류
Delete Success: 삭제 성공
Discard: 폐기
Please login: 로그인해주세요
Record no exist or with wrong ownership: 레코드가 존재하지 않거나 소유권이 잘못되었습니다
Reset: 초기화
Submit: 제출
Update Error: 업데이트 오류
Update Success: 업데이트 성공
Validation Failed: 유효성 검사 실패
failed: 실패
id: id
ok: ok
ppid parameter required: ppid 파라미터가 필요합니다
上传定价数据: 가격 데이터 업로드
下载定价数据: 가격 데이터 다운로드
价格: 가격
供应商: 공급업체
供应商折扣: 공급업체 할인
名称: 이름
启用日期: 유효 시작일
失效日期: 만료일
定价属于: 가격 소속
定价数据: 가격 데이터
定价文件: 가격 파일
定价时序数据没找到: 가격 시계열 데이터를 찾을 수 없습니다
定价模版: 가격 템플릿
定价测试: 가격 테스트
定价管理: 가격 관리
定价细项: 가격 세부 항목
定价项目: 가격 항목
定价项目id: 가격 항목 ID
定价项目时序: 가격 항목 시계열
所属机构: 소속 조직
折扣不能为空: 할인은 비워둘 수 없습니다
折扣不能大于1: 할인은 1보다 클 수 없습니다
折扣不能小于0: 할인은 0보다 작을 수 없습니다
折扣必须是数字: 할인은 숫자여야 합니다
描述: 설명
测试: 테스트
规格明细: 규격 상세
计费数据(json): 과금 데이터(JSON)
项目名称: 프로젝트 이름
验证定价: 가격 검증

View File

@ -1,49 +0,0 @@
Add Error: Add Error
Add Success: Add Success
Authorization Error: Authorization Error
Cancel: Cancel
Conform: Conform
Delete Error: Delete Error
Delete Success: Delete Success
Discard: Discard
Please login: Please login
Record no exist or with wrong ownership: Record no exist or with wrong ownership
Reset: Reset
Submit: Submit
Update Error: Update Error
Update Success: Update Success
Validation Failed: Validation Failed
failed: failed
id: id
ok: ok
ppid parameter required: ppid parameter required
上传定价数据: 上传定价数据
下载定价数据: 下载定价数据
价格: 价格
供应商: 供应商
供应商折扣: 供应商折扣
名称: 名称
启用日期: 启用日期
失效日期: 失效日期
定价属于: 定价属于
定价数据: 定价数据
定价文件: 定价文件
定价时序数据没找到: 定价时序数据没找到
定价模版: 定价模版
定价测试: 定价测试
定价管理: 定价管理
定价细项: 定价细项
定价项目: 定价项目
定价项目id: 定价项目id
定价项目时序: 定价项目时序
所属机构: 所属机构
折扣不能为空: 折扣不能为空
折扣不能大于1: 折扣不能大于1
折扣不能小于0: 折扣不能小于0
折扣必须是数字: 折扣必须是数字
描述: 描述
测试: 测试
规格明细: 规格明细
计费数据(json): 计费数据(json)
项目名称: 项目名称
验证定价: 验证定价

View File

@ -7,22 +7,13 @@
"browserfields": {
"exclouded": ["id", "ownerid", "pricing_spec" ],
"alters": {
"discount": {
"rules": [
{"type": "required", "message": "折扣不能为空"},
{"type": "number", "message": "折扣必须是数字"},
{"type": "min", "value": 0, "message": "折扣不能小于0"},
{"type": "max", "value": 1, "message": "折扣不能大于1"}
]
}
"providerid":{
"valueField": "id",
"textField": "orgname",
"dataurl":"{{entire_url('/rbac/get_provider.dspy')}}"
}
}
},
"data_filter": {
"fields": [
{"field": "name", "title": "项目名称", "uitype": "str"},
{"field": "providerid", "title": "供应商", "uitype": "code"}
]
},
"editexclouded": [
"id", "ownerid"
],
@ -54,8 +45,7 @@
"width": "70%",
"height": "70%",
"auto_open": true,
"archor": "cc",
"title": "定价测试"
"archor": "cc"
},
"options": {
"url": "{{entire_url('../test_pricing_program.ui')}}",

View File

@ -2,12 +2,13 @@
"tblname": "pricing_program_timing",
"title": "定价项目时序",
"params": {
"logined_userorgid": "ownerid",
"browserfields": {
"exclouded": ["id", "ppid" ],
"exclouded": ["id", "ownerid", "ppid" ],
"alters": {}
},
"editexclouded": [
"id", "ppid", "name"
"id", "ownerid", "ppid", "name"
],
"subtables":[
{
@ -28,16 +29,13 @@
{
"name": "upload_pricing_data",
"label": "上传定价数据",
"selected_row": true,
"icon": "{{entire_url('/bricks/imgs/upload.svg')}}"
}, {
"name": "download_pricing_data",
"label": "下载定价数据",
"selected_row": true,
"icon": "{{entire_url('/bricks/imgs/download.svg')}}"
}, {
"name": "test",
"selected_row": true,
"label": "验证定价",
"icon": "{{entire_url('/bricks/imgs/test.svg')}}"
}
@ -61,10 +59,7 @@
"wid": "self",
"event": "upload_pricing_data",
"actiontype": "urlwidget",
"target": "PopupWindow",
"popup_options": {
"title": "上传定价数据"
},
"target": "self",
"options": {
"params": {
"ppid": "{{params_kw.ppid}}"
@ -92,7 +87,6 @@
"actiontype": "urlwidget",
"target": "PopupWindow",
"popup_options": {
"title": "验证定价"
},
"options":{
"params": {

View File

@ -44,8 +44,7 @@
"name": "discount",
"title": "供应商折扣",
"type": "float",
"length": 18,
"dec": 2
"length": 18
},
{
"name": "description",

View File

@ -1,6 +0,0 @@
from pricing.pricing import (
PricingProgram,
test_pricing,
generate_formula_from_factors,
get_pricing_display,
)

View File

@ -3,8 +3,7 @@ from sqlor.dbpools import DBPools
from pricing.pricing import (
PricingProgram,
test_pricing,
generate_formula_from_factors,
get_pricing_display,
get_pricing_program
)
from ahserver.serverenv import ServerEnv
@ -28,6 +27,7 @@ def _bind_pricing_events(dbpools, dbname):
def load_pricing():
env = ServerEnv()
env.get_pricing_program = get_pricing_program
env.write_pricing_patten = PricingProgram.write_pricing_patten
env.write_pricing_data = PricingProgram.write_pricing_data
env.pricing_program_charging = PricingProgram.charging
@ -35,11 +35,6 @@ def load_pricing():
env.load_pricing_data = PricingProgram.load_pricing_data
env.get_pricing_program = PricingProgram.get_pricing_program
env.test_pricing = test_pricing
env.generate_formula_from_factors = generate_formula_from_factors
env.get_pricing_display = get_pricing_display
# Bind hot_reload event — only when running in ahserver (event_dispatcher available)
if getattr(env, 'event_dispatcher', None) is not None:
env.event_dispatcher.bind('hot_reload', PricingProgram.on_hot_reload)
dbpools = DBPools()
dbname = env.get_module_dbname('pricing')
if dbname:
@ -47,3 +42,4 @@ def load_pricing():
debug(f'Pricing event listeners bound for database: {dbname}')
else:
debug('Pricing event listeners skipped: no database configured for pricing module')

View File

@ -6,22 +6,9 @@ from sqlor.dbpools import DBPools, get_sor_context
from appPublic.log import debug, exception, info, MyLogger
from appPublic.timeUtils import curDateString
from appPublic.dictObject import DictObject
from appPublic.jsonConfig import getConfig
from .write_pattern import write_pattern_xlsx, load_xlsx_pricing
import yaml
def _cache_enabled():
"""Check if cache is enabled for pricing module in config.json"""
try:
config = getConfig()
module_cache = config.module_cache
if module_cache is None:
return True
return getattr(module_cache, 'pricing', True)
except Exception:
return True
"""
采用yaml描述定价策略
在pricing_program的pricing_spec表中定义定价的数据字段
@ -128,10 +115,8 @@ def typevalue(v, t):
return v
return f(v)
def check_value(field, spec_value, data_value, value_mode=None):
if value_mode is None:
value_mode = field.value_mode
if value_mode == 'between':
def check_value(field, spec_value, data_value):
if field.value_mode == 'between':
arr = spec_value.strip().split()
if len(arr) < 2 or len(arr) > 3:
e = f'{spec_value=} error'
@ -157,19 +142,17 @@ def check_value(field, spec_value, data_value, value_mode=None):
return arr[0] < fvalue and fvalue < arr[-1]
if arr[1] == '~=':
return arr[0] < fvalue and fvalue <= arr[-1]
if arr[1] == '=~=':
return arr[0] <= fvalue and fvalue <= arr[-1]
e = f'{arr[1]}不认识的期间逻辑,只支持:~ =~ ~= =~='
e = f'{arr[1]}不认识的期间逻辑,只支持:~ =~ ~='
exception(e)
raise Exception(e)
if value_mode == 'in':
if field.value_mode == 'in':
arr = spec_value.strip().split()
arr = [ typevalue(a, field.type) for a in arr ]
# debug(f'{arr=}, {data_value=}')
return data_value in arr
mode = value_mode
mode = field.value_mode
if not mode or mode == '=':
mode = '=='
ns = {
@ -190,13 +173,6 @@ def data_mapping(ns, name, v):
class PricingProgram:
pricing_data = {}
@staticmethod
def on_hot_reload(data=None):
"""Event handler for hot_reload event. Clears pricing cache."""
from appPublic.log import debug
debug(f'[pricing] on_hot_reload called, clearing pricing_data (data={data})')
PricingProgram.pricing_data.clear()
@staticmethod
async def get_pricing_program(ppid):
env = ServerEnv()
@ -448,10 +424,9 @@ class PricingProgram:
async def get_ppid_pricing(ppid):
dat = curDateString()
k = f'{ppid}.{dat}'
if _cache_enabled():
d = PricingProgram.pricing_data.get(k)
if d:
return d
d = PricingProgram.pricing_data.get(k)
if d:
return d
env = ServerEnv()
async with get_sor_context(env, 'pricing') as sor:
sql = """select a.name, a.ownerid, a.providerid,
@ -470,8 +445,7 @@ class PricingProgram:
e = Exception(f'{ppid=},{dat=} data not found')
exception(f'{e}')
raise e
d = recs[0]
if _cache_enabled():
d = recs[0]
PricingProgram.pricing_data[k] = d
dates = PricingProgram.pricing_data.get(ppid, [])
dates.append(dat)
@ -482,16 +456,15 @@ class PricingProgram:
PricingProgram.pricing_data[dk]
dates = dates[-2:]
PricingProgram.pricing_data[ppid] = dates
return d
return d
async def buffered_charging(ppid, data):
r = await PricingProgram.get_ppid_pricing(ppid)
prices = PricingProgram.get_pricing_from_ymalstr(data,
r.pricing_data)
r.pricing_data)
amt = 0.0
discount = max(0.0, min(1.0, r.discount)) if r.discount is not None else 1.0
for p in prices:
p.cost = p.amount * discount
p.cost = p.amount * r.discount
return prices
async def charging(sor, ppid, data):
@ -523,28 +496,20 @@ order by b.enabled_date desc"""
r.pricing_data)
debug(f'{r.prices=}')
amt = 0.0
discount = max(0.0, min(1.0, r.discount)) if r.discount is not None else 1.0
for p in r.prices:
p.cost = p.amount * discount
p.cost = p.amount * r.discount
return r.prices
@staticmethod
def get_pricing_from_ymalstr(config_data, yamlstr):
"""
解析定价YAML并计算费用
支持两种格式
1. 旧格式formula 字段eval计算
2. 新格式price_factors + unit_prices + unit自动计算
支持 derived 字段 fields 中定义 derived 表达式从原始 usage 数据计算衍生字段
例如uncached_prompt_tokens.derived = "prompt_tokens - prompt_tokens_details.cached_tokens"
yamlstr是从
"""
if config_data is None:
e = Exception(f'config_data is None, {yamlstr=}')
exception(f'{e=}')
raise e
# 用 DictObject 包装,支持 dot notation 属性访问derived 表达式依赖此特性)
config_data = DictObject(**config_data)
config_data = config_data.copy()
d = None
try:
d = yaml.safe_load(yamlstr)
@ -558,62 +523,19 @@ order by b.enabled_date desc"""
if not d.pricings:
exception(f'{d} has not "pricings"')
raise Exception(f'定价定义中没有pricing数据')
# 处理 derived 字段:从原始 usage 数据计算衍生字段
# DictObject 支持属性访问嵌套字段,如 prompt_tokens_details.cached_tokens
for field_name, field_def in d.fields.items():
if not isinstance(field_def, dict):
continue
derived_expr = field_def.get('derived')
if not derived_expr:
continue
# eval 环境:用 config_data 本身DictObject 支持 dot notation 属性访问)
eval_env = dict(config_data)
# 将顶层 key 也注入 eval 环境,使 eval 能找到嵌套属性
for k in list(config_data.keys()):
eval_env[k] = config_data[k]
try:
result = eval(derived_expr, {}, eval_env)
config_data[field_name] = result
debug(f'derived field {field_name} = {derived_expr} = {result}')
except Exception as e:
debug(f'derived field {field_name} evaluation failed: {derived_expr}, error: {e}')
config_data[field_name] = 0
# 单位映射表
unit_values = d.get('unit_values', {'百万': 1000000, '': 1, '': 1000, '': 1, '': 1, '毫秒': 0.001, '元/百万tokens': 1000000, '元/total_tokens': 1, '元/times': 1})
ret_items = []
for i, p in enumerate(d.pricings):
# 跳过需要人工审核的记录
if p.get('_NEEDS_MANUAL_REVIEW'):
debug(f'跳过需要人工审核的定价项: {i}')
if not p.formula:
debug(f'无公式:{p=}')
continue
# 判断是旧格式还是新格式
# 新格式要求 price_factors 是标量(str)、unit_prices 是标量(number)
# 生产数据中 price_factors 可能是 list、unit_prices 可能是 dict此时走旧格式 formula
_raw_pf = p.get('price_factors')
_raw_up = p.get('unit_prices')
is_new_format = (_raw_pf is not None and _raw_up is not None
and not isinstance(_raw_pf, list) and not isinstance(_raw_up, dict))
is_old_format = p.get('formula') is not None
if not is_new_format and not is_old_format:
debug(f'无公式也无price_factors{p=}')
continue
p_ok = True
times = 1
unit = 1
ns = DictObject(**config_data)
# 检查过滤条件(排除定价计算字段)
skip_keys = {'formula', 'price_factors', 'unit_prices', 'unit', 'min_amount', 'filters', 'pricing_type'}
for k, spec_value in p.items():
for k,spec_value in p.items():
if spec_value is None:
continue
if k in skip_keys:
if k == 'formula':
continue
f = d.fields.get(k)
if not f:
@ -621,6 +543,8 @@ order by b.enabled_date desc"""
exception(f'{e}')
raise Exception(e)
data_value = config_data.get(k)
# p[f'old_{k}'] = data_value
# p[f'mapping_{k}'] = data_mapping(d, k, data_value) #需要mapping的数据转换
data_value = data_mapping(d, k, data_value)
if data_value is None:
if 'default' in f.keys():
@ -632,338 +556,48 @@ order by b.enabled_date desc"""
try:
flg = check_value(f, spec_value, data_value)
if not flg:
# 条件不满足
# debug(f'条件不满足:{p=},{spec_value=}, {data_value=}, {k=}')
p_ok = False
break
except Exception as e:
msg = f'{p=},{f}: {spec_value=}, {data_value=}'
exception(f'{e}:{msg}')
break
# 检查 filters 区间条件新格式AND逻辑所有filter_item都要匹配
# 带 unit_prices 的 filter_item 是 tiered 定价,跳过(由后面第二块处理)
if p_ok and is_new_format and 'filters' in p:
for filter_item in p['filters']:
if 'unit_prices' in filter_item:
continue # tiered定价项不在此处检查
item_ok = True
item_value_mode = filter_item.get('value_mode')
for fk, fv in filter_item.items():
if fk in ('unit_prices', 'value_mode'):
continue
f = d.fields.get(fk)
if not f:
continue
data_value = config_data.get(fk)
data_value = data_mapping(d, fk, data_value)
if data_value is None:
continue # 数据中没有该键,视为匹配
try:
flg = check_value(f, fv, data_value, item_value_mode)
if not flg:
item_ok = False
break
except Exception as e:
debug(f'filter check error: {e}')
item_ok = False
break
if not item_ok:
p_ok = False
break
if not p_ok:
info(f'{config_data=}, {p=}, mismatched')
continue
np = p.copy()
np.data = config_data
if is_new_format:
# 新格式price_factors + unit_prices + unit
factor_name = p['price_factors']
unit_price = p['unit_prices']
unit_str = p.get('unit', '')
# 处理 filters 中的区间定价(查找匹配的 unit_prices
if 'filters' in p:
for filter_item in p['filters']:
item_value_mode = filter_item.get('value_mode')
for fk, fv in filter_item.items():
if fk in ('unit_prices', 'value_mode'):
continue
f = d.fields.get(fk)
if not f:
continue
data_value = config_data.get(fk)
data_value = data_mapping(d, fk, data_value)
if data_value is None:
continue
try:
flg = check_value(f, fv, data_value, item_value_mode)
if flg and 'unit_prices' in filter_item:
unit_price = filter_item['unit_prices']
except:
pass
# 获取 usage 值
if factor_name == 'flat':
# 固定费用
usage_value = 1
else:
usage_value = config_data.get(factor_name)
if usage_value is None:
debug(f'新格式config_data中缺少{factor_name}')
continue
usage_value = float(usage_value)
# 获取单位值
unit_val = unit_values.get(unit_str, 1)
if isinstance(unit_val, str):
unit_val = float(unit_val)
# 计算金额
amount = unit_price * usage_value / unit_val
# 应用 min_amount
min_amount = p.get('min_amount', 0)
if min_amount and amount < min_amount:
amount = min_amount
np.amount = amount
ret_items.append(np)
elif is_old_format:
# 旧格式formula
if p_ok and p.formula:
np = p.copy()
formula = p.formula
if not formula:
e = f'{p} not formula found'
exception(e)
raise Exception(e)
debug(f'{formula=}, {ns=}, {p=}, {d.fields=}')
env_data = DictObject(config_data)
np.amount = eval(formula, env_data)
np.data = config_data
np.amount = eval(formula, config_data.copy())
ret_items.append(np)
else:
info(f'{config_data=}, {p=}, {d.model_mappings=}, mismatched')
if len(ret_items) == 0:
e = f'{config_data=}{yamlstr=}没有找到合适的定价'
exception(e)
raise Exception(e)
return ret_items
def generate_formula_from_factors(price_factors):
"""从 price_factors 数组自动生成 formula 字符串。
price_factors 格式:
[
{"factor": "prompt_tokens", "unit_price": 3.2, "unit_label": "元/百万Token"},
{"factor": "completion_tokens", "unit_price": 16.0, "unit_label": "元/百万Token"}
]
返回: "(3.2 * float(prompt_tokens) + 16.0 * float(completion_tokens)) / 1000000.0"
"""
if not price_factors:
return None
parts = []
has_million = False
for f in price_factors:
factor = f.get('factor', '')
unit_price = f.get('unit_price', 0)
unit_label = f.get('unit_label', '')
# 判断单位是否是"百万"级别(元/百万Token 等)
if '百万' in unit_label:
has_million = True
parts.append(f'{unit_price} * float({factor})')
formula = ' + '.join(parts)
if has_million:
formula = f'({formula}) / 1000000.0'
return formula
async def get_pricing_display(ppid):
"""获取定价项目的可读定价数据,供客户/前端展示。
返回结构:
{
"ppid": "...",
"name": "...",
"pricing_type": "per_use",
"items": [
{
"filters": {"model": "doubao-seed-2-0-pro"},
"filter_labels": {"模型": "doubao-seed-2-0-pro"},
"price_factors": [
{
"factor": "prompt_tokens",
"label": "输入Token",
"unit_price": 0.000006,
"unit": "百万",
"unit_label": "元/百万Token"
}
],
"formula": "...",
"min_amount": 0.01
}
]
}
"""
try:
r = await PricingProgram.get_ppid_pricing(ppid)
except Exception:
return None
pricing_data_str = r.pricing_data
d = yaml.safe_load(pricing_data_str)
if not d:
raise Exception(f'{ppid} pricing_data 为空')
fields = d.get('fields', {})
pricings = d.get('pricings', [])
pricing_type = d.get('pricing_type', 'per_use')
unit_values = d.get('unit_values', {'百万': 1000000, '': 1, '': 1000, '': 1, '': 1, '毫秒': 0.001, '元/百万tokens': 1000000, '元/total_tokens': 1, '元/times': 1})
items = []
for p in pricings:
# 跳过需要人工审核的记录
if p.get('_NEEDS_MANUAL_REVIEW'):
continue
# 提取过滤条件role=filter 的字段)
filters = {}
filter_labels = {}
skip_keys = {'formula', 'price_factors', 'unit_prices', 'unit', 'filters', 'min_amount', 'pricing_type', 'name'}
# 先从 p.items() 中提取(直接字段)
for k, v in p.items():
if k in skip_keys:
continue
fdef = fields.get(k, {})
role = fdef.get('role', 'filter') if isinstance(fdef, dict) else 'filter'
if role == 'filter':
filters[k] = v
label = fdef.get('label', k) if isinstance(fdef, dict) else k
filter_labels[label] = v
# 再从 p['filters'] 列表中提取(如果存在)
raw_filters = p.get('filters')
if isinstance(raw_filters, list):
for fi in raw_filters:
if isinstance(fi, dict):
for k, v in fi.items():
fdef = fields.get(k, {})
filters[k] = v
label = fdef.get('label', k) if isinstance(fdef, dict) else k
filter_labels[label] = v
# 新格式price_factors(scalar) + unit_prices(scalar) + unit
# 注意:生产数据中 price_factors 可能是 list、unit_prices 可能是 dict混合格式
# 此时应走旧格式 formula 路径
_raw_pf = p.get('price_factors')
_raw_up = p.get('unit_prices')
is_new_format = (_raw_pf is not None and _raw_up is not None
and not isinstance(_raw_pf, list) and not isinstance(_raw_up, dict))
if is_new_format:
factor_name = _raw_pf
unit_price = _raw_up
unit_str = p.get('unit', '')
# 获取 factor label
fdef = fields.get(factor_name, {})
factor_label = fdef.get('label', factor_name) if isinstance(fdef, dict) else factor_name
# 新格式 unit_prices 已是展示价(如 6.0 元/百万),无需再乘 unit_val
display_price = unit_price
# 构建 unit_label (元/单位)
unit_label = f"元/{unit_str}" if unit_str else ""
price_factors_display = [{
'factor': factor_name,
'label': factor_label,
'unit_price': display_price,
'unit': unit_str,
'unit_label': unit_label
}]
# 处理 filters: 提取适用条件(model)到item级区间条件放tiered
# 注意filters 已在上方提取(支持 dict 和 list 两种格式)
# tiered 仅用于价格不同的阶梯定价
tiered_pricing = []
if isinstance(p.get('filters'), list):
for fi in p['filters']:
if not isinstance(fi, dict):
continue
raw_tier_price = fi.get('unit_prices')
if raw_tier_price is None or raw_tier_price == unit_price:
continue
fi_copy = {k: v for k, v in fi.items()
if k not in ('unit_prices', 'value_mode')
and not k.endswith('_tokens')}
if fi_copy:
tiered_pricing.append({
'filters': fi_copy,
'unit_prices': raw_tier_price
})
item = {'price_factors': price_factors_display}
if filters:
item['filters'] = filters
item['filter_labels'] = filter_labels
min_amount = p.get('min_amount', 0)
if min_amount:
item['min_amount'] = min_amount
if tiered_pricing:
item['price_factors'][0]['tiered'] = tiered_pricing
items.append(item)
else:
# 旧格式formula
price_factors = p.get('price_factors', None)
if not price_factors:
# fallback: 从 fields 中构建展示信息
price_factors = []
for k, fdef in fields.items():
if not isinstance(fdef, dict):
continue
role = fdef.get('role', 'filter')
if role == 'factor' or fdef.get('type') == 'float':
label = fdef.get('label', k)
price_factors.append({
'factor': k,
'label': label,
'unit_price': None,
'unit_label': ''
})
item = {'price_factors': price_factors}
if filters:
item['filters'] = filters
item['filter_labels'] = filter_labels
if p.get('formula'):
item['formula'] = p['formula']
items.append(item)
# 生成可读价格表
display_lines = [f"{getattr(r, 'name', '')}】定价:"]
for item in items:
# 构建过滤条件文本
filter_text = ''
if item.get('filter_labels'):
filter_parts = [f"{k}={v}" for k, v in item['filter_labels'].items()]
filter_text = f" [{', '.join(filter_parts)}]"
for pf in item.get('price_factors', []):
label = pf.get('label', pf.get('factor', ''))
up = pf.get('unit_price')
ul = pf.get('unit_label', '')
if up is not None:
display_lines.append(f" - {label}: {up} {ul}{filter_text}")
if pf.get('tiered'):
for t in pf['tiered']:
t_filters = ', '.join(f"{k}={v}" for k, v in t.get('filters', {}).items())
t_price = t.get('unit_prices', '')
display_lines.append(f" · {t_filters}: {t_price} {ul}")
return {
'ppid': ppid,
'name': getattr(r, 'name', ''),
'pricing_type': pricing_type,
'items': items,
'display_text': '\n'.join(display_lines)
}
async def get_pricing_program(ppid):
env = ServerEnv()
async with get_sor_context(env, 'pricing') as sor:
recs = await sor.R('pricing_program', {'id': ppid})
if len(recs) == 0:
exception(f'{ppid} not found in pricing_program_timing')
return None
pp = recs[0]
if ppt.pricing_data is None:
exception(f'{ppid} pricing_data is None in pricing_program_timing')
return None
try:
PricingProgram.pp_db2app(ppt)
except Exception as e:
return None
async def get_pricing_program_timeing(pptid):
env = ServerEnv()
@ -983,10 +617,8 @@ async def get_pricing_program_timeing(pptid):
return ppt
async def test_pricing(pptid, data):
ppt = await get_pricing_program_timeing(pptid)
# ppt.pricing_data 已被 ppt_db2app 解析为 dict需要转回 YAML 字符串
yamlstr = yaml.dump(ppt.pricing_data, allow_unicode=True) if isinstance(ppt.pricing_data, dict) else ppt.pricing_data
prices = PricingProgram.get_pricing_from_ymalstr(data, yamlstr)
ppt = get_pricing_program_timeing(pptid)
prices = PricingProgram.get_pricing_from_ymalstr(data, ppt.pricing_data)
if prices is None:
return None
amount = 0

View File

@ -1,115 +0,0 @@
#!/usr/bin/env python3
"""
pricing 模块 RBAC 权限管理脚本
使用方法:
cd ~/repos/sage
./py3/bin/python ~/repos/pricing/scripts/load_path.py
"""
import subprocess
import os
import sys
def find_sage_root():
candidates = [
os.path.expanduser("~/repos/sage"),
os.path.expanduser("~/sage"),
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
]
for c in candidates:
if os.path.isdir(os.path.join(c, "py3")) and os.path.isdir(os.path.join(c, "wwwroot")):
return c
return None
SAGE_ROOT = find_sage_root()
if not SAGE_ROOT:
print("ERROR: Cannot find Sage root directory")
sys.exit(1)
PYTHON = os.path.join(SAGE_ROOT, "py3", "bin", "python")
SET_PERM_SCRIPT = os.path.join(SAGE_ROOT, "set_role_perm.py")
MOD = "pricing"
# ============================================================
# 权限路径定义(逐条列举,禁止通配符)
# ============================================================
# any — 无需登录
PATHS_ANY = [
f"/{MOD}/menu.ui",
]
# logined — 所有已登录用户
PATHS_LOGINED = [
# 模块入口
f"/{MOD}",
f"/{MOD}/index.ui",
# 顶层 .ui 页面
f"/{MOD}/load_pricing_data.ui",
f"/{MOD}/pricing_test.ui",
f"/{MOD}/test_pricing_program.ui",
# 顶层 .dspy 文件
f"/{MOD}/download_pricing_data.dspy",
f"/{MOD}/download_pricing_pattern.dspy",
f"/{MOD}/get_all_pricing_programs.dspy",
f"/{MOD}/get_platform_providers.dspy",
f"/{MOD}/test_pricing_program.dspy",
f"/{MOD}/upload_pricing_data.dspy",
# api/ .dspy 文件
f"/{MOD}/api/get_pricing_display.dspy",
# CRUD: pricing_program (auto-generated by xls2ui)
f"/{MOD}/pricing_program",
f"/{MOD}/pricing_program/index.ui",
f"/{MOD}/pricing_program/get_pricing_program.dspy",
f"/{MOD}/pricing_program/add_pricing_program.dspy",
f"/{MOD}/pricing_program/update_pricing_program.dspy",
f"/{MOD}/pricing_program/delete_pricing_program.dspy",
# CRUD: pricing_program_timing (auto-generated by xls2ui)
f"/{MOD}/pricing_program_timing",
f"/{MOD}/pricing_program_timing/index.ui",
f"/{MOD}/pricing_program_timing/get_pricing_program_timing.dspy",
f"/{MOD}/pricing_program_timing/add_pricing_program_timing.dspy",
f"/{MOD}/pricing_program_timing/update_pricing_program_timing.dspy",
f"/{MOD}/pricing_program_timing/delete_pricing_program_timing.dspy",
]
# ============================================================
# 执行注册
# ============================================================
def run_set_perm(role, path):
cmd = [PYTHON, SET_PERM_SCRIPT, role, path]
result = subprocess.run(cmd, capture_output=True, text=True)
return result.returncode == 0
def register_role_paths(role, paths):
count = 0
for p in paths:
if run_set_perm(role, p):
count += 1
print(f" {role}: {count}/{len(paths)} paths registered")
return count
def main():
print(f"Sage root: {SAGE_ROOT}")
total = 0
total += register_role_paths("any", PATHS_ANY)
total += register_role_paths("logined", PATHS_LOGINED)
print(f"\nDone. Total {total} permission entries registered.")
print("NOTE: Restart Sage after permission changes to reload RBAC cache.")
if __name__ == "__main__":
main()

View File

@ -1,10 +0,0 @@
ppid = params_kw.get('ppid')
if not ppid:
return json.dumps({"status": "error", "message": "ppid parameter required"}, ensure_ascii=False)
try:
result = await get_pricing_display(ppid)
return json.dumps({"status": "ok", "data": result}, ensure_ascii=False, default=str)
except Exception as e:
exception(f'get_pricing_display({ppid}) failed: {e}\n{format_exc()}')
return json.dumps({"status": "error", "message": str(e)}, ensure_ascii=False)

View File

@ -3,7 +3,8 @@
"options": {
"width": "100%",
"height": "100%",
"padding": "0"
"padding": "0",
"bgcolor": "#0B1120"
},
"subwidgets": [
{
@ -17,7 +18,9 @@
{
"widgettype": "Title2",
"options": {
"text": "定价管理"
"text": "定价管理",
"color": "#F1F5F9",
"fontWeight": "700"
}
},
{
@ -27,77 +30,70 @@
"widgettype": "Text",
"options": {
"text": "模型定价项目与计费规则配置",
"cfontsize": 1.2
"fontSize": "14px",
"color": "#64748B"
}
}
]
},
{
"widgettype": "VScrollPanel",
"widgettype": "VBox",
"options": {
"css": "filler"
"bgcolor": "#1E293B",
"padding": "24px",
"borderRadius": "12px",
"border": "1px solid #334155",
"cursor": "pointer"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.pricing_content",
"options": {
"url": "{{entire_url('/pricing/pricing_program')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "VBox",
"widgettype": "Svg",
"options": {
"spacing": 24
},
"subwidgets": [
{
"widgettype": "ResponsableBox",
"options": {
"gap": "16px",
"minWidth": "300px"
},
"subwidgets": [
{
"widgettype": "VBox",
"options": {
"css": "card",
"cwidth": 25,
"padding": "16px",
"cursor": "pointer"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.pricing_content",
"options": {
"url": "{{entire_url('/pricing/pricing_program')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Title4",
"options": {
"text": "定价项目管理",
"marginBottom": "8px"
}
},
{
"widgettype": "Text",
"options": {
"text": "管理模型定价规则、计费项目和定时任务",
"cfontsize": 1.2
}
}
]
}
]
}
]
"svg": "<svg width=\"36\" height=\"36\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#F59E0B\" stroke-width=\"1.5\"><path d=\"M12 6v12m-3-2.818l.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 11-18 0 9 9 0 0118 0z\"/></svg>",
"width": "36px",
"height": "36px",
"marginBottom": "16px"
}
},
{
"widgettype": "VBox",
"id": "pricing_content"
"widgettype": "Title4",
"options": {
"text": "定价项目管理",
"color": "#F1F5F9",
"fontWeight": "600",
"marginBottom": "8px"
}
},
{
"widgettype": "Text",
"options": {
"text": "管理模型定价规则、计费项目和定时任务",
"fontSize": "14px",
"color": "#94A3B8"
}
}
]
},
{
"widgettype": "VBox",
"id": "pricing_content",
"css": "filler",
"options": {
"width": "100%",
"overflowY": "auto"
}
}
]
}

View File

@ -4,7 +4,6 @@
"options": {
"title": "{{pp.id}}",
"description": {{json.dumps(pp.description)}},
"height": "200%",
"fields":[
{
"name": "xlsx_file",

View File

@ -6,7 +6,7 @@
{
"name": "pricing",
"label": "定价管理",
"url": "{{entire_url('/pricing/pricing_program')}}"
"url": "{{entire_url('/pricing/index.ui')}}"
}
]
}

View File

@ -0,0 +1 @@
return await get_pricing_specs_by_pptid(params_kw.pptid)

View File

@ -8,16 +8,18 @@ if not id or len(id) > 32:
id = uuid()
ns['id'] = id
db = DBPools()
dbname = get_module_dbname('pricing')
async with db.sqlorContext(dbname) as sor:
r = await sor.C('pricing_program_timing', ns.copy())
fs = await sor_get_spec_fields(sor, ns.id)
if len(fs):
ns1 = {k:ns[k] for k in fs}
ns.spec_value = json.dumps(ns1)
r = await sor.C('pricing_item', ns.copy())
return {
"widgettype":"Message",
"options":{
"user_data":ns,
"cwidth":16,
"cheight":9,
"title":"Add Success",

View File

@ -7,7 +7,7 @@ ns = {
db = DBPools()
dbname = get_module_dbname('pricing')
async with db.sqlorContext(dbname) as sor:
r = await sor.D('pricing_program_timing', ns)
r = await sor.D('pricing_item', ns)
debug('delete success');
return {
"widgettype":"Message",

View File

@ -0,0 +1,118 @@
ns = params_kw.copy()
debug(f'get_pricing_item.dspy:{ns=}')
if not ns.get('page'):
ns['page'] = 1
if not ns.get('sort'):
ns['sort'] = 'name'
sql = '''select a.*, b.pptid_text, c.psid_text, d.subppid_text
from (select * from pricing_item where 1=1 [[filterstr]]) a left join (select id as pptid,
id as pptid_text from pricing_program_timing where 1 = 1) b on a.pptid = b.pptid left join (select id as psid,
name as psid_text from pricing_spec where 1 = 1) c on a.psid = c.psid left join (select id as subppid,
name as subppid_text from pricing_program where 1 = 1) d on a.subppid = d.subppid'''
filterjson = params_kw.get('data_filter')
if not filterjson:
fields = [ f['name'] for f in [
{
"name": "id",
"title": "id",
"type": "str",
"length": 32
},
{
"name": "name",
"title": "定价项名",
"type": "str",
"length": 256
},
{
"name": "description",
"title": "描述",
"type": "text"
},
{
"name": "pptid",
"title": "项目id",
"type": "str",
"length": 32
},
{
"name": "psid",
"title": "定价规格id",
"type": "str",
"length": 32
},
{
"name": "subppid",
"title": "子项目id",
"type": "str",
"length": 32,
"nullable": "yes"
},
{
"name": "spec_value",
"title": "规格值",
"type": "text"
},
{
"name": "pricing_unit",
"title": "定价单位",
"type": "float",
"length": 18,
"dec": 5
},
{
"name": "pricing_amount",
"title": "定价金额",
"type": "float",
"length": 18,
"dec": 5
},
{
"name": "cost_amount",
"title": "成本金额",
"type": "float",
"length": 18,
"dec": 5
}
] ]
filterjson = default_filterjson(fields, ns)
filterdic = ns.copy()
filterdic['filterstr'] = ''
filterdic['userorgid'] = '${userorgid}$'
filterdic['userid'] = '${userid}$'
if filterjson:
dbf = DBFilter(filterjson)
conds = dbf.gen(ns)
if conds:
ns.update(dbf.consts)
conds = f' and {conds}'
filterdic['filterstr'] = conds
ac = ArgsConvert('[[', ']]')
vars = ac.findAllVariables(sql)
NameSpace = {v:'${' + v + '}$' for v in vars if v != 'filterstr' }
filterdic.update(NameSpace)
sql = ac.convert(sql, filterdic)
debug(f'{sql=}')
db = DBPools()
dbname = get_module_dbname('pricing')
async with db.sqlorContext(dbname) as sor:
r = await sor.sqlPaging(sql, ns)
for d in r['rows']:
if d.spec_value:
ad = json.loads(d.spec_value)
d.update(ad)
return r
return {
"total":0,
"rows":[]
}

View File

@ -0,0 +1,6 @@
id = params_kw.id
async with get_sor_context(request._run_ns, 'pricing') as sor:
recs = await sor.R('pricing_spec', {'id': id})
if len(recs):
return recs.spec_names
return []

View File

@ -0,0 +1,189 @@
{
"id":"pricing_item_tbl",
"widgettype":"Tabular",
"options":{
"width":"100%",
"height":"100%",
"title":"项目定价项",
"css":"card",
"editable":{
"new_data_url":"{{entire_url('add_pricing_item.dspy')}}",
"delete_data_url":"{{entire_url('delete_pricing_item.dspy')}}",
"update_data_url":"{{entire_url('update_pricing_item.dspy')}}"
},
"data_url":"{{entire_url('./get_pricing_item.dspy')}}",
"data_method":"GET",
"data_params":{{json.dumps(params_kw, indent=4, ensure_ascii=False)}},
"row_options":{
"browserfields": {
"exclouded": [
"id",
"pptid"
],
"alters": {
"psid": {
"dataurl": "{{entire_url('../pi_get_all_specs.dspy')}}",
"textField": "name",
"valueField": "id",
"params": {
"pptid": "{{params_kw.pptid}}"
}
}
}
},
"editexclouded":[
"id",
"pptid"
],
"fields":[
{
"name": "id",
"title": "id",
"type": "str",
"length": 32,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "id"
},
{
"name": "name",
"title": "定价项名",
"type": "str",
"length": 256,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "定价项名"
},
{
"name": "description",
"title": "描述",
"type": "text",
"length": 0,
"uitype": "text",
"datatype": "text",
"label": "描述"
},
{
"name": "pptid",
"title": "项目id",
"type": "str",
"length": 32,
"label": "项目id",
"uitype": "code",
"valueField": "pptid",
"textField": "pptid_text",
"params": {
"dbname": "{{get_module_dbname('pricing')}}",
"table": "pricing_program_timing",
"tblvalue": "id",
"tbltext": "id",
"valueField": "pptid",
"textField": "pptid_text"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
},
{
"name": "psid",
"title": "定价规格id",
"type": "str",
"length": 32,
"label": "定价规格id",
"uitype": "code",
"valueField": "id",
"textField": "name",
"params": {
"pptid": "{{params_kw.pptid}}"
},
"dataurl": "{{entire_url('../pi_get_all_specs.dspy')}}"
},
{
"name": "subppid",
"title": "子项目id",
"type": "str",
"length": 32,
"nullable": "yes",
"label": "子项目id",
"uitype": "code",
"valueField": "subppid",
"textField": "subppid_text",
"params": {
"dbname": "{{get_module_dbname('pricing')}}",
"table": "pricing_program",
"tblvalue": "id",
"tbltext": "name",
"valueField": "subppid",
"textField": "subppid_text"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
},
{
"name": "pricing_unit",
"title": "定价单位",
"type": "float",
"length": 18,
"dec": 5,
"cwidth": 18,
"uitype": "float",
"datatype": "float",
"label": "定价单位"
},
{
"name": "pricing_amount",
"title": "定价金额",
"type": "float",
"length": 18,
"dec": 5,
"cwidth": 18,
"uitype": "float",
"datatype": "float",
"label": "定价金额"
},
{
"name": "cost_amount",
"title": "成本金额",
"type": "float",
"length": 18,
"dec": 5,
"cwidth": 18,
"uitype": "float",
"datatype": "float",
"label": "成本金额"
}
{{get_all_spec_fields_by_pptid(request, params_kw.pptid)}}
]
},
"page_rows":160,
"cache_limit":5
}
,"binds":[
{
"wid":"psid",
"event":"changed",
"actiontype":"script",
"target":"self",
"script":"this.hide_field(params.psid);"
}
]
}

View File

@ -7,13 +7,21 @@ for k,v in ns.items():
db = DBPools()
dbname = get_module_dbname('pricing')
async with db.sqlorContext(dbname) as sor:
r = await sor.U('pricing_program_timing', ns)
debug('update success');
ori = await sor.R('pricing_item', {'id': ns.id})
ori_sv = {}
if ori.spec_value:
ori_sv = json.loads(ori.spec_value)
fs = await sor_get_spec_fields(sor, ns.id)
if len(fs):
new_sv = {k:ns[k] for k in fs}
if new_sv:
ori_sv.update(new_sv)
ns.spec_value = json.dumps(ori_sv, indent=4)
r = await sor.U('pricing_item', ns.copy())
debug('update success, {ns=}');
return {
"widgettype":"Message",
"options":{

View File

@ -1,99 +0,0 @@
ns = params_kw.copy()
for k,v in ns.items():
if v == 'NaN' or v == 'null':
ns[k] = None
id = params_kw.id
if not id or len(id) > 32:
id = uuid()
ns['id'] = id
userorgid = await get_userorgid()
if not userorgid:
return {
"widgettype":"Error",
"options":{
"title":"Authorization Error",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"Please login"
}
}
ns['ownerid'] = userorgid
_validation_rules = json.loads(r'''{"discount": [{"type": "required", "message": "折扣不能为空"}, {"type": "number", "message": "折扣必须是数字"}, {"type": "min", "value": 0, "message": "折扣不能小于0"}, {"type": "max", "value": 1, "message": "折扣不能大于1"}]}''')
import re as _re
_errors = []
for _fname, _rules in _validation_rules.items():
_val = params_kw.get(_fname, '')
if _val is None: _val = ''
_val = str(_val)
for _rule in _rules:
_rt = _rule.get('type', '')
_rm = _rule.get('message', _fname)
_rv = _rule.get('value')
if _rt == 'required':
if not _val or _val.strip() == '':
_errors.append(_rm)
break
elif _rt == 'minlength':
if _val and len(_val) < int(_rv):
_errors.append(_rm)
break
elif _rt == 'maxlength':
if len(_val) > int(_rv):
_errors.append(_rm)
break
elif _rt in ('min', 'max'):
if _val:
try:
_n = float(_val)
if _rt == 'min' and _n < float(_rv): _errors.append(_rm); break
if _rt == 'max' and _n > float(_rv): _errors.append(_rm); break
except (ValueError, TypeError):
_errors.append(_rm)
break
elif _rt == 'pattern':
if _val and not _re.match(_rv, _val):
_errors.append(_rm)
break
elif _rt == 'email':
if _val and not _re.match(r'^[^\s@]+@[^\s@]+\.[^\s@]+$', _val):
_errors.append(_rm)
break
elif _rt == 'number':
if _val:
try: float(_val)
except (ValueError, TypeError): _errors.append(_rm); break
if _errors:
return {"widgettype":"Error","options":{"title":"Validation Failed","cwidth":16,"cheight":9,"timeout":3,"message":"; ".join(_errors)}}
db = DBPools()
dbname = get_module_dbname('pricing')
async with db.sqlorContext(dbname) as sor:
r = await sor.C('pricing_program', ns.copy())
return {
"widgettype":"Message",
"options":{
"cwidth":16,
"cheight":9,
"title":"Add Success",
"timeout":3,
"message":"ok"
}
}
return {
"widgettype":"Error",
"options":{
"title":"Add Error",
"cwidth":16,
"cheight":9,
"timeout":3,
"message":"failed"
}
}

View File

@ -1,47 +0,0 @@
ns = {
'id':params_kw['id'],
}
userorgid = await get_userorgid()
if not userorgid:
return {
"widgettype":"Error",
"options":{
"title":"Authorization Error",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"Please login"
}
}
ns['ownerid'] = userorgid
db = DBPools()
dbname = get_module_dbname('pricing')
async with db.sqlorContext(dbname) as sor:
r = await sor.D('pricing_program', ns)
debug('delete success');
return {
"widgettype":"Message",
"options":{
"title":"Delete Success",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"ok"
}
}
debug('Delete failed');
return {
"widgettype":"Error",
"options":{
"title":"Delete Error",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"failed"
}
}

View File

@ -1,135 +0,0 @@
ns = params_kw.copy()
userorgid = await get_userorgid()
if not userorgid:
return {
"widgettype":"Error",
"options":{
"title":"Authorization Error",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"Please login"
}
}
ns['ownerid'] = userorgid
ns['userorgid'] = userorgid
debug(f'get_pricing_program.dspy:{ns=}')
if not ns.get('page'):
ns['page'] = 1
if not ns.get('sort'):
ns['sort'] = 'name'
sql = '''select a.*, b.ownerid_text, c.providerid_text, d.pricing_belong_text
from (select * from pricing_program where 1=1 [[filterstr]]) a left join (select id as ownerid,
orgname as ownerid_text from organization where 1 = 1) b on a.ownerid = b.ownerid left join (select id as providerid,
orgname as providerid_text from organization where 1 = 1) c on a.providerid = c.providerid left join (select k as pricing_belong,
v as pricing_belong_text from appcodes_kv where parentid='pricing_belong') d on a.pricing_belong = d.pricing_belong'''
filterjson = params_kw.get('data_filter')
if filterjson and isinstance(filterjson, str):
try:
filterjson = json.loads(filterjson)
except (json.JSONDecodeError, TypeError):
filterjson = None
# data_filter可能是CRUD字段定义({"fields":[...]}),不是过滤条件,忽略
if filterjson and isinstance(filterjson, dict) and 'fields' in filterjson:
filterjson = None
fields_str=r'''[
{
"name": "id",
"title": "id",
"type": "str",
"length": 32
},
{
"name": "name",
"title": "项目名称",
"type": "str",
"length": 256
},
{
"name": "ownerid",
"title": "所属机构",
"type": "str",
"length": 32
},
{
"name": "providerid",
"title": "供应商",
"type": "str",
"length": 32
},
{
"name": "pricing_belong",
"title": "定价属于",
"type": "str",
"length": 32
},
{
"name": "discount",
"title": "供应商折扣",
"type": "float",
"length": 18,
"dec": 2
},
{
"name": "description",
"title": "描述",
"type": "text"
},
{
"name": "pricing_spec",
"title": "规格明细",
"type": "text"
}
]'''
ori_fields = json.loads(fields_str)
if not filterjson:
fields = [ f['name'] for f in ori_fields ]
filterjson = default_filterjson(fields, ns)
# 确保 logined 过滤条件始终生效
if filterjson:
if not isinstance(filterjson, dict) or 'AND' not in filterjson:
filterjson = {'AND': [filterjson] if filterjson else []}
filterjson['AND'].append({'field': 'ownerid', 'op': '=', 'var': '__logined_orgid__'})
ns['__logined_orgid__'] = userorgid
filterdic = ns.copy()
filterdic['filterstr'] = ''
filterdic['userorgid'] = '${userorgid}$'
filterdic['userid'] = '${userid}$'
if filterjson:
dbf = DBFilter(filterjson)
conds = dbf.gen(ns)
if conds:
ns.update(dbf.consts)
conds = f' and {conds}'
filterdic['filterstr'] = conds
ac = ArgsConvert('[[', ']]')
vars = ac.findAllVariables(sql)
NameSpace = {v:'${' + v + '}$' for v in vars if v != 'filterstr' }
filterdic.update(NameSpace)
sql = ac.convert(sql, filterdic)
debug(f'{sql=}')
db = DBPools()
dbname = get_module_dbname('pricing')
async with db.sqlorContext(dbname) as sor:
r = await sor.sqlPaging(sql, ns)
return r
return {
"total":0,
"rows":[]
}

View File

@ -1,289 +0,0 @@
{
"widgettype":"VBox",
"options":{"cheight":40,"width":"100%"},
"subwidgets":[{
"id":"pricing_program_tbl",
"widgettype":"Tabular",
"options":{
"width":"100%",
"height":"100%",
"title":"定价项目",
"toolbar":{
"tools": [
{
"name": "test",
"label": "测试",
"selected_row": true,
"icon": "{{entire_url('/bricks/imgs/test.svg')}}"
},
{
"selected_row": true,
"name": "pricing_program_timing",
"icon": "{{entire_url('/imgs/pricing_program_timing.svg')}}",
"label": "定价项目时序"
}
]
},
"css":"card",
"editable":{
"new_data_url":"{{entire_url('add_pricing_program.dspy')}}",
"delete_data_url":"{{entire_url('delete_pricing_program.dspy')}}",
"update_data_url":"{{entire_url('update_pricing_program.dspy')}}"
},
"data_url":"{{entire_url('./get_pricing_program.dspy')}}",
"data_method":"GET",
"data_params":{{json.dumps(params_kw, indent=4, ensure_ascii=False)}},
"row_options":{
"browserfields": {
"exclouded": [
"id",
"ownerid",
"pricing_spec"
],
"alters": {
"providerid": {
"valueField": "id",
"textField": "orgname",
"dataurl": "{{entire_url('/rbac/get_provider.dspy')}}"
},
"discount": {
"rules": [
{
"type": "required",
"message": "折扣不能为空"
},
{
"type": "number",
"message": "折扣必须是数字"
},
{
"type": "min",
"value": 0,
"message": "折扣不能小于0"
},
{
"type": "max",
"value": 1,
"message": "折扣不能大于1"
}
]
}
}
},
"editexclouded":[
"id",
"ownerid"
],
"fields":[
{
"name": "id",
"title": "id",
"type": "str",
"length": 32,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "id"
},
{
"name": "name",
"title": "项目名称",
"type": "str",
"length": 256,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "项目名称"
},
{
"name": "ownerid",
"title": "所属机构",
"type": "str",
"length": 32,
"label": "所属机构",
"uitype": "code",
"valueField": "ownerid",
"textField": "ownerid_text",
"params": {
"dbname": "{{get_module_dbname('pricing')}}",
"table": "organization",
"tblvalue": "id",
"tbltext": "orgname",
"valueField": "ownerid",
"textField": "ownerid_text"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
},
{
"name": "providerid",
"title": "供应商",
"type": "str",
"length": 32,
"label": "供应商",
"uitype": "code",
"valueField": "id",
"textField": "orgname",
"params": {
"dbname": "{{get_module_dbname('pricing')}}",
"table": "organization",
"tblvalue": "id",
"tbltext": "orgname",
"valueField": "providerid",
"textField": "providerid_text"
},
"dataurl": "{{entire_url('/rbac/get_provider.dspy')}}"
},
{
"name": "pricing_belong",
"title": "定价属于",
"type": "str",
"length": 32,
"label": "定价属于",
"uitype": "code",
"valueField": "pricing_belong",
"textField": "pricing_belong_text",
"params": {
"dbname": "{{get_module_dbname('pricing')}}",
"table": "appcodes_kv",
"tblvalue": "k",
"tbltext": "v",
"valueField": "pricing_belong",
"textField": "pricing_belong_text",
"cond": "parentid='pricing_belong'"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
},
{
"name": "discount",
"title": "供应商折扣",
"type": "float",
"length": 18,
"dec": 2,
"cwidth": 18,
"uitype": "float",
"datatype": "float",
"label": "供应商折扣",
"rules": [
{
"type": "required",
"message": "折扣不能为空"
},
{
"type": "number",
"message": "折扣必须是数字"
},
{
"type": "min",
"value": 0,
"message": "折扣不能小于0"
},
{
"type": "max",
"value": 1,
"message": "折扣不能大于1"
}
]
},
{
"name": "description",
"title": "描述",
"type": "text",
"length": 0,
"uitype": "text",
"datatype": "text",
"label": "描述"
},
{
"name": "pricing_spec",
"title": "规格明细",
"type": "text",
"length": 0,
"uitype": "text",
"datatype": "text",
"label": "规格明细"
}
]
},
"page_rows":160,
"cache_limit":5
}
,"binds":[
{
"wid": "self",
"event": "test",
"actiontype": "urlwidget",
"target": "PopupWindow",
"popup_options": {
"width": "70%",
"height": "70%",
"auto_open": true,
"archor": "cc",
"title": "定价测试"
},
"options": {
"url": "{{entire_url('../test_pricing_program.ui')}}",
"params": {}
}
},
{
"wid": "self",
"event": "pricing_program_timing",
"actiontype": "urlwidget",
"target": "PopupWindow",
"popup_options": {
"title": "定价项目时序",
"icon": "{{entire_url('/appbase/get_icon.dspy')}}?id=pricing_program_timing",
"resizable": true,
"height": "70%",
"width": "70%"
},
"params_mapping": {
"mapping": {
"id": "ppid",
"referer_widget": "referer_widget"
},
"need_other": false
},
"options": {
"method": "POST",
"params": {},
"url": "{{entire_url('../pricing_program_timing')}}"
}
}
]
}]
}

View File

@ -1,118 +0,0 @@
ns = params_kw.copy()
for k,v in ns.items():
if v == 'NaN' or v == 'null':
ns[k] = None
userorgid = await get_userorgid()
if not userorgid:
return {
"widgettype":"Error",
"options":{
"title":"Authorization Error",
"timeout":3,
"cwidth":16,
"cheight":9,
"message":"Please login"
}
}
ns['ownerid'] = userorgid
_validation_rules = json.loads(r'''{"discount": [{"type": "required", "message": "折扣不能为空"}, {"type": "number", "message": "折扣必须是数字"}, {"type": "min", "value": 0, "message": "折扣不能小于0"}, {"type": "max", "value": 1, "message": "折扣不能大于1"}]}''')
import re as _re
_errors = []
for _fname, _rules in _validation_rules.items():
_val = params_kw.get(_fname, '')
if _val is None: _val = ''
_val = str(_val)
for _rule in _rules:
_rt = _rule.get('type', '')
_rm = _rule.get('message', _fname)
_rv = _rule.get('value')
if _rt == 'required':
if not _val or _val.strip() == '':
_errors.append(_rm)
break
elif _rt == 'minlength':
if _val and len(_val) < int(_rv):
_errors.append(_rm)
break
elif _rt == 'maxlength':
if len(_val) > int(_rv):
_errors.append(_rm)
break
elif _rt in ('min', 'max'):
if _val:
try:
_n = float(_val)
if _rt == 'min' and _n < float(_rv): _errors.append(_rm); break
if _rt == 'max' and _n > float(_rv): _errors.append(_rm); break
except (ValueError, TypeError):
_errors.append(_rm)
break
elif _rt == 'pattern':
if _val and not _re.match(_rv, _val):
_errors.append(_rm)
break
elif _rt == 'email':
if _val and not _re.match(r'^[^\s@]+@[^\s@]+\.[^\s@]+$', _val):
_errors.append(_rm)
break
elif _rt == 'number':
if _val:
try: float(_val)
except (ValueError, TypeError): _errors.append(_rm); break
if _errors:
return {"widgettype":"Error","options":{"title":"Validation Failed","cwidth":16,"cheight":9,"timeout":3,"message":"; ".join(_errors)}}
db = DBPools()
dbname = get_module_dbname('pricing')
async with db.sqlorContext(dbname) as sor:
ns1 = {
"ownerid": userorgid,
"id": params_kw.id
}
recs = await sor.R('pricing_program', ns1)
if len(recs) < 1:
return {
"widgettype":"Error",
"options":{
"title":"Update Error",
"cwidth":16,
"cheight":9,
"timeout":3,
"message":"Record no exist or with wrong ownership"
}
}
r = await sor.U('pricing_program', ns)
debug('update success');
return {
"widgettype":"Message",
"options":{
"title":"Update Success",
"cwidth":16,
"cheight":9,
"timeout":3,
"message":"ok"
}
}
return {
"widgettype":"Error",
"options":{
"title":"Update Error",
"cwidth":16,
"cheight":9,
"timeout":3,
"message":"failed"
}
}

View File

@ -1,93 +0,0 @@
ns = params_kw.copy()
debug(f'get_pricing_program_timing.dspy:{ns=}')
if not ns.get('page'):
ns['page'] = 1
if not ns.get('sort'):
ns['sort'] = 'id'
sql = '''select a.*, b.ppid_text
from (select * from pricing_program_timing where 1=1 [[filterstr]]) a left join (select id as ppid,
name as ppid_text from pricing_program where 1 = 1) b on a.ppid = b.ppid'''
filterjson = params_kw.get('data_filter')
if filterjson and isinstance(filterjson, str):
try:
filterjson = json.loads(filterjson)
except (json.JSONDecodeError, TypeError):
filterjson = None
# data_filter可能是CRUD字段定义({"fields":[...]}),不是过滤条件,忽略
if filterjson and isinstance(filterjson, dict) and 'fields' in filterjson:
filterjson = None
fields_str=r'''[
{
"name": "id",
"title": "id",
"type": "str",
"length": 32
},
{
"name": "ppid",
"title": "定价项目id",
"type": "str",
"length": 32
},
{
"name": "name",
"title": "名称",
"type": "str",
"length": 256
},
{
"name": "pricing_data",
"title": "定价数据",
"type": "text"
},
{
"name": "enabled_date",
"title": "启用日期",
"type": "date"
},
{
"name": "expired_date",
"title": "失效日期",
"type": "date",
"default": "9999-12-31"
}
]'''
ori_fields = json.loads(fields_str)
if not filterjson:
fields = [ f['name'] for f in ori_fields ]
filterjson = default_filterjson(fields, ns)
filterdic = ns.copy()
filterdic['filterstr'] = ''
filterdic['userorgid'] = '${userorgid}$'
filterdic['userid'] = '${userid}$'
if filterjson:
dbf = DBFilter(filterjson)
conds = dbf.gen(ns)
if conds:
ns.update(dbf.consts)
conds = f' and {conds}'
filterdic['filterstr'] = conds
ac = ArgsConvert('[[', ']]')
vars = ac.findAllVariables(sql)
NameSpace = {v:'${' + v + '}$' for v in vars if v != 'filterstr' }
filterdic.update(NameSpace)
sql = ac.convert(sql, filterdic)
debug(f'{sql=}')
db = DBPools()
dbname = get_module_dbname('pricing')
async with db.sqlorContext(dbname) as sor:
r = await sor.sqlPaging(sql, ns)
return r
return {
"total":0,
"rows":[]
}

View File

@ -1,260 +0,0 @@
{
"widgettype":"VBox",
"options":{"cheight":40,"width":"100%"},
"subwidgets":[{
"id":"pricing_program_timing_tbl",
"widgettype":"Tabular",
"options":{
"width":"100%",
"height":"100%",
"title":"定价项目时序",
"toolbar":{
"tools": [
{
"name": "download_pattern",
"label": "定价模版",
"selected_row": true,
"icon": "{{entire_url('/bricks/imgs/download.svg')}}"
},
{
"name": "upload_pricing_data",
"label": "上传定价数据",
"selected_row": true,
"icon": "{{entire_url('/bricks/imgs/upload.svg')}}"
},
{
"name": "download_pricing_data",
"label": "下载定价数据",
"selected_row": true,
"icon": "{{entire_url('/bricks/imgs/download.svg')}}"
},
{
"name": "test",
"selected_row": true,
"label": "验证定价",
"icon": "{{entire_url('/bricks/imgs/test.svg')}}"
},
{
"selected_row": true,
"name": "pricing_item",
"icon": "{{entire_url('/imgs/pricing_item.svg')}}",
"label": "定价细项"
}
]
},
"css":"card",
"editable":{
"new_data_url":"{{entire_url('add_pricing_program_timing.dspy')}}",
"delete_data_url":"{{entire_url('delete_pricing_program_timing.dspy')}}",
"update_data_url":"{{entire_url('update_pricing_program_timing.dspy')}}"
},
"data_url":"{{entire_url('./get_pricing_program_timing.dspy')}}",
"data_method":"GET",
"data_params":{{json.dumps(params_kw, indent=4, ensure_ascii=False)}},
"row_options":{
"browserfields": {
"exclouded": [
"id",
"ppid"
],
"alters": {}
},
"editexclouded":[
"id",
"ppid",
"name"
],
"fields":[
{
"name": "id",
"title": "id",
"type": "str",
"length": 32,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "id"
},
{
"name": "ppid",
"title": "定价项目id",
"type": "str",
"length": 32,
"label": "定价项目id",
"uitype": "code",
"valueField": "ppid",
"textField": "ppid_text",
"params": {
"dbname": "{{get_module_dbname('pricing')}}",
"table": "pricing_program",
"tblvalue": "id",
"tbltext": "name",
"valueField": "ppid",
"textField": "ppid_text"
},
"dataurl": "{{entire_url('/appbase/get_code.dspy')}}"
},
{
"name": "name",
"title": "名称",
"type": "str",
"length": 256,
"cwidth": 18,
"uitype": "str",
"datatype": "str",
"label": "名称"
},
{
"name": "pricing_data",
"title": "定价数据",
"type": "text",
"length": 0,
"uitype": "text",
"datatype": "text",
"label": "定价数据"
},
{
"name": "enabled_date",
"title": "启用日期",
"type": "date",
"length": 0,
"uitype": "date",
"datatype": "date",
"label": "启用日期"
},
{
"name": "expired_date",
"title": "失效日期",
"type": "date",
"default": "9999-12-31",
"length": 0,
"uitype": "date",
"datatype": "date",
"label": "失效日期"
}
]
},
"page_rows":160,
"cache_limit":5
}
,"binds":[
{
"wid": "self",
"event": "download_pattern",
"actiontype": "newwindow",
"target": "self",
"options": {
"params": {
"ppid": "{{params_kw.ppid}}"
},
"method": "POST",
"url": "{{entire_url('../download_pricing_pattern.dspy')}}"
}
},
{
"wid": "self",
"event": "upload_pricing_data",
"actiontype": "urlwidget",
"target": "PopupWindow",
"popup_options": {
"title": "上传定价数据"
},
"options": {
"params": {
"ppid": "{{params_kw.ppid}}"
},
"method": "POST",
"url": "{{entire_url('../load_pricing_data.ui')}}"
}
},
{
"wid": "self",
"event": "download_pricing_data",
"actiontype": "newwindow",
"target": "self",
"options": {
"params": {
"id": "{{params_kw.id}}"
},
"method": "POST",
"url": "{{entire_url('../download_pricing_data.dspy')}}"
}
},
{
"wid": "self",
"event": "test",
"actiontype": "urlwidget",
"target": "PopupWindow",
"popup_options": {
"title": "验证定价"
},
"options": {
"params": {
"id": "{{params_kw.id}}"
},
"method": "POST",
"url": "{{entire_url('../pricing_test.ui')}}"
}
},
{
"wid": "self",
"event": "pricing_item",
"actiontype": "urlwidget",
"target": "PopupWindow",
"popup_options": {
"title": "定价细项",
"icon": "{{entire_url('/appbase/get_icon.dspy')}}?id=pricing_item",
"resizable": true,
"height": "70%",
"width": "70%"
},
"params_mapping": {
"mapping": {
"id": "pptid",
"referer_widget": "referer_widget"
},
"need_other": false
},
"options": {
"method": "POST",
"params": {},
"url": "{{entire_url('../pricing_item')}}"
}
}
]
}]
}

View File

@ -4,26 +4,19 @@ try:
if isinstance(data, str):
data = json.loads(data)
x = await buffered_charging(ppid, data)
subwidgets = []
if isinstance(x, list):
for r in x:
subwidgets.append({
"widgettype": "Text",
"options": {"text": json.dumps(r, ensure_ascii=False), "width": "100%"}
})
else:
subwidgets.append({
"widgettype": "Text",
"options": {"text": json.dumps(x, ensure_ascii=False), "width": "100%"}
})
return json.dumps({
"widgettype": "VScrollPanel",
"options": {"width": "100%", "height": "100%"},
"subwidgets": subwidgets
}, ensure_ascii=False)
return {
"status": "ok",
"data": {
"ppid": ppid,
"data": data,
"result": x
}
}
except Exception as e:
exception(f'{ppid=}, {data=}, {e}{format_exc()}')
return json.dumps({
"widgettype": "Text",
"options": {"width": "100%", "color": "red", "text": f'{ppid=}, {data=}, {e}'}
}, ensure_ascii=False)
return {
"status": "error",
"data": {
"message": f'{ppid=}, {data=}, {e}'
}
}

View File

@ -29,7 +29,7 @@
}
},
{
"widgettype": "VScrollPanel",
"widgettype": "VBox",
"id": "result",
"options": {
"height": "50%",
@ -41,11 +41,28 @@
{
"wid": "input_form",
"event": "submit",
"actiontype": "urlwidget",
"actiontype": "urldata",
"target": "result",
"options": {
"url": "{{entire_url('./test_pricing_program.dspy')}}",
"params":{}
},
"status_of": {
"ok": {
"widgettype": "Text",
"options":{
"width": "100%",
"text": "${result}"
}
},
"error": {
"widgettype": "Text",
"options":{
"width": "100%",
"color": "red",
"text": "${message}"
}
}
}
}
]