| @@ -35,13 +35,13 @@ open class ZebraPrinterUtil { | |||||
| * @param printDirection Valid values: N (Normal), R (Rotated 90), I (Inverted 180), B (Bottom-up 270) | * @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. | * @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 | // Check if the file exists and is readable | ||||
| if (!pdfFile.exists() || !pdfFile.canRead()) { | if (!pdfFile.exists() || !pdfFile.canRead()) { | ||||
| throw IllegalArgumentException("Error: File not found or not readable at path: ${pdfFile.absolutePath}") | throw IllegalArgumentException("Error: File not found or not readable at path: ${pdfFile.absolutePath}") | ||||
| } | } | ||||
| val renderDpi = dpi ?: 203 | |||||
| try { | try { | ||||
| // 1. Load the PDF document | // 1. Load the PDF document | ||||
| PDDocument.load(pdfFile).use { document -> | PDDocument.load(pdfFile).use { document -> | ||||
| @@ -59,7 +59,7 @@ open class ZebraPrinterUtil { | |||||
| println("DEBUG: Processing page ${pageIndex + 1} of $totalPages") | println("DEBUG: Processing page ${pageIndex + 1} of $totalPages") | ||||
| // 2. Render each page of the PDF as a monochrome image | // 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 | // 3. Convert the image to a ZPL format string | ||||
| val zplCommand = convertImageToZpl(image, printDirection) | 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 | package com.ffii.fpsms.modules.common.internalSetup | ||||
| import com.ffii.core.utils.ExcelUtils | 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.Sheet | ||||
| import org.apache.poi.ss.usermodel.Workbook | import org.apache.poi.ss.usermodel.Workbook | ||||
| import org.apache.poi.xssf.usermodel.XSSFWorkbook | import org.apache.poi.xssf.usermodel.XSSFWorkbook | ||||
| @@ -17,7 +15,7 @@ import java.io.FileInputStream | |||||
| import java.io.IOException | import java.io.IOException | ||||
| @Component | @Component | ||||
| open class UsersSetup { | |||||
| open class UserSetup { | |||||
| @Autowired | @Autowired | ||||
| private lateinit var userRepository: UserRepository | 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, | ip, | ||||
| port, | port, | ||||
| printQty, | printQty, | ||||
| ZebraPrinterUtil.PrintDirection.ROTATED | |||||
| ZebraPrinterUtil.PrintDirection.ROTATED, | |||||
| printer.dpi | |||||
| ) | ) | ||||
| } | } | ||||
| } | } | ||||
| @@ -488,7 +488,7 @@ open class JoPickOrderService( | |||||
| suggestedPickLotId = spl.id, | suggestedPickLotId = spl.id, | ||||
| stockOutLineQty = sol?.qty ?: 0.0, | stockOutLineQty = sol?.qty ?: 0.0, | ||||
| stockOutLineStatus = sol?.status, | stockOutLineStatus = sol?.status, | ||||
| routerIndex = warehouse?.order, | |||||
| routerIndex = warehouse?.order?.toIntOrNull(), | |||||
| routerArea = warehouse?.code, | routerArea = warehouse?.code, | ||||
| routerRoute = warehouse?.code, | routerRoute = warehouse?.code, | ||||
| uomShortDesc = uom?.udfShortDesc, | uomShortDesc = uom?.udfShortDesc, | ||||
| @@ -714,7 +714,7 @@ open class JoPickOrderService( | |||||
| "stockOutLineId" to sol?.id, | "stockOutLineId" to sol?.id, | ||||
| "stockOutLineStatus" to sol?.status, | "stockOutLineStatus" to sol?.status, | ||||
| "stockOutLineQty" to (sol?.qty ?: 0.0), | "stockOutLineQty" to (sol?.qty ?: 0.0), | ||||
| "routerIndex" to warehouse?.order, | |||||
| "routerIndex" to warehouse?.order?.toIntOrNull(), | |||||
| "routerArea" to warehouse?.code, | "routerArea" to warehouse?.code, | ||||
| "routerRoute" to warehouse?.code, | "routerRoute" to warehouse?.code, | ||||
| "uomShortDesc" to uom?.udfShortDesc, | "uomShortDesc" to uom?.udfShortDesc, | ||||
| @@ -2003,7 +2003,7 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): JobOrderLo | |||||
| suggestedPickLotId = spl.id, | suggestedPickLotId = spl.id, | ||||
| stockOutLineQty = sol?.qty ?: 0.0, | stockOutLineQty = sol?.qty ?: 0.0, | ||||
| stockOutLineStatus = sol?.status, | stockOutLineStatus = sol?.status, | ||||
| routerIndex = warehouse?.order, | |||||
| routerIndex = warehouse?.order?.toIntOrNull(), | |||||
| routerArea = warehouse?.code, | routerArea = warehouse?.code, | ||||
| routerRoute = warehouse?.code, | routerRoute = warehouse?.code, | ||||
| uomShortDesc = uom?.udfShortDesc, | uomShortDesc = uom?.udfShortDesc, | ||||
| @@ -759,7 +759,8 @@ open class JobOrderService( | |||||
| ip, | ip, | ||||
| port, | port, | ||||
| printQty, | 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) | @Column(name = "code", nullable = false, length = 30) | ||||
| open var code: String? = null | open var code: String? = null | ||||
| @NotNull | @NotNull | ||||
| @Column(name = "`order`", nullable = false,) | |||||
| open var order: Int? = null | |||||
| @Column(name = "`order`", nullable = false, length = 50) | |||||
| open var order: String? = null | |||||
| @NotNull | @NotNull | ||||
| @Column(name = "name", nullable = false, length = 30) | @Column(name = "name", nullable = false, length = 30) | ||||
| open var name: String? = null | open var name: String? = null | ||||
| @@ -636,8 +636,8 @@ open class BomService( | |||||
| val resolver = PathMatchingResourcePatternResolver() | val resolver = PathMatchingResourcePatternResolver() | ||||
| // val excels = resolver.getResources("bomImport/*.xlsx") | // 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/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") | // val excels = resolver.getResources("file:C:/Users/2Fi/Desktop/Third Wave of BOM Excel/*.xlsx") | ||||
| println("size: ${excels.size}") | println("size: ${excels.size}") | ||||
| val logExcel = ClassPathResource("excelTemplate/bom_excel_issue_log.xlsx") | val logExcel = ClassPathResource("excelTemplate/bom_excel_issue_log.xlsx") | ||||
| @@ -84,11 +84,13 @@ open class WarehouseService( | |||||
| } else { | } else { | ||||
| capacity = request.capacity | capacity = request.capacity | ||||
| } | } | ||||
| if (request.order == null && request.id == null) { | if (request.order == null && request.id == null) { | ||||
| // Set a default order for new warehouses | // 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) { | } else if (request.order != null) { | ||||
| order = request.order | order = request.order | ||||
| } | } | ||||
| @@ -247,23 +249,31 @@ open class WarehouseService( | |||||
| skippedInSecondLoop++ | skippedInSecondLoop++ | ||||
| continue | continue | ||||
| } | } | ||||
| // use unique identifier from orderMap to get order | // use unique identifier from orderMap to get order | ||||
| val refKey = ref ?: "${i + 1}" | 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) { | if (order == null) { | ||||
| logger.warn("Order not found for key: ${refKey}Key at row ${i + 1}") | logger.warn("Order not found for key: ${refKey}Key at row ${i + 1}") | ||||
| skippedInSecondLoop++ | skippedInSecondLoop++ | ||||
| continue | continue | ||||
| } | } | ||||
| val capacity = BigDecimal(10000) | val capacity = BigDecimal(10000) | ||||
| val code = "$store_id-$warehouse-$area-$slot" | val code = "$store_id-$warehouse-$area-$slot" | ||||
| val name = "$store_id-$storeLocation" | val name = "$store_id-$storeLocation" | ||||
| val description = "$store_id-$storeLocation" | val description = "$store_id-$storeLocation" | ||||
| // check if the warehouse exists | // check if the warehouse exists | ||||
| val existingWarehouse = if (!ref.isNullOrBlank()) { | val existingWarehouse = if (!ref.isNullOrBlank()) { | ||||
| try { | try { | ||||
| @@ -276,7 +286,7 @@ open class WarehouseService( | |||||
| } else { | } else { | ||||
| null | null | ||||
| } | } | ||||
| if (existingWarehouse != null) { | if (existingWarehouse != null) { | ||||
| // update the warehouse | // update the warehouse | ||||
| existingWarehouse.apply { | existingWarehouse.apply { | ||||
| @@ -12,7 +12,7 @@ data class SaveWarehouseRequest( | |||||
| val warehouse: String? = null, | val warehouse: String? = null, | ||||
| val area: String? = null, | val area: String? = null, | ||||
| val slot: String? = null, | val slot: String? = null, | ||||
| val order: Int? = null, | |||||
| val order: String? = null, | |||||
| val stockTakeSection: String? = null, | val stockTakeSection: String? = null, | ||||
| ) | ) | ||||
| data class NewWarehouseRequest( | data class NewWarehouseRequest( | ||||
| @@ -26,7 +26,7 @@ data class NewWarehouseRequest( | |||||
| val area: String, | val area: String, | ||||
| val slot: String, | val slot: String, | ||||
| val store_id: String, | val store_id: String, | ||||
| val order: Int, | |||||
| val order: String, | |||||
| val storeLocation: String, | val storeLocation: String, | ||||
| val stockTakeTable: String, | val stockTakeTable: String, | ||||
| val company: String, | val company: String, | ||||
| @@ -78,6 +78,7 @@ fun countDistinctItemsByWarehouseIds(@Param("warehouseIds") warehouseIds: List<L | |||||
| AND ill.deleted = false | AND ill.deleted = false | ||||
| """) | """) | ||||
| fun countAllByWarehouseIds(@Param("warehouseIds") warehouseIds: List<Long>): Long | fun countAllByWarehouseIds(@Param("warehouseIds") warehouseIds: List<Long>): Long | ||||
| @Query(""" | @Query(""" | ||||
| SELECT ill FROM InventoryLotLine ill | SELECT ill FROM InventoryLotLine ill | ||||
| JOIN ill.inventoryLot il | JOIN ill.inventoryLot il | ||||
| @@ -87,4 +88,21 @@ fun countAllByWarehouseIds(@Param("warehouseIds") warehouseIds: List<Long>): Lon | |||||
| ORDER BY il.expiryDate ASC | ORDER BY il.expiryDate ASC | ||||
| """) | """) | ||||
| fun findExpiredItems(@Param("today") today: LocalDate): List<InventoryLotLine> | 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 | // stockIn = stockInService.create(SaveStockInRequest(purchaseOrderId = request.purchaseOrderId)).entity as StockIn | ||||
| // var stockIn = stockInRepository.findByPurchaseOrderIdAndDeletedFalse(request.purchaseOrderId) | // var stockIn = stockInRepository.findByPurchaseOrderIdAndDeletedFalse(request.purchaseOrderId) | ||||
| } | } | ||||
| val item = itemRepository.findById(request.itemId).orElseThrow() | val item = itemRepository.findById(request.itemId).orElseThrow() | ||||
| // If request contains valid POL | // If request contains valid POL | ||||
| if (pol != null) { | if (pol != null) { | ||||
| @@ -168,7 +168,7 @@ open class StockInLineService( | |||||
| if (jo != null && jo?.bom != null) { | if (jo != null && jo?.bom != null) { | ||||
| // For job orders, demandQty comes from BOM's outputQty | // For job orders, demandQty comes from BOM's outputQty | ||||
| this.demandQty = jo?.bom?.outputQty | this.demandQty = jo?.bom?.outputQty | ||||
| } else if (pol != null) { | } else if (pol != null) { | ||||
| // For purchase orders, demandQty comes from PurchaseOrderLine's qty | // For purchase orders, demandQty comes from PurchaseOrderLine's qty | ||||
| this.demandQty = pol.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? | // 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 | // return list of stock in line, update data grid with the list | ||||
| stockInLine.apply { | stockInLine.apply { | ||||
| this.productionDate = request.productionDate?.atStartOfDay() ?: this.productionDate | this.productionDate = request.productionDate?.atStartOfDay() ?: this.productionDate | ||||
| this.productLotNo = request.productLotNo ?: this.productLotNo | this.productLotNo = request.productLotNo ?: this.productLotNo | ||||
| @@ -452,12 +452,12 @@ open class StockInLineService( | |||||
| } else { | } else { | ||||
| // For non-WIP, use original logic | // For non-WIP, use original logic | ||||
| if (request.acceptQty?.compareTo(request.acceptedQty) == 0) | if (request.acceptQty?.compareTo(request.acceptedQty) == 0) | ||||
| StockInLineStatus.COMPLETE.status | |||||
| else | |||||
| StockInLineStatus.COMPLETE.status | |||||
| else | |||||
| StockInLineStatus.PARTIALLY_COMPLETE.status | StockInLineStatus.PARTIALLY_COMPLETE.status | ||||
| } | } | ||||
| // this.inventoryLotLine = savedInventoryLotLine | |||||
| } | |||||
| // this.inventoryLotLine = savedInventoryLotLine | |||||
| } | |||||
| createStockLedgerForStockIn(stockInLine) | createStockLedgerForStockIn(stockInLine) | ||||
| // Update JO Status | // Update JO Status | ||||
| if (stockInLine.jobOrder != null) { //TODO Improve | if (stockInLine.jobOrder != null) { //TODO Improve | ||||
| @@ -621,8 +621,13 @@ open class StockInLineService( | |||||
| // 3. Call the utility function with the temporary file. | // 3. Call the utility function with the temporary file. | ||||
| val printQty = if (request.printQty == null || request.printQty <= 0) 1 else request.printQty | val printQty = if (request.printQty == null || request.printQty <= 0) 1 else request.printQty | ||||
| printer.ip?.let { ip -> printer.port?.let { port -> | 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 { | } finally { | ||||
| @@ -664,31 +669,31 @@ open class StockInLineService( | |||||
| val inventoryLot = inventoryLotRepository.findByIdAndDeletedFalse( | val inventoryLot = inventoryLotRepository.findByIdAndDeletedFalse( | ||||
| inventoryLotLine?.inventoryLot?.id ?: throw IllegalArgumentException("Inventory lot ID not found") | inventoryLotLine?.inventoryLot?.id ?: throw IllegalArgumentException("Inventory lot ID not found") | ||||
| ) ?: throw IllegalArgumentException("Inventory lot not found") | ) ?: throw IllegalArgumentException("Inventory lot not found") | ||||
| val stockIn=StockIn().apply { | val stockIn=StockIn().apply { | ||||
| this.code=stockTake?.code | this.code=stockTake?.code | ||||
| this.status=request.stockIntype | this.status=request.stockIntype | ||||
| //this.stockTake=stockTake | //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, | id = inventoryLotLine.id, | ||||
| inventoryLotId = inventoryLotLine.inventoryLot?.id, | inventoryLotId = inventoryLotLine.inventoryLot?.id, | ||||
| warehouseId = inventoryLotLine.warehouse?.id, | warehouseId = inventoryLotLine.warehouse?.id, | ||||
| @@ -713,11 +718,11 @@ open class StockInLineService( | |||||
| private fun createStockLedgerForStockIn(stockInLine: StockInLine) { | private fun createStockLedgerForStockIn(stockInLine: StockInLine) { | ||||
| val item = stockInLine.item ?: return | val item = stockInLine.item ?: return | ||||
| val inventory = inventoryRepository.findByItemId(item.id!!).orElse(null) ?: return | val inventory = inventoryRepository.findByItemId(item.id!!).orElse(null) ?: return | ||||
| val inQty = stockInLine.acceptedQty?.toDouble() ?: 0.0 | val inQty = stockInLine.acceptedQty?.toDouble() ?: 0.0 | ||||
| // 直接使用 inventory.onHandQty 作为 balance(已经是更新后的值) | // 直接使用 inventory.onHandQty 作为 balance(已经是更新后的值) | ||||
| val newBalance = (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() | val newBalance = (inventory.onHandQty ?: BigDecimal.ZERO).toDouble() | ||||
| val stockLedger = StockLedger().apply { | val stockLedger = StockLedger().apply { | ||||
| this.stockInLine = stockInLine | this.stockInLine = stockInLine | ||||
| this.inventory = inventory | this.inventory = inventory | ||||
| @@ -729,7 +734,139 @@ open class StockInLineService( | |||||
| this.itemCode = item.code | this.itemCode = item.code | ||||
| this.date = LocalDate.now() | this.date = LocalDate.now() | ||||
| } | } | ||||
| stockLedgerRepository.saveAndFlush(stockLedger) | 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 com.ffii.fpsms.modules.common.CodeGenerator | ||||
| import org.springframework.context.annotation.Lazy | import org.springframework.context.annotation.Lazy | ||||
| import com.ffii.fpsms.modules.bag.service.BagService | 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.StockLedgerRepository | ||||
| import com.ffii.fpsms.modules.stock.entity.InventoryRepository | import com.ffii.fpsms.modules.stock.entity.InventoryRepository | ||||
| @@ -1208,4 +1209,54 @@ private fun createStockLedgerForStockOut(stockOutLine: StockOutLine) { | |||||
| stockLedgerRepository.saveAndFlush(stockLedger) | 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.Valid; | ||||
| import jakarta.validation.constraints.NotBlank; | import jakarta.validation.constraints.NotBlank; | ||||
| import com.ffii.fpsms.modules.common.internalSetup.UsersSetup; | |||||
| @RestController | @RestController | ||||
| @RequestMapping("/user") | @RequestMapping("/user") | ||||
| public class UserController{ | public class UserController{ | ||||
| @@ -64,19 +62,17 @@ public class UserController{ | |||||
| private PasswordEncoder passwordEncoder; | private PasswordEncoder passwordEncoder; | ||||
| private SettingsService settingsService; | private SettingsService settingsService; | ||||
| private UserQrCodeService userQrCodeService; | private UserQrCodeService userQrCodeService; | ||||
| private UsersSetup usersSetup; | |||||
| public UserController( | public UserController( | ||||
| UserService userService, | UserService userService, | ||||
| PasswordEncoder passwordEncoder, | PasswordEncoder passwordEncoder, | ||||
| SettingsService settingsService, | SettingsService settingsService, | ||||
| UserQrCodeService userQrCodeService, | |||||
| UsersSetup usersSetup) { | |||||
| UserQrCodeService userQrCodeService | |||||
| ) { | |||||
| this.userService = userService; | this.userService = userService; | ||||
| this.passwordEncoder = passwordEncoder; | this.passwordEncoder = passwordEncoder; | ||||
| this.settingsService = settingsService; | this.settingsService = settingsService; | ||||
| this.userQrCodeService = userQrCodeService; | this.userQrCodeService = userQrCodeService; | ||||
| this.usersSetup = usersSetup; | |||||
| } | } | ||||
| // @Operation(summary = "list user", responses = { @ApiResponse(responseCode = "200"), | // @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`; | |||||