diff --git a/app/Utilities/SidebarPermissions.php b/app/Utilities/SidebarPermissions.php
index aaea8c18..7120b6e5 100644
--- a/app/Utilities/SidebarPermissions.php
+++ b/app/Utilities/SidebarPermissions.php
@@ -10,6 +10,8 @@ class SidebarPermissions
{
private const LEGACY_PAGE_KEYS = [
'refunds/small-payment-work-orders' => 'financial-operations/small-payment-work-orders',
+ 'financial-ledger' => 'financial-operations/ledger',
+ 'financial-ledger/ledger' => 'financial-operations/ledger',
];
private const FINAL_SECTION_KEYS = [
diff --git a/config/business_navigation.php b/config/business_navigation.php
index 018e4b30..402be577 100644
--- a/config/business_navigation.php
+++ b/config/business_navigation.php
@@ -881,6 +881,7 @@ $navigation = [
['slug' => 'bookkeeping-details', 'title' => '记账明细'],
['slug' => 'non-operating-expenses', 'title' => '营业外支出'],
['slug' => 'financial-analysis', 'title' => '财务经营分析表'],
+ ['slug' => 'ledger', 'title' => '财务台账'],
],
],
[
@@ -943,14 +944,6 @@ $navigation = [
['slug' => 'account-mapping', 'title' => '科目映射表'],
],
],
- [
- 'slug' => 'financial-ledger',
- 'title' => '财务台账',
- 'icon' => 'menu_book',
- 'pages' => [
- ['slug' => 'ledger', 'title' => '财务台账'],
- ],
- ],
[
'slug' => 'data-monitoring',
'title' => '数据监控',
diff --git a/docs/财务台账开发说明.md b/docs/财务台账开发说明.md
index eb46ab7e..0e6d6ed5 100644
--- a/docs/财务台账开发说明.md
+++ b/docs/财务台账开发说明.md
@@ -61,7 +61,7 @@ docker compose -f docker-compose.local.yml exec -u www-data app \
## 页面
-- 路由:`/workspace/financial-ledger/ledger`
+- 路由:`/workspace/financial-operations/ledger`(旧地址会自动跳转)
- 页面:账户总表、店铺台账、每日资金流水、动账明细、账户映射、数据来源
- 账户总表支持导出空白 Excel 录入模板,包含页面列、状态和账户类型选项及填写说明
- 写权限:账户映射、账户状态和账户类型修改仅系统管理员及配置的财务台账管理员角色;其他账号只读
diff --git a/resources/views/business/pages/financial-ledger/ledger.blade.php b/resources/views/business/pages/financial-ledger/ledger.blade.php
index ae08b65d..e1d42910 100644
--- a/resources/views/business/pages/financial-ledger/ledger.blade.php
+++ b/resources/views/business/pages/financial-ledger/ledger.blade.php
@@ -17,7 +17,7 @@
], static fn ($value): bool => trim((string) $value) !== ''));
$money = static fn ($value): string => $value === null || $value === '' ? '' : number_format((float) $value, 2);
$date = static fn ($value): string => $value ? \Illuminate\Support\Carbon::parse($value)->format('Y-m-d') : '';
- $ledgerPageUrl = route('business.pages.show', ['section' => 'financial-ledger', 'page' => 'ledger']);
+ $ledgerPageUrl = route('business.pages.show', ['section' => 'financial-operations', 'page' => 'ledger']);
$tabUrl = static fn (string $tab): string => $ledgerPageUrl . '?' . http_build_query(['tab' => $tab]);
$sourceLabels = ['dingtalk' => '钉钉审批', 'shop' => '店铺提现', 'payable' => '供应商应付', 'store' => '店铺开店', 'workorder' => '聚工单'];
@endphp
@@ -345,7 +345,7 @@
diff --git a/routes/admin.php b/routes/admin.php
index d3650580..e21de198 100644
--- a/routes/admin.php
+++ b/routes/admin.php
@@ -15,6 +15,7 @@ use App\Http\Controllers\Business\SupplierPayableActions;
use App\Http\Controllers\Business\PayablesFinalActions;
use App\Http\Controllers\Business\SupplierReconciliation as SupplierReconciliationActions;
use App\Http\Controllers\Business\WithdrawalManagement;
+use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use Modules\EcommerceFinance\Http\Controllers\AccountBills;
use Modules\EcommerceFinance\Http\Controllers\AlipayBills;
@@ -649,39 +650,60 @@ Route::patch('workspace/supplier-payables/supplier-management/free-shipping-manu
->middleware('business.page:supplier-payables,supplier-management')
->name('business.supplier-management.free-shipping.update');
+Route::post('workspace/financial-operations/ledger/mapping/{id}', [\App\Http\Controllers\Business\FinanceLedger::class, 'updateMapping'])
+ ->whereNumber('id')
+ ->middleware('business.page:financial-operations,ledger')
+ ->name('business.financial-operations.ledger.mapping.update');
+
+// Keep already-open ledger pages working while they post to the previous URL.
Route::post('workspace/financial-ledger/mapping/{id}', [\App\Http\Controllers\Business\FinanceLedger::class, 'updateMapping'])
->whereNumber('id')
- ->middleware('business.page:financial-ledger,ledger')
+ ->middleware('business.page:financial-operations,ledger')
->name('business.financial-ledger.mapping.update');
Route::post('workspace/financial-ledger/accounts/status', [\App\Http\Controllers\Business\FinanceLedger::class, 'updateAccountStatus'])
- ->middleware('business.page:financial-ledger,ledger')
+ ->middleware('business.page:financial-operations,ledger')
->name('business.financial-ledger.accounts.status');
Route::post('workspace/financial-ledger/accounts/type', [\App\Http\Controllers\Business\FinanceLedger::class, 'updateAccountType'])
- ->middleware('business.page:financial-ledger,ledger')
+ ->middleware('business.page:financial-operations,ledger')
->name('business.financial-ledger.accounts.type');
Route::get('workspace/financial-ledger/accounts/template', [\App\Http\Controllers\Business\FinanceLedger::class, 'downloadAccountTemplate'])
- ->middleware('business.page:financial-ledger,ledger')
+ ->middleware('business.page:financial-operations,ledger')
->name('business.financial-ledger.accounts.template');
Route::post('workspace/financial-ledger/mapping/shop/preview', [\App\Http\Controllers\Business\FinanceLedger::class, 'previewShopMapping'])
- ->middleware('business.page:financial-ledger,ledger')
+ ->middleware('business.page:financial-operations,ledger')
->name('business.financial-ledger.mapping.shop.preview');
Route::post('workspace/financial-ledger/mapping/shop/apply', [\App\Http\Controllers\Business\FinanceLedger::class, 'applyShopMapping'])
- ->middleware('business.page:financial-ledger,ledger')
+ ->middleware('business.page:financial-operations,ledger')
->name('business.financial-ledger.mapping.shop.apply');
Route::post('workspace/financial-ledger/mapping/payable/preview', [\App\Http\Controllers\Business\FinanceLedger::class, 'previewPayableMapping'])
- ->middleware('business.page:financial-ledger,ledger')
+ ->middleware('business.page:financial-operations,ledger')
->name('business.financial-ledger.mapping.payable.preview');
Route::post('workspace/financial-ledger/mapping/payable/apply', [\App\Http\Controllers\Business\FinanceLedger::class, 'applyPayableMapping'])
- ->middleware('business.page:financial-ledger,ledger')
+ ->middleware('business.page:financial-operations,ledger')
->name('business.financial-ledger.mapping.payable.apply');
+Route::get('workspace/financial-ledger/{page?}/{tab?}', function (Request $request, ?string $page = null, ?string $tab = null) {
+ abort_unless($page === null || $page === 'ledger', 404);
+
+ $query = $request->query();
+ if ($tab !== null) {
+ $query['tab'] = $tab;
+ }
+ $target = route('business.pages.show', [
+ 'section' => 'financial-operations',
+ 'page' => 'ledger',
+ ]);
+
+ return redirect()->to($query === [] ? $target : $target . '?' . http_build_query($query));
+})->name('business.financial-ledger.legacy');
+
Route::get('workspace/{section}/{page?}/{tab?}', 'Business\Pages@show')
->name('business.pages.show');
diff --git a/tests/Feature/Business/FinanceLedgerAccountStatusTest.php b/tests/Feature/Business/FinanceLedgerAccountStatusTest.php
index 085aa02f..7de58a29 100644
--- a/tests/Feature/Business/FinanceLedgerAccountStatusTest.php
+++ b/tests/Feature/Business/FinanceLedgerAccountStatusTest.php
@@ -28,7 +28,7 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
{
return route('business.pages.show', [
'company_id' => $this->company->id,
- 'section' => 'financial-ledger',
+ 'section' => 'financial-operations',
'page' => 'ledger',
]);
}
@@ -110,7 +110,7 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
$this->loginAs()->get(route('business.pages.show', [
'company_id' => $this->company->id,
- 'section' => 'financial-ledger',
+ 'section' => 'financial-operations',
'page' => 'ledger',
'accounts_short' => 'page-test',
]))
@@ -146,7 +146,7 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
$this->loginAs()->get(route('business.pages.show', [
'company_id' => $this->company->id,
- 'section' => 'financial-ledger',
+ 'section' => 'financial-operations',
'page' => 'ledger',
'accounts_short' => '筛选账户',
'accounts_status' => 'limited',
@@ -162,7 +162,7 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
$this->loginAs()->get(route('business.pages.show', [
'company_id' => $this->company->id,
- 'section' => 'financial-ledger',
+ 'section' => 'financial-operations',
'page' => 'ledger',
'accounts_short' => '筛选账户',
'accounts_status' => 'normal',
@@ -173,7 +173,7 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
$this->loginAs()->get(route('business.pages.show', [
'company_id' => $this->company->id,
- 'section' => 'financial-ledger',
+ 'section' => 'financial-operations',
'page' => 'ledger',
'accounts_short' => '测试账户',
'accounts_status' => 'normal',
@@ -227,7 +227,7 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
$this->loginAs()->get(route('business.pages.show', [
'company_id' => $this->company->id,
- 'section' => 'financial-ledger',
+ 'section' => 'financial-operations',
'page' => 'ledger',
'accounts_type' => '个人卡',
]))
@@ -239,7 +239,7 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase
$this->loginAs()->get(route('business.pages.show', [
'company_id' => $this->company->id,
- 'section' => 'financial-ledger',
+ 'section' => 'financial-operations',
'page' => 'ledger',
'accounts_type' => '公户',
]))
diff --git a/tests/Feature/Business/FinanceLedgerNavigationTest.php b/tests/Feature/Business/FinanceLedgerNavigationTest.php
new file mode 100644
index 00000000..f396fc1f
--- /dev/null
+++ b/tests/Feature/Business/FinanceLedgerNavigationTest.php
@@ -0,0 +1,122 @@
+keyBy('slug');
+ $financialOperations = $navigation->get('financial-operations');
+
+ $this->assertNotNull($financialOperations);
+ $this->assertSame('ledger', collect($financialOperations['pages'])->last()['slug']);
+ $this->assertFalse($navigation->has('financial-ledger'));
+ }
+
+ public function test_ledger_page_and_legacy_link_open_under_financial_operations(): void
+ {
+ $this->loginAs();
+
+ $target = route('business.pages.show', [
+ 'section' => 'financial-operations',
+ 'page' => 'ledger',
+ ]);
+
+ $this->get($target . '?tab=flows')
+ ->assertOk()
+ ->assertSeeText('财务经营报表')
+ ->assertSeeText('财务台账')
+ ->assertSee('data-ledger-page', false)
+ ->assertSee('data-ledger-tab="flows"', false)
+ ->assertSee($target . '?tab=accounts', false);
+
+ $legacy = route('business.pages.show', [
+ 'section' => 'financial-ledger',
+ 'page' => 'ledger',
+ ]);
+
+ $this->get($legacy . '?tab=flows')->assertRedirect($target . '?tab=flows');
+ $this->get($legacy . '/flows')->assertRedirect($target . '?tab=flows');
+ $this->get(route('business.financial-ledger.accounts.template'))->assertOk();
+ }
+
+ public function test_ledger_actions_use_financial_operations_permission(): void
+ {
+ foreach ([
+ 'business.financial-operations.ledger.mapping.update',
+ 'business.financial-ledger.mapping.update',
+ 'business.financial-ledger.accounts.status',
+ 'business.financial-ledger.accounts.type',
+ 'business.financial-ledger.accounts.template',
+ 'business.financial-ledger.mapping.shop.preview',
+ 'business.financial-ledger.mapping.shop.apply',
+ 'business.financial-ledger.mapping.payable.preview',
+ 'business.financial-ledger.mapping.payable.apply',
+ ] as $routeName) {
+ $route = app('router')->getRoutes()->getByName($routeName);
+
+ $this->assertNotNull($route, $routeName);
+ $this->assertContains(
+ 'business.page:financial-operations,ledger',
+ $route->gatherMiddleware(),
+ $routeName
+ );
+ }
+ }
+
+ public function test_legacy_ledger_permissions_follow_the_page(): void
+ {
+ $regular = $this->regularAccount();
+ $subject = 'user_' . $regular->id;
+
+ foreach (['financial-ledger', 'financial-ledger/ledger'] as $legacyKey) {
+ setting()->set('system.sidebar_permissions', [
+ 'configured' => [$subject],
+ 'subjects' => [$subject => [$legacyKey]],
+ ]);
+
+ $this->assertSame(
+ ['financial-operations/ledger'],
+ SidebarPermissions::allowedKeys($regular)
+ );
+
+ $navigation = SidebarPermissions::navigationFor($regular);
+ $this->assertSame(['financial-operations'], array_column($navigation, 'slug'));
+ $this->assertSame(['ledger'], array_column($navigation[0]['pages'], 'slug'));
+ }
+
+ $this->loginAs($regular)
+ ->get(route('business.pages.show', [
+ 'section' => 'financial-operations',
+ 'page' => 'ledger',
+ ]))
+ ->assertOk()
+ ->assertSee('data-ledger-page', false);
+ }
+
+ private function regularAccount(): User
+ {
+ $name = 'fixture-finance-ledger-' . User::count();
+ $user = User::create([
+ 'name' => $name,
+ 'email' => $name . '@example.test',
+ 'password' => 'fixture-unused-password',
+ 'enabled' => true,
+ 'locale' => 'zh-CN',
+ 'landing_page' => 'dashboard',
+ ]);
+ $user->roles()->attach(Role::where('name', 'user')->firstOrFail()->id);
+ $user->permissions()->attach(Permission::where('name', 'read-admin-panel')->firstOrFail()->id);
+ $user->companies()->attach($this->company->id);
+
+ return $user;
+ }
+}