diff --git a/src/main/java/com/ffii/fpsms/modules/master/entity/Items.kt b/src/main/java/com/ffii/fpsms/modules/master/entity/Items.kt index 51a525a..7a959c5 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/entity/Items.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/Items.kt @@ -34,6 +34,30 @@ open class Items : BaseEntity() { @Column(name = "countryOfOrigin") open var countryOfOrigin: String? = null + @Column(name = "inventorySheet") + open var inventorySheet: String? = null + + @Column(name = "store_id", nullable = true, length = 255) + open var store_id: String? = null + + @Column(name = "storeLocation", nullable = true, length = 255) + open var storeLocation: String? = null + + @Column(name = "warehouse", nullable = true, length = 255) + open var warehouse: String? = null + + @Column(name = "area", nullable = true, length = 255) + open var area: String? = null + + @Column(name = "slot", nullable = true, length = 255) + open var slot: String? = null + + @Column(name = "MTMSPickRoutingID", nullable = true) + open var MTMSPickRoutingID: Int? = null + + @Column(name = "LocationCode", nullable = true, length = 255) + open var LocationCode: String? = null + @Column(name = "maxQty") open var maxQty: Double? = null diff --git a/src/main/java/com/ffii/fpsms/modules/master/service/ItemsService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/ItemsService.kt index fad1f66..27465d8 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/service/ItemsService.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/service/ItemsService.kt @@ -2,6 +2,7 @@ package com.ffii.fpsms.modules.master.service import com.ffii.core.support.AbstractBaseEntityService import com.ffii.core.support.JdbcDao +import com.ffii.fpsms.modules.common.CodeGenerator import com.ffii.fpsms.modules.master.entity.* import com.ffii.fpsms.modules.master.web.models.ItemQc import com.ffii.fpsms.modules.master.web.models.ItemWithQcResponse @@ -14,6 +15,35 @@ import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.time.temporal.TemporalAdjusters import kotlin.jvm.optionals.getOrNull +import org.apache.poi.ss.usermodel.Cell +import org.apache.poi.ss.usermodel.CellType +import org.apache.poi.ss.usermodel.Sheet +import org.apache.poi.ss.usermodel.Workbook +import org.apache.poi.xssf.usermodel.XSSFWorkbook +import java.io.File +import java.io.FileInputStream +import java.math.BigDecimal +import com.ffii.fpsms.modules.stock.entity.Inventory +import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository +import com.ffii.fpsms.modules.stock.entity.InventoryRepository +import com.ffii.fpsms.modules.stock.entity.StockIn +import com.ffii.fpsms.modules.stock.entity.StockInLineRepository +import com.ffii.fpsms.modules.stock.entity.StockInRepository +import com.ffii.fpsms.modules.stock.enums.StockTakeLineStatus +import java.io.FileWriter +import java.io.PrintWriter +import com.ffii.fpsms.modules.stock.service.StockTakeService +import com.ffii.fpsms.modules.stock.web.model.SaveStockTakeRequest +import com.ffii.fpsms.modules.stock.enums.StockTakeStatus +import com.ffii.fpsms.modules.stock.service.StockInService +import com.ffii.fpsms.modules.stock.service.StockTakeLineService +import com.ffii.fpsms.modules.stock.web.model.SaveStockInRequest +import com.ffii.fpsms.modules.stock.web.model.SaveStockTakeLineRequest +import org.springframework.context.annotation.Lazy +import com.ffii.fpsms.modules.stock.service.StockInLineService +import com.ffii.fpsms.modules.stock.web.model.SaveStockInLineRequest +import com.ffii.fpsms.modules.stock.web.model.StockInLineStatus +import com.ffii.fpsms.modules.stock.web.model.SaveInventoryLotLineForSil @Service open class ItemsService( @@ -21,7 +51,167 @@ open class ItemsService( private val itemsRepository: ItemsRepository, private val qcCheckRepository: QcCheckRepository, private val qcItemsRepository: QcItemRepository, private val qcCategoryRepository: QcCategoryRepository, + private val inventoryRepository: InventoryRepository, + private val warehouseService: WarehouseService, + private val stockTakeService: StockTakeService, + @Lazy private val stockInService: StockInService, + private val stockInRepository: StockInRepository, + private val stockTakeLineService: StockTakeLineService, + @Lazy private val itemUomService: ItemUomService, + private val stockInLineService: StockInLineService, + private val stockInLineRepository: StockInLineRepository, + private val inventoryLotLineRepository: InventoryLotLineRepository, ): AbstractBaseEntityService(jdbcDao, itemsRepository) { + + private val excelImportPath: String = System.getProperty("user.home") + "/Downloads/StockTakeImport/" + + private fun assignStockInNo(): String { + val prefix = "SI" + val midfix = CodeGenerator.DEFAULT_MIDFIX + val latestCode = stockInRepository.findLatestCodeByPrefix("${prefix}-${midfix}") + return CodeGenerator.generateNo(prefix = prefix, latestCode = latestCode) + } + + inner class PatchLogWriter(private val logFilePath: String) { + private val logFile = File(logFilePath) + private val logWriter = PrintWriter(FileWriter(logFile, true)) + private val timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")) + + init { + writeHeader() + } + + fun writeHeader() { + logWriter.println("=".repeat(80)) + logWriter.println("PATCH ITEMS FROM EXCEL - LOG FILE") + logWriter.println("Start Time: $timestamp") + logWriter.println("=".repeat(80)) + logWriter.println() + } + + fun writeFileInfo(lastRow: Int, startRow: Int) { + logWriter.println("Excel File Information:") + logWriter.println(" Last Row: $lastRow") + logWriter.println(" Starting Row: ${startRow + 1} (0-indexed: $startRow)") + logWriter.println() + logWriter.println("-".repeat(80)) + logWriter.println() + } + + fun writeRowError(rowNum: Int, message: String) { + logWriter.println("Row ${rowNum + 1}: ERROR - $message") + } + + fun writeRowSkipped(rowNum: Int, reason: String) { + logWriter.println("Row ${rowNum + 1}: SKIPPED - $reason") + } + + fun writeItemPatched( + rowNum: Int, + itemCode: String, + itemId: Long?, + itemName: String?, + inventorySheet: String?, + type: String?, + storeId: String?, + storeLocation: String?, + warehouse: String?, + area: String?, + slot: String?, + locationCode: String?, + mtmsPickRoutingId: Int? + ) { + logWriter.println("Row ${rowNum + 1}: SUCCESS - Patched item '$itemCode' (ID: $itemId, Name: $itemName)") + logWriter.println(" - inventorySheet: '$inventorySheet'") + logWriter.println(" - type: '$type'") + logWriter.println(" - store_id: '$storeId'") + logWriter.println(" - storeLocation: '$storeLocation'") + logWriter.println(" - warehouse: '$warehouse'") + logWriter.println(" - area: '$area'") + logWriter.println(" - slot: '$slot'") + logWriter.println(" - LocationCode: '$locationCode'") + logWriter.println(" - MTMSPickRoutingID: '$mtmsPickRoutingId'") + } + + fun writeInventoryDeleted(rowNum: Int, inventoryId: Long?) { + logWriter.println(" INVENTORY: Deleted existing inventory (ID: $inventoryId)") + } + + fun writeInventoryDeleteError(rowNum: Int, message: String) { + logWriter.println(" INVENTORY: WARNING - Failed to delete existing inventory - $message") + } + + fun writeInventoryCreated(rowNum: Int, stockTakeCount: BigDecimal) { + logWriter.println(" INVENTORY: CREATED - onHandQty: $stockTakeCount, onHoldQty: 0") + } + + fun writeInventoryError(rowNum: Int, message: String) { + logWriter.println(" INVENTORY: ERROR - $message") + } + + fun writeInventoryIdNull(rowNum: Int) { + logWriter.println(" INVENTORY: ERROR - Item ID is null after save") + } + + fun writeEmptyLine() { + logWriter.println() + } + + fun writeSummary( + successCount: Int, + errorCount: Int, + inventoryCreatedCount: Int, + inventoryFailedCount: Int, + missingInventoryItems: List, + errors: List + ) { + logWriter.println() + logWriter.println("=".repeat(80)) + logWriter.println("SUMMARY") + logWriter.println("=".repeat(80)) + logWriter.println("End Time: ${LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))}") + logWriter.println() + logWriter.println("Items Patched: $successCount") + logWriter.println("Items with Errors: $errorCount") + logWriter.println() + logWriter.println("Inventory Records Created: $inventoryCreatedCount") + logWriter.println("Inventory Records Failed: $inventoryFailedCount") + logWriter.println() + + if (missingInventoryItems.isNotEmpty()) { + logWriter.println("Items Missing Inventory Records (${missingInventoryItems.size}):") + missingInventoryItems.forEach { logWriter.println(" - $it") } + logWriter.println() + } + + if (errors.isNotEmpty()) { + logWriter.println("Errors Encountered (${errors.size}):") + errors.forEach { logWriter.println(" - $it") } + } + + logWriter.println() + logWriter.println("=".repeat(80)) + logWriter.println("END OF LOG") + logWriter.println("=".repeat(80)) + } + + fun close() { + logWriter.close() + } + + fun getLogFilePath(): String { + return logFilePath + } + } + + private fun createPatchLogWriter(): PatchLogWriter { + val timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")) + val logFileName = "${excelImportPath}patch_log_$timestamp.txt" + return PatchLogWriter(logFileName) + } + + + // do mapping with projection open fun allItems(): List { // TODO: Replace by actual logic @@ -386,4 +576,537 @@ open class ItemsService( ) return jdbcDao.queryForInts(sql.toString(), args); } + + // Get String Value + private fun getCellStringValue(cell: Cell?): String? { + return cell?.let { + when (it.cellType) { + CellType.STRING -> it.stringCellValue.trim() + CellType.NUMERIC -> it.numericCellValue.toString().trim() + CellType.BLANK -> null + else -> null + } + }?.takeIf { it.isNotBlank() } + } + + @Throws(IOException::class) + @Transactional + open fun patchItemsFromExcel(workbook: Workbook?): String { + logger.info("--------- Start - Patch Items from Excel -------") + + if (workbook == null) { + logger.error("No Excel Import") + return "Import Excel failure" + } + + // Create log writer + val logWriter = createPatchLogWriter() + + try { + val sheet: Sheet = workbook.getSheetAt(0) + + // Column indices (0-indexed) + val COLUMN_INVENTORY_SHEET_INDEX = 4 // Column E + val COLUMN_CODE_INDEX = 7 // Column H + val COLUMN_TYPE_INDEX = 9 // Column J + val COLUMN_STORE_ID_INDEX = 13 // Column N + val COLUMN_STORE_LOCATION_INDEX = 14 // Column O + val COLUMN_WAREHOUSE_INDEX = 15 // Column P + val COLUMN_AREA_INDEX = 16 // Column Q + val COLUMN_SLOT_INDEX = 17 // Column R + val COLUMN_MTMS_PICK_ROUTING_ID_INDEX = 18 // Column S + val COLUMN_ON_HOLD_QTY_INDEX = 21 // Column V + + val START_ROW_INDEX = 4 // Starting from row 4 (0-indexed) + + var successCount = 0 + var errorCount = 0 + var inventoryCreatedCount = 0 + var inventoryFailedCount = 0 + val errors = mutableListOf() + val missingInventoryItems = mutableListOf() + + logWriter.writeFileInfo(sheet.lastRowNum, START_ROW_INDEX) + logger.info("Last row: ${sheet.lastRowNum}") + + // Create StockTake (one for entire import) + val startTime = LocalDateTime.now() + val stockTakeCode = stockTakeService.assignStockTakeNo() + val saveStockTakeReq = SaveStockTakeRequest( + code = stockTakeCode, + planStart = startTime, + planEnd = startTime, + actualStart = startTime, + actualEnd = null, + status = StockTakeStatus.PENDING.value, + remarks = "Created from Excel import - Patch Items" + ) + val savedStockTake = stockTakeService.saveStockTake(saveStockTakeReq) + logger.info("Created StockTake: ${savedStockTake.code} (ID: ${savedStockTake.id})") + + // Create StockIn (one for entire import, linked to StockTake) + val stockInCode = assignStockInNo() + val saveStockInReq = SaveStockInRequest( + code = stockInCode, // Use generated code + stockTakeId = savedStockTake.id + ) + val savedStockInResponse = stockInService.create(saveStockInReq) + val savedStockIn = savedStockInResponse.entity as StockIn + logger.info("Created StockIn: ${savedStockIn.code} (ID: ${savedStockIn.id})") + + //Tracking duplicates for stock take lines + val processedItemIds = mutableMapOf() + + for (i in START_ROW_INDEX..sheet.lastRowNum) { + val row = sheet.getRow(i) + + if (row == null) { + continue + } + + // Get item code - Debug + val itemCode = try { + val cell = row.getCell(COLUMN_CODE_INDEX) + + logger.info("=== DEBUG Row ${i + 1} ===") + logger.info("Reading from Column H (index ${COLUMN_CODE_INDEX})") + + if (cell == null) { + logger.warn("Row ${i + 1}: Cell is NULL") + } else { + logger.info("Row ${i + 1}: Cell type = ${cell.cellType}") + val cellI = row.getCell(COLUMN_CODE_INDEX + 1) + logger.info("Column I (index ${COLUMN_CODE_INDEX + 1}) = '${getCellStringValue(cellI)}'") + } + + val extractedCode = getCellStringValue(cell)?.trim() + logger.info("Extracted code = '$extractedCode'") + logger.info("=== END DEBUG Row ${i + 1} ===") + + extractedCode + } catch (e: Exception) { + logger.error("Import Error (Row ${i + 1} - Code Error): ${e.message}") + logWriter.writeRowError(i, "Failed to read code - ${e.message}") + errorCount++ + errors.add("Row ${i + 1}: Failed to read code - ${e.message}") + continue + } + + if (itemCode.isNullOrBlank()) { + logger.warn("Row ${i + 1}: Empty code, skipping") + logWriter.writeRowSkipped(i, "Empty code") + continue + } + + // Find item by code + val item = try { + itemsRepository.findByCodeAndDeletedFalse(itemCode) + } catch (e: Exception) { + logger.error("Import Error (Row ${i + 1} - Find Item Error): ${e.message}") + logWriter.writeRowError(i, "Failed to find item with code '$itemCode' - ${e.message}") + errorCount++ + errors.add("Row ${i + 1}: Failed to find item with code '$itemCode' - ${e.message}") + continue + } + + if (item == null) { + logger.warn("Row ${i + 1}: Item with code '$itemCode' not found, skipping") + logWriter.writeRowSkipped(i, "Item with code '$itemCode' not found in database") + errorCount++ + errors.add("Row ${i + 1}: Item with code '$itemCode' not found") + continue + } + + if (processedItemIds.containsKey(item.id!!)) { + val previousRow = processedItemIds[item.id!!]!! + 1 + logger.warn("Row ${i + 1}: Item '$itemCode' was already processed at row $previousRow - Creating duplicate StockTakeLine") + logWriter.writeRowError(i, "WARNING: Item '$itemCode' appears multiple times (first at row $previousRow)") + } + processedItemIds[item.id!!] = i // Track this item + + // Create StockTakeLine (for each row/item) + // Read quantity from Column V (same as inventory) + val qty = try { + val cell = row.getCell(COLUMN_ON_HOLD_QTY_INDEX) + when { + cell == null -> BigDecimal.ZERO + cell.cellType == CellType.NUMERIC -> cell.numericCellValue.toBigDecimal() + cell.cellType == CellType.STRING -> { + val strValue = cell.stringCellValue.trim() + if (strValue.isNotBlank()) strValue.toBigDecimalOrNull() ?: BigDecimal.ZERO + else BigDecimal.ZERO + } + else -> BigDecimal.ZERO + } + } catch (e: Exception) { + logger.warn("Row ${i + 1}: Failed to read quantity from column V - ${e.message}") + BigDecimal.ZERO + } + + // Get UOM for the item + val uom = itemUomService.findStockUnitByItemId(item.id!!)?.uom + + // Create StockTakeLine + val saveStockTakeLineReq = SaveStockTakeLineRequest( + stockTakeId = savedStockTake.id, + initialQty = qty, + finalQty = qty, + uomId = uom?.id, + status = StockTakeLineStatus.PENDING.value, + remarks = null, + inventoryLotLineId = null // Will be set later in Step 8 + ) + val savedStockTakeLine = try { + stockTakeLineService.saveStockTakeLine(saveStockTakeLineReq) + } catch (e: Exception) { + logger.error("Row ${i + 1}: Failed to create StockTakeLine for item '$itemCode' - ${e.message}") + logWriter.writeRowError(i, "Failed to create StockTakeLine - ${e.message}") + errorCount++ + errors.add("Row ${i + 1}: Failed to create StockTakeLine - ${e.message}") + continue + } + logger.info("Row ${i + 1}: Created StockTakeLine (ID: ${savedStockTakeLine.id}) for item '$itemCode'") + + //Create StockInLine (for each row/item) + // Get warehouse from item's LocationCode + val locationCode = item.LocationCode + val warehouse = if (locationCode != null) { + try { + warehouseService.findByCode(locationCode) + } catch (e: Exception) { + logger.warn("Row ${i + 1}: Failed to find warehouse with LocationCode '$locationCode' - ${e.message}") + null + } + } else { + logger.warn("Row ${i + 1}: Item '$itemCode' has no LocationCode, warehouse will be null") + null + } + + // Calculate expiry date (+30 days) + val expiryDate = LocalDateTime.now().plusDays(30).toLocalDate() + + // Generate lotNo: LT-YYYYMMDD-itemCode + val dateStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) + val lotNo = "LT-$dateStr-$itemCode" + + // Generate dnNo: DN-YYYYMMDD-itemCode + val dnNo = "DN-$dateStr-$itemCode" + + // Create StockInLine + val saveStockInLineReq = SaveStockInLineRequest( + stockInId = savedStockIn.id, + itemId = item.id!!, + acceptedQty = qty, + acceptQty = qty, + expiryDate = expiryDate, + warehouseId = warehouse?.id, + stockTakeLineId = savedStockTakeLine.id, + dnNo = dnNo, // Set dnNo + qcAccept = true, + status = StockInLineStatus.PENDING.status + ) + + val savedStockInLine = try { + stockInLineService.create(saveStockInLineReq) + } catch (e: Exception) { + logger.error("Row ${i + 1}: Failed to create StockInLine for item '$itemCode' - ${e.message}") + logWriter.writeRowError(i, "Failed to create StockInLine - ${e.message}") + errorCount++ + errors.add("Row ${i + 1}: Failed to create StockInLine - ${e.message}") + continue + } + logger.info("Row ${i + 1}: Created StockInLine (ID: ${savedStockInLine.id}) for item '$itemCode'") + + // Set lotNo on StockInLine (after creation, as it's not in SaveStockInLineRequest) + val stockInLineEntity = savedStockInLine.id?.let { + stockInLineRepository.findById(it).orElse(null) + } + if (stockInLineEntity != null) { + stockInLineEntity.lotNo = lotNo + stockInLineRepository.saveAndFlush(stockInLineEntity) + logger.info("Row ${i + 1}: Set lotNo '$lotNo' on StockInLine (ID: ${stockInLineEntity.id})") + } else { + logger.warn("Row ${i + 1}: Could not find StockInLine entity to set lotNo") + } + + val inventoryLotLines = if (qty > BigDecimal.ZERO && warehouse?.id != null) { + mutableListOf( + SaveInventoryLotLineForSil( + qty = qty, // Quantity from Column V + warehouseId = warehouse.id + ) + ) + } else { + logger.warn("Row ${i + 1}: Cannot create inventoryLotLines - qty: $qty, warehouse: ${warehouse?.id}") + null + } + + // Update StockInLine to RECEIVED status (this will trigger InventoryLot & InventoryLotLine creation) + saveStockInLineReq.apply { + id = savedStockInLine.id + status = StockInLineStatus.RECEIVED.status + this.dnNo = dnNo + this.inventoryLotLines = inventoryLotLines + } + val finalStockInLine = try { + stockInLineService.update(saveStockInLineReq) + } catch (e: Exception) { + logger.error("Row ${i + 1}: Failed to update StockInLine to RECEIVED for item '$itemCode' - ${e.message}") + logWriter.writeRowError(i, "Failed to update StockInLine to RECEIVED - ${e.message}") + errorCount++ + errors.add("Row ${i + 1}: Failed to update StockInLine to RECEIVED - ${e.message}") + continue + } + logger.info("Row ${i + 1}: Updated StockInLine to RECEIVED (ID: ${finalStockInLine.id}) for item '$itemCode' with lotNo: $lotNo, dnNo: $dnNo") + + val inventoryLotLine = if (finalStockInLine.id != null && warehouse?.id != null) { + try { + inventoryLotLineRepository.findByInventoryLotStockInLineIdAndWarehouseId( + inventoryLotStockInLineId = finalStockInLine.id!!, + warehouseId = warehouse.id!! + ) + } catch (e: Exception) { + logger.warn("Row ${i + 1}: Failed to find InventoryLotLine for StockInLine ${finalStockInLine.id} and warehouse ${warehouse.id} - ${e.message}") + null + } + } else { + logger.warn("Row ${i + 1}: Cannot find InventoryLotLine - finalStockInLine.id: ${finalStockInLine.id}, warehouse.id: ${warehouse?.id}") + null + } + + if (inventoryLotLine != null) { + val updateStockTakeLineReq = SaveStockTakeLineRequest( + id = savedStockTakeLine.id, + stockTakeId = savedStockTake.id, + initialQty = savedStockTakeLine.initialQty, + finalQty = savedStockTakeLine.finalQty, + uomId = savedStockTakeLine.uom?.id, + status = StockTakeLineStatus.COMPLETED.value, + completeDate = LocalDateTime.now(), + inventoryLotLineId = inventoryLotLine.id, + remarks = savedStockTakeLine.remarks + ) + try { + stockTakeLineService.saveStockTakeLine(updateStockTakeLineReq) + logger.info("Row ${i + 1}: Updated StockTakeLine (ID: ${savedStockTakeLine.id}) to COMPLETED with inventoryLotLineId: ${inventoryLotLine.id}") + } catch (e: Exception) { + logger.error("Row ${i + 1}: Failed to update StockTakeLine for item '$itemCode' - ${e.message}") + logWriter.writeRowError(i, "Failed to update StockTakeLine - ${e.message}") + // Don't continue here - this is not critical, just log the error + } + } else { + logger.warn("Row ${i + 1}: InventoryLotLine not found, StockTakeLine will not be updated with inventoryLotLineId") + logWriter.writeRowError(i, "InventoryLotLine not found for StockTakeLine update") + } + + // Update fields - Replace existing data with Excel data + try { + + // Save the item + val savedItem = itemsRepository.saveAndFlush(item) + + logger.info("Row ${i + 1}: After save - Saved item ID: ${savedItem.id}") + + successCount++ + logger.info("Row ${i + 1}: Successfully patched item '${item.name}' (code: $itemCode)") + + // Write to log file + logWriter.writeItemPatched( + rowNum = i, + itemCode = itemCode, + itemId = savedItem.id, + itemName = item.name, + inventorySheet = item.inventorySheet, + type = item.type, + storeId = item.store_id, + storeLocation = item.storeLocation, + warehouse = item.warehouse, + area = item.area, + slot = item.slot, + locationCode = item.LocationCode, + mtmsPickRoutingId = item.MTMSPickRoutingID + ) + + // Delete old inventory record if exists, then create new one + try { + val itemId = savedItem.id + if (itemId == null) { + logger.error("Row ${i + 1}: Item ID is null after save, cannot create inventory for item '$itemCode'") + logWriter.writeInventoryIdNull(i) + missingInventoryItems.add("Row ${i + 1}: Item '$itemCode' - Item ID is null") + inventoryFailedCount++ + } else { + // Check if inventory exists + val existingInventory = try { + inventoryRepository.findByItemId(itemId).orElse(null) + } catch (e: Exception) { + logger.warn("Row ${i + 1}: Failed to find existing inventory for item '$itemCode' (ID: $itemId) - ${e.message}") + null + } + + // Delete existing inventory if it exists + if (existingInventory != null) { + try { + inventoryRepository.delete(existingInventory) + inventoryRepository.flush() + logger.info("Row ${i + 1}: Deleted existing inventory record for item '${item.name}' (code: $itemCode)") + logWriter.writeInventoryDeleted(i, existingInventory.id) + } catch (e: Exception) { + logger.warn("Row ${i + 1}: Failed to delete existing inventory for item '$itemCode' - ${e.message}") + logWriter.writeInventoryDeleteError(i, e.message ?: "Unknown error") + } + } + + // Read quantity from Column V (will be used for onHandQty) + val stockTakeCount = try { + val cell = row.getCell(COLUMN_ON_HOLD_QTY_INDEX) + when { + cell == null -> BigDecimal.ZERO + cell.cellType == CellType.NUMERIC -> cell.numericCellValue.toBigDecimal() + cell.cellType == CellType.STRING -> { + val strValue = cell.stringCellValue.trim() + if (strValue.isNotBlank()) strValue.toBigDecimalOrNull() ?: BigDecimal.ZERO + else BigDecimal.ZERO + } + else -> BigDecimal.ZERO + } + } catch (e: Exception) { + logger.warn("Row ${i + 1}: Failed to read quantity from column V - ${e.message}") + BigDecimal.ZERO + } + + // Create new inventory record + try { + val inventory = Inventory() + inventory.item = savedItem + inventory.onHandQty = stockTakeCount // Column V value goes to onHandQty + inventory.onHoldQty = BigDecimal.ZERO // onHoldQty is always 0 + inventory.unavailableQty = BigDecimal.ZERO + inventory.price = BigDecimal.ZERO + inventory.cpu = BigDecimal.ZERO + inventory.cpm = BigDecimal.ZERO + inventory.status = "active" + + inventoryRepository.saveAndFlush(inventory) + inventoryCreatedCount++ + logger.info("Row ${i + 1}: Created new inventory record for item '${item.name}' (code: $itemCode) with onHandQty: $stockTakeCount") + logWriter.writeInventoryCreated(i, stockTakeCount) + + } catch (e: Exception) { + inventoryFailedCount++ + logger.error("Row ${i + 1}: Failed to create inventory record for item '$itemCode' (ID: $itemId) - ${e.message}", e) + logWriter.writeInventoryError(i, "Failed to create - ${e.message}") + missingInventoryItems.add("Row ${i + 1}: Item '$itemCode' - ${e.message}") + } + } + } catch (e: Exception) { + inventoryFailedCount++ + logger.error("Row ${i + 1}: Unexpected error during inventory creation for item '$itemCode' - ${e.message}", e) + logWriter.writeInventoryError(i, "Unexpected error - ${e.message}") + missingInventoryItems.add("Row ${i + 1}: Item '$itemCode' - ${e.message}") + } + + logWriter.writeEmptyLine() + + } catch (e: Exception) { + logger.error("Import Error (Row ${i + 1} - Save Error): ${e.message}") + logWriter.writeRowError(i, "Failed to save item '$itemCode' - ${e.message}") + errorCount++ + errors.add("Row ${i + 1}: Failed to save item '$itemCode' - ${e.message}") + } + } + + // Update StockTake to COMPLETED + val endTime = LocalDateTime.now() + saveStockTakeReq.apply { + id = savedStockTake.id + actualEnd = endTime + status = StockTakeStatus.COMPLETED.value + } + stockTakeService.saveStockTake(saveStockTakeReq) + logger.info("Updated StockTake to COMPLETED: ${savedStockTake.code}") + + // Write summary + logWriter.writeSummary( + successCount = successCount, + errorCount = errorCount, + inventoryCreatedCount = inventoryCreatedCount, + inventoryFailedCount = inventoryFailedCount, + missingInventoryItems = missingInventoryItems, + errors = errors + ) + + logger.info("--------- End - Patch Items from Excel -------") + logger.info("Success: $successCount, Errors: $errorCount") + logger.info("Inventory Created: $inventoryCreatedCount, Inventory Failed: $inventoryFailedCount") + logger.info("Log file saved to: ${logWriter.getLogFilePath()}") + + if (errors.isNotEmpty()) { + logger.error("Errors encountered: ${errors.joinToString("\n")}") + } + + return "Patch Excel completed. Success: $successCount, Errors: $errorCount. Inventory Created: $inventoryCreatedCount, Failed: $inventoryFailedCount. Log file: ${logWriter.getLogFilePath()}" + } finally { + logWriter.close() + } + } + + + /** + * Patch items from Excel file located in configured folder + * @param fileName The name of the Excel file (e.g., "items_patch.xlsx") + * @return Result message with success/error count + */ + @Throws(IOException::class) + @Transactional + open fun patchItemsFromExcelFileName(fileName: String): String { + val filePath = "$excelImportPath$fileName" + logger.info("Reading Excel file from: $filePath") + + val workbook: Workbook? = try { + val file = File(filePath) + + if (!file.exists()) { + logger.error("File not found: $filePath") + return "File not found: $filePath" + } + + if (!file.isFile) { + logger.error("Path is not a file: $filePath") + return "Path is not a file: $filePath" + } + + if (!file.canRead()) { + logger.error("File is not readable: $filePath") + return "File is not readable: $filePath" + } + + logger.info("Successfully located file: $filePath") + XSSFWorkbook(FileInputStream(file)) + } catch (e: java.io.FileNotFoundException) { + logger.error("File not found: $filePath - ${e.message}") + return "File not found: $filePath" + } catch (e: Exception) { + logger.error("Failed to read Excel file: ${e.message}", e) + return "Failed to read Excel file: ${e.message}" + } + + return try { + if (workbook == null) { + "Failed to read Excel workbook" + } else { + patchItemsFromExcel(workbook) + } + } catch (e: Exception) { + logger.error("Error processing Excel file: ${e.message}", e) + "Error processing Excel file: ${e.message}" + } finally { + // Close workbook to free resources + try { + workbook?.close() + } catch (e: Exception) { + logger.warn("Error closing workbook: ${e.message}") + } + } + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/master/web/ItemsController.kt b/src/main/java/com/ffii/fpsms/modules/master/web/ItemsController.kt index 6dbed96..4d6ee68 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/web/ItemsController.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/web/ItemsController.kt @@ -13,12 +13,17 @@ import jakarta.servlet.http.HttpServletRequest import jakarta.validation.Valid import org.springframework.web.bind.annotation.* import java.util.Collections.emptyList +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.http.ResponseEntity @RestController @RequestMapping("/items") class ItemsController( private val itemsService: ItemsService ) { + private val logger: Logger = LoggerFactory.getLogger(ItemsController::class.java) + @GetMapping fun allItems(): List { return itemsService.allItems() @@ -122,4 +127,20 @@ fun getItemsWithDetailsByPage(request: HttpServletRequest): RecordsRes { + return try { + if (fileName.isBlank()) { + return ResponseEntity.badRequest().body("File name cannot be empty") + } + + logger.info("Request to patch items from file: $fileName") + val result = itemsService.patchItemsFromExcelFileName(fileName) + ResponseEntity.ok(result) + } catch (e: Exception) { + logger.error("Error patching from file: ${e.message}", e) + ResponseEntity.badRequest().body("Error: ${e.message}") + } + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockInRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockInRepository.kt index 6ed6d2d..828e15d 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockInRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockInRepository.kt @@ -16,4 +16,9 @@ interface StockInRepository : AbstractRepository { fun findByStockTakeIdAndDeletedFalse(stockTakeId: Long): StockIn? fun findByIdAndDeletedIsFalse(id: Serializable): StockIn? + + @Query(""" + select si.code from StockIn si where si.code like :prefix% and si.deleted = false order by si.code desc limit 1 + """) + fun findLatestCodeByPrefix(prefix: String): String? } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockInService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockInService.kt index 083e632..bf92a74 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockInService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockInService.kt @@ -31,7 +31,7 @@ open class StockInService( stockIn.apply { status = StockInStatus.PENDING.status orderDate = LocalDateTime.now() - code = LocalDateTime.now().toString().take(19) + code = request.code ?: LocalDateTime.now().toString().take(19) } if (request.purchaseOrderId != null) { val purchaseOrder : PurchaseOrder = purchaseOrderRepository.findByIdAndDeletedFalse(request.purchaseOrderId).orElseThrow(); diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt index 3ead4c3..9e508d2 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt @@ -32,6 +32,7 @@ import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRecord import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRecordRepository import com.ffii.fpsms.modules.common.CodeGenerator +import org.springframework.context.annotation.Lazy @Service open class StockOutLineService( @@ -44,7 +45,7 @@ open class StockOutLineService( private val itemUomRespository: ItemUomRespository, private val pickOrderRepository: PickOrderRepository, private val inventoryLotLineRepository: InventoryLotLineRepository, - private val suggestedPickLotService: SuggestedPickLotService, + @Lazy private val suggestedPickLotService: SuggestedPickLotService, private val doPickOrderRepository: DoPickOrderRepository, private val doPickOrderRecordRepository: DoPickOrderRecordRepository, private val deliveryOrderRepository: DeliveryOrderRepository, diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeService.kt index 4a9a7fa..cb54ac8 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeService.kt @@ -22,6 +22,7 @@ import org.slf4j.LoggerFactory import org.springframework.stereotype.Service import java.math.BigDecimal import java.time.LocalDateTime +import org.springframework.context.annotation.Lazy @Service class StockTakeService( @@ -30,8 +31,8 @@ class StockTakeService( val warehouseService: WarehouseService, val stockInService: StockInService, val stockInLineService: StockInLineService, - val itemsService: ItemsService, - val itemUomService: ItemUomService, + @Lazy val itemsService: ItemsService, + @Lazy val itemUomService: ItemUomService, val stockTakeLineService: StockTakeLineService, val inventoryLotLineRepository: InventoryLotLineRepository, ) { diff --git a/src/main/resources/db/changelog/changes/20251127_01_KelvinY/01_add_inventorySheet_to_items.sql b/src/main/resources/db/changelog/changes/20251127_01_KelvinY/01_add_inventorySheet_to_items.sql new file mode 100644 index 0000000..b79e258 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20251127_01_KelvinY/01_add_inventorySheet_to_items.sql @@ -0,0 +1,5 @@ +-- liquibase formatted sql +-- changeset KelvinY:add_inventorySheet_to_items + +ALTER TABLE `fpsmsdb`.`items` +ADD COLUMN `inventorySheet` VARCHAR(255) NULL; \ No newline at end of file