供应商对账:财务比对筛选与分页优化 #406
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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('晨丰', [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user