@dazitech/cli 3.0.7 → 3.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +1 -1
  2. package/dist/clis/dazi-app.js +1 -1
  3. package/dist/clis/dazi-flow.js +1 -1
  4. package/dist/clis/dazi-onto.js +73 -22
  5. package/dist/clis/dazi.js +266 -171
  6. package/dist/docs/flow/flow-project-guide.md +1 -1
  7. package/dist/docs/guides/quickstart.md +18 -4
  8. package/dist/docs/guides/troubleshooting.md +1 -1
  9. package/dist/docs/guides/workspace-v3.md +43 -23
  10. package/dist/docs/index.json +9 -3
  11. package/dist/docs/onto/action-guide.md +3 -3
  12. package/dist/docs/onto/dazi_script_sdk_reference.md +178 -174
  13. package/dist/docs/onto/dazi_script_seed_data_guide.md +158 -155
  14. package/dist/docs/onto/function-guide.md +37 -10
  15. package/dist/docs/onto/space-management.md +3 -1
  16. package/dist/docs/onto//346/234/254/344/275/223/350/204/232/346/234/254/347/274/226/345/206/231/346/214/207/345/215/227.md +138 -34
  17. package/dist/docs/onto//346/234/254/344/275/223/350/247/204/345/210/222/346/214/207/345/215/227.md +73 -31
  18. package/dist/docs/onto//350/247/204/345/210/222/347/244/272/344/276/213_/344/272/247/345/223/201/351/224/200/345/224/256/346/234/254/344/275/223/350/247/204/345/210/222/346/226/271/346/241/210.md +497 -0
  19. package/dist/docs/onto//350/247/204/345/210/222/347/244/272/344/276/213_/345/210/251/346/266/246/345/210/206/346/236/220/346/234/254/344/275/223/346/226/271/346/241/210.md +597 -541
  20. package/dist/examples/index.json +202 -22
  21. package/dist/examples/onto/README.md +43 -0
  22. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/function/profit_fn_account_breakdown.py +99 -0
  23. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/function/profit_fn_budget_vs_actual.py +116 -0
  24. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/function/profit_fn_cost_center_profit.py +85 -0
  25. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/function/profit_fn_get_summary.py +76 -0
  26. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/function/profit_fn_mom_analysis.py +86 -0
  27. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/function/profit_fn_top_accounts.py +103 -0
  28. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/function/profit_fn_yoy_analysis.py +86 -0
  29. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/function/save_test_arguments.ps1 +27 -0
  30. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/function/test_arguments/profit.fn.account_breakdown.json +10 -0
  31. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/function/test_arguments/profit.fn.budget_vs_actual.json +10 -0
  32. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/function/test_arguments/profit.fn.cost_center_profit.json +9 -0
  33. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/function/test_arguments/profit.fn.get_summary.json +9 -0
  34. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/function/test_arguments/profit.fn.mom_analysis.json +9 -0
  35. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/function/test_arguments/profit.fn.top_accounts.json +11 -0
  36. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/function/test_arguments/profit.fn.yoy_analysis.json +9 -0
  37. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/setup/profit_ontology_init.py +521 -0
  38. package/dist/examples/onto//345/210/251/346/266/246/347/244/272/344/276/213/setup/profit_seed_data.py +213 -0
  39. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/README.md +25 -0
  40. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/sales_fn_channel_mix.py +86 -0
  41. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/sales_fn_customer_segmentation.py +123 -0
  42. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/sales_fn_get_summary.py +81 -0
  43. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/sales_fn_mom_analysis.py +90 -0
  44. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/sales_fn_region_breakdown.py +85 -0
  45. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/sales_fn_top_products.py +101 -0
  46. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/sales_fn_yoy_analysis.py +90 -0
  47. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/save_test_arguments.ps1 +25 -0
  48. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/test_arguments/sales.fn.channel_mix.json +8 -0
  49. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/test_arguments/sales.fn.customer_segmentation.json +10 -0
  50. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/test_arguments/sales.fn.get_summary.json +8 -0
  51. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/test_arguments/sales.fn.mom_analysis.json +8 -0
  52. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/test_arguments/sales.fn.region_breakdown.json +8 -0
  53. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/test_arguments/sales.fn.top_products.json +10 -0
  54. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/functions/test_arguments/sales.fn.yoy_analysis.json +8 -0
  55. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/setup/README.md +5 -0
  56. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/setup/sales_ontology_init.py +403 -0
  57. package/dist/examples/onto//351/224/200/345/224/256/347/244/272/344/276/213/setup/sales_seed_data.py +124 -0
  58. package/dist/prompts/index.json +2 -2
  59. package/dist/prompts/onto/action-design.md +4 -1
  60. package/dist/prompts/onto/function-design.md +9 -2
  61. package/dist/prompts/onto/rule-seed.md +5 -1
  62. package/dist/prompts/onto/script-publish-run.md +72 -24
  63. package/package.json +1 -1
  64. package/dist/examples/onto/function/profit_fn_customer_segmentation.py +0 -117
  65. package/dist/examples/onto/function/profit_fn_mom_analysis.py +0 -89
  66. package/dist/examples/onto/function/profit_fn_top_products.py +0 -89
  67. package/dist/examples/onto/function/profit_fn_yoy_analysis.py +0 -89
  68. package/dist/examples/onto/setup/profit_ontology_init.py +0 -388
@@ -0,0 +1,213 @@
1
+ """利润分析演示数据灌入 — space__zlj
2
+
3
+ 前置:先执行 profit_ontology_init.py 建表。
4
+ 幂等:actual_journal_entry 已有数据则跳过。
5
+
6
+ 放置:项目/潘达石化/本体/ontos/利润分析本体方案/setup/profit_seed_data.py
7
+ 发布:dazi onto script publish 项目/潘达石化/本体/ontos/利润分析本体方案/setup/profit_seed_data.py --space space__zlj --type data
8
+ """
9
+
10
+ import json
11
+ import random
12
+ from datetime import date, datetime, timedelta
13
+
14
+ _SEED_DT = datetime(2025, 1, 1, 0, 0, 0)
15
+ _BUDGET_VERSION = "2026年度预算"
16
+
17
+
18
+ def _calc_signed(account_type, debit, credit):
19
+ if account_type == "收入":
20
+ return round(credit - debit, 2)
21
+ if account_type in ("成本", "费用"):
22
+ return round(debit - credit, 2)
23
+ return round(debit - credit, 2)
24
+
25
+
26
+ def _make_entry_line(seq, posting_date, account, cc, amount):
27
+ account_type = account["account_type"]
28
+ debit = amount if account_type in ("成本", "费用") else 0.0
29
+ credit = amount if account_type == "收入" else 0.0
30
+ if account_type == "收入":
31
+ debit, credit = 0.0, amount
32
+ signed = _calc_signed(account_type, debit, credit)
33
+ fy = posting_date.year
34
+ fp = posting_date.month
35
+ entry_id = f"JE{posting_date.strftime('%Y%m%d')}{seq // 10:04d}"
36
+ line_id = f"JL{posting_date.strftime('%Y%m%d')}{seq:05d}"
37
+ return {
38
+ "entry_id": entry_id,
39
+ "line_id": line_id,
40
+ "posting_date": posting_date,
41
+ "fiscal_year": fy,
42
+ "fiscal_period": fp,
43
+ "account_id": account["account_id"],
44
+ "account_code": account["account_code"],
45
+ "account_name": account["account_name"],
46
+ "account_type": account["account_type"],
47
+ "pl_category": account["pl_category"],
48
+ "account_level": account["account_level"],
49
+ "cost_center_id": cc["cost_center_id"],
50
+ "cost_center_name": cc["cost_center_name"],
51
+ "department": cc["department"],
52
+ "profit_center": cc["profit_center"],
53
+ "debit_amount": round(debit, 2),
54
+ "credit_amount": round(credit, 2),
55
+ "amount_signed": signed,
56
+ "currency": "CNY",
57
+ "voucher_no": f"V{posting_date.strftime('%Y%m')}{seq:04d}",
58
+ "source_system": "GL",
59
+ "description": f"{account['account_name']}-{cc['cost_center_name']}",
60
+ "created_at": datetime.combine(posting_date, datetime.min.time()),
61
+ }
62
+
63
+
64
+ def main():
65
+ space_id = "space__zlj"
66
+ s = space.get(space_id)
67
+
68
+ output.print("=== 利润分析演示数据灌入 ===")
69
+
70
+ try:
71
+ n = int(s.sql.query_one("SELECT count() FROM actual_journal_entry") or 0)
72
+ except Exception:
73
+ n = 0
74
+ if n > 0:
75
+ output.print(f"actual_journal_entry 已有 {n} 行,跳过灌数")
76
+ output.print("__JSON_SUMMARY__" + json.dumps({"ok": True, "skipped": True, "rows": n}, ensure_ascii=True))
77
+ return
78
+
79
+ accounts = [
80
+ {"account_id": "ACC6000", "account_code": "6000", "account_name": "营业收入", "account_type": "收入", "pl_category": "营业收入", "parent_account_id": "", "account_level": 1, "is_leaf": False, "normal_balance": "贷", "status": "启用"},
81
+ {"account_id": "ACC6001", "account_code": "6001", "account_name": "主营业务收入", "account_type": "收入", "pl_category": "营业收入", "parent_account_id": "ACC6000", "account_level": 2, "is_leaf": True, "normal_balance": "贷", "status": "启用"},
82
+ {"account_id": "ACC6050", "account_code": "6050", "account_name": "其他业务收入", "account_type": "收入", "pl_category": "营业收入", "parent_account_id": "ACC6000", "account_level": 2, "is_leaf": True, "normal_balance": "贷", "status": "启用"},
83
+ {"account_id": "ACC6400", "account_code": "6400", "account_name": "营业成本", "account_type": "成本", "pl_category": "营业成本", "parent_account_id": "", "account_level": 1, "is_leaf": False, "normal_balance": "借", "status": "启用"},
84
+ {"account_id": "ACC6401", "account_code": "6401", "account_name": "主营业务成本", "account_type": "成本", "pl_category": "营业成本", "parent_account_id": "ACC6400", "account_level": 2, "is_leaf": True, "normal_balance": "借", "status": "启用"},
85
+ {"account_id": "ACC6402", "account_code": "6402", "account_name": "原材料成本", "account_type": "成本", "pl_category": "营业成本", "parent_account_id": "ACC6400", "account_level": 2, "is_leaf": True, "normal_balance": "借", "status": "启用"},
86
+ {"account_id": "ACC6600", "account_code": "6600", "account_name": "期间费用", "account_type": "费用", "pl_category": "期间费用", "parent_account_id": "", "account_level": 1, "is_leaf": False, "normal_balance": "借", "status": "启用"},
87
+ {"account_id": "ACC6601", "account_code": "6601", "account_name": "销售费用", "account_type": "费用", "pl_category": "期间费用", "parent_account_id": "ACC6600", "account_level": 2, "is_leaf": True, "normal_balance": "借", "status": "启用"},
88
+ {"account_id": "ACC6602", "account_code": "6602", "account_name": "管理费用", "account_type": "费用", "pl_category": "期间费用", "parent_account_id": "ACC6600", "account_level": 2, "is_leaf": True, "normal_balance": "借", "status": "启用"},
89
+ {"account_id": "ACC6603", "account_code": "6603", "account_name": "财务费用", "account_type": "费用", "pl_category": "期间费用", "parent_account_id": "ACC6600", "account_level": 2, "is_leaf": True, "normal_balance": "借", "status": "启用"},
90
+ ]
91
+ for a in accounts:
92
+ a["created_at"] = _SEED_DT
93
+
94
+ cost_centers = [
95
+ {"cost_center_id": "CC01", "cost_center_code": "PC-PROD", "cost_center_name": "生产中心", "department": "生产部", "company_code": "PD", "profit_center": "制造利润中心", "status": "启用"},
96
+ {"cost_center_id": "CC02", "cost_center_code": "PC-SALE", "cost_center_name": "销售中心", "department": "销售部", "company_code": "PD", "profit_center": "销售利润中心", "status": "启用"},
97
+ {"cost_center_id": "CC03", "cost_center_code": "PC-MGMT", "cost_center_name": "管理中心", "department": "管理部", "company_code": "PD", "profit_center": "管理利润中心", "status": "启用"},
98
+ {"cost_center_id": "CC04", "cost_center_code": "PC-RD", "cost_center_name": "研发中心", "department": "研发部", "company_code": "PD", "profit_center": "研发利润中心", "status": "启用"},
99
+ {"cost_center_id": "CC05", "cost_center_code": "PC-FIN", "cost_center_name": "财务中心", "department": "财务部", "company_code": "PD", "profit_center": "财务利润中心", "status": "启用"},
100
+ ]
101
+ for c in cost_centers:
102
+ c["created_at"] = _SEED_DT
103
+
104
+ s.sql.insert_rows("account_master", accounts)
105
+ s.sql.insert_rows("cost_center_dimension", cost_centers)
106
+ output.print("OK 维表数据")
107
+
108
+ leaf_accounts = [a for a in accounts if a["is_leaf"]]
109
+ acct_dict = {a["account_id"]: a for a in accounts}
110
+ cc_dict = {c["cost_center_id"]: c for c in cost_centers}
111
+
112
+ random.seed(9520)
113
+ fact_rows = []
114
+ seq = 1
115
+ start = date(2025, 1, 1)
116
+ end = date(2026, 6, 30)
117
+
118
+ # 科目基准金额(月)
119
+ base_amounts = {
120
+ "ACC6001": 800000,
121
+ "ACC6050": 50000,
122
+ "ACC6401": 480000,
123
+ "ACC6402": 120000,
124
+ "ACC6601": 80000,
125
+ "ACC6602": 60000,
126
+ "ACC6603": 20000,
127
+ }
128
+
129
+ d = start
130
+ while d <= end:
131
+ if d.day <= 3:
132
+ for acc_id, base in base_amounts.items():
133
+ account = acct_dict[acc_id]
134
+ for cc in cost_centers:
135
+ if account["account_type"] == "收入" and cc["cost_center_id"] not in ("CC01", "CC02"):
136
+ continue
137
+ if account["account_type"] == "成本" and cc["cost_center_id"] not in ("CC01", "CC04"):
138
+ continue
139
+ if account["account_type"] == "费用":
140
+ if account["account_id"] == "ACC6601" and cc["cost_center_id"] != "CC02":
141
+ continue
142
+ if account["account_id"] == "ACC6602" and cc["cost_center_id"] not in ("CC03", "CC04"):
143
+ continue
144
+ if account["account_id"] == "ACC6603" and cc["cost_center_id"] != "CC05":
145
+ continue
146
+ jitter = random.uniform(0.85, 1.15)
147
+ seasonal = 1.0 + 0.1 * ((d.month - 1) % 3)
148
+ amount = round(base * jitter * seasonal / max(len(cost_centers), 1) / 3, 2)
149
+ if amount <= 0:
150
+ continue
151
+ fact_rows.append(_make_entry_line(seq, d, account, cc, amount))
152
+ seq += 1
153
+ d += timedelta(days=1)
154
+
155
+ inserted = s.sql.insert_rows("actual_journal_entry", fact_rows)
156
+ output.print(f"OK 实际分录表插入 {inserted} 行")
157
+
158
+ budget_rows = []
159
+ bseq = 1
160
+ for year in (2025, 2026):
161
+ version = _BUDGET_VERSION if year == 2026 else f"{year}年度预算"
162
+ for period in range(1, 13):
163
+ if year == 2026 and period > 6:
164
+ continue
165
+ for account in leaf_accounts:
166
+ base = base_amounts.get(account["account_id"], 50000)
167
+ for cc in cost_centers:
168
+ if account["account_type"] == "收入" and cc["cost_center_id"] not in ("CC01", "CC02"):
169
+ continue
170
+ if account["account_type"] == "成本" and cc["cost_center_id"] not in ("CC01", "CC04"):
171
+ continue
172
+ if account["account_type"] == "费用":
173
+ if account["account_id"] == "ACC6601" and cc["cost_center_id"] != "CC02":
174
+ continue
175
+ if account["account_id"] == "ACC6602" and cc["cost_center_id"] not in ("CC03", "CC04"):
176
+ continue
177
+ if account["account_id"] == "ACC6603" and cc["cost_center_id"] != "CC05":
178
+ continue
179
+ budget_amt = round(base / 6 * random.uniform(0.95, 1.05), 2)
180
+ budget_rows.append({
181
+ "budget_id": f"BUD{year}",
182
+ "line_id": f"BL{year}{period:02d}{bseq:05d}",
183
+ "budget_version": version,
184
+ "fiscal_year": year,
185
+ "fiscal_period": period,
186
+ "account_id": account["account_id"],
187
+ "account_code": account["account_code"],
188
+ "account_name": account["account_name"],
189
+ "account_type": account["account_type"],
190
+ "pl_category": account["pl_category"],
191
+ "cost_center_id": cc["cost_center_id"],
192
+ "cost_center_name": cc["cost_center_name"],
193
+ "department": cc["department"],
194
+ "budget_amount": budget_amt,
195
+ "currency": "CNY",
196
+ "status": "已发布",
197
+ "created_at": _SEED_DT,
198
+ })
199
+ bseq += 1
200
+
201
+ binserted = s.sql.insert_rows("budget_entry", budget_rows)
202
+ output.print(f"OK 预算表插入 {binserted} 行")
203
+
204
+ summary = {
205
+ "ok": True,
206
+ "space_id": space_id,
207
+ "accounts": len(accounts),
208
+ "cost_centers": len(cost_centers),
209
+ "fact_inserted": inserted,
210
+ "budget_inserted": binserted,
211
+ }
212
+ output.success("灌数完成")
213
+ output.print("__JSON_SUMMARY__" + json.dumps(summary, ensure_ascii=True, default=str))
@@ -0,0 +1,25 @@
1
+ # functions
2
+
3
+ 本体函数脚本。发布须带 `--register-function-id`,与 `plans/` 规划文档中的 function_id 一致。
4
+
5
+ ## 测试参数(test_arguments)
6
+
7
+ 函数测试完成后,须将默认入参写入平台函数定义,侧栏 **Onto → 运行函数** 才会预填参数。
8
+
9
+ 1. 每个函数对应 `test_arguments/<function_id>.json`(与脚本内 `TEST_ARGUMENTS` 保持一致)
10
+ 2. 在 **dazi-work 根**批量入库(推荐):
11
+
12
+ ```powershell
13
+ .\项目\潘达石化\本体\ontos\产品销售本体方案\functions\save_test_arguments.ps1
14
+ ```
15
+
16
+ 或手动单条(**须用平台内部 id `ofn_xxx`**,可先 `dazi onto function list --space space__zlj` 查看):
17
+
18
+ ```powershell
19
+ dazi onto function save-test-arguments ofn_156a399fbe0e4636 --space space__zlj `
20
+ --arguments-json-file 项目/潘达石化/本体/ontos/产品销售本体方案/functions/test_arguments/sales.fn.get_summary.json
21
+ ```
22
+
23
+ > CLI 的 `save-test-arguments` 当前 PATCH 路径使用内部 id,直接传 `sales.fn.get_summary` 会 404。
24
+
25
+ **注意**:`arguments` 字段中的键名须与函数 `ctx.params` 读取一致;日期范围与 `sales_seed_data.py` 演示数据一致(2025-01-01 ~ 2026-06-30)。
@@ -0,0 +1,86 @@
1
+ """渠道销售占比 sales.fn.channel_mix
2
+
3
+ 参数:start_date, end_date
4
+ 返回:各渠道销售额、订单数及占比
5
+
6
+ 发布:
7
+ dazi onto script publish 项目/潘达石化/本体/ontos/产品销售本体方案/functions/sales_fn_channel_mix.py \\
8
+ --space space__zlj --register-function-id sales.fn.channel_mix
9
+
10
+ 保存测试参数(function run 验证通过后;侧栏「运行函数」预填):
11
+ 见 functions/README.md;推荐 save_test_arguments.ps1(CLI 须 ofn_xxx 内部 id,勿直接用 function_id)
12
+ """
13
+
14
+ TEST_ARGUMENTS = {
15
+ "v": 1,
16
+ "arguments": {"start_date": "2025-01-01", "end_date": "2026-06-30"},
17
+ "object_type_code": "SalesChannel",
18
+ }
19
+
20
+
21
+ def _build_where(start_date, end_date):
22
+ clauses = ["f.order_status IN ('已完成', '已发货')"]
23
+ if start_date and end_date:
24
+ clauses.append(f"f.order_date >= '{start_date}' AND f.order_date <= '{end_date}'")
25
+ return "WHERE " + " AND ".join(clauses)
26
+
27
+
28
+ def _ontology_fn_body(p):
29
+ params = dict(p.get_params() or {})
30
+ start_date = params.get("start_date", "")
31
+ end_date = params.get("end_date", "")
32
+ where_clause = _build_where(start_date, end_date)
33
+
34
+ sql = f"""
35
+ SELECT
36
+ f.channel_id,
37
+ any(c.channel_name) AS channel_name,
38
+ sum(f.sales_amount) AS sales_amount,
39
+ uniq(f.order_id) AS order_count
40
+ FROM sales_order_fact AS f
41
+ LEFT JOIN channel_dimension AS c ON f.channel_id = c.channel_id
42
+ {where_clause}
43
+ GROUP BY f.channel_id
44
+ ORDER BY sales_amount DESC
45
+ """
46
+
47
+ result = p.sql.query(sql)
48
+ if not result:
49
+ return p.function_result(
50
+ columns=["channel_id", "channel_name", "sales_amount", "order_count", "share_pct"],
51
+ data=[],
52
+ row_count=0,
53
+ )
54
+
55
+ total = sum(float(r.get("sales_amount", 0) or 0) for r in result)
56
+ data = []
57
+ for row in result:
58
+ sales_amount = float(row.get("sales_amount", 0) or 0)
59
+ data.append({
60
+ "channel_id": str(row.get("channel_id", "")),
61
+ "channel_name": str(row.get("channel_name", "")),
62
+ "sales_amount": round(sales_amount, 2),
63
+ "order_count": int(row.get("order_count", 0) or 0),
64
+ "share_pct": round(sales_amount / total if total > 0 else 0.0, 4),
65
+ })
66
+
67
+ return p.function_result(
68
+ columns=["channel_id", "channel_name", "sales_amount", "order_count", "share_pct"],
69
+ data=data,
70
+ row_count=len(data),
71
+ )
72
+
73
+
74
+ def main():
75
+ s = space.get(ctx.space_id or "")
76
+ _Ports = type(
77
+ "_Ports",
78
+ (),
79
+ {
80
+ "get_params": lambda self: dict(ctx.params or {}),
81
+ "function_result": lambda self, **kw: onto.function_result(**kw),
82
+ },
83
+ )
84
+ p = _Ports()
85
+ p.sql = s.sql
86
+ return _ontology_fn_body(p)
@@ -0,0 +1,123 @@
1
+ """客户销售分层 sales.fn.customer_segmentation
2
+
3
+ 参数:metric(sales_amount|quantity), method(quartile|percentile), start_date, end_date
4
+ 返回:客户销售额及分层(VIP/High/Medium/Low)
5
+
6
+ 发布:
7
+ dazi onto script publish 项目/潘达石化/本体/ontos/产品销售本体方案/functions/sales_fn_customer_segmentation.py \\
8
+ --space space__zlj --register-function-id sales.fn.customer_segmentation
9
+
10
+ 保存测试参数(function run 验证通过后;侧栏「运行函数」预填):
11
+ 见 functions/README.md;推荐 save_test_arguments.ps1(CLI 须 ofn_xxx 内部 id,勿直接用 function_id)
12
+ """
13
+
14
+ TEST_ARGUMENTS = {
15
+ "v": 1,
16
+ "arguments": {
17
+ "metric": "sales_amount",
18
+ "method": "quartile",
19
+ "start_date": "2025-01-01",
20
+ "end_date": "2026-06-30",
21
+ },
22
+ "object_type_code": "Customer",
23
+ }
24
+
25
+
26
+ def _build_where(start_date, end_date):
27
+ clauses = ["order_status IN ('已完成', '已发货')"]
28
+ if start_date and end_date:
29
+ clauses.append(f"order_date >= '{start_date}' AND order_date <= '{end_date}'")
30
+ return "WHERE " + " AND ".join(clauses)
31
+
32
+
33
+ def _ontology_fn_body(p):
34
+ params = dict(p.get_params() or {})
35
+ metric = params.get("metric", "sales_amount")
36
+ method = params.get("method", "quartile")
37
+ start_date = params.get("start_date", "")
38
+ end_date = params.get("end_date", "")
39
+ where_clause = _build_where(start_date, end_date)
40
+ order_col = "quantity" if metric == "quantity" else "sales_amount"
41
+
42
+ sql = f"""
43
+ SELECT
44
+ customer_id,
45
+ customer_region,
46
+ sum(sales_amount) AS sales_amount,
47
+ sum(quantity) AS quantity
48
+ FROM sales_order_fact
49
+ {where_clause}
50
+ GROUP BY customer_id, customer_region
51
+ ORDER BY {order_col} DESC
52
+ """
53
+
54
+ result = p.sql.query(sql)
55
+ if not result:
56
+ return p.function_result(
57
+ columns=["customer_id", "customer_region", "sales_amount", "quantity", "segment"],
58
+ data=[],
59
+ row_count=0,
60
+ )
61
+
62
+ values = [float(row.get(order_col, 0) or 0) for row in result]
63
+ total = len(values)
64
+ if total == 0:
65
+ return p.function_result(
66
+ columns=["customer_id", "customer_region", "sales_amount", "quantity", "segment"],
67
+ data=[],
68
+ row_count=0,
69
+ )
70
+
71
+ sorted_vals = sorted(values, reverse=True)
72
+ if method == "percentile":
73
+ p80_idx = min(int(total * 0.8), total - 1)
74
+ p50_idx = min(int(total * 0.5), total - 1)
75
+ p20_idx = min(int(total * 0.2), total - 1)
76
+ else:
77
+ p80_idx = min(int(total * 0.25), total - 1)
78
+ p50_idx = min(int(total * 0.5), total - 1)
79
+ p20_idx = min(int(total * 0.75), total - 1)
80
+ p80 = sorted_vals[p80_idx]
81
+ p50 = sorted_vals[p50_idx]
82
+ p20 = sorted_vals[p20_idx]
83
+
84
+ def get_segment(val):
85
+ if val >= p80:
86
+ return "VIP"
87
+ if val >= p50:
88
+ return "High"
89
+ if val >= p20:
90
+ return "Medium"
91
+ return "Low"
92
+
93
+ data = []
94
+ for row in result:
95
+ val = float(row.get(order_col, 0) or 0)
96
+ data.append({
97
+ "customer_id": str(row.get("customer_id", "")),
98
+ "customer_region": str(row.get("customer_region", "")),
99
+ "sales_amount": round(float(row.get("sales_amount", 0) or 0), 2),
100
+ "quantity": int(row.get("quantity", 0) or 0),
101
+ "segment": get_segment(val),
102
+ })
103
+
104
+ return p.function_result(
105
+ columns=["customer_id", "customer_region", "sales_amount", "quantity", "segment"],
106
+ data=data,
107
+ row_count=len(data),
108
+ )
109
+
110
+
111
+ def main():
112
+ s = space.get(ctx.space_id or "")
113
+ _Ports = type(
114
+ "_Ports",
115
+ (),
116
+ {
117
+ "get_params": lambda self: dict(ctx.params or {}),
118
+ "function_result": lambda self, **kw: onto.function_result(**kw),
119
+ },
120
+ )
121
+ p = _Ports()
122
+ p.sql = s.sql
123
+ return _ontology_fn_body(p)
@@ -0,0 +1,81 @@
1
+ """销售总览函数 sales.fn.get_summary
2
+
3
+ 参数:start_date, end_date(可选)
4
+ 返回:总销售额、总销量、订单数、客单价、动销 SKU 数
5
+
6
+ 发布:
7
+ dazi onto script publish 项目/潘达石化/本体/ontos/产品销售本体方案/functions/sales_fn_get_summary.py \\
8
+ --space space__zlj --register-function-id sales.fn.get_summary
9
+
10
+ 保存测试参数(function run 验证通过后;侧栏「运行函数」预填):
11
+ 见 functions/README.md;推荐 save_test_arguments.ps1(CLI 须 ofn_xxx 内部 id,勿直接用 function_id)
12
+ """
13
+
14
+ TEST_ARGUMENTS = {
15
+ "v": 1,
16
+ "arguments": {"start_date": "2025-01-01", "end_date": "2026-06-30"},
17
+ "object_type_code": "SalesAnalysis",
18
+ }
19
+
20
+
21
+ def _valid_order_clause():
22
+ return "order_status IN ('已完成', '已发货')"
23
+
24
+
25
+ def _build_where(start_date, end_date):
26
+ clauses = [_valid_order_clause()]
27
+ if start_date and end_date:
28
+ clauses.append(f"order_date >= '{start_date}' AND order_date <= '{end_date}'")
29
+ return "WHERE " + " AND ".join(clauses)
30
+
31
+
32
+ def _ontology_fn_body(p):
33
+ params = dict(p.get_params() or {})
34
+ start_date = params.get("start_date", "")
35
+ end_date = params.get("end_date", "")
36
+ where_clause = _build_where(start_date, end_date)
37
+
38
+ sql = f"""
39
+ SELECT
40
+ sum(sales_amount) AS total_sales,
41
+ sum(quantity) AS total_quantity,
42
+ uniq(order_id) AS order_count,
43
+ uniq(product_id) AS product_count
44
+ FROM sales_order_fact
45
+ {where_clause}
46
+ """
47
+
48
+ rows = p.sql.query(sql)
49
+ row = rows[0] if rows else {}
50
+ total_sales = float(row.get("total_sales", 0) or 0)
51
+ order_count = int(row.get("order_count", 0) or 0)
52
+ avg_order_value = total_sales / order_count if order_count > 0 else 0.0
53
+
54
+ data = [{
55
+ "total_sales": round(total_sales, 2),
56
+ "total_quantity": int(row.get("total_quantity", 0) or 0),
57
+ "order_count": order_count,
58
+ "avg_order_value": round(avg_order_value, 2),
59
+ "product_count": int(row.get("product_count", 0) or 0),
60
+ }]
61
+
62
+ return p.function_result(
63
+ columns=["total_sales", "total_quantity", "order_count", "avg_order_value", "product_count"],
64
+ data=data,
65
+ row_count=1,
66
+ )
67
+
68
+
69
+ def main():
70
+ s = space.get(ctx.space_id or "")
71
+ _Ports = type(
72
+ "_Ports",
73
+ (),
74
+ {
75
+ "get_params": lambda self: dict(ctx.params or {}),
76
+ "function_result": lambda self, **kw: onto.function_result(**kw),
77
+ },
78
+ )
79
+ p = _Ports()
80
+ p.sql = s.sql
81
+ return _ontology_fn_body(p)
@@ -0,0 +1,90 @@
1
+ """月度环比分析 sales.fn.mom_analysis
2
+
3
+ 参数:start_date, end_date
4
+ 返回:月度销售额、销量、订单数及环比增长率
5
+
6
+ 发布:
7
+ dazi onto script publish 项目/潘达石化/本体/ontos/产品销售本体方案/functions/sales_fn_mom_analysis.py \\
8
+ --space space__zlj --register-function-id sales.fn.mom_analysis
9
+
10
+ 保存测试参数(function run 验证通过后;侧栏「运行函数」预填):
11
+ 见 functions/README.md;推荐 save_test_arguments.ps1(CLI 须 ofn_xxx 内部 id,勿直接用 function_id)
12
+ """
13
+
14
+ TEST_ARGUMENTS = {
15
+ "v": 1,
16
+ "arguments": {"start_date": "2025-01-01", "end_date": "2026-06-30"},
17
+ "object_type_code": "SalesAnalysis",
18
+ }
19
+
20
+
21
+ def _build_where(start_date, end_date):
22
+ clauses = ["order_status IN ('已完成', '已发货')"]
23
+ if start_date and end_date:
24
+ clauses.append(f"order_date >= '{start_date}' AND order_date <= '{end_date}'")
25
+ return "WHERE " + " AND ".join(clauses)
26
+
27
+
28
+ def _ontology_fn_body(p):
29
+ params = dict(p.get_params() or {})
30
+ start_date = params.get("start_date", "")
31
+ end_date = params.get("end_date", "")
32
+ where_clause = _build_where(start_date, end_date)
33
+
34
+ sql = f"""
35
+ SELECT
36
+ formatDateTime(order_date, '%Y-%m') AS year_month,
37
+ sum(sales_amount) AS sales_amount,
38
+ sum(quantity) AS quantity,
39
+ uniq(order_id) AS order_count
40
+ FROM sales_order_fact
41
+ {where_clause}
42
+ GROUP BY formatDateTime(order_date, '%Y-%m')
43
+ ORDER BY year_month
44
+ """
45
+
46
+ result = p.sql.query(sql)
47
+ if not result:
48
+ return p.function_result(
49
+ columns=["year_month", "sales_amount", "quantity", "order_count", "mom_growth"],
50
+ data=[],
51
+ row_count=0,
52
+ )
53
+
54
+ data = []
55
+ prev_sales = None
56
+ for row in result:
57
+ sales_amount = float(row.get("sales_amount", 0) or 0)
58
+ if prev_sales is not None and prev_sales != 0:
59
+ mom_growth = (sales_amount - prev_sales) / prev_sales
60
+ else:
61
+ mom_growth = 0.0
62
+ data.append({
63
+ "year_month": str(row.get("year_month", "")),
64
+ "sales_amount": round(sales_amount, 2),
65
+ "quantity": int(row.get("quantity", 0) or 0),
66
+ "order_count": int(row.get("order_count", 0) or 0),
67
+ "mom_growth": round(mom_growth, 4),
68
+ })
69
+ prev_sales = sales_amount
70
+
71
+ return p.function_result(
72
+ columns=["year_month", "sales_amount", "quantity", "order_count", "mom_growth"],
73
+ data=data,
74
+ row_count=len(data),
75
+ )
76
+
77
+
78
+ def main():
79
+ s = space.get(ctx.space_id or "")
80
+ _Ports = type(
81
+ "_Ports",
82
+ (),
83
+ {
84
+ "get_params": lambda self: dict(ctx.params or {}),
85
+ "function_result": lambda self, **kw: onto.function_result(**kw),
86
+ },
87
+ )
88
+ p = _Ports()
89
+ p.sql = s.sql
90
+ return _ontology_fn_body(p)