user->attachPermission( Permission::where('name', 'read-admin-panel')->firstOrFail(), ); } public function testPageReplacesLegacyTableWithWorkbookGroupsAndFilters(): void { FundForecast::create([ 'company_id' => $this->company->id, 'budget_month' => '2026-09', 'company_name' => '测试资金公司', 'region' => '华东', 'report_status' => '待审核', 'balance_bank' => 100, 'balance_jd' => 20, 'income_jd' => 80, 'income_other' => 20, 'expense_supplier_goods' => 40, 'expense_salary' => 10, 'previous_month_balance' => 30, ]); $response = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'fund-management', 'page' => 'fund-forecast', ])); $response ->assertOk() ->assertSeeText('公司总数') ->assertSeeText('预测月份') ->assertSeeText('所属区域') ->assertSeeText('填报状态') ->assertSeeText('测试资金公司') ->assertSeeText('淘系支付宝') ->assertSeeText('店铺花费(暂估10%)') ->assertSeeText('上月末总余额') ->assertSeeText('缺口/盈余') ->assertSee('data-group="balance"', false) ->assertSee('data-group="income"', false) ->assertSee('data-group="expense"', false) ->assertDontSeeText('店铺余额(预算)') ->assertDontSeeText('银行余额(实际)'); $row = FundForecast::firstOrFail(); $this->assertSame('120.00', $row->previous_month_balance); $this->assertSame('100.00', $row->forecast_month_income); $this->assertSame('50.00', $row->forecast_month_expense); $this->assertSame('170.00', $row->gap_surplus); $this->assertFalse($row->has_gap); $this->assertSame($this->company->id, $row->company_id); } public function testCompanyFilterOnlyShowsMatchingForecast(): void { foreach (['目标公司', '其他公司'] as $companyName) { FundForecast::create([ 'company_id' => $this->company->id, 'budget_month' => '2026-09', 'company_name' => $companyName, ]); } $this->loginAs()->get(route('business.pages.show', [ 'section' => 'fund-management', 'page' => 'fund-forecast', 'company_name' => '目标', ])) ->assertOk() ->assertSeeText('目标公司') ->assertDontSeeText('其他公司'); } public function testImporterUnderstandsTwoRowWorkbookHeaders(): void { $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->fromArray([ ['预策月份', '公司名称', '余额', null, '收入', null, '支出', null, '汇总', null, null, null, null], [null, null, '京东', '抖音', '京东', '其他收入', '供应商货款', '工资', '上月末总余额', '预策本月收入', '预策本月支付', '缺口/盈余', '是否有缺口'], ['2026年9月', '导入测试公司', 12, 3, 100, 20, 30, 10, 50, 999, 999, -999, '是'], ]); $path = tempnam(sys_get_temp_dir(), 'fund-forecast-'); (new Xlsx($spreadsheet))->save($path); try { $result = app(FundForecastWorkbookImporter::class)->import( new UploadedFile($path, '资金预测表.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', null, true), (int) $this->company->id, ); } finally { @unlink($path); } $this->assertSame(['created' => 1, 'updated' => 0], $result); $row = FundForecast::where('company_name', '导入测试公司')->firstOrFail(); $this->assertSame('2026-09', $row->budget_month); $this->assertSame('12.00', $row->balance_jd); $this->assertSame('100.00', $row->income_jd); $this->assertSame('15.00', $row->previous_month_balance); $this->assertSame('120.00', $row->forecast_month_income); $this->assertSame('40.00', $row->forecast_month_expense); $this->assertSame('95.00', $row->gap_surplus); $this->assertFalse($row->has_gap); $this->assertSame('待审核', $row->report_status); } public function testTemplateDownloadContainsImportableTwoRowHeaders(): void { $response = $this->loginAs()->get(route('business.fund-forecast.template')); $response ->assertOk() ->assertHeader('content-type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); $path = tempnam(sys_get_temp_dir(), 'fund-forecast-template-'); file_put_contents($path, $response->streamedContent()); try { $sheet = IOFactory::load($path)->getActiveSheet(); $this->assertSame('资金预测表', $sheet->getTitle()); $this->assertSame('预测月份', $sheet->getCell('A1')->getValue()); $this->assertSame('公司名称', $sheet->getCell('B1')->getValue()); $this->assertSame('余额', $sheet->getCell('C1')->getValue()); $this->assertSame('银行', $sheet->getCell('C2')->getValue()); $this->assertSame('汇总', $sheet->getCell('AA1')->getValue()); $this->assertSame('是否有缺口', $sheet->getCell('AE2')->getValue()); $this->assertSame('=SUM(C3:H3)', $sheet->getCell('AA3')->getValue()); $this->assertSame('=SUM(I3:N3)', $sheet->getCell('AB3')->getValue()); $this->assertSame('=SUM(O3:Z3)', $sheet->getCell('AC3')->getValue()); $this->assertSame('=AA3+AB3-AC3', $sheet->getCell('AD3')->getValue()); $this->assertSame('=IF(AD3<0,"是","否")', $sheet->getCell('AE3')->getValue()); $this->assertSame('C3', $sheet->getFreezePane()); } finally { @unlink($path); } } public function testSummaryFieldsAreRecalculatedAndNegativeGapIsMarked(): void { $row = FundForecast::create([ 'company_id' => $this->company->id, 'budget_month' => '2026-09', 'company_name' => '缺口测试公司', 'balance_bank' => 10, 'balance_wechat' => 5, 'income_other' => 20, 'expense_supplier_goods' => 50, 'previous_month_balance' => 999, 'forecast_month_income' => 999, 'forecast_month_expense' => 999, 'gap_surplus' => 999, 'has_gap' => false, ]); $this->assertSame('15.00', $row->previous_month_balance); $this->assertSame('20.00', $row->forecast_month_income); $this->assertSame('50.00', $row->forecast_month_expense); $this->assertSame('-15.00', $row->gap_surplus); $this->assertTrue($row->has_gap); } }