| @@ -9,12 +9,14 @@ import org.springframework.beans.factory.annotation.Autowired | |||
| import org.springframework.transaction.annotation.Transactional | |||
| import com.ffii.fpsms.modules.master.entity.ItemsRepository | |||
| import com.ffii.fpsms.modules.master.entity.WarehouseRepository | |||
| import com.ffii.fpsms.modules.master.service.ItemUomService | |||
| import com.ffii.fpsms.modules.stock.service.StockInLineService | |||
| import com.ffii.fpsms.modules.stock.web.model.StockInRequest | |||
| import java.io.File | |||
| import java.io.FileInputStream | |||
| import java.io.IOException | |||
| import java.math.BigDecimal | |||
| import java.math.RoundingMode | |||
| import java.time.LocalDate | |||
| import java.time.format.DateTimeFormatter | |||
| import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository | |||
| @@ -32,7 +34,9 @@ open class InventorySetup { | |||
| @Autowired | |||
| private lateinit var stockInLineService: StockInLineService | |||
| private val DEFAULT_WAREHOUSE_CODE = "2F-W201-#A-01" | |||
| @Autowired | |||
| private lateinit var itemUomService: ItemUomService | |||
| private val DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy/MM/dd") | |||
| @Transactional(rollbackFor = [Exception::class]) | |||
| @@ -46,101 +50,98 @@ open class InventorySetup { | |||
| val workbook: Workbook = XSSFWorkbook(inputStream) | |||
| try { | |||
| val sheet: Sheet = workbook.getSheetAt(0) | |||
| val inventoryDataList = mutableListOf<InventoryData>() | |||
| // Column indices | |||
| val COLUMN_ITEM_NO_INDEX = 6 // Column G | |||
| val COLUMN_STORE_ID_INDEX = 14 // Column O | |||
| val COLUMN_WAREHOUSE_INDEX = 16 // Column Q | |||
| val COLUMN_AREA_INDEX = 17 // Column R | |||
| val COLUMN_SLOT_INDEX = 18 // Column S | |||
| val COLUMN_PRODUCT_LOT_NO_INDEX = 21 // Column V | |||
| val COLUMN_QUANTITY_INDEX = 22 // Column W | |||
| val COLUMN_EXPIRY_DATE_INDEX = 26 // Column AA | |||
| // New template: B=store_id, C=warehouse, D=area, E=slot, F=item code, J=productionLotNo, K=quantity (sales unit), O=expiry date | |||
| val COLUMN_STORE_ID_INDEX = 1 // B | |||
| val COLUMN_WAREHOUSE_INDEX = 2 // C | |||
| val COLUMN_AREA_INDEX = 3 // D | |||
| val COLUMN_SLOT_INDEX = 4 // E | |||
| val COLUMN_ITEM_CODE_INDEX = 5 // F | |||
| val COLUMN_PRODUCT_LOT_NO_INDEX = 9 // J | |||
| val COLUMN_QUANTITY_INDEX = 10 // K (sales unit) | |||
| val COLUMN_EXPIRY_DATE_INDEX = 14 // O | |||
| val START_ROW_INDEX = 2 // Start from Excel row 3 (0-based: index 2) | |||
| val START_ROW_INDEX = 6 // Start from Excel row 7 (0-based: index 6) | |||
| // ──────────────────────────────────────────────── | |||
| // Process data rows (starting from Excel row 3, index 2) | |||
| // Process all sheets, data rows from row 7 | |||
| // ──────────────────────────────────────────────── | |||
| for (rowIndex in START_ROW_INDEX..sheet.lastRowNum) { | |||
| val row = sheet.getRow(rowIndex) ?: continue | |||
| for (sheetIndex in 0 until workbook.numberOfSheets) { | |||
| val sheet: Sheet = workbook.getSheetAt(sheetIndex) | |||
| val sheetName = sheet.sheetName | |||
| for (rowIndex in START_ROW_INDEX..sheet.lastRowNum) { | |||
| val row = sheet.getRow(rowIndex) ?: continue | |||
| val itemNo = ExcelUtils.getStringValue(row.getCell(COLUMN_ITEM_NO_INDEX))?.trim() ?: "" | |||
| val itemNo = ExcelUtils.getStringValue(row.getCell(COLUMN_ITEM_CODE_INDEX))?.trim() ?: "" | |||
| // Skip if itemNo is empty (required field) | |||
| if (itemNo.isEmpty()) { | |||
| println("SKIP row ${rowIndex + 1} - Missing itemNo") | |||
| continue | |||
| } | |||
| if (itemNo.isEmpty()) { | |||
| println("SKIP sheet '$sheetName' row ${rowIndex + 1} - Missing item code") | |||
| continue | |||
| } | |||
| // Read warehouse location fields | |||
| val store_id = ExcelUtils.getStringValue(row.getCell(COLUMN_STORE_ID_INDEX))?.trim() ?: "" | |||
| val warehouse = ExcelUtils.getStringValue(row.getCell(COLUMN_WAREHOUSE_INDEX))?.trim() ?: "" | |||
| val area = ExcelUtils.getStringValue(row.getCell(COLUMN_AREA_INDEX))?.trim() ?: "" | |||
| val slot = ExcelUtils.getStringValue(row.getCell(COLUMN_SLOT_INDEX))?.trim() ?: "" | |||
| // Generate LocationCode: store_id-warehouse-area-slot | |||
| val locationCode = if (store_id.isNotEmpty() && warehouse.isNotEmpty() && | |||
| area.isNotEmpty() && slot.isNotEmpty()) { | |||
| "$store_id-$warehouse-$area-$slot" | |||
| } else { | |||
| "" | |||
| } | |||
| val store_id = ExcelUtils.getStringValue(row.getCell(COLUMN_STORE_ID_INDEX))?.trim() ?: "" | |||
| val warehouse = ExcelUtils.getStringValue(row.getCell(COLUMN_WAREHOUSE_INDEX))?.trim() ?: "" | |||
| val area = ExcelUtils.getStringValue(row.getCell(COLUMN_AREA_INDEX))?.trim() ?: "" | |||
| val slot = ExcelUtils.getStringValue(row.getCell(COLUMN_SLOT_INDEX))?.trim() ?: "" | |||
| val quantityStr = ExcelUtils.getStringValue(row.getCell(COLUMN_QUANTITY_INDEX))?.trim() | |||
| val quantity = quantityStr?.toBigDecimalOrNull() | |||
| val locationCode = if (store_id.isNotEmpty() && warehouse.isNotEmpty() && | |||
| area.isNotEmpty() && slot.isNotEmpty()) { | |||
| "$store_id-$warehouse-$area-$slot" | |||
| } else { | |||
| "" | |||
| } | |||
| if (quantity == null || quantity <= BigDecimal.ZERO) { | |||
| println("SKIP row ${rowIndex + 1} - Invalid quantity: $quantityStr") | |||
| continue | |||
| } | |||
| if (locationCode.isEmpty()) { | |||
| println("SKIP sheet '$sheetName' row ${rowIndex + 1} - No location (store_id/warehouse/area/slot required)") | |||
| continue | |||
| } | |||
| val productLotNo = ExcelUtils.getStringValue(row.getCell(COLUMN_PRODUCT_LOT_NO_INDEX))?.trim() | |||
| val quantityStr = ExcelUtils.getStringValue(row.getCell(COLUMN_QUANTITY_INDEX))?.trim() | |||
| val salesQuantity = quantityStr?.toBigDecimalOrNull() | |||
| // Parse expiry date with fallback to default (1 month later) | |||
| val expiryDateStr = ExcelUtils.getStringValue(row.getCell(COLUMN_EXPIRY_DATE_INDEX))?.trim() | |||
| val expiryDate = expiryDateStr?.let { | |||
| try { | |||
| LocalDate.parse(it, DATE_FORMAT) | |||
| } catch (e: Exception) { | |||
| println("WARNING row ${rowIndex + 1} - Invalid expiry date format: $it (expected YYYY/MM/DD), using default: 1 month later") | |||
| null | |||
| if (salesQuantity == null || salesQuantity <= BigDecimal.ZERO) { | |||
| println("SKIP sheet '$sheetName' row ${rowIndex + 1} - Invalid or zero quantity: $quantityStr") | |||
| continue | |||
| } | |||
| } ?: run { | |||
| // If expiryDateStr is null or empty, or parsing failed, use default | |||
| LocalDate.now().plusMonths(1) | |||
| } | |||
| // If parsing failed, use default | |||
| val finalExpiryDate = expiryDate ?: LocalDate.now().plusMonths(1) | |||
| println("=== Processing row ${rowIndex + 1} (itemNo: $itemNo, qty: $quantity, expiryDate: $finalExpiryDate, locationCode: ${locationCode.ifEmpty { "DEFAULT" }}) ===") | |||
| val productLotNo = ExcelUtils.getStringValue(row.getCell(COLUMN_PRODUCT_LOT_NO_INDEX))?.trim() | |||
| inventoryDataList.add( | |||
| InventoryData( | |||
| itemNo = itemNo, | |||
| locationCode = locationCode, | |||
| quantity = quantity, | |||
| productLotNo = productLotNo, | |||
| expiryDate = finalExpiryDate | |||
| val expiryDateStr = ExcelUtils.getStringValue(row.getCell(COLUMN_EXPIRY_DATE_INDEX))?.trim() | |||
| val expiryDate = expiryDateStr?.let { | |||
| try { | |||
| LocalDate.parse(it, DATE_FORMAT) | |||
| } catch (e: Exception) { | |||
| println("WARNING sheet '$sheetName' row ${rowIndex + 1} - Invalid expiry date format: $it (expected yyyy/MM/dd), using default: 1 month later") | |||
| null | |||
| } | |||
| } ?: LocalDate.now().plusMonths(1) | |||
| val finalExpiryDate = expiryDate ?: LocalDate.now().plusMonths(1) | |||
| inventoryDataList.add( | |||
| InventoryData( | |||
| itemNo = itemNo, | |||
| locationCode = locationCode, | |||
| quantitySalesUnit = salesQuantity, | |||
| productLotNo = productLotNo, | |||
| expiryDate = finalExpiryDate | |||
| ) | |||
| ) | |||
| ) | |||
| } | |||
| } | |||
| println("Total valid rows collected: ${inventoryDataList.size}") | |||
| // ──────────────────────────────────────────────── | |||
| // Create stock in for each item | |||
| // Create stock in (convert sales qty -> stock qty per item) | |||
| // ──────────────────────────────────────────────── | |||
| var createdCount = 0 | |||
| var notFoundCount = 0 | |||
| var warehouseNotFoundCount = 0 | |||
| var warehouseSkippedCount = 0 | |||
| for (data in inventoryDataList) { | |||
| try { | |||
| // Find item by code (itemNo) | |||
| val item = itemsRepository.findFirstByCodeAndDeletedFalse(data.itemNo) | |||
| if (item == null) { | |||
| @@ -154,23 +155,11 @@ open class InventorySetup { | |||
| continue | |||
| } | |||
| // Find warehouse by LocationCode, fallback to default if not found | |||
| var warehouse = if (data.locationCode.isNotEmpty()) { | |||
| warehouseRepository.findByCodeAndDeletedIsFalse(data.locationCode) | |||
| } else { | |||
| null | |||
| } | |||
| // If warehouse not found, use default warehouse | |||
| val warehouse = warehouseRepository.findByCodeAndDeletedIsFalse(data.locationCode) | |||
| if (warehouse == null) { | |||
| println("WAREHOUSE NOT FOUND - Warehouse with code '${data.locationCode}' does not exist, using default: $DEFAULT_WAREHOUSE_CODE") | |||
| warehouse = warehouseRepository.findByCodeAndDeletedIsFalse(DEFAULT_WAREHOUSE_CODE) | |||
| warehouseNotFoundCount++ | |||
| if (warehouse == null) { | |||
| println("ERROR - Default warehouse '$DEFAULT_WAREHOUSE_CODE' also not found in database") | |||
| continue | |||
| } | |||
| println("SKIP - No warehouse for location '${data.locationCode}' (no default)") | |||
| warehouseSkippedCount++ | |||
| continue | |||
| } | |||
| if (warehouse.id == null) { | |||
| @@ -178,18 +167,34 @@ open class InventorySetup { | |||
| continue | |||
| } | |||
| // Generate dnNo: LT-YYYYMMDD-itemCode | |||
| val stockQuantityRaw = itemUomService.findSalesUnitByItemId(item.id!!)?.uom?.id?.let { salesUomId -> | |||
| itemUomService.convertQtyToStockQty(item.id!!, salesUomId, data.quantitySalesUnit) | |||
| } ?: data.quantitySalesUnit | |||
| val stockQuantity = stockQuantityRaw.setScale(0, RoundingMode.FLOOR) | |||
| if (stockQuantity != stockQuantityRaw) { | |||
| println("FLOOR applied for item ${data.itemNo}: $stockQuantityRaw -> $stockQuantity (sales qty: ${data.quantitySalesUnit})") | |||
| } | |||
| if (stockQuantity <= BigDecimal.ZERO) { | |||
| if (stockQuantityRaw > BigDecimal.ZERO) { | |||
| println("SKIP - Stock quantity became 0 after floor for item ${data.itemNo} (sales qty: ${data.quantitySalesUnit}, converted before floor: $stockQuantityRaw)") | |||
| } else { | |||
| println("SKIP - Stock quantity <= 0 for item ${data.itemNo} (sales qty: ${data.quantitySalesUnit})") | |||
| } | |||
| continue | |||
| } | |||
| val today = LocalDate.now() | |||
| val dnNo = "LT-${today.format(DateTimeFormatter.ofPattern("yyyyMMdd"))}-${data.itemNo}" | |||
| // Use generic stock in | |||
| val stockInRequest = StockInRequest( | |||
| itemId = item.id!!, | |||
| itemNo = data.itemNo, | |||
| demandQty = data.quantity, | |||
| acceptedQty = data.quantity, | |||
| demandQty = stockQuantity, | |||
| acceptedQty = stockQuantity, | |||
| expiryDate = data.expiryDate, | |||
| lotNo = null, // System will auto-generate | |||
| lotNo = null, | |||
| productLotNo = data.productLotNo, | |||
| dnNo = dnNo, | |||
| type = "OPEN", | |||
| @@ -198,7 +203,7 @@ open class InventorySetup { | |||
| stockInLineService.createStockIn(stockInRequest) | |||
| createdCount++ | |||
| println("CREATED stock in for item: ${data.itemNo} (dnNo: $dnNo, warehouse: ${warehouse.code})") | |||
| println("CREATED stock in for item: ${data.itemNo} (sales qty: ${data.quantitySalesUnit} -> stock qty: $stockQuantity, warehouse: ${warehouse.code})") | |||
| } catch (e: Exception) { | |||
| println("Error processing inventory row (itemNo: ${data.itemNo}): ${e.message}") | |||
| e.printStackTrace() | |||
| @@ -206,7 +211,7 @@ open class InventorySetup { | |||
| } | |||
| } | |||
| println("Import finished. Created $createdCount stock in records, Not found: $notFoundCount items, Warehouse not found (used default): $warehouseNotFoundCount.") | |||
| println("Import finished. Created $createdCount stock in records, Not found: $notFoundCount items, Skipped (no location): $warehouseSkippedCount.") | |||
| return createdCount | |||
| } finally { | |||
| @@ -218,7 +223,7 @@ open class InventorySetup { | |||
| data class InventoryData( | |||
| val itemNo: String, | |||
| val locationCode: String, | |||
| val quantity: BigDecimal, | |||
| val quantitySalesUnit: BigDecimal, | |||
| val productLotNo: String?, | |||
| val expiryDate: LocalDate | |||
| ) | |||