From aba8039b8d879d6cb52da7e3eb5a7ff9d5ea7c36 Mon Sep 17 00:00:00 2001 From: shanshi <3316401569@qq.com> Date: Tue, 20 Oct 2026 13:38:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AF=BC=E5=87=BA=E8=B4=A2=E5=8A=A1?= =?UTF-8?q?=E5=8F=B0=E8=B4=A6=E8=B4=A6=E6=88=B7=E5=BD=95=E5=85=A5=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controllers/Business/FinanceLedger.php | 7 + docs/财务台账开发说明.md | 1 + .../Services/FinanceLedgerAccountTemplate.php | 200 ++++++++++++++++++ .../pages/financial-ledger/ledger.blade.php | 2 +- routes/admin.php | 4 + .../FinanceLedgerAccountStatusTest.php | 1 + .../FinanceLedgerAccountTemplateTest.php | 83 ++++++++ 7 files changed, 297 insertions(+), 1 deletion(-) create mode 100644 modules/EcommerceFinance/Services/FinanceLedgerAccountTemplate.php create mode 100644 tests/Feature/Business/FinanceLedgerAccountTemplateTest.php diff --git a/app/Http/Controllers/Business/FinanceLedger.php b/app/Http/Controllers/Business/FinanceLedger.php index 536907b1..3f6c97f0 100644 --- a/app/Http/Controllers/Business/FinanceLedger.php +++ b/app/Http/Controllers/Business/FinanceLedger.php @@ -9,12 +9,19 @@ use Illuminate\Http\Request; use Illuminate\Routing\Controller; use Illuminate\Support\Facades\DB; use Illuminate\Validation\Rule; +use Modules\EcommerceFinance\Services\FinanceLedgerAccountTemplate; use Modules\EcommerceFinance\Services\FinanceLedgerCalculator; use Modules\EcommerceFinance\Services\FinanceLedgerPayableMappingBulk; use Modules\EcommerceFinance\Services\FinanceLedgerShopMappingBulk; +use Symfony\Component\HttpFoundation\StreamedResponse; class FinanceLedger extends Controller { + public function downloadAccountTemplate(FinanceLedgerAccountTemplate $template): StreamedResponse + { + return $template->download(); + } + public function updateMapping(Request $request, int $id, FinanceLedgerCalculator $calculator): JsonResponse { abort_unless(FinanceLedgerAccess::canEditMapping(user()), 403, '只有财务台账管理员可以修改账户映射。'); diff --git a/docs/财务台账开发说明.md b/docs/财务台账开发说明.md index 90809490..7c729e01 100644 --- a/docs/财务台账开发说明.md +++ b/docs/财务台账开发说明.md @@ -63,6 +63,7 @@ docker compose -f docker-compose.local.yml exec -u www-data app \ - 路由:`/workspace/financial-ledger/ledger` - 页面:账户总表、店铺台账、每日资金流水、动账明细、账户映射、数据来源 +- 账户总表支持导出空白 Excel 录入模板,包含页面列、状态和账户类型选项及填写说明 - 写权限:账户映射、账户状态和账户类型修改仅系统管理员及配置的财务台账管理员角色;其他账号只读 - 调度:`finance-ledger:sync-rpt` 每天 02:30(Asia/Shanghai) diff --git a/modules/EcommerceFinance/Services/FinanceLedgerAccountTemplate.php b/modules/EcommerceFinance/Services/FinanceLedgerAccountTemplate.php new file mode 100644 index 00000000..7d23054a --- /dev/null +++ b/modules/EcommerceFinance/Services/FinanceLedgerAccountTemplate.php @@ -0,0 +1,200 @@ +makeSpreadsheet(); + + return response()->streamDownload(function () use ($spreadsheet): void { + $writer = new Xlsx($spreadsheet); + $writer->setPreCalculateFormulas(false); + $writer->save('php://output'); + $spreadsheet->disconnectWorksheets(); + }, '财务台账-账户总表录入模板.xlsx', [ + 'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'Cache-Control' => 'no-store, no-cache, must-revalidate', + ]); + } + + private function makeSpreadsheet(): Spreadsheet + { + $spreadsheet = new Spreadsheet(); + $spreadsheet->getProperties() + ->setTitle('财务台账账户总表录入模板') + ->setSubject('账户总表重新录入'); + + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setTitle('账户总表'); + $lastColumn = Coordinate::stringFromColumnIndex(count(self::HEADERS)); + $lastRow = self::MAX_ROWS + 1; + + foreach (self::HEADERS as $index => $header) { + $column = Coordinate::stringFromColumnIndex($index + 1); + $sheet->setCellValue("{$column}1", $header); + $sheet->getColumnDimension($column)->setWidth($this->width($index)); + } + + $sheet->freezePane('A2'); + $sheet->setAutoFilter("A1:{$lastColumn}1"); + $sheet->getRowDimension(1)->setRowHeight(28); + $sheet->getStyle("A1:{$lastColumn}1")->getAlignment() + ->setHorizontal(Alignment::HORIZONTAL_CENTER) + ->setVertical(Alignment::VERTICAL_CENTER) + ->setWrapText(true); + $sheet->getStyle("A1:{$lastColumn}1")->getFont()->setBold(true)->getColor()->setARGB('FFFFFFFF'); + $sheet->getStyle("A1:{$lastColumn}1")->getFill() + ->setFillType(Fill::FILL_SOLID) + ->getStartColor()->setARGB('FF856632'); + $sheet->getStyle("A1:{$lastColumn}{$lastRow}")->getBorders()->getAllBorders() + ->setBorderStyle(Border::BORDER_THIN)->getColor()->setARGB('FFD8D2C7'); + + $sheet->getStyle("A2:N{$lastRow}")->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_TEXT); + $sheet->getStyle("O2:Q{$lastRow}")->getNumberFormat()->setFormatCode('#,##0.00'); + $sheet->getStyle("R2:R{$lastRow}")->getNumberFormat()->setFormatCode('0.00'); + $sheet->getStyle("S2:Y{$lastRow}")->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_TEXT); + + $options = $spreadsheet->createSheet(); + $options->setTitle('选项'); + $options->setCellValue('A1', '账户状态'); + $options->setCellValue('B1', '账户类型'); + $statuses = array_values(FinanceLedgerAccountStatus::OPTIONS); + $types = $this->accountTypes(); + foreach ($statuses as $index => $status) { + $options->setCellValue('A' . ($index + 2), $status); + } + foreach ($types as $index => $type) { + $options->setCellValue('B' . ($index + 2), $type); + } + $options->setSheetState(Worksheet::SHEETSTATE_HIDDEN); + + $this->applyListValidation($sheet, 'C', '\'选项\'!$A$2:$A$' . (count($statuses) + 1), '请选择账户状态'); + if ($types !== []) { + $this->applyListValidation($sheet, 'H', '\'选项\'!$B$2:$B$' . (count($types) + 1), '请选择账户类型'); + } + + $notes = $spreadsheet->createSheet(); + $notes->setTitle('填写说明'); + $notes->fromArray([ + ['项目', '说明'], + ['用途', '账户总表重新录入模板,不包含当前账户数据。'], + ['必填', '公司名称、账户简称。'], + ['账户状态', implode(' / ', $statuses)], + ['账户类型', $types !== [] ? implode(' / ', $types) : '留空或按实际填写'], + ['金额', '当前余额、卡住金额、每日可出按数字填写;限额列保留原有组合写法。'], + ['提交前', '请核对账户数、余额合计和期初合计,再导入系统。'], + ], null, 'A1'); + $notes->getColumnDimension('A')->setWidth(16); + $notes->getColumnDimension('B')->setWidth(70); + $notes->getStyle('A1:B1')->getFont()->setBold(true)->getColor()->setARGB('FFFFFFFF'); + $notes->getStyle('A1:B1')->getFill() + ->setFillType(Fill::FILL_SOLID) + ->getStartColor()->setARGB('FF856632'); + $notes->getStyle('A1:B7')->getAlignment()->setVertical(Alignment::VERTICAL_TOP)->setWrapText(true); + $notes->getStyle('A1:B7')->getBorders()->getAllBorders() + ->setBorderStyle(Border::BORDER_THIN)->getColor()->setARGB('FFD8D2C7'); + $notes->getRowDimension(1)->setRowHeight(26); + + $spreadsheet->setActiveSheetIndex(0); + + return $spreadsheet; + } + + private function applyListValidation( + Worksheet $sheet, + string $column, + string $formula, + string $prompt, + ): void { + $validation = $sheet->getCell("{$column}2")->getDataValidation(); + $validation->setType(DataValidation::TYPE_LIST); + $validation->setErrorStyle(DataValidation::STYLE_STOP); + $validation->setAllowBlank(true); + $validation->setShowInputMessage(true); + $validation->setPromptTitle('选择' . $prompt); + $validation->setPrompt($prompt); + $validation->setShowErrorMessage(true); + $validation->setErrorTitle('选项无效'); + $validation->setError($prompt); + $validation->setFormula1($formula); + $validation->setShowDropDown(true); + + for ($row = 2; $row <= self::MAX_ROWS + 1; $row++) { + $sheet->getCell("{$column}{$row}")->setDataValidation(clone $validation); + } + } + + private function accountTypes(): array + { + $connection = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror')); + $base = $connection->table('fin_accounts') + ->whereNotNull('account_type')->where('account_type', '<>', '') + ->distinct()->pluck('account_type')->all(); + $overrides = $connection->table('fin_account_type_overrides') + ->whereNotNull('account_type')->where('account_type', '<>', '') + ->distinct()->pluck('account_type')->all(); + $types = array_values(array_unique(array_merge($base, $overrides))); + sort($types, SORT_NATURAL | SORT_FLAG_CASE); + + return $types; + } + + private function width(int $index): int + { + return match ($index) { + 0, 5 => 30, + 1, 4, 6, 23 => 22, + 2, 3, 7, 8, 17 => 13, + 14, 15, 16 => 16, + default => 18, + }; + } +} diff --git a/resources/views/business/pages/financial-ledger/ledger.blade.php b/resources/views/business/pages/financial-ledger/ledger.blade.php index c53ea74e..297ca5c0 100644 --- a/resources/views/business/pages/financial-ledger/ledger.blade.php +++ b/resources/views/business/pages/financial-ledger/ledger.blade.php @@ -145,7 +145,7 @@ @if ($activeAccountFilterCount > 0) 筛选中{{ $activeAccountFilterCount }} @endif -
refresh重置
+
refresh重置download导出录入模板
账户数{{ number_format((int) ($ledgerAccountSummary['count'] ?? 0)) }}
diff --git a/routes/admin.php b/routes/admin.php index 2d2bb058..712f70d4 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -645,6 +645,10 @@ Route::post('workspace/financial-ledger/accounts/type', [\App\Http\Controllers\B ->middleware('business.page:financial-ledger,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') + ->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') ->name('business.financial-ledger.mapping.shop.preview'); diff --git a/tests/Feature/Business/FinanceLedgerAccountStatusTest.php b/tests/Feature/Business/FinanceLedgerAccountStatusTest.php index 7483ece1..87fca225 100644 --- a/tests/Feature/Business/FinanceLedgerAccountStatusTest.php +++ b/tests/Feature/Business/FinanceLedgerAccountStatusTest.php @@ -64,6 +64,7 @@ class FinanceLedgerAccountStatusTest extends FeatureTestCase ->assertSee('data-account-type', false) ->assertSee('ledger-type-select', false) ->assertSee('data-mapping-bulk', false) + ->assertSee('导出录入模板') ->assertSee('未填写') ->assertDontSee('data-account-filter-state', false) ->assertSee('value="normal" selected', false); diff --git a/tests/Feature/Business/FinanceLedgerAccountTemplateTest.php b/tests/Feature/Business/FinanceLedgerAccountTemplateTest.php new file mode 100644 index 00000000..afcf4dd2 --- /dev/null +++ b/tests/Feature/Business/FinanceLedgerAccountTemplateTest.php @@ -0,0 +1,83 @@ +user->attachPermission( + Permission::where('name', 'read-admin-panel')->firstOrFail() + ); + DB::connection('business_mirror')->table('fin_accounts')->insert([ + 'company_name' => '模板测试公司', + 'account_short' => '模板测试账户', + 'account_type' => '公户', + ]); + } + + public function test_account_master_template_is_blank_and_contains_page_columns_and_validations(): void + { + $response = $this->loginAs()->get(route('business.financial-ledger.accounts.template', [ + 'company_id' => $this->company->id, + ])); + $response->assertOk(); + $this->assertSame( + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + $response->headers->get('content-type'), + ); + + $path = tempnam(sys_get_temp_dir(), 'ledger-template-'); + file_put_contents($path, $response->streamedContent()); + + try { + $spreadsheet = IOFactory::load($path); + $sheet = $spreadsheet->getSheetByName('账户总表'); + $this->assertNotNull($sheet); + $this->assertSame([ + '公司名称', + '账户简称', + '账户状态', + '地区', + '账号', + '开户行', + '开户城市/网点', + '账户类型', + '公司规划', + '后4位', + '单笔限额', + '日限额', + '月限额', + '年限额', + '当前余额', + '卡住金额', + '每日可出', + '预计出清天数', + '提额目标', + '提额状态', + '提额动作', + '已做动作', + '下一步', + '备注', + '责任人', + ], $sheet->rangeToArray('A1:Y1')[0]); + $this->assertNull($sheet->getCell('A2')->getValue()); + $this->assertSame(DataValidation::TYPE_LIST, $sheet->getCell('C2')->getDataValidation()->getType()); + $this->assertSame(DataValidation::TYPE_LIST, $sheet->getCell('H2')->getDataValidation()->getType()); + $this->assertSame(Worksheet::SHEETSTATE_HIDDEN, $spreadsheet->getSheetByName('选项')->getSheetState()); + $this->assertNotNull($spreadsheet->getSheetByName('填写说明')); + $spreadsheet->disconnectWorksheets(); + } finally { + @unlink($path); + } + } +}