weaver-work-cli 0.1.8 → 0.1.20
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/README.md +11 -11
- package/dist/cmd/action.js +76 -0
- package/dist/cmd/index.js +2 -0
- package/dist/cmd/refs/index.js +103 -0
- package/dist/cmd/register.js +31 -0
- package/dist/cmd/root.js +1 -0
- package/dist/cmd/setup/index.js +25 -5
- package/dist/cmd/skills/index.js +37 -5
- package/dist/core/connector.js +54 -0
- package/dist/core/context.js +2 -1
- package/dist/core/input.js +48 -0
- package/dist/core/output.js +298 -3
- package/dist/core/refs.js +432 -0
- package/dist/core/state.js +26 -0
- package/dist/internal/e10/auth/commands.js +2 -2
- package/dist/internal/e10/auth/session.js +14 -0
- package/dist/internal/e10/auth/xiaoe.js +116 -51
- package/dist/internal/e10/request-runtime.js +5 -0
- package/dist/internal/skills/install.js +148 -28
- package/dist/internal/skills/reader.js +12 -9
- package/dist/shortcuts/blog/continuation.js +74 -0
- package/dist/shortcuts/blog/errors.js +60 -0
- package/dist/shortcuts/blog/host.js +175 -0
- package/dist/shortcuts/blog/index.js +324 -0
- package/dist/shortcuts/blog/manifest.js +415 -0
- package/dist/shortcuts/blog/operations/comment.js +125 -0
- package/dist/shortcuts/blog/operations/comments.js +31 -0
- package/dist/shortcuts/blog/operations/edit.js +131 -0
- package/dist/shortcuts/blog/operations/export.js +46 -0
- package/dist/shortcuts/blog/operations/like.js +80 -0
- package/dist/shortcuts/blog/operations/publish.js +157 -0
- package/dist/shortcuts/blog/operations/query-user.js +58 -0
- package/dist/shortcuts/blog/operations/query.js +96 -0
- package/dist/shortcuts/blog/operations/registry.js +54 -0
- package/dist/shortcuts/blog/operations/remind.js +176 -0
- package/dist/shortcuts/blog/operations/shared.js +330 -0
- package/dist/shortcuts/blog/operations/types.js +16 -0
- package/dist/shortcuts/blog/operations.js +9 -0
- package/dist/shortcuts/caizhilian/continuation.js +70 -0
- package/dist/shortcuts/caizhilian/errors.js +53 -0
- package/dist/shortcuts/caizhilian/host.js +190 -0
- package/dist/shortcuts/caizhilian/index.js +186 -0
- package/dist/shortcuts/caizhilian/manifest.js +42 -0
- package/dist/shortcuts/caizhilian/operations/api.js +801 -0
- package/dist/shortcuts/caizhilian/operations/confirm.js +75 -0
- package/dist/shortcuts/caizhilian/operations/files.js +306 -0
- package/dist/shortcuts/caizhilian/operations/form.js +170 -0
- package/dist/shortcuts/caizhilian/operations/handlers/collab.js +611 -0
- package/dist/shortcuts/caizhilian/operations/handlers/document.js +890 -0
- package/dist/shortcuts/caizhilian/operations/handlers/file-collection.js +346 -0
- package/dist/shortcuts/caizhilian/operations/handlers/file-upload.js +192 -0
- package/dist/shortcuts/caizhilian/operations/handlers/fileops.js +570 -0
- package/dist/shortcuts/caizhilian/operations/handlers/folder.js +969 -0
- package/dist/shortcuts/caizhilian/operations/handlers/search.js +318 -0
- package/dist/shortcuts/caizhilian/operations/handlers/setting-attach.js +294 -0
- package/dist/shortcuts/caizhilian/operations/handlers/setting-backup.js +423 -0
- package/dist/shortcuts/caizhilian/operations/handlers/setting-encrypt.js +238 -0
- package/dist/shortcuts/caizhilian/operations/handlers/setting-storage.js +364 -0
- package/dist/shortcuts/caizhilian/operations/handlers/system.js +106 -0
- package/dist/shortcuts/caizhilian/operations/registry.js +151 -0
- package/dist/shortcuts/caizhilian/operations/shared.js +408 -0
- package/dist/shortcuts/caizhilian/operations/specs.js +623 -0
- package/dist/shortcuts/caizhilian/operations/types.js +57 -0
- package/dist/shortcuts/caizhilian/operations.js +21 -0
- package/dist/shortcuts/contract/continuation.js +70 -0
- package/dist/shortcuts/contract/errors.js +62 -0
- package/dist/shortcuts/contract/host.js +205 -0
- package/dist/shortcuts/contract/index.js +318 -0
- package/dist/shortcuts/contract/manifest.js +596 -0
- package/dist/shortcuts/contract/operations/clause-reference.js +78 -0
- package/dist/shortcuts/contract/operations/form-info.js +371 -0
- package/dist/shortcuts/contract/operations/read.js +118 -0
- package/dist/shortcuts/contract/operations/registry.js +43 -0
- package/dist/shortcuts/contract/operations/request.js +300 -0
- package/dist/shortcuts/contract/operations/shared.js +133 -0
- package/dist/shortcuts/contract/operations/subject.js +157 -0
- package/dist/shortcuts/contract/operations/template.js +81 -0
- package/dist/shortcuts/contract/operations/types.js +7 -0
- package/dist/shortcuts/contract/operations/workflow.js +52 -0
- package/dist/shortcuts/contract/operations/write.js +131 -0
- package/dist/shortcuts/contract/operations.js +8 -0
- package/dist/shortcuts/hrm/index.js +83 -2
- package/dist/shortcuts/hrm/operations/employee.js +160 -30
- package/dist/shortcuts/hrm/operations/org.js +17 -3
- package/dist/shortcuts/hrm/operations/position.js +18 -2
- package/dist/shortcuts/index.js +5 -1
- package/dist/shortcuts/invoice/index.js +74 -36
- package/dist/shortcuts/invoice/manifest.js +674 -4
- package/dist/shortcuts/invoice/markdown.js +172 -0
- package/dist/shortcuts/invoice/operations/add.js +3 -3
- package/dist/shortcuts/invoice/operations/delete.js +2 -2
- package/dist/shortcuts/invoice/operations/get.js +10 -2
- package/dist/shortcuts/invoice/operations/import.js +2 -2
- package/dist/shortcuts/invoice/operations/registry.js +8 -1
- package/dist/shortcuts/invoice/operations/reim-auto.js +719 -0
- package/dist/shortcuts/invoice/operations/reim.js +85 -2
- package/dist/shortcuts/invoice/operations/shared.js +75 -8
- package/dist/shortcuts/invoice/operations/update.js +2 -2
- package/dist/shortcuts/invoice/operations/upload-and-validate.js +93 -0
- package/dist/shortcuts/invoice/operations/validate-preview.js +2 -1
- package/dist/shortcuts/invoice/projection.js +328 -0
- package/dist/shortcuts/meeting/host.js +18 -1
- package/dist/shortcuts/meeting/manifest.js +30 -18
- package/dist/shortcuts/meeting/operations/eb/catalog.js +390 -0
- package/dist/shortcuts/meeting/operations/eb/conflict.js +389 -0
- package/dist/shortcuts/meeting/operations/eb/factory.js +389 -0
- package/dist/shortcuts/meeting/operations/eb/index.js +22 -0
- package/dist/shortcuts/meeting/operations/env.js +8 -1
- package/dist/shortcuts/meeting/operations/registry.js +2 -0
- package/dist/shortcuts/meeting/operations/signs.js +216 -2
- package/dist/shortcuts/workflow/endpoints.js +2 -1
- package/dist/shortcuts/workflow/errors.js +8 -2
- package/dist/shortcuts/workflow/host.js +6 -3
- package/dist/shortcuts/workflow/operations/form.js +931 -589
- package/dist/shortcuts/workflow/operations/read.js +128 -205
- package/dist/shortcuts/workflow/operations/shared.js +21 -0
- package/dist/shortcuts/workflow/operations/write.js +750 -52
- package/dist/shortcuts/workflow/operations.js +36 -10
- package/docs/_catalog.md +6 -0
- package/docs/agent-invoice.md +5 -1
- package/docs/agent-io-channel-design.md +468 -0
- package/docs/agent-skill-install.md +9 -4
- package/docs/blog.md +123 -0
- package/docs/caizhilian.md +304 -0
- package/docs/contract.md +91 -0
- package/docs/e10-auth.md +2 -2
- package/docs/hrm.md +19 -9
- package/docs/invoice.md +5 -1
- package/docs/meeting.md +1 -1
- package/docs/operation-manual.md +15 -3
- package/docs/skill-review-2026-09-19.md +2 -2
- package/package.json +2 -2
- package/scripts/package-skill.mjs +32 -20
- package/skill-template/business-info.json +113 -14
- package/skill-template/domains/blog.md +24 -0
- package/skill-template/domains/caizhilian.md +22 -0
- package/skill-template/domains/jinchengda.md +23 -0
- package/skill-template/domains/meeting.md +1 -1
- package/skill-template/domains/workflow.md +1 -1
- package/skill-template/skill-template.md +1 -1
- package/skills/weaver-e10-blog/SKILL.md +111 -0
- package/skills/weaver-e10-blog/references/blog-comment.md +51 -0
- package/skills/weaver-e10-blog/references/blog-comments.md +39 -0
- package/skills/weaver-e10-blog/references/blog-default-permission.md +38 -0
- package/skills/weaver-e10-blog/references/blog-disabled-capabilities.md +11 -0
- package/skills/weaver-e10-blog/references/blog-edit.md +51 -0
- package/skills/weaver-e10-blog/references/blog-export.md +42 -0
- package/skills/weaver-e10-blog/references/blog-like.md +50 -0
- package/skills/weaver-e10-blog/references/blog-publish.md +52 -0
- package/skills/weaver-e10-blog/references/blog-query-user.md +45 -0
- package/skills/weaver-e10-blog/references/blog-query.md +49 -0
- package/skills/weaver-e10-blog/references/blog-remind.md +51 -0
- package/skills/weaver-e10-blog/references/blog-resolve-users.md +38 -0
- package/skills/weaver-e10-blog/references/source-manifest.json +1540 -0
- package/skills/weaver-e10-caizhilian/SKILL.md +216 -0
- package/skills/weaver-e10-caizhilian/references/agent-entry.md +106 -0
- package/skills/weaver-e10-caizhilian/references/collaboration.md +112 -0
- package/skills/weaver-e10-caizhilian/references/document.md +111 -0
- package/skills/weaver-e10-caizhilian/references/file-collection.md +136 -0
- package/skills/weaver-e10-caizhilian/references/file-upload.md +309 -0
- package/skills/weaver-e10-caizhilian/references/files.md +120 -0
- package/skills/weaver-e10-caizhilian/references/folder.md +118 -0
- package/skills/weaver-e10-caizhilian/references/org-browser.md +104 -0
- package/skills/weaver-e10-caizhilian/references/setting-attach.md +410 -0
- package/skills/weaver-e10-caizhilian/references/setting-backup.md +372 -0
- package/skills/weaver-e10-caizhilian/references/setting-encrypt.md +399 -0
- package/skills/weaver-e10-caizhilian/references/setting-storage.md +603 -0
- package/skills/weaver-e10-caizhilian/references/source-manifest.json +5079 -0
- package/skills/weaver-e10-caizhilian/references/withheld.md +93 -0
- package/skills/weaver-e10-caizhilian/references/write-protocol.md +120 -0
- package/skills/weaver-e10-calendar/SKILL.md +4 -3
- package/skills/weaver-e10-ebuilder-form/SKILL.md +4 -9
- package/skills/weaver-e10-ebuilder-form/references/context-routing.md +0 -9
- package/skills/weaver-e10-ebuilder-form/references/custom-api.md +1 -5
- package/skills/weaver-e10-ebuilder-form/references/fields-browser.md +1 -5
- package/skills/weaver-e10-ebuilder-form/references/list-nlist.md +1 -5
- package/skills/weaver-e10-ebuilder-form/references/openapi-read.md +1 -5
- package/skills/weaver-e10-ebuilder-form/references/openapi-write.md +1 -5
- package/skills/weaver-e10-esb/SKILL.md +2 -2
- package/skills/weaver-e10-esb/references/source-manifest.json +9 -9
- package/skills/weaver-e10-hrm/SKILL.md +54 -6
- package/skills/weaver-e10-hrm/references/employee.md +89 -15
- package/skills/weaver-e10-hrm/references/org.md +7 -4
- package/skills/weaver-e10-hrm/references/position.md +14 -7
- package/skills/weaver-e10-hrm/references/source-manifest.json +1 -1
- package/skills/weaver-e10-jinchengda/SKILL.md +166 -0
- package/skills/weaver-e10-jinchengda/references/contract-agent-entry.md +82 -0
- package/skills/weaver-e10-jinchengda/references/contract-clause-recommender.md +69 -0
- package/skills/weaver-e10-jinchengda/references/contract-clause-reference-save.md +78 -0
- package/skills/weaver-e10-jinchengda/references/contract-disabled-capabilities.md +27 -0
- package/skills/weaver-e10-jinchengda/references/contract-request-status.md +71 -0
- package/skills/weaver-e10-jinchengda/references/contract-request-write.md +94 -0
- package/skills/weaver-e10-jinchengda/references/contract-signing-subjects.md +89 -0
- package/skills/weaver-e10-jinchengda/references/contract-subject-create.md +77 -0
- package/skills/weaver-e10-jinchengda/references/contract-template-selector.md +66 -0
- package/skills/weaver-e10-jinchengda/references/contract-workflow-discovery.md +94 -0
- package/skills/weaver-e10-jinchengda/references/source-manifest.json +259 -0
- package/skills/weaver-e10-jiuchuanhui/SKILL.md +5 -11
- package/skills/weaver-e10-jucailin/SKILL.md +4 -3
- package/skills/weaver-e10-mail/SKILL.md +4 -11
- package/skills/weaver-e10-meeting/SKILL.md +66 -32
- package/skills/weaver-e10-meeting/references/meeting-change.md +5 -2
- package/skills/weaver-e10-meeting/references/meeting-create.md +2 -1
- package/skills/weaver-e10-meeting/references/meeting-eb.md +114 -0
- package/skills/weaver-e10-meeting/references/meeting-lifecycle.md +9 -8
- package/skills/weaver-e10-meeting/references/meeting-receipts-signs.md +35 -7
- package/skills/weaver-e10-meeting/references/meeting-update.md +1 -1
- package/skills/weaver-e10-okr/SKILL.md +6 -11
- package/skills/weaver-e10-okr/references/source-manifest.json +3 -3
- package/skills/weaver-e10-plan/SKILL.md +3 -3
- package/skills/weaver-e10-plan/references/plan-collect-and-compose.md +1 -1
- package/skills/weaver-e10-plan/references/plan-overview-and-routing.md +1 -1
- package/skills/weaver-e10-plan/references/source-manifest.json +13 -13
- package/skills/weaver-e10-qiyecheng/SKILL.md +4 -10
- package/skills/weaver-e10-qiyecheng/references/source-manifest.json +9 -9
- package/skills/weaver-e10-shared/SKILL.md +36 -25
- package/skills/weaver-e10-shared/references/e10-auth-and-session.md +10 -31
- package/skills/weaver-e10-shared/references/json-output-contract.md +20 -11
- package/skills/weaver-e10-shijingran/SKILL.md +2 -1
- package/skills/weaver-e10-shijingran/references/source-manifest.json +9 -9
- package/skills/weaver-e10-skill-maker/SKILL.md +4 -10
- package/skills/weaver-e10-skill-maker/references/business-skill-generation.md +5 -3
- package/skills/weaver-e10-skill-maker/references/module-cli-generation.md +12 -27
- package/skills/weaver-e10-skill-maker/references/module-gap-audit-and-repair.md +17 -5
- package/skills/weaver-e10-skill-maker/references/source-update-detection.md +4 -27
- package/skills/weaver-e10-wenshuding/SKILL.md +5 -16
- package/skills/weaver-e10-wenshuding/references/source-manifest.json +7 -7
- package/skills/weaver-e10-workflow/SKILL.md +32 -37
- package/skills/weaver-e10-workflow/references/source-manifest.json +39 -75
- package/skills/weaver-e10-workflow/references/workflow-batch.md +90 -29
- package/skills/weaver-e10-workflow/references/workflow-create.md +79 -109
- package/skills/weaver-e10-workflow/references/workflow-memory.md +19 -9
- package/skills/weaver-e10-workflow/references/workflow-operate.md +2 -2
- package/skills/weaver-e10-workflow/references/workflow-query.md +38 -20
- package/skills/weaver-e10-workflow/references/workflow-share.md +18 -2
- package/skills/weaver-e10-workflow/references/workflow-view.md +3 -4
- package/skills/weaver-e10-yepiaotong/SKILL.md +75 -39
- package/skills/weaver-e10-yepiaotong/references/invoice-detail.md +22 -1
- package/skills/weaver-e10-yepiaotong/references/invoice-enterprise-list.md +3 -1
- package/skills/weaver-e10-yepiaotong/references/invoice-output-view.md +135 -0
- package/skills/weaver-e10-yepiaotong/references/invoice-personal-list.md +15 -3
- package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +1 -1
- package/skills/weaver-e10-yepiaotong/references/invoice-summarize.py +217 -0
- package/skills/weaver-e10-yepiaotong/references/invoice-upload-and-validate.md +88 -0
- package/skills/weaver-e10-yepiaotong/references/invoice-validation-preview.md +7 -2
- package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +89 -9
- package/skills/weaver-e10-yepiaotong/references/reim-examples.md +1 -0
- package/skills/weaver-e10-yepiaotong/references/reim-expense-subject-rules.md +70 -49
- package/skills/weaver-e10-yepiaotong/references/reim-fast-path.md +119 -0
- package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +55 -36
- package/skills/weaver-e10-yepiaotong/references/reim-matching-rules.md +4 -0
- package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +149 -0
- package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +125 -5
- package/skills/weaver-e10-yepiaotong/references/source-manifest.json +430 -63
- package/skills/weaver-e10-yimiaoban/SKILL.md +4 -12
- package/skills/weaver-e10-yimiaoban/references/source-manifest.json +3 -3
- package/skills/weaver-e10-ziguanjia/SKILL.md +2 -1
- package/skills/weaver-e10-ziguanjia/references/ziguanjia-agent-entry.md +3 -3
- package/tools/weaver-skill-detector-1.0.13/SKILL.md +9 -9
- package/tools/weaver-skill-detector-1.0.13/references/checklist.md +3 -3
- package/tools/weaver-skill-detector-1.0.13/scripts/__pycache__/validate_skill.cpython-313.pyc +0 -0
- package/tools/weaver-skill-detector-1.0.13/scripts/validate_skill.py +98 -91
- package/scripts/sync-connector-skills.mjs +0 -172
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
import { invoiceValidationError } from '../errors.js';
|
|
2
|
+
import { quoteEmployeeIdsInJsonText } from './reim.js';
|
|
3
|
+
import { asNonEmptyString, assertBusinessSuccess, objectFingerprint, optionalEpochSeconds, reqBase, workflow, } from './shared.js';
|
|
4
|
+
/** 总部费用报销流程(实测 2026-09-14/09-21 两次 apply 成功的固化映射)。 */
|
|
5
|
+
export const REIM_TEMPLATE_WORKFLOW_ID = '100003460000000060';
|
|
6
|
+
/** 上述流程的显示名,用于预览时把工作流 ID 渲染成「中文(id)」。 */
|
|
7
|
+
export const REIM_TEMPLATE_WORKFLOW_NAME = '总部费用报销';
|
|
8
|
+
/** 费用类型(科目)默认值:日常消费。 */
|
|
9
|
+
const DEFAULT_SUBJECT_ID = '1256664958132969489';
|
|
10
|
+
/** 币种:人民币。 */
|
|
11
|
+
const CNY_ID = '100001050000000001';
|
|
12
|
+
/** 报销方式选项:银行(个人报销)。 */
|
|
13
|
+
const BANK_OPTION_VALUE = '7';
|
|
14
|
+
/** 明细表「相关发票」字段,用作费用明细子表的结构签名。 */
|
|
15
|
+
const DETAIL_INVOICE_FIELD_ID = '100003720000077896';
|
|
16
|
+
/** 明细表「费用类型(科目)」「币种」字段 ID,用于给明细行渲染中文名。 */
|
|
17
|
+
const DETAIL_SUBJECT_FIELD_ID = '100003720000000360';
|
|
18
|
+
const DETAIL_CURRENCY_FIELD_ID = '100003720000052200';
|
|
19
|
+
/** 主表「报销方式」「相关客户」「合同号」字段 ID。 */
|
|
20
|
+
const MAIN_METHOD_FIELD_ID = '100003720000000084';
|
|
21
|
+
const MAIN_CUSTOMER_FIELD_ID = '100003720000000687';
|
|
22
|
+
const MAIN_CONTRACT_FIELD_ID = '100003720000001376';
|
|
23
|
+
/**
|
|
24
|
+
* 模板固化映射自带的字典值兜底显示名。
|
|
25
|
+
* `row-info`/`superiors` 等实时接口取到名字时优先用实时值,这里只兜实时值缺失的情况。
|
|
26
|
+
* 这些值与 `REIM_TEMPLATE_WORKFLOW_ID` 一样是「总部费用报销」模板的固化口径,不是通用字典。
|
|
27
|
+
*/
|
|
28
|
+
const TEMPLATE_SELECT_OPTIONS = {
|
|
29
|
+
[MAIN_METHOD_FIELD_ID]: {
|
|
30
|
+
'7': '银行(个人报销)',
|
|
31
|
+
'4': '冲销借款',
|
|
32
|
+
'6': '银行(付公司)',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
const TEMPLATE_SUBJECT_NAMES = { [DEFAULT_SUBJECT_ID]: '日常消费' };
|
|
36
|
+
const TEMPLATE_CURRENCY_NAMES = { [CNY_ID]: '人民币' };
|
|
37
|
+
/** 非出差场景固定使用的相关客户(见 skill 文档 Step 4.5),仅作名称兜底。 */
|
|
38
|
+
const TEMPLATE_CUSTOMER_NAMES = { '100504500000311504': '泛微上海' };
|
|
39
|
+
const MONEY_PATTERN = /^-?\d+(?:\.\d{1,2})?$/u;
|
|
40
|
+
function boundedInteger(value, name, options) {
|
|
41
|
+
const parsed = value === undefined ? options.defaultValue : Number(value);
|
|
42
|
+
if (!Number.isInteger(parsed) || parsed < options.min || parsed > options.max) {
|
|
43
|
+
throw invoiceValidationError('input_invalid', `${name} 必须是 ${options.min}-${options.max} 的整数`);
|
|
44
|
+
}
|
|
45
|
+
return parsed;
|
|
46
|
+
}
|
|
47
|
+
function optionalString(value, name, maxLength = 500) {
|
|
48
|
+
if (value === undefined || value === null || value === '')
|
|
49
|
+
return undefined;
|
|
50
|
+
const text = asNonEmptyString(value, name);
|
|
51
|
+
if (text.length > maxLength)
|
|
52
|
+
throw invoiceValidationError('input_invalid', `${name} 长度不能超过 ${maxLength}`);
|
|
53
|
+
return text;
|
|
54
|
+
}
|
|
55
|
+
function toCents(value) {
|
|
56
|
+
if (value === undefined || value === null || value === '')
|
|
57
|
+
return null;
|
|
58
|
+
const match = MONEY_PATTERN.exec(String(value));
|
|
59
|
+
if (!match)
|
|
60
|
+
return null;
|
|
61
|
+
const negative = match[0].startsWith('-');
|
|
62
|
+
const [whole, fraction = ''] = match[0].replace('-', '').split('.');
|
|
63
|
+
return (Number(whole) * 100 + Number(fraction.padEnd(2, '0'))) * (negative ? -1 : 1);
|
|
64
|
+
}
|
|
65
|
+
function centsToMoney(cents) {
|
|
66
|
+
const sign = cents < 0 ? '-' : '';
|
|
67
|
+
const absolute = Math.abs(cents);
|
|
68
|
+
const whole = Math.floor(absolute / 100);
|
|
69
|
+
const fraction = absolute % 100;
|
|
70
|
+
return `${sign}${whole}.${String(fraction).padStart(2, '0')}`;
|
|
71
|
+
}
|
|
72
|
+
function sumMoney(values) {
|
|
73
|
+
let sum = 0;
|
|
74
|
+
let invalid = 0;
|
|
75
|
+
for (const value of values) {
|
|
76
|
+
const cents = toCents(value);
|
|
77
|
+
if (cents === null) {
|
|
78
|
+
invalid += 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
sum += cents;
|
|
82
|
+
}
|
|
83
|
+
return { amount: centsToMoney(sum), invalid };
|
|
84
|
+
}
|
|
85
|
+
function todayInShanghai() {
|
|
86
|
+
return new Intl.DateTimeFormat('en-CA', { timeZone: 'Asia/Shanghai' }).format(new Date());
|
|
87
|
+
}
|
|
88
|
+
function invoiceRecordId(info) {
|
|
89
|
+
const raw = info.fid ?? info.id;
|
|
90
|
+
return raw === undefined || raw === null ? '' : String(raw);
|
|
91
|
+
}
|
|
92
|
+
function cleanFysm(value) {
|
|
93
|
+
return String(value ?? '').replace(/\*/g, '').trim();
|
|
94
|
+
}
|
|
95
|
+
function reimRequestNames(info) {
|
|
96
|
+
const entries = info?.oreim?.list_reim;
|
|
97
|
+
if (!Array.isArray(entries))
|
|
98
|
+
return [];
|
|
99
|
+
return entries
|
|
100
|
+
.map((entry) => (typeof entry?.name === 'string' ? entry.name : ''))
|
|
101
|
+
.filter((name) => name.trim());
|
|
102
|
+
}
|
|
103
|
+
function candidateItem(info) {
|
|
104
|
+
const pro = info?.comm_info?.pro || {};
|
|
105
|
+
const price = info?.comm_info?.price || {};
|
|
106
|
+
return {
|
|
107
|
+
fid: invoiceRecordId(info),
|
|
108
|
+
number: String(info.number ?? ''),
|
|
109
|
+
date: String(pro.date ?? ''),
|
|
110
|
+
total: String(price.total ?? ''),
|
|
111
|
+
buyer: String(info?.comm_info?.buyer?.company ?? ''),
|
|
112
|
+
payer: String(info?.comm_info?.payer?.company ?? ''),
|
|
113
|
+
typeName: String(pro.cname ?? ''),
|
|
114
|
+
status: typeof pro.status === 'number' ? pro.status : Number(pro.status ?? 0),
|
|
115
|
+
valid: typeof pro.valid === 'number' ? pro.valid : Number(pro.valid ?? 0),
|
|
116
|
+
reimRequests: reimRequestNames(info),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/** 全额红冲发票的票面状态值。 */
|
|
120
|
+
const RED_REVERSED_STATUS = 8;
|
|
121
|
+
/** 判断发票是否为全额红冲(`comm_info.pro.status === 8`)。 */
|
|
122
|
+
function isRedReversedInvoice(info) {
|
|
123
|
+
const raw = info?.comm_info?.pro?.status;
|
|
124
|
+
const status = typeof raw === 'number' ? raw : Number(raw ?? 0);
|
|
125
|
+
return status === RED_REVERSED_STATUS;
|
|
126
|
+
}
|
|
127
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
128
|
+
const results = new Array(items.length);
|
|
129
|
+
let cursor = 0;
|
|
130
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
131
|
+
while (cursor < items.length) {
|
|
132
|
+
const index = cursor;
|
|
133
|
+
cursor += 1;
|
|
134
|
+
results[index] = await fn(items[index], index);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
await Promise.all(workers);
|
|
138
|
+
return results;
|
|
139
|
+
}
|
|
140
|
+
function optionalName(value) {
|
|
141
|
+
const text = value === undefined || value === null ? '' : String(value).trim();
|
|
142
|
+
return text || undefined;
|
|
143
|
+
}
|
|
144
|
+
async function fetchSuperiors(host, warnings) {
|
|
145
|
+
const value = await host.invoiceRequest({
|
|
146
|
+
path: '/api/inc/book/getEmployeeSuperiors',
|
|
147
|
+
method: 'GET',
|
|
148
|
+
rewriteJsonText: quoteEmployeeIdsInJsonText,
|
|
149
|
+
operation: 'invoice.reim.template.build',
|
|
150
|
+
});
|
|
151
|
+
assertBusinessSuccess(value, 'reim_superiors_failed', '获取人员上级失败');
|
|
152
|
+
const data = (value?.data && typeof value.data === 'object' ? value.data : value);
|
|
153
|
+
const currentEmployeeId = String(data?.currentEmployee?.id ?? '');
|
|
154
|
+
if (!currentEmployeeId)
|
|
155
|
+
throw invoiceValidationError('superiors_invalid', '未取得当前人员 ID,无法组装报销单');
|
|
156
|
+
const directId = data?.directSuperior?.id === undefined || data?.directSuperior?.id === null ? undefined : String(data.directSuperior.id);
|
|
157
|
+
if (!directId)
|
|
158
|
+
warnings.push('接口未返回 directSuperior(直属上级),报销人上级/承担人直接上级字段留空,需在确认时补充');
|
|
159
|
+
const allSuperiorIds = Array.isArray(data?.allSuperiors)
|
|
160
|
+
? data.allSuperiors.map((item) => String(item?.id ?? '')).filter((id) => id)
|
|
161
|
+
: [];
|
|
162
|
+
if (!allSuperiorIds.length)
|
|
163
|
+
warnings.push('接口未返回 allSuperiors,承担人所有上级字段留空');
|
|
164
|
+
const allSuperiorNames = Array.isArray(data?.allSuperiors)
|
|
165
|
+
? data.allSuperiors.map((item) => optionalName(item?.name) ?? '').filter((name) => name)
|
|
166
|
+
: [];
|
|
167
|
+
return {
|
|
168
|
+
currentEmployeeId,
|
|
169
|
+
currentEmployeeName: optionalName(data?.currentEmployee?.name),
|
|
170
|
+
directSuperiorId: directId,
|
|
171
|
+
directSuperiorName: optionalName(data?.directSuperior?.name),
|
|
172
|
+
allSuperiorIds,
|
|
173
|
+
allSuperiorNames,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
async function resolveWorkflowId(host, input) {
|
|
177
|
+
if (input.workflowId !== undefined) {
|
|
178
|
+
const id = asNonEmptyString(input.workflowId, 'workflowId');
|
|
179
|
+
return { id, name: id === REIM_TEMPLATE_WORKFLOW_ID ? REIM_TEMPLATE_WORKFLOW_NAME : undefined };
|
|
180
|
+
}
|
|
181
|
+
const value = await host.invoiceRequest({
|
|
182
|
+
path: '/api/inc/book/getReimburseWorkflowByRequestIds',
|
|
183
|
+
method: 'POST',
|
|
184
|
+
body: { requestIds: [] },
|
|
185
|
+
operation: 'invoice.reim.template.build',
|
|
186
|
+
});
|
|
187
|
+
assertBusinessSuccess(value, 'reim_workflow_resolve_failed', '解析候选报销流程失败');
|
|
188
|
+
const data = (value?.data && typeof value.data === 'object' ? value.data : value);
|
|
189
|
+
const candidates = Array.isArray(data?.candidateWorkflows) ? data.candidateWorkflows : [];
|
|
190
|
+
if (candidates.length === 0)
|
|
191
|
+
throw invoiceValidationError('workflow_unresolved', '没有可用的候选报销流程');
|
|
192
|
+
if (candidates.length > 1) {
|
|
193
|
+
const names = candidates.map((item) => `${item?.workFlowName ?? ''}(${item?.workFlowId ?? ''})`).join('、');
|
|
194
|
+
throw invoiceValidationError('input_ambiguous', `候选报销流程多于一个,请显式传入 workflowId:${names}`);
|
|
195
|
+
}
|
|
196
|
+
const id = String(candidates[0].workFlowId);
|
|
197
|
+
const name = optionalName(candidates[0].workFlowName)
|
|
198
|
+
?? (id === REIM_TEMPLATE_WORKFLOW_ID ? REIM_TEMPLATE_WORKFLOW_NAME : undefined);
|
|
199
|
+
return { id, name };
|
|
200
|
+
}
|
|
201
|
+
/** 按名称在指定字段的可选项里反查 ID(`fieldRelateSearch` 只支持名称模糊匹配,不支持按 ID 反查)。 */
|
|
202
|
+
async function searchFieldOptionId(host, workflowId, fieldId, name) {
|
|
203
|
+
const value = await host.invoiceRequest({
|
|
204
|
+
path: '/api/government/formStructure/fieldRelateSearch',
|
|
205
|
+
method: 'GET',
|
|
206
|
+
query: { keywords: name, fieldId, formModeType: 'wf', workflowId, nodeId: '100004140000000271' },
|
|
207
|
+
operation: 'invoice.reim.template.build',
|
|
208
|
+
});
|
|
209
|
+
assertBusinessSuccess(value, 'reim_field_search_failed', `按名称「${name}」搜索字段可选项失败`);
|
|
210
|
+
const items = Array.isArray(value?.data) ? value.data : [];
|
|
211
|
+
const ids = Array.from(new Set(items.map((item) => String(item?.id ?? '')).filter((id) => Boolean(id))));
|
|
212
|
+
if (!ids.length)
|
|
213
|
+
throw invoiceValidationError('related_customer_unresolved', `按名称「${name}」没有搜索到相关客户,请改用 relatedCustomerId`);
|
|
214
|
+
if (ids.length > 1) {
|
|
215
|
+
const names = items.map((item) => `${item?.name ?? ''}(${item?.id ?? ''})`).join('、');
|
|
216
|
+
throw invoiceValidationError('input_ambiguous', `名称「${name}」匹配到多个相关客户,请改用 relatedCustomerId:${names}`);
|
|
217
|
+
}
|
|
218
|
+
return ids[0];
|
|
219
|
+
}
|
|
220
|
+
function collectSelectOptions(raw) {
|
|
221
|
+
const collected = {};
|
|
222
|
+
const fields = Array.isArray(raw) ? raw : [];
|
|
223
|
+
for (const field of fields) {
|
|
224
|
+
const fieldId = String(field?.fieldId ?? '');
|
|
225
|
+
const options = field?.options;
|
|
226
|
+
if (!fieldId || !Array.isArray(options) || !options.length)
|
|
227
|
+
continue;
|
|
228
|
+
const map = {};
|
|
229
|
+
for (const option of options) {
|
|
230
|
+
const optionValue = option?.optionValue === undefined || option?.optionValue === null ? '' : String(option.optionValue);
|
|
231
|
+
const optionName = optionalName(option?.optionName);
|
|
232
|
+
if (optionValue && optionName)
|
|
233
|
+
map[optionValue] = optionName;
|
|
234
|
+
}
|
|
235
|
+
if (Object.keys(map).length)
|
|
236
|
+
collected[fieldId] = map;
|
|
237
|
+
}
|
|
238
|
+
return collected;
|
|
239
|
+
}
|
|
240
|
+
async function fetchStructure(host, workflowId) {
|
|
241
|
+
const value = await host.invoiceRequest({
|
|
242
|
+
path: '/api/inc/book/getFormStructure',
|
|
243
|
+
method: 'GET',
|
|
244
|
+
query: { workflowId, nodeId: '1' },
|
|
245
|
+
operation: 'invoice.reim.template.build',
|
|
246
|
+
});
|
|
247
|
+
assertBusinessSuccess(value, 'reim_structure_failed', '获取报销表单结构失败');
|
|
248
|
+
const data = (value?.data && typeof value.data === 'object' ? value.data : value);
|
|
249
|
+
const mainFormId = String(data?.mainFormId ?? '');
|
|
250
|
+
if (!mainFormId)
|
|
251
|
+
throw invoiceValidationError('structure_invalid', '表单结构缺少 mainFormId');
|
|
252
|
+
const subTables = Array.isArray(data?.subTables) ? data.subTables : [];
|
|
253
|
+
const byName = subTables.find((table) => String(table?.subFormName ?? '').includes('费用明细'));
|
|
254
|
+
const bySignature = subTables.find((table) => Array.isArray(table?.subFields)
|
|
255
|
+
&& table.subFields.some((field) => String(field?.fieldId ?? '') === DETAIL_INVOICE_FIELD_ID));
|
|
256
|
+
const target = byName || bySignature;
|
|
257
|
+
if (!target) {
|
|
258
|
+
const names = subTables.map((table) => String(table?.subFormName ?? '')).filter(Boolean).join('、');
|
|
259
|
+
throw invoiceValidationError('structure_invalid', `表单结构中没有可识别的费用明细子表(现有子表:${names || '无'})`);
|
|
260
|
+
}
|
|
261
|
+
const selectOptions = {
|
|
262
|
+
...TEMPLATE_SELECT_OPTIONS,
|
|
263
|
+
...collectSelectOptions(data?.mainFields),
|
|
264
|
+
};
|
|
265
|
+
for (const table of subTables) {
|
|
266
|
+
Object.assign(selectOptions, collectSelectOptions(table?.subFields));
|
|
267
|
+
}
|
|
268
|
+
return {
|
|
269
|
+
mainFormId,
|
|
270
|
+
detailSubFormId: String(target.subFormId),
|
|
271
|
+
detailSubFormName: String(target.subFormName ?? ''),
|
|
272
|
+
selectOptions,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
async function fetchInvoiceInfos(host, invoiceIds) {
|
|
276
|
+
const value = await host.invoiceRequest({
|
|
277
|
+
path: '/api/inc/get',
|
|
278
|
+
method: 'POST',
|
|
279
|
+
body: { ids: invoiceIds, mask: 0, userFlag: '1', isDelFlag: '1', req_base: reqBase() },
|
|
280
|
+
operation: 'invoice.reim.template.build',
|
|
281
|
+
});
|
|
282
|
+
assertBusinessSuccess(value, 'invoice_detail_failed', '获取发票详情失败');
|
|
283
|
+
const infos = Array.isArray(value?.infos) ? value.infos : [];
|
|
284
|
+
const byId = new Map();
|
|
285
|
+
for (const entry of infos) {
|
|
286
|
+
if (entry?.ret?.ret !== 0 || !entry.info)
|
|
287
|
+
continue;
|
|
288
|
+
byId.set(invoiceRecordId(entry.info), entry.info);
|
|
289
|
+
}
|
|
290
|
+
const missing = invoiceIds.filter((id) => !byId.has(id));
|
|
291
|
+
if (missing.length) {
|
|
292
|
+
throw invoiceValidationError('invoice_not_found', `以下发票未找到或无权查看:${missing.join('、')}`);
|
|
293
|
+
}
|
|
294
|
+
return byId;
|
|
295
|
+
}
|
|
296
|
+
async function fetchRowInfo(host, userId, invoiceId) {
|
|
297
|
+
const value = await host.invoiceFormRequest({
|
|
298
|
+
path: '/api/secondev/fna/expense/dataInfo/getRowInfo',
|
|
299
|
+
method: 'POST',
|
|
300
|
+
query: { sf_request_type: 'fetch' },
|
|
301
|
+
fields: { cdzt: userId, requestid: '', invoiceId },
|
|
302
|
+
operation: 'invoice.reim.template.build',
|
|
303
|
+
});
|
|
304
|
+
assertBusinessSuccess(value, 'reim_row_info_failed', `获取发票 ${invoiceId} 行预填失败`);
|
|
305
|
+
return (value?.data && typeof value.data === 'object' ? value.data : value);
|
|
306
|
+
}
|
|
307
|
+
function detailField(fieldId, fieldName, value) {
|
|
308
|
+
return { fieldId, fieldName, value };
|
|
309
|
+
}
|
|
310
|
+
function mainField(fieldId, fieldName, value, optionValue) {
|
|
311
|
+
return optionValue !== undefined
|
|
312
|
+
? { fieldId, fieldName, optionValue }
|
|
313
|
+
: { fieldId, fieldName, value };
|
|
314
|
+
}
|
|
315
|
+
/** 把单个 ID 渲染成「中文(id)」;没有中文名时原样返回 ID,绝不编造。 */
|
|
316
|
+
function renderIdWithName(id, names) {
|
|
317
|
+
const text = id === undefined || id === null ? '' : String(id).trim();
|
|
318
|
+
if (!text)
|
|
319
|
+
return '';
|
|
320
|
+
const name = names[text];
|
|
321
|
+
return name ? `${name}(${text})` : text;
|
|
322
|
+
}
|
|
323
|
+
/** 渲染逗号分隔的 ID 串(如「承担人所有上级」),逐项带中文名。 */
|
|
324
|
+
function renderIdList(value, names) {
|
|
325
|
+
const text = value === undefined || value === null ? '' : String(value);
|
|
326
|
+
if (!text)
|
|
327
|
+
return '';
|
|
328
|
+
const parts = text.split(',').map((part) => part.trim()).filter(Boolean);
|
|
329
|
+
if (parts.length <= 1)
|
|
330
|
+
return renderIdWithName(text, names);
|
|
331
|
+
return parts.map((part) => renderIdWithName(part, names)).join(', ');
|
|
332
|
+
}
|
|
333
|
+
/** 生成主表字段的可读文本,供预览直接展示,避免调用方看到裸 ID。 */
|
|
334
|
+
function renderMainFieldView(field, dictionaries) {
|
|
335
|
+
if (field.optionValue !== undefined) {
|
|
336
|
+
const optionValue = String(field.optionValue);
|
|
337
|
+
const optionName = dictionaries.selectOptions[field.fieldId]?.[optionValue];
|
|
338
|
+
return {
|
|
339
|
+
fieldId: field.fieldId,
|
|
340
|
+
fieldName: field.fieldName,
|
|
341
|
+
optionValue,
|
|
342
|
+
text: optionName ? `${optionName}(optionValue=${optionValue})` : `optionValue=${optionValue}`,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
const value = field.value === undefined || field.value === null ? '' : String(field.value);
|
|
346
|
+
if (dictionaries.employees[value]) {
|
|
347
|
+
return { fieldId: field.fieldId, fieldName: field.fieldName, value, text: `${dictionaries.employees[value]}(${value})` };
|
|
348
|
+
}
|
|
349
|
+
if (dictionaries.customers[value]) {
|
|
350
|
+
return { fieldId: field.fieldId, fieldName: field.fieldName, value, text: `${dictionaries.customers[value]}(${value})` };
|
|
351
|
+
}
|
|
352
|
+
if (value.includes(',') && value.split(',').every((part) => dictionaries.employees[part.trim()])) {
|
|
353
|
+
return { fieldId: field.fieldId, fieldName: field.fieldName, value, text: renderIdList(value, dictionaries.employees) };
|
|
354
|
+
}
|
|
355
|
+
return { fieldId: field.fieldId, fieldName: field.fieldName, value, text: value };
|
|
356
|
+
}
|
|
357
|
+
export async function buildReimCreatePayload(input, host, operationName) {
|
|
358
|
+
const invoiceIdsRaw = input.invoiceIds;
|
|
359
|
+
if (invoiceIdsRaw === undefined && input.invoiceId !== undefined) {
|
|
360
|
+
input.invoiceIds = [input.invoiceId];
|
|
361
|
+
}
|
|
362
|
+
if (!Array.isArray(input.invoiceIds) || input.invoiceIds.length === 0) {
|
|
363
|
+
throw invoiceValidationError('input_invalid', 'invoiceIds 必须是非空数组');
|
|
364
|
+
}
|
|
365
|
+
if (input.invoiceIds.length > 100)
|
|
366
|
+
throw invoiceValidationError('input_invalid', 'invoiceIds 单次最多 100 张');
|
|
367
|
+
const invoiceIds = input.invoiceIds.map((id, index) => {
|
|
368
|
+
const text = String(id ?? '').trim();
|
|
369
|
+
if (!/^[1-9]\d*$/.test(text))
|
|
370
|
+
throw invoiceValidationError('input_invalid', `invoiceIds[${index}] 必须是正整数 ID`);
|
|
371
|
+
return text;
|
|
372
|
+
});
|
|
373
|
+
if (new Set(invoiceIds).size !== invoiceIds.length) {
|
|
374
|
+
throw invoiceValidationError('input_invalid', 'invoiceIds 不能包含重复 ID');
|
|
375
|
+
}
|
|
376
|
+
const requestName = optionalString(input.requestName, 'requestName', 100);
|
|
377
|
+
if (!requestName)
|
|
378
|
+
throw invoiceValidationError('input_invalid', 'requestName 必填');
|
|
379
|
+
const reason = optionalString(input.reason, 'reason');
|
|
380
|
+
let relatedCustomerId = optionalString(input.relatedCustomerId, 'relatedCustomerId', 32);
|
|
381
|
+
const relatedCustomerNameInput = optionalString(input.relatedCustomerName, 'relatedCustomerName', 200);
|
|
382
|
+
const contractNoInput = optionalString(input.contractNo, 'contractNo', 100);
|
|
383
|
+
const contractNo = contractNoInput ?? '20260999';
|
|
384
|
+
const warnings = [];
|
|
385
|
+
const session = await host.getSession();
|
|
386
|
+
const superiors = await fetchSuperiors(host, warnings);
|
|
387
|
+
const workflowRef = await resolveWorkflowId(host, input);
|
|
388
|
+
const workflowId = workflowRef.id;
|
|
389
|
+
const structure = await fetchStructure(host, workflowId);
|
|
390
|
+
const invoices = await fetchInvoiceInfos(host, invoiceIds);
|
|
391
|
+
// 相关客户:只给名称时反查 ID;ID 与名称都给时不额外调接口。
|
|
392
|
+
if (!relatedCustomerId && relatedCustomerNameInput) {
|
|
393
|
+
relatedCustomerId = await searchFieldOptionId(host, workflowId, MAIN_CUSTOMER_FIELD_ID, relatedCustomerNameInput);
|
|
394
|
+
}
|
|
395
|
+
for (const id of invoiceIds) {
|
|
396
|
+
const info = invoices.get(id);
|
|
397
|
+
const number = String(info.number ?? id);
|
|
398
|
+
const status = Number(info?.comm_info?.pro?.status ?? 0);
|
|
399
|
+
if (status === 8) {
|
|
400
|
+
throw invoiceValidationError('invoice_not_reimbursable', `发票 ${number} 已全额红冲(status=8),不能用于报销`);
|
|
401
|
+
}
|
|
402
|
+
const assigned = reimRequestNames(info);
|
|
403
|
+
if (assigned.length) {
|
|
404
|
+
warnings.push(`发票 ${number} 已挂在报销单:${assigned.join('、')},继续组装存在重复报销风险`);
|
|
405
|
+
}
|
|
406
|
+
if (!MONEY_PATTERN.test(String(info?.comm_info?.price?.total ?? ''))) {
|
|
407
|
+
throw invoiceValidationError('invoice_total_invalid', `发票 ${number} 的票面金额缺失或格式异常`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const rowInfos = await mapWithConcurrency(invoiceIds, 4, async (id, index) => {
|
|
411
|
+
try {
|
|
412
|
+
return await fetchRowInfo(host, session.userId, id);
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
warnings.push(`发票 ${String(invoices.get(id)?.number ?? id)} 行预填获取失败(第 ${index + 1} 行按票面兜底)`);
|
|
416
|
+
return {};
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
// 每行的字典值(费用科目 / 币种 / 说明)来自 row-info 实时返回;取不到中文名时退回模板固化值。
|
|
420
|
+
const rowMeta = invoiceIds.map((id, index) => {
|
|
421
|
+
const info = invoices.get(id);
|
|
422
|
+
const rowInfo = rowInfos[index] || {};
|
|
423
|
+
const subjectId = String(rowInfo.subjectId || DEFAULT_SUBJECT_ID);
|
|
424
|
+
const currencyEntry = Array.isArray(rowInfo.bz) ? rowInfo.bz[0] : undefined;
|
|
425
|
+
const currencyId = String(currencyEntry?.id ?? CNY_ID);
|
|
426
|
+
return {
|
|
427
|
+
invoiceId: id,
|
|
428
|
+
invoiceNumber: String(info?.number ?? ''),
|
|
429
|
+
subjectId,
|
|
430
|
+
subjectName: optionalName(rowInfo.subjectName) ?? TEMPLATE_SUBJECT_NAMES[subjectId],
|
|
431
|
+
currencyId,
|
|
432
|
+
currencyName: optionalName(currencyEntry?.name) ?? TEMPLATE_CURRENCY_NAMES[currencyId],
|
|
433
|
+
expenseNote: cleanFysm(rowInfo.fysm),
|
|
434
|
+
};
|
|
435
|
+
});
|
|
436
|
+
const detailRows = invoiceIds.map((id, index) => {
|
|
437
|
+
const info = invoices.get(id);
|
|
438
|
+
const rowInfo = rowInfos[index] || {};
|
|
439
|
+
const pro = info?.comm_info?.pro || {};
|
|
440
|
+
const total = String(info?.comm_info?.price?.total ?? '');
|
|
441
|
+
return {
|
|
442
|
+
dataIndex: index + 1,
|
|
443
|
+
subFormId: structure.detailSubFormId,
|
|
444
|
+
invoiceId: id,
|
|
445
|
+
fields: [
|
|
446
|
+
detailField('100003720000000419', '费用日期', String(rowInfo.date || pro.date || '')),
|
|
447
|
+
detailField('100003720000000228', '实报金额', total),
|
|
448
|
+
detailField('100003720000016373', '申请金额', total),
|
|
449
|
+
detailField('1211391578999111696', '人民币金额', total),
|
|
450
|
+
detailField(DETAIL_SUBJECT_FIELD_ID, '费用类型(科目)', rowMeta[index].subjectId),
|
|
451
|
+
detailField('100003720000000361', '费用说明', cleanFysm(rowInfo.fysm)),
|
|
452
|
+
detailField('100003720000000362', '附件数', '1'),
|
|
453
|
+
detailField(DETAIL_CURRENCY_FIELD_ID, '币种', CNY_ID),
|
|
454
|
+
detailField(DETAIL_INVOICE_FIELD_ID, '相关发票', id),
|
|
455
|
+
],
|
|
456
|
+
};
|
|
457
|
+
});
|
|
458
|
+
const { amount: totalAmount } = sumMoney(detailRows.map((row) => row.fields.find((field) => field.fieldId === '100003720000000228')?.value));
|
|
459
|
+
if (!totalAmount || toCents(totalAmount) === 0) {
|
|
460
|
+
throw invoiceValidationError('invoice_total_invalid', '发票金额合计为 0 或无法解析,拒绝组装报销单');
|
|
461
|
+
}
|
|
462
|
+
const today = todayInShanghai();
|
|
463
|
+
const mainFields = [
|
|
464
|
+
mainField('100003720000000073', '申请人', superiors.currentEmployeeId),
|
|
465
|
+
mainField('100003720000000075', '报销日期', today),
|
|
466
|
+
mainField('100003720000000088', '报销事由', reason || requestName),
|
|
467
|
+
mainField('100003720000000084', '报销方式', undefined, BANK_OPTION_VALUE),
|
|
468
|
+
];
|
|
469
|
+
if (relatedCustomerId)
|
|
470
|
+
mainFields.push(mainField(MAIN_CUSTOMER_FIELD_ID, '相关客户', relatedCustomerId));
|
|
471
|
+
else
|
|
472
|
+
warnings.push('未提供 relatedCustomerId,相关客户字段留空');
|
|
473
|
+
mainFields.push(mainField('100003720000001375', '人员ID', superiors.currentEmployeeId));
|
|
474
|
+
if (!contractNoInput) {
|
|
475
|
+
warnings.push('contractNo 未传入,已使用默认合同号 "20260999"');
|
|
476
|
+
}
|
|
477
|
+
if (contractNo)
|
|
478
|
+
mainFields.push(mainField(MAIN_CONTRACT_FIELD_ID, '合同号', contractNo));
|
|
479
|
+
mainFields.push(mainField('100003720000008409', '承担主体', superiors.currentEmployeeId), mainField('100003720000012512', '提单人员', superiors.currentEmployeeId), mainField('100003720000012513', '提单人员ID', superiors.currentEmployeeId), mainField('100003720000012514', '申请日期', today));
|
|
480
|
+
if (superiors.directSuperiorId) {
|
|
481
|
+
mainFields.push(mainField('100003720000013859', '报销人上级', superiors.directSuperiorId), mainField('100003720000029984', '承担人直接上级', superiors.directSuperiorId));
|
|
482
|
+
}
|
|
483
|
+
if (superiors.allSuperiorIds.length) {
|
|
484
|
+
mainFields.push(mainField('1159505470551416896', '承担人所有上级', superiors.allSuperiorIds.join(',')));
|
|
485
|
+
}
|
|
486
|
+
mainFields.push(mainField('100003720000000080', '报销总金额', totalAmount), mainField('100003720000012506', '报销合计(小写)', totalAmount), mainField('100003720000013853', '实际报销金额new', totalAmount), mainField('1211462428610863218', '人民币金额合计', totalAmount), mainField('100003720000000081', '附件总数', String(detailRows.length)));
|
|
487
|
+
const payload = {
|
|
488
|
+
work_flow_id: workflowId,
|
|
489
|
+
mainFormId: structure.mainFormId,
|
|
490
|
+
request_name: requestName,
|
|
491
|
+
main_fields: mainFields,
|
|
492
|
+
detail_rows: detailRows,
|
|
493
|
+
need_user_confirm: [],
|
|
494
|
+
unable_to_fill: [],
|
|
495
|
+
};
|
|
496
|
+
// 字典类 ID → 中文名。实时接口取到的名字优先,模板固化值兜底;都取不到就如实记进 unresolved。
|
|
497
|
+
const unresolved = [];
|
|
498
|
+
const employeeNames = {};
|
|
499
|
+
if (superiors.currentEmployeeName)
|
|
500
|
+
employeeNames[superiors.currentEmployeeId] = superiors.currentEmployeeName;
|
|
501
|
+
if (superiors.directSuperiorId && superiors.directSuperiorName) {
|
|
502
|
+
employeeNames[superiors.directSuperiorId] = superiors.directSuperiorName;
|
|
503
|
+
}
|
|
504
|
+
superiors.allSuperiorIds.forEach((id, index) => {
|
|
505
|
+
const name = superiors.allSuperiorNames[index];
|
|
506
|
+
if (name)
|
|
507
|
+
employeeNames[id] = name;
|
|
508
|
+
else
|
|
509
|
+
unresolved.push(`人员 ${id}`);
|
|
510
|
+
});
|
|
511
|
+
const subjectNames = { ...TEMPLATE_SUBJECT_NAMES };
|
|
512
|
+
const currencyNames = { ...TEMPLATE_CURRENCY_NAMES };
|
|
513
|
+
for (const meta of rowMeta) {
|
|
514
|
+
if (meta.subjectName)
|
|
515
|
+
subjectNames[meta.subjectId] = meta.subjectName;
|
|
516
|
+
else
|
|
517
|
+
unresolved.push(`费用科目 ${meta.subjectId}`);
|
|
518
|
+
if (meta.currencyName)
|
|
519
|
+
currencyNames[meta.currencyId] = meta.currencyName;
|
|
520
|
+
else
|
|
521
|
+
unresolved.push(`币种 ${meta.currencyId}`);
|
|
522
|
+
}
|
|
523
|
+
const customerNames = {};
|
|
524
|
+
if (relatedCustomerId) {
|
|
525
|
+
const name = relatedCustomerNameInput ?? TEMPLATE_CUSTOMER_NAMES[relatedCustomerId];
|
|
526
|
+
if (name)
|
|
527
|
+
customerNames[relatedCustomerId] = name;
|
|
528
|
+
else {
|
|
529
|
+
unresolved.push(`相关客户 ${relatedCustomerId}`);
|
|
530
|
+
warnings.push(`相关客户 ${relatedCustomerId} 无法自动取到中文名(字段可选项目只支持按名称搜索,不支持按 ID 反查);如需展示中文名请传入 relatedCustomerName`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
const labels = {
|
|
534
|
+
workflow: { id: workflowId, name: workflowRef.name ?? '' },
|
|
535
|
+
mainFormId: structure.mainFormId,
|
|
536
|
+
detailSubForm: { id: structure.detailSubFormId, name: structure.detailSubFormName },
|
|
537
|
+
employees: employeeNames,
|
|
538
|
+
subjects: subjectNames,
|
|
539
|
+
currencies: currencyNames,
|
|
540
|
+
customers: customerNames,
|
|
541
|
+
selectOptions: structure.selectOptions,
|
|
542
|
+
customer: relatedCustomerId ? { id: relatedCustomerId, name: customerNames[relatedCustomerId] ?? null } : null,
|
|
543
|
+
unresolved: Array.from(new Set(unresolved)),
|
|
544
|
+
};
|
|
545
|
+
const dictionaries = {
|
|
546
|
+
employees: employeeNames,
|
|
547
|
+
customers: customerNames,
|
|
548
|
+
selectOptions: structure.selectOptions,
|
|
549
|
+
};
|
|
550
|
+
const preview = {
|
|
551
|
+
requestName,
|
|
552
|
+
workflowId,
|
|
553
|
+
mainFormId: structure.mainFormId,
|
|
554
|
+
mainFieldCount: mainFields.length,
|
|
555
|
+
detailRowCount: detailRows.length,
|
|
556
|
+
totalAmount,
|
|
557
|
+
invoiceCount: invoiceIds.length,
|
|
558
|
+
// 以下三项已把字典类 ID 渲染成「中文(id)」,调用方可直接展示,无需额外查询。
|
|
559
|
+
mainFields: mainFields.map((field) => renderMainFieldView(field, dictionaries)),
|
|
560
|
+
detailRows: rowMeta.map((meta, index) => ({
|
|
561
|
+
dataIndex: index + 1,
|
|
562
|
+
invoiceId: meta.invoiceId,
|
|
563
|
+
invoiceNumber: meta.invoiceNumber,
|
|
564
|
+
amount: String(detailRows[index].fields.find((field) => field.fieldId === '100003720000000228')?.value ?? ''),
|
|
565
|
+
expenseNote: meta.expenseNote,
|
|
566
|
+
subject: renderIdWithName(meta.subjectId, subjectNames),
|
|
567
|
+
currency: renderIdWithName(meta.currencyId, currencyNames),
|
|
568
|
+
})),
|
|
569
|
+
unresolvedLabels: labels.unresolved,
|
|
570
|
+
};
|
|
571
|
+
return { payload, preview, labels, warnings, workflowId };
|
|
572
|
+
}
|
|
573
|
+
function templateEnvelope(result, operationName) {
|
|
574
|
+
return {
|
|
575
|
+
status: 'COMPLETE',
|
|
576
|
+
data: {
|
|
577
|
+
payload: result.payload,
|
|
578
|
+
preview: result.preview,
|
|
579
|
+
labels: result.labels,
|
|
580
|
+
warnings: result.warnings,
|
|
581
|
+
workflow: workflow(operationName, 'COMPLETE'),
|
|
582
|
+
},
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
/** 票夹候选分类:自动翻页拉取未报销发票,按已挂单/全额红冲/可报三分类输出。 */
|
|
586
|
+
export class InvoiceReimCandidatesListOperation {
|
|
587
|
+
name = 'invoice.reim.candidates.list';
|
|
588
|
+
async execute(input, { host }) {
|
|
589
|
+
const pageSize = boundedInteger(input.page_size, 'page_size', { min: 10, max: 200, defaultValue: 50 });
|
|
590
|
+
const maxItems = boundedInteger(input.max_items, 'max_items', { min: 1, max: 500, defaultValue: 200 });
|
|
591
|
+
// 报销场景默认剔除全额红冲发票:红冲票不可报销,混进候选会让调用方多一道人工剔除。
|
|
592
|
+
const includeRedReversed = input.include_red_reversed === true;
|
|
593
|
+
const dateBegin = optionalEpochSeconds(input.date_begin, 'date_begin');
|
|
594
|
+
const dateEnd = optionalEpochSeconds(input.date_end, 'date_end');
|
|
595
|
+
if (dateBegin !== undefined && dateEnd !== undefined && dateBegin > dateEnd) {
|
|
596
|
+
throw invoiceValidationError('input_invalid', 'date_begin 不能晚于 date_end');
|
|
597
|
+
}
|
|
598
|
+
const warnings = [];
|
|
599
|
+
const kept = [];
|
|
600
|
+
let total = 0;
|
|
601
|
+
let hasMore = false;
|
|
602
|
+
let startPos = 0;
|
|
603
|
+
let skippedRedReversed = 0;
|
|
604
|
+
let pulled = 0;
|
|
605
|
+
do {
|
|
606
|
+
const body = {
|
|
607
|
+
flag: 0,
|
|
608
|
+
page_size: pageSize,
|
|
609
|
+
start_pos: startPos,
|
|
610
|
+
req_type: 1,
|
|
611
|
+
sreim: '3',
|
|
612
|
+
bill_type: 0,
|
|
613
|
+
req_base: reqBase(),
|
|
614
|
+
};
|
|
615
|
+
if (dateBegin !== undefined)
|
|
616
|
+
body.date_begin = dateBegin;
|
|
617
|
+
if (dateEnd !== undefined)
|
|
618
|
+
body.date_end = dateEnd;
|
|
619
|
+
const result = await host.invoiceRequest({ path: '/api/inc/sync', method: 'POST', body, operation: this.name });
|
|
620
|
+
total = Number(result?.total ?? 0);
|
|
621
|
+
const infos = Array.isArray(result?.infos) ? result.infos : [];
|
|
622
|
+
for (const info of infos) {
|
|
623
|
+
pulled += 1;
|
|
624
|
+
if (!includeRedReversed && isRedReversedInvoice(info)) {
|
|
625
|
+
skippedRedReversed += 1;
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
kept.push(info);
|
|
629
|
+
}
|
|
630
|
+
startPos += infos.length;
|
|
631
|
+
hasMore = result?.end_flag === 1;
|
|
632
|
+
// 空页但 end_flag 未置位时直接收手,避免接口异常导致空转。
|
|
633
|
+
if (infos.length === 0)
|
|
634
|
+
break;
|
|
635
|
+
} while (kept.length < maxItems && pulled < total && hasMore);
|
|
636
|
+
const capped = kept.slice(0, maxItems);
|
|
637
|
+
if (kept.length > maxItems) {
|
|
638
|
+
warnings.push(`符合条件发票共 ${kept.length} 张,已按 max_items=${maxItems} 截断,可调大 max_items 或缩小日期范围`);
|
|
639
|
+
}
|
|
640
|
+
const available = [];
|
|
641
|
+
const assigned = [];
|
|
642
|
+
const redReversed = [];
|
|
643
|
+
for (const info of capped) {
|
|
644
|
+
const item = candidateItem(info);
|
|
645
|
+
if (item.status === RED_REVERSED_STATUS)
|
|
646
|
+
redReversed.push(item);
|
|
647
|
+
else if (item.reimRequests.length)
|
|
648
|
+
assigned.push(item);
|
|
649
|
+
else
|
|
650
|
+
available.push(item);
|
|
651
|
+
}
|
|
652
|
+
const summarize = (items) => {
|
|
653
|
+
const { amount, invalid } = sumMoney(items.map((item) => item.total));
|
|
654
|
+
if (invalid)
|
|
655
|
+
warnings.push(`${invalid} 张发票金额无法解析,未计入合计`);
|
|
656
|
+
return amount;
|
|
657
|
+
};
|
|
658
|
+
const summary = {
|
|
659
|
+
availableCount: available.length,
|
|
660
|
+
assignedCount: assigned.length,
|
|
661
|
+
redReversedCount: redReversed.length,
|
|
662
|
+
availableAmount: summarize(available),
|
|
663
|
+
assignedAmount: summarize(assigned),
|
|
664
|
+
redReversedAmount: summarize(redReversed),
|
|
665
|
+
filteredRedReversedCount: skippedRedReversed,
|
|
666
|
+
};
|
|
667
|
+
return {
|
|
668
|
+
status: 'COMPLETE',
|
|
669
|
+
data: {
|
|
670
|
+
total,
|
|
671
|
+
fetched: capped.length,
|
|
672
|
+
hasMore: pulled < total,
|
|
673
|
+
available,
|
|
674
|
+
assigned,
|
|
675
|
+
redReversed,
|
|
676
|
+
summary,
|
|
677
|
+
warnings,
|
|
678
|
+
workflow: workflow(this.name, 'COMPLETE'),
|
|
679
|
+
},
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
/** 报销单模板组装:固化「总部费用报销」字段映射,从发票 ID 直接产出可提交 payload(只读)。 */
|
|
684
|
+
export class InvoiceReimTemplateBuildOperation {
|
|
685
|
+
name = 'invoice.reim.template.build';
|
|
686
|
+
async execute(input, { host }) {
|
|
687
|
+
const result = await buildReimCreatePayload(input, host, this.name);
|
|
688
|
+
return templateEnvelope(result, this.name);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
/** 一站式 prepare:模板组装 + 签发 flow.create.apply continuation,停在 AWAITING_CONFIRMATION。 */
|
|
692
|
+
export class InvoiceReimFlowCreateAutoPrepareOperation {
|
|
693
|
+
name = 'invoice.reim.flow.create.auto-prepare';
|
|
694
|
+
async execute(input, { host, continuation }) {
|
|
695
|
+
const result = await buildReimCreatePayload(input, host, this.name);
|
|
696
|
+
const session = await host.getSession();
|
|
697
|
+
const continuationToken = await continuation.issue({
|
|
698
|
+
operation: 'invoice.reim.flow.create.apply',
|
|
699
|
+
workflow: workflow('invoice.reim.flow.create', 'AWAITING_CONFIRMATION'),
|
|
700
|
+
baseUrl: session.baseUrl,
|
|
701
|
+
profile: session.profile,
|
|
702
|
+
userId: session.userId,
|
|
703
|
+
fingerprint: objectFingerprint(result.payload),
|
|
704
|
+
preview: result.preview,
|
|
705
|
+
});
|
|
706
|
+
return {
|
|
707
|
+
status: 'AWAITING_CONFIRMATION',
|
|
708
|
+
data: {
|
|
709
|
+
preview: result.preview,
|
|
710
|
+
payload: result.payload,
|
|
711
|
+
labels: result.labels,
|
|
712
|
+
continuation: continuationToken,
|
|
713
|
+
expiresInSeconds: 600,
|
|
714
|
+
warnings: result.warnings,
|
|
715
|
+
workflow: workflow('invoice.reim.flow.create', 'AWAITING_CONFIRMATION'),
|
|
716
|
+
},
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
}
|