Преглед на файлове

setup functions + bug fix

master
kelvin.yau преди 2 седмици
родител
ревизия
ceda882857
променени са 24 файла, в които са добавени 1300 реда и са изтрити 99 реда
  1. +3
    -3
      src/main/java/com/ffii/core/utils/ZebraPrinterUtil.kt
  2. +167
    -0
      src/main/java/com/ffii/fpsms/modules/common/internalSetup/SetupController.kt
  3. +293
    -0
      src/main/java/com/ffii/fpsms/modules/common/internalSetup/inventorySetup.kt
  4. +155
    -0
      src/main/java/com/ffii/fpsms/modules/common/internalSetup/itemSetup.kt
  5. +1
    -3
      src/main/java/com/ffii/fpsms/modules/common/internalSetup/userSetup.kt
  6. +167
    -0
      src/main/java/com/ffii/fpsms/modules/common/internalSetup/warehouseSetup.kt
  7. +2
    -1
      src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt
  8. +3
    -3
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt
  9. +2
    -1
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt
  10. +2
    -2
      src/main/java/com/ffii/fpsms/modules/master/entity/Warehouse.kt
  11. +2
    -2
      src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt
  12. +20
    -10
      src/main/java/com/ffii/fpsms/modules/master/service/WarehouseService.kt
  13. +2
    -2
      src/main/java/com/ffii/fpsms/modules/master/web/models/SaveWarehouseRequest.kt
  14. +18
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt
  15. +171
    -34
      src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt
  16. +51
    -0
      src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt
  17. +180
    -0
      src/main/java/com/ffii/fpsms/modules/stock/service/StockTransferRecordService.kt
  18. +18
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/StockTransferRecordController.kt
  19. +9
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/model/CreateStockTransferRequest.kt
  20. +17
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/model/StockInRequest.kt
  21. +7
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/model/StockOutRequest.kt
  22. +2
    -33
      src/main/java/com/ffii/fpsms/modules/user/web/UserController.java
  23. +0
    -5
      src/main/resources/db/changelog/changes/20260119_fai/02_cheduler_log query copy.sql
  24. +8
    -0
      src/main/resources/db/changelog/changes/20260119_fai/02_scheduler_log_query_copy.sql

+ 3
- 3
src/main/java/com/ffii/core/utils/ZebraPrinterUtil.kt Целия файл

@@ -35,13 +35,13 @@ open class ZebraPrinterUtil {
* @param printDirection Valid values: N (Normal), R (Rotated 90), I (Inverted 180), B (Bottom-up 270)
* @throws Exception if there is an error during file processing or printing.
*/
fun printPdfToZebra(pdfFile: File, printerIp: String, printerPort: Int, printQty: Int? = 1, printDirection: PrintDirection = PrintDirection.NORMAL) {
fun printPdfToZebra(pdfFile: File, printerIp: String, printerPort: Int, printQty: Int? = 1, printDirection: PrintDirection = PrintDirection.NORMAL, dpi: Int? = 203) {

// Check if the file exists and is readable
if (!pdfFile.exists() || !pdfFile.canRead()) {
throw IllegalArgumentException("Error: File not found or not readable at path: ${pdfFile.absolutePath}")
}
val renderDpi = dpi ?: 203
try {
// 1. Load the PDF document
PDDocument.load(pdfFile).use { document ->
@@ -59,7 +59,7 @@ open class ZebraPrinterUtil {
println("DEBUG: Processing page ${pageIndex + 1} of $totalPages")

// 2. Render each page of the PDF as a monochrome image
val image = renderer.renderImage(pageIndex, 203 / 72f, ImageType.BINARY)
val image = renderer.renderImage(pageIndex, renderDpi / 72f, ImageType.BINARY)

// 3. Convert the image to a ZPL format string
val zplCommand = convertImageToZpl(image, printDirection)


+ 167
- 0
src/main/java/com/ffii/fpsms/modules/common/internalSetup/SetupController.kt Целия файл

@@ -0,0 +1,167 @@
package com.ffii.fpsms.modules.common.internalSetup

import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/setup")
class SetupController(
private val userSetup: UserSetup,
private val warehouseSetup: WarehouseSetup,
private val itemsSetup: ItemsSetup,
private val inventorySetup: InventorySetup
) {

@PostMapping("/user")
fun importUsersFromExcel(@RequestBody request: Map<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)
}
}
}

+ 293
- 0
src/main/java/com/ffii/fpsms/modules/common/internalSetup/inventorySetup.kt Целия файл

@@ -0,0 +1,293 @@
package com.ffii.fpsms.modules.common.internalSetup

import com.ffii.core.utils.ExcelUtils
import org.apache.poi.ss.usermodel.Sheet
import org.apache.poi.ss.usermodel.Workbook
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.springframework.stereotype.Component
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.transaction.annotation.Transactional
import com.ffii.fpsms.modules.master.entity.ItemsRepository
import com.ffii.fpsms.modules.master.entity.WarehouseRepository
import com.ffii.fpsms.modules.stock.service.StockInLineService
import com.ffii.fpsms.modules.stock.web.model.StockInRequest
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.math.BigDecimal
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository
import com.ffii.fpsms.modules.stock.web.model.PrintQrCodeForSilRequest

@Component
open class InventorySetup {

@Autowired
private lateinit var itemsRepository: ItemsRepository

@Autowired
private lateinit var warehouseRepository: WarehouseRepository

@Autowired
private lateinit var stockInLineService: StockInLineService

private val DEFAULT_WAREHOUSE_CODE = "2F-W201-#A-01"
private val DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy/MM/dd")

@Transactional(rollbackFor = [Exception::class])
open fun importExcelFromLocal(filePath: String): Int {
val file = File(filePath)
if (!file.exists()) {
throw IOException("File not found: $filePath")
}

FileInputStream(file).use { inputStream ->
val workbook: Workbook = XSSFWorkbook(inputStream)

try {
val sheet: Sheet = workbook.getSheetAt(0)
val inventoryDataList = mutableListOf<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
}

}

+ 155
- 0
src/main/java/com/ffii/fpsms/modules/common/internalSetup/itemSetup.kt Целия файл

@@ -0,0 +1,155 @@
package com.ffii.fpsms.modules.common.internalSetup

import com.ffii.core.utils.ExcelUtils
import org.apache.poi.ss.usermodel.Sheet
import org.apache.poi.ss.usermodel.Workbook
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.springframework.stereotype.Component
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.transaction.annotation.Transactional
import com.ffii.fpsms.modules.master.entity.Items
import com.ffii.fpsms.modules.master.entity.ItemsRepository
import java.io.File
import java.io.FileInputStream
import java.io.IOException

@Component
open class ItemsSetup {

@Autowired
private lateinit var itemsRepository: ItemsRepository

data class ItemsData(
val code: String,
val type: String?,
val store_id: String?,
val warehouse: String?,
val area: String?,
val slot: String?
)

@Transactional(rollbackFor = [Exception::class])
open fun importExcelFromLocal(filePath: String): Int {
val file = File(filePath)
if (!file.exists()) {
throw IOException("File not found: $filePath")
}

FileInputStream(file).use { inputStream ->
val workbook: Workbook = XSSFWorkbook(inputStream)

try {
val sheet: Sheet = workbook.getSheetAt(0)
val itemsDataList = mutableListOf<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()
}
}
}
}

src/main/java/com/ffii/fpsms/modules/common/internalSetup/usersSetup.kt → src/main/java/com/ffii/fpsms/modules/common/internalSetup/userSetup.kt Целия файл

@@ -1,8 +1,6 @@
package com.ffii.fpsms.modules.common.internalSetup

import com.ffii.core.utils.ExcelUtils
import org.apache.poi.ss.usermodel.Cell
import org.apache.poi.ss.usermodel.Row
import org.apache.poi.ss.usermodel.Sheet
import org.apache.poi.ss.usermodel.Workbook
import org.apache.poi.xssf.usermodel.XSSFWorkbook
@@ -17,7 +15,7 @@ import java.io.FileInputStream
import java.io.IOException

@Component
open class UsersSetup {
open class UserSetup {

@Autowired
private lateinit var userRepository: UserRepository

+ 167
- 0
src/main/java/com/ffii/fpsms/modules/common/internalSetup/warehouseSetup.kt Целия файл

@@ -0,0 +1,167 @@
package com.ffii.fpsms.modules.common.internalSetup

import com.ffii.core.utils.ExcelUtils
import org.apache.poi.ss.usermodel.Sheet
import org.apache.poi.ss.usermodel.Workbook
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.springframework.stereotype.Component
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.transaction.annotation.Transactional
import com.ffii.fpsms.modules.master.entity.Warehouse
import com.ffii.fpsms.modules.master.entity.WarehouseRepository
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.math.BigDecimal

@Component
open class WarehouseSetup {

@Autowired
private lateinit var warehouseRepository: WarehouseRepository

data class WarehouseData(
val store_id: String,
val warehouse: String,
val area: String,
val slot: String,
val order: String?,
val stockTakeSection: String?
)

@Transactional(rollbackFor = [Exception::class])
open fun importExcelFromLocal(filePath: String): Int {
val file = File(filePath)
if (!file.exists()) {
throw IOException("File not found: $filePath")
}

FileInputStream(file).use { inputStream ->
val workbook: Workbook = XSSFWorkbook(inputStream)

try {
val sheet: Sheet = workbook.getSheetAt(0)
val warehouseDataList = mutableListOf<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()
}
}
}
}

+ 2
- 1
src/main/java/com/ffii/fpsms/modules/deliveryOrder/service/DeliveryOrderService.kt Целия файл

@@ -1070,7 +1070,8 @@ open class DeliveryOrderService(
ip,
port,
printQty,
ZebraPrinterUtil.PrintDirection.ROTATED
ZebraPrinterUtil.PrintDirection.ROTATED,
printer.dpi
)
}
}


+ 3
- 3
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JoPickOrderService.kt Целия файл

@@ -488,7 +488,7 @@ open class JoPickOrderService(
suggestedPickLotId = spl.id,
stockOutLineQty = sol?.qty ?: 0.0,
stockOutLineStatus = sol?.status,
routerIndex = warehouse?.order,
routerIndex = warehouse?.order?.toIntOrNull(),
routerArea = warehouse?.code,
routerRoute = warehouse?.code,
uomShortDesc = uom?.udfShortDesc,
@@ -714,7 +714,7 @@ open class JoPickOrderService(
"stockOutLineId" to sol?.id,
"stockOutLineStatus" to sol?.status,
"stockOutLineQty" to (sol?.qty ?: 0.0),
"routerIndex" to warehouse?.order,
"routerIndex" to warehouse?.order?.toIntOrNull(),
"routerArea" to warehouse?.code,
"routerRoute" to warehouse?.code,
"uomShortDesc" to uom?.udfShortDesc,
@@ -2003,7 +2003,7 @@ open fun getJobOrderLotsHierarchicalByPickOrderId(pickOrderId: Long): JobOrderLo
suggestedPickLotId = spl.id,
stockOutLineQty = sol?.qty ?: 0.0,
stockOutLineStatus = sol?.status,
routerIndex = warehouse?.order,
routerIndex = warehouse?.order?.toIntOrNull(),
routerArea = warehouse?.code,
routerRoute = warehouse?.code,
uomShortDesc = uom?.udfShortDesc,


+ 2
- 1
src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt Целия файл

@@ -759,7 +759,8 @@ open class JobOrderService(
ip,
port,
printQty,
ZebraPrinterUtil.PrintDirection.ROTATED
ZebraPrinterUtil.PrintDirection.ROTATED,
printer.dpi
)
}
}


+ 2
- 2
src/main/java/com/ffii/fpsms/modules/master/entity/Warehouse.kt Целия файл

@@ -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


+ 2
- 2
src/main/java/com/ffii/fpsms/modules/master/service/BomService.kt Целия файл

@@ -636,8 +636,8 @@ open class BomService(
val resolver = PathMatchingResourcePatternResolver()
// val excels = resolver.getResources("bomImport/*.xlsx")
//val excels = resolver.getResources("file:C:/Users/Kelvin YAU/Downloads/bom/*.xlsx")
//val excels = resolver.getResources("file:C:/Users/Kelvin YAU/Downloads/bom/*.xlsx")
val excels = resolver.getResources("file:C:/Users/kw093/Downloads/bom/bom/*.xlsx")
val excels = resolver.getResources("file:C:/Users/Kelvin YAU/Downloads/datasetup/bom/*.xlsx")
// val excels = resolver.getResources("file:C:/Users/kw093/Downloads/bom/bom/*.xlsx")
// val excels = resolver.getResources("file:C:/Users/2Fi/Desktop/Third Wave of BOM Excel/*.xlsx")
println("size: ${excels.size}")
val logExcel = ClassPathResource("excelTemplate/bom_excel_issue_log.xlsx")


+ 20
- 10
src/main/java/com/ffii/fpsms/modules/master/service/WarehouseService.kt Целия файл

@@ -84,11 +84,13 @@ open class WarehouseService(
} else {
capacity = request.capacity
}
if (request.order == null && request.id == null) {
// Set a default order for new warehouses
val maxOrder = warehouseRepository.findAll().mapNotNull { it.order }.maxOrNull() ?: 0
order = maxOrder + 1
val maxOrder = warehouseRepository.findAll()
.mapNotNull { it.order?.toIntOrNull() }
.maxOrNull() ?: 0
order = (maxOrder + 1).toString()
} else if (request.order != null) {
order = request.order
}
@@ -247,23 +249,31 @@ open class WarehouseService(
skippedInSecondLoop++
continue
}
// use unique identifier from orderMap to get order
val refKey = ref ?: "${i + 1}"
val order = orderMap[refKey]
val orderInt = orderMap[refKey] // This is Int

if (orderInt == null) {
logger.warn("Order not found for key: ${refKey}Key at row ${i + 1}")
skippedInSecondLoop++
continue
}

val order = orderInt.toString() // Convert Int to String

if (order == null) {
logger.warn("Order not found for key: ${refKey}Key at row ${i + 1}")
skippedInSecondLoop++
continue
}
val capacity = BigDecimal(10000)
val code = "$store_id-$warehouse-$area-$slot"
val name = "$store_id-$storeLocation"
val description = "$store_id-$storeLocation"
// check if the warehouse exists
val existingWarehouse = if (!ref.isNullOrBlank()) {
try {
@@ -276,7 +286,7 @@ open class WarehouseService(
} else {
null
}
if (existingWarehouse != null) {
// update the warehouse
existingWarehouse.apply {


+ 2
- 2
src/main/java/com/ffii/fpsms/modules/master/web/models/SaveWarehouseRequest.kt Целия файл

@@ -12,7 +12,7 @@ data class SaveWarehouseRequest(
val warehouse: String? = null,
val area: String? = null,
val slot: String? = null,
val order: Int? = null,
val order: String? = null,
val stockTakeSection: String? = null,
)
data class NewWarehouseRequest(
@@ -26,7 +26,7 @@ data class NewWarehouseRequest(
val area: String,
val slot: String,
val store_id: String,
val order: Int,
val order: String,
val storeLocation: String,
val stockTakeTable: String,
val company: String,


+ 18
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/InventoryLotLineRepository.kt Целия файл

@@ -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>
}

+ 171
- 34
src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt Целия файл

@@ -118,7 +118,7 @@ open class StockInLineService(
// stockIn = stockInService.create(SaveStockInRequest(purchaseOrderId = request.purchaseOrderId)).entity as StockIn
// var stockIn = stockInRepository.findByPurchaseOrderIdAndDeletedFalse(request.purchaseOrderId)
}
val item = itemRepository.findById(request.itemId).orElseThrow()
// If request contains valid POL
if (pol != null) {
@@ -168,7 +168,7 @@ open class StockInLineService(
if (jo != null && jo?.bom != null) {
// For job orders, demandQty comes from BOM's outputQty
this.demandQty = jo?.bom?.outputQty
} else if (pol != null) {
// For purchase orders, demandQty comes from PurchaseOrderLine's qty
this.demandQty = pol.qty
@@ -406,7 +406,7 @@ open class StockInLineService(
}
// TODO: check all status to prevent reverting progress due to multiple users access to the same po?
// return list of stock in line, update data grid with the list
stockInLine.apply {
this.productionDate = request.productionDate?.atStartOfDay() ?: this.productionDate
this.productLotNo = request.productLotNo ?: this.productLotNo
@@ -452,12 +452,12 @@ open class StockInLineService(
} else {
// For non-WIP, use original logic
if (request.acceptQty?.compareTo(request.acceptedQty) == 0)
StockInLineStatus.COMPLETE.status
else
StockInLineStatus.COMPLETE.status
else
StockInLineStatus.PARTIALLY_COMPLETE.status
}
// this.inventoryLotLine = savedInventoryLotLine
}
// this.inventoryLotLine = savedInventoryLotLine
}
createStockLedgerForStockIn(stockInLine)
// Update JO Status
if (stockInLine.jobOrder != null) { //TODO Improve
@@ -621,8 +621,13 @@ open class StockInLineService(
// 3. Call the utility function with the temporary file.
val printQty = if (request.printQty == null || request.printQty <= 0) 1 else request.printQty
printer.ip?.let { ip -> printer.port?.let { port ->
ZebraPrinterUtil.printPdfToZebra(tempPdfFile, ip, port, printQty,
ZebraPrinterUtil.PrintDirection.ROTATED
ZebraPrinterUtil.printPdfToZebra(
tempPdfFile,
ip,
port,
printQty,
ZebraPrinterUtil.PrintDirection.ROTATED,
printer.dpi
)
} }
} finally {
@@ -664,31 +669,31 @@ open class StockInLineService(
val inventoryLot = inventoryLotRepository.findByIdAndDeletedFalse(
inventoryLotLine?.inventoryLot?.id ?: throw IllegalArgumentException("Inventory lot ID not found")
) ?: throw IllegalArgumentException("Inventory lot not found")
val stockIn=StockIn().apply {
this.code=stockTake?.code
this.status=request.stockIntype
//this.stockTake=stockTake
}
stockInRepository.save(stockIn)
val stockInLine = StockInLine().apply {
//this.stockTakeLine=stockTakeLine
this.item=inventoryLot.item
this.itemNo=request.itemNo
this.stockIn = stockIn
this.demandQty=request.demandQty
this.acceptedQty=request.acceptedQty
this.expiryDate=inventoryLot.expiryDate
this.inventoryLot=inventoryLot
this.inventoryLotLine=inventoryLotLine
this.lotNo=newLotNo
this.status = "completed"
this.type = request.stockInLineType
}
stockInLineRepository.save(stockInLine)
val updateRequest = SaveInventoryLotLineRequest(
}
stockInRepository.save(stockIn)
val stockInLine = StockInLine().apply {
//this.stockTakeLine=stockTakeLine
this.item=inventoryLot.item
this.itemNo=request.itemNo
this.stockIn = stockIn
this.demandQty=request.demandQty
this.acceptedQty=request.acceptedQty
this.expiryDate=inventoryLot.expiryDate
this.inventoryLot=inventoryLot
this.inventoryLotLine=inventoryLotLine
this.lotNo=newLotNo
this.status = "completed"
this.type = request.stockInLineType
}
stockInLineRepository.save(stockInLine)
val updateRequest = SaveInventoryLotLineRequest(
id = inventoryLotLine.id,
inventoryLotId = inventoryLotLine.inventoryLot?.id,
warehouseId = inventoryLotLine.warehouse?.id,
@@ -713,11 +718,11 @@ open class StockInLineService(
private fun createStockLedgerForStockIn(stockInLine: StockInLine) {
val item = stockInLine.item ?: return
val inventory = inventoryRepository.findByItemId(item.id!!).orElse(null) ?: return
val inQty = stockInLine.acceptedQty?.toDouble() ?: 0.0
// 直接使用 inventory.onHandQty 作为 balance(已经是更新后的值)
val newBalance = (inventory.onHandQty ?: BigDecimal.ZERO).toDouble()
val stockLedger = StockLedger().apply {
this.stockInLine = stockInLine
this.inventory = inventory
@@ -729,7 +734,139 @@ open class StockInLineService(
this.itemCode = item.code
this.date = LocalDate.now()
}
stockLedgerRepository.saveAndFlush(stockLedger)
}
}

@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
}
}

+ 51
- 0
src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt Целия файл

@@ -37,6 +37,7 @@ import com.ffii.fpsms.modules.bag.web.model.CreateBagLotLineRequest
import com.ffii.fpsms.modules.common.CodeGenerator
import org.springframework.context.annotation.Lazy
import com.ffii.fpsms.modules.bag.service.BagService
import com.ffii.fpsms.modules.common.SecurityUtils
import com.ffii.fpsms.modules.stock.entity.StockLedgerRepository
import com.ffii.fpsms.modules.stock.entity.InventoryRepository

@@ -1208,4 +1209,54 @@ private fun createStockLedgerForStockOut(stockOutLine: StockOutLine) {
stockLedgerRepository.saveAndFlush(stockLedger)
}

open fun createStockOut(request: StockOutRequest): StockOutLine {

val inventoryLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId).orElseThrow()

// Step 1: Increase outQty in inventory_lot_line table
val updatedInventoryLotLine = inventoryLotLine.apply {
val currentOutQty = this.outQty ?: BigDecimal.ZERO
val newOutQty = currentOutQty + BigDecimal.valueOf(request.qty)
this.outQty = newOutQty

val currentInQty = this.inQty ?: BigDecimal.ZERO
if (newOutQty.compareTo(currentInQty) == 0) {
this.status = InventoryLotLineStatus.UNAVAILABLE
}
}
inventoryLotLineRepository.save(updatedInventoryLotLine)

// Step 2: Create a row of stock_out
val currentUser = SecurityUtils.getUser().orElseThrow()
val stockOut = StockOut().apply {
this.type = request.type
this.completeDate = LocalDateTime.now()
this.handler = currentUser.id
this.status = StockOutStatus.COMPLETE.status
}
val savedStockOut = stockOutRepository.save(stockOut)

// Step 3: Create a row in stock_out_line table
val itemId = updatedInventoryLotLine.inventoryLot?.item?.id
?: throw IllegalArgumentException("InventoryLotLine must have an associated item")
val item = itemRepository.findById(itemId).orElseThrow()

val stockOutLine = StockOutLine().apply {
this.item = item
this.qty = request.qty
this.stockOut = savedStockOut
this.inventoryLotLine = updatedInventoryLotLine
this.status = StockOutLineStatus.COMPLETE.status
this.pickTime = LocalDateTime.now()
this.pickerId = currentUser.id
this.type = request.type
}
val savedStockOutLine = saveAndFlush(stockOutLine)

// Step 4: Create a row in stock_ledger table
createStockLedgerForStockOut(savedStockOutLine)

return savedStockOutLine
}
}

+ 180
- 0
src/main/java/com/ffii/fpsms/modules/stock/service/StockTransferRecordService.kt Целия файл

@@ -0,0 +1,180 @@
package com.ffii.fpsms.modules.stock.service

import com.ffii.core.support.AbstractBaseEntityService
import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.master.web.models.MessageResponse
import com.ffii.fpsms.modules.stock.entity.StockInLine
import com.ffii.fpsms.modules.stock.entity.StockOutLine
import com.ffii.fpsms.modules.stock.entity.StockTransferRecord
import com.ffii.fpsms.modules.stock.entity.StockTransferRecordRepository
import com.ffii.fpsms.modules.stock.web.model.CreateStockTransferRequest
import com.ffii.fpsms.modules.stock.web.model.StockInRequest
import com.ffii.fpsms.modules.stock.web.model.StockOutRequest
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.math.BigDecimal
import java.time.LocalDate
import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository
import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus


@Service
open class StockTransferRecordService(
private val jdbcDao: JdbcDao,
private val stockTransferRecordRepository: StockTransferRecordRepository,
private val stockOutLineService: StockOutLineService,
private val stockInLineService: StockInLineService,
private val inventoryLotLineRepository: InventoryLotLineRepository
) : AbstractBaseEntityService<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
}
}

+ 18
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/StockTransferRecordController.kt Целия файл

@@ -0,0 +1,18 @@
package com.ffii.fpsms.modules.stock.web

import com.ffii.fpsms.modules.master.web.models.MessageResponse
import com.ffii.fpsms.modules.stock.service.StockTransferRecordService
import com.ffii.fpsms.modules.stock.web.model.CreateStockTransferRequest
import jakarta.validation.Valid
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping("/stockTransferRecord")
class StockTransferRecordController(
private val stockTransferRecordService: StockTransferRecordService
) {
@PostMapping("/create")
fun createStockTransfer(@Valid @RequestBody request: CreateStockTransferRequest): MessageResponse {
return stockTransferRecordService.createStockTransfer(request)
}
}

+ 9
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/model/CreateStockTransferRequest.kt Целия файл

@@ -0,0 +1,9 @@
package com.ffii.fpsms.modules.stock.web.model

import java.math.BigDecimal

data class CreateStockTransferRequest(
val inventoryLotLineId: Long,
val transferredQty: BigDecimal,
val warehouseId: Long? = null
)

+ 17
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/model/StockInRequest.kt Целия файл

@@ -0,0 +1,17 @@
package com.ffii.fpsms.modules.stock.web.model

import java.math.BigDecimal
import java.time.LocalDate

data class StockInRequest(
val itemId: Long,
val itemNo: String,
val demandQty: BigDecimal? = null,
val acceptedQty: BigDecimal,
val expiryDate: LocalDate,
val lotNo: String? = null,
val productLotNo: String? = null,
val dnNo: String? = null,
val type: String? = null,
val warehouseId: Long? = null
)

+ 7
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/model/StockOutRequest.kt Целия файл

@@ -0,0 +1,7 @@
package com.ffii.fpsms.modules.stock.web.model

data class StockOutRequest(
val inventoryLotLineId: Long,
val qty: Double,
val type: String,
)

+ 2
- 33
src/main/java/com/ffii/fpsms/modules/user/web/UserController.java Целия файл

@@ -53,8 +53,6 @@ import java.io.UnsupportedEncodingException;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;

import com.ffii.fpsms.modules.common.internalSetup.UsersSetup;

@RestController
@RequestMapping("/user")
public class UserController{
@@ -64,19 +62,17 @@ public class UserController{
private PasswordEncoder passwordEncoder;
private SettingsService settingsService;
private UserQrCodeService userQrCodeService;
private UsersSetup usersSetup;

public UserController(
UserService userService,
PasswordEncoder passwordEncoder,
SettingsService settingsService,
UserQrCodeService userQrCodeService,
UsersSetup usersSetup) {
UserQrCodeService userQrCodeService
) {
this.userService = userService;
this.passwordEncoder = passwordEncoder;
this.settingsService = settingsService;
this.userQrCodeService = userQrCodeService;
this.usersSetup = usersSetup;
}
// @Operation(summary = "list user", responses = { @ApiResponse(responseCode = "200"),
@@ -298,31 +294,4 @@ public class UserController{

}

@PostMapping("/users-setup")
public ResponseEntity<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);
}
}

}

+ 0
- 5
src/main/resources/db/changelog/changes/20260119_fai/02_cheduler_log query copy.sql Целия файл

@@ -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`;

+ 8
- 0
src/main/resources/db/changelog/changes/20260119_fai/02_scheduler_log_query_copy.sql Целия файл

@@ -0,0 +1,8 @@
--liquibase formatted sql
--changeset author:add_scheduler_log query

--preconditions onFail:MARK_RAN
--precondition-sql-check expectedResult:0 SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'scheduler_sync_log' AND COLUMN_NAME = 'query'

ALTER TABLE `fpsmsdb`.`scheduler_sync_log`
ADD COLUMN `query` VARCHAR(2000) NULL AFTER `endTime`;

Зареждане…
Отказ
Запис