From 74d089a767e7418cc28aceb27a3830a9bbfd8475 Mon Sep 17 00:00:00 2001 From: "kelvin.yau" Date: Wed, 3 Dec 2025 11:58:03 +0800 Subject: [PATCH 1/3] 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 From fb33e522c78fd7e21c88d26db61aa6481e6e7361 Mon Sep 17 00:00:00 2001 From: anna Date: Wed, 3 Dec 2025 22:29:31 +0800 Subject: [PATCH 2/3] stock issue --- .../entity/PickExecutionIssueRepository.kt | 28 +++++++++++++++++-- .../pickOrder/enums/PickExecutionIssueEnum.kt | 8 ++++++ .../service/PickExecutionIssueService.kt | 11 ++++++++ .../web/PickExecutionIssueController.kt | 9 ++++++ 4 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/ffii/fpsms/modules/pickOrder/enums/PickExecutionIssueEnum.kt diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssueRepository.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssueRepository.kt index 783e684..552417a 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssueRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/entity/PickExecutionIssueRepository.kt @@ -2,9 +2,11 @@ package com.ffii.fpsms.modules.pickOrder.entity import org.springframework.data.jpa.repository.JpaRepository -import org.springframework.stereotype.Repository import org.springframework.data.jpa.repository.Query import org.springframework.data.repository.query.Param +import org.springframework.stereotype.Repository +import com.ffii.fpsms.modules.pickOrder.enums.PickExecutionIssueEnum + @Repository interface PickExecutionIssueRepository : JpaRepository { fun findByPickOrderIdAndDeletedFalse(pickOrderId: Long): List @@ -53,4 +55,26 @@ fun findDoIssues(): List ORDER BY p.created DESC """) fun findMaterialIssues(): List -} \ No newline at end of file + + @Query(""" + SELECT p FROM PickExecutionIssue p + WHERE p.badItemQty IS NOT NULL + AND p.badItemQty > 0 + AND p.deleted = false + AND p.handleStatus IN (:statuses) + ORDER BY p.created DESC +""") + fun getBadItemList_statusIn(@Param("statuses") statuses: List): List + + @Query(""" + SELECT p FROM PickExecutionIssue p + WHERE p.badItemQty IS NOT NULL + AND p.badItemQty > 0 + AND p.deleted = false + ORDER BY p.created DESC +""") + fun getBadItemList(): List + +} + + diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/enums/PickExecutionIssueEnum.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/enums/PickExecutionIssueEnum.kt new file mode 100644 index 0000000..de3a72f --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/enums/PickExecutionIssueEnum.kt @@ -0,0 +1,8 @@ +package com.ffii.fpsms.modules.pickOrder.enums + +enum class PickExecutionIssueEnum (val value: String) { + PENDING("pending"), + SORT_AND_REPAIR("Sort and Repair"), + Dispose("Dispose"); +} + diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt index 040ff1a..ec67c0f 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/service/PickExecutionIssueService.kt @@ -27,6 +27,7 @@ import com.ffii.fpsms.modules.pickOrder.entity.PickOrderLineRepository import com.ffii.fpsms.modules.deliveryOrder.service.DoPickOrderService import com.ffii.fpsms.modules.jobOrder.entity.JoPickOrderRepository import com.ffii.fpsms.modules.jobOrder.entity.JoPickOrderRecordRepository +import com.ffii.fpsms.modules.pickOrder.enums.PickExecutionIssueEnum import com.ffii.fpsms.modules.stock.web.model.StockOutLineStatus import com.ffii.fpsms.modules.stock.web.model.StockOutStatus import com.ffii.fpsms.modules.pickOrder.enums.PickOrderLineStatus @@ -631,6 +632,16 @@ private fun handleBothMissAndBadItem(request: PickExecutionIssueRequest, missQty open fun getPickExecutionIssuesByPickOrderLine(pickOrderLineId: Long): List { return pickExecutionIssueRepository.findByPickOrderLineIdAndDeletedFalse(pickOrderLineId) } + + open fun getBadItemList(status: PickExecutionIssueEnum? = null): List{ + if(status == null){ + return pickExecutionIssueRepository.getBadItemList(); + }else{ + val statuses = listOf(status); + return pickExecutionIssueRepository.getBadItemList_statusIn(statuses); + } + } + open fun getAllPickExecutionIssues(type: String? = null): List { println("=== getAllPickExecutionIssues Debug ===") println("Requested type: $type") diff --git a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickExecutionIssueController.kt b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickExecutionIssueController.kt index 3ae2501..a27ffe3 100644 --- a/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickExecutionIssueController.kt +++ b/src/main/java/com/ffii/fpsms/modules/pickOrder/web/PickExecutionIssueController.kt @@ -3,6 +3,7 @@ package com.ffii.fpsms.modules.pickOrder.web import com.ffii.fpsms.modules.master.web.models.MessageResponse import com.ffii.fpsms.modules.pickOrder.entity.PickExecutionIssue +import com.ffii.fpsms.modules.pickOrder.enums.PickExecutionIssueEnum import com.ffii.fpsms.modules.pickOrder.service.PickExecutionIssueService // 修复导入路径 import com.ffii.fpsms.modules.stock.web.model.PickExecutionIssueRequest import org.springframework.web.bind.annotation.* @@ -34,4 +35,12 @@ class PickExecutionIssueController( println("=== Controller received type parameter: $type ===") return pickExecutionIssueService.getAllPickExecutionIssues(type) } + + @GetMapping("/badItemList") + fun getBadItemList( + @RequestParam(required = false) status: PickExecutionIssueEnum? + ): List { + + return pickExecutionIssueService.getBadItemList(status) + } } \ No newline at end of file From 373f3d844d948381287436edece8394a629b6c53 Mon Sep 17 00:00:00 2001 From: "kelvin.yau" Date: Thu, 4 Dec 2025 19:44:49 +0800 Subject: [PATCH 3/3] updated import stock take function, supporting function to print all QR codes by searching DO ID --- .../web/DeliveryOrderController.kt | 8 +++++ .../modules/master/service/ItemsService.kt | 23 +++++++++++--- .../stock/entity/StockInLineRepository.kt | 4 +++ .../stock/service/StockInLineService.kt | 30 ++++++++++++++++++- .../web/model/PrintQrCodeForDoRequest.kt | 7 +++++ 5 files changed, 67 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/ffii/fpsms/modules/stock/web/model/PrintQrCodeForDoRequest.kt diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt index 635a574..5812e5e 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/web/DeliveryOrderController.kt @@ -33,11 +33,14 @@ import java.io.OutputStream import java.io.UnsupportedEncodingException import java.text.ParseException import java.time.LocalDateTime +import com.ffii.fpsms.modules.deliveryOrder.web.models.PrintQrCodeForDoRequest +import com.ffii.fpsms.modules.stock.service.StockInLineService @RequestMapping("/do") @RestController class DeliveryOrderController( private val deliveryOrderService: DeliveryOrderService, + private val stockInLineService: StockInLineService, ) { @GetMapping("/list") fun getDoList(): List { @@ -217,4 +220,9 @@ class DeliveryOrderController( fun printDN(@ModelAttribute request: PrintDNLabelsRequest) { deliveryOrderService.printDNLabels(request) } + + @GetMapping("/batchPrintQrCode") + fun printQrCodeForDeliveryOrder(@ModelAttribute request: PrintQrCodeForDoRequest) { + stockInLineService.printQrCodeForDeliveryOrder(request) + } } \ No newline at end of file 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 92cadca..50c8652 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 @@ -25,6 +25,7 @@ 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.InventoryLotRepository import com.ffii.fpsms.modules.stock.entity.InventoryRepository import com.ffii.fpsms.modules.stock.entity.StockIn import com.ffii.fpsms.modules.stock.entity.StockInLineRepository @@ -61,6 +62,7 @@ open class ItemsService( private val stockInLineService: StockInLineService, private val stockInLineRepository: StockInLineRepository, private val inventoryLotLineRepository: InventoryLotLineRepository, + private val inventoryLotRepository: InventoryLotRepository, ): AbstractBaseEntityService(jdbcDao, itemsRepository) { private val excelImportPath: String = System.getProperty("user.home") + "/Downloads/StockTakeImport/" @@ -908,9 +910,12 @@ open class ItemsService( // Calculate expiry date (+30 days) val expiryDate = LocalDateTime.now().plusDays(30).toLocalDate() - // Generate lotNo: LT-YYYYMMDD-itemCode + // Generate productLotNo: LT-YYYYMMDD-itemCode (for productLotNo field) val dateStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) - val lotNo = "LT-$dateStr-$itemCode" + val productLotNo = "LT-$dateStr-$itemCode" + + // Generate lotNo: LT-YYYYMMDD-XXXX (sequential number, same for StockInLine and InventoryLot) + val lotNo = stockInLineService.assignLotNo() // Generate dnNo: DN-YYYYMMDD-itemCode val dnNo = "DN-$dateStr-$itemCode" @@ -924,7 +929,8 @@ open class ItemsService( expiryDate = expiryDate, warehouseId = warehouse?.id, stockTakeLineId = savedStockTakeLine.id, - dnNo = dnNo, // Set dnNo + dnNo = dnNo, + productLotNo = productLotNo, // LT-YYYYMMDD-itemCode qcAccept = true, status = StockInLineStatus.PENDING.status ) @@ -970,6 +976,7 @@ open class ItemsService( id = savedStockInLine.id status = StockInLineStatus.PENDING.status this.dnNo = dnNo + this.productLotNo = lotNo this.qcAccept = true this.acceptedQty = qty this.acceptQty = qty @@ -1002,7 +1009,14 @@ open class ItemsService( 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})") + // Update InventoryLot's lotNo to match StockInLine's lotNo (LT-YYYYMMDD-XXXX format) + val inventoryLot = refreshedStockInLine.inventoryLot!! + if (inventoryLot.lotNo != lotNo) { + inventoryLot.lotNo = lotNo + inventoryLotRepository.saveAndFlush(inventoryLot) + logger.info("Row ${i + 1}: Updated InventoryLot lotNo to '$lotNo' to match StockInLine (InventoryLot ID: ${inventoryLot.id})") + } + logger.info("Row ${i + 1}: InventoryLot created (ID: ${inventoryLot.id}, lotNo: ${inventoryLot.lotNo})") } // Step 7b: Second update to RECEIVED with inventoryLotLines to create InventoryLotLine @@ -1017,6 +1031,7 @@ open class ItemsService( id = savedStockInLine.id status = StockInLineStatus.RECEIVED.status // Explicitly set to RECEIVED this.dnNo = dnNo + this.productLotNo = productLotNo this.acceptedQty = qty this.acceptQty = qty this.inventoryLotLines = inventoryLotLines // REQUIRED for InventoryLotLine creation diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLineRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLineRepository.kt index 6d38b50..f81d440 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLineRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockInLineRepository.kt @@ -3,6 +3,7 @@ package com.ffii.fpsms.modules.stock.entity import com.ffii.core.support.AbstractRepository import com.ffii.fpsms.modules.stock.entity.projection.QrCodeInfo import com.ffii.fpsms.modules.stock.entity.projection.StockInLineInfo +import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository import java.util.Optional @@ -19,4 +20,7 @@ interface StockInLineRepository : AbstractRepository { fun findAllByPurchaseOrderIdAndDeletedFalse(purchaseOrderId: Long): Optional> fun findStockInLineInfoByInventoryLotLineId(inventoryLotLineId: Long): Optional fun findStockInLineInfoById(id: Long): Optional + + @Query("SELECT sil FROM StockInLine sil WHERE sil.item.id = :itemId AND sil.deleted = false") + fun findAllByItemIdAndDeletedFalse(itemId: Long): List } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt index 226aff1..f776758 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt @@ -46,7 +46,8 @@ import net.sf.jasperreports.engine.JasperExportManager import net.sf.jasperreports.engine.JasperPrint import java.io.File import kotlin.jvm.optionals.getOrNull -import kotlin.math.max +import com.ffii.fpsms.modules.deliveryOrder.entity.DeliveryOrderRepository +import com.ffii.fpsms.modules.deliveryOrder.web.models.PrintQrCodeForDoRequest @Serializable data class QrContent(val itemId: Long, val stockInLineId: Long) @@ -71,6 +72,7 @@ open class StockInLineService( private val itemUomRepository: ItemUomRespository, private val printerService: PrinterService, private val stockTakeLineRepository: StockTakeLineRepository, + private val deliveryOrderRepository: DeliveryOrderRepository, ): AbstractBaseEntityService(jdbcDao, stockInLineRepository) { open fun getStockInLineInfo(stockInLineId: Long): StockInLineInfo { @@ -577,4 +579,30 @@ open class StockInLineService( tempPdfFile.delete() } } + + @Transactional + open fun printQrCodeForDeliveryOrder(request: PrintQrCodeForDoRequest) { + // Get delivery order by ID + val deliveryOrder = deliveryOrderRepository.findByIdAndDeletedIsFalse(request.deliveryOrderId) + ?: throw NoSuchElementException("Delivery order not found with ID: ${request.deliveryOrderId}") + + // Loop through delivery order lines + deliveryOrder.deliveryOrderLines.forEach { deliveryOrderLine -> + val itemId = deliveryOrderLine.item?.id + ?: throw IllegalStateException("Delivery order line ${deliveryOrderLine.id} has no item") + + // Find all stock in lines matching this itemId + val stockInLines = stockInLineRepository.findAllByItemIdAndDeletedFalse(itemId) + + // Print QR code for each stock in line + stockInLines.forEach { stockInLine -> + val printRequest = PrintQrCodeForSilRequest( + stockInLineId = stockInLine.id!!, + printerId = request.printerId, + printQty = request.printQty + ) + printQrCode(printRequest) + } + } + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/PrintQrCodeForDoRequest.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/PrintQrCodeForDoRequest.kt new file mode 100644 index 0000000..7d55449 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/PrintQrCodeForDoRequest.kt @@ -0,0 +1,7 @@ +package com.ffii.fpsms.modules.deliveryOrder.web.models + +data class PrintQrCodeForDoRequest( + val deliveryOrderId: Long, + val printerId: Long, + val printQty: Int?, +) \ No newline at end of file