mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:41:59 +00:00
Excel 期 5:列表按期累积、穿插的小计、表格对象起草、CSV 走配方(E)
为什么:每月一份的明细导出是最常见的场景,期 4 之前列表只能每期替换;夹在中间的小计会让规则起草失败;
表格对象这个现成的边界信号没用上;CSV 不能走配方。四项都复用现有的执行器、核对、累积、溯源,不另起一套。
- 列表按期累积:ListBlock.period 二选一——{column, check: within_context} 按日期列分期,试运行多一条结构类核对
D(每行的日期都在本期统计期内,空着也不行);{synthesize: 统计期} 每行加「统计期起」「统计期止」两列,列表在
统计期定下来之前就抽完了,这两格先放占位、写库时换成真值。accumulate_blockers 放行绑定了统计期的列表;合计行、
小计行另存的表各期主键分不开,照旧不能累积;同一订单两期都出现由并集的主键核对拒收。U3 按列表的分期方式核对。
说明里「按哪一列筛选」用分期列,加的两列有列说明;空行之后文字行的说法补了并集版本(列表能累积以后要用到)。
- 穿插的小计:ListRows.subtotals {label_column, pick, scope: since_previous | group_column, group_column, keep_as}。
小计行不进数据,每个数字格一条合计格(分段键是块 id + ·小计),核对模块按它所管那组明细的 rowid 区间用 SQL
求和比对(新 T 编号,标题「小计行按各组明细求和」),整行记进排除的行(reason=subtotal),可以另存成
reported_total 表(小计项、分组取值、各数字列);数字格全空报 subtotal_blank(结构类);合计行的公式引用了小计格
时按它管的明细换算。静态校验:标签列、分组列要在列里,按组小计要给分组列,小计和合计的开头词不能互为前缀。
规则起草把第一列同一个合计字样、下面还有明细的行提议成小计(以前记 stray_totals 失败),框选同一条规则。
- 表格对象起草:工作表有表格对象时按它出列表块:表头行、列范围、数据行、汇总行(映射成 total_row,标签取汇总行
第一列的文字)都由表格对象给,锚点仍按表头文字、不记坐标;表名优先用表格对象的名字(含数字的不用)。不调用 AI。
- CSV 走配方:新增 csv_grid,按内容认出 CSV(不是 zip、不是 OLE、开头没有 NUL),编码和分隔符复用 tabular 读 CSV
的那一段,适配成一张名为「CSV」的工作表(SheetScan.path=csv:,WorkbookScan.format=csv);只有写法毫无歧义的
整数、小数转成数,千分位、前导零、日期写法一律留作文字交给配方的规则。坐标沿用「工作表!A1」(偏离细案的
R{行}C{列},理由见 csv_grid 的模块说明)。CSV 不做稀疏判定。导入向导和简单上传的结构提示都放行 CSV。
- 每个列表一道题「每期怎么更新」(q_list_period):每期替换 / 按期累积并按某个日期列核对 / 按期累积并加统计期
起止两列,后两种同时把导入模式改成按期累积、缺统计期的工作表一并配上(问题集合按草稿生成、回答时不变,草稿里
的列表还不符合累积资格,q_mode 不会出现,所以并成一道题)。确认项新增 subtotals:<块>、list_period:<块>。
- 前端配方面板:列表块多「小计行」(标签列、开头词、管哪些明细、分组列、另存)和「按期累积时每一行属于哪一期」;
回执「排除的行」认 subtotal。
验证:新增 tests/test_csv_grid.py、test_recipe_p5.py(执行器、核对、校验、资格、并集物化、起草)、
test_recipe_p5_e2e.py(经接口:三份月度 CSV 累积、部分重叠拒收、同一期替换要确认;带穿插小计的 Excel 起草、
核对、提交)。去掉小计识别、统计期回填、D 核对分别回跑,新测试 10 条失败。旧测试里「夹在中间的小计一律失败」
「列表只能每期替换」的断言按新行为改写。
This commit is contained in:
@@ -2162,8 +2162,9 @@ await section('数据 · 表格:按配方导入', async () => {
|
||||
await dropZone.waitFor({ timeout: 4000 }).catch(() => {})
|
||||
const pickHint = await dropZone.innerText().catch(() => '')
|
||||
const accept = await dropZone.locator('input[type="file"]').getAttribute('accept').catch(() => '')
|
||||
check('选文件区写「拖入文件」,提示的格式与选择框收的一致(.xlsx、.xlsm)', pickHint.includes('拖入文件')
|
||||
&& accept === '.xlsx,.xlsm' && pickHint.includes('.xlsx') && pickHint.includes('.xlsm'), `${pickHint} | ${accept}`)
|
||||
check('选文件区写「拖入文件」,提示的格式与选择框收的一致(.xlsx、.xlsm、CSV / TSV)', pickHint.includes('拖入文件')
|
||||
&& accept === '.xlsx,.xlsm,.csv,.tsv' && pickHint.includes('.xlsx') && pickHint.includes('.xlsm')
|
||||
&& pickHint.includes('CSV'), `${pickHint} | ${accept}`)
|
||||
const dropFile = async (name) => {
|
||||
const dt = await page.evaluateHandle((n) => {
|
||||
const d = new DataTransfer()
|
||||
@@ -2174,9 +2175,9 @@ await section('数据 · 表格:按配方导入', async () => {
|
||||
await dropZone.dispatchEvent('drop', { dataTransfer: dt })
|
||||
await page.waitForTimeout(200)
|
||||
}
|
||||
await dropFile('drop_flow.csv')
|
||||
check('……拖进来的不是 Excel:不选中,说明只支持 .xlsx、.xlsm', await dropZone.locator('[data-wizard-file]').count() === 0
|
||||
&& (await page.locator('body').innerText()).includes('不是 Excel 文件'))
|
||||
await dropFile('drop_flow.pdf')
|
||||
check('……拖进来的不是 Excel 或 CSV:不选中,说明支持哪几种', await dropZone.locator('[data-wizard-file]').count() === 0
|
||||
&& (await page.locator('body').innerText()).includes('不是 Excel 或 CSV 文件'))
|
||||
await dropFile('drop_flow.xlsx')
|
||||
check('……拖进来的文件被选中,名字取自文件名', (await dropZone.locator('[data-wizard-file]').innerText().catch(() => '')).includes('drop_flow.xlsx')
|
||||
&& (await dialog(page).locator('[data-wizard-name]').inputValue().catch(() => '')) === 'drop_flow')
|
||||
|
||||
Reference in New Issue
Block a user