From 74d089a767e7418cc28aceb27a3830a9bbfd8475 Mon Sep 17 00:00:00 2001 From: "kelvin.yau" Date: Wed, 3 Dec 2025 11:58:03 +0800 Subject: [PATCH] update stock take import function, added column to items table, please reload backend --- .../ffii/fpsms/modules/master/entity/Items.kt | 3 + .../master/entity/WarehouseRepository.kt | 16 + .../modules/master/service/BomService.kt | 2 +- .../modules/master/service/ItemsService.kt | 373 ++++++++++++++---- .../master/service/WarehouseService.kt | 11 + .../stock/entity/InventoryRepository.kt | 3 + .../01_add_company_to_items.sql | 5 + 7 files changed, 330 insertions(+), 83 deletions(-) create mode 100644 src/main/resources/db/changelog/changes/20251203_01_KelvinY/01_add_company_to_items.sql 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 7a959c5..470ea26 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 @@ -58,6 +58,9 @@ open class Items : BaseEntity() { @Column(name = "LocationCode", nullable = true, length = 255) open var LocationCode: String? = null + @Column(name = "company", nullable = true, length = 30) + open var company: String? = null + @Column(name = "maxQty") open var maxQty: Double? = null diff --git a/src/main/java/com/ffii/fpsms/modules/master/entity/WarehouseRepository.kt b/src/main/java/com/ffii/fpsms/modules/master/entity/WarehouseRepository.kt index 4707a3a..0dce39e 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/entity/WarehouseRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/WarehouseRepository.kt @@ -2,6 +2,7 @@ package com.ffii.fpsms.modules.master.entity import com.ffii.core.support.AbstractRepository import com.ffii.fpsms.modules.master.entity.projections.WarehouseCombo +import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository import java.io.Serializable @@ -12,4 +13,19 @@ interface WarehouseRepository : AbstractRepository { fun findByIdAndDeletedIsFalse(id: Serializable): Warehouse?; fun findByCodeAndDeletedIsFalse(code: String): Warehouse?; + + @Query(""" + SELECT w FROM Warehouse w + WHERE w.code = :code + AND w.stockTakeTable = :stockTakeTable + AND w.company = :company + AND w.storeLocation = :storeLocation + AND w.deleted = false +""") + fun findByCodeAndStockTakeTableAndCompanyAndStoreLocationAndDeletedIsFalse( + code: String, + stockTakeTable: String, + company: String, + storeLocation: String + ): Warehouse? } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt index bb2ae96..f334e71 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt @@ -513,7 +513,7 @@ open class BomService( val resolver = PathMatchingResourcePatternResolver() // val excels = resolver.getResources("bomImport/*.xlsx") //val excels = resolver.getResources("file:C:/Users/Kelvin YAU/Downloads/bom/*.xlsx") - val excels = resolver.getResources("file:C:/Users/kw093/Downloads/bom/*.xlsx") + val excels = resolver.getResources("file:C:/Users/Kelvin YAU/Downloads/bom/*.xlsx") // val excels = resolver.getResources("file:C:/Users/2Fi/Desktop/Third Wave of BOM Excel/*.xlsx") println("size: ${excels.size}") val logExcel = ClassPathResource("excelTemplate/bom_excel_issue_log.xlsx") 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 27465d8..92cadca 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 @@ -142,7 +142,7 @@ open class ItemsService( } fun writeInventoryCreated(rowNum: Int, stockTakeCount: BigDecimal) { - logWriter.println(" INVENTORY: CREATED - onHandQty: $stockTakeCount, onHoldQty: 0") + logWriter.println(" INVENTORY: CREATED - onHandQty: $stockTakeCount") } fun writeInventoryError(rowNum: Int, message: String) { @@ -160,6 +160,7 @@ open class ItemsService( fun writeSummary( successCount: Int, errorCount: Int, + duplicateSkippedCount: Int, inventoryCreatedCount: Int, inventoryFailedCount: Int, missingInventoryItems: List, @@ -173,6 +174,7 @@ open class ItemsService( logWriter.println() logWriter.println("Items Patched: $successCount") logWriter.println("Items with Errors: $errorCount") + logWriter.println("Duplicate Items Skipped: $duplicateSkippedCount") logWriter.println() logWriter.println("Inventory Records Created: $inventoryCreatedCount") logWriter.println("Inventory Records Failed: $inventoryFailedCount") @@ -606,8 +608,9 @@ open class ItemsService( 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_COMPANY_INDEX = 1 // Column B + 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 @@ -617,12 +620,13 @@ open class ItemsService( 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) + val START_ROW_INDEX = 3 // Starting from row 4 (0-indexed) var successCount = 0 var errorCount = 0 var inventoryCreatedCount = 0 var inventoryFailedCount = 0 + var duplicateSkippedCount = 0 val errors = mutableListOf() val missingInventoryItems = mutableListOf() @@ -664,28 +668,12 @@ open class ItemsService( continue } - // Get item code - Debug + // Get item code 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 + getCellStringValue(cell)?.trim() } catch (e: Exception) { - logger.error("Import Error (Row ${i + 1} - Code Error): ${e.message}") + logger.error("Row ${i + 1}: Failed to read code - ${e.message}") logWriter.writeRowError(i, "Failed to read code - ${e.message}") errorCount++ errors.add("Row ${i + 1}: Failed to read code - ${e.message}") @@ -719,16 +707,18 @@ open class ItemsService( 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)") + duplicateSkippedCount++ + logger.warn("Row ${i + 1}: Item '$itemCode' was already processed at row $previousRow - Skipping duplicate") + logWriter.writeRowSkipped(i, "Item '$itemCode' already processed at row $previousRow - Skipped") + continue // Skip to next item } - processedItemIds[item.id!!] = i // Track this item + processedItemIds[item.id!!] = i // 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 { + val extractedValue = when { cell == null -> BigDecimal.ZERO cell.cellType == CellType.NUMERIC -> cell.numericCellValue.toBigDecimal() cell.cellType == CellType.STRING -> { @@ -738,6 +728,8 @@ open class ItemsService( } else -> BigDecimal.ZERO } + // Add 500 to the extracted value + extractedValue + BigDecimal(500) } catch (e: Exception) { logger.warn("Row ${i + 1}: Failed to read quantity from column V - ${e.message}") BigDecimal.ZERO @@ -767,18 +759,149 @@ open class ItemsService( } logger.info("Row ${i + 1}: Created StockTakeLine (ID: ${savedStockTakeLine.id}) for item '$itemCode'") + try { + // Read Company (Column B) + val company = getCellStringValue(row.getCell(COLUMN_COMPANY_INDEX))?.trim() + if (!company.isNullOrBlank()) { + item.company = company + logger.info("Row ${i + 1}: Company read from Column B: '$company'") + } + + // Read InventorySheet (Column E) + val inventorySheet = getCellStringValue(row.getCell(COLUMN_INVENTORY_SHEET_INDEX))?.trim() + if (!inventorySheet.isNullOrBlank()) { + item.inventorySheet = inventorySheet + } + + // Read Type (Column J) + val type = getCellStringValue(row.getCell(COLUMN_TYPE_INDEX))?.trim() + if (!type.isNullOrBlank()) { + item.type = type + } + + // Read Store ID (Column N) - String type + val storeId = getCellStringValue(row.getCell(COLUMN_STORE_ID_INDEX))?.trim() + if (!storeId.isNullOrBlank()) { + item.store_id = storeId + } + + // Read Store Location (Column O) + val storeLocation = getCellStringValue(row.getCell(COLUMN_STORE_LOCATION_INDEX))?.trim() + if (!storeLocation.isNullOrBlank()) { + item.storeLocation = storeLocation + } + + // Read Warehouse (Column P) + val warehouseFromExcel = getCellStringValue(row.getCell(COLUMN_WAREHOUSE_INDEX))?.trim() + if (!warehouseFromExcel.isNullOrBlank()) { + item.warehouse = warehouseFromExcel + } + + // Read Area (Column Q) + val area = getCellStringValue(row.getCell(COLUMN_AREA_INDEX))?.trim() + if (!area.isNullOrBlank()) { + item.area = area + } + + // Read Slot (Column R) + val slot = getCellStringValue(row.getCell(COLUMN_SLOT_INDEX))?.trim() + if (!slot.isNullOrBlank()) { + item.slot = slot + } + + // Combine Column N (Store ID), Column P (Warehouse), Column Q (Area), and Column R (Slot) to form LocationCode + // Format: "N-P-Q-R" (e.g., "ST01-WH01-A01-S01") + val locationCodeParts = mutableListOf() + if (!storeId.isNullOrBlank()) { + locationCodeParts.add(storeId) + } + if (!warehouseFromExcel.isNullOrBlank()) { + locationCodeParts.add(warehouseFromExcel) + } + if (!area.isNullOrBlank()) { + locationCodeParts.add(area) + } + if (!slot.isNullOrBlank()) { + locationCodeParts.add(slot) + } + + val locationCodeFromExcel = if (locationCodeParts.isNotEmpty()) { + locationCodeParts.joinToString("-") + } else { + null + } + + if (!locationCodeFromExcel.isNullOrBlank()) { + item.LocationCode = locationCodeFromExcel + logger.info("Row ${i + 1}: LocationCode created: '$locationCodeFromExcel' (Format: N-P-Q-R, N='$storeId', P='$warehouseFromExcel', Q='$area', R='$slot')") + } else { + logger.warn("Row ${i + 1}: LocationCode cannot be created - Column N='$storeId', P='$warehouseFromExcel', Q='$area', R='$slot' (all empty or null)") + } + // Read MTMS Pick Routing ID (Column S) - Int type + val mtmsPickRoutingId = try { + val cell = row.getCell(COLUMN_MTMS_PICK_ROUTING_ID_INDEX) + when { + cell == null -> null + cell.cellType == CellType.NUMERIC -> cell.numericCellValue.toInt() + cell.cellType == CellType.STRING -> { + val strValue = cell.stringCellValue.trim() + if (strValue.isNotBlank()) strValue.toIntOrNull() else null + } + else -> null + } + } catch (e: Exception) { + logger.warn("Row ${i + 1}: Failed to read MTMS Pick Routing ID from column S - ${e.message}") + null + } + if (mtmsPickRoutingId != null) { + item.MTMSPickRoutingID = mtmsPickRoutingId + } + + } catch (e: Exception) { + logger.error("Row ${i + 1}: Failed to read Excel columns for item '$itemCode' - ${e.message}") + logWriter.writeRowError(i, "Failed to read Excel columns - ${e.message}") + // Continue processing - don't fail the entire row + } + + //Create StockInLine (for each row/item) - // Get warehouse from item's LocationCode + // Get warehouse from item's LocationCode, inventorySheet, company, and storeLocation + // Match: item.LocationCode -> warehouse.code AND item.inventorySheet -> warehouse.stockTakeTable AND item.company -> warehouse.company AND item.storeLocation -> warehouse.storeLocation val locationCode = item.LocationCode - val warehouse = if (locationCode != null) { + val inventorySheet = item.inventorySheet + val company = item.company + val storeLocation = item.storeLocation + logger.info("Row ${i + 1}: Looking up warehouse with LocationCode: '$locationCode', inventorySheet: '$inventorySheet', company: '$company', storeLocation: '$storeLocation'") + + val warehouse = if (locationCode != null && locationCode.isNotBlank() && + inventorySheet != null && inventorySheet.isNotBlank() && + company != null && company.isNotBlank() && + storeLocation != null && storeLocation.isNotBlank()) { try { - warehouseService.findByCode(locationCode) + val foundWarehouse = warehouseService.findByCodeAndStockTakeTableAndCompanyAndStoreLocation(locationCode, inventorySheet, company, storeLocation) + if (foundWarehouse != null) { + logger.info("Row ${i + 1}: Found warehouse: code='${foundWarehouse.code}', stockTakeTable='${foundWarehouse.stockTakeTable}', company='${foundWarehouse.company}', storeLocation='${foundWarehouse.storeLocation}', id=${foundWarehouse.id}") + } else { + logger.warn("Row ${i + 1}: Warehouse not found in database for LocationCode: '$locationCode', inventorySheet: '$inventorySheet', company: '$company', storeLocation: '$storeLocation'") + } + foundWarehouse } catch (e: Exception) { - logger.warn("Row ${i + 1}: Failed to find warehouse with LocationCode '$locationCode' - ${e.message}") + logger.warn("Row ${i + 1}: Failed to find warehouse with LocationCode '$locationCode', inventorySheet '$inventorySheet', company '$company', storeLocation '$storeLocation' - ${e.message}") null } } else { - logger.warn("Row ${i + 1}: Item '$itemCode' has no LocationCode, warehouse will be null") + if (locationCode.isNullOrBlank()) { + logger.warn("Row ${i + 1}: Item '$itemCode' has no LocationCode (null or blank), warehouse will be null") + } + if (inventorySheet.isNullOrBlank()) { + logger.warn("Row ${i + 1}: Item '$itemCode' has no inventorySheet (null or blank), warehouse will be null") + } + if (company.isNullOrBlank()) { + logger.warn("Row ${i + 1}: Item '$itemCode' has no company (null or blank), warehouse will be null") + } + if (storeLocation.isNullOrBlank()) { + logger.warn("Row ${i + 1}: Item '$itemCode' has no storeLocation (null or blank), warehouse will be null") + } null } @@ -841,62 +964,139 @@ open class ItemsService( null } - // Update StockInLine to RECEIVED status (this will trigger InventoryLot & InventoryLotLine creation) + // Step 7a: First update to PENDING with qcAccept=true to create InventoryLot + // This will create InventoryLot and automatically set status to RECEIVED saveStockInLineReq.apply { id = savedStockInLine.id - status = StockInLineStatus.RECEIVED.status + status = StockInLineStatus.PENDING.status this.dnNo = dnNo - this.inventoryLotLines = inventoryLotLines + this.qcAccept = true + this.acceptedQty = qty + this.acceptQty = qty + // Don't set inventoryLotLines yet } - val finalStockInLine = try { + val updatedForLot = 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}") + logger.error("Row ${i + 1}: Failed to update StockInLine to PENDING (for InventoryLot) for item '$itemCode' - ${e.message}") + logWriter.writeRowError(i, "Failed to update StockInLine to PENDING - ${e.message}") errorCount++ - errors.add("Row ${i + 1}: Failed to update StockInLine to RECEIVED - ${e.message}") + errors.add("Row ${i + 1}: Failed to update StockInLine to PENDING - ${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 { + // Check if update was successful + if (updatedForLot?.id == null) { + logger.error("Row ${i + 1}: StockInLine update returned null ID for item '$itemCode'") + logWriter.writeRowError(i, "StockInLine update returned null") + errorCount++ + errors.add("Row ${i + 1}: StockInLine update returned null") + continue + } + + logger.info("Row ${i + 1}: Updated StockInLine to PENDING (ID: ${updatedForLot.id}) - InventoryLot should be created") + + // Refresh StockInLine entity to get the InventoryLot that was just created + val refreshedStockInLine = stockInLineRepository.findById(updatedForLot.id!!).orElse(null) + if (refreshedStockInLine?.inventoryLot == null) { + logger.warn("Row ${i + 1}: InventoryLot was not created for StockInLine ${updatedForLot.id}") + logWriter.writeRowError(i, "InventoryLot was not created") + } else { + logger.info("Row ${i + 1}: InventoryLot created (ID: ${refreshedStockInLine.inventoryLot?.id}, lotNo: ${refreshedStockInLine.inventoryLot?.lotNo})") + } + + // Step 7b: Second update to RECEIVED with inventoryLotLines to create InventoryLotLine + // Only proceed if warehouse is available (InventoryLotLine requires warehouse) + if (warehouse?.id == null) { + logger.warn("Row ${i + 1}: Warehouse is null (no LocationCode), skipping InventoryLotLine creation for item '$itemCode'") + logWriter.writeRowError(i, "Warehouse is null - InventoryLotLine will not be created") + // Continue without InventoryLotLine - StockTakeLine will not be updated with inventoryLotLineId + } else { + // Warehouse is available, proceed with InventoryLotLine creation + saveStockInLineReq.apply { + id = savedStockInLine.id + status = StockInLineStatus.RECEIVED.status // Explicitly set to RECEIVED + this.dnNo = dnNo + this.acceptedQty = qty + this.acceptQty = qty + this.inventoryLotLines = inventoryLotLines // REQUIRED for InventoryLotLine creation + } + val finalStockInLine = try { + stockInLineService.update(saveStockInLineReq) + } catch (e: Exception) { + logger.error("Row ${i + 1}: Failed to update StockInLine to RECEIVED (for InventoryLotLine) 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 + } + + if (finalStockInLine?.id == null) { + logger.error("Row ${i + 1}: StockInLine update to RECEIVED returned null ID for item '$itemCode'") + logWriter.writeRowError(i, "StockInLine update to RECEIVED returned null") + errorCount++ + errors.add("Row ${i + 1}: StockInLine update to RECEIVED returned null") + continue + } + + logger.info("Row ${i + 1}: Updated StockInLine to RECEIVED (ID: ${finalStockInLine.id}) for item '$itemCode' with lotNo: $lotNo, dnNo: $dnNo") + + + // Step 8: Find InventoryLotLine and update StockTakeLine + // Flush first to ensure InventoryLotLine is persisted + stockInLineRepository.flush() + + // Verify InventoryLotLine was created by querying directly + val inventoryLotLine = 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 + logger.warn("Row ${i + 1}: Failed to find InventoryLotLine - ${e.message}") + // Try alternative: find by InventoryLot + try { + val refreshedStockInLine = stockInLineRepository.findById(finalStockInLine.id!!).orElse(null) + refreshedStockInLine?.inventoryLot?.id?.let { inventoryLotId -> + inventoryLotLineRepository.findAllByInventoryLotId(inventoryLotId) + .firstOrNull { it.warehouse?.id == warehouse.id } + } + } catch (e2: Exception) { + logger.warn("Row ${i + 1}: Alternative search failed - ${e2.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 + if (inventoryLotLine != null) { + logger.info("Row ${i + 1}: InventoryLotLine found (ID: ${inventoryLotLine.id}, qty: ${inventoryLotLine.inQty})") + } else { + logger.warn("Row ${i + 1}: InventoryLotLine not found for StockInLine ${finalStockInLine.id}") + logWriter.writeRowError(i, "InventoryLotLine not found") + } + + 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}") + } + } 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") } - } 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 @@ -936,21 +1136,27 @@ open class ItemsService( 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) + // Check if inventory exists - Find ALL inventory records for this item + val existingInventories = try { + inventoryRepository.findAllByItemIdAndDeletedIsFalse(itemId) } catch (e: Exception) { logger.warn("Row ${i + 1}: Failed to find existing inventory for item '$itemCode' (ID: $itemId) - ${e.message}") - null + emptyList() } - // Delete existing inventory if it exists - if (existingInventory != null) { + // Delete ALL existing inventory records if any exist + if (existingInventories.isNotEmpty()) { try { - inventoryRepository.delete(existingInventory) + val deletedCount = existingInventories.size + existingInventories.forEach { inventory -> + inventoryRepository.delete(inventory) + } inventoryRepository.flush() - logger.info("Row ${i + 1}: Deleted existing inventory record for item '${item.name}' (code: $itemCode)") - logWriter.writeInventoryDeleted(i, existingInventory.id) + logger.info("Row ${i + 1}: Deleted $deletedCount existing inventory record(s) for item '${item.name}' (code: $itemCode)") + if (deletedCount > 1) { + logger.warn("Row ${i + 1}: Found $deletedCount duplicate inventory records for item '$itemCode' - all deleted") + } + logWriter.writeInventoryDeleted(i, existingInventories.first().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") @@ -960,16 +1166,18 @@ open class ItemsService( // Read quantity from Column V (will be used for onHandQty) val stockTakeCount = try { val cell = row.getCell(COLUMN_ON_HOLD_QTY_INDEX) - when { + val extractedValue = 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 strValue.toBigDecimalOrNull() ?: BigDecimal.ZERO } else -> BigDecimal.ZERO } + // Add 500 to the extracted value + extractedValue + BigDecimal(500) } catch (e: Exception) { logger.warn("Row ${i + 1}: Failed to read quantity from column V - ${e.message}") BigDecimal.ZERO @@ -1032,12 +1240,13 @@ open class ItemsService( errorCount = errorCount, inventoryCreatedCount = inventoryCreatedCount, inventoryFailedCount = inventoryFailedCount, + duplicateSkippedCount = duplicateSkippedCount, missingInventoryItems = missingInventoryItems, errors = errors ) logger.info("--------- End - Patch Items from Excel -------") - logger.info("Success: $successCount, Errors: $errorCount") + logger.info("Success: $successCount, Errors: $errorCount, Duplicates Skipped: $duplicateSkippedCount") logger.info("Inventory Created: $inventoryCreatedCount, Inventory Failed: $inventoryFailedCount") logger.info("Log file saved to: ${logWriter.getLogFilePath()}") diff --git a/src/main/java/com/ffii/fpsms/modules/master/service/WarehouseService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/WarehouseService.kt index cb5e56c..bd55024 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/service/WarehouseService.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/service/WarehouseService.kt @@ -342,4 +342,15 @@ open class WarehouseService( // calculate total sort value: floor * 10000 + letter * 100 + slot return floorOrder * 10000L + letterOrder * 100L + slot } + + open fun findByCodeAndStockTakeTableAndCompanyAndStoreLocation( + code: String, + stockTakeTable: String, + company: String, + storeLocation: String + ): Warehouse? { + return warehouseRepository.findByCodeAndStockTakeTableAndCompanyAndStoreLocationAndDeletedIsFalse( + code, stockTakeTable, company, storeLocation + ) + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryRepository.kt index e5cb69e..98cf7fb 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryRepository.kt @@ -26,4 +26,7 @@ interface InventoryRepository: AbstractRepository { fun findInventoryInfoByItemInAndDeletedIsFalse(items: List): List fun findByItemId(itemId: Long): Optional + + @Query("SELECT i FROM Inventory i WHERE i.item.id = :itemId AND i.deleted = false") + fun findAllByItemIdAndDeletedIsFalse(itemId: Long): List } \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/20251203_01_KelvinY/01_add_company_to_items.sql b/src/main/resources/db/changelog/changes/20251203_01_KelvinY/01_add_company_to_items.sql new file mode 100644 index 0000000..de8e7bb --- /dev/null +++ b/src/main/resources/db/changelog/changes/20251203_01_KelvinY/01_add_company_to_items.sql @@ -0,0 +1,5 @@ +-- liquibase formatted sql +-- changeset KelvinY:add_company_to_items + +ALTER TABLE `fpsmsdb`.`items` +ADD COLUMN `company` VARCHAR(30) NULL; \ No newline at end of file