diff --git a/app/Http/Controllers/Business/FinanceLedger.php b/app/Http/Controllers/Business/FinanceLedger.php
index 0c877320..a328b41f 100644
--- a/app/Http/Controllers/Business/FinanceLedger.php
+++ b/app/Http/Controllers/Business/FinanceLedger.php
@@ -3,6 +3,7 @@
namespace App\Http\Controllers\Business;
use App\Utilities\FinanceLedgerAccountStatus;
+use App\Utilities\FinanceLedgerAccountShouldKeep;
use App\Utilities\FinanceLedgerAccountType;
use App\Utilities\FinanceLedgerAccess;
use Illuminate\Http\JsonResponse;
@@ -176,6 +177,84 @@ class FinanceLedger extends Controller
], 200, ['Cache-Control' => 'private, no-store']);
}
+ public function updateAccountPlan(Request $request): JsonResponse
+ {
+ abort_unless(FinanceLedgerAccess::canEditAccountStatus(user()), 403, '只有财务台账管理员可以修改公司规划。');
+
+ $data = $request->validate([
+ 'account_short' => ['required', 'string', 'max:100'],
+ 'company_plan' => ['nullable', 'string', 'max:50'],
+ ]);
+ $accountShort = trim((string) $data['account_short']);
+ $plan = trim((string) ($data['company_plan'] ?? ''));
+ if ($accountShort === '') {
+ abort(422, '账户简称不能为空。');
+ }
+
+ $connection = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
+ abort_unless(
+ $connection->table('fin_accounts')->where('account_short', $accountShort)->exists(),
+ 422,
+ '账户简称不存在,请刷新账户总表后重试。',
+ );
+ $allowedPlans = $connection->table('fin_accounts')
+ ->whereNotNull('company_plan')
+ ->where('company_plan', '<>', '')
+ ->distinct()
+ ->pluck('company_plan')
+ ->map(static fn (mixed $value): string => trim((string) $value))
+ ->filter()
+ ->values()
+ ->all();
+ abort_unless($plan === '' || in_array($plan, $allowedPlans, true), 422, '请选择现有公司规划。');
+
+ $connection->table('fin_accounts')->where('account_short', $accountShort)->update([
+ 'company_plan' => $plan !== '' ? $plan : null,
+ 'updated_at' => now(),
+ ]);
+
+ return response()->json([
+ 'ok' => true,
+ 'account_short' => $accountShort,
+ 'company_plan' => $plan,
+ 'label' => $plan !== '' ? $plan : '未填写',
+ ], 200, ['Cache-Control' => 'private, no-store']);
+ }
+
+ public function updateAccountKeep(Request $request): JsonResponse
+ {
+ abort_unless(FinanceLedgerAccess::canEditAccountStatus(user()), 403, '只有财务台账管理员可以修改账户是否保留。');
+
+ $data = $request->validate([
+ 'account_short' => ['required', 'string', 'max:100'],
+ 'account_should_keep' => ['nullable', 'string', Rule::in(FinanceLedgerAccountShouldKeep::OPTIONS)],
+ ]);
+ $accountShort = trim((string) $data['account_short']);
+ $keep = FinanceLedgerAccountShouldKeep::normalize((string) ($data['account_should_keep'] ?? ''));
+ if ($accountShort === '') {
+ abort(422, '账户简称不能为空。');
+ }
+
+ $connection = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
+ abort_unless(
+ $connection->table('fin_accounts')->where('account_short', $accountShort)->exists(),
+ 422,
+ '账户简称不存在,请刷新账户总表后重试。',
+ );
+
+ $connection->table('fin_accounts')->where('account_short', $accountShort)->update([
+ 'account_should_keep' => $keep !== '' ? $keep : null,
+ 'updated_at' => now(),
+ ]);
+
+ return response()->json([
+ 'ok' => true,
+ 'account_short' => $accountShort,
+ 'account_should_keep' => $keep,
+ 'label' => $keep !== '' ? $keep : '未填写',
+ ], 200, ['Cache-Control' => 'private, no-store']);
+ }
+
public function previewShopMapping(FinanceLedgerShopMappingBulk $bulk): JsonResponse
{
abort_unless(FinanceLedgerAccess::canEditMapping(user()), 403, '只有财务台账管理员可以批量修改账户映射。');
diff --git a/modules/EcommerceFinance/View/FinanceLedgerPageData.php b/modules/EcommerceFinance/View/FinanceLedgerPageData.php
index 1d5a9951..f76310ef 100644
--- a/modules/EcommerceFinance/View/FinanceLedgerPageData.php
+++ b/modules/EcommerceFinance/View/FinanceLedgerPageData.php
@@ -59,6 +59,8 @@ class FinanceLedgerPageData
'ledgerAccountOptions' => $this->accountOptions(),
'ledgerAccountFilterOptions' => $this->accountFilterOptions(),
'ledgerAccountStatusOptions' => FinanceLedgerAccountStatus::OPTIONS,
+ 'ledgerAccountPlanOptions' => $this->accountPlanOptions(),
+ 'ledgerAccountKeepOptions' => FinanceLedgerAccountShouldKeep::OPTIONS,
'ledgerCanEditMapping' => FinanceLedgerAccess::canEditMapping(user()),
'ledgerCanEditAccountStatus' => FinanceLedgerAccess::canEditAccountStatus(user()),
'ledgerCanEditAccountType' => FinanceLedgerAccess::canEditAccountType(user()),
@@ -376,6 +378,21 @@ class FinanceLedgerPageData
];
}
+ private function accountPlanOptions(): array
+ {
+ return $this->db()->table('fin_accounts')
+ ->whereNotNull('company_plan')
+ ->where('company_plan', '<>', '')
+ ->distinct()
+ ->orderBy('company_plan')
+ ->pluck('company_plan')
+ ->map(static fn (mixed $value): string => trim((string) $value))
+ ->filter()
+ ->unique()
+ ->values()
+ ->all();
+ }
+
private function db()
{
return DB::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
diff --git a/resources/views/business/pages/financial-ledger/ledger.blade.php b/resources/views/business/pages/financial-ledger/ledger.blade.php
index df2880b3..ced7755e 100644
--- a/resources/views/business/pages/financial-ledger/ledger.blade.php
+++ b/resources/views/business/pages/financial-ledger/ledger.blade.php
@@ -9,6 +9,8 @@
$accountStatusOptions = $ledgerAccountStatusOptions ?? \App\Utilities\FinanceLedgerAccountStatus::OPTIONS;
$accountFilterOptions = $ledgerAccountFilterOptions ?? ['regions' => [], 'types' => []];
$accountTypeOptions = $accountFilterOptions['types'] ?? [];
+ $accountPlanOptions = $ledgerAccountPlanOptions ?? [];
+ $accountKeepOptions = $ledgerAccountKeepOptions ?? \App\Utilities\FinanceLedgerAccountShouldKeep::OPTIONS;
$activeAccountFilterCount = count(array_filter([
request('accounts_q'),
request('accounts_short'),
@@ -133,6 +135,8 @@
.ledger-inline-select { display:block; height:1.75rem; margin:0 auto; padding:0 .1rem; border:1px solid var(--vip-border, #D8D2C7); border-radius:.4rem; background:#fff; color:var(--vip-content-ink, #171717); font:inherit; font-size:.68rem; font-weight:600; text-align:center; text-align-last:center; cursor:pointer; }
.ledger-status-select { width:4.15rem; min-width:4.15rem; }
.ledger-type-select { width:5.2rem; min-width:5.2rem; }
+ .ledger-plan-select { width:6.5rem; min-width:6.5rem; }
+ .ledger-keep-select { width:4.8rem; min-width:4.8rem; }
.ledger-inline-select:focus { border-color:var(--vip-content-accent-dark, #856632); outline:none; box-shadow:0 0 0 3px rgba(133,102,50,.16); }
.ledger-inline-select[data-status="normal"] { border-color:rgba(83,121,62,.38); background:#f3f7ee; color:var(--vip-success, #53793E); }
.ledger-inline-select[data-status="online_banking_abnormal"] { border-color:rgba(184,0,0,.34); background:#fff5f5; color:var(--vip-danger, #B80000); }
@@ -247,7 +251,29 @@
@endif
{{ $row->region }} | {{ $row->account_no }} | {{ $row->bank_name }} | {{ $row->bank_city_branch }} |
- {{ $row->company_plan }} | {{ $row->account_should_keep }} | {{ $row->account_last4 }} | {{ $row->single_limit }} | {{ $row->daily_limit }} | {{ $row->monthly_limit }} | {{ $row->yearly_limit }} | {{ $money($row->current_balance) }} | {{ $money($row->blocked_amount) }} | {{ $money($row->daily_available) }} | {{ $row->clearing_days !== null ? number_format((float) $row->clearing_days, 2) : '' }} | {{ $row->raise_target }} | {{ $row->raise_status }} | {{ $row->raise_actions }} | {{ $row->done_actions }} | {{ $row->next_action }} | {{ $row->remark }} | {{ $row->owner }} |
+
+ {{ $row->company_plan ?: '未填写' }}
+ @if ($canEditAccountStatus)
+
+ @endif
+ |
+
+ {{ $row->account_should_keep ?: '未填写' }}
+ @if ($canEditAccountStatus)
+
+ @endif
+ |
+ {{ $row->account_last4 }} | {{ $row->single_limit }} | {{ $row->daily_limit }} | {{ $row->monthly_limit }} | {{ $row->yearly_limit }} | {{ $money($row->current_balance) }} | {{ $money($row->blocked_amount) }} | {{ $money($row->daily_available) }} | {{ $row->clearing_days !== null ? number_format((float) $row->clearing_days, 2) : '' }} | {{ $row->raise_target }} | {{ $row->raise_status }} | {{ $row->raise_actions }} | {{ $row->done_actions }} | {{ $row->next_action }} | {{ $row->remark }} | {{ $row->owner }} |
@empty
没有匹配的账户数据。 |
@@ -787,6 +813,9 @@
display.textContent = payload.label || (value || '未填写');
if (field === 'status') display.dataset.status = value;
}
+ if (field === 'account_should_keep') {
+ item.closest('td')?.setAttribute('data-account-should-keep', value);
+ }
});
notify(successLabel + ':' + (payload.label || ''));
} catch (error) {
@@ -802,6 +831,8 @@
};
bindInlineAccountSelect('[data-account-status]', 'status', '账户状态已保存');
bindInlineAccountSelect('[data-account-type]', 'account_type', '账户类型已保存');
+ bindInlineAccountSelect('[data-account-plan]', 'company_plan', '公司规划已保存');
+ bindInlineAccountSelect('[data-account-keep]', 'account_should_keep', '账户是否保留已保存');
applyAccountEditMode(accountEditToggle?.checked === true);
let accountPageRequest = null;
@@ -864,6 +895,8 @@
}
bindInlineAccountSelect('[data-account-status]', 'status', '账户状态已保存');
bindInlineAccountSelect('[data-account-type]', 'account_type', '账户类型已保存');
+ bindInlineAccountSelect('[data-account-plan]', 'company_plan', '公司规划已保存');
+ bindInlineAccountSelect('[data-account-keep]', 'account_should_keep', '账户是否保留已保存');
applyAccountEditMode(accountEditToggle?.checked === true);
applyAccountColumnLayout();
bindAccountPagination();
diff --git a/routes/admin.php b/routes/admin.php
index e21de198..34d6f88e 100644
--- a/routes/admin.php
+++ b/routes/admin.php
@@ -669,6 +669,14 @@ Route::post('workspace/financial-ledger/accounts/type', [\App\Http\Controllers\B
->middleware('business.page:financial-operations,ledger')
->name('business.financial-ledger.accounts.type');
+Route::post('workspace/financial-ledger/accounts/plan', [\App\Http\Controllers\Business\FinanceLedger::class, 'updateAccountPlan'])
+ ->middleware('business.page:financial-operations,ledger')
+ ->name('business.financial-ledger.accounts.plan');
+
+Route::post('workspace/financial-ledger/accounts/keep', [\App\Http\Controllers\Business\FinanceLedger::class, 'updateAccountKeep'])
+ ->middleware('business.page:financial-operations,ledger')
+ ->name('business.financial-ledger.accounts.keep');
+
Route::get('workspace/financial-ledger/accounts/template', [\App\Http\Controllers\Business\FinanceLedger::class, 'downloadAccountTemplate'])
->middleware('business.page:financial-operations,ledger')
->name('business.financial-ledger.accounts.template');
diff --git a/tests/Feature/Business/FinanceLedgerAccountStatusTest.php b/tests/Feature/Business/FinanceLedgerAccountStatusTest.php
index 3111d229..99959389 100644
--- a/tests/Feature/Business/FinanceLedgerAccountStatusTest.php
+++ b/tests/Feature/Business/FinanceLedgerAccountStatusTest.php
@@ -49,6 +49,20 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
]);
}
+ private function planUrl(): string
+ {
+ return route('business.financial-ledger.accounts.plan', [
+ 'company_id' => $this->company->id,
+ ]);
+ }
+
+ private function keepUrl(): string
+ {
+ return route('business.financial-ledger.accounts.keep', [
+ 'company_id' => $this->company->id,
+ ]);
+ }
+
public function test_account_table_renders_status_selector_with_four_options(): void
{
$this->loginAs()->get($this->pageUrl())
@@ -64,6 +78,10 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
->assertSee('name="accounts_region"', false)
->assertSee('name="accounts_type"', false)
->assertSee('data-account-type', false)
+ ->assertSee('data-account-plan', false)
+ ->assertSee('data-account-keep', false)
+ ->assertSee('data-account-plan-display', false)
+ ->assertSee('data-account-keep-display', false)
->assertSee('ledger-type-select', false)
->assertSee('data-mapping-bulk', false)
->assertSee('导出录入模板')
@@ -303,6 +321,41 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
->assertSee('value="normal" selected', false);
}
+ public function test_company_plan_and_account_keep_can_be_updated(): void
+ {
+ DB::connection('business_mirror')->table('fin_accounts')->insert([
+ 'company_name' => '规划选项公司',
+ 'account_short' => '规划选项账户',
+ 'company_plan' => '待注销',
+ ]);
+
+ $this->loginAs()->postJson($this->planUrl(), [
+ 'account_short' => '测试账户',
+ 'company_plan' => '待注销',
+ ])->assertOk()->assertJsonPath('label', '待注销');
+
+ $this->loginAs()->postJson($this->keepUrl(), [
+ 'account_short' => '测试账户',
+ 'account_should_keep' => '否',
+ ])->assertOk()->assertJsonPath('label', '否');
+
+ $this->assertDatabaseHas('fin_accounts', [
+ 'account_short' => '测试账户',
+ 'company_plan' => '待注销',
+ 'account_should_keep' => '否',
+ ], 'business_mirror');
+
+ $this->withExceptionHandling()->loginAs()->postJson($this->planUrl(), [
+ 'account_short' => '测试账户',
+ 'company_plan' => '不存在的规划',
+ ])->assertUnprocessable()->assertJsonPath('message', '请选择现有公司规划。');
+
+ $this->withExceptionHandling()->loginAs()->postJson($this->keepUrl(), [
+ 'account_short' => '测试账户',
+ 'account_should_keep' => '可能',
+ ])->assertUnprocessable()->assertJsonValidationErrors('account_should_keep');
+ }
+
public function test_invalid_status_or_account_is_rejected(): void
{
$this->withExceptionHandling()->loginAs()->postJson($this->statusUrl(), [
@@ -344,6 +397,14 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
'account_short' => '测试账户',
'status' => 'limited',
])->assertForbidden();
+ $this->withExceptionHandling()->loginAs($viewer)->postJson($this->planUrl(), [
+ 'account_short' => '测试账户',
+ 'company_plan' => '保留',
+ ])->assertForbidden();
+ $this->withExceptionHandling()->loginAs($viewer)->postJson($this->keepUrl(), [
+ 'account_short' => '测试账户',
+ 'account_should_keep' => '否',
+ ])->assertForbidden();
$this->assertDatabaseCount('fin_account_statuses', 0, 'business_mirror');
}