fix: 管理费用汇总排除不涉及付款和未打款流程 #802

Merged
yueyang merged 1 commits from fix/yueyang-management-expenses-paid-approved-summary into main 2026-10-30 22:02:31 +08:00
5 changed files with 36 additions and 29 deletions

View File

@ -34,7 +34,7 @@
| 本次接入 | 普通商品资料的工作平台采购价格接入 `jiansun.dbo.pm_priceAdjustment`(镜像 `js_pm_price_adjustment`):按 `skuCode` 关联 `jst_sku.sku_id`,取 `effectTime` 不晚于当前时间的最新 `purchasePrice`,并在页面和导出中标记“工作平台价格”;源表没有可靠更新时间,按 `Id` 稳定键追加,首次全量完成前只显示普通商品自身采购价。 |
| 待有效凭据后首次同步 | 普通商品当前成本价、商品类型和更新时间继续读取 `operations_replica.rpt.jst_sku`;`rpt.fys_skuCostPriceUpdate` 是一 SKU 一行的旧快照,不能充当变价历史。完整成本价变更仍读取聚水潭经典开放平台“商品操作日志查询”接口 `worklog.query`,只镜像操作内容含“采购价”的记录到 `jushuitan_cost_price_history_rows`;经典接口必须在服务器私有配置中使用同一应用的 `JUSHUITAN_PARTNER_ID / JUSHUITAN_PARTNER_KEY / JUSHUITAN_TOKEN`,不得把新开放平台的 App Key / App Secret / Access Token 混用。页面在首次同步完成前明确显示待同步或失败,不把 0 条解释为真实无历史。 |
| 本次接入 | 流程明细读取 `operations_replica/rpt.ding_Workflow + rpt.ding_WorkflowScheme` 的结构化审批主表、费用明细和付款明细,写入 `ding_process_detail_mirror` 与 `ding_process_payment_mirror`;主表的 `originatorDeptName` 作为发起人部门单独保留。禁止先把明细表和付款表铺平成 CSV 后逐行求和;付款明细必须按“对应序号”关联到代表明细行,但合计始终读取独立付款事实。每个纳入范围的源审批至少保留一条页面记录,没有费用金额的审批以空金额显示,不静默隐藏。首次全量先运行 `business-mirror:sync-ding-process-details --full --dry-run` 校验,再使用 `business-mirror:sync-ding-process-details --full` 发布;日常通过 `updateAt/createAt` 48 小时重叠增量同步。当前纳入日常报销、应付、线上代发、种菜、快递面单、固定资产、日常物品、样品、一次性采买、板材、工资、外贸费用和种菜充值等 14 个财务流程;发现新的明细流程编码时全量同步报错并保留上一版。 |
| 本次接入 | 财务经营报表 › 管理费用使用 `oa_process_account_mapping` 保存“流程科目映射”工作簿中的 191 个科目编码、科目名称、费用大类、二级、三级、是否进费比和是否分摊规则;页面按审批完成时间默认展示本月 OA 流程中状态为“已完成”的审批明细,不要求出纳已完成打款,按费用大类和二级汇总进费比/不进费比/未映射金额,点击二级查看三级汇总与 9 列明细模板。审批完成时间写入 `ding_process_detail_mirror.approval_finished_at`。 |
| 本次接入 | 财务经营报表 › 管理费用使用 `oa_process_account_mapping` 保存“流程科目映射”工作簿中的 191 个科目编码、科目名称、费用大类、二级、三级、是否进费比和是否分摊规则;页面按审批完成时间默认展示本月 OA 流程中审批状态为“已完成”、涉及付款且出纳已打款的明细,不涉及付款或出纳未打款的数据不进入汇总,按费用大类和二级汇总进费比/不进费比/未映射金额,点击二级查看三级汇总与 9 列明细模板。审批完成时间写入 `ding_process_detail_mirror.approval_finished_at`。 |
流程明细另设独立定时任务,避免与每分钟科目映射消费争用同一上游目录时被静默跳过。正式发布脚本会覆盖安装 `/etc/cron.d/caiwu-ding-process-details`:每 5 分钟尝试运行 `business-mirror:sync-ding-process-details`,使用独立调度锁并在共用上游锁上最多等待 15 分钟;`scripts/install-ding-process-sync-cron.ps1` 仅作为手工修复入口。任务时间和镜像状态统一使用平台固定时区,审批等待时长按实际小时展示。OA 页面每小时最多把一次当前页“审批中”的审批编号写入定向刷新队列,由每分钟检查一次的批处理命令消费;其他时间直接展示最近镜像,已完成流程不进入即时刷新链路,仍由常规增量和每周全量校准处理付款等后续变化。增量同步除跟进审批主表更新时间外,还以 `ding_WorkflowScheme` 的数字 `id` 高水位发现后补表单行,避免付款明细等新组件写入后主审批 `updateAt` 不变而被漏掉;原地改写同一组件仍由每周全量校准兜底。付款状态只按钉钉“出纳打款”节点判断,分为“出纳状态待同步 / 出纳未开始 / 出纳待打款 / 出纳退回 / 出纳已打款 / 流程终止 / 不涉及付款”。金额差异不参与付款状态判断,另以“付款差额 = 费用合计 − 已付款合计”展示。不能因为金额存在少量差异就覆盖出纳节点事实,也不能因为出纳已通过就隐藏金额差异。

View File

@ -292,6 +292,11 @@ class ManagementExpensesPageData
'd.account_code',
)
->where('d.status', '已完成')
->where('d.cashier_status', 'completed')
->where(function (Builder $paymentSource): void {
$paymentSource->whereNull('d.payment_source_state')
->orWhere('d.payment_source_state', '<>', 'not_applicable');
})
->whereNotNull('d.approval_finished_at');
if (($filters['approved_start'] ?? '') !== '') {

View File

@ -518,8 +518,9 @@
<div>
<h2>{{ $pageTitle ?? '管理费用' }}</h2>
<p>
数据源为 OA 流程中审批状态为“已完成”的审批明细。科目规则来自“流程科目映射”,
默认展示本月审批完成的数据;二级汇总区分进费比、不进费比和未映射科目,
数据源为 OA 流程中审批已完成、涉及付款且出纳已打款的明细。科目规则来自“流程科目映射”,
默认展示本月审批完成的数据;不涉及付款或出纳未打款的审批不计入汇总,
二级汇总区分进费比、不进费比和未映射科目,
点击二级可查看三级和明细模板。
</p>
</div>
@ -532,7 +533,7 @@
@endif
<div class="me-stats">
<div class="me-stats__item"><span>已审批金额</span><strong>¥{{ $money($summary['amount_total'] ?? 0) }}</strong></div>
<div class="me-stats__item"><span>已打款金额</span><strong>¥{{ $money($summary['amount_total'] ?? 0) }}</strong></div>
<div class="me-stats__item"><span>进费比金额</span><strong>¥{{ $money($summary['in_fee_ratio_amount'] ?? 0) }}</strong></div>
<div class="me-stats__item"><span>不进费比金额</span><strong>¥{{ $money($summary['non_fee_ratio_amount'] ?? 0) }}</strong></div>
</div>
@ -543,7 +544,7 @@
<span class="material-icons-outlined" aria-hidden="true">account_tree</span>
二级汇总
</span>
<span class="me-panel__hint">点击二级行查看三级和已审批明细</span>
<span class="me-panel__hint">点击二级行查看三级和已打款审批明细</span>
</div>
<div class="me-summary-wrap">
<table class="me-table">
@ -581,7 +582,7 @@
</td>
</tr>
@empty
<tr><td colspan="7" class="me-empty">当前筛选条件没有已审批的 OA 流程明细。</td></tr>
<tr><td colspan="7" class="me-empty">当前筛选条件没有符合汇总口径的 OA 流程明细。</td></tr>
@endforelse
</tbody>
</table>
@ -679,9 +680,9 @@
<div class="me-panel__head">
<span class="me-panel__title" id="me-detail-title">
<span class="material-icons-outlined" aria-hidden="true">table_view</span>
OA 已审批明细模板
OA 已打款审批明细模板
</span>
<span class="me-panel__hint">仅展示审批状态为“已完成”的流程明细</span>
<span class="me-panel__hint">仅展示审批已完成、涉及付款且出纳已打款的流程明细</span>
</div>
<div class="me-detail-wrap">
<table class="me-table">
@ -710,7 +711,7 @@
<td>{{ $row['allocation'] }}</td>
</tr>
@empty
<tr><td colspan="9" class="me-empty">当前筛选条件没有已审批的 OA 流程明细。</td></tr>
<tr><td colspan="9" class="me-empty">当前筛选条件没有符合汇总口径的 OA 流程明细。</td></tr>
@endforelse
</tbody>
</table>
@ -751,7 +752,7 @@
<div class="me-dialog__head">
<div class="me-dialog__title">
<strong id="management-expenses-dialog-title">二级明细</strong>
<span id="management-expenses-dialog-subtitle">三级汇总与 OA 已审批明细</span>
<span id="management-expenses-dialog-subtitle">三级汇总与 OA 已打款审批明细</span>
</div>
<button type="button" class="me-dialog__close" id="management-expenses-dialog-close" aria-label="关闭明细">×</button>
</div>
@ -927,7 +928,7 @@
const loadDetails = async () => {
if (!activeGroup) return;
detailStatus.textContent = '正在读取 OA 已审批明细…';
detailStatus.textContent = '正在读取符合口径的 OA 审批明细…';
detailRows.replaceChildren();
const params = new URLSearchParams(window.location.search);
params.delete('page');
@ -955,7 +956,7 @@
prevButton.disabled = activePage <= 1;
nextButton.disabled = activePage >= lastPage;
detailStatus.textContent = activeThird === ''
? '当前二级全部已审批明细'
? '当前二级全部已打款审批明细'
: `三级:${activeThird}`;
} catch (error) {
detailStatus.textContent = '明细读取失败,请稍后重试。';
@ -973,7 +974,7 @@
activeThird = '';
activePage = 1;
title.textContent = `${activeGroup.category} / ${activeGroup.secondLevel}`;
subtitle.textContent = '点击三级行继续查看对应 OA 已审批明细';
subtitle.textContent = '点击三级行继续查看对应已打款审批明细';
const groupRows = secondLevelRows.find((row) =>
row.expense_category === activeGroup.category
&& row.second_level === activeGroup.secondLevel

View File

@ -74,7 +74,8 @@ class ManagementExpensesPageTest extends FeatureTestCase
]))->assertOk()
->assertSee('管理费用')
->assertSee('二级汇总')
->assertSee('OA 已审批明细模板')
->assertSee('OA 已打款审批明细模板')
->assertSee('不涉及付款或出纳未打款的审批不计入汇总')
->assertSee('审批时间')
->assertSee('进费比')
->assertSee('不进费比')

View File

@ -40,30 +40,30 @@ class ManagementExpensesPageDataTest extends TestCase
parent::tearDown();
}
public function test_page_groups_only_approved_oa_rows_by_second_level_and_fee_ratio(): void
public function test_page_groups_only_approved_paid_oa_rows_by_second_level_and_fee_ratio(): void
{
$data = $this->pageData->page(Request::create(
'/1/workspace/financial-operations/management-expenses',
'GET',
));
$this->assertSame(5, $data['rows']->total());
$this->assertSame(445.0, $data['summary']['amount_total']);
$this->assertSame(270.0, $data['summary']['in_fee_ratio_amount']);
$this->assertSame(125.0, $data['summary']['non_fee_ratio_amount']);
$this->assertSame(3, $data['rows']->total());
$this->assertSame(350.0, $data['summary']['amount_total']);
$this->assertSame(200.0, $data['summary']['in_fee_ratio_amount']);
$this->assertSame(100.0, $data['summary']['non_fee_ratio_amount']);
$this->assertSame(50.0, $data['summary']['unmapped_amount']);
$this->assertSame(5, $data['summary']['row_count']);
$this->assertSame(3, $data['summary']['row_count']);
$this->assertSame('2026-10-01', $data['filters']['approved_start']);
$this->assertSame('2026-10-31', $data['filters']['approved_end']);
$groups = collect($data['secondLevelSummary'])->keyBy(
static fn (array $row): string => $row['expense_category'] . '|' . $row['second_level'],
);
$this->assertSame(125.0, $groups['管理费用|办公费']['amount_total']);
$this->assertSame(100.0, $groups['管理费用|办公费']['amount_total']);
$this->assertSame(1, $groups['管理费用|办公费']['approval_count']);
$this->assertSame(270.0, $groups['销售费用|技术服务费']['in_fee_ratio_amount']);
$this->assertSame(200.0, $groups['销售费用|技术服务费']['in_fee_ratio_amount']);
$this->assertSame(0.0, $groups['销售费用|技术服务费']['non_fee_ratio_amount']);
$this->assertSame(270.0, $groups['销售费用|技术服务费']['amount_total']);
$this->assertSame(200.0, $groups['销售费用|技术服务费']['amount_total']);
$this->assertSame(50.0, $groups['未映射科目|未映射科目']['unmapped_amount']);
$this->assertCount(3, $data['secondLevelSummary']);
}
@ -78,11 +78,11 @@ class ManagementExpensesPageDataTest extends TestCase
$this->assertSame('', $data['filters']['approved_start']);
$this->assertSame('', $data['filters']['approved_end']);
$this->assertSame(6, $data['rows']->total());
$this->assertSame(465.0, $data['summary']['amount_total']);
$this->assertSame(4, $data['rows']->total());
$this->assertSame(370.0, $data['summary']['amount_total']);
}
public function test_detail_template_filters_and_paginates_approved_rows(): void
public function test_detail_template_filters_and_paginates_approved_paid_rows(): void
{
$data = $this->pageData->details(Request::create(
'/1/workspace/financial-operations/management-expenses/details',
@ -94,14 +94,14 @@ class ManagementExpensesPageDataTest extends TestCase
],
));
$this->assertSame(2, $data['pagination']['total']);
$this->assertSame(125.0, $data['summary']['amount_total']);
$this->assertSame(1, $data['pagination']['total']);
$this->assertSame(100.0, $data['summary']['amount_total']);
$this->assertSame(
['审批编号', '部门', '用途', '分类', '二级', '三级', '金额', '是否进费比', '是否分摊'],
array_values(ManagementExpensesPageData::DETAIL_COLUMNS),
);
$this->assertSame(
['5602.04', '5602.04'],
['5602.04'],
array_column($data['rows'], 'business_id'),
);
$this->assertSame('不进费比', $data['rows'][0]['fee_ratio']);