user->attachPermission( Permission::where('name', 'read-admin-panel')->firstOrFail() ); } public function test_final_table_page_keeps_the_maintenance_toolbar_and_starts_empty(): void { $this->loginAs() ->get(route('business.pages.show', [ 'section' => 'supplier-payables-final', 'page' => 'board-deductions', ])) ->assertOk() ->assertSeeText('导入') ->assertSeeText('导出') ->assertSeeText('查询') ->assertSeeText('重置') ->assertSeeText('编辑') ->assertSeeText('下载导入模板') ->assertSeeText('月份') ->assertSeeText('入库日期') ->assertSeeText('申领单号') ->assertSeeText('暂无数据,请点击“导入”上传本表数据。') ->assertSee('workspace/supplier-payables-final/actions/board-deductions/import', false) ->assertSee('workspace/supplier-payables-final/actions/board-deductions/template', false) // 导入/编辑弹窗的行内脚本必须随页面输出,局部替换导航才不会出现“点了没反应”。 ->assertSee('data-supplier-import-modal', false) ->assertSee('data-supplier-import-choose', false) ->assertSee('modal.dataset.ready', false) ->assertSee('data-supplier-maintenance-modal', false) // 页签是局部替换导航,弹窗脚本要带 replay 标记,替换后才会重新执行并绑定。 ->assertSee('data-caiwu-replay', false) ->assertDontSee('workspace/supplier-payables/actions/board-deductions/import', false); } public function test_final_template_downloads_the_same_columns(): void { $response = $this->loginAs() ->get(route('business.supplier-payables-final.board-deductions.template')) ->assertOk() ->assertHeader('content-type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); $path = tempnam(sys_get_temp_dir(), 'caiwu-template') . '.xlsx'; try { file_put_contents($path, $response->streamedContent()); $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($path); $header = $spreadsheet->getSheet(0)->rangeToArray('A1:C1')[0]; $notes = $spreadsheet->getSheet(1)->rangeToArray('A2:B2')[0]; $spreadsheet->disconnectWorksheets(); $this->assertSame(['记录标识', '月份', '入库日期'], $header); $this->assertSame('记录标识', $notes[0]); $this->assertStringContainsString('选填', (string) $notes[1]); } finally { @unlink($path); } } public function test_final_payment_reconciliation_template_keeps_the_extra_columns_in_order(): void { $response = $this->loginAs() ->get(route('business.supplier-payables-final.payment-reconciliation.template')) ->assertOk() ->assertHeader('content-type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); $path = tempnam(sys_get_temp_dir(), 'caiwu-template') . '.xlsx'; try { file_put_contents($path, $response->streamedContent()); $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($path); $header = $spreadsheet->getSheet(0)->rangeToArray('A1:U1')[0]; $spreadsheet->disconnectWorksheets(); $this->assertSame([ '记录标识', '月份', '入库单号', '采购单号', '日期', '供应商', '货号', '商品编码', '规格名称', '数量', '单价', '金额', '预估单价', '预估金额', '供应商商品编码', '备注', '仓库名称', '快递名称', '快递单号', '未发出', '对账人', ], $header); } finally { @unlink($path); } } public function test_final_payment_reconciliation_import_keeps_the_extra_columns(): void { $file = UploadedFile::fake()->createWithContent( 'payment-reconciliation.csv', "\xEF\xBB\xBF月份,入库单号,采购单号,日期,供应商,货号,商品编码,规格名称,数量,单价,金额,预估单价,预估金额,供应商商品编码,备注,仓库名称,快递名称,快递单号,未发出,对账人\n" . "2026-09,RK-1,CG-1,2026-09-08,终版供应商,X-1,P-1,规格甲,2,10,20,9,18,SC-1,备注甲,华东仓,顺丰,SF-1,未发出,对账人甲\n", ); $this->loginAs() ->post(route('business.supplier-payables-final.payment-reconciliation.import'), ['import' => $file]) ->assertStatus(302) ->assertSessionHas('success', '导入完成:新增 1 条,更新 0 条。'); $this->assertDatabaseHas('supplier_payable_overrides', [ 'company_id' => $this->company->id, 'dataset' => 'final:supplier:payment-reconciliation', ]); $this->loginAs() ->get(route('business.pages.show', [ 'section' => 'supplier-payables-final', 'page' => 'payment-reconciliation', ])) ->assertOk() ->assertSeeText('入库单号') ->assertSeeText('采购单号') ->assertSeeText('日期') ->assertSeeText('对账人') ->assertSeeText('RK-1') ->assertSeeText('CG-1') ->assertSeeText('2026-09-08') ->assertSeeText('未发出') ->assertSeeText('对账人甲') ->assertSeeText('SF-1'); $export = $this->loginAs()->get(route('business.supplier-payables-final.payment-reconciliation.export')); $export->assertOk(); $this->assertStringContainsString('采购单号,日期,供应商', $export->streamedContent()); $this->assertStringContainsString('快递单号,未发出,对账人', $export->streamedContent()); } public function test_final_return_reconciliation_template_adds_wms_and_creator_columns(): void { $response = $this->loginAs() ->get(route('business.supplier-payables-final.return-reconciliation.template')) ->assertOk() ->assertHeader('content-type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); $path = tempnam(sys_get_temp_dir(), 'caiwu-template') . '.xlsx'; try { file_put_contents($path, $response->streamedContent()); $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($path); $header = $spreadsheet->getSheet(0)->rangeToArray('A1:T1')[0]; $spreadsheet->disconnectWorksheets(); $this->assertSame([ '记录标识', '月份', '发货时间', '明细供应商', '条形码', '商品名称', '发货量', '单价', '金额', '预估单价', '预估金额', '供应商条码', '备注', '标题', 'wms配货单号', '仓库名称', '快递单号', '标记多标签', '创建人', '对账人', ], $header); $this->assertNotContains('快递名称', $header); } finally { @unlink($path); } } public function test_final_return_reconciliation_import_keeps_wms_and_creator_columns(): void { $file = UploadedFile::fake()->createWithContent( 'return-reconciliation.csv', "\xEF\xBB\xBF月份,发货时间,明细供应商,条形码,商品名称,发货量,单价,金额,预估单价,预估金额,供应商条码,备注,标题,wms配货单号,仓库名称,快递单号,标记多标签,创建人,对账人\n" . "2026-09,2026-09-01,退货供应商,BC-1,商品甲,3,10,30,9,27,SB-1,备注甲,标题甲,WMS-1,华南仓,SF-9,多标签,创建人甲,对账人乙\n", ); $this->loginAs() ->post(route('business.supplier-payables-final.return-reconciliation.import'), ['import' => $file]) ->assertStatus(302) ->assertSessionHas('success', '导入完成:新增 1 条,更新 0 条。'); $this->assertDatabaseHas('supplier_payable_overrides', [ 'company_id' => $this->company->id, 'dataset' => 'final:supplier:return-reconciliation', ]); $this->loginAs() ->get(route('business.pages.show', [ 'section' => 'supplier-payables-final', 'page' => 'return-reconciliation', ])) ->assertOk() ->assertSeeText('wms配货单号') ->assertSeeText('创建人') ->assertSeeText('WMS-1') ->assertSeeText('创建人甲') ->assertDontSeeText('快递名称'); $export = $this->loginAs()->get(route('business.supplier-payables-final.return-reconciliation.export')); $export->assertOk(); $this->assertStringContainsString('备注,标题,wms配货单号', $export->streamedContent()); $this->assertStringContainsString('标记多标签,创建人,对账人', $export->streamedContent()); $this->assertStringNotContainsString('快递名称', $export->streamedContent()); } public function test_final_return_reconciliation_page_paginates_fifteen_rows(): void { $records = []; for ($index = 1; $index <= 20; $index++) { $records[sprintf('return:%02d', $index)] = [ 'summary_month' => '2026-09', 'send_time' => '2026-09-30', 'supplier' => '退货供应商', 'barcode' => sprintf('RC-%02d', $index), 'sku_name' => '退货商品', 'qty' => '1', 'unit_price' => '10', 'amount' => '10', ]; } app(SupplierPayableOverrideStore::class)->saveBatch( 'final:supplier:return-reconciliation', $records, (int) $this->company->id, (int) $this->user->id, ); $baseUrl = route('business.pages.show', [ 'company_id' => (int) $this->company->id, 'section' => 'supplier-payables-final', 'page' => 'return-reconciliation', ]); $url = fn (int $page): string => $baseUrl . '?page=' . $page; $this->loginAs()->get($url(1)) ->assertOk() ->assertSeeText('每页 15 条') ->assertSeeText('第 1 页 / 共 2 页') ->assertSeeText('RC-01') ->assertDontSeeText('RC-16'); $this->loginAs()->get($url(2)) ->assertOk() ->assertSeeText('第 2 页 / 共 2 页') ->assertSeeText('RC-16') ->assertSeeText('RC-20') ->assertDontSeeText('RC-01'); } public function test_final_payment_reconciliation_page_paginates_fifteen_rows(): void { $records = []; for ($index = 1; $index <= 20; $index++) { $records[sprintf('payment:%02d', $index)] = [ 'summary_month' => '2026-09', 'stockNo' => sprintf('RK-%02d', $index), 'purchaseNo' => sprintf('CG-%02d', $index), 'date' => '2026-09-30', 'supplier' => '货款供应商', 'skuCode' => sprintf('PM-%02d', $index), 'productCode' => sprintf('P-%02d', $index), 'skuName' => '货款商品', 'qty' => '1', 'unitPrice' => '10', 'amount' => '10', ]; } app(SupplierPayableOverrideStore::class)->saveBatch( 'final:supplier:payment-reconciliation', $records, (int) $this->company->id, (int) $this->user->id, ); $baseUrl = route('business.pages.show', [ 'company_id' => (int) $this->company->id, 'section' => 'supplier-payables-final', 'page' => 'payment-reconciliation', ]); $url = fn (int $page): string => $baseUrl . '?page=' . $page; $this->loginAs()->get($url(1)) ->assertOk() ->assertSeeText('每页 15 条') ->assertSeeText('第 1 页 / 共 2 页') ->assertSeeText('PM-01') ->assertDontSeeText('PM-16'); $this->loginAs()->get($url(2)) ->assertOk() ->assertSeeText('第 2 页 / 共 2 页') ->assertSeeText('PM-16') ->assertSeeText('PM-20') ->assertDontSeeText('PM-01'); } public function test_final_import_reads_files_larger_than_one_chunk(): void { $lines = ['月份,入库单号,采购单号,日期,供应商,货号,商品编码,规格名称,数量,单价,金额,预估单价,预估金额,供应商商品编码,备注,仓库名称,快递名称,快递单号,未发出,对账人']; for ($i = 1; $i <= 4500; $i++) { $lines[] = "2026-09,RK-{$i},CG-{$i},2026-09-08,供应商{$i},X-{$i},P-{$i},规格甲,2,10,20,9,18,SC-1,备注甲,华东仓,顺丰,SF-{$i},未发出,对账人甲"; } $file = UploadedFile::fake()->createWithContent( 'payment-reconciliation.csv', "\xEF\xBB\xBF" . implode("\n", $lines) . "\n", ); $result = app(PayablesFinalImporter::class)->import( app(PayablesFinalRegistry::class)->fields('supplier-payables-final', 'payment-reconciliation'), 'final:supplier:payment-reconciliation', $file, (int) $this->company->id, (int) $this->user->id, ); $this->assertSame(['created' => 4500, 'updated' => 0, 'rows' => 4500], $result); } public function test_final_import_stops_at_the_row_limit_without_writing(): void { $lines = ['月份,入库单号,采购单号,日期,供应商,货号,商品编码,规格名称,数量,单价,金额,预估单价,预估金额,供应商商品编码,备注,仓库名称,快递名称,快递单号,未发出,对账人']; for ($i = 1; $i <= 10050; $i++) { $lines[] = "2026-09,RK-{$i},CG-{$i},2026-09-08,供应商{$i},X-{$i},P-{$i},规格甲,2,10,20,9,18,SC-1,备注甲,华东仓,顺丰,SF-{$i},未发出,对账人甲"; } $file = UploadedFile::fake()->createWithContent( 'payment-reconciliation.csv', "\xEF\xBB\xBF" . implode("\n", $lines) . "\n", ); try { app(PayablesFinalImporter::class)->import( app(PayablesFinalRegistry::class)->fields('supplier-payables-final', 'payment-reconciliation'), 'final:supplier:payment-reconciliation', $file, (int) $this->company->id, (int) $this->user->id, ); $this->fail('超过 1 万行应当拒绝导入。'); } catch (ValidationException $exception) { $this->assertSame( ['单次最多导入 10,000 条记录,请拆分文件后重试。'], $exception->errors()['import'], ); } $this->assertSame(0, (int) DB::connection(config('database.supplier_payable_maintenance_connection')) ->table('supplier_payable_overrides') ->where('dataset', 'final:supplier:payment-reconciliation') ->count()); } public function test_product_price_final_page_reuses_the_price_template_columns(): void { $this->loginAs() ->get(route('business.pages.show', [ 'section' => 'supplier-payables-final', 'page' => 'product-prices', ])) ->assertOk() ->assertSeeText('商品编码') ->assertSeeText('采购价') ->assertSeeText('京喜限价') ->assertSeeText('暂无数据,请点击“导入”上传本表数据。') ->assertSee('workspace/supplier-payables-final/actions/product-prices/template', false); $this->loginAs() ->get(route('business.supplier-payables-final.product-prices.template')) ->assertOk() ->assertHeader('content-type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); } public function test_import_writes_rows_into_the_final_dataset_only(): void { $file = UploadedFile::fake()->createWithContent( 'board-deductions.csv', "\xEF\xBB\xBF月份,入库日期,出库日期,家具厂,压贴厂,类型,规格,原料名称,实收数量,单价,金额,单位,批次日期,出库单号,出库备注,申领单号,单号类型,送货单号\n" . "2026-09,2026-09-01,2026-09-02,终版家具厂,终版压贴厂,类型A,规格A,原料A,3,12.5,37.5,张,2026-09-03,OUT-1,备注A,CLM-1,单号,DL-1\n", ); $this->loginAs() ->post(route('business.supplier-payables-final.board-deductions.import'), ['import' => $file]) ->assertStatus(302) ->assertSessionHas('success', '导入完成:新增 1 条,更新 0 条。'); $this->assertDatabaseHas('supplier_payable_overrides', [ 'company_id' => $this->company->id, 'dataset' => 'final:supplier:board-deductions', ]); $this->assertSame(0, DB::connection(config('database.supplier_payable_maintenance_connection')) ->table('supplier_payable_overrides') ->where('company_id', $this->company->id) ->where('dataset', 'board-deductions') ->count()); $this->loginAs() ->get(route('business.pages.show', [ 'section' => 'supplier-payables-final', 'page' => 'board-deductions', ])) ->assertOk() ->assertSeeText('2026-09') ->assertSeeText('终版家具厂') ->assertSeeText('CLM-1') ->assertDontSeeText('暂无数据,请点击“导入”上传本表数据。'); } public function test_final_export_and_edit_work_on_the_imported_rows(): void { $file = UploadedFile::fake()->createWithContent( 'board-deductions.csv', "入库日期,出库日期,家具厂,压贴厂,类型,规格,原料名称,实收数量,单价,金额,单位,批次日期,出库单号,出库备注,申领单号,单号类型,送货单号\n" . "2026-09-11,2026-09-12,导出家具厂,厂二,类型B,规格B,原料B,1,10,10,张,2026-09-13,OUT-2,备注B,CLM-2,单号,DL-2\n", ); $this->loginAs()->post(route('business.supplier-payables-final.board-deductions.import'), ['import' => $file]) ->assertStatus(302); $connection = DB::connection(config('database.supplier_payable_maintenance_connection')); $recordKey = (string) $connection->table('supplier_payable_overrides') ->where('company_id', $this->company->id) ->where('dataset', 'final:supplier:board-deductions') ->value('record_key'); $this->assertStringStartsWith('hash:', $recordKey); $export = $this->loginAs()->get(route('business.supplier-payables-final.board-deductions.export')); $export->assertOk(); $this->assertStringContainsString('导出家具厂', $export->streamedContent()); $this->loginAs() ->patch(route('business.supplier-payables-final.board-deductions.update', ['recordKey' => $recordKey]), [ 'values' => ['furniture_factory' => '编辑后的家具厂'], ]) ->assertStatus(302) ->assertSessionHas('success', '保存成功。'); $this->loginAs() ->get(route('business.pages.show', [ 'section' => 'supplier-payables-final', 'page' => 'board-deductions', ])) ->assertOk() ->assertSeeText('编辑后的家具厂'); } public function test_final_page_keyword_filter_narrows_the_rows(): void { $file = UploadedFile::fake()->createWithContent( 'board-deductions.csv', "入库日期,出库日期,家具厂,压贴厂,类型,规格,原料名称,实收数量,单价,金额,单位,批次日期,出库单号,出库备注,申领单号,单号类型,送货单号\n" . "2026-09-21,2026-09-22,筛选甲厂,厂二,类型C,规格C,原料C,1,1,1,张,2026-09-23,OUT-3,备注C,CLM-3,单号,DL-3\n" . "2026-09-21,2026-09-22,筛选乙厂,厂二,类型D,规格D,原料D,1,1,1,张,2026-09-23,OUT-4,备注D,CLM-4,单号,DL-4\n", ); $this->loginAs()->post(route('business.supplier-payables-final.board-deductions.import'), ['import' => $file]) ->assertStatus(302); $this->loginAs() ->get(route('business.pages.show', [ 'section' => 'supplier-payables-final', 'page' => 'board-deductions', 'keyword' => '筛选甲厂', ])) ->assertOk() ->assertSeeText('筛选甲厂') ->assertDontSeeText('筛选乙厂') ->assertSeeText('共 1 条'); } }