供应商对账:财务比对筛选与分页优化

This commit is contained in:
苏木 2026-10-20 17:02:57 +08:00
parent bcb29aac1c
commit beea07387d
3 changed files with 167 additions and 9 deletions

View File

@ -23,6 +23,9 @@ class SupplierReconciliationPageData
/** 退货明细行较高:每页 10 行,并在页面内固定表头滚动查看。 */
private const RETURN_PER_PAGE = 10;
/** 财务端比对明细:每页 15 条。 */
private const COMPARISON_PER_PAGE = 15;
public function __construct(
private readonly SupplierPayableOverrideStore $store,
private readonly SupplierStatementStore $statements,
@ -109,17 +112,31 @@ class SupplierReconciliationPageData
}
// 选中厂家时页面切换成「比对明细」视图(列表不再渲染,避免 100+ 家时往下翻)。
$selected = trim((string) request()->query('supplier', ''));
$supplierQuery = trim((string) request()->query('supplier', ''));
$selected = in_array($supplierQuery, $supplierNames, true) ? $supplierQuery : '';
$supplierNotFound = $supplierQuery !== '' && $selected === '';
$detail = null;
if ($selected !== '' && in_array($selected, $supplierNames, true)) {
if ($selected !== '') {
$detail = $this->detail($mine, $selected, $companyId);
} else {
$selected = '';
$totalRows = count($detail['rows']);
$pages = max((int) ceil($totalRows / self::COMPARISON_PER_PAGE), 1);
$currentPage = min(max((int) request()->query('compare_page', 1), 1), $pages);
$detail['total_rows'] = $totalRows;
$detail['page'] = $currentPage;
$detail['pages'] = $pages;
$detail['per_page'] = self::COMPARISON_PER_PAGE;
$detail['rows'] = array_slice(
$detail['rows'],
($currentPage - 1) * self::COMPARISON_PER_PAGE,
self::COMPARISON_PER_PAGE,
);
}
return [
'view' => 'business.pages.supplier-reconciliation.finance-data',
'supplierNames' => $supplierNames,
'supplierQuery' => $supplierQuery,
'supplierNotFound' => $supplierNotFound,
'uploads' => $uploads,
'months' => $months,
'month' => $month,

View File

@ -36,6 +36,9 @@
.supplier-reconciliation-page .sr-summary-cell { padding: 10px 12px; border: 1px solid var(--vip-border, #D8D2C7); border-radius: 8px; display: grid; gap: 4px; }
.supplier-reconciliation-page .sr-summary-cell b { font-size: 1.125rem; font-weight: 600; }
.supplier-reconciliation-page .sr-summary-cell span { color: var(--vip-content-muted, #595959); font-size: .75rem; }
.supplier-reconciliation-page .sr-comparison-scroll { max-height: min(62vh, 560px); overflow: auto; scrollbar-gutter: stable; }
.supplier-reconciliation-page .sr-comparison-scroll thead th { position: sticky; top: 0; z-index: 4; box-shadow: 0 1px 0 var(--vip-border, #D8D2C7); }
.supplier-reconciliation-page .sr-pagination { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; color: var(--vip-content-muted, #595959); font-size: .8125rem; }
.supplier-reconciliation-page .sr-robot-config { padding: 10px 12px; border: 1px solid #EFEAE0; border-radius: 8px; background: #FCFAF5; }
.supplier-reconciliation-page .sr-robot-config summary { cursor: pointer; color: #5B5343; font-size: .8125rem; font-weight: 600; }
.supplier-reconciliation-page .sr-robot-form { display: grid; grid-template-columns: minmax(260px, 2fr) minmax(180px, 1fr) auto; gap: 10px; align-items: end; margin-top: 12px; }
@ -82,12 +85,17 @@
</select>
</label>
<label class="sr-field"><span>供应商</span>
<select name="supplier" onchange="this.form.submit()">
<option value="">请选择厂家</option>
<input type="search"
name="supplier"
value="{{ $supplierQuery !== '' ? $supplierQuery : $selectedSupplier }}"
list="finance-supplier-options"
placeholder="输入厂家名称搜索"
autocomplete="off">
<datalist id="finance-supplier-options">
@foreach ($supplierNames as $supplier)
<option value="{{ $supplier }}" @selected($selectedSupplier === $supplier)>{{ $supplier }}</option>
<option value="{{ $supplier }}"></option>
@endforeach
</select>
</datalist>
</label>
<button type="submit" class="sr-button is-primary">查询</button>
</div>
@ -97,6 +105,11 @@
<a href="{{ route('vendors.index') }}" class="sr-button">厂家登录账号管理</a>
@endcanany
</form>
@if ($supplierNotFound)
<div class="biz-alert biz-alert-error" role="alert">
未找到名为「{{ $supplierQuery }}」的供应商,请输入完整名称并从搜索建议中选择。
</div>
@endif
<p class="sr-hint">
比对口径:按「采购单号 + 日期 + 条码」核对,同一采购单号、日期和条码的多行先合并数量与金额。
数量、金额一致 → 正常结算;厂家多(我方没有)→ 不结算、退回修改;
@ -251,7 +264,7 @@
<div class="sr-summary-cell"><span>按口径结算合计</span><b>{{ number_format($detail['summary']['settleAmount'], 2) }}</b></div>
<div class="sr-summary-cell"><span>差异金额</span><b>{{ number_format($detail['summary']['differenceAmount'], 2) }}</b></div>
</div>
<div class="sr-scroll">
<div class="sr-scroll sr-comparison-scroll">
<table>
<thead>
<tr>
@ -361,6 +374,43 @@
</tbody>
</table>
</div>
<div class="sr-pagination">
@php
$comparisonPageCount = max((int) ($detail['pages'] ?? 1), 1);
$comparisonPageCurrent = min(max((int) ($detail['page'] ?? 1), 1), $comparisonPageCount);
$comparisonPageStart = max(1, min($comparisonPageCurrent - 4, $comparisonPageCount - 8));
$comparisonPageEnd = min($comparisonPageCount, $comparisonPageStart + 8);
$comparisonPageUrl = fn (int $number): string => route('business.pages.show', array_merge(request()->query(), [
'section' => $section['slug'],
'page' => $page['slug'],
'compare_page' => $number,
]));
@endphp
<span>
共 {{ number_format((int) ($detail['total_rows'] ?? 0)) }} 条
· 每页 {{ number_format((int) ($detail['per_page'] ?? 15)) }} 条
· 第 {{ $comparisonPageCurrent }} / {{ $comparisonPageCount }} 页
</span>
@if ($comparisonPageCurrent > 1)
<a class="sr-button is-small" href="{{ $comparisonPageUrl($comparisonPageCurrent - 1) }}">上一页</a>
@endif
@if ($comparisonPageStart > 1)
<a class="sr-button is-small" href="{{ $comparisonPageUrl(1) }}">1</a>
@if ($comparisonPageStart > 2) <span>…</span> @endif
@endif
@for ($number = $comparisonPageStart; $number <= $comparisonPageEnd; $number++)
<a class="sr-button is-small {{ $number === $comparisonPageCurrent ? 'is-primary' : '' }}"
@if ($number === $comparisonPageCurrent) aria-current="page" @endif
href="{{ $comparisonPageUrl($number) }}">{{ $number }}</a>
@endfor
@if ($comparisonPageEnd < $comparisonPageCount)
@if ($comparisonPageEnd < $comparisonPageCount - 1) <span>…</span> @endif
<a class="sr-button is-small" href="{{ $comparisonPageUrl($comparisonPageCount) }}">{{ $comparisonPageCount }}</a>
@endif
@if ($comparisonPageCurrent < $comparisonPageCount)
<a class="sr-button is-small" href="{{ $comparisonPageUrl($comparisonPageCurrent + 1) }}">下一页</a>
@endif
</div>
</div>
@php $returnAccounting = $financeReturnPanel['accounting_summary']; @endphp

View File

@ -47,6 +47,7 @@ class SupplierReconciliationTest extends FeatureTestCase
->assertSeeText('厂家多')
->assertSeeText('取金额低的一方结算')
->assertSeeText('采购单号 + 日期 + 条码')
->assertSee('list="finance-supplier-options"', false)
->assertSeeText('对账人')
->assertSeeText('下载对账单模板');
}
@ -114,11 +115,101 @@ class SupplierReconciliationTest extends FeatureTestCase
->assertSeeText('厂家多 1.00')
->assertSeeText('厂家多 ¥128.00')
->assertSeeText('编号-ZSJ-1N1097V01')
->assertSee('sr-comparison-scroll', false)
->assertSeeText('每页 15 条')
->assertSeeText('ZSJ-3')
->assertSeeText('ZSJ-2')
->assertDontSeeText('各厂家上传情况');
}
public function test_finance_supplier_filter_requires_an_exact_searchable_name(): void
{
$store = app(SupplierPayableOverrideStore::class);
$companyId = (int) $this->company->id;
foreach (['晨丰', '鸣飞家具'] as $supplier) {
$store->save('final:supplier:payment-reconciliation', 'supplier:' . $supplier, [
'summary_month' => '2026-09',
'purchaseNo' => 'PO-' . $supplier,
'date' => '2026-09-30',
'supplier' => $supplier,
'skuCode' => 'A-1',
'qty' => '1',
'unitPrice' => '10',
'amount' => '10',
'reconciler' => '张三',
], $companyId, null);
}
$pageUrl = route('business.pages.show', [
'company_id' => $companyId,
'section' => 'supplier-reconciliation',
'page' => 'finance-data',
]);
$this->loginAs()->get($pageUrl . '?supplier=' . urlencode('晨丰'))
->assertOk()
->assertSeeText('晨丰 · 比对明细')
->assertSee('value="晨丰"', false);
$this->loginAs()->get($pageUrl . '?supplier=' . urlencode('不存在的厂家'))
->assertOk()
->assertSeeText('未找到名为「不存在的厂家」的供应商')
->assertDontSeeText('晨丰 · 比对明细')
->assertDontSeeText('鸣飞家具 · 比对明细');
}
public function test_finance_comparison_rows_are_paginated_fifteen_per_page(): void
{
$companyId = (int) $this->company->id;
$store = app(SupplierPayableOverrideStore::class);
$statementRows = [];
for ($i = 1; $i <= 20; $i++) {
$purchaseNo = sprintf('PO-%02d', $i);
$barcode = sprintf('A-%02d', $i);
$store->save('final:supplier:payment-reconciliation', 'compare:' . $i, [
'summary_month' => '2026-09',
'purchaseNo' => $purchaseNo,
'date' => '2026-09-30',
'supplier' => '晨丰',
'skuCode' => $barcode,
'qty' => '1',
'unitPrice' => '10',
'amount' => '10',
'reconciler' => '张三',
], $companyId, null);
$statementRows[] = array_merge(
$this->statementRow($barcode . 'N1097V01', '10.00', '1', '10.00'),
['purchaseNo' => $purchaseNo],
);
}
$this->seedStatement('晨丰', $statementRows);
$pageUrl = fn (int $page): string => route('business.pages.show', [
'company_id' => $companyId,
'section' => 'supplier-reconciliation',
'page' => 'finance-data',
'supplier' => '晨丰',
'compare_page' => $page,
]);
$this->loginAs()->get($pageUrl(1))
->assertOk()
->assertSeeText('第 1 / 2 页')
->assertSeeText('每页 15 条')
->assertSeeText('A-01')
->assertSeeText('A-15')
->assertDontSeeText('A-16')
->assertDontSeeText('A-20');
$this->loginAs()->get($pageUrl(2))
->assertOk()
->assertSeeText('第 2 / 2 页')
->assertSeeText('A-16')
->assertSeeText('A-20')
->assertDontSeeText('A-15')
->assertDontSeeText('A-01');
}
public function test_supplier_page_shows_uploads_and_return_rows(): void
{
$this->seedStatement('晨丰', [