2026-02-21 18:19:03 +09:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
|
|
|
|
|
|
|
|
use Illuminate\Console\Attributes\AsCommand;
|
|
|
|
|
|
use Illuminate\Console\Command;
|
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
#[AsCommand(name: 'migrate:5130-bending-stock', description: '5130 레거시 절곡품 코드 생성 + BD-* 전체 품목 초기 재고 셋팅')]
|
2026-02-21 18:19:03 +09:00
|
|
|
|
class Migrate5130BendingStock extends Command
|
|
|
|
|
|
{
|
|
|
|
|
|
protected $signature = 'migrate:5130-bending-stock
|
|
|
|
|
|
{--tenant_id=287 : Target tenant ID (default: 287 경동기업)}
|
|
|
|
|
|
{--dry-run : 실제 저장 없이 시뮬레이션만 수행}
|
2026-02-22 03:04:47 +09:00
|
|
|
|
{--min-stock=100 : 품목별 초기 재고 수량 (기본: 100)}
|
|
|
|
|
|
{--rollback : 초기 재고 셋팅 롤백 (init_stock 소스 데이터 삭제)}';
|
2026-02-21 18:19:03 +09:00
|
|
|
|
|
|
|
|
|
|
private string $sourceDb = 'chandj';
|
|
|
|
|
|
|
|
|
|
|
|
private string $targetDb = 'mysql';
|
|
|
|
|
|
|
|
|
|
|
|
// 5130 prod 코드 → 한글명
|
|
|
|
|
|
private array $prodNames = [
|
2026-02-22 03:04:47 +09:00
|
|
|
|
'R' => '가이드레일(벽면)', 'S' => '가이드레일(측면)',
|
|
|
|
|
|
'G' => '연기차단재', 'B' => '하단마감재(스크린)',
|
|
|
|
|
|
'T' => '하단마감재(철재)', 'L' => 'L-Bar', 'C' => '케이스',
|
2026-02-21 18:19:03 +09:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// 5130 spec 코드 → 한글명
|
|
|
|
|
|
private array $specNames = [
|
2026-02-22 03:04:47 +09:00
|
|
|
|
'I' => '화이바원단', 'S' => 'SUS', 'U' => 'SUS2', 'E' => 'EGI',
|
|
|
|
|
|
'A' => '스크린용', 'D' => 'D형', 'C' => 'C형', 'M' => '본체',
|
|
|
|
|
|
'T' => '본체(철재)', 'B' => '후면코너부', 'L' => '린텔부',
|
|
|
|
|
|
'P' => '점검구', 'F' => '전면부',
|
2026-02-21 18:19:03 +09:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
// 5130 slength 코드 → 한글명
|
|
|
|
|
|
private array $slengthNames = [
|
2026-02-22 03:04:47 +09:00
|
|
|
|
'53' => 'W50×3000', '54' => 'W50×4000', '83' => 'W80×3000',
|
|
|
|
|
|
'84' => 'W80×4000', '12' => '1219mm', '24' => '2438mm',
|
|
|
|
|
|
'30' => '3000mm', '35' => '3500mm', '40' => '4000mm',
|
|
|
|
|
|
'41' => '4150mm', '42' => '4200mm', '43' => '4300mm',
|
2026-02-21 18:19:03 +09:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
private array $stats = [
|
2026-02-22 03:04:47 +09:00
|
|
|
|
'items_found' => 0,
|
|
|
|
|
|
'items_created_5130' => 0,
|
|
|
|
|
|
'items_category_updated' => 0,
|
2026-02-21 18:19:03 +09:00
|
|
|
|
'stocks_created' => 0,
|
2026-02-22 03:04:47 +09:00
|
|
|
|
'stocks_skipped' => 0,
|
2026-02-21 18:19:03 +09:00
|
|
|
|
'lots_created' => 0,
|
2026-02-22 03:04:47 +09:00
|
|
|
|
'transactions_created' => 0,
|
2026-02-21 18:19:03 +09:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
public function handle(): int
|
|
|
|
|
|
{
|
|
|
|
|
|
$tenantId = (int) $this->option('tenant_id');
|
|
|
|
|
|
$dryRun = $this->option('dry-run');
|
|
|
|
|
|
$rollback = $this->option('rollback');
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$minStock = (int) $this->option('min-stock');
|
2026-02-21 18:19:03 +09:00
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$this->info('=== BD-* 절곡품 초기 재고 셋팅 ===');
|
2026-02-21 18:19:03 +09:00
|
|
|
|
$this->info("Tenant ID: {$tenantId}");
|
|
|
|
|
|
$this->info('Mode: '.($dryRun ? 'DRY-RUN (시뮬레이션)' : 'LIVE'));
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$this->info("초기 재고: {$minStock}개/품목");
|
2026-02-21 18:19:03 +09:00
|
|
|
|
$this->newLine();
|
|
|
|
|
|
|
|
|
|
|
|
if ($rollback) {
|
2026-02-22 03:04:47 +09:00
|
|
|
|
return $this->rollbackInitStock($tenantId, $dryRun);
|
2026-02-21 18:19:03 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
// 0. 5130 레거시 데이터에서 BD-{PROD}{SPEC}-{SLENGTH} 아이템 생성
|
|
|
|
|
|
$this->info('📥 Step 0: 5130 레거시 코드 → BD 아이템 생성...');
|
|
|
|
|
|
$this->createLegacyItems($tenantId, $dryRun);
|
2026-02-21 18:19:03 +09:00
|
|
|
|
$this->newLine();
|
|
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
// 1. 전체 BD-* 아이템 조회 (기존 58개 + 5130 생성분)
|
|
|
|
|
|
$this->info('📥 Step 1: BD-* 절곡품 품목 조회...');
|
|
|
|
|
|
$items = DB::connection($this->targetDb)
|
|
|
|
|
|
->table('items')
|
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
|
->where('code', 'like', 'BD-%')
|
|
|
|
|
|
->whereNull('deleted_at')
|
|
|
|
|
|
->select('id', 'code', 'name', 'item_type', 'item_category', 'unit', 'options')
|
|
|
|
|
|
->orderBy('code')
|
|
|
|
|
|
->get();
|
|
|
|
|
|
|
|
|
|
|
|
$this->stats['items_found'] = $items->count();
|
|
|
|
|
|
$this->info(" - BD-* 품목: {$items->count()}건");
|
|
|
|
|
|
|
|
|
|
|
|
if ($items->isEmpty()) {
|
|
|
|
|
|
$this->warn('BD-* 품목이 없습니다. 종료합니다.');
|
2026-02-21 18:19:03 +09:00
|
|
|
|
|
|
|
|
|
|
return self::SUCCESS;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
// 2. item_category 미설정 품목 업데이트
|
|
|
|
|
|
$this->newLine();
|
|
|
|
|
|
$this->info('🏷️ Step 2: item_category 업데이트...');
|
|
|
|
|
|
$needsCategoryUpdate = $items->filter(fn ($item) => $item->item_category !== 'BENDING');
|
|
|
|
|
|
|
|
|
|
|
|
if ($needsCategoryUpdate->isNotEmpty()) {
|
|
|
|
|
|
$this->info(" - item_category 미설정/불일치: {$needsCategoryUpdate->count()}건");
|
|
|
|
|
|
if (! $dryRun) {
|
|
|
|
|
|
DB::connection($this->targetDb)
|
|
|
|
|
|
->table('items')
|
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
|
->where('code', 'like', 'BD-%')
|
|
|
|
|
|
->whereNull('deleted_at')
|
|
|
|
|
|
->where(function ($q) {
|
|
|
|
|
|
$q->whereNull('item_category')
|
|
|
|
|
|
->orWhere('item_category', '!=', 'BENDING');
|
|
|
|
|
|
})
|
|
|
|
|
|
->update(['item_category' => 'BENDING', 'updated_at' => now()]);
|
|
|
|
|
|
}
|
|
|
|
|
|
$this->stats['items_category_updated'] = $needsCategoryUpdate->count();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$this->info(' - 모든 품목 BENDING 카테고리 설정 완료');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 현재 재고 현황 표시
|
2026-02-21 18:19:03 +09:00
|
|
|
|
$this->newLine();
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$this->info('📊 Step 3: 현재 재고 현황...');
|
|
|
|
|
|
$this->showCurrentStockStatus($tenantId, $items);
|
2026-02-21 18:19:03 +09:00
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
// 4. 재고 셋팅 대상 확인
|
2026-02-21 18:19:03 +09:00
|
|
|
|
$this->newLine();
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$this->info('📦 Step 4: 재고 셋팅 대상 확인...');
|
|
|
|
|
|
$itemsNeedingStock = $this->getItemsNeedingStock($tenantId, $items, $minStock);
|
|
|
|
|
|
|
|
|
|
|
|
if ($itemsNeedingStock->isEmpty()) {
|
|
|
|
|
|
$this->info(" - 모든 품목이 이미 {$minStock}개 이상 재고 보유. 추가 작업 불필요.");
|
|
|
|
|
|
$this->showStats();
|
|
|
|
|
|
|
|
|
|
|
|
return self::SUCCESS;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$this->info(" - 재고 셋팅 필요: {$itemsNeedingStock->count()}건");
|
|
|
|
|
|
$this->table(
|
|
|
|
|
|
['코드', '품목명', '현재고', '목표', '추가수량'],
|
|
|
|
|
|
$itemsNeedingStock->map(fn ($item) => [
|
|
|
|
|
|
$item->code,
|
|
|
|
|
|
mb_strlen($item->name) > 30 ? mb_substr($item->name, 0, 30).'...' : $item->name,
|
|
|
|
|
|
number_format($item->current_qty),
|
|
|
|
|
|
number_format($minStock),
|
|
|
|
|
|
number_format($item->supplement_qty),
|
|
|
|
|
|
])->toArray()
|
|
|
|
|
|
);
|
2026-02-21 18:19:03 +09:00
|
|
|
|
|
|
|
|
|
|
if ($dryRun) {
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$this->stats['stocks_created'] = $itemsNeedingStock->filter(fn ($i) => ! $i->has_stock)->count();
|
|
|
|
|
|
$this->stats['lots_created'] = $itemsNeedingStock->count();
|
|
|
|
|
|
$this->stats['transactions_created'] = $itemsNeedingStock->count();
|
2026-02-21 18:19:03 +09:00
|
|
|
|
$this->showStats();
|
|
|
|
|
|
$this->info('🔍 DRY RUN 완료. 실제 실행은 --dry-run 플래그를 제거하세요.');
|
|
|
|
|
|
|
|
|
|
|
|
return self::SUCCESS;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
if (! $this->confirm('초기 재고를 셋팅하시겠습니까?')) {
|
2026-02-21 18:19:03 +09:00
|
|
|
|
$this->info('취소되었습니다.');
|
|
|
|
|
|
|
|
|
|
|
|
return self::SUCCESS;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
// 5. 실행
|
|
|
|
|
|
$this->newLine();
|
|
|
|
|
|
$this->info('🚀 Step 5: 초기 재고 셋팅 실행...');
|
|
|
|
|
|
DB::connection($this->targetDb)->transaction(function () use ($tenantId, $itemsNeedingStock, $minStock) {
|
|
|
|
|
|
$this->executeStockSetup($tenantId, $itemsNeedingStock, $minStock);
|
2026-02-21 18:19:03 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
$this->newLine();
|
|
|
|
|
|
$this->showStats();
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$this->info('✅ 초기 재고 셋팅 완료!');
|
2026-02-21 18:19:03 +09:00
|
|
|
|
|
|
|
|
|
|
return self::SUCCESS;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-22 03:04:47 +09:00
|
|
|
|
* 현재 재고 현황 표시
|
2026-02-21 18:19:03 +09:00
|
|
|
|
*/
|
2026-02-22 03:04:47 +09:00
|
|
|
|
private function showCurrentStockStatus(int $tenantId, \Illuminate\Support\Collection $items): void
|
2026-02-21 18:19:03 +09:00
|
|
|
|
{
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$itemIds = $items->pluck('id');
|
2026-02-21 18:19:03 +09:00
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$stocks = DB::connection($this->targetDb)
|
|
|
|
|
|
->table('stocks')
|
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
|
->whereIn('item_id', $itemIds)
|
|
|
|
|
|
->whereNull('deleted_at')
|
|
|
|
|
|
->get()
|
|
|
|
|
|
->keyBy('item_id');
|
2026-02-21 18:19:03 +09:00
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$hasStock = 0;
|
|
|
|
|
|
$noStock = 0;
|
2026-02-21 18:19:03 +09:00
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
foreach ($items as $item) {
|
|
|
|
|
|
$stock = $stocks->get($item->id);
|
|
|
|
|
|
if ($stock && (float) $stock->stock_qty > 0) {
|
|
|
|
|
|
$hasStock++;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$noStock++;
|
2026-02-21 18:19:03 +09:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$this->info(" - 재고 있음: {$hasStock}건");
|
|
|
|
|
|
$this->info(" - 재고 없음: {$noStock}건");
|
2026-02-21 18:19:03 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-22 03:04:47 +09:00
|
|
|
|
* 재고 셋팅이 필요한 품목 목록 조회
|
2026-02-21 18:19:03 +09:00
|
|
|
|
*/
|
2026-02-22 03:04:47 +09:00
|
|
|
|
private function getItemsNeedingStock(int $tenantId, \Illuminate\Support\Collection $items, int $minStock): \Illuminate\Support\Collection
|
2026-02-21 18:19:03 +09:00
|
|
|
|
{
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$itemIds = $items->pluck('id');
|
2026-02-21 18:19:03 +09:00
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$stocks = DB::connection($this->targetDb)
|
|
|
|
|
|
->table('stocks')
|
2026-02-21 18:19:03 +09:00
|
|
|
|
->where('tenant_id', $tenantId)
|
2026-02-22 03:04:47 +09:00
|
|
|
|
->whereIn('item_id', $itemIds)
|
2026-02-21 18:19:03 +09:00
|
|
|
|
->whereNull('deleted_at')
|
2026-02-22 03:04:47 +09:00
|
|
|
|
->get()
|
|
|
|
|
|
->keyBy('item_id');
|
2026-02-21 18:19:03 +09:00
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$result = collect();
|
2026-02-21 18:19:03 +09:00
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
foreach ($items as $item) {
|
|
|
|
|
|
$stock = $stocks->get($item->id);
|
|
|
|
|
|
$currentQty = $stock ? (float) $stock->stock_qty : 0;
|
|
|
|
|
|
|
|
|
|
|
|
if ($currentQty >= $minStock) {
|
|
|
|
|
|
$this->stats['stocks_skipped']++;
|
|
|
|
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$supplementQty = $minStock - $currentQty;
|
|
|
|
|
|
|
|
|
|
|
|
$item->has_stock = (bool) $stock;
|
|
|
|
|
|
$item->stock_id = $stock?->id;
|
|
|
|
|
|
$item->current_qty = $currentQty;
|
|
|
|
|
|
$item->supplement_qty = $supplementQty;
|
2026-02-21 18:19:03 +09:00
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$result->push($item);
|
2026-02-21 18:19:03 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
return $result;
|
2026-02-21 18:19:03 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-22 03:04:47 +09:00
|
|
|
|
* 초기 재고 셋팅 실행
|
2026-02-21 18:19:03 +09:00
|
|
|
|
*/
|
2026-02-22 03:04:47 +09:00
|
|
|
|
private function executeStockSetup(int $tenantId, \Illuminate\Support\Collection $items, int $minStock): void
|
2026-02-21 18:19:03 +09:00
|
|
|
|
{
|
2026-02-22 03:04:47 +09:00
|
|
|
|
foreach ($items as $item) {
|
|
|
|
|
|
$stockId = $item->stock_id;
|
2026-02-21 18:19:03 +09:00
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
// Stock 레코드가 없으면 생성
|
|
|
|
|
|
if (! $item->has_stock) {
|
2026-02-21 18:19:03 +09:00
|
|
|
|
$stockId = DB::connection($this->targetDb)->table('stocks')->insertGetId([
|
|
|
|
|
|
'tenant_id' => $tenantId,
|
2026-02-22 03:04:47 +09:00
|
|
|
|
'item_id' => $item->id,
|
|
|
|
|
|
'item_code' => $item->code,
|
|
|
|
|
|
'item_name' => $item->name,
|
2026-02-21 18:19:03 +09:00
|
|
|
|
'item_type' => 'bent_part',
|
2026-02-22 03:04:47 +09:00
|
|
|
|
'unit' => $item->unit ?? 'EA',
|
2026-02-21 18:19:03 +09:00
|
|
|
|
'stock_qty' => 0,
|
|
|
|
|
|
'safety_stock' => 0,
|
|
|
|
|
|
'reserved_qty' => 0,
|
|
|
|
|
|
'available_qty' => 0,
|
|
|
|
|
|
'lot_count' => 0,
|
|
|
|
|
|
'status' => 'out',
|
|
|
|
|
|
'created_at' => now(),
|
|
|
|
|
|
'updated_at' => now(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
$this->stats['stocks_created']++;
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$this->line(" + Stock 생성: {$item->code}");
|
2026-02-21 18:19:03 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// FIFO 순서 계산
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$maxFifo = DB::connection($this->targetDb)
|
|
|
|
|
|
->table('stock_lots')
|
|
|
|
|
|
->where('stock_id', $stockId)
|
|
|
|
|
|
->max('fifo_order');
|
|
|
|
|
|
$nextFifo = ($maxFifo ?? 0) + 1;
|
2026-02-21 18:19:03 +09:00
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
// LOT 번호 생성
|
|
|
|
|
|
$lotNo = 'INIT-'.now()->format('ymd').'-'.str_replace(['-', ' ', '*'], ['', '', 'x'], $item->code);
|
2026-02-21 18:19:03 +09:00
|
|
|
|
|
|
|
|
|
|
// 중복 체크
|
|
|
|
|
|
$existingLot = DB::connection($this->targetDb)
|
|
|
|
|
|
->table('stock_lots')
|
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
|
->where('stock_id', $stockId)
|
|
|
|
|
|
->where('lot_no', $lotNo)
|
|
|
|
|
|
->whereNull('deleted_at')
|
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
|
|
if ($existingLot) {
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$this->warn(" ⚠️ 이미 LOT 존재 (skip): {$lotNo}");
|
2026-02-21 18:19:03 +09:00
|
|
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$supplementQty = $item->supplement_qty;
|
2026-02-21 18:19:03 +09:00
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
// StockLot 생성
|
2026-02-21 18:19:03 +09:00
|
|
|
|
$stockLotId = DB::connection($this->targetDb)->table('stock_lots')->insertGetId([
|
|
|
|
|
|
'tenant_id' => $tenantId,
|
|
|
|
|
|
'stock_id' => $stockId,
|
|
|
|
|
|
'lot_no' => $lotNo,
|
2026-02-22 03:04:47 +09:00
|
|
|
|
'fifo_order' => $nextFifo,
|
|
|
|
|
|
'receipt_date' => now()->toDateString(),
|
|
|
|
|
|
'qty' => $supplementQty,
|
2026-02-21 18:19:03 +09:00
|
|
|
|
'reserved_qty' => 0,
|
2026-02-22 03:04:47 +09:00
|
|
|
|
'available_qty' => $supplementQty,
|
|
|
|
|
|
'unit' => $item->unit ?? 'EA',
|
2026-02-21 18:19:03 +09:00
|
|
|
|
'status' => 'available',
|
|
|
|
|
|
'created_at' => now(),
|
|
|
|
|
|
'updated_at' => now(),
|
|
|
|
|
|
]);
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$this->stats['lots_created']++;
|
2026-02-21 18:19:03 +09:00
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
// StockTransaction 생성
|
2026-02-21 18:19:03 +09:00
|
|
|
|
DB::connection($this->targetDb)->table('stock_transactions')->insert([
|
|
|
|
|
|
'tenant_id' => $tenantId,
|
|
|
|
|
|
'stock_id' => $stockId,
|
|
|
|
|
|
'stock_lot_id' => $stockLotId,
|
|
|
|
|
|
'type' => 'IN',
|
2026-02-22 03:04:47 +09:00
|
|
|
|
'qty' => $supplementQty,
|
|
|
|
|
|
'balance_qty' => 0,
|
|
|
|
|
|
'reference_type' => 'init_stock',
|
|
|
|
|
|
'reference_id' => 0,
|
2026-02-21 18:19:03 +09:00
|
|
|
|
'lot_no' => $lotNo,
|
|
|
|
|
|
'reason' => 'receiving',
|
2026-02-22 03:04:47 +09:00
|
|
|
|
'remark' => "절곡품 초기 재고 셋팅 (min-stock={$minStock})",
|
|
|
|
|
|
'item_code' => $item->code,
|
|
|
|
|
|
'item_name' => $item->name,
|
|
|
|
|
|
'created_at' => now(),
|
2026-02-21 18:19:03 +09:00
|
|
|
|
]);
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$this->stats['transactions_created']++;
|
2026-02-21 18:19:03 +09:00
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
// Stock 집계 갱신
|
2026-02-21 18:19:03 +09:00
|
|
|
|
$this->refreshStockFromLots($stockId, $tenantId);
|
|
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$this->line(" ✅ {$item->code}: 0 → {$supplementQty} (+{$supplementQty})");
|
2026-02-21 18:19:03 +09:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Stock 집계 갱신 (LOT 기반)
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function refreshStockFromLots(int $stockId, int $tenantId): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$lotStats = DB::connection($this->targetDb)
|
|
|
|
|
|
->table('stock_lots')
|
|
|
|
|
|
->where('stock_id', $stockId)
|
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
|
->whereNull('deleted_at')
|
|
|
|
|
|
->selectRaw('
|
|
|
|
|
|
COALESCE(SUM(qty), 0) as total_qty,
|
|
|
|
|
|
COALESCE(SUM(reserved_qty), 0) as total_reserved,
|
|
|
|
|
|
COALESCE(SUM(available_qty), 0) as total_available,
|
|
|
|
|
|
COUNT(*) as lot_count,
|
|
|
|
|
|
MIN(receipt_date) as oldest_lot_date,
|
|
|
|
|
|
MAX(receipt_date) as latest_receipt_date
|
|
|
|
|
|
')
|
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
|
|
$stockQty = (float) $lotStats->total_qty;
|
|
|
|
|
|
|
|
|
|
|
|
DB::connection($this->targetDb)
|
|
|
|
|
|
->table('stocks')
|
|
|
|
|
|
->where('id', $stockId)
|
|
|
|
|
|
->update([
|
|
|
|
|
|
'stock_qty' => $stockQty,
|
2026-02-22 03:04:47 +09:00
|
|
|
|
'reserved_qty' => (float) $lotStats->total_reserved,
|
|
|
|
|
|
'available_qty' => (float) $lotStats->total_available,
|
2026-02-21 18:19:03 +09:00
|
|
|
|
'lot_count' => (int) $lotStats->lot_count,
|
|
|
|
|
|
'oldest_lot_date' => $lotStats->oldest_lot_date,
|
|
|
|
|
|
'last_receipt_date' => $lotStats->latest_receipt_date,
|
2026-02-22 03:04:47 +09:00
|
|
|
|
'status' => $stockQty > 0 ? 'normal' : 'out',
|
2026-02-21 18:19:03 +09:00
|
|
|
|
'updated_at' => now(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-22 03:04:47 +09:00
|
|
|
|
* 롤백: init_stock 참조 데이터 삭제
|
2026-02-21 18:19:03 +09:00
|
|
|
|
*/
|
2026-02-22 03:04:47 +09:00
|
|
|
|
private function rollbackInitStock(int $tenantId, bool $dryRun): int
|
2026-02-21 18:19:03 +09:00
|
|
|
|
{
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$this->warn('⚠️ 롤백: 초기 재고 셋팅 데이터를 삭제합니다.');
|
2026-02-21 18:19:03 +09:00
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
// init_stock으로 생성된 트랜잭션
|
2026-02-21 18:19:03 +09:00
|
|
|
|
$txCount = DB::connection($this->targetDb)
|
|
|
|
|
|
->table('stock_transactions')
|
|
|
|
|
|
->where('tenant_id', $tenantId)
|
2026-02-22 03:04:47 +09:00
|
|
|
|
->where('reference_type', 'init_stock')
|
2026-02-21 18:19:03 +09:00
|
|
|
|
->count();
|
|
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
// init_stock 트랜잭션에 연결된 LOT
|
|
|
|
|
|
$lotIds = DB::connection($this->targetDb)
|
|
|
|
|
|
->table('stock_transactions')
|
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
|
->where('reference_type', 'init_stock')
|
|
|
|
|
|
->whereNotNull('stock_lot_id')
|
|
|
|
|
|
->pluck('stock_lot_id')
|
|
|
|
|
|
->unique();
|
|
|
|
|
|
|
|
|
|
|
|
// 5130으로 생성된 아이템
|
|
|
|
|
|
$legacyItemCount = DB::connection($this->targetDb)
|
2026-02-21 18:19:03 +09:00
|
|
|
|
->table('items')
|
|
|
|
|
|
->where('tenant_id', $tenantId)
|
2026-02-22 03:04:47 +09:00
|
|
|
|
->where('options->source', '5130_migration')
|
2026-02-21 18:19:03 +09:00
|
|
|
|
->whereNull('deleted_at')
|
|
|
|
|
|
->count();
|
|
|
|
|
|
|
|
|
|
|
|
$this->info(' 삭제 대상:');
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$this->info(" - stock_transactions (reference_type=init_stock): {$txCount}건");
|
|
|
|
|
|
$this->info(" - stock_lots (연결 LOT): {$lotIds->count()}건");
|
|
|
|
|
|
$this->info(" - items (source=5130_migration): {$legacyItemCount}건");
|
2026-02-21 18:19:03 +09:00
|
|
|
|
|
|
|
|
|
|
if ($dryRun) {
|
|
|
|
|
|
$this->info('DRY RUN - 실제 삭제 없음');
|
|
|
|
|
|
|
|
|
|
|
|
return self::SUCCESS;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (! $this->confirm('정말 롤백하시겠습니까? 되돌릴 수 없습니다.')) {
|
|
|
|
|
|
return self::SUCCESS;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
DB::connection($this->targetDb)->transaction(function () use ($tenantId, $lotIds) {
|
2026-02-21 18:19:03 +09:00
|
|
|
|
// 1. 트랜잭션 삭제
|
|
|
|
|
|
DB::connection($this->targetDb)
|
|
|
|
|
|
->table('stock_transactions')
|
|
|
|
|
|
->where('tenant_id', $tenantId)
|
2026-02-22 03:04:47 +09:00
|
|
|
|
->where('reference_type', 'init_stock')
|
2026-02-21 18:19:03 +09:00
|
|
|
|
->delete();
|
|
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
// 2. LOT에서 stock_id 목록 수집 (집계 갱신용)
|
|
|
|
|
|
$affectedStockIds = collect();
|
|
|
|
|
|
if ($lotIds->isNotEmpty()) {
|
|
|
|
|
|
$affectedStockIds = DB::connection($this->targetDb)
|
|
|
|
|
|
->table('stock_lots')
|
|
|
|
|
|
->whereIn('id', $lotIds)
|
|
|
|
|
|
->pluck('stock_id')
|
|
|
|
|
|
->unique();
|
|
|
|
|
|
|
|
|
|
|
|
// LOT 삭제
|
|
|
|
|
|
DB::connection($this->targetDb)
|
|
|
|
|
|
->table('stock_lots')
|
|
|
|
|
|
->whereIn('id', $lotIds)
|
|
|
|
|
|
->delete();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 영향받은 Stock 집계 갱신
|
|
|
|
|
|
foreach ($affectedStockIds as $stockId) {
|
|
|
|
|
|
$this->refreshStockFromLots($stockId, $tenantId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 5130 migration으로 생성된 아이템 + 연결 stocks 삭제
|
2026-02-21 18:19:03 +09:00
|
|
|
|
$migrationItemIds = DB::connection($this->targetDb)
|
|
|
|
|
|
->table('items')
|
|
|
|
|
|
->where('tenant_id', $tenantId)
|
2026-02-22 03:04:47 +09:00
|
|
|
|
->where('options->source', '5130_migration')
|
2026-02-21 18:19:03 +09:00
|
|
|
|
->whereNull('deleted_at')
|
|
|
|
|
|
->pluck('id');
|
|
|
|
|
|
|
|
|
|
|
|
if ($migrationItemIds->isNotEmpty()) {
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$migrationStockIds = DB::connection($this->targetDb)
|
2026-02-21 18:19:03 +09:00
|
|
|
|
->table('stocks')
|
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
|
->whereIn('item_id', $migrationItemIds)
|
|
|
|
|
|
->pluck('id');
|
|
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
if ($migrationStockIds->isNotEmpty()) {
|
2026-02-21 18:19:03 +09:00
|
|
|
|
DB::connection($this->targetDb)
|
|
|
|
|
|
->table('stock_lots')
|
2026-02-22 03:04:47 +09:00
|
|
|
|
->whereIn('stock_id', $migrationStockIds)
|
2026-02-21 18:19:03 +09:00
|
|
|
|
->delete();
|
|
|
|
|
|
|
|
|
|
|
|
DB::connection($this->targetDb)
|
|
|
|
|
|
->table('stocks')
|
2026-02-22 03:04:47 +09:00
|
|
|
|
->whereIn('id', $migrationStockIds)
|
2026-02-21 18:19:03 +09:00
|
|
|
|
->delete();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
DB::connection($this->targetDb)
|
|
|
|
|
|
->table('items')
|
|
|
|
|
|
->whereIn('id', $migrationItemIds)
|
|
|
|
|
|
->delete();
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
$this->info('✅ 롤백 완료');
|
|
|
|
|
|
|
|
|
|
|
|
return self::SUCCESS;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-22 03:04:47 +09:00
|
|
|
|
* 5130 레거시 데이터에서 BD-{PROD}{SPEC}-{SLENGTH} 아이템 생성
|
2026-02-21 18:19:03 +09:00
|
|
|
|
*/
|
2026-02-22 03:04:47 +09:00
|
|
|
|
private function createLegacyItems(int $tenantId, bool $dryRun): void
|
2026-02-21 18:19:03 +09:00
|
|
|
|
{
|
2026-02-22 03:04:47 +09:00
|
|
|
|
// 5130 lot 테이블에서 고유 prod+spec+slength 조합 추출
|
|
|
|
|
|
$lots = DB::connection($this->sourceDb)
|
|
|
|
|
|
->table('lot')
|
|
|
|
|
|
->where(function ($q) {
|
|
|
|
|
|
$q->whereNull('is_deleted')
|
|
|
|
|
|
->orWhere('is_deleted', 0);
|
|
|
|
|
|
})
|
|
|
|
|
|
->whereNotNull('prod')
|
|
|
|
|
|
->where('prod', '!=', '')
|
|
|
|
|
|
->whereNotNull('surang')
|
|
|
|
|
|
->where('surang', '>', 0)
|
|
|
|
|
|
->select('prod', 'spec', 'slength')
|
|
|
|
|
|
->distinct()
|
|
|
|
|
|
->get();
|
2026-02-21 18:19:03 +09:00
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
// bending_work_log 테이블에서도 추출 (lot에 없는 조합 포함)
|
|
|
|
|
|
$workLogs = DB::connection($this->sourceDb)
|
|
|
|
|
|
->table('bending_work_log')
|
|
|
|
|
|
->where(function ($q) {
|
|
|
|
|
|
$q->whereNull('is_deleted')
|
|
|
|
|
|
->orWhere('is_deleted', 0);
|
|
|
|
|
|
})
|
|
|
|
|
|
->whereNotNull('prod_code')
|
|
|
|
|
|
->where('prod_code', '!=', '')
|
|
|
|
|
|
->select('prod_code as prod', 'spec_code as spec', 'slength_code as slength')
|
|
|
|
|
|
->distinct()
|
|
|
|
|
|
->get();
|
2026-02-21 18:19:03 +09:00
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$allRecords = $lots->merge($workLogs);
|
2026-02-21 18:19:03 +09:00
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
if ($allRecords->isEmpty()) {
|
|
|
|
|
|
$this->info(' - 5130 데이터 없음');
|
|
|
|
|
|
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-02-21 18:19:03 +09:00
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
// 고유 제품 조합 추출
|
|
|
|
|
|
$uniqueProducts = [];
|
|
|
|
|
|
foreach ($allRecords as $row) {
|
|
|
|
|
|
$key = trim($row->prod).'-'.trim($row->spec ?? '').'-'.trim($row->slength ?? '');
|
|
|
|
|
|
if (! isset($uniqueProducts[$key])) {
|
|
|
|
|
|
$uniqueProducts[$key] = [
|
|
|
|
|
|
'prod' => trim($row->prod),
|
|
|
|
|
|
'spec' => trim($row->spec ?? ''),
|
|
|
|
|
|
'slength' => trim($row->slength ?? ''),
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$this->info(" - 5130 고유 제품 조합: ".count($uniqueProducts).'개');
|
|
|
|
|
|
|
|
|
|
|
|
$created = 0;
|
|
|
|
|
|
$skipped = 0;
|
|
|
|
|
|
|
|
|
|
|
|
foreach ($uniqueProducts as $data) {
|
|
|
|
|
|
$itemCode = "BD-{$data['prod']}{$data['spec']}-{$data['slength']}";
|
|
|
|
|
|
$prodName = $this->prodNames[$data['prod']] ?? $data['prod'];
|
|
|
|
|
|
$specName = $this->specNames[$data['spec']] ?? $data['spec'];
|
|
|
|
|
|
$slengthName = $this->slengthNames[$data['slength']] ?? $data['slength'];
|
|
|
|
|
|
$itemName = implode(' ', array_filter([$prodName, $specName, $slengthName]));
|
|
|
|
|
|
|
|
|
|
|
|
// 이미 존재하는지 확인
|
|
|
|
|
|
$existing = DB::connection($this->targetDb)
|
|
|
|
|
|
->table('items')
|
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
|
->where('code', $itemCode)
|
|
|
|
|
|
->whereNull('deleted_at')
|
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
|
|
if ($existing) {
|
|
|
|
|
|
$skipped++;
|
|
|
|
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (! $dryRun) {
|
|
|
|
|
|
DB::connection($this->targetDb)->table('items')->insert([
|
|
|
|
|
|
'tenant_id' => $tenantId,
|
|
|
|
|
|
'code' => $itemCode,
|
|
|
|
|
|
'name' => $itemName,
|
|
|
|
|
|
'item_type' => 'PT',
|
|
|
|
|
|
'item_category' => 'BENDING',
|
|
|
|
|
|
'unit' => 'EA',
|
|
|
|
|
|
'options' => json_encode([
|
|
|
|
|
|
'source' => '5130_migration',
|
|
|
|
|
|
'lot_managed' => true,
|
|
|
|
|
|
'consumption_method' => 'auto',
|
|
|
|
|
|
'production_source' => 'self_produced',
|
|
|
|
|
|
'input_tracking' => true,
|
|
|
|
|
|
'legacy_prod' => $data['prod'],
|
|
|
|
|
|
'legacy_spec' => $data['spec'],
|
|
|
|
|
|
'legacy_slength' => $data['slength'],
|
|
|
|
|
|
]),
|
|
|
|
|
|
'is_active' => true,
|
|
|
|
|
|
'created_at' => now(),
|
|
|
|
|
|
'updated_at' => now(),
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$created++;
|
|
|
|
|
|
}
|
2026-02-21 18:19:03 +09:00
|
|
|
|
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$this->stats['items_created_5130'] = $created;
|
|
|
|
|
|
$this->info(" - 신규 생성: {$created}건, 기존 존재 (skip): {$skipped}건");
|
2026-02-21 18:19:03 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 통계 출력
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function showStats(): void
|
|
|
|
|
|
{
|
|
|
|
|
|
$this->newLine();
|
|
|
|
|
|
$this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$this->info('📊 실행 통계');
|
2026-02-21 18:19:03 +09:00
|
|
|
|
$this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$this->info(" 5130 아이템 생성: {$this->stats['items_created_5130']}건");
|
|
|
|
|
|
$this->info(" BD-* 품목 수 (전체): {$this->stats['items_found']}건");
|
|
|
|
|
|
$this->info(" 카테고리 업데이트: {$this->stats['items_category_updated']}건");
|
2026-02-21 18:19:03 +09:00
|
|
|
|
$this->info(" Stock 레코드 생성: {$this->stats['stocks_created']}건");
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$this->info(" 기존 재고 충분 (skip): {$this->stats['stocks_skipped']}건");
|
2026-02-21 18:19:03 +09:00
|
|
|
|
$this->info(" StockLot 생성: {$this->stats['lots_created']}건");
|
2026-02-22 03:04:47 +09:00
|
|
|
|
$this->info(" 입고 트랜잭션: {$this->stats['transactions_created']}건");
|
2026-02-21 18:19:03 +09:00
|
|
|
|
$this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
|
|
|
|
}
|
2026-02-22 03:04:47 +09:00
|
|
|
|
}
|