Files
sam-api/app/Console/Commands/Migrate5130BendingStock.php
권혁성 9c88138de8 feat(WEB): 5130 레거시 절곡품 재고 마이그레이션 커맨드 추가
- php artisan migrate:5130-bending-stock 커맨드 생성
- 5130 lot 테이블 → SAM stocks + stock_lots 마이그레이션
- 5130 bending_work_log → SAM stock_transactions(OUT) 마이그레이션
- prod+spec+slength 3코드 → BD-{PROD}{SPEC}-{SLENGTH} 아이템 코드 매핑
- --dry-run 시뮬레이션, --rollback 롤백 지원
- 기존 BD- 아이템 item_category='BENDING' 자동 업데이트
- FIFO 기반 LOT 수량 차감 및 Stock 집계 갱신

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 04:19:47 +09:00

774 lines
28 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 App\Console\Commands;
use Illuminate\Console\Attributes\AsCommand;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
#[AsCommand(name: 'migrate:5130-bending-stock', description: '5130 레거시 절곡품 재고(lot, bending_work_log)를 SAM stocks/stock_lots/stock_transactions로 마이그레이션')]
class Migrate5130BendingStock extends Command
{
protected $signature = 'migrate:5130-bending-stock
{--tenant_id=287 : Target tenant ID (default: 287 경동기업)}
{--dry-run : 실제 저장 없이 시뮬레이션만 수행}
{--rollback : 마이그레이션 롤백 (migration 소스 데이터 삭제)}';
private string $sourceDb = 'chandj';
private string $targetDb = 'mysql';
// 5130 prod 코드 → 한글명
private array $prodNames = [
'R' => '가이드레일(벽면)',
'S' => '가이드레일(측면)',
'G' => '연기차단재',
'B' => '하단마감재(스크린)',
'T' => '하단마감재(철재)',
'L' => 'L-Bar',
'C' => '케이스',
];
// 5130 spec 코드 → 한글명
private array $specNames = [
'I' => '화이바원단',
'S' => 'SUS',
'U' => 'SUS2',
'E' => 'EGI',
'A' => '스크린용',
'D' => 'D형',
'C' => 'C형',
'M' => '본체',
'T' => '본체(철재)',
'B' => '후면코너부',
'L' => '린텔부',
'P' => '점검구',
'F' => '전면부',
];
// 5130 slength 코드 → 한글명
private array $slengthNames = [
'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',
];
// 통계
private array $stats = [
'items_updated' => 0,
'items_created' => 0,
'stocks_created' => 0,
'lots_created' => 0,
'transactions_in' => 0,
'transactions_out' => 0,
'skipped_lots' => 0,
];
public function handle(): int
{
$tenantId = (int) $this->option('tenant_id');
$dryRun = $this->option('dry-run');
$rollback = $this->option('rollback');
$this->info('=== 5130 → SAM 절곡품 재고 마이그레이션 ===');
$this->info("Tenant ID: {$tenantId}");
$this->info('Mode: '.($dryRun ? 'DRY-RUN (시뮬레이션)' : 'LIVE'));
$this->newLine();
if ($rollback) {
return $this->rollbackMigration($tenantId, $dryRun);
}
// 1. 5130 데이터 읽기
$this->info('📥 Step 1: 5130 레거시 데이터 읽기...');
$lots = $this->readLegacyLots();
$workLogs = $this->readLegacyWorkLogs();
$this->info(" - lot 레코드: {$lots->count()}");
$this->info(" - bending_work_log 레코드: {$workLogs->count()}");
$this->newLine();
if ($lots->isEmpty()) {
$this->warn('5130 lot 데이터가 없습니다. 마이그레이션을 종료합니다.');
return self::SUCCESS;
}
// 2. 재고 요약 계산
$this->info('📊 Step 2: 재고 현황 계산...');
$stockSummary = $this->calculateStockSummary($lots, $workLogs);
$this->showSummary($stockSummary);
$this->newLine();
// 3. 기존 BD- 아이템 item_category 업데이트 현황
$this->info('🏷️ Step 3: 기존 BD- 아이템 카테고리 업데이트...');
$this->updateExistingBdItems($tenantId, $dryRun);
$this->newLine();
if ($dryRun) {
$this->showStats();
$this->info('🔍 DRY RUN 완료. 실제 실행은 --dry-run 플래그를 제거하세요.');
return self::SUCCESS;
}
if (! $this->confirm('마이그레이션을 진행하시겠습니까?')) {
$this->info('취소되었습니다.');
return self::SUCCESS;
}
// 4. 실행
$this->info('🚀 Step 4: 마이그레이션 실행...');
DB::connection($this->targetDb)->transaction(function () use ($tenantId, $lots, $workLogs, $stockSummary) {
$this->executeMigration($tenantId, $lots, $workLogs, $stockSummary);
});
$this->newLine();
$this->showStats();
$this->info('✅ 마이그레이션 완료!');
return self::SUCCESS;
}
/**
* 5130 lot 테이블 데이터 읽기
*/
private function readLegacyLots(): \Illuminate\Support\Collection
{
return DB::connection($this->sourceDb)
->table('lot')
->where('is_deleted', 0)
->whereNotNull('prod')
->where('prod', '!=', '')
->whereNotNull('surang')
->where('surang', '>', 0)
->select('num', 'reg_date', 'lot_number', 'prod', 'spec', 'slength', 'surang', 'rawLot', 'author', 'remark')
->orderBy('reg_date')
->orderBy('num')
->get();
}
/**
* 5130 bending_work_log 테이블 데이터 읽기
*/
private function readLegacyWorkLogs(): \Illuminate\Support\Collection
{
return DB::connection($this->sourceDb)
->table('bending_work_log')
->where('is_deleted', 0)
->whereNotNull('prod_code')
->where('prod_code', '!=', '')
->where('quantity', '>', 0)
->select('id', 'work_date', 'work_order_no', 'prod_code', 'spec_code', 'slength_code', 'quantity', 'unit', 'work_type', 'worker', 'remark', 'created_at')
->orderBy('work_date')
->orderBy('id')
->get();
}
/**
* prod+spec+slength 기준 재고 요약 계산
*/
private function calculateStockSummary(\Illuminate\Support\Collection $lots, \Illuminate\Support\Collection $workLogs): array
{
$summary = [];
// 입고(lot) 합산
foreach ($lots as $lot) {
$key = $this->makeProductKey($lot->prod, $lot->spec, $lot->slength);
if (! isset($summary[$key])) {
$summary[$key] = [
'prod' => $lot->prod,
'spec' => $lot->spec,
'slength' => $lot->slength,
'total_in' => 0,
'total_out' => 0,
'lot_count' => 0,
];
}
$summary[$key]['total_in'] += (float) $lot->surang;
$summary[$key]['lot_count']++;
}
// 출고(bending_work_log) 합산
foreach ($workLogs as $log) {
$key = $this->makeProductKey($log->prod_code, $log->spec_code, $log->slength_code);
if (! isset($summary[$key])) {
$summary[$key] = [
'prod' => $log->prod_code,
'spec' => $log->spec_code,
'slength' => $log->slength_code,
'total_in' => 0,
'total_out' => 0,
'lot_count' => 0,
];
}
$summary[$key]['total_out'] += (float) $log->quantity;
}
return $summary;
}
/**
* 재고 요약 표시
*/
private function showSummary(array $summary): void
{
$headers = ['코드', '품목명', '입고합계', '출고합계', '현재고', 'LOT수'];
$rows = [];
foreach ($summary as $key => $item) {
$netStock = $item['total_in'] - $item['total_out'];
$rows[] = [
$key,
$this->makeItemName($item['prod'], $item['spec'], $item['slength']),
number_format($item['total_in']),
number_format($item['total_out']),
number_format($netStock),
$item['lot_count'],
];
}
$this->table($headers, $rows);
$this->info(' - 품목 종류: '.count($summary).'개');
}
/**
* 기존 BD- 아이템의 item_category를 BENDING으로 업데이트
*/
private function updateExistingBdItems(int $tenantId, bool $dryRun): void
{
$count = 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');
})
->count();
$this->info(" - BD- 아이템 중 item_category 미설정: {$count}");
if ($count > 0 && ! $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->info("{$count}건 업데이트 완료");
}
$this->stats['items_updated'] = $count;
}
/**
* 마이그레이션 실행
*/
private function executeMigration(int $tenantId, \Illuminate\Support\Collection $lots, \Illuminate\Support\Collection $workLogs, array $stockSummary): void
{
$itemMap = []; // productKey => item_id
$stockMap = []; // productKey => stock_id
$lotMap = []; // productKey => [stock_lot_ids]
// 1. 각 제품 조합별 아이템 & 스톡 생성
$this->info(' 📦 아이템 & 재고 레코드 생성...');
foreach ($stockSummary as $key => $data) {
$itemCode = $this->makeItemCode($data['prod'], $data['spec'], $data['slength']);
$itemName = $this->makeItemName($data['prod'], $data['spec'], $data['slength']);
// 아이템 조회 또는 생성
$item = DB::connection($this->targetDb)
->table('items')
->where('tenant_id', $tenantId)
->where('code', $itemCode)
->whereNull('deleted_at')
->first();
if (! $item) {
$itemId = DB::connection($this->targetDb)->table('items')->insertGetId([
'tenant_id' => $tenantId,
'code' => $itemCode,
'name' => $itemName,
'item_type' => 'PT',
'item_category' => 'BENDING',
'unit' => 'EA',
'source' => '5130_migration',
'options' => json_encode([
'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(),
]);
$this->stats['items_created']++;
$this->line(" + 아이템 생성: {$itemCode} ({$itemName})");
} else {
$itemId = $item->id;
$this->line(" ✓ 기존 아이템 사용: {$itemCode}");
}
$itemMap[$key] = $itemId;
// Stock 레코드 조회 또는 생성
$stock = DB::connection($this->targetDb)
->table('stocks')
->where('tenant_id', $tenantId)
->where('item_id', $itemId)
->whereNull('deleted_at')
->first();
if (! $stock) {
$stockId = DB::connection($this->targetDb)->table('stocks')->insertGetId([
'tenant_id' => $tenantId,
'item_id' => $itemId,
'item_code' => $itemCode,
'item_name' => $itemName,
'item_type' => 'bent_part',
'unit' => 'EA',
'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']++;
} else {
$stockId = $stock->id;
}
$stockMap[$key] = $stockId;
}
// 2. LOT 데이터 마이그레이션 (입고)
$this->newLine();
$this->info(' 📋 LOT 데이터 마이그레이션 (입고)...');
$fifoCounters = []; // stockId => current fifo_order
foreach ($lots as $lot) {
$key = $this->makeProductKey($lot->prod, $lot->spec, $lot->slength);
if (! isset($stockMap[$key])) {
$this->stats['skipped_lots']++;
continue;
}
$stockId = $stockMap[$key];
$itemCode = $this->makeItemCode($lot->prod, $lot->spec, $lot->slength);
$itemName = $this->makeItemName($lot->prod, $lot->spec, $lot->slength);
// FIFO 순서 계산
if (! isset($fifoCounters[$stockId])) {
$existing = DB::connection($this->targetDb)
->table('stock_lots')
->where('stock_id', $stockId)
->max('fifo_order');
$fifoCounters[$stockId] = ($existing ?? 0);
}
$fifoCounters[$stockId]++;
// LOT 번호 생성 (5130 lot_number 사용, 없으면 생성)
$lotNo = $lot->lot_number;
if (empty($lotNo)) {
$regDate = $lot->reg_date ? Carbon::parse($lot->reg_date)->format('ymd') : Carbon::now()->format('ymd');
$lotNo = "5130-{$regDate}-{$lot->num}";
}
// 중복 체크
$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) {
$this->stats['skipped_lots']++;
continue;
}
$qty = (float) $lot->surang;
$receiptDate = $lot->reg_date ?? now()->toDateString();
$stockLotId = DB::connection($this->targetDb)->table('stock_lots')->insertGetId([
'tenant_id' => $tenantId,
'stock_id' => $stockId,
'lot_no' => $lotNo,
'fifo_order' => $fifoCounters[$stockId],
'receipt_date' => $receiptDate,
'qty' => $qty,
'reserved_qty' => 0,
'available_qty' => $qty,
'unit' => 'EA',
'supplier' => null,
'supplier_lot' => $lot->rawLot ?: null,
'location' => null,
'status' => 'available',
'receiving_id' => null,
'work_order_id' => null,
'created_at' => now(),
'updated_at' => now(),
]);
// 입고 트랜잭션 기록
// balance_qty는 나중에 refreshFromLots 후 업데이트되므로 임시로 qty 사용
DB::connection($this->targetDb)->table('stock_transactions')->insert([
'tenant_id' => $tenantId,
'stock_id' => $stockId,
'stock_lot_id' => $stockLotId,
'type' => 'IN',
'qty' => $qty,
'balance_qty' => 0, // 나중에 갱신
'reference_type' => 'migration',
'reference_id' => $lot->num,
'lot_no' => $lotNo,
'reason' => 'receiving',
'remark' => '5130 레거시 마이그레이션 (lot.num='.$lot->num.')',
'item_code' => $itemCode,
'item_name' => $itemName,
'created_at' => $receiptDate,
]);
$this->stats['lots_created']++;
$this->stats['transactions_in']++;
if (! isset($lotMap[$key])) {
$lotMap[$key] = [];
}
$lotMap[$key][] = $stockLotId;
}
$this->info(" ✅ LOT {$this->stats['lots_created']}건 생성");
// 3. 출고 로그 마이그레이션 (bending_work_log → stock_transactions OUT)
$this->newLine();
$this->info(' 📋 출고 로그 마이그레이션...');
foreach ($workLogs as $log) {
$key = $this->makeProductKey($log->prod_code, $log->spec_code, $log->slength_code);
if (! isset($stockMap[$key])) {
continue;
}
$stockId = $stockMap[$key];
$itemCode = $this->makeItemCode($log->prod_code, $log->spec_code, $log->slength_code);
$itemName = $this->makeItemName($log->prod_code, $log->spec_code, $log->slength_code);
$qty = (float) $log->quantity;
$workDate = $log->work_date ?? ($log->created_at ?? now()->toDateString());
DB::connection($this->targetDb)->table('stock_transactions')->insert([
'tenant_id' => $tenantId,
'stock_id' => $stockId,
'stock_lot_id' => null, // 레거시 데이터는 특정 LOT 추적 불가
'type' => 'OUT',
'qty' => -$qty,
'balance_qty' => 0, // 나중에 갱신
'reference_type' => 'migration',
'reference_id' => $log->id,
'lot_no' => null,
'reason' => 'work_order_input',
'remark' => '5130 레거시 마이그레이션 (bending_work_log.id='.$log->id.', worker='.$log->worker.')',
'item_code' => $itemCode,
'item_name' => $itemName,
'created_at' => $workDate,
]);
$this->stats['transactions_out']++;
}
$this->info(" ✅ 출고 트랜잭션 {$this->stats['transactions_out']}건 생성");
// 4. LOT 수량 조정 (출고분 차감)
$this->newLine();
$this->info(' 🔄 LOT 수량 조정 (출고분 FIFO 차감)...');
foreach ($stockSummary as $key => $data) {
if (! isset($stockMap[$key])) {
continue;
}
$stockId = $stockMap[$key];
$totalOut = $data['total_out'];
if ($totalOut <= 0) {
continue;
}
// FIFO 순서로 LOT에서 차감
$remainingOut = $totalOut;
$stockLots = DB::connection($this->targetDb)
->table('stock_lots')
->where('stock_id', $stockId)
->where('tenant_id', $tenantId)
->whereNull('deleted_at')
->orderBy('fifo_order')
->get();
foreach ($stockLots as $sl) {
if ($remainingOut <= 0) {
break;
}
$lotQty = (float) $sl->qty;
$deduct = min($lotQty, $remainingOut);
$newQty = $lotQty - $deduct;
$remainingOut -= $deduct;
$status = $newQty <= 0 ? 'used' : 'available';
DB::connection($this->targetDb)
->table('stock_lots')
->where('id', $sl->id)
->update([
'qty' => max(0, $newQty),
'available_qty' => max(0, $newQty),
'status' => $status,
'updated_at' => now(),
]);
}
if ($remainingOut > 0) {
$this->warn(" ⚠️ {$key}: 출고량이 입고량보다 {$remainingOut}만큼 초과 (데이터 불일치)");
}
}
// 5. Stock 집계 갱신
$this->newLine();
$this->info(' 📊 Stock 집계 갱신...');
foreach ($stockMap as $key => $stockId) {
$this->refreshStockFromLots($stockId, $tenantId);
}
// 6. balance_qty 갱신 (트랜잭션별 잔량)
$this->info(' 📊 트랜잭션 잔량(balance_qty) 갱신...');
foreach ($stockMap as $key => $stockId) {
$stock = DB::connection($this->targetDb)
->table('stocks')
->where('id', $stockId)
->first();
if ($stock) {
DB::connection($this->targetDb)
->table('stock_transactions')
->where('stock_id', $stockId)
->where('tenant_id', $tenantId)
->update(['balance_qty' => $stock->stock_qty]);
}
}
$this->info(' ✅ 집계 갱신 완료');
}
/**
* 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;
$reservedQty = (float) $lotStats->total_reserved;
$availableQty = (float) $lotStats->total_available;
$status = 'normal';
if ($stockQty <= 0) {
$status = 'out';
}
DB::connection($this->targetDb)
->table('stocks')
->where('id', $stockId)
->update([
'stock_qty' => $stockQty,
'reserved_qty' => $reservedQty,
'available_qty' => $availableQty,
'lot_count' => (int) $lotStats->lot_count,
'oldest_lot_date' => $lotStats->oldest_lot_date,
'last_receipt_date' => $lotStats->latest_receipt_date,
'status' => $status,
'updated_at' => now(),
]);
}
/**
* 롤백: migration 참조 데이터 삭제
*/
private function rollbackMigration(int $tenantId, bool $dryRun): int
{
$this->warn('⚠️ 롤백: migration 소스 데이터를 삭제합니다.');
// 마이그레이션으로 생성된 트랜잭션
$txCount = DB::connection($this->targetDb)
->table('stock_transactions')
->where('tenant_id', $tenantId)
->where('reference_type', 'migration')
->count();
// 마이그레이션으로 생성된 아이템 (source = '5130_migration')
$itemCount = DB::connection($this->targetDb)
->table('items')
->where('tenant_id', $tenantId)
->where('source', '5130_migration')
->whereNull('deleted_at')
->count();
$this->info(' 삭제 대상:');
$this->info(" - stock_transactions (reference_type=migration): {$txCount}");
$this->info(" - items (source=5130_migration): {$itemCount}");
if ($dryRun) {
$this->info('DRY RUN - 실제 삭제 없음');
return self::SUCCESS;
}
if (! $this->confirm('정말 롤백하시겠습니까? 되돌릴 수 없습니다.')) {
return self::SUCCESS;
}
DB::connection($this->targetDb)->transaction(function () use ($tenantId) {
// 1. 트랜잭션 삭제
DB::connection($this->targetDb)
->table('stock_transactions')
->where('tenant_id', $tenantId)
->where('reference_type', 'migration')
->delete();
// 2. migration으로 생성된 아이템의 stock_lots 삭제
$migrationItemIds = DB::connection($this->targetDb)
->table('items')
->where('tenant_id', $tenantId)
->where('source', '5130_migration')
->whereNull('deleted_at')
->pluck('id');
if ($migrationItemIds->isNotEmpty()) {
$stockIds = DB::connection($this->targetDb)
->table('stocks')
->where('tenant_id', $tenantId)
->whereIn('item_id', $migrationItemIds)
->pluck('id');
if ($stockIds->isNotEmpty()) {
DB::connection($this->targetDb)
->table('stock_lots')
->whereIn('stock_id', $stockIds)
->delete();
DB::connection($this->targetDb)
->table('stocks')
->whereIn('id', $stockIds)
->delete();
}
// 3. 아이템 삭제
DB::connection($this->targetDb)
->table('items')
->whereIn('id', $migrationItemIds)
->delete();
}
});
$this->info('✅ 롤백 완료');
return self::SUCCESS;
}
/**
* 5130 코드 → 제품 키 생성
*/
private function makeProductKey(string $prod, ?string $spec, ?string $slength): string
{
return trim($prod).'-'.trim($spec ?? '').'-'.trim($slength ?? '');
}
/**
* 5130 코드 → SAM 아이템 코드 생성
* 형식: BD-{PROD}{SPEC}-{SLENGTH} (예: BD-RS-40)
*/
private function makeItemCode(string $prod, ?string $spec, ?string $slength): string
{
$p = trim($prod);
$s = trim($spec ?? '');
$l = trim($slength ?? '');
return "BD-{$p}{$s}-{$l}";
}
/**
* 5130 코드 → 사람이 읽을 수 있는 아이템명 생성
*/
private function makeItemName(string $prod, ?string $spec, ?string $slength): string
{
$prodName = $this->prodNames[trim($prod)] ?? trim($prod);
$specName = $this->specNames[trim($spec ?? '')] ?? trim($spec ?? '');
$slengthName = $this->slengthNames[trim($slength ?? '')] ?? trim($slength ?? '');
$parts = array_filter([$prodName, $specName, $slengthName]);
return implode(' ', $parts);
}
/**
* 통계 출력
*/
private function showStats(): void
{
$this->newLine();
$this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
$this->info('📊 마이그레이션 통계');
$this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
$this->info(" 아이템 카테고리 업데이트: {$this->stats['items_updated']}");
$this->info(" 아이템 신규 생성: {$this->stats['items_created']}");
$this->info(" Stock 레코드 생성: {$this->stats['stocks_created']}");
$this->info(" StockLot 생성: {$this->stats['lots_created']}");
$this->info(" 입고 트랜잭션: {$this->stats['transactions_in']}");
$this->info(" 출고 트랜잭션: {$this->stats['transactions_out']}");
$this->info(" 스킵된 LOT: {$this->stats['skipped_lots']}");
$this->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
}
}