From b0fdeb88936eaebdee52f29b11966cc2a930e9ed Mon Sep 17 00:00:00 2001 From: shanshi <3316401569@qq.com> Date: Fri, 6 Nov 2026 16:55:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=9A=94=E7=A6=BB=E6=9C=B1=E4=BF=8A?= =?UTF-8?q?=E7=A5=A5=E6=9C=88=E5=BA=A6=E8=B4=A6=E5=8D=95=E8=B4=A6=E6=88=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Console/Commands/VerifyFinanceLedger.php | 23 +- .../Controllers/Business/FinanceLedger.php | 49 +++-- .../FinanceLedgerIndependentAccounts.php | 36 ++++ .../Services/CompanyAlipayBillImporter.php | 6 + .../CompanyAlipayStatementImporter.php | 4 + .../Services/FinanceLedgerAccountTemplate.php | 3 +- .../Services/FinanceLedgerCalculator.php | 28 ++- .../FinanceLedgerPayableMappingBulk.php | 4 +- .../Services/FinanceLedgerShopDirectory.php | 3 +- .../Services/FinanceLedgerShopMappingBulk.php | 6 +- .../FinanceLedgerSnapshotImporter.php | 2 + .../FinanceLedgerTransactionWriter.php | 2 + .../View/FinanceLedgerPageData.php | 196 ++++++++++++++---- .../EcommerceFinance/View/ShopFlowQuery.php | 16 +- .../pages/financial-ledger/ledger.blade.php | 41 +++- .../FinanceLedgerHeaderControlsTest.php | 105 ++++++++++ .../FinanceLedgerManualTransactionTest.php | 116 +++++++++-- tests/Feature/Business/ShopFlowPageTest.php | 31 +++ .../Console/ImportCompanyAlipayBillsTest.php | 23 ++ .../ImportCompanyAlipayStatementTest.php | 47 +++++ .../FinanceLedgerCalculatorTest.php | 46 ++++ 21 files changed, 691 insertions(+), 96 deletions(-) create mode 100644 app/Utilities/FinanceLedgerIndependentAccounts.php diff --git a/app/Console/Commands/VerifyFinanceLedger.php b/app/Console/Commands/VerifyFinanceLedger.php index 9d2e8cb9..bf467440 100644 --- a/app/Console/Commands/VerifyFinanceLedger.php +++ b/app/Console/Commands/VerifyFinanceLedger.php @@ -2,6 +2,7 @@ namespace App\Console\Commands; +use App\Utilities\FinanceLedgerIndependentAccounts; use Illuminate\Console\Command; use Illuminate\Support\Facades\DB; @@ -14,19 +15,23 @@ class VerifyFinanceLedger extends Command public function handle(): int { $db = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror')); - $accounts = (int) $db->table('fin_accounts')->count(); + $accountQuery = FinanceLedgerIndependentAccounts::exclude($db->table('fin_accounts')); + $openingQuery = FinanceLedgerIndependentAccounts::exclude($db->table('fin_account_opening')); + $transactionQuery = FinanceLedgerIndependentAccounts::exclude($db->table('fin_transactions')); + $mappingQuery = FinanceLedgerIndependentAccounts::exclude($db->table('fin_account_map')); + $accounts = (int) (clone $accountQuery)->count(); $shops = (int) $db->table('fin_shops')->count(); $companies = (int) $db->table('fin_companies')->count(); - $openingRows = (int) $db->table('fin_account_opening')->count(); - $transactions = (int) $db->table('fin_transactions')->count(); + $openingRows = (int) (clone $openingQuery)->count(); + $transactions = (int) (clone $transactionQuery)->count(); $flows = (int) $db->table('fin_daily_flows')->count(); - $maps = (int) $db->table('fin_account_map')->count(); - $mapped = (int) $db->table('fin_account_map')->whereNotNull('account_short')->where('account_short', '<>', '')->count(); + $maps = (int) (clone $mappingQuery)->count(); + $mapped = (int) (clone $mappingQuery)->whereNotNull('account_short')->where('account_short', '<>', '')->count(); - $accountBalance = round((float) $db->table('fin_accounts')->sum('current_balance'), 2); - $openingSum = round((float) $db->table('fin_account_opening')->sum('opening_balance'), 2); - $income = round((float) $db->table('fin_transactions')->sum('income'), 2); - $expense = round((float) $db->table('fin_transactions')->sum('expense'), 2); + $accountBalance = round((float) (clone $accountQuery)->sum('current_balance'), 2); + $openingSum = round((float) (clone $openingQuery)->sum('opening_balance'), 2); + $income = round((float) (clone $transactionQuery)->sum('income'), 2); + $expense = round((float) (clone $transactionQuery)->sum('expense'), 2); $flowOpening = round((float) $db->table('fin_daily_flows')->sum('opening_balance'), 2); $flowInflow = round((float) $db->table('fin_daily_flows')->sum('inflow'), 2); $flowOutflow = round((float) $db->table('fin_daily_flows')->sum('outflow'), 2); diff --git a/app/Http/Controllers/Business/FinanceLedger.php b/app/Http/Controllers/Business/FinanceLedger.php index d7ebb777..46abc8f6 100644 --- a/app/Http/Controllers/Business/FinanceLedger.php +++ b/app/Http/Controllers/Business/FinanceLedger.php @@ -6,6 +6,7 @@ use App\Utilities\FinanceLedgerAccountStatus; use App\Utilities\FinanceLedgerAccountShouldKeep; use App\Utilities\FinanceLedgerAccountType; use App\Utilities\FinanceLedgerAccess; +use App\Utilities\FinanceLedgerIndependentAccounts; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Routing\Controller; @@ -84,10 +85,11 @@ class FinanceLedger extends Controller if ($accountType !== '') { $connection = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror')); $allowedTypes = FinanceLedgerAccountType::options( - $connection->table('fin_accounts') + FinanceLedgerIndependentAccounts::exclude($connection->table('fin_accounts')) ->whereNotNull('account_type')->where('account_type', '<>', '') ->distinct()->pluck('account_type')->all(), $connection->table('fin_account_type_overrides') + ->whereNotIn('account_short', FinanceLedgerIndependentAccounts::names()) ->whereNotNull('account_type')->where('account_type', '<>', '') ->distinct()->pluck('account_type')->all(), ); @@ -110,7 +112,7 @@ class FinanceLedger extends Controller 'ok' => true, 'message' => $message, 'account' => $result, - 'total_accounts' => $connection->table('fin_accounts')->count(), + 'total_accounts' => FinanceLedgerIndependentAccounts::exclude($connection->table('fin_accounts'))->count(), 'total_companies' => $connection->table('fin_companies')->count(), ], 201, ['Cache-Control' => 'private, no-store']); } @@ -180,7 +182,10 @@ class FinanceLedger extends Controller 'pending_owner' => trim((string) $request->query('pending_owner', '')), ]; - $query = $connection->table('fin_accounts as accounts') + $query = FinanceLedgerIndependentAccounts::exclude( + $connection->table('fin_accounts as accounts'), + 'accounts.account_short', + ) ->join('fin_account_statuses as account_status', 'account_status.account_short', '=', 'accounts.account_short') ->leftJoin('fin_account_type_overrides as type_override', 'type_override.account_short', '=', 'accounts.account_short') ->leftJoin('fin_account_todos as account_todo', 'account_todo.account_short', '=', 'accounts.account_short') @@ -304,10 +309,11 @@ class FinanceLedger extends Controller }); $typeOptions = FinanceLedgerAccountType::options( - $connection->table('fin_accounts') + FinanceLedgerIndependentAccounts::exclude($connection->table('fin_accounts')) ->whereNotNull('account_type')->where('account_type', '<>', '') ->distinct()->orderBy('account_type')->pluck('account_type')->all(), $connection->table('fin_account_type_overrides') + ->whereNotIn('account_short', FinanceLedgerIndependentAccounts::names()) ->whereNotNull('account_type')->where('account_type', '<>', '') ->distinct()->orderBy('account_type')->pluck('account_type')->all(), ); @@ -385,7 +391,10 @@ class FinanceLedger extends Controller $headerFilterValue = ''; } - $query = $connection->table('fin_accounts as accounts') + $query = FinanceLedgerIndependentAccounts::exclude( + $connection->table('fin_accounts as accounts'), + 'accounts.account_short', + ) ->leftJoin('fin_account_statuses as account_status', 'account_status.account_short', '=', 'accounts.account_short') ->leftJoin('fin_account_type_overrides as type_override', 'type_override.account_short', '=', 'accounts.account_short') ->leftJoin('fin_account_keep_todos as account_todo', 'account_todo.account_short', '=', 'accounts.account_short') @@ -1114,9 +1123,12 @@ class FinanceLedger extends Controller abort_unless(FinanceLedgerAccess::canEditTransactions(user()), 403, '当前账号没有新增手工动账明细权限。'); $openingDate = (string) config('finance_ledger.opening_date', '2026-10-17'); + $isIndependent = FinanceLedgerIndependentAccounts::contains($request->input('account_short')); $data = $request->validate([ 'idempotency_key' => ['required', 'uuid'], - 'txn_date' => ['required', 'date', 'after_or_equal:' . $openingDate], + 'txn_date' => $isIndependent + ? ['required', 'date'] + : ['required', 'date', 'after_or_equal:' . $openingDate], 'account_short' => ['required', 'string', 'max:100'], 'counterparty_info' => ['nullable', 'string', 'max:255'], 'detail' => ['required', 'string', 'max:4000'], @@ -1210,6 +1222,8 @@ class FinanceLedger extends Controller ->where('account_short', $accountShort) ->whereDate('flow_date', (string) $row->txn_date) ->first(['flow_date', 'opening_balance', 'inflow', 'outflow', 'closing_balance']); + $summary = $pageData->transactionSummary(); + $isIndependent = FinanceLedgerIndependentAccounts::contains($accountShort); return response()->json([ 'ok' => true, 'created' => $inserted, @@ -1231,7 +1245,9 @@ class FinanceLedger extends Controller 'account' => [ 'account_short' => (string) ($accountInfo->account_short ?? $accountShort), 'company_name' => (string) ($accountInfo->company_name ?? ''), - 'current_balance' => round((float) ($accountInfo->current_balance ?? 0), 2), + 'current_balance' => $isIndependent + ? $summary['current_balance'] + : round((float) ($accountInfo->current_balance ?? 0), 2), ], 'daily_flow' => $flow === null ? null : [ 'flow_date' => (string) $flow->flow_date, @@ -1240,8 +1256,8 @@ class FinanceLedger extends Controller 'outflow' => round((float) $flow->outflow, 2), 'closing_balance' => round((float) $flow->closing_balance, 2), ], - 'summary' => $pageData->transactionSummary(), - 'total_transactions' => $connection->table('fin_transactions')->count(), + 'summary' => $summary, + 'total_transactions' => $pageData->globalTransactionCount(), 'next_idempotency_key' => (string) Str::uuid(), ], $inserted ? 201 : 200, ['Cache-Control' => 'private, no-store']); } @@ -1535,8 +1551,11 @@ class FinanceLedger extends Controller abort_unless(FinanceLedgerAccess::canEditTransactions(user()), 403, '当前账号没有编辑手工动账明细权限。'); $openingDate = (string) config('finance_ledger.opening_date', '2026-10-17'); + $isIndependent = FinanceLedgerIndependentAccounts::contains($request->input('account_short')); $data = $request->validate([ - 'txn_date' => ['required', 'date', 'after_or_equal:' . $openingDate], + 'txn_date' => $isIndependent + ? ['required', 'date'] + : ['required', 'date', 'after_or_equal:' . $openingDate], 'account_short' => ['required', 'string', 'max:100'], 'counterparty_info' => ['nullable', 'string', 'max:255'], 'detail' => ['required', 'string', 'max:4000'], @@ -1550,7 +1569,9 @@ class FinanceLedger extends Controller return response()->json([ 'ok' => true, 'transaction' => $editor->update($id, $data), - 'message' => '手工明细已保存并重新计算资金流水。', + 'message' => FinanceLedgerIndependentAccounts::contains($data['account_short']) + ? '手工明细已保存到独立账单。' + : '手工明细已保存并重新计算资金流水。', ], 200, ['Cache-Control' => 'private, no-store']); } @@ -1561,12 +1582,14 @@ class FinanceLedger extends Controller { abort_unless(FinanceLedgerAccess::canEditTransactions(user()), 403, '当前账号没有删除手工动账明细权限。'); - $editor->delete($id); + $deleted = $editor->delete($id); return response()->json([ 'ok' => true, 'deleted' => true, - 'message' => '手工明细已删除并重新计算资金流水。', + 'message' => FinanceLedgerIndependentAccounts::contains($deleted['account_short'] ?? null) + ? '手工明细已从独立账单删除。' + : '手工明细已删除并重新计算资金流水。', ], 200, ['Cache-Control' => 'private, no-store']); } diff --git a/app/Utilities/FinanceLedgerIndependentAccounts.php b/app/Utilities/FinanceLedgerIndependentAccounts.php new file mode 100644 index 00000000..88b6bda6 --- /dev/null +++ b/app/Utilities/FinanceLedgerIndependentAccounts.php @@ -0,0 +1,36 @@ +where(function (Builder $scope) use ($column): void { + $scope->whereNull($column)->orWhereNotIn($column, self::ACCOUNTS); + }); + } +} diff --git a/modules/EcommerceFinance/Services/CompanyAlipayBillImporter.php b/modules/EcommerceFinance/Services/CompanyAlipayBillImporter.php index c600a7ec..3d477a0f 100644 --- a/modules/EcommerceFinance/Services/CompanyAlipayBillImporter.php +++ b/modules/EcommerceFinance/Services/CompanyAlipayBillImporter.php @@ -2,6 +2,7 @@ namespace Modules\EcommerceFinance\Services; +use App\Utilities\FinanceLedgerIndependentAccounts; use Carbon\CarbonImmutable; use Illuminate\Database\ConnectionInterface; use Illuminate\Database\DatabaseManager; @@ -48,6 +49,11 @@ class CompanyAlipayBillImporter } $account = $this->account($accountNo, $accountShort); + if (FinanceLedgerIndependentAccounts::contains($account->account_short)) { + throw new InvalidArgumentException( + '“' . $account->account_short . '”是独立月度账单账户,请使用账务组合查询月度导入。', + ); + } $rows = $this->rows($path, $accountNo, $account->account_short, $date); if ($rows === []) { throw new InvalidArgumentException("支付宝账单中没有 {$date} 的有效交易,已保留原有动账明细。"); diff --git a/modules/EcommerceFinance/Services/CompanyAlipayStatementImporter.php b/modules/EcommerceFinance/Services/CompanyAlipayStatementImporter.php index e35400c9..c8ca1e62 100644 --- a/modules/EcommerceFinance/Services/CompanyAlipayStatementImporter.php +++ b/modules/EcommerceFinance/Services/CompanyAlipayStatementImporter.php @@ -2,6 +2,7 @@ namespace Modules\EcommerceFinance\Services; +use App\Utilities\FinanceLedgerIndependentAccounts; use Carbon\CarbonImmutable; use Illuminate\Database\ConnectionInterface; use Illuminate\Database\DatabaseManager; @@ -86,6 +87,9 @@ class CompanyAlipayStatementImporter )); } + if (FinanceLedgerIndependentAccounts::contains($account->account_short) && $replaceSources === []) { + $replaceSources = [self::SOURCE]; + } $replaceIds = $this->replacementIds($account, $from, $to, $replaceSources); $result = [ 'dry_run' => $dryRun, diff --git a/modules/EcommerceFinance/Services/FinanceLedgerAccountTemplate.php b/modules/EcommerceFinance/Services/FinanceLedgerAccountTemplate.php index f5658433..8af03bf2 100644 --- a/modules/EcommerceFinance/Services/FinanceLedgerAccountTemplate.php +++ b/modules/EcommerceFinance/Services/FinanceLedgerAccountTemplate.php @@ -2,6 +2,7 @@ namespace Modules\EcommerceFinance\Services; +use App\Utilities\FinanceLedgerIndependentAccounts; use App\Utilities\FinanceLedgerAccountStatus; use App\Utilities\FinanceLedgerAccountShouldKeep; use App\Utilities\FinanceLedgerAccountType; @@ -185,7 +186,7 @@ class FinanceLedgerAccountTemplate private function accountTypes(): array { $connection = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror')); - $base = $connection->table('fin_accounts') + $base = FinanceLedgerIndependentAccounts::exclude($connection->table('fin_accounts')) ->whereNotNull('account_type')->where('account_type', '<>', '') ->distinct()->pluck('account_type')->all(); $overrides = $connection->table('fin_account_type_overrides') diff --git a/modules/EcommerceFinance/Services/FinanceLedgerCalculator.php b/modules/EcommerceFinance/Services/FinanceLedgerCalculator.php index 65916f57..e24ae0c3 100644 --- a/modules/EcommerceFinance/Services/FinanceLedgerCalculator.php +++ b/modules/EcommerceFinance/Services/FinanceLedgerCalculator.php @@ -3,6 +3,7 @@ namespace Modules\EcommerceFinance\Services; use App\Utilities\FinanceLedgerAccountStatus; +use App\Utilities\FinanceLedgerIndependentAccounts; use Illuminate\Support\Facades\DB; /** @@ -30,7 +31,9 @@ class FinanceLedgerCalculator ->groupBy('account_short') ->map(static fn ($rows) => $rows->last()); - $accountCompanies = $connection->table('fin_accounts') + $accountCompanies = FinanceLedgerIndependentAccounts::exclude( + $connection->table('fin_accounts'), + ) ->select(['account_short', 'company_name']) ->whereNotNull('account_short') ->where('account_short', '<>', '') @@ -225,11 +228,24 @@ class FinanceLedgerCalculator $this->refreshAccountAmounts(); - $companies = $connection->table('fin_accounts') + $companies = FinanceLedgerIndependentAccounts::exclude( + $connection->table('fin_accounts'), + ) ->selectRaw('company_name, SUM(current_balance) AS balance') ->groupBy('company_name') ->pluck('balance', 'company_name') ->all(); + foreach ($connection->table('fin_accounts') + ->whereIn('account_short', FinanceLedgerIndependentAccounts::names()) + ->whereNotNull('company_name') + ->where('company_name', '<>', '') + ->distinct() + ->pluck('company_name') as $companyName) { + $companyName = (string) $companyName; + if (! array_key_exists($companyName, $companies)) { + $companies[$companyName] = 0; + } + } $this->updateBalances( $connection, @@ -259,6 +275,10 @@ class FinanceLedgerCalculator { $connection = $this->connection(); if ($accountShort !== null) { + if (FinanceLedgerIndependentAccounts::contains($accountShort)) { + return 0; + } + $status = $connection->table('fin_account_statuses') ->where('account_short', $accountShort) ->value('status'); @@ -279,10 +299,12 @@ class FinanceLedgerCalculator ->where('status', '<>', FinanceLedgerAccountStatus::NORMAL) ->pluck('account_short') ->filter() + ->reject(static fn (mixed $account): bool => FinanceLedgerIndependentAccounts::contains($account)) ->unique() ->values() ->all(); - $normalCount = $connection->table('fin_accounts')->update([ + $normalQuery = FinanceLedgerIndependentAccounts::exclude($connection->table('fin_accounts')); + $normalCount = $normalQuery->update([ 'daily_available' => $connection->raw('current_balance'), 'blocked_amount' => 0, 'updated_at' => now(), diff --git a/modules/EcommerceFinance/Services/FinanceLedgerPayableMappingBulk.php b/modules/EcommerceFinance/Services/FinanceLedgerPayableMappingBulk.php index e607681b..54e262db 100644 --- a/modules/EcommerceFinance/Services/FinanceLedgerPayableMappingBulk.php +++ b/modules/EcommerceFinance/Services/FinanceLedgerPayableMappingBulk.php @@ -3,6 +3,7 @@ namespace Modules\EcommerceFinance\Services; use App\Models\Auth\User; +use App\Utilities\FinanceLedgerIndependentAccounts; use Illuminate\Support\Facades\DB; /** @@ -17,7 +18,8 @@ class FinanceLedgerPayableMappingBulk { $connection = $this->connection(); $accountsByCompany = []; - foreach ($connection->table('fin_accounts')->get(['company_name', 'account_short', 'account_type']) as $account) { + foreach (FinanceLedgerIndependentAccounts::exclude($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 === '') { diff --git a/modules/EcommerceFinance/Services/FinanceLedgerShopDirectory.php b/modules/EcommerceFinance/Services/FinanceLedgerShopDirectory.php index 5a734bf8..85bd1cfb 100644 --- a/modules/EcommerceFinance/Services/FinanceLedgerShopDirectory.php +++ b/modules/EcommerceFinance/Services/FinanceLedgerShopDirectory.php @@ -2,6 +2,7 @@ namespace Modules\EcommerceFinance\Services; +use App\Utilities\FinanceLedgerIndependentAccounts; use Illuminate\Database\DatabaseManager; use Illuminate\Support\Facades\Schema; use Modules\EcommerceFinance\Repositories\BusinessInformationMirrorRepository; @@ -192,7 +193,7 @@ class FinanceLedgerShopDirectory return ['by_account_no' => [], 'by_short' => []]; } - $accounts = $connection->table('fin_accounts') + $accounts = FinanceLedgerIndependentAccounts::exclude($connection->table('fin_accounts')) ->get(['account_short', 'account_no', 'bank_name', 'account_type']) ->filter(function (object $account): bool { $label = (string) ($account->account_type ?? '') . ' ' . (string) ($account->bank_name ?? ''); diff --git a/modules/EcommerceFinance/Services/FinanceLedgerShopMappingBulk.php b/modules/EcommerceFinance/Services/FinanceLedgerShopMappingBulk.php index 7c2a0bbe..81a55ddd 100644 --- a/modules/EcommerceFinance/Services/FinanceLedgerShopMappingBulk.php +++ b/modules/EcommerceFinance/Services/FinanceLedgerShopMappingBulk.php @@ -3,6 +3,7 @@ namespace Modules\EcommerceFinance\Services; use App\Models\Auth\User; +use App\Utilities\FinanceLedgerIndependentAccounts; use Illuminate\Support\Facades\DB; /** @@ -22,7 +23,10 @@ class FinanceLedgerShopMappingBulk ->where('account_short', '<>', '/') ->pluck('account_short', 'shop_name'); $masterAccounts = array_fill_keys( - $connection->table('fin_accounts')->distinct()->pluck('account_short')->all(), + FinanceLedgerIndependentAccounts::exclude($connection->table('fin_accounts')) + ->distinct() + ->pluck('account_short') + ->all(), true, ); $rows = $connection->table('fin_account_map') diff --git a/modules/EcommerceFinance/Services/FinanceLedgerSnapshotImporter.php b/modules/EcommerceFinance/Services/FinanceLedgerSnapshotImporter.php index 9493935b..119d18d7 100644 --- a/modules/EcommerceFinance/Services/FinanceLedgerSnapshotImporter.php +++ b/modules/EcommerceFinance/Services/FinanceLedgerSnapshotImporter.php @@ -2,6 +2,7 @@ namespace Modules\EcommerceFinance\Services; +use App\Utilities\FinanceLedgerIndependentAccounts; use Illuminate\Support\Facades\DB; use RuntimeException; @@ -102,6 +103,7 @@ class FinanceLedgerSnapshotImporter $connection->transaction(function () use ($connection, $rows, $now): void { $connection->table('fin_transactions') ->where('source', '<>', FinanceLedgerTransactionWriter::MANUAL_SOURCE) + ->whereNotIn('account_short', FinanceLedgerIndependentAccounts::names()) ->delete(); foreach (array_chunk($rows, 500) as $chunk) { $connection->table('fin_transactions')->insert($chunk); diff --git a/modules/EcommerceFinance/Services/FinanceLedgerTransactionWriter.php b/modules/EcommerceFinance/Services/FinanceLedgerTransactionWriter.php index b89e9729..493beb53 100644 --- a/modules/EcommerceFinance/Services/FinanceLedgerTransactionWriter.php +++ b/modules/EcommerceFinance/Services/FinanceLedgerTransactionWriter.php @@ -2,6 +2,7 @@ namespace Modules\EcommerceFinance\Services; +use App\Utilities\FinanceLedgerIndependentAccounts; use Illuminate\Support\Facades\DB; use RuntimeException; @@ -101,6 +102,7 @@ class FinanceLedgerTransactionWriter if ($replaceExisting) { $connection->table('fin_transactions') ->whereNotIn('source', [self::MANUAL_SOURCE, self::SHOP_WITHDRAWAL_SOURCE]) + ->whereNotIn('account_short', FinanceLedgerIndependentAccounts::names()) ->delete(); } foreach (array_chunk($rows, 500) as $chunk) { diff --git a/modules/EcommerceFinance/View/FinanceLedgerPageData.php b/modules/EcommerceFinance/View/FinanceLedgerPageData.php index 7d76e117..d1ba8dbb 100644 --- a/modules/EcommerceFinance/View/FinanceLedgerPageData.php +++ b/modules/EcommerceFinance/View/FinanceLedgerPageData.php @@ -8,6 +8,7 @@ use App\Utilities\FinanceLedgerAccountStatus; use App\Utilities\FinanceLedgerAccountShouldKeep; use App\Utilities\FinanceLedgerAccountType; use App\Utilities\FinanceLedgerAccess; +use App\Utilities\FinanceLedgerIndependentAccounts; use Carbon\CarbonImmutable; use Illuminate\Database\Query\Builder; use Illuminate\Pagination\LengthAwarePaginator; @@ -37,9 +38,9 @@ class FinanceLedgerPageData ]; public const TRANSACTION_VIEWS = [ - 'all' => ['title' => '全部明细', 'account_short' => null], - 'zhu-junxiang-alipay' => ['title' => '朱俊祥支付宝', 'account_short' => '朱俊祥支付宝'], - 'zhu-junxiang-wechat' => ['title' => '朱俊祥微信', 'account_short' => '朱俊祥微信'], + 'all' => ['title' => '全部明细', 'account_short' => null, 'independent' => false], + 'zhu-junxiang-alipay' => ['title' => '朱俊祥支付宝', 'account_short' => '朱俊祥支付宝', 'independent' => true], + 'zhu-junxiang-wechat' => ['title' => '朱俊祥微信', 'account_short' => '朱俊祥微信', 'independent' => true], ]; private const ACCOUNT_FILTER_EMPTY = '__empty__'; @@ -66,7 +67,9 @@ class FinanceLedgerPageData $availableBreakdown = $this->availableBreakdown($shops); $flows = $this->flows($flowPeriod); $transactions = $this->transactions($transactionView['account_short'], $transactionPeriod); - $transactionViewAccountExists = $transactionView['account_short'] === null + $transactionViewIsIndependent = (bool) ($transactionView['independent'] ?? false); + $transactionViewAccountExists = $transactionViewIsIndependent + || $transactionView['account_short'] === null || $this->db()->table('fin_accounts') ->where('account_short', $transactionView['account_short']) ->exists(); @@ -93,6 +96,7 @@ class FinanceLedgerPageData 'ledgerTransactionViews' => self::TRANSACTION_VIEWS, 'ledgerTransactionViewAccount' => $transactionView['account_short'], 'ledgerTransactionViewAccountExists' => $transactionViewAccountExists, + 'ledgerTransactionViewIsIndependent' => $transactionViewIsIndependent, 'ledgerMapping' => $this->mapping($mappingPeriod), 'ledgerMappingPeriod' => $mappingPeriod, 'ledgerSourceSummary' => $this->sourceSummary($sourcePeriod), @@ -116,7 +120,10 @@ class FinanceLedgerPageData private function accounts(): array { - $query = $this->db()->table('fin_accounts as a'); + $query = FinanceLedgerIndependentAccounts::exclude( + $this->db()->table('fin_accounts as a'), + 'a.account_short', + ); $keyword = trim((string) request('accounts_q', '')); if ($keyword !== '') { $query->where(function (Builder $builder) use ($keyword): void { @@ -534,7 +541,8 @@ class FinanceLedgerPageData */ private function availableBreakdown(array $shops): array { - $bankAvailable = round((float) $this->db()->table('fin_accounts')->sum('daily_available'), 2); + $bankAvailableQuery = FinanceLedgerIndependentAccounts::exclude($this->db()->table('fin_accounts')); + $bankAvailable = round((float) $bankAvailableQuery->sum('daily_available'), 2); $shopAvailable = round((float) ($shops['overall_summary']['balance'] ?? 0), 2); return [ @@ -547,7 +555,7 @@ class FinanceLedgerPageData private function flows(array $period): array { - $query = $this->db()->table('fin_daily_flows'); + $query = FinanceLedgerIndependentAccounts::exclude($this->db()->table('fin_daily_flows')); $this->applyDateRange($query, 'flow_date', $period); foreach (['flows_company' => 'company_name', 'flows_account' => 'account_short'] as $param => $column) { $value = trim((string) request($param, '')); @@ -629,6 +637,7 @@ class FinanceLedgerPageData if ($forcedAccountShort !== null) { $query->whereRaw($effectiveAccount . ' = ?', [$forcedAccountShort]); } else { + $this->excludeIndependentEffectiveAccount($query); $account = trim((string) request('txns_account', '')); if ($account !== '') { $query->whereRaw($effectiveAccount . ' LIKE ?', ['%' . $account . '%']); @@ -712,12 +721,18 @@ class FinanceLedgerPageData private function transactionSummaryMetrics(Builder $query, ?string $accountShort, array $period): array { + $isIndependent = FinanceLedgerIndependentAccounts::contains($accountShort); + return [ 'count' => (clone $query)->count(), - 'previous_closing' => $this->transactionPreviousMonthClosing($accountShort, $period), + 'previous_closing' => $isIndependent + ? $this->independentTransactionOpening((string) $accountShort, $period) + : $this->transactionPreviousMonthClosing($accountShort, $period), 'income' => round((float) (clone $query)->sum('t.income'), 2), 'expense' => round((float) (clone $query)->sum('t.expense'), 2), - 'current_balance' => $this->transactionCurrentBalance($accountShort), + 'current_balance' => $isIndependent + ? $this->independentTransactionCurrentBalance((string) $accountShort) + : $this->transactionCurrentBalance($accountShort), ]; } @@ -727,6 +742,7 @@ class FinanceLedgerPageData if ($accountShort !== null) { $query->where('account_short', $accountShort); } else { + FinanceLedgerIndependentAccounts::exclude($query); $account = trim((string) request('txns_account', '')); if ($account !== '') { $query->where('account_short', 'like', '%' . $account . '%'); @@ -790,20 +806,83 @@ class FinanceLedgerPageData return; } + FinanceLedgerIndependentAccounts::exclude($query, 'f.account_short'); $account = trim((string) request('txns_account', '')); if ($account !== '') { $query->where('f.account_short', 'like', '%' . $account . '%'); } } + public function globalTransactionCount(): int + { + $query = $this->transactionScopeQuery(); + $this->excludeIndependentEffectiveAccount($query); + + return (clone $query)->count(); + } + + private function independentTransactionOpening(string $accountShort, array $period): ?float + { + $historyQuery = $this->transactionScopeQuery(); + $effectiveAccount = $this->transactionHeaderColumns()['effective_account_short']['sort']; + $historyQuery->whereRaw($effectiveAccount . ' = ?', [$accountShort]) + ->whereNotNull('t.balance'); + + $periodQuery = clone $historyQuery; + $this->applyDateRange($periodQuery, 't.txn_date', $period); + $first = $periodQuery + ->orderBy('t.txn_date') + ->orderBy('t.id') + ->first(['t.txn_date', 't.income', 't.expense', 't.balance']); + if ($first !== null) { + return round( + (float) $first->balance - (float) $first->income + (float) $first->expense, + 2, + ); + } + + $from = $period['from'] ?? null; + if ($from === null) { + return null; + } + + $prior = (clone $historyQuery) + ->where('t.txn_date', '<', $from) + ->orderByDesc('t.txn_date') + ->orderByDesc('t.id') + ->value('t.balance'); + + return $prior === null ? null : round((float) $prior, 2); + } + + private function independentTransactionCurrentBalance(string $accountShort): ?float + { + $effectiveAccount = $this->transactionHeaderColumns()['effective_account_short']['sort']; + $balance = $this->transactionScopeQuery() + ->whereRaw($effectiveAccount . ' = ?', [$accountShort]) + ->whereNotNull('t.balance') + ->orderByDesc('t.txn_date') + ->orderByDesc('t.id') + ->value('t.balance'); + + return $balance === null ? null : round((float) $balance, 2); + } + + private function excludeIndependentEffectiveAccount(Builder $query): void + { + $effectiveAccount = $this->transactionHeaderColumns()['effective_account_short']['sort']; + $names = FinanceLedgerIndependentAccounts::names(); + $query->whereRaw( + $effectiveAccount . ' NOT IN (' . implode(', ', array_fill(0, count($names), '?')) . ')', + $names, + ); + } + private function transactionQuery(): Builder { $effectiveAccount = $this->transactionHeaderColumns()['effective_account_short']['sort']; - return $this->db()->table('fin_transactions as t') - ->leftJoin('fin_account_map as m', function ($join): void { - $join->on('m.source', '=', 't.source')->on('m.source_value', '=', 't.source_value'); - }) + return $this->transactionScopeQuery() ->select([ 't.id', 't.txn_date', @@ -826,15 +905,21 @@ class FinanceLedgerPageData ->selectRaw($effectiveAccount . ' AS effective_account_short'); } + private function transactionScopeQuery(): Builder + { + return $this->db()->table('fin_transactions as t') + ->leftJoin('fin_account_map as m', function ($join): void { + $join->on('m.source', '=', 't.source')->on('m.source_value', '=', 't.source_value'); + }); + } + private function transactionAccountHeaderOptions(): array { $effectiveAccount = $this->transactionHeaderColumns()['effective_account_short']['sort']; + $query = $this->transactionScopeQuery(); + $this->excludeIndependentEffectiveAccount($query); - return $this->db()->table('fin_transactions as t') - ->leftJoin('fin_account_map as m', function ($join): void { - $join->on('m.source', '=', 't.source') - ->on('m.source_value', '=', 't.source_value'); - }) + return $query ->selectRaw($effectiveAccount . ' AS filter_value') ->distinct() ->orderBy('filter_value') @@ -848,11 +933,14 @@ class FinanceLedgerPageData private function transactionDateHeaderOptions(): array { - return $this->db()->table('fin_transactions') - ->select('txn_date') + $query = $this->transactionScopeQuery(); + $this->excludeIndependentEffectiveAccount($query); + + return $query + ->select('t.txn_date') ->distinct() - ->orderByDesc('txn_date') - ->pluck('txn_date') + ->orderByDesc('t.txn_date') + ->pluck('t.txn_date') ->map(static fn (mixed $value): string => (string) $value) ->filter() ->unique() @@ -953,6 +1041,12 @@ class FinanceLedgerPageData ->selectRaw("SUM({$t}.income) AS income_sum") ->selectRaw("SUM({$t}.expense) AS expense_sum") ->groupBy('t.source', 't.source_value'); + $effectiveAccount = $this->transactionHeaderColumns()['effective_account_short']['sort']; + $names = FinanceLedgerIndependentAccounts::names(); + $query->whereRaw( + $effectiveAccount . ' NOT IN (' . implode(', ', array_fill(0, count($names), '?')) . ')', + $names, + ); $this->applyDateRange($query, 't.txn_date', $period); return $query; @@ -960,12 +1054,15 @@ class FinanceLedgerPageData private function sourceSummary(array $period): array { - $query = $this->db()->table('fin_transactions') - ->selectRaw('source, process_type, COUNT(*) AS rows_count, SUM(income) AS income, SUM(expense) AS expense, MIN(txn_date) AS first_date, MAX(txn_date) AS last_date') - ->groupBy('source', 'process_type') - ->orderBy('source') + $t = $this->queryAlias('t'); + $query = $this->transactionScopeQuery(); + $this->excludeIndependentEffectiveAccount($query); + $query + ->selectRaw("{$t}.source, {$t}.process_type, COUNT(*) AS rows_count, SUM({$t}.income) AS income, SUM({$t}.expense) AS expense, MIN({$t}.txn_date) AS first_date, MAX({$t}.txn_date) AS last_date") + ->groupBy('t.source', 't.process_type') + ->orderBy('t.source') ->orderByDesc('rows_count'); - $this->applyDateRange($query, 'txn_date', $period); + $this->applyDateRange($query, 't.txn_date', $period); $rows = $query ->get() @@ -982,24 +1079,28 @@ class FinanceLedgerPageData private function meta(array $transactionPeriod, array $flowPeriod): array { - $transactionQuery = $this->db()->table('fin_transactions'); - $this->applyDateRange($transactionQuery, 'txn_date', $transactionPeriod); - $flowQuery = $this->db()->table('fin_daily_flows'); + $transactionQuery = $this->transactionScopeQuery(); + $this->excludeIndependentEffectiveAccount($transactionQuery); + $this->applyDateRange($transactionQuery, 't.txn_date', $transactionPeriod); + $flowQuery = FinanceLedgerIndependentAccounts::exclude($this->db()->table('fin_daily_flows')); $this->applyDateRange($flowQuery, 'flow_date', $flowPeriod); + $accountQuery = FinanceLedgerIndependentAccounts::exclude($this->db()->table('fin_accounts')); + $openingQuery = FinanceLedgerIndependentAccounts::exclude($this->db()->table('fin_account_opening')); + $mappingQuery = FinanceLedgerIndependentAccounts::exclude($this->db()->table('fin_account_map')); return [ - 'accounts' => $this->db()->table('fin_accounts')->count(), + 'accounts' => (clone $accountQuery)->count(), 'shops' => $this->db()->table('fin_shops')->count(), 'companies' => $this->db()->table('fin_companies')->count(), - 'opening_rows' => $this->db()->table('fin_account_opening')->count(), - 'opening_sum' => round((float) $this->db()->table('fin_account_opening')->sum('opening_balance'), 2), + 'opening_rows' => (clone $openingQuery)->count(), + 'opening_sum' => round((float) (clone $openingQuery)->sum('opening_balance'), 2), 'transactions' => (clone $transactionQuery)->count(), 'income' => round((float) (clone $transactionQuery)->sum('income'), 2), 'expense' => round((float) (clone $transactionQuery)->sum('expense'), 2), 'flows' => (clone $flowQuery)->count(), - 'mapping_total' => $this->db()->table('fin_account_map')->count(), - 'mapping_done' => $this->db()->table('fin_account_map')->whereNotNull('account_short')->where('account_short', '<>', '')->count(), - 'last_synced_at' => $this->db()->table('fin_transactions')->max('synced_at'), + 'mapping_total' => (clone $mappingQuery)->count(), + 'mapping_done' => (clone $mappingQuery)->whereNotNull('account_short')->where('account_short', '<>', '')->count(), + 'last_synced_at' => (clone $transactionQuery)->max('t.synced_at'), ]; } @@ -1019,19 +1120,27 @@ class FinanceLedgerPageData private function accountOptions(): array { - return $this->db()->table('fin_accounts') + return FinanceLedgerIndependentAccounts::exclude($this->db()->table('fin_accounts')) ->select('account_short')->distinct()->orderBy('account_short')->pluck('account_short')->all(); } private function manualTransactionAccountOptions(): array { - return $this->db()->table('fin_accounts') + $view = $this->transactionView(); + $query = $this->db()->table('fin_accounts') ->select(['account_short', 'company_name']) ->whereNotNull('account_short') ->where('account_short', '<>', '') ->orderBy('account_short') ->orderBy('company_name') - ->orderBy('id') + ->orderBy('id'); + if (FinanceLedgerIndependentAccounts::contains($view['account_short'])) { + $query->where('account_short', $view['account_short']); + } else { + FinanceLedgerIndependentAccounts::exclude($query); + } + + return $query ->get() ->groupBy('account_short') ->map(static function ($rows, string $accountShort): array { @@ -1052,16 +1161,17 @@ class FinanceLedgerPageData private function accountFilterOptions(): array { - $baseTypes = $this->db()->table('fin_accounts') + $baseTypes = FinanceLedgerIndependentAccounts::exclude($this->db()->table('fin_accounts')) ->whereNotNull('account_type')->where('account_type', '<>', '') ->distinct()->orderBy('account_type')->pluck('account_type')->all(); $overrideTypes = $this->db()->table('fin_account_type_overrides') + ->whereNotIn('account_short', FinanceLedgerIndependentAccounts::names()) ->whereNotNull('account_type')->where('account_type', '<>', '') ->distinct()->orderBy('account_type')->pluck('account_type')->all(); $types = FinanceLedgerAccountType::options($baseTypes, $overrideTypes); return [ - 'regions' => $this->db()->table('fin_accounts') + 'regions' => FinanceLedgerIndependentAccounts::exclude($this->db()->table('fin_accounts')) ->whereNotNull('region')->where('region', '<>', '') ->distinct()->orderBy('region')->pluck('region')->all(), 'types' => $types, @@ -1070,7 +1180,7 @@ class FinanceLedgerPageData private function accountPlanOptions(): array { - return $this->db()->table('fin_accounts') + return FinanceLedgerIndependentAccounts::exclude($this->db()->table('fin_accounts')) ->whereNotNull('company_plan') ->where('company_plan', '<>', '') ->distinct() @@ -1089,7 +1199,7 @@ class FinanceLedgerPageData ->whereNotNull('name') ->pluck('name') ->merge( - $this->db()->table('fin_accounts') + FinanceLedgerIndependentAccounts::exclude($this->db()->table('fin_accounts')) ->whereNotNull('company_name') ->distinct() ->pluck('company_name'), diff --git a/modules/EcommerceFinance/View/ShopFlowQuery.php b/modules/EcommerceFinance/View/ShopFlowQuery.php index e7654114..a10d1912 100644 --- a/modules/EcommerceFinance/View/ShopFlowQuery.php +++ b/modules/EcommerceFinance/View/ShopFlowQuery.php @@ -2,6 +2,7 @@ namespace Modules\EcommerceFinance\View; +use App\Utilities\FinanceLedgerIndependentAccounts; use Illuminate\Http\Request; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Facades\DB; @@ -338,7 +339,7 @@ class ShopFlowQuery ->pluck('company_name'), ) ->merge( - $this->db()->table('fin_accounts') + FinanceLedgerIndependentAccounts::exclude($this->db()->table('fin_accounts')) ->whereNotNull('company_name') ->where('company_name', '<>', '') ->distinct() @@ -386,7 +387,7 @@ class ShopFlowQuery private function accountRows(): array { - return $this->db()->table('fin_accounts') + return FinanceLedgerIndependentAccounts::exclude($this->db()->table('fin_accounts')) ->orderBy('company_name') ->orderBy('account_short') ->orderBy('id') @@ -469,7 +470,7 @@ class ShopFlowQuery private function transactionQuery(string $accountShort, string $start, string $end) { // 钉钉付款的原始 account_short 常为空,按账户映射解析;无映射时保留原值兜底。 - return $this->db()->table('fin_transactions as t') + $query = $this->db()->table('fin_transactions as t') ->leftJoin('fin_account_map as m', function ($join): void { $join->on('m.source', '=', 't.source') ->on('m.source_value', '=', 't.source_value'); @@ -492,6 +493,15 @@ class ShopFlowQuery $expense->where('t.source', 'dingtalk')->where('t.expense', '>', 0); }); }); + + $prefix = $this->db()->getTablePrefix(); + $effectiveAccount = "COALESCE(NULLIF({$prefix}m.account_short, ''), NULLIF({$prefix}t.account_short, ''), '未匹配')"; + $names = FinanceLedgerIndependentAccounts::names(); + + return $query->whereRaw( + $effectiveAccount . ' NOT IN (' . implode(', ', array_fill(0, count($names), '?')) . ')', + $names, + ); } private function transactionColumns(): array diff --git a/resources/views/business/pages/financial-ledger/ledger.blade.php b/resources/views/business/pages/financial-ledger/ledger.blade.php index a68b2186..4cf1f57b 100644 --- a/resources/views/business/pages/financial-ledger/ledger.blade.php +++ b/resources/views/business/pages/financial-ledger/ledger.blade.php @@ -66,6 +66,7 @@ $transactionViews = $ledgerTransactionViews ?? []; $transactionViewAccount = $ledgerTransactionViewAccount ?? null; $transactionViewAccountExists = (bool) ($ledgerTransactionViewAccountExists ?? true); + $transactionViewIsIndependent = (bool) ($ledgerTransactionViewIsIndependent ?? false); $currentMonthStart = now()->startOfMonth()->toDateString(); $currentMonthEnd = now()->endOfMonth()->toDateString(); $flowPeriod = $ledgerFlowPeriod ?? ['from' => $currentMonthStart, 'to' => $currentMonthEnd]; @@ -844,7 +845,7 @@