From d13997c74edb244e6ed90ee378c996dc8c86da0a Mon Sep 17 00:00:00 2001 From: yueyang <2495544247@qq.com> Date: Sat, 24 Oct 2026 19:01:39 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=E8=B4=B9=E7=94=A8=E6=98=8E?= =?UTF-8?q?=E7=BB=86=E5=B8=83=E5=B1=80=E5=B9=B6=E6=8C=89=E5=BC=80=E7=A5=A8?= =?UTF-8?q?=E5=8F=A3=E5=BE=84=E6=8B=86=E5=88=86=E6=B1=87=E6=80=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../View/BillingDetailsPageData.php | 98 +++++++++++++------ .../billing-details.blade.php | 68 +++++++++---- tests/Feature/Settings/NavigationTest.php | 4 + .../BillingDetailsExpenseFilterTest.php | 13 +-- 4 files changed, 129 insertions(+), 54 deletions(-) diff --git a/modules/EcommerceFinance/View/BillingDetailsPageData.php b/modules/EcommerceFinance/View/BillingDetailsPageData.php index ff9c3465..61b8aea8 100644 --- a/modules/EcommerceFinance/View/BillingDetailsPageData.php +++ b/modules/EcommerceFinance/View/BillingDetailsPageData.php @@ -357,7 +357,12 @@ class BillingDetailsPageData /** * 费用明细的账单项目汇总:只响应汇总请求,避免拖慢主表首屏。 * - * @return array{items:list,item_count:int,rows:int,net:float} + * @return array{ + * items:list, + * item_count:int, + * invoice_net:float, + * non_invoice_net:float + * } */ public function billItemSummary(?array $input = null, array $forcedFilters = []): array { @@ -366,39 +371,75 @@ class BillingDetailsPageData $signature = md5((string) json_encode($filters, JSON_UNESCAPED_UNICODE)); return cache()->remember( - 'billing-details-item-summary-v1:' . $signature, + 'billing-details-item-summary-v2:' . $signature, now()->addMinutes(2), function () use ($filters): array { $connection = $this->connection(); + $rulesByPlatform = $this->expenseRulesByPlatform(); $parts = []; $bind = []; foreach (self::SOURCES as $label => $meta) { $branch = $this->branch($label, $meta, $filters); + $invoiceConditions = []; + $nonInvoiceConditions = []; + foreach (self::BRANCH_PLATFORMS[$label] ?? [] as $platform) { + $invoiceItems = array_keys($rulesByPlatform[$platform]['invoice_expense'] ?? []); + if ($invoiceItems !== []) { + $invoiceConditions[] = '(' . $this->storePrefixCondition($platform) + . ' AND ' . $this->normalizedItemSql() + . ' IN (' . $this->literals($invoiceItems) . '))'; + } + $nonInvoiceItems = array_keys($rulesByPlatform[$platform]['non_invoice_expense'] ?? []); + if ($nonInvoiceItems !== []) { + $nonInvoiceConditions[] = '(' . $this->storePrefixCondition($platform) + . ' AND ' . $this->normalizedItemSql() + . ' IN (' . $this->literals($nonInvoiceItems) . '))'; + } + } + if ($invoiceConditions === [] && $nonInvoiceConditions === []) { + continue; + } + + $bucketSql = 'CASE'; + if ($invoiceConditions !== []) { + $bucketSql .= ' WHEN (' . implode(' OR ', $invoiceConditions) + . ") THEN 'invoice'"; + } + if ($nonInvoiceConditions !== []) { + $bucketSql .= ' WHEN (' . implode(' OR ', $nonInvoiceConditions) + . ") THEN 'non_invoice'"; + } + $bucketSql .= " ELSE 'unknown' END"; + foreach ($branch['scopes'] as $scope) { - $parts[] = 'SELECT bill_item, COUNT(*) AS c, ' + $parts[] = 'SELECT ' . $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 bill_item'; + . ' GROUP BY invoice_bucket, bill_item'; $bind = array_merge($bind, $scope['bind']); } } $totals = []; - $rows = 0; - $net = 0.0; + $invoiceNet = 0.0; + $nonInvoiceNet = 0.0; foreach ($connection->select(implode(' UNION ALL ', $parts), $bind) as $part) { - $partRows = (int) $part->c; - $partIncome = (float) $part->inc; - $partExpense = (float) $part->exp; - $rows += $partRows; - $net += $partIncome - $partExpense; + $bucket = (string) ($part->invoice_bucket ?? ''); + if (! in_array($bucket, ['invoice', 'non_invoice'], true)) { + continue; + } + $net = (float) $part->inc - (float) $part->exp; + if ($bucket === 'invoice') { + $invoiceNet += $net; + } else { + $nonInvoiceNet += $net; + } $this->accumulateBillItemTotal( $totals, (string) ($part->bill_item ?? ''), - $partRows, - $partIncome, - $partExpense, + $net, + $bucket, ); } @@ -407,40 +448,41 @@ class BillingDetailsPageData return [ 'items' => $items, 'item_count' => count($items), - 'rows' => $rows, - 'net' => round($net, 2), + 'invoice_net' => round($invoiceNet, 2), + 'non_invoice_net' => round($nonInvoiceNet, 2), ]; }, ); } - /** @param array $totals */ + /** @param array $totals */ private function accumulateBillItemTotal( array &$totals, string $billItem, - int $rows, - float $income, - float $expense, + float $net, + string $invoiceBucket, ): void { $billItem = $this->normalizeItem($billItem) ?: '未填写账单项目'; - $totals[$billItem] ??= ['rows' => 0, 'income' => 0.0, 'expense' => 0.0]; - $totals[$billItem]['rows'] += $rows; - $totals[$billItem]['income'] += $income; - $totals[$billItem]['expense'] += $expense; + $totals[$billItem] ??= ['invoice_net' => 0.0, 'non_invoice_net' => 0.0]; + $key = $invoiceBucket === 'invoice' ? 'invoice_net' : 'non_invoice_net'; + $totals[$billItem][$key] += $net; } /** - * @param array $totals - * @return list + * @param array $totals + * @return list */ private function sortedBillItemTotals(array $totals): array { $items = []; foreach ($totals as $item => $total) { + $invoiceNet = round((float) $total['invoice_net'], 2); + $nonInvoiceNet = round((float) $total['non_invoice_net'], 2); $items[] = [ 'item' => $item, - 'rows' => (int) $total['rows'], - 'net' => round((float) $total['income'] - (float) $total['expense'], 2), + 'invoice_net' => $invoiceNet, + 'non_invoice_net' => $nonInvoiceNet, + 'net' => round($invoiceNet + $nonInvoiceNet, 2), ]; } usort($items, static function (array $left, array $right): int { 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 175601a4..f507a985 100644 --- a/resources/views/business/pages/financial-operations/billing-details.blade.php +++ b/resources/views/business/pages/financial-operations/billing-details.blade.php @@ -26,6 +26,17 @@ .pd-stat__note { margin-top:.1875rem; color:#595959; font-size:.75rem; } .pd-stat .is-income { color:#53793e; } .pd-stat .is-expense { color:#b80000; } + .pd-page-intro { order:1; } + .pd-page-title-row { display:flex; flex-wrap:wrap; align-items:center; gap:.65rem 1rem; margin:.25rem 0 .5rem; } + .pd-page-title-row h2 { margin:0; font-size:1.05rem; } + .pd-page-description { color:#595959; font-size:.8125rem; margin-bottom:.75rem; } + .pd-compact-summary { display:flex; flex-wrap:wrap; align-items:center; gap:.35rem .85rem; margin-left:auto; padding:.35rem .6rem; border:1px solid #e3dac9; border-radius:.45rem; background:#faf6ee; color:#595959; font-size:.75rem; } + .pd-compact-summary span { display:inline-flex; align-items:baseline; gap:.25rem; white-space:nowrap; } + .pd-compact-summary strong { color:#2f2a23; font-size:.8125rem; font-variant-numeric:tabular-nums; } + .expense-details-page { display:flex; flex-direction:column; } + .expense-details-page #pdItemSummary { order:2; } + .expense-details-page #pdFilterForm { order:3; } + .expense-details-page .pd-detail-list { order:4; } .pd-item-summary { margin:0 0 1rem; border:1px solid #d8d2c7; border-radius:.75rem; background:#fff; overflow:hidden; } .pd-item-summary__head { display:flex; flex-wrap:wrap; align-items:center; gap:.5rem .875rem; padding:.75rem 1rem; border-bottom:1px solid #e8e0d2; background:#faf6ee; } .pd-item-summary__head strong { color:#2f2a23; } @@ -107,19 +118,31 @@ .pd-export-progress__bar { height:.5rem; border:1px solid #d8d2c7; border-radius:999px; background:#fff; overflow:hidden; } .pd-export-progress__bar span { display:block; height:100%; width:0; background:#856632; transition:width .3s ease; } .pd-export-progress__text { margin-top:.35rem; color:#595959; font-size:.75rem; } - @media (max-width:900px) { .pd-stats { grid-template-columns:repeat(2, minmax(0, 1fr)); } } + @media (max-width:900px) { .pd-stats { grid-template-columns:repeat(2, minmax(0, 1fr)); } .pd-compact-summary { margin-left:0; } } @media (max-width:520px) { .pd-stats { grid-template-columns:1fr; } } -

{{ $pageTitle }}

-
- 数据源=各平台已导入账单(与「店铺收款账户账单」同源);财务备注、财务类型由科目映射表按「平台+账单项目(wx 自动去前缀)」带出,匹配不到的留空并标红。 - @if ($expenseOnly) - 本页只展示科目映射表中财务类型=费用的账单;账单项目按当前筛选汇总,金额单位为万。 - @else - 同一交易单号跨月合并后已与订单实付对平的,差额备注优先显示为差额=跨月收款(合并已对平);未对平仍显示差额待核实。 - @endif - 账单项目不再单独筛选,直接在关键词里输入即可(关键词同时匹配记录编码 / 订单号 / 账单项目 / 商户订单号 / 备注)。 +
+
+

{{ $pageTitle }}

+ @if ($expenseOnly) +
+ 收入 ¥{{ number_format($summary['income'], 2) }} + 支出 ¥{{ number_format($summary['expense'], 2) }} + 差额 ¥{{ number_format($summary['net'], 2) }} + 笔数 {{ number_format($summary['rows']) }} +
+ @endif +
+
+ 数据源=各平台已导入账单(与「店铺收款账户账单」同源);财务备注、财务类型由科目映射表按「平台+账单项目(wx 自动去前缀)」带出,匹配不到的留空并标红。 + @if ($expenseOnly) + 本页只展示科目映射表中财务类型=费用的账单;账单项目按当前筛选汇总,金额单位为万。 + @else + 同一交易单号跨月合并后已与订单实付对平的,差额备注优先显示为差额=跨月收款(合并已对平);未对平仍显示差额待核实。 + @endif + 账单项目不再单独筛选,直接在关键词里输入即可(关键词同时匹配记录编码 / 订单号 / 账单项目 / 商户订单号 / 备注)。 +
@@ -206,6 +229,7 @@
+ @if (! $expenseOnly)
收入合计
@@ -228,6 +252,7 @@
当前筛选结果
+ @endif @if ($expenseOnly)
@@ -241,16 +266,16 @@ 账单项目 - 汇总金额(万) - 笔数 + 开票金额(万) + 不开票金额(万) 合计 - — - — + — + — @@ -258,6 +283,7 @@
@endif +
@if (! empty($error))
{{ $error }}
@else @@ -349,6 +375,7 @@
@endif +