| @@ -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) | |||
| @@ -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<String, String>): ResponseEntity<Map<String, Any>> { | |||
| 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<String, String>): ResponseEntity<Map<String, Any>> { | |||
| 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<String, String>): ResponseEntity<Map<String, Any>> { | |||
| 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<String, String>): ResponseEntity<Map<String, Any>> { | |||
| 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<String, Any>): ResponseEntity<Map<String, Any>> { | |||
| 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) | |||
| } | |||
| } | |||
| } | |||
| @@ -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<InventoryData>() | |||
| // 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 | |||
| } | |||
| } | |||
| @@ -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<ItemsData>() | |||
| // 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() | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -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 | |||
| @@ -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<WarehouseData>() | |||
| // 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() | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -1070,7 +1070,8 @@ open class DeliveryOrderService( | |||
| ip, | |||
| port, | |||
| printQty, | |||
| ZebraPrinterUtil.PrintDirection.ROTATED | |||
| ZebraPrinterUtil.PrintDirection.ROTATED, | |||
| printer.dpi | |||
| ) | |||
| } | |||
| } | |||
| @@ -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, | |||
| @@ -759,7 +759,8 @@ open class JobOrderService( | |||
| ip, | |||
| port, | |||
| printQty, | |||
| ZebraPrinterUtil.PrintDirection.ROTATED | |||
| ZebraPrinterUtil.PrintDirection.ROTATED, | |||
| printer.dpi | |||
| ) | |||
| } | |||
| } | |||
| @@ -15,8 +15,8 @@ open class Warehouse : BaseEntity<Long>() { | |||
| @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 | |||
| @@ -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") | |||
| @@ -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 { | |||
| @@ -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, | |||
| @@ -78,6 +78,7 @@ fun countDistinctItemsByWarehouseIds(@Param("warehouseIds") warehouseIds: List<L | |||
| AND ill.deleted = false | |||
| """) | |||
| fun countAllByWarehouseIds(@Param("warehouseIds") warehouseIds: List<Long>): Long | |||
| @Query(""" | |||
| SELECT ill FROM InventoryLotLine ill | |||
| JOIN ill.inventoryLot il | |||
| @@ -87,4 +88,21 @@ fun countAllByWarehouseIds(@Param("warehouseIds") warehouseIds: List<Long>): Lon | |||
| ORDER BY il.expiryDate ASC | |||
| """) | |||
| fun findExpiredItems(@Param("today") today: LocalDate): List<InventoryLotLine> | |||
| @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<InventoryLotLine> | |||
| } | |||
| @@ -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) | |||
| } | |||
| } | |||
| @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 | |||
| } | |||
| } | |||
| @@ -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 | |||
| } | |||
| } | |||
| @@ -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<StockTransferRecord, Long, StockTransferRecordRepository>( | |||
| 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 | |||
| } | |||
| } | |||
| @@ -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) | |||
| } | |||
| } | |||
| @@ -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 | |||
| ) | |||
| @@ -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 | |||
| ) | |||
| @@ -0,0 +1,7 @@ | |||
| package com.ffii.fpsms.modules.stock.web.model | |||
| data class StockOutRequest( | |||
| val inventoryLotLineId: Long, | |||
| val qty: Double, | |||
| val type: String, | |||
| ) | |||
| @@ -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<Map<String, Object>> importUsersFromExcel(@RequestBody Map<String, String> request) { | |||
| String filePath = request.get("filePath"); | |||
| if (filePath == null || filePath.isEmpty()) { | |||
| Map<String, Object> 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<String, Object> 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<String, Object> errorResponse = new HashMap<>(); | |||
| errorResponse.put("success", false); | |||
| errorResponse.put("error", e.getMessage()); | |||
| return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); | |||
| } | |||
| } | |||
| } | |||
| @@ -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`; | |||
| @@ -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`; | |||