CaiWuXiTong/tests/Feature/Console/InventoryReportAuthoritativeImportTest.php

328 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Tests\Feature\Console;
use App\Models\Business\InventoryWarehouseNameMapping;
use App\Models\Business\SystemizationTodo;
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_it_creates_and_reconciles_one_unmapped_warehouse_task_per_month(): void
{
$path = $this->workbook([
$this->headers(),
$this->row('2026-08-01', '仓储方甲', '主仓甲', 'SKU-A', 1, 1),
$this->row('2026-08-01', '仓储方乙', '主仓乙', 'SKU-B', 1, 1),
$this->row('2026-08-01', '仓储方乙', '主仓乙', 'SKU-C', 1, 1),
]);
$this->artisan('business-mirror:import-inventory-report-month', ['path' => $path])
->expectsOutputToContain('2 个仓储方/仓库组合未映射')
->assertSuccessful();
$sourceKey = 'inventory-warehouse-unmapped:2026-08';
$todo = SystemizationTodo::withTrashed()
->allCompanies()
->where('company_id', 1)
->where('source_key', $sourceKey)
->firstOrFail();
$this->assertSame(SystemizationTodo::STATUS_INCOMPLETE, $todo->status);
$this->assertStringContainsString('仓储方甲 / 主仓甲(1 行)', (string) $todo->progress);
$this->assertStringContainsString('仓储方乙 / 主仓乙(2 行)', (string) $todo->progress);
InventoryWarehouseNameMapping::allCompanies()->create([
'company_id' => 1,
'warehouse_provider' => '仓储方甲',
'warehouse' => '主仓甲',
'warehouse_short_name' => '仓储方甲简称',
]);
$this->artisan('inventory:sync-warehouse-mapping-alert', ['--month' => '2026-08'])
->expectsOutputToContain('1 个仓储方/仓库组合未映射')
->assertSuccessful();
$todo->refresh();
$this->assertSame(SystemizationTodo::STATUS_INCOMPLETE, $todo->status);
$this->assertStringNotContainsString('仓储方甲 / 主仓甲', (string) $todo->progress);
$this->assertStringContainsString('仓储方乙 / 主仓乙(2 行)', (string) $todo->progress);
$this->assertSame(1, SystemizationTodo::withTrashed()
->allCompanies()
->where('company_id', 1)
->where('source_key', $sourceKey)
->count());
InventoryWarehouseNameMapping::allCompanies()->create([
'company_id' => 1,
'warehouse_provider' => '仓储方乙',
'warehouse' => '主仓乙',
'warehouse_short_name' => '仓储方乙简称',
]);
$this->artisan('inventory:sync-warehouse-mapping-alert', ['--month' => '2026-08'])
->expectsOutputToContain('仓库简称映射完整')
->assertSuccessful();
$todo->refresh();
$this->assertSame(SystemizationTodo::STATUS_COMPLETED, $todo->status);
$this->assertNotNull($todo->completed_at);
$this->assertStringContainsString('自动完成提醒', (string) $todo->progress);
$this->assertSame(1, SystemizationTodo::withTrashed()
->allCompanies()
->where('company_id', 1)
->where('source_key', $sourceKey)
->count());
$this->artisan('inventory:sync-warehouse-mapping-alert', ['--month' => '2026-08'])
->assertSuccessful();
$this->assertSame(1, SystemizationTodo::withTrashed()
->allCompanies()
->where('company_id', 1)
->where('source_key', $sourceKey)
->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;
}
}