From ceda8828576bb6d6349e9a9e37dbd73f01834328 Mon Sep 17 00:00:00 2001 From: "kelvin.yau" Date: Mon, 19 Jan 2026 09:59:38 +0800 Subject: [PATCH] setup functions + bug fix --- .../com/ffii/core/utils/ZebraPrinterUtil.kt | 6 +- .../common/internalSetup/SetupController.kt | 167 ++++++++++ .../common/internalSetup/inventorySetup.kt | 293 ++++++++++++++++++ .../modules/common/internalSetup/itemSetup.kt | 155 +++++++++ .../{usersSetup.kt => userSetup.kt} | 4 +- .../common/internalSetup/warehouseSetup.kt | 167 ++++++++++ .../service/DeliveryOrderService.kt | 3 +- .../jobOrder/service/JoPickOrderService.kt | 6 +- .../jobOrder/service/JobOrderService.kt | 3 +- .../fpsms/modules/master/entity/Warehouse.kt | 4 +- .../modules/master/service/BomService.kt | 4 +- .../master/service/WarehouseService.kt | 30 +- .../master/web/models/SaveWarehouseRequest.kt | 4 +- .../entity/InventoryLotLineRepository.kt | 18 ++ .../stock/service/StockInLineService.kt | 205 ++++++++++-- .../stock/service/StockOutLineService.kt | 51 +++ .../service/StockTransferRecordService.kt | 180 +++++++++++ .../web/StockTransferRecordController.kt | 18 ++ .../web/model/CreateStockTransferRequest.kt | 9 + .../modules/stock/web/model/StockInRequest.kt | 17 + .../stock/web/model/StockOutRequest.kt | 7 + .../modules/user/web/UserController.java | 35 +-- .../02_cheduler_log query copy.sql | 5 - .../02_scheduler_log_query_copy.sql | 8 + 24 files changed, 1300 insertions(+), 99 deletions(-) create mode 100644 src/main/java/com/ffii/fpsms/modules/common/internalSetup/SetupController.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/common/internalSetup/inventorySetup.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/common/internalSetup/itemSetup.kt rename src/main/java/com/ffii/fpsms/modules/common/internalSetup/{usersSetup.kt => userSetup.kt} (98%) create mode 100644 src/main/java/com/ffii/fpsms/modules/common/internalSetup/warehouseSetup.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/stock/web/StockTransferRecordController.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/stock/web/model/CreateStockTransferRequest.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/stock/web/model/StockInRequest.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/stock/web/model/StockOutRequest.kt delete mode 100644 src/main/resources/db/changelog/changes/20260119_fai/02_cheduler_log query copy.sql create mode 100644 src/main/resources/db/changelog/changes/20260119_fai/02_scheduler_log_query_copy.sql diff --git a/src/main/java/com/ffii/core/utils/ZebraPrinterUtil.kt b/src/main/java/com/ffii/core/utils/ZebraPrinterUtil.kt index f86c3f0..fbefae4 100644 --- a/src/main/java/com/ffii/core/utils/ZebraPrinterUtil.kt +++ b/src/main/java/com/ffii/core/utils/ZebraPrinterUtil.kt @@ -35,13 +35,13 @@ open class ZebraPrinterUtil { * @param printDirection Valid values: N (Normal), R (Rotated 90), I (Inverted 180), B (Bottom-up 270) * @throws Exception if there is an error during file processing or printing. */ - fun printPdfToZebra(pdfFile: File, printerIp: String, printerPort: Int, printQty: Int? = 1, printDirection: PrintDirection = PrintDirection.NORMAL) { + fun printPdfToZebra(pdfFile: File, printerIp: String, printerPort: Int, printQty: Int? = 1, printDirection: PrintDirection = PrintDirection.NORMAL, dpi: Int? = 203) { // Check if the file exists and is readable if (!pdfFile.exists() || !pdfFile.canRead()) { throw IllegalArgumentException("Error: File not found or not readable at path: ${pdfFile.absolutePath}") } - + val renderDpi = dpi ?: 203 try { // 1. Load the PDF document PDDocument.load(pdfFile).use { document -> @@ -59,7 +59,7 @@ open class ZebraPrinterUtil { println("DEBUG: Processing page ${pageIndex + 1} of $totalPages") // 2. Render each page of the PDF as a monochrome image - val image = renderer.renderImage(pageIndex, 203 / 72f, ImageType.BINARY) + val image = renderer.renderImage(pageIndex, renderDpi / 72f, ImageType.BINARY) // 3. Convert the image to a ZPL format string val zplCommand = convertImageToZpl(image, printDirection) diff --git a/src/main/java/com/ffii/fpsms/modules/common/internalSetup/SetupController.kt b/src/main/java/com/ffii/fpsms/modules/common/internalSetup/SetupController.kt new file mode 100644 index 0000000..dc03097 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/common/internalSetup/SetupController.kt @@ -0,0 +1,167 @@ +package com.ffii.fpsms.modules.common.internalSetup + +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/setup") +class SetupController( + private val userSetup: UserSetup, + private val warehouseSetup: WarehouseSetup, + private val itemsSetup: ItemsSetup, + private val inventorySetup: InventorySetup +) { + + @PostMapping("/user") + fun importUsersFromExcel(@RequestBody request: Map): ResponseEntity> { + val filePath = request["filePath"] + + if (filePath == null || filePath.isEmpty()) { + val errorResponse = mapOf( + "success" to false, + "error" to "filePath is required" + ) + return ResponseEntity.badRequest().body(errorResponse) + } + + try { + val createdCount = userSetup.importExcelFromLocal(filePath) + val response = mapOf( + "success" to true, + "message" to "Users imported successfully", + "createdCount" to createdCount + ) + return ResponseEntity.ok(response) + } catch (e: Exception) { + val errorResponse = mapOf( + "success" to false, + "error" to (e.message ?: "Unknown error occurred") + ) + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse) + } + } + + @PostMapping("/warehouse") + fun importWarehousesFromExcel(@RequestBody request: Map): ResponseEntity> { + val filePath = request["filePath"] + + if (filePath == null || filePath.isEmpty()) { + val errorResponse = mapOf( + "success" to false, + "error" to "filePath is required" + ) + return ResponseEntity.badRequest().body(errorResponse) + } + + try { + val createdCount = warehouseSetup.importExcelFromLocal(filePath) + val response = mapOf( + "success" to true, + "message" to "Warehouses imported successfully", + "createdCount" to createdCount + ) + return ResponseEntity.ok(response) + } catch (e: Exception) { + val errorResponse = mapOf( + "success" to false, + "error" to (e.message ?: "Unknown error occurred") + ) + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse) + } + } + + @PostMapping("/item") + fun importItemsFromExcel(@RequestBody request: Map): ResponseEntity> { + val filePath = request["filePath"] + + if (filePath == null || filePath.isEmpty()) { + val errorResponse = mapOf( + "success" to false, + "error" to "filePath is required" + ) + return ResponseEntity.badRequest().body(errorResponse) + } + + try { + val updatedCount = itemsSetup.importExcelFromLocal(filePath) + val response = mapOf( + "success" to true, + "message" to "Items imported successfully", + "updatedCount" to updatedCount + ) + return ResponseEntity.ok(response) + } catch (e: Exception) { + val errorResponse = mapOf( + "success" to false, + "error" to (e.message ?: "Unknown error occurred") + ) + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse) + } + } + + @PostMapping("/inventory") + fun importInventoryFromExcel(@RequestBody request: Map): ResponseEntity> { + val filePath = request["filePath"] + + if (filePath == null || filePath.isEmpty()) { + val errorResponse = mapOf( + "success" to false, + "error" to "filePath is required" + ) + return ResponseEntity.badRequest().body(errorResponse) + } + + try { + val createdCount = inventorySetup.importExcelFromLocal(filePath) + val response = mapOf( + "success" to true, + "message" to "Inventory stock in created successfully", + "createdCount" to createdCount + ) + return ResponseEntity.ok(response) + } catch (e: Exception) { + val errorResponse = mapOf( + "success" to false, + "error" to (e.message ?: "Unknown error occurred") + ) + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse) + } + } + + @PostMapping("/inventory/print-all-lot-stockin-labels") + fun printAllLotStockInLabels(@RequestBody request: Map): ResponseEntity> { + val printerId = request["printerId"] as? Long + val printQty = (request["printQty"] as? Number)?.toInt() ?: 1 + + if (printerId == null) { + val errorResponse = mapOf( + "success" to false, + "error" to "printerId is required" + ) + return ResponseEntity.badRequest().body(errorResponse) + } + + try { + val printedCount = inventorySetup.printAllLotStockInLabels( + printerId = printerId, + printQty = printQty + ) + val response = mapOf( + "success" to true, + "message" to "Lot stock-in labels printed successfully", + "printedCount" to printedCount + ) + return ResponseEntity.ok(response) + } catch (e: Exception) { + val errorResponse = mapOf( + "success" to false, + "error" to (e.message ?: "Unknown error occurred") + ) + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse) + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/common/internalSetup/inventorySetup.kt b/src/main/java/com/ffii/fpsms/modules/common/internalSetup/inventorySetup.kt new file mode 100644 index 0000000..5eb4dfb --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/common/internalSetup/inventorySetup.kt @@ -0,0 +1,293 @@ +package com.ffii.fpsms.modules.common.internalSetup + +import com.ffii.core.utils.ExcelUtils +import org.apache.poi.ss.usermodel.Sheet +import org.apache.poi.ss.usermodel.Workbook +import org.apache.poi.xssf.usermodel.XSSFWorkbook +import org.springframework.stereotype.Component +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.transaction.annotation.Transactional +import com.ffii.fpsms.modules.master.entity.ItemsRepository +import com.ffii.fpsms.modules.master.entity.WarehouseRepository +import com.ffii.fpsms.modules.stock.service.StockInLineService +import com.ffii.fpsms.modules.stock.web.model.StockInRequest +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.math.BigDecimal +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository +import com.ffii.fpsms.modules.stock.web.model.PrintQrCodeForSilRequest + +@Component +open class InventorySetup { + + @Autowired + private lateinit var itemsRepository: ItemsRepository + + @Autowired + private lateinit var warehouseRepository: WarehouseRepository + + @Autowired + private lateinit var stockInLineService: StockInLineService + + private val DEFAULT_WAREHOUSE_CODE = "2F-W201-#A-01" + private val DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy/MM/dd") + + @Transactional(rollbackFor = [Exception::class]) + open fun importExcelFromLocal(filePath: String): Int { + val file = File(filePath) + if (!file.exists()) { + throw IOException("File not found: $filePath") + } + + FileInputStream(file).use { inputStream -> + val workbook: Workbook = XSSFWorkbook(inputStream) + + try { + val sheet: Sheet = workbook.getSheetAt(0) + val inventoryDataList = mutableListOf() + + // Column indices + val COLUMN_ITEM_NO_INDEX = 6 // Column G + val COLUMN_STORE_ID_INDEX = 14 // Column O + val COLUMN_WAREHOUSE_INDEX = 16 // Column Q + val COLUMN_AREA_INDEX = 17 // Column R + val COLUMN_SLOT_INDEX = 18 // Column S + val COLUMN_PRODUCT_LOT_NO_INDEX = 21 // Column V + val COLUMN_QUANTITY_INDEX = 22 // Column W + val COLUMN_EXPIRY_DATE_INDEX = 26 // Column AA + + val START_ROW_INDEX = 2 // Start from Excel row 3 (0-based: index 2) + + // ──────────────────────────────────────────────── + // Process data rows (starting from Excel row 3, index 2) + // ──────────────────────────────────────────────── + for (rowIndex in START_ROW_INDEX..sheet.lastRowNum) { + val row = sheet.getRow(rowIndex) ?: continue + + val itemNo = ExcelUtils.getStringValue(row.getCell(COLUMN_ITEM_NO_INDEX))?.trim() ?: "" + + // Skip if itemNo is empty (required field) + if (itemNo.isEmpty()) { + println("SKIP row ${rowIndex + 1} - Missing itemNo") + continue + } + + // Read warehouse location fields + val store_id = ExcelUtils.getStringValue(row.getCell(COLUMN_STORE_ID_INDEX))?.trim() ?: "" + val warehouse = ExcelUtils.getStringValue(row.getCell(COLUMN_WAREHOUSE_INDEX))?.trim() ?: "" + val area = ExcelUtils.getStringValue(row.getCell(COLUMN_AREA_INDEX))?.trim() ?: "" + val slot = ExcelUtils.getStringValue(row.getCell(COLUMN_SLOT_INDEX))?.trim() ?: "" + + // Generate LocationCode: store_id-warehouse-area-slot + val locationCode = if (store_id.isNotEmpty() && warehouse.isNotEmpty() && + area.isNotEmpty() && slot.isNotEmpty()) { + "$store_id-$warehouse-$area-$slot" + } else { + "" + } + + val quantityStr = ExcelUtils.getStringValue(row.getCell(COLUMN_QUANTITY_INDEX))?.trim() + val quantity = quantityStr?.toBigDecimalOrNull() + + if (quantity == null || quantity <= BigDecimal.ZERO) { + println("SKIP row ${rowIndex + 1} - Invalid quantity: $quantityStr") + continue + } + + val productLotNo = ExcelUtils.getStringValue(row.getCell(COLUMN_PRODUCT_LOT_NO_INDEX))?.trim() + + // Parse expiry date with fallback to default (1 month later) + val expiryDateStr = ExcelUtils.getStringValue(row.getCell(COLUMN_EXPIRY_DATE_INDEX))?.trim() + val expiryDate = expiryDateStr?.let { + try { + LocalDate.parse(it, DATE_FORMAT) + } catch (e: Exception) { + println("WARNING row ${rowIndex + 1} - Invalid expiry date format: $it (expected YYYY/MM/DD), using default: 1 month later") + null + } + } ?: run { + // If expiryDateStr is null or empty, or parsing failed, use default + LocalDate.now().plusMonths(1) + } + + // If parsing failed, use default + val finalExpiryDate = expiryDate ?: LocalDate.now().plusMonths(1) + + println("=== Processing row ${rowIndex + 1} (itemNo: $itemNo, qty: $quantity, expiryDate: $finalExpiryDate, locationCode: ${locationCode.ifEmpty { "DEFAULT" }}) ===") + + inventoryDataList.add( + InventoryData( + itemNo = itemNo, + locationCode = locationCode, + quantity = quantity, + productLotNo = productLotNo, + expiryDate = finalExpiryDate + ) + ) + } + + println("Total valid rows collected: ${inventoryDataList.size}") + + // ──────────────────────────────────────────────── + // Create stock in for each item + // ──────────────────────────────────────────────── + var createdCount = 0 + var notFoundCount = 0 + var warehouseNotFoundCount = 0 + + for (data in inventoryDataList) { + try { + // Find item by code (itemNo) + val item = itemsRepository.findByCodeAndDeletedFalse(data.itemNo) + + if (item == null) { + println("NOT FOUND - Item with code '${data.itemNo}' does not exist") + notFoundCount++ + continue + } + + if (item.id == null) { + println("ERROR - Item '${data.itemNo}' has no ID") + continue + } + + // Find warehouse by LocationCode, fallback to default if not found + var warehouse = if (data.locationCode.isNotEmpty()) { + warehouseRepository.findByCodeAndDeletedIsFalse(data.locationCode) + } else { + null + } + + // If warehouse not found, use default warehouse + if (warehouse == null) { + println("WAREHOUSE NOT FOUND - Warehouse with code '${data.locationCode}' does not exist, using default: $DEFAULT_WAREHOUSE_CODE") + warehouse = warehouseRepository.findByCodeAndDeletedIsFalse(DEFAULT_WAREHOUSE_CODE) + warehouseNotFoundCount++ + + if (warehouse == null) { + println("ERROR - Default warehouse '$DEFAULT_WAREHOUSE_CODE' also not found in database") + continue + } + } + + if (warehouse.id == null) { + println("ERROR - Warehouse '${warehouse.code}' has no ID") + continue + } + + // Generate dnNo: LT-YYYYMMDD-itemCode + val today = LocalDate.now() + val dnNo = "LT-${today.format(DateTimeFormatter.ofPattern("yyyyMMdd"))}-${data.itemNo}" + + // Use generic stock in + val stockInRequest = StockInRequest( + itemId = item.id!!, + itemNo = data.itemNo, + demandQty = data.quantity, + acceptedQty = data.quantity, + expiryDate = data.expiryDate, + lotNo = null, // System will auto-generate + productLotNo = data.productLotNo, + dnNo = dnNo, + type = "OPEN", + warehouseId = warehouse.id!! + ) + + stockInLineService.createStockIn(stockInRequest) + createdCount++ + println("CREATED stock in for item: ${data.itemNo} (dnNo: $dnNo, warehouse: ${warehouse.code})") + } catch (e: Exception) { + println("Error processing inventory row (itemNo: ${data.itemNo}): ${e.message}") + e.printStackTrace() + continue + } + } + + println("Import finished. Created $createdCount stock in records, Not found: $notFoundCount items, Warehouse not found (used default): $warehouseNotFoundCount.") + return createdCount + + } finally { + workbook.close() + } + } + } + + data class InventoryData( + val itemNo: String, + val locationCode: String, + val quantity: BigDecimal, + val productLotNo: String?, + val expiryDate: LocalDate + ) + + @Autowired + private lateinit var inventoryLotLineRepository: InventoryLotLineRepository + @Transactional(rollbackFor = [Exception::class]) + open fun printAllLotStockInLabels(printerId: Long, printQty: Int = 1): Int { + println("=== Starting to print stock in labels for all lots ===") + + // Get all inventory lot lines that are not deleted and have stock in lines + val allInventoryLotLines = try { + inventoryLotLineRepository.findAllByDeletedIsFalseAndHasStockInLine() + } catch (e: Exception) { + println("Error fetching inventory lot lines: ${e.message}") + e.printStackTrace() + return 0 + } + + if (allInventoryLotLines.isEmpty()) { + println("No inventory lot lines found to print") + return 0 + } + + println("Found ${allInventoryLotLines.size} inventory lot lines to print") + + var printedCount = 0 + var errorCount = 0 + + // Loop through each inventory lot line + for ((index, inventoryLotLine) in allInventoryLotLines.withIndex()) { + try { + // Get the associated stock in line + val stockInLine = inventoryLotLine.inventoryLot?.stockInLine + ?: throw IllegalArgumentException("No stock in line associated with inventory lot line ID: ${inventoryLotLine.id}") + + val stockInLineId = stockInLine.id + ?: throw IllegalArgumentException("Stock in line has no ID") + + println("Processing lot ${index + 1}/${allInventoryLotLines.size}: Lot No: ${inventoryLotLine.inventoryLot?.lotNo}, StockInLineId: $stockInLineId") + + // Create print request + val printRequest = PrintQrCodeForSilRequest( + stockInLineId = stockInLineId, + printerId = printerId, + printQty = printQty + ) + + // Print the label + stockInLineService.printQrCode(printRequest) + printedCount++ + + println("✓ Successfully printed label for lot: ${inventoryLotLine.inventoryLot?.lotNo}") + + } catch (e: Exception) { + errorCount++ + println("✗ Error printing label for inventory lot line ID ${inventoryLotLine.id}: ${e.message}") + e.printStackTrace() + // Continue with next lot instead of stopping + continue + } + } + + println("=== Printing finished ===") + println("Total processed: ${allInventoryLotLines.size}") + println("Successfully printed: $printedCount") + println("Errors: $errorCount") + + return printedCount + } + +} \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/common/internalSetup/itemSetup.kt b/src/main/java/com/ffii/fpsms/modules/common/internalSetup/itemSetup.kt new file mode 100644 index 0000000..0f238f3 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/common/internalSetup/itemSetup.kt @@ -0,0 +1,155 @@ +package com.ffii.fpsms.modules.common.internalSetup + +import com.ffii.core.utils.ExcelUtils +import org.apache.poi.ss.usermodel.Sheet +import org.apache.poi.ss.usermodel.Workbook +import org.apache.poi.xssf.usermodel.XSSFWorkbook +import org.springframework.stereotype.Component +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.transaction.annotation.Transactional +import com.ffii.fpsms.modules.master.entity.Items +import com.ffii.fpsms.modules.master.entity.ItemsRepository +import java.io.File +import java.io.FileInputStream +import java.io.IOException + +@Component +open class ItemsSetup { + + @Autowired + private lateinit var itemsRepository: ItemsRepository + + data class ItemsData( + val code: String, + val type: String?, + val store_id: String?, + val warehouse: String?, + val area: String?, + val slot: String? + ) + + @Transactional(rollbackFor = [Exception::class]) + open fun importExcelFromLocal(filePath: String): Int { + val file = File(filePath) + if (!file.exists()) { + throw IOException("File not found: $filePath") + } + + FileInputStream(file).use { inputStream -> + val workbook: Workbook = XSSFWorkbook(inputStream) + + try { + val sheet: Sheet = workbook.getSheetAt(0) + val itemsDataList = mutableListOf() + + // Column indices + val COLUMN_CODE_INDEX = 0 // Column A + val COLUMN_TYPE_INDEX = 2 // Column C + val COLUMN_STORE_ID_INDEX = 3 // Column D + val COLUMN_WAREHOUSE_INDEX = 5 // Column F + val COLUMN_AREA_INDEX = 6 // Column G + val COLUMN_SLOT_INDEX = 7 // Column H + + val START_ROW_INDEX = 3 // Start from Excel row 4 (0-based: index 3) + + // ──────────────────────────────────────────────── + // Process data rows (starting from Excel row 4, index 3) + // ──────────────────────────────────────────────── + for (rowIndex in START_ROW_INDEX..sheet.lastRowNum) { + val row = sheet.getRow(rowIndex) ?: continue + + val code = ExcelUtils.getStringValue(row.getCell(COLUMN_CODE_INDEX))?.trim() ?: "" + + // Skip if code is empty (required field) + if (code.isEmpty()) { + println("SKIP row ${rowIndex + 1} - Missing code") + continue + } + + val type = ExcelUtils.getStringValue(row.getCell(COLUMN_TYPE_INDEX))?.trim() + val store_id = ExcelUtils.getStringValue(row.getCell(COLUMN_STORE_ID_INDEX))?.trim() + val warehouse = ExcelUtils.getStringValue(row.getCell(COLUMN_WAREHOUSE_INDEX))?.trim() + val area = ExcelUtils.getStringValue(row.getCell(COLUMN_AREA_INDEX))?.trim() + val slot = ExcelUtils.getStringValue(row.getCell(COLUMN_SLOT_INDEX))?.trim() + + println("=== Processing row ${rowIndex + 1} (code: $code, type: $type) ===") + + itemsDataList.add( + ItemsData( + code = code, + type = type, + store_id = store_id, + warehouse = warehouse, + area = area, + slot = slot + ) + ) + } + + println("Total valid rows collected: ${itemsDataList.size}") + + // ──────────────────────────────────────────────── + // Update items + // ──────────────────────────────────────────────── + var updatedCount = 0 + var notFoundCount = 0 + + for (data in itemsDataList) { + try { + // Find item by code + val existingItem = itemsRepository.findByCodeAndDeletedFalse(data.code) + + if (existingItem == null) { + println("NOT FOUND - Item with code '${data.code}' does not exist") + notFoundCount++ + continue + } + + // Generate LocationCode: store_id-warehouse-area-slot + val locationCode = if (data.store_id != null && data.warehouse != null && + data.area != null && data.slot != null) { + "${data.store_id}-${data.warehouse}-${data.area}-${data.slot}" + } else { + null + } + + // Update existing item + existingItem.apply { + if (data.type != null) { + this.type = data.type + } + if (data.store_id != null) { + this.store_id = data.store_id + } + if (data.warehouse != null) { + this.warehouse = data.warehouse + } + if (data.area != null) { + this.area = data.area + } + if (data.slot != null) { + this.slot = data.slot + } + if (locationCode != null) { + this.LocationCode = locationCode + } + } + + itemsRepository.save(existingItem) + updatedCount++ + println("UPDATED item: ${data.code} (LocationCode: $locationCode)") + } catch (e: Exception) { + println("Error processing item row (code: ${data.code}): ${e.message}") + continue + } + } + + println("Import finished. Updated $updatedCount items, Not found: $notFoundCount items.") + return updatedCount + + } finally { + workbook.close() + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/common/internalSetup/usersSetup.kt b/src/main/java/com/ffii/fpsms/modules/common/internalSetup/userSetup.kt similarity index 98% rename from src/main/java/com/ffii/fpsms/modules/common/internalSetup/usersSetup.kt rename to src/main/java/com/ffii/fpsms/modules/common/internalSetup/userSetup.kt index 436c0e2..04b4664 100644 --- a/src/main/java/com/ffii/fpsms/modules/common/internalSetup/usersSetup.kt +++ b/src/main/java/com/ffii/fpsms/modules/common/internalSetup/userSetup.kt @@ -1,8 +1,6 @@ package com.ffii.fpsms.modules.common.internalSetup import com.ffii.core.utils.ExcelUtils -import org.apache.poi.ss.usermodel.Cell -import org.apache.poi.ss.usermodel.Row import org.apache.poi.ss.usermodel.Sheet import org.apache.poi.ss.usermodel.Workbook import org.apache.poi.xssf.usermodel.XSSFWorkbook @@ -17,7 +15,7 @@ import java.io.FileInputStream import java.io.IOException @Component -open class UsersSetup { +open class UserSetup { @Autowired private lateinit var userRepository: UserRepository diff --git a/src/main/java/com/ffii/fpsms/modules/common/internalSetup/warehouseSetup.kt b/src/main/java/com/ffii/fpsms/modules/common/internalSetup/warehouseSetup.kt new file mode 100644 index 0000000..625313b --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/common/internalSetup/warehouseSetup.kt @@ -0,0 +1,167 @@ +package com.ffii.fpsms.modules.common.internalSetup + +import com.ffii.core.utils.ExcelUtils +import org.apache.poi.ss.usermodel.Sheet +import org.apache.poi.ss.usermodel.Workbook +import org.apache.poi.xssf.usermodel.XSSFWorkbook +import org.springframework.stereotype.Component +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.transaction.annotation.Transactional +import com.ffii.fpsms.modules.master.entity.Warehouse +import com.ffii.fpsms.modules.master.entity.WarehouseRepository +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.math.BigDecimal + +@Component +open class WarehouseSetup { + + @Autowired + private lateinit var warehouseRepository: WarehouseRepository + + data class WarehouseData( + val store_id: String, + val warehouse: String, + val area: String, + val slot: String, + val order: String?, + val stockTakeSection: String? + ) + + @Transactional(rollbackFor = [Exception::class]) + open fun importExcelFromLocal(filePath: String): Int { + val file = File(filePath) + if (!file.exists()) { + throw IOException("File not found: $filePath") + } + + FileInputStream(file).use { inputStream -> + val workbook: Workbook = XSSFWorkbook(inputStream) + + try { + val sheet: Sheet = workbook.getSheetAt(0) + val warehouseDataList = mutableListOf() + + // Column indices + val COLUMN_STORE_ID_INDEX = 1 // Column B + val COLUMN_WAREHOUSE_INDEX = 2 // Column C + val COLUMN_AREA_INDEX = 3 // Column D + val COLUMN_SLOT_INDEX = 4 // Column E + val COLUMN_ORDER_INDEX = 5 // Column F + val COLUMN_STOCK_TAKE_SECTION_INDEX = 6 // Column G + + val START_ROW_INDEX = 8 // Start from Excel row 9 (0-based: index 8) + + // ──────────────────────────────────────────────── + // Process data rows (starting from Excel row 9, index 8) + // ──────────────────────────────────────────────── + for (rowIndex in START_ROW_INDEX..sheet.lastRowNum) { + val row = sheet.getRow(rowIndex) ?: continue + + val store_id = ExcelUtils.getStringValue(row.getCell(COLUMN_STORE_ID_INDEX))?.trim() ?: "" + val warehouse = ExcelUtils.getStringValue(row.getCell(COLUMN_WAREHOUSE_INDEX))?.trim() ?: "" + val area = ExcelUtils.getStringValue(row.getCell(COLUMN_AREA_INDEX))?.trim() ?: "" + val slot = ExcelUtils.getStringValue(row.getCell(COLUMN_SLOT_INDEX))?.trim() ?: "" + + // Skip if required fields are empty + if (store_id.isEmpty() || warehouse.isEmpty() || area.isEmpty() || slot.isEmpty()) { + println("SKIP row ${rowIndex + 1} - Missing required fields (store_id, warehouse, area, or slot)") + continue + } + + val order = ExcelUtils.getStringValue(row.getCell(COLUMN_ORDER_INDEX))?.trim() + + val stockTakeSection = ExcelUtils.getStringValue(row.getCell(COLUMN_STOCK_TAKE_SECTION_INDEX))?.trim() + + println("=== Processing row ${rowIndex + 1} (store_id: $store_id, warehouse: $warehouse, area: $area, slot: $slot) ===") + + warehouseDataList.add( + WarehouseData( + store_id = store_id, + warehouse = warehouse, + area = area, + slot = slot, + order = order, + stockTakeSection = stockTakeSection + ) + ) + } + + println("Total valid rows collected: ${warehouseDataList.size}") + + // ──────────────────────────────────────────────── + // Create warehouses + // ──────────────────────────────────────────────── + var createdCount = 0 + var updatedCount = 0 + + for (data in warehouseDataList) { + try { + // Generate code: store_id-warehouse-area-slot + val code = "${data.store_id}-${data.warehouse}-${data.area}-${data.slot}" + + // Generate name and description (using store_id-warehouse) + val name = "${data.store_id}-${data.warehouse}" + val description = name + + // Capacity is always 10000.00 + val capacity = BigDecimal("10000.00") + + // Check if warehouse exists by code + val existingWarehouse = warehouseRepository.findByCodeAndDeletedIsFalse(code) + + if (existingWarehouse != null) { + // Update existing warehouse + existingWarehouse.apply { + this.code = code + this.name = name + this.description = description + this.store_id = data.store_id + this.warehouse = data.warehouse + this.area = data.area + this.slot = data.slot + this.capacity = capacity + this.order = data.order + if (data.stockTakeSection != null) { + this.stockTakeSection = data.stockTakeSection + } + } + warehouseRepository.save(existingWarehouse) + updatedCount++ + println("UPDATED warehouse: $code (order: ${data.order ?: "not set"})") + } else { + // Create new warehouse + val newWarehouse = Warehouse().apply { + this.code = code + this.name = name + this.description = description + this.capacity = capacity + this.store_id = data.store_id + this.warehouse = data.warehouse + this.area = data.area + this.slot = data.slot + this.order = data.order ?: "" + if (data.stockTakeSection != null) { + this.stockTakeSection = data.stockTakeSection + } + } + warehouseRepository.save(newWarehouse) + createdCount++ + println("CREATED warehouse: $code (order: ${data.order ?: ""})") + } + } catch (e: Exception) { + println("Error processing warehouse row: ${e.message}") + continue + } + } + + println("Import finished. Created $createdCount new warehouses, Updated $updatedCount warehouses.") + return createdCount + + } finally { + workbook.close() + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt index fe21b4d..467cf82 100644 --- a/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt @@ -1070,7 +1070,8 @@ open class DeliveryOrderService( ip, port, printQty, - ZebraPrinterUtil.PrintDirection.ROTATED + ZebraPrinterUtil.PrintDirection.ROTATED, + printer.dpi ) } } diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt index f9698f2..7ad7b70 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt @@ -488,7 +488,7 @@ open class JoPickOrderService( suggestedPickLotId = spl.id, stockOutLineQty = sol?.qty ?: 0.0, stockOutLineStatus = sol?.status, - routerIndex = warehouse?.order, + routerIndex = warehouse?.order?.toIntOrNull(), routerArea = warehouse?.code, routerRoute = warehouse?.code, uomShortDesc = uom?.udfShortDesc, @@ -714,7 +714,7 @@ open class JoPickOrderService( "stockOutLineId" to sol?.id, "stockOutLineStatus" to sol?.status, "stockOutLineQty" to (sol?.qty ?: 0.0), - "routerIndex" to warehouse?.order, + "routerIndex" to warehouse?.order?.toIntOrNull(), "routerArea" to warehouse?.code, "routerRoute" to warehouse?.code, "uomShortDesc" to uom?.udfShortDesc, @@ -2003,7 +2003,7 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): JobOrderLo suggestedPickLotId = spl.id, stockOutLineQty = sol?.qty ?: 0.0, stockOutLineStatus = sol?.status, - routerIndex = warehouse?.order, + routerIndex = warehouse?.order?.toIntOrNull(), routerArea = warehouse?.code, routerRoute = warehouse?.code, uomShortDesc = uom?.udfShortDesc, diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt index efac3a7..43df6e9 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt @@ -759,7 +759,8 @@ open class JobOrderService( ip, port, printQty, - ZebraPrinterUtil.PrintDirection.ROTATED + ZebraPrinterUtil.PrintDirection.ROTATED, + printer.dpi ) } } diff --git a/src/main/java/com/ffii/fpsms/modules/master/entity/Warehouse.kt b/src/main/java/com/ffii/fpsms/modules/master/entity/Warehouse.kt index 80f327e..77ceee0 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/entity/Warehouse.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/Warehouse.kt @@ -15,8 +15,8 @@ open class Warehouse : BaseEntity() { @Column(name = "code", nullable = false, length = 30) open var code: String? = null @NotNull - @Column(name = "`order`", nullable = false,) - open var order: Int? = null + @Column(name = "`order`", nullable = false, length = 50) + open var order: String? = null @NotNull @Column(name = "name", nullable = false, length = 30) open var name: String? = null 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 7d7a68c..db74f18 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 @@ -636,8 +636,8 @@ 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/Kelvin YAU/Downloads/bom/*.xlsx") - val excels = resolver.getResources("file:C:/Users/kw093/Downloads/bom/bom/*.xlsx") + val excels = resolver.getResources("file:C:/Users/Kelvin YAU/Downloads/datasetup/bom/*.xlsx") + // val excels = resolver.getResources("file:C:/Users/kw093/Downloads/bom/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/WarehouseService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/WarehouseService.kt index ba2bb1b..cba4b38 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 @@ -84,11 +84,13 @@ open class WarehouseService( } else { capacity = request.capacity } - + if (request.order == null && request.id == null) { // Set a default order for new warehouses - val maxOrder = warehouseRepository.findAll().mapNotNull { it.order }.maxOrNull() ?: 0 - order = maxOrder + 1 + val maxOrder = warehouseRepository.findAll() + .mapNotNull { it.order?.toIntOrNull() } + .maxOrNull() ?: 0 + order = (maxOrder + 1).toString() } else if (request.order != null) { order = request.order } @@ -247,23 +249,31 @@ open class WarehouseService( skippedInSecondLoop++ continue } - + // use unique identifier from orderMap to get order val refKey = ref ?: "${i + 1}" - val order = orderMap[refKey] - + val orderInt = orderMap[refKey] // This is Int + + if (orderInt == null) { + logger.warn("Order not found for key: ${refKey}Key at row ${i + 1}") + skippedInSecondLoop++ + continue + } + + val order = orderInt.toString() // Convert Int to String + if (order == null) { logger.warn("Order not found for key: ${refKey}Key at row ${i + 1}") skippedInSecondLoop++ continue } - + val capacity = BigDecimal(10000) val code = "$store_id-$warehouse-$area-$slot" val name = "$store_id-$storeLocation" val description = "$store_id-$storeLocation" - - + + // check if the warehouse exists val existingWarehouse = if (!ref.isNullOrBlank()) { try { @@ -276,7 +286,7 @@ open class WarehouseService( } else { null } - + if (existingWarehouse != null) { // update the warehouse existingWarehouse.apply { diff --git a/src/main/java/com/ffii/fpsms/modules/master/web/models/SaveWarehouseRequest.kt b/src/main/java/com/ffii/fpsms/modules/master/web/models/SaveWarehouseRequest.kt index 5c57fcc..6d6d26c 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/web/models/SaveWarehouseRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/web/models/SaveWarehouseRequest.kt @@ -12,7 +12,7 @@ data class SaveWarehouseRequest( val warehouse: String? = null, val area: String? = null, val slot: String? = null, - val order: Int? = null, + val order: String? = null, val stockTakeSection: String? = null, ) data class NewWarehouseRequest( @@ -26,7 +26,7 @@ data class NewWarehouseRequest( val area: String, val slot: String, val store_id: String, - val order: Int, + val order: String, val storeLocation: String, val stockTakeTable: String, val company: String, diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt index 7790845..ce4b8f2 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt @@ -78,6 +78,7 @@ fun countDistinctItemsByWarehouseIds(@Param("warehouseIds") warehouseIds: List): Long + @Query(""" SELECT ill FROM InventoryLotLine ill JOIN ill.inventoryLot il @@ -87,4 +88,21 @@ fun countAllByWarehouseIds(@Param("warehouseIds") warehouseIds: List): Lon ORDER BY il.expiryDate ASC """) fun findExpiredItems(@Param("today") today: LocalDate): List + + @Query(""" + SELECT ill FROM InventoryLotLine ill + WHERE ill.inventoryLot.lotNo = :lotNo + AND ill.inventoryLot.item.id = :itemId + AND ill.warehouse.id = :warehouseId + AND ill.deleted = false + """) + fun findByLotNoAndItemIdAndWarehouseId(lotNo: String, itemId: Long, warehouseId: Long): InventoryLotLine? + + @Query(""" + SELECT ill FROM InventoryLotLine ill + WHERE ill.deleted = false + AND ill.inventoryLot.stockInLine IS NOT NULL + ORDER BY ill.inventoryLot.lotNo +""") + fun findAllByDeletedIsFalseAndHasStockInLine(): 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 7f58c49..40aad0d 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 @@ -118,7 +118,7 @@ open class StockInLineService( // stockIn = stockInService.create(SaveStockInRequest(purchaseOrderId = request.purchaseOrderId)).entity as StockIn // var stockIn = stockInRepository.findByPurchaseOrderIdAndDeletedFalse(request.purchaseOrderId) } - + val item = itemRepository.findById(request.itemId).orElseThrow() // If request contains valid POL if (pol != null) { @@ -168,7 +168,7 @@ open class StockInLineService( if (jo != null && jo?.bom != null) { // For job orders, demandQty comes from BOM's outputQty this.demandQty = jo?.bom?.outputQty - + } else if (pol != null) { // For purchase orders, demandQty comes from PurchaseOrderLine's qty this.demandQty = pol.qty @@ -406,7 +406,7 @@ open class StockInLineService( } // TODO: check all status to prevent reverting progress due to multiple users access to the same po? // return list of stock in line, update data grid with the list - + stockInLine.apply { this.productionDate = request.productionDate?.atStartOfDay() ?: this.productionDate this.productLotNo = request.productLotNo ?: this.productLotNo @@ -452,12 +452,12 @@ open class StockInLineService( } else { // For non-WIP, use original logic if (request.acceptQty?.compareTo(request.acceptedQty) == 0) - StockInLineStatus.COMPLETE.status - else + StockInLineStatus.COMPLETE.status + else StockInLineStatus.PARTIALLY_COMPLETE.status } - // this.inventoryLotLine = savedInventoryLotLine - } + // this.inventoryLotLine = savedInventoryLotLine + } createStockLedgerForStockIn(stockInLine) // Update JO Status if (stockInLine.jobOrder != null) { //TODO Improve @@ -621,8 +621,13 @@ open class StockInLineService( // 3. Call the utility function with the temporary file. val printQty = if (request.printQty == null || request.printQty <= 0) 1 else request.printQty printer.ip?.let { ip -> printer.port?.let { port -> - ZebraPrinterUtil.printPdfToZebra(tempPdfFile, ip, port, printQty, - ZebraPrinterUtil.PrintDirection.ROTATED + ZebraPrinterUtil.printPdfToZebra( + tempPdfFile, + ip, + port, + printQty, + ZebraPrinterUtil.PrintDirection.ROTATED, + printer.dpi ) } } } finally { @@ -664,31 +669,31 @@ open class StockInLineService( val inventoryLot = inventoryLotRepository.findByIdAndDeletedFalse( inventoryLotLine?.inventoryLot?.id ?: throw IllegalArgumentException("Inventory lot ID not found") ) ?: throw IllegalArgumentException("Inventory lot not found") - + val stockIn=StockIn().apply { this.code=stockTake?.code this.status=request.stockIntype //this.stockTake=stockTake - } - stockInRepository.save(stockIn) - val stockInLine = StockInLine().apply { - //this.stockTakeLine=stockTakeLine - this.item=inventoryLot.item - this.itemNo=request.itemNo - this.stockIn = stockIn - this.demandQty=request.demandQty - this.acceptedQty=request.acceptedQty - this.expiryDate=inventoryLot.expiryDate - this.inventoryLot=inventoryLot - this.inventoryLotLine=inventoryLotLine - this.lotNo=newLotNo - this.status = "completed" - this.type = request.stockInLineType - - - } - stockInLineRepository.save(stockInLine) - val updateRequest = SaveInventoryLotLineRequest( + } + stockInRepository.save(stockIn) + val stockInLine = StockInLine().apply { + //this.stockTakeLine=stockTakeLine + this.item=inventoryLot.item + this.itemNo=request.itemNo + this.stockIn = stockIn + this.demandQty=request.demandQty + this.acceptedQty=request.acceptedQty + this.expiryDate=inventoryLot.expiryDate + this.inventoryLot=inventoryLot + this.inventoryLotLine=inventoryLotLine + this.lotNo=newLotNo + this.status = "completed" + this.type = request.stockInLineType + + + } + stockInLineRepository.save(stockInLine) + val updateRequest = SaveInventoryLotLineRequest( id = inventoryLotLine.id, inventoryLotId = inventoryLotLine.inventoryLot?.id, warehouseId = inventoryLotLine.warehouse?.id, @@ -713,11 +718,11 @@ open class StockInLineService( private fun createStockLedgerForStockIn(stockInLine: StockInLine) { val item = stockInLine.item ?: return val inventory = inventoryRepository.findByItemId(item.id!!).orElse(null) ?: return - + val inQty = stockInLine.acceptedQty?.toDouble() ?: 0.0 // 直接使用 inventory.onHandQty 作为 balance(已经是更新后的值) val newBalance = (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() - + val stockLedger = StockLedger().apply { this.stockInLine = stockInLine this.inventory = inventory @@ -729,7 +734,139 @@ open class StockInLineService( this.itemCode = item.code this.date = LocalDate.now() } - + stockLedgerRepository.saveAndFlush(stockLedger) } -} \ No newline at end of file + + @Transactional + open fun createStockIn(request: StockInRequest): StockInLine { + // Step 1: Create a row of stock_in + val prefix = "SI" + val midfix = CodeGenerator.DEFAULT_MIDFIX + val latestCode = stockInRepository.findLatestCodeByPrefix("$prefix-$midfix") + val code = CodeGenerator.generateNo(prefix = prefix, midfix = midfix, latestCode = latestCode) + + val stockIn = StockIn().apply { + this.code = code + this.status = StockInStatus.COMPLETE.status + this.orderDate = LocalDateTime.now() + this.completeDate = LocalDateTime.now() + } + val savedStockIn = stockInRepository.save(stockIn) + + // Step 2: Create a row in stock_in_line table + val item = itemRepository.findById(request.itemId).orElseThrow() + + val stockInLine = StockInLine().apply { + this.item = item + this.itemNo = request.itemNo + this.stockIn = savedStockIn + this.demandQty = request.demandQty + this.acceptedQty = request.acceptedQty + this.receiptDate = LocalDateTime.now() + this.productionDate = LocalDateTime.now() + this.expiryDate = request.expiryDate + this.status = StockInLineStatus.COMPLETE.status + this.lotNo = request.lotNo ?: assignLotNo() + this.productLotNo = request.productLotNo + this.dnNo = request.dnNo + this.type = request.type + } + val savedStockInLine = saveAndFlush(stockInLine) + + + // Step 3: Create a row in inventory_lot table + val lotNo = request.lotNo ?: assignLotNo() + val inventoryLot = InventoryLot().apply { + this.item = item + this.stockInLine = savedStockInLine + this.stockInDate = LocalDateTime.now() + this.expiryDate = request.expiryDate + this.lotNo = lotNo + } + val savedInventoryLot = inventoryLotRepository.saveAndFlush(inventoryLot) + + // Link back to StockInLine + savedStockInLine.inventoryLot = savedInventoryLot + + // Step 4: Create a row in inventory_lot_line table + val stockItemUom = itemUomRepository.findBaseUnitByItemIdAndStockUnitIsTrueAndDeletedIsFalse( + itemId = request.itemId + ) ?: throw IllegalArgumentException("Stock UOM not found for item ${request.itemId}") + + val warehouse = if (request.warehouseId != null) { + warehouseRepository.findById(request.warehouseId).orElseThrow() + } else { + throw IllegalArgumentException("warehouseId is required for creating inventoryLotLine") + } + + val inventoryLotLine = InventoryLotLine().apply { + this.inventoryLot = savedInventoryLot + this.warehouse = warehouse + this.inQty = request.acceptedQty + this.outQty = BigDecimal.ZERO + this.holdQty = BigDecimal.ZERO + this.status = InventoryLotLineStatus.AVAILABLE + this.stockUom = stockItemUom + } + val savedInventoryLotLine = inventoryLotLineRepository.saveAndFlush(inventoryLotLine) + + // Link back to StockInLine + savedStockInLine.inventoryLotLine = savedInventoryLotLine + + saveAndFlush(savedStockInLine) + + // Step 5: Create Stock Ledger entry + createStockLedgerForStockIn(savedStockInLine) + + return savedStockInLine + } + + @Transactional + open fun createStockInForExistingInventoryLotLine( + request: StockInRequest, + existingInventoryLotLine: InventoryLotLine + ): StockInLine { + // Step 1: Create StockIn + val prefix = "SI" + val midfix = CodeGenerator.DEFAULT_MIDFIX + val latestCode = stockInRepository.findLatestCodeByPrefix("$prefix-$midfix") + val code = CodeGenerator.generateNo(prefix = prefix, midfix = midfix, latestCode = latestCode) + + val stockIn = StockIn().apply { + this.code = code + this.status = StockInStatus.COMPLETE.status + this.orderDate = LocalDateTime.now() + this.completeDate = LocalDateTime.now() + } + val savedStockIn = stockInRepository.save(stockIn) + + // Step 2: Create StockInLine and link to existing InventoryLotLine + val item = itemRepository.findById(request.itemId).orElseThrow() + + val stockInLine = StockInLine().apply { + this.item = item + this.itemNo = request.itemNo + this.stockIn = savedStockIn + this.demandQty = request.demandQty + this.acceptedQty = request.acceptedQty + this.receiptDate = LocalDateTime.now() + this.productionDate = LocalDateTime.now() + this.expiryDate = request.expiryDate + this.status = StockInLineStatus.COMPLETE.status + this.lotNo = request.lotNo + this.productLotNo = request.productLotNo + this.dnNo = request.dnNo + this.type = request.type + // Link to existing InventoryLot and InventoryLotLine + this.inventoryLot = existingInventoryLotLine.inventoryLot + this.inventoryLotLine = existingInventoryLotLine + } + val savedStockInLine = saveAndFlush(stockInLine) + + // Step 3: Create Stock Ledger entry + createStockLedgerForStockIn(savedStockInLine) + + return savedStockInLine + } +} 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 f4a5840..065749f 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 @@ -37,6 +37,7 @@ import com.ffii.fpsms.modules.bag.web.model.CreateBagLotLineRequest import com.ffii.fpsms.modules.common.CodeGenerator import org.springframework.context.annotation.Lazy import com.ffii.fpsms.modules.bag.service.BagService +import com.ffii.fpsms.modules.common.SecurityUtils import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository import com.ffii.fpsms.modules.stock.entity.InventoryRepository @@ -1208,4 +1209,54 @@ private fun createStockLedgerForStockOut(stockOutLine: StockOutLine) { stockLedgerRepository.saveAndFlush(stockLedger) } + + open fun createStockOut(request: StockOutRequest): StockOutLine { + + val inventoryLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId).orElseThrow() + + // Step 1: Increase outQty in inventory_lot_line table + val updatedInventoryLotLine = inventoryLotLine.apply { + val currentOutQty = this.outQty ?: BigDecimal.ZERO + val newOutQty = currentOutQty + BigDecimal.valueOf(request.qty) + this.outQty = newOutQty + + val currentInQty = this.inQty ?: BigDecimal.ZERO + if (newOutQty.compareTo(currentInQty) == 0) { + this.status = InventoryLotLineStatus.UNAVAILABLE + } + } + inventoryLotLineRepository.save(updatedInventoryLotLine) + + // Step 2: Create a row of stock_out + val currentUser = SecurityUtils.getUser().orElseThrow() + val stockOut = StockOut().apply { + this.type = request.type + this.completeDate = LocalDateTime.now() + this.handler = currentUser.id + this.status = StockOutStatus.COMPLETE.status + } + val savedStockOut = stockOutRepository.save(stockOut) + + // Step 3: Create a row in stock_out_line table + val itemId = updatedInventoryLotLine.inventoryLot?.item?.id + ?: throw IllegalArgumentException("InventoryLotLine must have an associated item") + val item = itemRepository.findById(itemId).orElseThrow() + + val stockOutLine = StockOutLine().apply { + this.item = item + this.qty = request.qty + this.stockOut = savedStockOut + this.inventoryLotLine = updatedInventoryLotLine + this.status = StockOutLineStatus.COMPLETE.status + this.pickTime = LocalDateTime.now() + this.pickerId = currentUser.id + this.type = request.type + } + val savedStockOutLine = saveAndFlush(stockOutLine) + + // Step 4: Create a row in stock_ledger table + createStockLedgerForStockOut(savedStockOutLine) + + return savedStockOutLine + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockTransferRecordService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockTransferRecordService.kt index e69de29..7ebf943 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockTransferRecordService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockTransferRecordService.kt @@ -0,0 +1,180 @@ +package com.ffii.fpsms.modules.stock.service + +import com.ffii.core.support.AbstractBaseEntityService +import com.ffii.core.support.JdbcDao +import com.ffii.fpsms.modules.master.web.models.MessageResponse +import com.ffii.fpsms.modules.stock.entity.StockInLine +import com.ffii.fpsms.modules.stock.entity.StockOutLine +import com.ffii.fpsms.modules.stock.entity.StockTransferRecord +import com.ffii.fpsms.modules.stock.entity.StockTransferRecordRepository +import com.ffii.fpsms.modules.stock.web.model.CreateStockTransferRequest +import com.ffii.fpsms.modules.stock.web.model.StockInRequest +import com.ffii.fpsms.modules.stock.web.model.StockOutRequest +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.math.BigDecimal +import java.time.LocalDate +import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository +import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus + + +@Service +open class StockTransferRecordService( + private val jdbcDao: JdbcDao, + private val stockTransferRecordRepository: StockTransferRecordRepository, + private val stockOutLineService: StockOutLineService, + private val stockInLineService: StockInLineService, + private val inventoryLotLineRepository: InventoryLotLineRepository +) : AbstractBaseEntityService( + jdbcDao, + stockTransferRecordRepository +) { + + @Transactional + open fun createStockTransfer(request: CreateStockTransferRequest): MessageResponse { + // Step 1: Stock Out - using generic function + val stockOutLine = createStockOut(request) + + // Step 2: Stock In - using generic function + val stockInLine = createStockIn(request) + + // Step 3: Create Stock Transfer Record + val stockTransferRecord = createStockTransferRecord(request, stockOutLine, stockInLine) + + return MessageResponse( + id = stockTransferRecord.id, + name = "Stock Transfer Record", + code = "STOCK_TRANSFER_CREATED", + type = "success", + message = "Stock transfer completed successfully", + errorPosition = null + ) + } + + private fun createStockOut(request: CreateStockTransferRequest): StockOutLine { + + val stockOutRequest = StockOutRequest( + inventoryLotLineId = request.inventoryLotLineId, + qty = request.transferredQty.toDouble(), + type = "TRF" + ) + + return stockOutLineService.createStockOut(stockOutRequest) + } + + private fun createStockIn(request: CreateStockTransferRequest): StockInLine { + // Step 1: Get inventoryLotLine to extract item information + val inventoryLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId) + .orElseThrow { IllegalArgumentException("InventoryLotLine not found with id: ${request.inventoryLotLineId}") } + + // Step 2: Extract itemId and itemCode from inventoryLotLine + val inventoryLot = inventoryLotLine.inventoryLot + ?: throw IllegalArgumentException("InventoryLotLine must have an associated InventoryLot") + + val item = inventoryLot.item + ?: throw IllegalArgumentException("InventoryLotLine must have an associated item") + + val itemId = item.id + ?: throw IllegalArgumentException("Item must have an id") + + val itemCode = item.code + ?: throw IllegalArgumentException("Item must have a code") + + val expiryDate = inventoryLot.expiryDate + ?: throw IllegalArgumentException("InventoryLot must have an expiryDate") + + val lotNo = inventoryLot.lotNo + ?: throw IllegalArgumentException("InventoryLot must have a lotNo") + + val productLotNo = inventoryLot.stockInLine?.productLotNo + val dnNo = inventoryLot.stockInLine?.dnNo + + // Step 3: Validate required fields + if (request.warehouseId == null) { + throw IllegalArgumentException("warehouseId is required for stock in") + } + + // Step 4: Check if existing InventoryLotLine with same lot and warehouse exists + val existingInventoryLotLine = inventoryLotLineRepository.findByLotNoAndItemIdAndWarehouseId( + lotNo = lotNo, + itemId = itemId, + warehouseId = request.warehouseId + ) + + // Step 5: Map to StockInRequest + val stockInRequest = StockInRequest( + itemId = itemId, + itemNo = itemCode, + demandQty = request.transferredQty, + acceptedQty = request.transferredQty, + expiryDate = expiryDate, + lotNo = lotNo, + productLotNo = productLotNo, + dnNo = dnNo, + type = "TRF", + warehouseId = request.warehouseId + ) + + return if (existingInventoryLotLine != null) { + // Update existing InventoryLotLine's inQty + existingInventoryLotLine.apply { + val currentInQty = this.inQty ?: BigDecimal.ZERO + this.inQty = currentInQty + request.transferredQty + + // Update status if it was UNAVAILABLE and now has stock + if (this.status == InventoryLotLineStatus.UNAVAILABLE) { + this.status = InventoryLotLineStatus.AVAILABLE + } + } + inventoryLotLineRepository.saveAndFlush(existingInventoryLotLine) + + // Create StockIn and StockInLine for record keeping, linking to existing InventoryLotLine + stockInLineService.createStockInForExistingInventoryLotLine(stockInRequest, existingInventoryLotLine) + } else { + // Normal flow: create new InventoryLotLine + stockInLineService.createStockIn(stockInRequest) + } + } + + private fun createStockTransferRecord( + request: CreateStockTransferRequest, + stockOutLine: StockOutLine, + stockInLine: StockInLine + ): StockTransferRecord { + // Get source location from stockOutLine's inventoryLotLine warehouse + val startLocation = stockOutLine.inventoryLotLine?.warehouse?.code + ?: stockOutLine.inventoryLotLine?.warehouse?.name + + // Get target location from stockInLine's inventoryLot warehouse + val targetLocation = stockInLine.inventoryLotLine?.warehouse?.code + ?: stockInLine.inventoryLotLine?.warehouse?.name + + // Get item information + val item = stockInLine.item + ?: throw IllegalArgumentException("StockInLine must have an associated item") + + // Create StockTransferRecord + val stockTransferRecord = StockTransferRecord().apply { + this.item = item + this.itemCode = item.code + this.itemName = item.name + this.lotNo = stockInLine.lotNo + this.transferredQty = request.transferredQty + this.startLocation = startLocation + this.targetLocation = targetLocation + this.stockInLine = stockInLine + this.stockOutLine = stockOutLine + } + val savedStockTransferRecord = stockTransferRecordRepository.save(stockTransferRecord) + + // Link back to StockInLine and StockOutLine + stockInLine.stockTransferRecord = savedStockTransferRecord + stockOutLine.stockTransferRecord = savedStockTransferRecord + + // Save the updated lines + stockInLineService.saveAndFlush(stockInLine) + stockOutLineService.saveAndFlush(stockOutLine) + + return savedStockTransferRecord + } +} \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/StockTransferRecordController.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/StockTransferRecordController.kt new file mode 100644 index 0000000..9a10655 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/StockTransferRecordController.kt @@ -0,0 +1,18 @@ +package com.ffii.fpsms.modules.stock.web + +import com.ffii.fpsms.modules.master.web.models.MessageResponse +import com.ffii.fpsms.modules.stock.service.StockTransferRecordService +import com.ffii.fpsms.modules.stock.web.model.CreateStockTransferRequest +import jakarta.validation.Valid +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/stockTransferRecord") +class StockTransferRecordController( + private val stockTransferRecordService: StockTransferRecordService +) { + @PostMapping("/create") + fun createStockTransfer(@Valid @RequestBody request: CreateStockTransferRequest): MessageResponse { + return stockTransferRecordService.createStockTransfer(request) + } +} \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/CreateStockTransferRequest.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/CreateStockTransferRequest.kt new file mode 100644 index 0000000..b091db0 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/CreateStockTransferRequest.kt @@ -0,0 +1,9 @@ +package com.ffii.fpsms.modules.stock.web.model + +import java.math.BigDecimal + +data class CreateStockTransferRequest( + val inventoryLotLineId: Long, + val transferredQty: BigDecimal, + val warehouseId: Long? = null +) \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockInRequest.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockInRequest.kt new file mode 100644 index 0000000..5a935dd --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockInRequest.kt @@ -0,0 +1,17 @@ +package com.ffii.fpsms.modules.stock.web.model + +import java.math.BigDecimal +import java.time.LocalDate + +data class StockInRequest( + val itemId: Long, + val itemNo: String, + val demandQty: BigDecimal? = null, + val acceptedQty: BigDecimal, + val expiryDate: LocalDate, + val lotNo: String? = null, + val productLotNo: String? = null, + val dnNo: String? = null, + val type: String? = null, + val warehouseId: Long? = null +) \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockOutRequest.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockOutRequest.kt new file mode 100644 index 0000000..e9eec12 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockOutRequest.kt @@ -0,0 +1,7 @@ +package com.ffii.fpsms.modules.stock.web.model + +data class StockOutRequest( + val inventoryLotLineId: Long, + val qty: Double, + val type: String, +) \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/user/web/UserController.java b/src/main/java/com/ffii/fpsms/modules/user/web/UserController.java index 8df4dd5..ed08b73 100644 --- a/src/main/java/com/ffii/fpsms/modules/user/web/UserController.java +++ b/src/main/java/com/ffii/fpsms/modules/user/web/UserController.java @@ -53,8 +53,6 @@ import java.io.UnsupportedEncodingException; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; -import com.ffii.fpsms.modules.common.internalSetup.UsersSetup; - @RestController @RequestMapping("/user") public class UserController{ @@ -64,19 +62,17 @@ public class UserController{ private PasswordEncoder passwordEncoder; private SettingsService settingsService; private UserQrCodeService userQrCodeService; - private UsersSetup usersSetup; public UserController( UserService userService, PasswordEncoder passwordEncoder, SettingsService settingsService, - UserQrCodeService userQrCodeService, - UsersSetup usersSetup) { + UserQrCodeService userQrCodeService + ) { this.userService = userService; this.passwordEncoder = passwordEncoder; this.settingsService = settingsService; this.userQrCodeService = userQrCodeService; - this.usersSetup = usersSetup; } // @Operation(summary = "list user", responses = { @ApiResponse(responseCode = "200"), @@ -298,31 +294,4 @@ public class UserController{ } - @PostMapping("/users-setup") - public ResponseEntity> importUsersFromExcel(@RequestBody Map request) { - String filePath = request.get("filePath"); - - if (filePath == null || filePath.isEmpty()) { - Map errorResponse = new HashMap<>(); - errorResponse.put("success", false); - errorResponse.put("error", "filePath is required"); - return ResponseEntity.badRequest().body(errorResponse); - } - - try { - int createdCount = usersSetup.importExcelFromLocal(filePath); - Map response = new HashMap<>(); - response.put("success", true); - response.put("message", "Users imported successfully"); - response.put("createdCount", createdCount); - return ResponseEntity.ok(response); - } catch (Exception e) { - logger.error("Error importing users from Excel", e); - Map errorResponse = new HashMap<>(); - errorResponse.put("success", false); - errorResponse.put("error", e.getMessage()); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); - } - } - } diff --git a/src/main/resources/db/changelog/changes/20260119_fai/02_cheduler_log query copy.sql b/src/main/resources/db/changelog/changes/20260119_fai/02_cheduler_log query copy.sql deleted file mode 100644 index 8d64207..0000000 --- a/src/main/resources/db/changelog/changes/20260119_fai/02_cheduler_log query copy.sql +++ /dev/null @@ -1,5 +0,0 @@ ---liquibase formatted sql ---changeset author:add_scheduler_log query - -ALTER TABLE `fpsmsdb`.`scheduler_sync_log` -ADD COLUMN `query` VARCHAR(2000) NULL AFTER `endTime`; diff --git a/src/main/resources/db/changelog/changes/20260119_fai/02_scheduler_log_query_copy.sql b/src/main/resources/db/changelog/changes/20260119_fai/02_scheduler_log_query_copy.sql new file mode 100644 index 0000000..3f3bb5f --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260119_fai/02_scheduler_log_query_copy.sql @@ -0,0 +1,8 @@ +--liquibase formatted sql +--changeset author:add_scheduler_log query + +--preconditions onFail:MARK_RAN +--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'scheduler_sync_log' AND COLUMN_NAME = 'query' + +ALTER TABLE `fpsmsdb`.`scheduler_sync_log` +ADD COLUMN `query` VARCHAR(2000) NULL AFTER `endTime`; \ No newline at end of file