fix: 完善报销制单备注(09-08-reimbursement-payment-closeout)
这个提交包含在:
@@ -1,5 +1,11 @@
|
|||||||
# 业务闭环方案(规划,未实施)
|
# 业务闭环方案(规划,未实施)
|
||||||
|
|
||||||
|
## 2026-09-11 已批准备注增量
|
||||||
|
|
||||||
|
仅修改buildPaymentOrderRows的备注列,使用已验证且已trim的payeeName和entityCode拼成“姓名报销款 单号”。payeeName来自组批冻结的snapshot_json.payee.name;路由将归属人的收款信息冻结为payee,生成器不另查现职员工/提交人。每个batch item保持一行,即使姓名/收款账号相同也不合并。姓名或单号为空沿现有blockers整批拒绝。
|
||||||
|
|
||||||
|
不改模板列宽/格式、不缩写或截断、不引入未经银行证明的长度阈值。测试使用合成姓名,逐列xlrd读回用途、备注及文字类型,并证明同名两单独立对应、旧序号不重排。BX/PB编号规则本轮不改;编号说明以PRD最新节为准,不将日期前缀误当每日重置的证据。
|
||||||
|
|
||||||
## 1. 复用而非重建
|
## 1. 复用而非重建
|
||||||
|
|
||||||
现有入口是付款页面的待组批、付款批次、复核及“下载银行制单”。后端 POST /api/payment-batches/:id/payment-order 要求财务角色与 payment.order.export,批次 reviewed,完整且可用的同源收付款快照。生成成功会更新导出计数和审计;不能把这个 POST 当作生产只读预检。
|
现有入口是付款页面的待组批、付款批次、复核及“下载银行制单”。后端 POST /api/payment-batches/:id/payment-order 要求财务角色与 payment.order.export,批次 reviewed,完整且可用的同源收付款快照。生成成功会更新导出计数和审计;不能把这个 POST 当作生产只读预检。
|
||||||
@@ -18,7 +24,7 @@
|
|||||||
| 9 | 客户方流水号 | 批次号+行号,稳定、唯一并满足长度限制 |
|
| 9 | 客户方流水号 | 批次号+行号,稳定、唯一并满足长度限制 |
|
||||||
| 10 | 金额 | 已批准批次项金额,数值且两位小数 |
|
| 10 | 金额 | 已批准批次项金额,数值且两位小数 |
|
||||||
| 11 | 用途 | 报销款,不复制工资/公积金用途 |
|
| 11 | 用途 | 报销款,不复制工资/公积金用途 |
|
||||||
| 12 | 备注 | 报销单号 |
|
| 12 | 备注 | 冻结收款人姓名+“报销款 ”+完整报销单号(09-11已批准增量) |
|
||||||
| 13–15 | 短信标志/手机/附加信息 | 0、不通知,后两列空;不读样例手机号 |
|
| 13–15 | 短信标志/手机/附加信息 | 0、不通知,后两列空;不读样例手机号 |
|
||||||
|
|
||||||
用户三个 input 文档的表头完全一致,空白模板与内置模板相同,账号列为文本。检查原件只提取结构与摘要,原件不改、不重新保存、不复制真实行到测试夹具。工资及公积金样例仅用于确定文件格式,不暗中扩为新业务。
|
用户三个 input 文档的表头完全一致,空白模板与内置模板相同,账号列为文本。检查原件只提取结构与摘要,原件不改、不重新保存、不复制真实行到测试夹具。工资及公积金样例仅用于确定文件格式,不暗中扩为新业务。
|
||||||
|
|||||||
@@ -1,5 +1,22 @@
|
|||||||
# 执行顺序(本地技术审查/修复已批准,真实业务动作另批)
|
# 执行顺序(本地技术审查/修复已批准,真实业务动作另批)
|
||||||
|
|
||||||
|
## 本轮复核输入与提交准备(未提交)
|
||||||
|
|
||||||
|
用户批准整理两笔独立改动的复核/提交方案。基线均为5d8cdf12;01为制单备注5文件,02为UI布局11文件,路径无重叠。仅生成补丁、摘要清单并用临时索引验证顺序合并,不改变实际HEAD/index,不创建commit,不推送或部署。复核入口位于layout副本的artifacts/qa-layout/review-preparation/README.md;临时合并树不是发布候选commit。既有验证按输入不变复用,本轮不新增测试通过计数;UI补跑外层143、网银导入未验证、未独立复核等边界保留。提交前须核对冻结摘要,发布前还需验证合并后的实际候选,不能把单包测试冒充集成验收。
|
||||||
|
|
||||||
|
## 2026-09-11 制单备注小改(已批准,独立执行副本)
|
||||||
|
|
||||||
|
基线5d8cdf12;独立remarks工作副本执行,官方current已切本task。原integration副本的既有制单准备/恢复/账号隔离记录不覆盖、不混入本补丁。不新建task,不提交、推送或部署。
|
||||||
|
|
||||||
|
- [x] 核制单一单一行、冻结payee来源、模板没有备注长度说明,BX配置驱动/PB按日三位流水;编号实现不改。
|
||||||
|
- [x] 先补备注、多人/同人多单及缺失输入回归,跑红后改备注拼接。
|
||||||
|
- [x] 跑制单单测与真实BIFF8读回、编号/付款路由相关回归、语法与流程检查。
|
||||||
|
- [x] 主会话复核最小差异,记录银行导入未验证和候选未发布;不要求真人代测。
|
||||||
|
|
||||||
|
本轮验证:旧实现15项中3项按预期失败(备注缺姓名与用途),修改一行运行时后4个测试文件121/121通过:制单15、编号8、付款路由82、付款就绪16。追加用途/备注BIFF8文本类型断言后,制单15/15再通过;不将重复执行累加为新测试数。真实固定模板生成并用xlrd读回,验证15列、3个sheet、模板表头/列宽、中文备注、金额、账号前导零、稳定客户流水号。node --check、git diff --check、Trellis治理和process-contract baseline检查通过。
|
||||||
|
|
||||||
|
主会话检查确认运行时只改备注拼接,不动权限、状态、审批、编号或数据迁移;本轮不是独立复核。现有银行模板没有长度上限说明,网银实际导入及长备注接受性未验证。仅本地完成,未连接生产、未导出真实制单、未提交/推送/部署。下一步将本增量纳入候选复核与提交准备;提交/推送和部署须明确授权,不能直接覆盖原integration任务记录或将原有冻结候选视为包含本改动。
|
||||||
|
|
||||||
## 2026-09-11 业务就绪只读检查准备(未执行生产查询)
|
## 2026-09-11 业务就绪只读检查准备(未执行生产查询)
|
||||||
|
|
||||||
复用已确认身份和经理/主体回填回执,不再猜邮箱或索取工号。排除 smoke 账号,分别核对能登录、能提交、能审批/复核、能组批/登记,不能合并成一个笼统 ready。
|
复用已确认身份和经理/主体回填回执,不再猜邮箱或索取工号。排除 smoke 账号,分别核对能登录、能提交、能审批/复核、能组批/登记,不能合并成一个笼统 ready。
|
||||||
|
|||||||
@@ -2,6 +2,15 @@
|
|||||||
|
|
||||||
## Goal
|
## Goal
|
||||||
|
|
||||||
|
### 2026-09-11 制单备注与编号解释(已批准规划实施,未部署)
|
||||||
|
|
||||||
|
- 用途继续为“报销款”;每行备注为冻结收款快照姓名+“报销款 ”+该行完整报销编号,例如“合成收款人报销款 BX-20260828-0014”。不从提交人、当前员工资料或固定真人姓名取值。
|
||||||
|
- 现有一单一行保持,不按收款人合并转账;同一批次同一收款人的多张单仍逐行对应完整编号。缺少姓名或单号继续整批拒绝,不猜测、不截断。
|
||||||
|
- 保留BIFF8模板、15列、用途、金额、账号文本、客户流水号、排序、权限/复核/审计与下载行为;不修改历史文件、历史单号、计数器或生产配置,不新增迁移。
|
||||||
|
- 编号只标识创建顺序,不表示此前单据已经报销/付款。BX当前生成器优先既有序列配置,回退resetPeriod=none;PB按日三位流水,从001起。不能把讨论中的“若按日则0001起”直接改成重置生产BX或将PB扩位;周期变化另行确认。本轮仅解释并保留现状,生产具体BX序列配置未查询。
|
||||||
|
- 固定空模板未提供备注长度上限;本轮验证完整文字写入/读回,不自造银行限制,不做字符截断。实际网银导入及长备注限制仍未验证。
|
||||||
|
- 验收:合成收款人与提交人不同、同收款人多单、一单一行、长姓名/编号不丢失、缺失输入拒绝及BIFF8逐列读回;编号单测回归。生产测试账号治理暂停,不并入本轮。
|
||||||
|
|
||||||
### 2026-09-10 本轮批准范围
|
### 2026-09-10 本轮批准范围
|
||||||
|
|
||||||
本轮报销/付款体验全部完成并合并验证后再部署,不等待项目所有存量技术债。付款三项为中文文件名、下载免手填理由、操作卡下方处理进度;退回留痕删除由 09-04 task 独立交付,共同验收。已发布的发票优先录入保留回归,不重复实施。本段覆盖下方历史 R8 的“制单下载必须原因”;不覆盖回单下载、完整账号查看或取消原因策略。真实付款仍由业务人员执行。
|
本轮报销/付款体验全部完成并合并验证后再部署,不等待项目所有存量技术债。付款三项为中文文件名、下载免手填理由、操作卡下方处理进度;退回留痕删除由 09-04 task 独立交付,共同验收。已发布的发票优先录入保留回归,不重复实施。本段覆盖下方历史 R8 的“制单下载必须原因”;不覆盖回单下载、完整账号查看或取消原因策略。真实付款仍由业务人员执行。
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ function buildPaymentOrderRows(config, batch) {
|
|||||||
serial,
|
serial,
|
||||||
Number(amount.toFixed(2)),
|
Number(amount.toFixed(2)),
|
||||||
"报销款",
|
"报销款",
|
||||||
entityCode,
|
`${payeeName}报销款 ${entityCode}`,
|
||||||
"0",
|
"0",
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ const { encryptStringOrThrow } = require("../../../src/services/crypto");
|
|||||||
const {
|
const {
|
||||||
PaymentOrderXlsError,
|
PaymentOrderXlsError,
|
||||||
buildCustomerSerial,
|
buildCustomerSerial,
|
||||||
|
buildPaymentOrderRows,
|
||||||
generatePaymentOrderXls,
|
generatePaymentOrderXls,
|
||||||
} = require("../../../src/services/paymentOrderXls");
|
} = require("../../../src/services/paymentOrderXls");
|
||||||
|
|
||||||
@@ -42,6 +43,41 @@ function buildBatch(overrides = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("payment order BIFF8 generator", () => {
|
describe("payment order BIFF8 generator", () => {
|
||||||
|
it("uses frozen payee names and keeps separate rows for the same payee", () => {
|
||||||
|
const batch = buildBatch();
|
||||||
|
for (const item of batch.items) {
|
||||||
|
item.snapshot_json.payee.name = " 合成收款人 ";
|
||||||
|
item.snapshot_json.submitter = { name: "合成提交人" };
|
||||||
|
item.owner = { name: "已改名归属人" };
|
||||||
|
}
|
||||||
|
const result = buildPaymentOrderRows(config, batch);
|
||||||
|
expect(result.rows.map((row) => row.slice(10, 12))).toEqual([
|
||||||
|
["报销款", "合成收款人报销款 BX-20260802-0001"],
|
||||||
|
["报销款", "合成收款人报销款 BX-20260802-0002"],
|
||||||
|
]);
|
||||||
|
expect(result.serials).toEqual(["PB-20260802-002-R001", "PB-20260802-002-R002"]);
|
||||||
|
expect(result.totalAmount).toBe("3.20");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves complete names and document codes without truncation", () => {
|
||||||
|
const batch = buildBatch();
|
||||||
|
const name = "合成姓名".repeat(20);
|
||||||
|
const code = "BX-20260802-000123456789";
|
||||||
|
batch.items = [batch.items[0]];
|
||||||
|
batch.items[0].snapshot_json.payee.name = ` ${name} `;
|
||||||
|
batch.items[0].entity_code = ` ${code} `;
|
||||||
|
expect(buildPaymentOrderRows(config, batch).rows[0][11]).toBe(`${name}报销款 ${code}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["payee_name_missing", (item) => { item.snapshot_json.payee.name = " "; }],
|
||||||
|
["entity_code_missing", (item) => { item.entity_code = " "; }],
|
||||||
|
])("rejects the entire export for %s", (code, invalidate) => {
|
||||||
|
const batch = buildBatch();
|
||||||
|
invalidate(batch.items[0]);
|
||||||
|
expect(() => buildPaymentOrderRows(config, batch)).toThrow(expect.objectContaining({ code }));
|
||||||
|
});
|
||||||
|
|
||||||
it("generates a BIFF8 workbook with stable rows, totals and customer serials", () => {
|
it("generates a BIFF8 workbook with stable rows, totals and customer serials", () => {
|
||||||
const first = generatePaymentOrderXls(config, buildBatch());
|
const first = generatePaymentOrderXls(config, buildBatch());
|
||||||
const second = generatePaymentOrderXls(config, buildBatch());
|
const second = generatePaymentOrderXls(config, buildBatch());
|
||||||
@@ -67,7 +103,7 @@ describe("payment order BIFF8 generator", () => {
|
|||||||
"sheet=book.sheet_by_index(0)",
|
"sheet=book.sheet_by_index(0)",
|
||||||
"source=template.sheet_by_index(0)",
|
"source=template.sheet_by_index(0)",
|
||||||
"rows=[row for row in range(1,sheet.nrows) if sheet.cell_value(row,0) != '']",
|
"rows=[row for row in range(1,sheet.nrows) if sheet.cell_value(row,0) != '']",
|
||||||
"print(json.dumps({'biff':book.biff_version,'sheets':book.sheet_names(),'columns':sheet.ncols,'headerMatches':sheet.row_values(0)==source.row_values(0),'widthsMatch':all(sheet.computed_column_width(col)==source.computed_column_width(col) for col in range(15)),'rows':[sheet.row_values(row) for row in rows],'textTypes':[[sheet.cell_type(row,col) for col in [1,3,4,8]] for row in rows],'amountFormats':[book.format_map[book.xf_list[sheet.cell_xf_index(row,9)].format_key].format_str for row in rows]}))",
|
"print(json.dumps({'biff':book.biff_version,'sheets':book.sheet_names(),'columns':sheet.ncols,'headerMatches':sheet.row_values(0)==source.row_values(0),'widthsMatch':all(sheet.computed_column_width(col)==source.computed_column_width(col) for col in range(15)),'rows':[sheet.row_values(row) for row in rows],'textTypes':[[sheet.cell_type(row,col) for col in [1,3,4,8,10,11]] for row in rows],'amountFormats':[book.format_map[book.xf_list[sheet.cell_xf_index(row,9)].format_key].format_str for row in rows]}))",
|
||||||
].join("\n"), templatePath], { input: generated.buffer, maxBuffer: 1024 * 1024 });
|
].join("\n"), templatePath], { input: generated.buffer, maxBuffer: 1024 * 1024 });
|
||||||
expect(readback.stderr.toString("utf8")).toBe("");
|
expect(readback.stderr.toString("utf8")).toBe("");
|
||||||
expect(readback.status).toBe(0);
|
expect(readback.status).toBe(0);
|
||||||
@@ -78,15 +114,16 @@ describe("payment order BIFF8 generator", () => {
|
|||||||
columns: 15,
|
columns: 15,
|
||||||
headerMatches: true,
|
headerMatches: true,
|
||||||
widthsMatch: true,
|
widthsMatch: true,
|
||||||
textTypes: [[1, 1, 1, 1], [1, 1, 1, 1]],
|
textTypes: [[1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1]],
|
||||||
amountFormats: ["0.00", "0.00"],
|
amountFormats: ["0.00", "0.00"],
|
||||||
});
|
});
|
||||||
expect(parsed.rows).toHaveLength(2);
|
expect(parsed.rows).toHaveLength(2);
|
||||||
expect(parsed.rows[0]).toEqual([
|
expect(parsed.rows[0]).toEqual([
|
||||||
"1", payerAccount, "合成测试公司", "01", payeeAccount, "甲", "中国建设银行", "",
|
"1", payerAccount, "合成测试公司", "01", payeeAccount, "甲", "中国建设银行", "",
|
||||||
"PB-20260802-002-R001", 1.2, "报销款", "BX-20260802-0001", "0", "", "",
|
"PB-20260802-002-R001", 1.2, "报销款", "甲报销款 BX-20260802-0001", "0", "", "",
|
||||||
]);
|
]);
|
||||||
expect(parsed.rows[1][9]).toBe(2);
|
expect(parsed.rows[1][9]).toBe(2);
|
||||||
|
expect(parsed.rows[1].slice(10, 12)).toEqual(["报销款", "乙报销款 BX-20260802-0002"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("fails the whole export and reports every blocking payee issue", () => {
|
it("fails the whole export and reports every blocking payee issue", () => {
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户