276 lines
11 KiB
PHP
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);
|
|
}
|
|
}
|