vps-work 2.1.3 → 2.3.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vps-work",
3
- "version": "2.1.3",
3
+ "version": "2.3.0",
4
4
  "description": "VPS Work CLI - tenant-aware domain shortcuts and bundled agent skills for Cursor / Claude Code / Trae.",
5
5
  "license": "MIT",
6
6
  "author": "he-linyan",
@@ -45,17 +45,21 @@
45
45
  },
46
46
  "scripts": {
47
47
  "dev": "tsx src/bin/vps-work.ts",
48
- "build": "tsup",
48
+ "build": "tsup && node scripts/build-office.mjs",
49
49
  "lint": "eslint \"src/**/*.ts\" \"tests/**/*.ts\"",
50
50
  "typecheck": "tsc --noEmit",
51
51
  "test": "vitest run",
52
52
  "test:watch": "vitest",
53
- "prepublishOnly": "npm test && npm run build"
53
+ "prepublishOnly": "npm test && npm run build",
54
+ "pretest": "node scripts/build-office.mjs"
54
55
  },
55
56
  "dependencies": {
56
57
  "@inquirer/prompts": "^7.2.0",
57
58
  "commander": "^13.0.0",
59
+ "fast-xml-parser": "^5.3.4",
60
+ "jszip": "^3.10.1",
58
61
  "undici": "^7.2.0",
62
+ "utif2": "^4.1.0",
59
63
  "yaml": "^2.6.1",
60
64
  "zod": "^3.24.1"
61
65
  },
@@ -66,6 +70,7 @@
66
70
  "@types/node": "^22.10.5",
67
71
  "@typescript-eslint/eslint-plugin": "^8.20.0",
68
72
  "@typescript-eslint/parser": "^8.20.0",
73
+ "esbuild": "^0.27.7",
69
74
  "eslint": "^9.18.0",
70
75
  "tsup": "^8.3.5",
71
76
  "tsx": "^4.19.2",
@@ -0,0 +1,24 @@
1
+ import { build } from 'esbuild';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { copyFile } from 'node:fs/promises';
6
+
7
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
8
+ const vendor = resolve(root, '../../vendor/vpsoffice');
9
+ const require = createRequire(resolve(root, 'package.json'));
10
+ await build({
11
+ entryPoints: [resolve(vendor, 'packages/automation/src/index.ts')],
12
+ outfile: resolve(root, 'dist/office-runtime.cjs'),
13
+ bundle: true, platform: 'node', format: 'cjs', target: 'node20',
14
+ alias: {
15
+ '@genoffice/pptx-engine': resolve(vendor, 'packages/pptx-engine/src/index.ts'),
16
+ '@genoffice/pptx-engine/identity': resolve(vendor, 'packages/pptx-engine/src/identity.ts'),
17
+ '@genoffice/pptx-engine/custgeom': resolve(vendor, 'packages/pptx-engine/src/custgeom.ts'),
18
+ '@genoffice/pptx-engine/table-grid': resolve(vendor, 'packages/pptx-engine/src/table-grid.ts'),
19
+ '@genoffice/pptx-engine/background-promote': resolve(vendor, 'packages/pptx-engine/src/background-promote.ts'),
20
+ ...Object.fromEntries(['jszip', 'fast-xml-parser', 'utif2', 'zod'].map(name => [name, require.resolve(name)])),
21
+ },
22
+ });
23
+ await copyFile(resolve(vendor, 'LICENSE'), resolve(root, 'dist/OFFICE-LICENSE'));
24
+ await copyFile(resolve(vendor, 'NOTICE'), resolve(root, 'dist/OFFICE-NOTICE'));
@@ -0,0 +1,123 @@
1
+ /** Real Electron editor smoke test. Build CLI and Office apps first; uses isolated userData. */
2
+ import { createServer } from 'node:http';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { execFile } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+ import { mkdtemp, mkdir, writeFile, readFile, rm } from 'node:fs/promises';
7
+ import { tmpdir } from 'node:os';
8
+ import { dirname, join, resolve } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { createRequire } from 'node:module';
11
+ import assert from 'node:assert/strict';
12
+
13
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
14
+ const vendor = resolve(root, '../../vendor/vpsoffice');
15
+ const require = createRequire(join(vendor, 'package.json'));
16
+ const { _electron } = require('@playwright/test');
17
+ const runtime = createRequire(join(root, 'package.json'))(join(root, 'dist/office-runtime.cjs'));
18
+ const run = promisify(execFile);
19
+ const scratch = await mkdtemp(join(tmpdir(), 'vps-office-desktop-'));
20
+ const token = randomUUID();
21
+ let endpoint;
22
+ const host = createServer((req, res) => {
23
+ void (async () => {
24
+ if (req.headers.authorization !== `Bearer ${token}`) { res.writeHead(403).end(); return; }
25
+ const chunks = [];
26
+ for await (const chunk of req) chunks.push(chunk);
27
+ const body = Buffer.concat(chunks).toString();
28
+ if (req.url === '/vps/register-office') {
29
+ endpoint = JSON.parse(body).url;
30
+ res.setHeader('Content-Type', 'application/json'); res.end('{"ok":true}'); return;
31
+ }
32
+ if (!endpoint) { res.writeHead(503).end('{"error":"not ready"}'); return; }
33
+ const reply = await fetch(`${endpoint}${req.url}`, { method: 'POST', headers: { Authorization: `Bearer ${token}` }, body });
34
+ res.writeHead(reply.status, { 'Content-Type': 'application/json' }).end(await reply.text());
35
+ })().catch(error => { res.writeHead(500).end(JSON.stringify({ error: String(error) })); });
36
+ });
37
+ await new Promise(resolve => host.listen(0, '127.0.0.1', resolve));
38
+ const url = `http://127.0.0.1:${host.address().port}`;
39
+ const config = join(scratch, 'connection.json');
40
+ await writeFile(config, JSON.stringify({ protocolVersion: 1, url, token }), { mode: 0o600 });
41
+ const cli = async (...args) => {
42
+ const { stdout } = await run(process.execPath, [join(root, 'dist/vps-work.cjs'), 'office', ...args, '--connection-file', config], { maxBuffer: 16 * 1024 * 1024, timeout: 130_000 });
43
+ return JSON.parse(stdout);
44
+ };
45
+ const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
46
+ let app;
47
+ try {
48
+ for (const format of (process.env.OFFICE_TEST_FORMATS ?? 'docx,xlsx,pptx,md,pdf').split(',')) {
49
+ endpoint = undefined;
50
+ const path = join(scratch, `test.${format}`);
51
+ const spec = format === 'docx' ? { blocks: [{ type: 'paragraph', runs: [{ text: 'Original document' }] }] }
52
+ : format === 'xlsx' ? { sheets: ['Sheet1'], cells: [{ sheet: 'Sheet1', address: 'A1', value: 'Original cell' }] } : format === 'md' ? { text: '# Original document' } : { ops: [] };
53
+ const pdfLib = format === 'pdf' ? createRequire(join(vendor, 'apps/pdf/package.json'))('pdf-lib') : null;
54
+ if (pdfLib) {
55
+ const seed = await pdfLib.PDFDocument.create(); seed.addPage();
56
+ await writeFile(path, await seed.save());
57
+ } else await writeFile(path, await runtime.createDocument(format, spec));
58
+ const userData = join(scratch, format);
59
+ await mkdir(userData);
60
+ await writeFile(join(userData, 'app-settings.json'), JSON.stringify({ onboardingSeen: true }));
61
+ const env = { ...process.env, GENOFFICE_USER_DATA: userData, GENOFFICE_LANG: 'en', VPS_OFFICE_HOST_URL: url, VPS_OFFICE_HOST_TOKEN: token };
62
+ delete env.ELECTRON_RUN_AS_NODE;
63
+ app = await _electron.launch({ executablePath: require('electron'), args: [join(vendor, 'apps/shell'), path], env });
64
+ app.process().stderr.on('data', bytes => { if (process.env.OFFICE_TEST_VERBOSE) process.stderr.write(bytes); });
65
+ let session;
66
+ let snapshot;
67
+ for (let n = 0; n < 60; n++) {
68
+ if (endpoint) {
69
+ const result = await cli('+sessions');
70
+ session = result.sessions.find(s => s.kind === ({ docx: 'docs', xlsx: 'sheets', pptx: 'slides', md: 'markdown', pdf: 'pdf' }[format]));
71
+ if (session?.ready) {
72
+ snapshot = await cli('+session-read', '--session-id', session.id).catch(() => undefined);
73
+ if (snapshot && !snapshot.content?.isError) break;
74
+ }
75
+ }
76
+ await delay(500);
77
+ }
78
+ assert.ok(snapshot, `${format} session must become readable`);
79
+ const tools = await cli('+tools', '--session-id', session.id);
80
+ let tool, input;
81
+ if (format === 'docx') { tool = 'replace_blocks'; input = { startBlockIndex: 0, endBlockIndex: 0, html: '<p>Changed through vps-work office</p>' }; }
82
+ else if (format === 'md') { tool = 'replace_text'; input = { blockIndex: 0, find: 'Original document', replace: 'Changed through vps-work office' }; }
83
+ else if (format === 'xlsx') {
84
+ tool = 'propose_operations';
85
+ // Sheet IDs come from the native context rather than assumptions about filenames.
86
+ const text = snapshot.content.output;
87
+ const match = text.match(/sheetId[=: ]+"?([A-Za-z0-9_-]+)/i) ?? text.match(/id[=: ]+"?([A-Za-z0-9_-]+)/i);
88
+ if (!match) throw new Error(`Cannot resolve sheet ID from context: ${text}`);
89
+ input = { summary: 'Bridge smoke test', operations: [{ op: 'set_cell', sheetId: match[1], address: 'A1', value: 'Changed through vps-work office' }] };
90
+ } else if (format === 'pdf') { tool = 'rotate_page'; input = { page: 1, direction: 'right' };
91
+ } else { tool = 'apply_ops'; input = { ops: [{ op: 'addElement', target: { slide: 0 }, kind: 'textbox', offset: { x: 476250, y: 476250, cx: 4762500, cy: 952500 }, paragraphs: [{ runs: [{ text: 'Changed through vps-work office' }] }] }] }; }
92
+ assert.ok(tools.tools.some(t => t.name === tool));
93
+ const argsFile = join(scratch, `${format}-args.json`);
94
+ await writeFile(argsFile, JSON.stringify(input));
95
+ const changed = await cli('+call', '--session-id', session.id, '--tool', tool, '--args-file', argsFile, '--expected-version', tools.version);
96
+ assert.ok(!changed.result?.isError, JSON.stringify(changed));
97
+ assert.notEqual(changed.version, tools.version, 'native mutation must advance session version');
98
+ const after = await cli('+session-read', '--session-id', session.id);
99
+ if (format === 'xlsx') {
100
+ await writeFile(argsFile, JSON.stringify({ range: 'A1', sheetId: input.operations[0].sheetId }));
101
+ const cells = await cli('+call', '--session-id', session.id, '--tool', 'read_range', '--args-file', argsFile, '--expected-version', after.version);
102
+ assert.ok(JSON.stringify(cells).includes('Changed through vps-work office'), 'xlsx native read_range must see unsaved edits');
103
+ after.version = cells.version;
104
+ } else if (format === 'pdf') assert.deepEqual(after.content.pending.rotations, [[0, 90]]);
105
+ else assert.ok(JSON.stringify(after.content).includes('Changed through vps-work office'), `${format} unsaved content should be visible`);
106
+ await assert.rejects(cli('+session-save', '--session-id', session.id, '--expected-version', tools.version), /version conflict/);
107
+ await cli('+session-save', '--session-id', session.id, '--expected-version', after.version);
108
+ if (pdfLib) {
109
+ const saved = await pdfLib.PDFDocument.load(await readFile(path));
110
+ assert.equal(saved.getPage(0).getRotation().angle, 90);
111
+ } else {
112
+ const saved = await runtime.readDocument(format, await readFile(path));
113
+ assert.ok(JSON.stringify(saved).includes('Changed through vps-work office'), `${format} must persist native edits`);
114
+ }
115
+ console.log(`PASS ${format}: discover → read → native tool → stale-version rejection → save → disk verification`);
116
+ await app.close(); app = undefined;
117
+ }
118
+ } finally {
119
+ if (app) await app.close();
120
+ host.closeAllConnections();
121
+ await new Promise(resolve => host.close(resolve));
122
+ await rm(scratch, { recursive: true, force: true });
123
+ }
package/skills/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
  |--------|------------|------|
9
9
  | (共用) | `vps-shared` | 登录、身份、`+` 约定 |
10
10
  | `sales` | `sale-performance-query` | 总销日报、部门 OKR 达成、销售人效 |
11
- | `store` | `vps-store-management` | 门店接待、进店转化与客户过程数据 |
11
+ | `store` | `vps-store-management` | Walk-in 部门/日期分组、门店库存筛选分组、接待转化与客户过程数据 |
12
12
  | `hr` | `vps-hr` | 人员档案、组织架构、岗位职责/SOP(部门、岗位、个人查询)、薪酬单据、工资条 |
13
13
  | `attendance` | `vps-attendance` | 考勤日报、排班、假期余额与考勤申请 |
14
14
  | `knowledge` | `vps-knowledge` | 企业知识库、V盘资料导入、检索、问答与知识图谱 |
@@ -22,7 +22,7 @@
22
22
  | `drive` | `vps-v-drive` | 管理账号级 OSS V盘普通文件、文件夹、传输记录与回收站 |
23
23
  | `creative` | `vps-creative` | 内容创作画布:AI 生图、生视频、文字转语音、音乐生成与模型列表 |
24
24
  | `tools` | `vps-tools` | 平台公共工具:OSS 上传、公开地址与限时签名地址 |
25
- | `im` / `mail` / `meeting` | `vps-im` / `vps-mail` / `vps-meeting` | IM 消息与会话附件下载、站内信和会议协作(`vemory` 保留兼容) |
25
+ | `im` / `mail` / `meeting` | `vps-im` / `vps-mail` / `vps-meeting` | IM 消息与会话附件下载、企业电子邮件和会议协作(`vemory` 保留兼容) |
26
26
 
27
27
  细粒度 `vps-*` skill(如 `vps-daily-sales-report`)承载 script_key 与参数说明;Agent 优先读域聚合 skill,再按需下钻子 skill。
28
28
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: sale-performance-query
3
- description: "使用 `vps-work sales +…` 查询 Vertu 销售业绩和订单。用于公司、部门、销售员的销售额、销量、普通销售订单、订单关联的开票申请、正式发票、会计凭证与付款状态、已对账销售收款、JXSD 经销商订单、代理商 Sell-in/Sell-out、代理商五件套来源和 Walk-in、经销商库存与终销、客户画像、新老客占比、环比同比、部门下钻、目标达成、定金退款、人效、个人 OKR、团队 PK,以及按商品系列、款式或 IOT 分析销量销额。除经销商默认仓库库存外,通用商品库存、价格、发货与商品主数据改用 vps-product。"
3
+ description: "使用 `vps-work sales +…` 查询 Vertu 销售业绩和订单。用于公司、部门、销售员的销售额、销量、普通销售订单、按销售单/FPSQ/购买方查询开票申请及详情链接、订单商品 VSN/IMEI 和激活地址、正式发票、会计凭证与付款状态、已对账销售收款、JXSD 经销商订单、代理商 Sell-in/Sell-out、代理商五件套来源和 Walk-in、经销商库存与终销、客户画像、新老客占比、环比同比、部门下钻、目标达成、定金退款、人效、个人 OKR、团队 PK,以及按商品系列、款式或 IOT 分析销量销额。除经销商默认仓库库存外,通用商品库存、价格、发货与商品主数据改用 vps-product。"
4
4
  ---
5
5
 
6
6
  # 业绩查询
@@ -13,7 +13,7 @@ description: "使用 `vps-work sales +…` 查询 Vertu 销售业绩和订单。
13
13
  - 查询前确认用户要的时间、部门层级、是否含定金;不明确时采用 Shortcut 默认值,但回答必须说明口径。
14
14
  - 如果用户要求本地服务或刚改过 CLI,先 `npm run build`,并显式传 `--base-url http://127.0.0.1:3920` 或确认 `vps-work auth status --json` 指向本地。
15
15
  - 引用结果前先看返回的 `filter`:必须核对 `period/start_date/end_date/include_deposit/dept_l1/dept_l2/dept_l3/dept_l4/dept_l5`;商品查询还必须核对 `product_subcategory/product_style/product_style_details/smart_wearable`,趋势查询继续核对 `grain/group_by/breakdown_by`。
16
- - 返回含 `pagination` 的列表必须检查 `pagination.has_more` / `next_page`,不要把第一页 `rows` 当作全量;需要完整清单时持续翻页直到 `has_more=false`。
16
+ - 返回含 `pagination` 的列表必须检查 `pagination.has_more` / `next_page` / `next_offset`,不要把第一页 `rows` 当作全量;需要完整清单时持续翻页直到 `has_more=false`。
17
17
  - 空结果先检查权限、过滤条件、姓名/工号拼写、是否默认剔除了定金;不要直接断言全公司为 0。
18
18
 
19
19
  ## Shortcuts(推荐优先使用)
@@ -23,10 +23,10 @@ Shortcut 是对常用销售经营查询的高级封装(`vps-work sales +<verb>
23
23
  | Shortcut | 说明 |
24
24
  |----------|------|
25
25
  | [`+headline-kpi`](references/vps-sales-headline-kpi.md) | 获取公司/部门核心 KPI:销售额、销量、订单 |
26
- | `+orders` / `+order-list` | 查询订单明细;订单结果已带 `商品码`、分摊比例、客户来源、二级客户来源、最新发货日期和物流单号摘要,支持 `--logistics-no` 反查订单 |
26
+ | `+orders` / `+order-list` | 查询订单明细及商品 VSN/IMEI、激活信息([设备字段与过滤](../vps-daily-sales-report/reference.md#订单设备与激活信息));订单结果已带 `商品码`、销售 `user_id`、是否分摊、分摊比例、完整分摊部门、部门比例明细、客户来源、二级客户来源、最新发货日期和物流单号摘要,支持 `--is-apportioned true|false` 过滤和 `--logistics-no` 反查订单 |
27
27
  | [`+order-finance`](references/vps-sales-order-finance.md) / `+finance-detail` | 按销售单号一次查看订单、开票申请、正式发票、过账/付款状态和关联会计凭证摘要 |
28
28
  | [`+reconciled-receipts`](references/vps-sales-reconciled-receipts.md) / `+sales-receipts` | 查询最近 30 个自然日内已对账销售单据的收款明细;按单据、收款方式和收款主体聚合 |
29
- | [`+invoice-status`](references/vps-sales-invoice-status.md) / `+invoice-applications` | 按一个或多个销售单号精确查询开票申请,区分未找到、未申请、草稿、审批中、已完成和已取消 |
29
+ | [`+invoice-status`](references/vps-sales-invoice-status.md) / `+invoice-applications` | 按销售单/FPSQ/购买方查询开票申请,返回真实申请 ID、开票主体、购买方、状态和详情链接 |
30
30
  | [`+account-orders`](references/vps-sales-account-orders.md) / `+account-sales-detail` | 按会计科目穿透已过账凭证关联的销售订单行;区分直接关联与同凭证关联 |
31
31
  | [`+invoice-attachments`](references/vps-sales-invoice-attachments.md) | 按销售单号查询已过账客户发票/退款单附件;可由本地服务安全下载,不返回存储签名 URL |
32
32
  | [`+dealer-orders`](references/vps-sales-dealer-orders.md) / `+dealer-order-list` | 查询独立的 `dealer.sale.order` 经销商订单;仅返回 `JXSD` 前缀,支持经销商、商品和 VSN/IMEI 筛选 |
@@ -76,10 +76,10 @@ Shortcut 是对常用销售经营查询的高级封装(`vps-work sales +<verb>
76
76
  - 用户问 `vps-ep` 代理商个性化页面的其他模块时,使用同名 `+agent-*` 看板命令;先用 `+agent-list` 获取完整名称,再向相关命令传 `--agent-name <完整名称>`。
77
77
  - 选择单个代理商优先用 `--agent-name <完整名称>` 标准化中英文括号后精确匹配;`--dealer` 是兼容参数,在代理商看板命令中同样按精确名称处理。检查有 `filter` 的返回时同时核对 `filter.agent_name`、`nature=domestic` 和权限范围。
78
78
  - 代理商目标来自 `线下代理OKR` 且严格按请求日期范围取数;结果为 `0` 时先区分“当期无目标记录”和“存在目标记录但值为零”,不得自行回退上月目标。
79
- - 查“订单、订单明细、某商品订单、某客户订单、物流单号对应哪个订单”优先用 `+orders`。订单列表返回 `商品码`、`分摊比例`、`客户来源`、`二级客户来源`、`最新发货日期`、`物流单号`、`物流单号数`,一般不需要再查物流明细。
79
+ - 查“订单、订单明细、某商品订单、某客户订单、物流单号对应哪个订单”优先用 `+orders`。订单列表返回 `商品码`、`销售user_id`、`是否分摊`、`分摊比例`、`分摊部门`、`分摊明细`、`客户来源`、`二级客户来源`、`最新发货日期`、`物流单号`、`物流单号数`;多销售分摊订单的 `销售user_id` 返回去重后的完整 ID 列表。只看分摊订单传 `--is-apportioned true`,只看未分摊订单传 `--is-apportioned false`。
80
80
  - 查“某销售单的开票申请、正式发票、会计凭证、过账和付款状态”优先使用 `+order-finance --order-no ...`,避免拼接多个局部查询。
81
81
  - 查“已对账销售收款、某销售单收款方式/主体/金额”使用 `+reconciled-receipts`。其时间范围固定为最近 30 个自然日,即使指定单号也不放宽;结果默认排除“京东自营”和“商场代收”收款方式。
82
- - 查“某销售单有没有申请开票、申请到哪一步、销售单与开票申请如何关联”使用 `+invoice-status --order-names ...`。禁止用 `+reconciled-receipts` 的“已开票”布尔值代替真实申请状态。
82
+ - 查销售单申请状态用 `+invoice-status --order-names ...`;按 FPSQ 单号或购买方查申请/详情链接用 `--application-names ...` / `--buyer ...`。先读 [开票申请查询](references/vps-sales-invoice-status.md),使用接口返回的真实 ID 和链接;历史“FPSQ 无 ID 通道”结论已失效。该接口无默认日期窗口,不应套用销售业绩时间/部门过滤。禁止用 `+reconciled-receipts` 的“已开票”布尔值代替真实申请状态。
83
83
  - 查“某会计科目对应哪些销售单/销售明细”使用 `+account-orders`。必须至少传 `--account-code` 或 `--account-name`,并区分会计行金额、发票行金额与销售订单行金额。
84
84
  - 查“某销售单的发票附件”使用 `+invoice-attachments --order-no <销售单号>`;只查询当前用户销售数据范围内的订单。需要文件时加 `--download-dir <目录>`,附件由 Vertu 服务代理下载,不直接展示对象存储 URL。
85
85
  - 订单列表客户信息默认按常用规则脱敏:手机号保留前三后四,客户名称保留首字;有 `sales.pii:read` 时才返回明文。
@@ -108,7 +108,7 @@ Shortcut 是对常用销售经营查询的高级封装(`vps-work sales +<verb>
108
108
  - “黄金3 / 黄金4 / 金钻”使用 `--product-style-details "黄金3,黄金4,金钻"`;服务端会把“金钻”“4金3钻”规范化为商品维表的精确值 `4金/3钻`。需要限定大折叠时必须同时传 `--product-subcategory "VERTU ALPHAFOLD"`。
109
109
  - “智能穿戴”使用 `--smart-wearable`。它同时限定商品大类为戒指/手链,并使用与 `vps-ep` 一致的系列白名单:智能健康戒指、AI钻戒2代、VERTU AI 水晶智能戒指、VERTU AI 智能戒指、VERTU关心智能手链。
110
110
  - “IOT / IOT配件”使用 `--product-category IOT配件`;IOT 分类包含手链。商品趋势同时返回去重销售单数 `orders` 和与 vps-ep 商品页一致的明细行数 `product_order_count`,二者不能混报。含/不含代理折扣、置换排除及库存口径读取 `vps-product`。
111
- - 聚合查询不要用 `+orders --product <词>` 代替上述精确过滤;`--product` 会在名称、编码和分类上做模糊搜索,只适合找订单明细。
111
+ - 聚合查询不要只用 `+orders --product <词>` 代替上述精确过滤;`--product` 会在名称、编码和分类上做模糊搜索。明确商品编码时使用 `+orders --product-code <编码> --group-by product` 查询期间销量与金额;要查看每张订单下按销售人员、业绩部门和分摊比例拆开的商品记录,使用 `--group-by order-product`;要按商品和销售人员汇总期间业绩,使用 `--group-by product-salesperson`。分摊订单的原归属数量与分摊数量口径不同,应分别展示。
112
112
  - 必须检查响应 `filter` 是否原样返回上述商品条件;缺失任一条件时,不得把结果称为该系列或款式的业绩。
113
113
 
114
114
  ### 大折叠高定趋势标准调用
@@ -1,37 +1,42 @@
1
- # 销售单开票申请状态
2
-
3
- ## 接口
1
+ # 开票申请状态、记录 ID 与详情链接
4
2
 
5
3
  | 项目 | 值 |
6
4
  |------|----|
7
- | CLI | `vps-work sales +invoice-status --order-names <单号>`(别名 `+invoice-applications`) |
5
+ | CLI | `vps-work sales +invoice-status`(别名 `+invoice-applications`) |
8
6
  | skill_key | `vps-daily-sales-report` |
9
7
  | script_key | `sales_invoice_status` |
10
8
  | 权限 | `sales.account_orders:read` |
11
9
 
12
- ## 查询口径
10
+ ## 选择查询条件
11
+
12
+ 至少提供 `--order-names`、`--application-names`、`--buyer` 之一,多个条件取交集。单号精确匹配,逗号分隔或服务端字符串数组,最多100个;购买方按名称包含匹配,`%`、`_` 是普通字符。此接口没有默认30天窗口,不使用 period、日期或部门过滤。
13
+
14
+ - 按销售单查询:覆盖直接/联合开票申请;未指定其他条件时,缺失销售单返回 `not_found`,无申请返回 `not_applied`。多次申请逐条返回,`是否最新申请` 标识该销售单按申请日期和 ID 排序后的最新申请。
15
+ - 按 FPSQ/购买方查询:每张申请一行,`关联销售单` 列出直接/联合订单;无销售单的申请也可返回。`是否最新申请` 为 null,不存在的申请返回空结果。
16
+ - 状态为 `draft/草稿`、`approval/审批中`、`done/已完成`、`cancel/已取消`。收款查询中的“已开票”布尔值不能代替申请状态。
17
+ - 分页用 `--limit`(1–1000,默认500)、`--offset`(0–4999,默认0);根据 `pagination.has_more` 和 `next_offset` 继续查询。
18
+
19
+ ## ID 与详情链接
13
20
 
14
- - `order_names` 必填并精确匹配销售单号;一次最多 100 个,重复值自动去重。
15
- - 同时覆盖开票申请的 `sales_order` 直接关联和 `joint_order` 联合开票关联。
16
- - 一个销售单存在多次申请时逐条返回,并用 `是否最新申请=true` 标识按申请日期、申请 ID 排序后的最新一条。
17
- - 找不到销售单返回 `not_found / 未找到销售单`;销售单存在但没有开票申请返回 `not_applied / 未申请`。
18
- - 原始状态完整映射为:`draft / 草稿`、`approval / 审批中`、`done / 已完成`、`cancel / 已取消`。
19
- - `+reconciled-receipts` 中原有的“已开票”只表示申请数量大于零,不能用于回答真实申请状态。
21
+ 返回 `开票申请ID`、`开票申请单号`、`开票主体ID`、`开票主体`、`购买方`、`详情链接`。开票主体取申请单的 `invoice_entity`,不能用销售单公司推断。
20
22
 
21
- ## 参数
23
+ 优先使用返回的 `详情链接`。它以真实数字 ID 和稳定动作标识 `vt_sale_order.action_invoice_application` 打开表单,不写死 action 数字或自动添加 cids;缺少 ERP 地址配置时为相对 ERP 的 `/web#...` 路径。无申请记录时链接为 null。打开链接仍需 ERP 登录及记录权限。
22
24
 
23
- | CLI | execute 参数 | 说明 |
24
- |-----|--------------|------|
25
- | `--order-names SO1,SO2` | `order_names` | 必填;销售单号精确匹配,逗号分隔 |
26
- | `--limit <n>` | `limit` | 1–1000,默认 500 |
27
- | `--offset <n>` | `offset` | 0–4999,默认 0 |
25
+ 此前“FPSQ → ID 无通道”的结论不再适用于新版服务。不要从 FPSQ 编号推算 ID、用单号替代 `id=`,或要求用户逐张抄 ID。若当前服务确实未返回字段,说明环境/版本差异,不转向绕过白名单。
26
+
27
+ ## 示例与旧版 CLI
28
+
29
+ ```bash
30
+ vps-work sales +invoice-status --order-names SO001,SO002
31
+ vps-work sales +invoice-status --application-names FPSQ2026062203845
32
+ vps-work sales +invoice-status --buyer 温州森诺 --limit 100
33
+ ```
28
34
 
29
- ## 示例
35
+ 新快捷参数随 vps-work 2.2.0 提供。旧 CLI 可使用现有通用入口调用已上线的服务:
30
36
 
31
37
  ```bash
32
- vps-work sales +invoice-status \
33
- --order-names SO001,SO002 \
34
- --limit 100
38
+ vps-work exec --skill vps-daily-sales-report --script sales_invoice_status --param buyer=温州森诺
39
+ vps-work exec --skill vps-daily-sales-report --script sales_invoice_status --param application_names=FPSQ2026062203845
35
40
  ```
36
41
 
37
- 回答时优先使用 `申请状态`,需要解释或排障时再带 `状态代码`;存在多条申请时不要只凭第一条推断,先查看 `是否最新申请` 和是否存在已取消的历史申请。
42
+ 完整字段、组合过滤和旧订单兼容语义见 [接口参考](../../vps-daily-sales-report/reference.md#开票申请详情与购买方查询)。
@@ -12,7 +12,7 @@
12
12
  ## 返回内容
13
13
 
14
14
  - `order`:销售单号、日期、状态、金额、币种、公司及是否完成财务对账。
15
- - `invoice_applications`:直接或联合开票申请、申请日期、申请金额和真实流程状态。
15
+ - `invoice_applications`:直接或联合开票申请、申请日期、申请金额、真实流程状态,以及 `开票申请ID`、`开票主体ID`、`开票主体`、`购买方`、`详情链接`。按 FPSQ/购买方独立查询时改用 [开票申请查询](vps-sales-invoice-status.md)。
16
16
  - `invoices_and_accounting_moves`:发票/退款单对应的凭证号、发票日期、会计日期、过账状态、付款状态、未税金额、税额、含税金额、待付金额、借贷合计、冲销关系及附件摘要。
17
17
  - `accounting_lines`:凭证分录的科目编码、科目名称、摘要、借方、贷方、余额和外币金额;单次最多 1000 条,必须检查 `accounting_lines_pagination.truncated`。
18
18
  - 开票申请状态、发票过账状态、付款状态是三套不同状态,回答时禁止混用。
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: vps-daily-report-assistant
3
3
  description: |
4
- 面向 Vertu 日报系统的查询与提交。用户要查每日应填未交或打卡未交日报名单、本部门日报、员工日报、自己的日报、可汇报任务、提交今日日报、管理员删除当日日报测试数据、同步日报助手卡片时使用。入口 CLI 为 `vps-work report +…`,当前提供 `+missing-daily-reports`、`+checkin-missing`、`+mine`、`+department`、`+tasks`、`+submit`、`+delete-today`、`+sync`。触发词:日报、部门日报、应填未交、未交日报、打卡未交、员工日报、我的日报、日报任务、提交日报、删除今日日报、同步日报助手、daily report。
4
+ 面向 Vertu 日报系统的查询与提交。用户要查每日应填未交或打卡未交日报名单、本部门日报、员工日报、自己的日报、可汇报任务、提交今日日报、管理员删除当日日报测试数据、同步日报助手卡片时使用。入口 CLI 为 `vps-work report +…`,当前提供 `+missing-daily-reports`、`+checkin-missing`、`+mine`、`+department`、`+tasks`、`+submit`、`+submit-existing-progress`、`+delete-today`、`+sync`。触发词:日报、部门日报、应填未交、未交日报、打卡未交、员工日报、我的日报、日报任务、提交日报、删除今日日报、同步日报助手、daily report。
5
5
  metadata:
6
6
  requires:
7
7
  bins: ['vps-work']
@@ -22,6 +22,7 @@ metadata:
22
22
  - 查当前登录用户所在部门及子部门成员日报:`+department`。
23
23
  - 查我可以填写日报的未完成研发任务:`+tasks`。
24
24
  - 提交今日日报并推送当前用户部门群:`+submit`。
25
+ - 任务进度已经逐条更新后,汇总当天进展并提交日报:`+submit-existing-progress`。
25
26
  - 管理员删除今天的日报测试数据:`+delete-today`。
26
27
  - 同步日报助手会话与今日日报卡片:`+sync`。
27
28
 
@@ -37,16 +38,28 @@ metadata:
37
38
  | `+department --date YYYY-MM-DD --limit 500` | `GET /v1/daily-report/department` | 默认查当前用户部门树;`--department-path` 需要指定部门权限,`--all-departments` 需要全公司权限 |
38
39
  | `+tasks --limit 50` | `GET /v1/daily-report/tasks` | 我的可汇报研发任务 |
39
40
  | `+submit` | `POST /v1/daily-report/submit` | 提交今日日报,更新任务进度并推送本系统 IM 当前用户部门群 |
41
+ | `+submit-existing-progress` | `POST /v1/daily-report/submit-existing-progress` | 汇总今天已有的任务进度活动,提交日报并推送部门群,不重复更新任务 |
40
42
  | `+delete-today --confirm` | `POST /v1/daily-report/admin/delete-today` | 管理员删除今天的日报提交记录;默认撤回本系统 IM 日报消息 |
41
43
  | `+sync` | `POST /v1/daily-report/sync` | 同步日报助手会话与今日日报卡片 |
42
44
 
43
45
  ## 提交日报
44
46
 
47
+ 推荐的“更新任务 → 绑定 OKR → 提交日报”流程:
48
+
49
+ ```bash
50
+ vps-work task +tc-okr-options --period 2026-09
51
+ vps-work task +tc-task-update --task-id <uuid> --progress 40 --activity-content "完成接口联调,耗时:2.5h" --okr-kr-id <kr_id>
52
+ # 当天至少记录三条独立进展后提交,提交动作不会再次改变任务进度
53
+ vps-work report +submit-existing-progress --tomorrow "继续联调并完成上线验证"
54
+ ```
55
+
56
+ 需要在提交日报时一并更新任务进度时,使用普通 `+submit`:
57
+
45
58
  单任务提交:
46
59
 
47
60
  ```bash
48
61
  vps-work report +submit \
49
- --task-id 2454 \
62
+ --task-id 11111111-1111-4111-8111-111111111111 \
50
63
  --progress 80 \
51
64
  --spent-hours 6 \
52
65
  --content "今日进展" \
@@ -57,7 +70,7 @@ vps-work report +submit \
57
70
 
58
71
  ```bash
59
72
  vps-work report +submit \
60
- --items-json '[{"task_id":2454,"progress":80,"spent_hours":3,"content":"接口联调"},{"task_id":2455,"progress":60,"spent_hours":3,"content":"问题修复"}]' \
73
+ --items-json '[{"task_id":"11111111-1111-4111-8111-111111111111","progress":80,"spent_hours":3,"content":"接口联调\n补充测试"},{"task_id":"22222222-2222-4222-8222-222222222222","progress":60,"spent_hours":3,"content":"问题修复"}]' \
61
74
  --tomorrow "继续联调并补充测试"
62
75
  ```
63
76
 
@@ -65,7 +78,7 @@ vps-work report +submit \
65
78
 
66
79
  | 字段 | 说明 |
67
80
  |------|------|
68
- | `task_id` | PMS 任务数字 ID |
81
+ | `task_id` | `+tasks` 返回的任务中心 UUID |
69
82
  | `progress` | 目标进度,0-100,必须高于当前进度 |
70
83
  | `spent_hours` | 今日投入小时数,(0, 24] |
71
84
  | `content` | 今日工作内容;多行会拆成多条进度 |
@@ -111,7 +124,8 @@ vps-work report +delete-today --confirm
111
124
  4. 提交日报前检查 `task_id`、`progress`、`spent_hours`、`content`、`tomorrow` 是否齐全;信息不全先追问。
112
125
  5. 用户要“删除今天的日报/重测提交”时,仅管理员可用 `vps-work report +delete-today --confirm`;除非明确要求重放进度,否则不要加 `--with-progress`。
113
126
  6. 提交后如需要刷新卡片,调用 `vps-work report +sync`。
114
- 7. 输出时先说明结论,再列日期、人员、部门、任务 ID、项目、进度等关键字段。
127
+ 7. 如果已经通过 `task +tc-task-update` 记录了当天进展,使用 `+submit-existing-progress`;不要再用相同进度调用普通 `+submit`。
128
+ 8. 输出时先说明结论,再列日期、人员、部门、任务 ID、项目、进度等关键字段。
115
129
 
116
130
  ## 硬约束
117
131
 
@@ -119,7 +133,9 @@ vps-work report +delete-today --confirm
119
133
  - `+missing-daily-reports` 是“每日应填但未交”口径;`+checkin-missing` 是“打卡但未交”口径,二者不可混用。
120
134
  - `+mine` 的 `--user-id` 是 `res.users.id`,仅管理员可查询其他用户;姓名解析不在本命令内完成。
121
135
  - `+department` 默认范围由当前登录用户决定;`--department-path` 需要 `daily_report.departments.selected:read`,`--all-departments` 需要 `daily_report.departments.all:read`,两者不能同时使用。
122
- - `task_id` 是 PMS 任务数字 ID,不是 task-center UUID。
136
+ - `task_id` 必须使用 `+tasks` 返回的任务中心 UUID。
137
+ - 今日工作至少三条;同一任务的 `content` 可用换行拆成多条独立进展。明日计划至少一条。
138
+ - `--dry-run` / `--validate-only` 只输出完整提交预览,不更新任务、不保存日报、不推送群消息。
123
139
  - `+delete-today` 是管理员测试命令,必须带 `--confirm`;默认不删除任务进度,`--with-progress` 只在明确需要重放提交进度时使用。
124
140
  - 写操作前若用户意图不明确或字段缺失,先确认再提交。
125
141
  - 日期参数使用 `YYYY-MM-DD`。
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: vps-daily-sales-report
3
3
  description: |
4
- 公司级全部门销售总览,含销售额/销量、新老客销售占比、环比/同比、商品系列周月趋势、IOT 商品页订单数、已对账销售收款明细、销售单开票申请状态、正式发票、会计凭证与付款状态、含/不含代理折扣、最新组织渠道、高定销售、智能穿戴、大折叠黄金3/黄金4/金钻、部门目标达成率。支持任意时间窗口(CLI: period 枚举或 YYYY-MM-DD、--start-date/--end-date;execute/API: start_date/end_date),数据基于代码化销售行口径 + 管理端同源的部门目标,通过 `vps-work sales +…` 取数。本技能面向公司全部门汇总,并有新客/老客结构、环比/同比/目标达成。触发词:总销日报、公司日销、日报、今日销售、本月销售、已对账收款、销售收款明细、申请开票、开票状态、开票审批、正式发票、会计凭证、付款状态、商品趋势、IOT、商品订单数、商品折扣、含代理、不含代理、高定、智能穿戴、大折叠、黄金3、黄金4、金钻、新客销售、上月、上周、环比、同比、部门达成率、全部门销售。
4
+ 公司级全部门销售总览,含销售额/销量、新老客销售占比、环比/同比、商品系列周月趋势、IOT 商品页订单数、已对账销售收款明细、按销售单/FPSQ/购买方查开票申请及详情链接、商品 VSN/IMEI 与激活地址、正式发票、会计凭证与付款状态、含/不含代理折扣、最新组织渠道、高定销售、智能穿戴、大折叠黄金3/黄金4/金钻、部门目标达成率。支持任意时间窗口(CLI: period 枚举或 YYYY-MM-DD、--start-date/--end-date;execute/API: start_date/end_date),数据基于代码化销售行口径 + 管理端同源的部门目标,通过 `vps-work sales +…` 取数。本技能面向公司全部门汇总,并有新客/老客结构、环比/同比/目标达成。触发词:总销日报、公司日销、日报、今日销售、本月销售、已对账收款、销售收款明细、申请开票、FPSQ、购买方、申请单直达、VSN、IMEI、激活地址、开票状态、开票审批、正式发票、会计凭证、付款状态、商品趋势、IOT、商品订单数、商品折扣、含代理、不含代理、高定、智能穿戴、大折叠、黄金3、黄金4、金钻、新客销售、上月、上周、环比、同比、部门达成率、全部门销售。
5
5
  ---
6
6
 
7
7
  # 公司总销日报
@@ -12,6 +12,12 @@ description: |
12
12
 
13
13
  用户要查公司级全部门销售总览:今日/本月/上月/任意时间段销售额/量、部门排名、环比同比、目标达成。销售实际走代码化销售行口径,部门目标默认来自管理端“部门业绩目标”同源的 `部门OKR_1`,统一通过 `vps-work sales +…` 取数。
14
14
 
15
+ ## 订单设备与开票申请入口
16
+
17
+ - 查询订单对应商品、VSN、IMEI、激活地址:使用 `sales +orders`,先读 [订单设备与激活信息](reference.md#订单设备与激活信息)。地址缺失与权限受限不同,不能用收货地址补齐。
18
+ - 查询 FPSQ 申请单 ID、购买方开票申请或生成申请详情链接:使用 `sales +invoice-status`,先读 [开票申请详情与购买方查询](reference.md#开票申请详情与购买方查询)。服务已返回真实 ID 和详情链接,不需要用户抄 ID,也不需要放开 SQL 表白名单。
19
+ - 旧版 CLI 不认识新参数时,使用该参考中的 `exec --skill vps-daily-sales-report --script sales_invoice_status` 示例调用当前服务。新接口信息优先于历史“FPSQ 无 ID 通道”的说明。
20
+
15
21
  ## 业绩口径
16
22
 
17
23
  下列脚本**默认不含定金**:`LEFT JOIN mv_product` 过滤 `"定金"='是'` 的 SKU:
@@ -43,7 +49,7 @@ IOT 用 `product_category=IOT配件`,分类包含手链。`monthly_daily_trend
43
49
 
44
50
  ## 时间窗口(统一约定)
45
51
 
46
- 所有脚本统一接受下列时间参数(优先级由高到低):
52
+ 销售业绩聚合脚本接受下列时间参数(优先级由高到低)。`sales_invoice_status` 按单号/购买方查询,无默认日期窗口,不接受这些时间过滤;`order_list` 默认今天,`reconciled_sales_receipts` 固定最近30天。
47
53
 
48
54
  1. `--start-date` + `--end-date`(YYYY-MM-DD,闭区间;execute/API 为 `start_date` + `end_date`)— 完全自定义
49
55
  2. `--period YYYY-MM-DD` 或单独 `--start-date YYYY-MM-DD` — 单日查询
@@ -55,17 +61,17 @@ IOT 用 `product_category=IOT配件`,分类包含手链。`monthly_daily_trend
55
61
 
56
62
  ## 部门过滤(统一约定)
57
63
 
58
- 所有脚本统一接受 `dept_l1 / dept_l2 / dept_l3 / dept_l4 / dept_l5`,全部可选,按 AND 拼接。空串等同于不过滤。
64
+ 支持销售范围过滤的脚本接受 `dept_l1 / dept_l2 / dept_l3 / dept_l4 / dept_l5`,全部可选,按 AND 拼接。空串等同于不过滤。
59
65
 
60
66
  不要使用 `dept`、`department`、`dept_name`、`department_name` 表示销售部门;这些字段无法区分一级/二级/三级/四级部门,服务端会拒绝。用户只说“部门”时,先确认层级或用组织结构查询定位。
61
67
 
62
68
  ## 硬约束
63
69
 
64
70
  - **中文标识符双引号**:销售结果中的 `"销售日期"`、`"一级部门"` 等中文字段按 PostgreSQL 规则处理
65
- - **取数走 CLI 封装**:脚本通过 `vps-work sales +…` 直连规范视图执行,无需手写连接
71
+ - **取数走 VPS 业务工具**:通过 `vps-work sales +…` 查询 VPS 平台提供的销售数据,连接与认证由宿主管理,无需配置数据库连接
66
72
  - **行数控制**:可在 SQL 内自由使用 `LIMIT n`、`LIMIT n OFFSET m`、`OFFSET m LIMIT n`、`LIMIT %s`、`FETCH FIRST n ROWS ONLY` 等任一标准写法;若末尾未写,服务端按 `default_sql_limit`(500)追加默认上限,并以 `max_sql_rows`(2000)做硬截断兜底
67
73
  - **OKR JOIN 必须先缩销售**:禁止目标明细直连未聚合销售行;必须先按 部门 × 月份 聚合销售,再与目标合并
68
- - **列表必须翻页**:返回 `pagination.has_more=true` 时,用 `page=next_page` 继续查询,不能把第一页 `rows` 当全量
74
+ - **列表必须翻页**:返回 `pagination.has_more=true` 时,按接口使用 `page=next_page` 或 `offset=next_offset`(开票申请),不能把第一页 `rows` 当全量
69
75
  - **排除项必须披露**:凡使用默认或显式排除(包括不含定金、部门/人员/商品过滤、仅保留有实绩行),回答必须逐项说明;分页、Top N、权限范围造成的非全量覆盖也必须说明。若计算排除后指标,必须同时给出排除项及排除前后的口径,不能把它称为未加限定的“公司整体”。
70
76
 
71
77
  ## 常见坑点
@@ -16,10 +16,10 @@
16
16
  | `service_dept_sales_summary` | 问"权益部/服务部销售业绩" | `vps-work sales +service-sales --period last_n_days --days 30` | 笔数/金额/经办人 Top |
17
17
  | `monthly_daily_trend` | 问"按日/周/月的销售走势、环比、渠道×款式明细、IOT 订单或商品折扣" | `vps-work sales +trend --period this_year --grain month --group-by org_channel --breakdown-by product_style1` | 日/周/月 × 主维度 × 明细维度,含销量、销额、去重订单、商品页订单行数、含/不含代理折扣、上期、环比、累计、同期 |
18
18
  | `employee_ranking_top_bottom` | 问"销售员 TOP10 / 末位 / 红黑榜" | `vps-work sales +ranking --period this_month --metric amount --top-n 10 --bottom-n 10` | TOP+BOTTOM 销售员双榜 |
19
- | `order_list` | 问"订单明细/某商品订单/某客户订单/物流单号对应订单" | `vps-work sales +orders --period this_month --logistics-no SF123` | 订单号、商品、`商品码`、客户脱敏信息、金额、分摊比例、客户来源、二级客户来源、最新发货日期、物流单号摘要 |
19
+ | `order_list` | 问"订单明细/订单下商品销量金额/期间商品销量金额/某客户订单" | `vps-work sales +orders --period this_month --group-by product --product-code SKU001` | 默认按订单;`--group-by order-product` 按订单和商品,`--group-by product` 按期间商品汇总,含数量、分摊数量和金额 |
20
20
  | `account_sales_detail` | 问“某会计科目对应哪些销售单明细” | `vps-work sales +account-orders --account-code 6001 --period this_month` | 科目行借贷余额、凭证、关联方式、销售订单行与发票行金额 |
21
21
  | `reconciled_sales_receipts` | 问“最近30天已对账销售单收了哪些款” | `vps-work sales +reconciled-receipts --order-types sale,service --invoiced true` | 按销售单、收款方式和收款主体聚合的收款明细;退差价只扣单据金额,真实退货整单排除 |
22
- | `sales_invoice_status` | 问“某销售单是否申请开票、审批到哪一步” | `vps-work sales +invoice-status --order-names SO001,SO002` | 精确映射销售单与直接/联合开票申请,返回真实申请状态;单号必填,最多100个 |
22
+ | `sales_invoice_status` | 问“某销售单是否申请开票、审批到哪一步” | `vps-work sales +invoice-status --order-names SO001,SO002` | 按销售单、FPSQ 单号或购买方查询,返回申请 ID、开票主体、购买方、真实状态和详情链接;至少一个筛选条件,单号最多100个 |
23
23
  | `order_finance` | 问“某销售单的开票申请、发票、会计凭证和付款状态” | `vps-work sales +order-finance --order-no SO001` | 返回订单财务全景;精确单号必填,并按当前用户销售数据范围校验 |
24
24
  | `order_logistics` | 明确问"某订单/某物流单号/某出库单号的发货明细" | `vps-work sales +order-logistics --order-no SO123` 或 `vps-work sales +order-logistics --logistics-no SF123` | 订单号、下单时间、销售员、客户掩码信息、出库单、物流单号、发货商品与收货信息 |
25
25
 
@@ -77,7 +77,11 @@
77
77
  | `top_n` / `bottom_n` | int | 10 | employee_ranking_top_bottom | TOP / BOTTOM 各取 N |
78
78
  | `exclude_zero` | bool | true | employee_ranking_top_bottom | 末位榜是否排除 0 |
79
79
  | `order_no` | string | "" | order_list / order_logistics | 订单号;`order_logistics` 中也可匹配出库单号 |
80
+ | `product_code` | string | "" | order_list | 商品编码精确匹配;明确的单品优先使用此项 |
81
+ | `group_by` | string | order | order_list | `order`=订单列表;`order_product`=每张订单的各商品按销售人员、业绩部门、分摊比例等差异分行;`product`=期间各商品总计;`product_salesperson`=期间商品按销售人员和业绩部门分组。CLI 可传 `--group-by order-product` / `product` / `product-salesperson` |
82
+
80
83
  | `logistics_no` | string | "" | order_list / order_logistics | 物流单号或出库单号;订单查询用它反查订单摘要 |
84
+ | `is_apportioned` | bool | 未限制 | order_list | true=只返回有有效分摊记录的订单,false=只返回未分摊订单;CLI 参数为 `--is-apportioned true|false` |
81
85
  | `department` | string | "" | order_logistics | 兼容旧用法;部门/销售员维度一般优先用 `order_list` |
82
86
  | `salesperson` | string | "" | order_list / order_logistics | 销售员姓名 / 登录名包含匹配;批量订单优先用 `order_list` |
83
87
  | `user_id` | int | "" | order_logistics | 兼容旧用法:销售员 用户 ID 精确匹配 |
@@ -92,6 +96,10 @@
92
96
  | `limit` | int | 20/30/100/500 | 全部 | 返回行数上限;`dept_target_achievement` 默认 500 |
93
97
  | `page` | int | 1 | 返回列表的脚本 | 页码,从 1 开始;配合 `pagination.next_page` 翻页 |
94
98
 
99
+ 订单内查单品:`vps-work sales +orders --order-no SO001 --product-code SKU001 --group-by order-product --period this_month`。
100
+
101
+ 期间查单品:`vps-work sales +orders --start-date 2026-09-01 --end-date 2026-09-30 --product-code SKU001 --group-by product`。`数量`沿用销售业绩原单归属口径;`分摊数量`按业绩分摊比例计算,`金额`为实际业绩金额。指定部门或销售员时,两种数量可能不同。
102
+
95
103
  ## 五、CLI Shortcut 默认时间
96
104
 
97
105
  | Shortcut | 默认时间 |
@@ -186,3 +194,49 @@ Content-Type: application/json
186
194
  - 返回 `pagination` 的脚本必须检查 `has_more`;为 true 时继续请求 `page=next_page`。
187
195
  - **达成率 = 实际销额 / 期间目标 × 100**
188
196
  - 月/季/年 → 整月目标不折算;日/周/custom → 按覆盖天数加权折算
197
+
198
+ ### 订单设备与激活信息
199
+
200
+ `order_list`(`vps-work sales +orders`)在 `order` / `order_product` 粒度返回 `设备明细` 数组,每台设备包含 `order_line_id`、`product_id`、`product_code`(商品号/商品码)、`vsn`、`imei1`、`imei2`、`activation_time`、`activation_country`、`activation_province`、`activation_city`、`activation_address`、`address_access`。期间商品汇总不返回设备数组。
201
+
202
+ - `--vsn` / 参数 `vsn`:VSN 包含匹配。
203
+ - `--imei` / 参数 `imei`:IMEI1 或 IMEI2 包含匹配。
204
+ - `--activation-address` / 参数 `activation_address`:国家、省、市或激活地址原文包含匹配,沿用客户信息权限;本人权限仅允许本人订单。多个条件必须命中同一台设备。
205
+ - `--product-code`:商品码精确匹配;`--order-no`:订单号包含匹配。日期范围仍生效,默认今天。
206
+
207
+ 关联来源为业务销售行 VSN、最新 VSN 索引、按 IMEI1 匹配的最新手机激活台账(国家/省/市)及该 VSN 和商品的最新激活日志(地址原文)。激活时间优先取台账,缺失时取日志。这里是设备当前可查的激活信息,不是下单时地址快照;地址原文不保证到街道,不使用收货地址补齐。缺失字段为 null,无可关联设备时数组为空,不代表未激活。无地址权限时位置字段为 null、`address_access=restricted`,有权限为 `allowed`。
208
+
209
+ 设备条件筛选命中的订单商品销售行,金额和数量仍按原销售口径计算;设备数组仅列出符合设备条件的记录。未关联当前销售明细的历史订单返回空数组。
210
+
211
+ ```bash
212
+ vps-work sales +orders --period this_month --order-no SO001
213
+ vps-work sales +orders --period this_year --product-code SKU001 --vsn VSN001
214
+ vps-work sales +orders --period this_year --imei 860000000000001
215
+ vps-work sales +orders --period this_month --activation-address 上海
216
+ ```
217
+
218
+
219
+ ### 开票申请详情与购买方查询
220
+
221
+ `vps-work sales +invoice-status` 支持以下条件,多个条件取交集,无默认30天窗口:
222
+
223
+ - `--order-names` / `order_names`:销售单号精确匹配,逗号分隔或服务端字符串数组,最多100个。
224
+ - `--application-names` / `application_names`:FPSQ 申请单号精确匹配,同样最多100个。
225
+ - `--buyer` / `buyer`:购买方名称包含匹配,`%`、`_` 按普通字符处理。
226
+ - `--limit` / `--offset`:分页;默认500行,偏移0。
227
+
228
+ 至少提供一种条件。保留原财务读取权限 `sales.account_orders:read`,不开放通用 SQL 白名单。按销售单查询保留“未找到销售单/未申请”的原有结果;增加申请/购买方条件后仅返回匹配申请。仅按申请/购买方查询时,每张申请一行,`关联销售单` 列出直接/联合订单,没有关联销售单的申请也可返回;不存在的申请返回空结果。`是否最新申请` 仅在销售单查询模式提供,其余为 null。
229
+
230
+ 新增字段:`开票申请ID`、`开票主体ID`、`开票主体`、`购买方`、`详情链接`。开票主体取申请的 `invoice_entity`,不推断销售单公司。链接使用真实数字 ID 与稳定动作标识 `vt_sale_order.action_invoice_application`;不写死 action 数字,不自动添加 cids。服务端配置 ERP 地址时返回完整链接,否则返回相对 ERP 的 `/web#...` 路径。无申请记录时链接为 null。打开页面仍需 ERP 登录和记录权限,不能将查到 ID 等同于已获页面访问权限。
231
+
232
+ ```bash
233
+ vps-work sales +invoice-status --application-names FPSQ2026062203845,FPSQ2026062203846
234
+ vps-work sales +invoice-status --buyer 温州森诺 --limit 100
235
+ ```
236
+
237
+ 尚未升级 CLI 时,可使用已有的通用执行入口调用新服务:
238
+
239
+ ```bash
240
+ vps-work exec --skill vps-daily-sales-report --script sales_invoice_status --param buyer=温州森诺
241
+ vps-work exec --skill vps-daily-sales-report --script sales_invoice_status --param application_names=FPSQ2026062203845
242
+ ```