CaiWuXiTong/modules/EcommerceFinance/View/SupplierReconciliationPageData.php
苏木 eeb3001946 供应商对账:退货明细按导入顺序展示,不再按金额排序
退货扣减确认表取消「金额从高到低」排序,按导入顺序分页展示(每页 15 行),页面说明同步调整。
2026-10-17 10:24:00 +08:00

276 lines
11 KiB
PHP

<?php
namespace Modules\EcommerceFinance\View;
use App\Http\Controllers\Business\PayablesFinalActions;
use Modules\EcommerceFinance\Services\SupplierPayableOverrideStore;
use Modules\EcommerceFinance\Services\SupplierReturnConfirmation;
use Modules\EcommerceFinance\Services\SupplierStatementMatcher;
use Modules\EcommerceFinance\Services\SupplierStatementStore;
/**
* 「供应商对账」页面数据:
* - 财务端数据:按供应商汇总 + 行级差异(条码 + 单价 匹配,四类结果带结算口径)
* - 厂家端数据:厂家上传的对账单(实时)与只读的退货明细
*/
class SupplierReconciliationPageData
{
/** 厂家上传的对账数据表:每页行数(短表 + 翻页)。 */
private const PER_PAGE = 15;
public function __construct(
private readonly SupplierPayableOverrideStore $store,
private readonly SupplierStatementStore $statements,
private readonly SupplierStatementMatcher $matcher,
private readonly SupplierReturnConfirmation $returnConfirmations,
) {
}
/** @return array<string,mixed> */
public function financeData(): array
{
$companyId = $this->companyId();
$mine = $this->store->listRows(
PayablesFinalActions::DATASET_PREFIX . 'supplier:payment-reconciliation',
$companyId,
);
$months = [];
foreach ($mine as $row) {
$month = substr(trim((string) ($row['summary_month'] ?? '')), 0, 7);
if ($month !== '') {
$months[$month] = true;
}
}
$months = array_keys($months);
rsort($months);
$month = trim((string) request()->query('month', ''));
if ($month !== '' && preg_match('/^\d{4}-\d{2}$/', $month) === 1) {
$mine = array_values(array_filter(
$mine,
static fn (array $row): bool => substr(trim((string) ($row['summary_month'] ?? '')), 0, 7) === $month,
));
} else {
$month = '';
}
$reconcilers = [];
foreach ($mine as $row) {
$name = trim((string) ($row['reconciler'] ?? ''));
if ($name !== '') {
$reconcilers[$name] = true;
}
}
$reconcilers = array_keys($reconcilers);
sort($reconcilers);
$reconciler = trim((string) request()->query('reconciler', ''));
if ($reconciler !== '') {
$mine = array_values(array_filter(
$mine,
static fn (array $row): bool => trim((string) ($row['reconciler'] ?? '')) === $reconciler,
));
}
$suppliers = [];
foreach ($mine as $row) {
$name = trim((string) ($row['supplier'] ?? ''));
if ($name !== '') {
$suppliers[$name] = true;
}
}
foreach ($this->statements->uploadedSuppliers($companyId) as $name) {
$suppliers[$name] = true;
}
$supplierNames = array_keys($suppliers);
sort($supplierNames);
$uploads = [];
foreach ($supplierNames as $name) {
$latest = $this->statements->uploads($name, $companyId, 1);
$uploads[$name] = $latest[0] ?? null;
}
// 选中厂家时页面切换成「比对明细」视图(列表不再渲染,避免 100+ 家时往下翻)。
$selected = trim((string) request()->query('supplier', ''));
$detail = null;
if ($selected !== '' && in_array($selected, $supplierNames, true)) {
$detail = $this->detail($mine, $selected, $companyId);
} else {
$selected = '';
}
return [
'view' => 'business.pages.supplier-reconciliation.finance-data',
'supplierNames' => $supplierNames,
'uploads' => $uploads,
'months' => $months,
'month' => $month,
'reconcilers' => $reconcilers,
'reconciler' => $reconciler,
'selectedSupplier' => $selected,
'detail' => $detail,
'listUrl' => route('business.pages.show', array_filter([
'section' => 'supplier-reconciliation',
'page' => 'finance-data',
'month' => $month,
'reconciler' => $reconciler,
], static fn (mixed $value): bool => $value !== '' && $value !== null)),
'statementTemplateUrl' => route('business.supplier-reconciliation.template'),
'uploadUrl' => route('business.supplier-reconciliation.statement.upload'),
];
}
/** @return array<string,mixed> */
public function supplierData(): array
{
$companyId = $this->companyId();
$mine = $this->store->listRows(
PayablesFinalActions::DATASET_PREFIX . 'supplier:payment-reconciliation',
$companyId,
);
$suppliers = [];
foreach ($mine as $row) {
$name = trim((string) ($row['supplier'] ?? ''));
if ($name !== '') {
$suppliers[$name] = true;
}
}
foreach ($this->statements->uploadedSuppliers($companyId) as $name) {
$suppliers[$name] = true;
}
$supplierNames = array_keys($suppliers);
sort($supplierNames);
$selected = trim((string) request()->query('supplier', ''));
if ($selected === '' || ! in_array($selected, $supplierNames, true)) {
$selected = $supplierNames[0] ?? '';
}
$page = max((int) request()->query('rows_page', 1), 1);
$statementRows = $selected !== '' ? $this->statements->rows($selected, $companyId) : [];
$returnRows = $selected !== '' ? $this->returnRows($selected, $companyId) : [];
$returnPanel = $selected !== '' ? $this->returnConfirmationPanel($selected, $returnRows, $companyId) : null;
return [
'view' => 'business.pages.supplier-reconciliation.supplier-data',
'supplierNames' => $supplierNames,
'selectedSupplier' => $selected,
'uploads' => $selected !== '' ? $this->statements->uploads($selected, $companyId) : [],
'statementRows' => array_slice($statementRows, ($page - 1) * self::PER_PAGE, self::PER_PAGE),
'statementTotal' => count($statementRows),
'returnPanel' => $returnPanel,
'rowsPage' => $page,
'perPage' => self::PER_PAGE,
'statementTemplateUrl' => route('business.supplier-reconciliation.template'),
'uploadUrl' => route('business.supplier-reconciliation.statement.upload'),
'returnConfirmUrl' => route('business.supplier-reconciliation.return.confirm'),
'returnDisputeUrl' => route('business.supplier-reconciliation.return.dispute'),
'returnConfirmAllUrl' => route('business.supplier-reconciliation.return.confirm-all'),
];
}
/**
* 退货扣减确认面板:共 N 行、合计扣减、异议暂不扣减;表格展示金额最高的 15 行。
*
* @param list<array<string,mixed>> $returnRows
* @return array<string,mixed>
*/
private function returnConfirmationPanel(string $supplier, array $returnRows, int $companyId): array
{
$statuses = $this->returnConfirmations->statuses($supplier, $companyId);
$total = 0.0;
$confirmedAmount = 0.0;
$disputedAmount = 0.0;
$disputedCount = 0;
$confirmedCount = 0;
$rows = [];
foreach ($returnRows as $row) {
$key = (string) ($row['_action_key'] ?? '');
$deduction = $this->returnDeduction($row);
$status = $statuses[$key] ?? '';
$total += $deduction;
if ($status === SupplierReturnConfirmation::STATUS_CONFIRMED) {
$confirmedAmount += $deduction;
$confirmedCount++;
} elseif ($status === SupplierReturnConfirmation::STATUS_DISPUTED) {
$disputedAmount += $deduction;
$disputedCount++;
}
$rows[] = ['_key' => $key, 'status' => $status, 'deduction' => $deduction] + $row;
}
// 全部退货行按导入顺序分页展示(每页 15 行),不做金额排序。
$perPage = self::PER_PAGE;
$pages = max((int) ceil(count($rows) / $perPage), 1);
$page = min(max((int) request()->query('return_page', 1), 1), $pages);
return [
'supplier' => $supplier,
'rows' => array_slice($rows, ($page - 1) * $perPage, $perPage),
'page' => $page,
'pages' => $pages,
'per_page' => $perPage,
'total_rows' => count($returnRows),
'total_amount' => round($total, 2),
'confirmed_count' => $confirmedCount,
'confirmed_amount' => round($confirmedAmount, 2),
'disputed_count' => $disputedCount,
'disputed_amount' => round($disputedAmount, 2),
];
}
/** 扣减金额:金额为空时用预估金额(与采购方记账口径一致)。 */
private function returnDeduction(array $row): float
{
$amount = trim((string) ($row['amount'] ?? ''));
if ($amount !== '' && is_numeric(str_replace(',', '', $amount))) {
return (float) str_replace(',', '', $amount);
}
$guess = trim((string) ($row['guess_amount'] ?? ''));
return is_numeric(str_replace(',', '', $guess)) ? (float) str_replace(',', '', $guess) : 0.0;
}
/** @return array<string,mixed> */
private function detail(array $mine, string $supplier, int $companyId): array
{
$mineRows = array_values(array_filter(
$mine,
static fn (array $row): bool => trim((string) ($row['supplier'] ?? '')) === $supplier,
));
$theirRows = $this->statements->rows($supplier, $companyId);
$compared = $this->matcher->compare($mineRows, $theirRows);
$compared['mineRows'] = count($mineRows);
$compared['theirsRows'] = count($theirRows);
return $compared;
}
/** @return list<array<string,mixed>> 该厂家的退货明细(我方数据,展示给厂家确认扣减) */
public function returnRows(string $supplier, int $companyId): array
{
$rows = $this->store->listRows(
PayablesFinalActions::DATASET_PREFIX . 'supplier:return-reconciliation',
$companyId,
);
return array_values(array_filter(
$rows,
static fn (array $row): bool => trim((string) ($row['supplier'] ?? '')) === $supplier,
));
}
private function companyId(): int
{
return (int) (company_id() ?: 1);
}
}