From c9f5e358c41e12f98aaa2fb9121303371f87ce5d Mon Sep 17 00:00:00 2001 From: yueyang <2495544247@qq.com> Date: Sun, 25 Oct 2026 14:50:52 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B4=B9=E7=94=A8=E5=88=86=E7=B1=BB=E5=BC=B9?= =?UTF-8?q?=E7=AA=97=E5=A2=9E=E5=8A=A0=E5=B9=B3=E5=8F=B0=E6=B1=87=E6=80=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 费用分类弹窗增加平台汇总 --- .../View/BillingDetailsPageData.php | 63 +++++++++++++- .../billing-details.blade.php | 87 ++++++++++++++++--- .../ExpenseDetailsDrilldownPageTest.php | 13 +++ .../BillingDetailsExpenseFilterTest.php | 18 +++- 4 files changed, 161 insertions(+), 20 deletions(-) diff --git a/modules/EcommerceFinance/View/BillingDetailsPageData.php b/modules/EcommerceFinance/View/BillingDetailsPageData.php index d3c0d9b4..640932fa 100644 --- a/modules/EcommerceFinance/View/BillingDetailsPageData.php +++ b/modules/EcommerceFinance/View/BillingDetailsPageData.php @@ -373,6 +373,7 @@ class BillingDetailsPageData * invoice_net:float, * non_invoice_net:float, * net:float, + * platforms:list, * items:list * }>, * category_count:int, @@ -388,7 +389,7 @@ class BillingDetailsPageData $signature = md5((string) json_encode($filters, JSON_UNESCAPED_UNICODE)); return cache()->remember( - 'billing-details-item-summary-v5:' . $signature, + 'billing-details-item-summary-v6:' . $signature, now()->addMinutes(2), function () use ($filters): array { $connection = $this->connection(); @@ -398,6 +399,7 @@ class BillingDetailsPageData $bind = []; foreach (self::SOURCES as $label => $meta) { $branch = $this->branch($label, $meta, $filters); + $platformSql = $this->expensePlatformSql($label); $invoiceConditions = []; $nonInvoiceConditions = []; $memoConditions = []; @@ -447,11 +449,12 @@ class BillingDetailsPageData foreach ($branch['scopes'] as $scope) { $parts[] = 'SELECT ' . $memoSql . ' AS ledger_memo, ' + . $platformSql . ' AS platform_label, ' . $bucketSql . ' AS invoice_bucket, bill_item, ' . 'COALESCE(SUM(' . $branch['income'] . '),0) AS inc, ' . 'COALESCE(SUM(' . $branch['expense'] . '),0) AS exp ' . 'FROM ' . $branch['table'] . ' WHERE ' . $scope['where'] - . ' GROUP BY ledger_memo, invoice_bucket, bill_item'; + . ' GROUP BY ledger_memo, platform_label, invoice_bucket, bill_item'; $bind = array_merge($bind, $scope['bind']); } } @@ -473,6 +476,7 @@ class BillingDetailsPageData $this->accumulateLedgerMemoTotal( $totals, (string) ($part->ledger_memo ?? ''), + (string) ($part->platform_label ?? ''), (string) ($part->bill_item ?? ''), $net, $bucket, @@ -496,22 +500,32 @@ class BillingDetailsPageData * @param array, * items:array * }> $totals */ private function accumulateLedgerMemoTotal( array &$totals, string $ledgerMemo, + string $platform, string $billItem, float $net, string $invoiceBucket, ): void { $ledgerMemo = trim($ledgerMemo) ?: '未填写账单费用'; + $platform = trim($platform) ?: '未填写平台'; $billItem = $this->normalizeItem($billItem) ?: '未填写账单项目'; - $totals[$ledgerMemo] ??= ['invoice_net' => 0.0, 'non_invoice_net' => 0.0, 'items' => []]; + $totals[$ledgerMemo] ??= [ + 'invoice_net' => 0.0, + 'non_invoice_net' => 0.0, + 'platforms' => [], + 'items' => [], + ]; + $totals[$ledgerMemo]['platforms'][$platform] ??= ['invoice_net' => 0.0, 'non_invoice_net' => 0.0]; $totals[$ledgerMemo]['items'][$billItem] ??= ['invoice_net' => 0.0, 'non_invoice_net' => 0.0]; $key = $invoiceBucket === 'invoice' ? 'invoice_net' : 'non_invoice_net'; $totals[$ledgerMemo][$key] += $net; + $totals[$ledgerMemo]['platforms'][$platform][$key] += $net; $totals[$ledgerMemo]['items'][$billItem][$key] += $net; } @@ -519,6 +533,7 @@ class BillingDetailsPageData * @param array, * items:array * }> $totals * @return list, * items:list * }> */ @@ -535,6 +551,24 @@ class BillingDetailsPageData foreach ($totals as $memo => $total) { $invoiceNet = round((float) $total['invoice_net'], 2); $nonInvoiceNet = round((float) $total['non_invoice_net'], 2); + $platforms = []; + foreach ($total['platforms'] as $platform => $platformTotal) { + $platformInvoiceNet = round((float) $platformTotal['invoice_net'], 2); + $platformNonInvoiceNet = round((float) $platformTotal['non_invoice_net'], 2); + $platforms[] = [ + 'platform' => $platform, + 'invoice_net' => $platformInvoiceNet, + 'non_invoice_net' => $platformNonInvoiceNet, + 'net' => round($platformInvoiceNet + $platformNonInvoiceNet, 2), + ]; + } + usort($platforms, static function (array $left, array $right): int { + if (abs(abs($left['net']) - abs($right['net'])) >= 0.005) { + return abs($right['net']) <=> abs($left['net']); + } + + return strnatcasecmp($left['platform'], $right['platform']); + }); $billItems = []; foreach ($total['items'] as $billItem => $itemTotal) { $itemInvoiceNet = round((float) $itemTotal['invoice_net'], 2); @@ -558,6 +592,7 @@ class BillingDetailsPageData 'invoice_net' => $invoiceNet, 'non_invoice_net' => $nonInvoiceNet, 'net' => round($invoiceNet + $nonInvoiceNet, 2), + 'platforms' => $platforms, 'items' => $billItems, ]; } @@ -1534,6 +1569,28 @@ class BillingDetailsPageData return $parts === [] ? '1 = 1' : '(' . implode(' OR ', $parts) . ')'; } + /** + * 账单来源分支 → 费用分类弹窗的平台名称。 + * + * 同一来源可能混合多个平台(例如淘系支付宝包含天猫、淘宝、阿里巴巴、支付宝), + * 按店铺名前缀还原,无法识别时保留来源名称。 + */ + private function expensePlatformSql(string $branchLabel): string + { + $conditions = []; + foreach (self::BRANCH_PLATFORMS[$branchLabel] ?? [] as $platform) { + $conditions[] = 'WHEN ' . $this->storePrefixCondition($platform) + . ' THEN ' . $this->literal($platform); + } + + if ($conditions === []) { + return $this->literal($branchLabel); + } + + return 'CASE ' . implode(' ', $conditions) + . ' ELSE ' . $this->literal($branchLabel) . ' END'; + } + /** * 取与查询区间有交集的账期的 active_generation(与 JD / 抖音账单页仓储同一口径)。 * diff --git a/resources/views/business/pages/financial-operations/billing-details.blade.php b/resources/views/business/pages/financial-operations/billing-details.blade.php index cb5f9034..8b39dc1c 100644 --- a/resources/views/business/pages/financial-operations/billing-details.blade.php +++ b/resources/views/business/pages/financial-operations/billing-details.blade.php @@ -69,6 +69,11 @@ .pd-expense-dialog__close { display:inline-flex; align-items:center; justify-content:center; width:2rem; height:2rem; padding:0; border:1px solid #d8d2c7; border-radius:999px; background:#fff; color:#595959; font-size:1.25rem; cursor:pointer; } .pd-expense-dialog__close:hover { background:#f7f0de; color:#856632; } .pd-expense-dialog__body { max-height:calc(86vh - 6rem); overflow:auto; padding:0 1.25rem 1.25rem; } + .pd-expense-dialog__tabs { display:flex; align-items:center; gap:1.25rem; margin:0 -1.25rem; padding:0 1.25rem; border-bottom:1px solid #e8e0d2; background:#fff; } + .pd-expense-dialog__tab { min-height:2.75rem; padding:0 .2rem; border:0; border-bottom:2px solid transparent; background:transparent; color:#595959; font:inherit; font-size:.8125rem; font-weight:600; cursor:pointer; } + .pd-expense-dialog__tab:hover { color:#856632; } + .pd-expense-dialog__tab[aria-selected="true"] { border-bottom-color:#b9954f; color:#856632; } + .pd-expense-dialog__panel[hidden] { display:none; } .pd-expense-dialog__table { width:100%; border-collapse:collapse; font-size:.875rem; } .pd-expense-dialog__table th { position:sticky; top:0; z-index:1; padding:.65rem .85rem; border-bottom:1px solid #d8d2c7; background:#f3ece0; color:#3d3a35; text-align:left; white-space:nowrap; } .pd-expense-dialog__table td { padding:.6rem .85rem; border-bottom:1px solid #eee7da; color:#3d3a35; } @@ -322,7 +327,7 @@

费用分类明细

- 按账单名称汇总;开票与不开票金额均以万元展示 + 按平台或账单名称查看当前费用分类明细,金额单位为万元
当前分类合计(万元) @@ -331,18 +336,36 @@
- - - - - - - - - - -
账单名称开票金额(万)不开票金额(万)总金额(万)
- +
+ + +
+
+ + + + + + + + +
平台总金额(万)
+ +
+
@endif @@ -736,6 +759,14 @@ var categoryDialog = document.getElementById('pdExpenseCategoryDialog'); var categoryDialogTitle = document.getElementById('pdExpenseCategoryDialogTitle'); var categoryDialogTotal = document.getElementById('pdExpenseCategoryDialogTotal'); + var categoryDialogTabs = Array.prototype.slice.call( + categoryDialog.querySelectorAll('[data-pd-expense-tab]') + ); + var categoryDialogPanels = Array.prototype.slice.call( + categoryDialog.querySelectorAll('[data-pd-expense-panel]') + ); + var categoryDialogPlatformRows = document.getElementById('pdExpenseCategoryDialogPlatformRows'); + var categoryDialogPlatformEmpty = document.getElementById('pdExpenseCategoryDialogPlatformEmpty'); var categoryDialogRows = document.getElementById('pdExpenseCategoryDialogRows'); var categoryDialogEmpty = document.getElementById('pdExpenseCategoryDialogEmpty'); var summaryParams = new URLSearchParams(window.location.search); @@ -749,13 +780,38 @@ }); }; + function setCategoryDialogTab(name) { + categoryDialogTabs.forEach(function (tab) { + tab.setAttribute( + 'aria-selected', + String(tab.getAttribute('data-pd-expense-tab') === name) + ); + }); + categoryDialogPanels.forEach(function (panel) { + panel.hidden = panel.getAttribute('data-pd-expense-panel') !== name; + }); + } + function openCategoryDialog(category) { + setCategoryDialogTab('platform'); categoryDialogTitle.textContent = String(category.memo || '费用分类') + ' · 费用明细'; categoryDialogTotal.textContent = formatWan( category.net === undefined ? Number(category.invoice_net || 0) + Number(category.non_invoice_net || 0) : category.net ); + categoryDialogPlatformRows.replaceChildren(); + (category.platforms || []).forEach(function (platform) { + var tr = document.createElement('tr'); + [platform.platform, formatWan(platform.net)].forEach(function (value, index) { + var cell = document.createElement('td'); + cell.textContent = value; + if (index > 0) cell.className = 'num'; + tr.appendChild(cell); + }); + categoryDialogPlatformRows.appendChild(tr); + }); + categoryDialogPlatformEmpty.hidden = Number((category.platforms || []).length) > 0; categoryDialogRows.replaceChildren(); (category.items || []).forEach(function (item) { var tr = document.createElement('tr'); @@ -789,6 +845,11 @@ } document.getElementById('pdExpenseCategoryDialogClose').addEventListener('click', closeCategoryDialog); + categoryDialogTabs.forEach(function (tab) { + tab.addEventListener('click', function () { + setCategoryDialogTab(tab.getAttribute('data-pd-expense-tab')); + }); + }); categoryDialog.addEventListener('click', function (event) { if (event.target === categoryDialog) closeCategoryDialog(); }); diff --git a/tests/Feature/EcommerceFinance/ExpenseDetailsDrilldownPageTest.php b/tests/Feature/EcommerceFinance/ExpenseDetailsDrilldownPageTest.php index 6b3610cf..58301937 100644 --- a/tests/Feature/EcommerceFinance/ExpenseDetailsDrilldownPageTest.php +++ b/tests/Feature/EcommerceFinance/ExpenseDetailsDrilldownPageTest.php @@ -21,6 +21,9 @@ class ExpenseDetailsDrilldownPageTest extends FeatureTestCase ->assertSee('/workspace/financial-operations/expense-details/item-summary', false) ->assertSee('id="pdItemSummary"', false) ->assertSee('id="pdExpenseCategoryDialog"', false) + ->assertSee('id="pdExpenseCategoryPlatformTab"', false) + ->assertSee('id="pdExpenseCategoryBillTab"', false) + ->assertSee('id="pdExpenseCategoryDialogPlatformRows"', false) ->assertDontSee('data-col="record_code"', false) ->assertDontSee('data-col="bill_item"', false); @@ -42,7 +45,17 @@ class ExpenseDetailsDrilldownPageTest extends FeatureTestCase ], $columns[1]); $this->assertStringContainsString('pd-item-summary__open', $template); $this->assertStringContainsString('openCategoryDialog(category)', $template); + $this->assertStringContainsString('category.platforms', $template); + $this->assertStringContainsString("setCategoryDialogTab('platform')", $template); $this->assertStringContainsString('pd-expense-dialog__table', $template); + $this->assertSame(1, preg_match( + '/data-pd-expense-panel="platform".*?<\/section>/s', + $template, + $platformPanel, + )); + $this->assertStringContainsString('平台', $platformPanel[0]); + $this->assertStringContainsString('总金额(万)', $platformPanel[0]); + $this->assertStringNotContainsString('账单名称', $platformPanel[0]); $this->assertStringNotContainsString('pd-item-summary__toggle', $template); $this->assertStringNotContainsString('pd-item-summary__drill', $template); $this->assertStringContainsString( diff --git a/tests/Unit/EcommerceFinance/BillingDetailsExpenseFilterTest.php b/tests/Unit/EcommerceFinance/BillingDetailsExpenseFilterTest.php index 6d5cb153..fbc5f253 100644 --- a/tests/Unit/EcommerceFinance/BillingDetailsExpenseFilterTest.php +++ b/tests/Unit/EcommerceFinance/BillingDetailsExpenseFilterTest.php @@ -35,10 +35,10 @@ class BillingDetailsExpenseFilterTest extends TestCase $totals = []; $accumulate = new ReflectionMethod(BillingDetailsPageData::class, 'accumulateLedgerMemoTotal'); $accumulate->setAccessible(true); - $accumulate->invokeArgs($data, [&$totals, '技术服务费', 'wx天猫佣金', -90.0, 'invoice']); - $accumulate->invokeArgs($data, [&$totals, '技术服务费', '基础软件服务费', -10.0, 'non_invoice']); - $accumulate->invokeArgs($data, [&$totals, '推广费用', '代扣返点积分', -5.0, 'invoice']); - $accumulate->invokeArgs($data, [&$totals, '', '', -0.5, 'non_invoice']); + $accumulate->invokeArgs($data, [&$totals, '技术服务费', '天猫', 'wx天猫佣金', -90.0, 'invoice']); + $accumulate->invokeArgs($data, [&$totals, '技术服务费', '支付宝', '基础软件服务费', -10.0, 'non_invoice']); + $accumulate->invokeArgs($data, [&$totals, '推广费用', '抖音', '代扣返点积分', -5.0, 'invoice']); + $accumulate->invokeArgs($data, [&$totals, '', '拼多多', '', -0.5, 'non_invoice']); $sorted = new ReflectionMethod(BillingDetailsPageData::class, 'sortedLedgerMemoTotals'); $sorted->setAccessible(true); @@ -50,6 +50,10 @@ class BillingDetailsExpenseFilterTest extends TestCase 'invoice_net' => -90.0, 'non_invoice_net' => -10.0, 'net' => -100.0, + 'platforms' => [ + ['platform' => '天猫', 'invoice_net' => -90.0, 'non_invoice_net' => 0.0, 'net' => -90.0], + ['platform' => '支付宝', 'invoice_net' => 0.0, 'non_invoice_net' => -10.0, 'net' => -10.0], + ], 'items' => [ ['bill_item' => '天猫佣金', 'invoice_net' => -90.0, 'non_invoice_net' => 0.0, 'net' => -90.0], ['bill_item' => '基础软件服务费', 'invoice_net' => 0.0, 'non_invoice_net' => -10.0, 'net' => -10.0], @@ -60,6 +64,9 @@ class BillingDetailsExpenseFilterTest extends TestCase 'invoice_net' => -5.0, 'non_invoice_net' => 0.0, 'net' => -5.0, + 'platforms' => [ + ['platform' => '抖音', 'invoice_net' => -5.0, 'non_invoice_net' => 0.0, 'net' => -5.0], + ], 'items' => [ ['bill_item' => '代扣返点积分', 'invoice_net' => -5.0, 'non_invoice_net' => 0.0, 'net' => -5.0], ], @@ -69,6 +76,9 @@ class BillingDetailsExpenseFilterTest extends TestCase 'invoice_net' => 0.0, 'non_invoice_net' => -0.5, 'net' => -0.5, + 'platforms' => [ + ['platform' => '拼多多', 'invoice_net' => 0.0, 'non_invoice_net' => -0.5, 'net' => -0.5], + ], 'items' => [ ['bill_item' => '未填写账单项目', 'invoice_net' => 0.0, 'non_invoice_net' => -0.5, 'net' => -0.5], ],