CaiWuXiTong/modules/EcommerceFinance/Services/FinanceLedgerPayableMappingBulk.php
shanshi 20dc695549 feat: 完善财务台账账户维护与分页体验 (#412)
合并账户维护、筛选、列设置、无刷新分页、录入模板及批量映射工具。
2026-10-21 11:00:56 +08:00

151 lines
5.3 KiB
PHP

<?php
namespace Modules\EcommerceFinance\Services;
use App\Models\Auth\User;
use Illuminate\Support\Facades\DB;
/**
* 按账户总表批量建立“供应商应付”公司名称到账户简称的映射。
*
* 公司只有唯一账户,或虽有多个账户但只有一个公户时自动关联;
* 多公户、只有私人卡等歧义情况保留人工选择。
*/
class FinanceLedgerPayableMappingBulk
{
public function preview(): array
{
$connection = $this->connection();
$accountsByCompany = [];
foreach ($connection->table('fin_accounts')->get(['company_name', 'account_short', 'account_type']) as $account) {
$company = trim((string) $account->company_name);
$accountShort = trim((string) $account->account_short);
if ($company === '' || $accountShort === '') {
continue;
}
$accountsByCompany[$company][$accountShort] = [
'account_short' => $accountShort,
'account_type' => trim((string) $account->account_type),
];
}
$rows = $connection->table('fin_account_map')
->where('source', 'payable')
->where(function ($query): void {
$query->whereNull('account_short')->orWhere('account_short', '');
})
->orderBy('source_value')
->get(['id', 'source_value']);
$candidates = [];
$ambiguous = [];
$missing = [];
foreach ($rows as $row) {
$company = trim((string) $row->source_value);
$accounts = array_values($accountsByCompany[$company] ?? []);
if ($accounts === []) {
$missing[] = $company;
continue;
}
if (count($accounts) === 1) {
$candidates[] = $this->candidate($row, $accounts[0]['account_short'], 'single_account');
continue;
}
$publicAccounts = array_values(array_filter(
$accounts,
static fn (array $account): bool => $account['account_type'] === '公户',
));
if (count($publicAccounts) === 1) {
$candidates[] = $this->candidate($row, $publicAccounts[0]['account_short'], 'single_public_account');
continue;
}
$ambiguous[] = [
'company' => $company,
'accounts' => array_values(array_map(
static fn (array $account): string => $account['account_short'],
$accounts,
)),
];
}
return [
'total' => $rows->count(),
'eligible' => count($candidates),
'ambiguous' => count($ambiguous),
'missing' => count($missing),
'candidates' => $candidates,
'ambiguous_values' => $ambiguous,
'missing_values' => $missing,
];
}
public function apply(?User $user = null): array
{
$preview = $this->preview();
$connection = $this->connection();
$userName = (string) ($user?->name ?? '');
$userId = (int) ($user?->id ?? 0);
$now = now();
$mapped = 0;
if ($preview['candidates'] !== []) {
$connection->transaction(function () use ($connection, $preview, $userName, $userId, $now, &$mapped): void {
foreach ($preview['candidates'] as $candidate) {
$updated = $connection->table('fin_account_map')
->where('id', $candidate['id'])
->where(function ($query): void {
$query->whereNull('account_short')->orWhere('account_short', '');
})
->update([
'account_short' => $candidate['account_short'],
'updated_by' => $userName,
'updated_at' => $now,
]);
if ($updated !== 1) {
continue;
}
$connection->table('fin_account_map_events')->insert([
'source' => 'payable',
'source_value' => $candidate['source_value'],
'old_account_short' => null,
'new_account_short' => $candidate['account_short'],
'user_id' => $userId,
'user_name' => $userName,
'created_at' => $now,
]);
$mapped++;
}
});
}
if ($mapped > 0) {
app(FinanceLedgerCalculator::class)->recalculate();
}
return [
'total' => $preview['total'],
'mapped' => $mapped,
'ambiguous' => $preview['ambiguous'],
'missing' => $preview['missing'],
];
}
private function candidate(object $row, string $accountShort, string $matchType): array
{
return [
'id' => (int) $row->id,
'source_value' => trim((string) $row->source_value),
'account_short' => $accountShort,
'match_type' => $matchType,
];
}
private function connection()
{
return DB::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
}
}