From cf4885d46e8c925b7ba6cd4510be1aa2eb1e697f Mon Sep 17 00:00:00 2001 From: "kelvin.yau" Date: Sun, 15 Mar 2026 10:13:42 +0800 Subject: [PATCH] update import inventory function for new excel --- .../common/internalSetup/inventorySetup.kt | 189 +++++++++--------- 1 file changed, 97 insertions(+), 92 deletions(-) diff --git a/src/main/java/com/ffii/fpsms/modules/common/internalSetup/inventorySetup.kt b/src/main/java/com/ffii/fpsms/modules/common/internalSetup/inventorySetup.kt index be0a2e5..2fb88c6 100644 --- a/src/main/java/com/ffii/fpsms/modules/common/internalSetup/inventorySetup.kt +++ b/src/main/java/com/ffii/fpsms/modules/common/internalSetup/inventorySetup.kt @@ -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() - // 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 )