CaiWuXiTong/tests/Feature/Console/InventoryReportAuthoritativeImportTest.php
2026-09-26 22:45:37 +08:00

253 lines
10 KiB
PHP

<?php
namespace Tests\Feature\Console;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Modules\EcommerceFinance\Services\InventoryReportAuthoritativeImporter;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Tests\TestCase;
class InventoryReportAuthoritativeImportTest extends TestCase
{
private array $temporaryFiles = [];
protected function setUp(): void
{
parent::setUp();
config([
'database.connections.business_mirror' => [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
],
'upstream_mirrors.connection' => 'business_mirror',
]);
DB::purge('business_mirror');
$this->createTables();
}
protected function tearDown(): void
{
foreach ($this->temporaryFiles as $file) {
@unlink($file);
}
parent::tearDown();
}
public function test_it_imports_monthly_warehouse_rows_and_preserves_opening_quantity(): void
{
$path = $this->workbook([
$this->headers(),
$this->row('2026-08-01', '简尚-崔桥工厂仓', '简尚-崔桥工厂仓主仓', '15962G', 14, 6, [
'销售出仓' => -28,
'调拨入' => 20,
]),
$this->row('2026-08-01', '简尚-崔桥工厂仓', '简尚-崔桥工厂仓进货仓', '15962G', 0, 0, [
'采购进仓' => 20,
'调拨出' => -20,
]),
]);
$this->artisan('business-mirror:import-inventory-report-month', ['path' => $path])
->assertSuccessful();
$state = DB::connection('business_mirror')->table('inventory_report_authoritative_states')->first();
$this->assertSame(InventoryReportAuthoritativeImporter::DATASET_KEY, $state->dataset_key);
$this->assertSame('2026-08-01', $state->source_month);
$this->assertSame(2, (int) $state->source_row_count);
$this->assertDatabaseHas('inventory_report_authoritative_rows', [
'generation' => $state->active_generation,
'warehouse' => '简尚-崔桥工厂仓主仓',
'product_code' => '15962G',
'opening_quantity' => 14,
'closing_quantity' => 6,
'sales_out' => -28,
'transfer_in' => 20,
], 'business_mirror');
}
public function test_it_keeps_authoritative_generations_for_multiple_months(): void
{
$august = app(InventoryReportAuthoritativeImporter::class)->import($this->workbook([
$this->headers(),
$this->row('2026-08-01', '仓储方', '主仓', 'SKU-A', 10, 12, ['采购进仓' => 2]),
]));
$july = app(InventoryReportAuthoritativeImporter::class)->import($this->workbook([
$this->headers(),
$this->row('2026-07-01', '仓储方', '主仓', 'SKU-A', 8, 10, ['采购进仓' => 2]),
]));
$months = DB::connection('business_mirror')
->table('inventory_report_authoritative_months')
->where('dataset_key', InventoryReportAuthoritativeImporter::DATASET_KEY)
->orderBy('source_month')
->get();
$this->assertCount(2, $months);
$this->assertSame('2026-07-01', (string) $months[0]->source_month);
$this->assertSame($july['generation'], (string) $months[0]->active_generation);
$this->assertSame('2026-08-01', (string) $months[1]->source_month);
$this->assertSame($august['generation'], (string) $months[1]->active_generation);
$this->assertSame(2, DB::connection('business_mirror')
->table('inventory_report_authoritative_rows')->count());
}
public function test_invalid_replacement_keeps_previous_generation(): void
{
$valid = app(InventoryReportAuthoritativeImporter::class)->import($this->workbook([
$this->headers(),
$this->row('2026-08-01', '仓储方', '主仓', 'SKU-A', 1, 1),
]));
$invalid = $this->workbook([
$this->headers(),
$this->row('2026-08-01', '仓储方', '主仓', 'SKU-A', 1, 2),
]);
$this->artisan('business-mirror:import-inventory-report-month', ['path' => $invalid])
->expectsOutputToContain('已保留上一版有效数据')
->assertFailed();
$state = DB::connection('business_mirror')->table('inventory_report_authoritative_states')->first();
$this->assertSame($valid['generation'], $state->active_generation);
$this->assertSame(1, DB::connection('business_mirror')->table('inventory_report_authoritative_rows')
->where('generation', $valid['generation'])->count());
}
public function test_it_finds_the_detail_sheet_when_a_pivot_sheet_comes_first(): void
{
$temporary = tempnam(sys_get_temp_dir(), 'inventory-report-multi-sheet-');
$path = $temporary . '.xlsx';
rename($temporary, $path);
$spreadsheet = new Spreadsheet();
$spreadsheet->getActiveSheet()->setTitle('汇总');
$spreadsheet->getActiveSheet()->fromArray([
['条码+仓库', '求和项:期初数量'],
['SKU-A主仓', 99],
]);
$detail = $spreadsheet->createSheet();
$detail->setTitle('明细');
$detail->fromArray([
$this->headers(),
$this->row('2026-08-01', '仓储方', '主仓', 'SKU-A', 1, 1),
]);
(new Xlsx($spreadsheet))->save($path);
$spreadsheet->disconnectWorksheets();
$this->temporaryFiles[] = $path;
$result = app(InventoryReportAuthoritativeImporter::class)->import($path);
$this->assertSame('2026-08-01', $result['source_month']);
$this->assertSame(1, $result['source_rows']);
$this->assertDatabaseHas('inventory_report_authoritative_rows', [
'generation' => $result['generation'],
'product_code' => 'SKU-A',
'warehouse' => '主仓',
], 'business_mirror');
}
private function createTables(): void
{
$schema = Schema::connection('business_mirror');
$schema->create('inventory_report_authoritative_states', function (Blueprint $table): void {
$table->string('dataset_key')->primary();
$table->string('status');
$table->string('active_generation')->nullable();
$table->string('pending_generation')->nullable();
$table->string('source_filename')->nullable();
$table->string('source_sha256')->nullable();
$table->date('source_month')->nullable();
$table->unsignedBigInteger('source_row_count')->default(0);
$table->dateTime('last_attempted_at')->nullable();
$table->dateTime('last_succeeded_at')->nullable();
$table->string('last_error_code')->nullable();
$table->timestamps();
});
$schema->create('inventory_report_authoritative_rows', function (Blueprint $table): void {
$table->string('generation');
$table->string('row_key');
$table->date('report_month');
$table->string('owner');
$table->string('warehouse_provider');
$table->string('warehouse');
$table->string('product_code');
$table->string('category')->nullable();
foreach ([
'opening_quantity', 'closing_quantity', 'processing_in', 'processing_out',
'purchase_in', 'purchase_return', 'other_in', 'other_return', 'sales_out',
'sales_return', 'stocktake', 'transfer_out', 'transfer_in', 'opening', 'other_out',
] as $column) {
$table->decimal($column, 20, 4)->default(0);
}
$table->dateTime('imported_at');
$table->primary(['generation', 'row_key']);
});
$schema->create('inventory_report_authoritative_months', function (Blueprint $table): void {
$table->string('dataset_key');
$table->date('source_month');
$table->string('status')->default('succeeded');
$table->string('active_generation')->nullable();
$table->string('source_filename')->nullable();
$table->string('source_sha256')->nullable();
$table->unsignedBigInteger('source_row_count')->default(0);
$table->dateTime('last_succeeded_at')->nullable();
$table->timestamps();
$table->primary(['dataset_key', 'source_month']);
});
}
private function headers(): array
{
return [
'货主', '月份', '仓储方', '仓库', '商品编码', '图片', '商品名称', '商品简称',
'分类', '虚拟分类', '供应商款号', '颜色规格', '品牌', '单位', '期初数量', '期末数量',
'加工进仓', '加工出仓', '采购进仓', '采购退货', '其它进仓', '其它退货',
'销售出仓', '销售退货', '盘点', '调拨出', '调拨入', '期初', '其它出库',
];
}
private function row(
string $month,
string $provider,
string $warehouse,
string $sku,
float $opening,
float $closing,
array $movements = [],
): array {
$row = array_fill(0, 29, null);
$row[0] = '常州简尚家居有限公司';
$row[1] = $month;
$row[2] = $provider;
$row[3] = $warehouse;
$row[4] = $sku;
$row[8] = '测试分类';
$row[14] = $opening;
$row[15] = $closing;
foreach ([
'加工进仓', '加工出仓', '采购进仓', '采购退货', '其它进仓', '其它退货',
'销售出仓', '销售退货', '盘点', '调拨出', '调拨入', '期初', '其它出库',
] as $offset => $header) {
$row[16 + $offset] = $movements[$header] ?? 0;
}
return $row;
}
private function workbook(array $rows): string
{
$temporary = tempnam(sys_get_temp_dir(), 'inventory-report-month-');
$path = $temporary . '.xlsx';
rename($temporary, $path);
$spreadsheet = new Spreadsheet();
$spreadsheet->getActiveSheet()->fromArray($rows);
(new Xlsx($spreadsheet))->save($path);
$spreadsheet->disconnectWorksheets();
$this->temporaryFiles[] = $path;
return $path;
}
}