Browse Source

Import Stock Take Excel Function

master
kelvin.yau 2 weeks ago
parent
commit
f95aff4618
8 changed files with 784 additions and 4 deletions
  1. +24
    -0
      src/main/java/com/ffii/fpsms/modules/master/entity/Items.kt
  2. +723
    -0
      src/main/java/com/ffii/fpsms/modules/master/service/ItemsService.kt
  3. +21
    -0
      src/main/java/com/ffii/fpsms/modules/master/web/ItemsController.kt
  4. +5
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockInRepository.kt
  5. +1
    -1
      src/main/java/com/ffii/fpsms/modules/stock/service/StockInService.kt
  6. +2
    -1
      src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt
  7. +3
    -2
      src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeService.kt
  8. +5
    -0
      src/main/resources/db/changelog/changes/20251127_01_KelvinY/01_add_inventorySheet_to_items.sql

+ 24
- 0
src/main/java/com/ffii/fpsms/modules/master/entity/Items.kt View File

@@ -34,6 +34,30 @@ open class Items : BaseEntity<Long>() {
@Column(name = "countryOfOrigin")
open var countryOfOrigin: String? = null

@Column(name = "inventorySheet")
open var inventorySheet: String? = null

@Column(name = "store_id", nullable = true, length = 255)
open var store_id: String? = null

@Column(name = "storeLocation", nullable = true, length = 255)
open var storeLocation: String? = null

@Column(name = "warehouse", nullable = true, length = 255)
open var warehouse: String? = null

@Column(name = "area", nullable = true, length = 255)
open var area: String? = null

@Column(name = "slot", nullable = true, length = 255)
open var slot: String? = null

@Column(name = "MTMSPickRoutingID", nullable = true)
open var MTMSPickRoutingID: Int? = null

@Column(name = "LocationCode", nullable = true, length = 255)
open var LocationCode: String? = null

@Column(name = "maxQty")
open var maxQty: Double? = null



+ 723
- 0
src/main/java/com/ffii/fpsms/modules/master/service/ItemsService.kt View File

@@ -2,6 +2,7 @@ package com.ffii.fpsms.modules.master.service

import com.ffii.core.support.AbstractBaseEntityService
import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.common.CodeGenerator
import com.ffii.fpsms.modules.master.entity.*
import com.ffii.fpsms.modules.master.web.models.ItemQc
import com.ffii.fpsms.modules.master.web.models.ItemWithQcResponse
@@ -14,6 +15,35 @@ import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.temporal.TemporalAdjusters
import kotlin.jvm.optionals.getOrNull
import org.apache.poi.ss.usermodel.Cell
import org.apache.poi.ss.usermodel.CellType
import org.apache.poi.ss.usermodel.Sheet
import org.apache.poi.ss.usermodel.Workbook
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import java.io.File
import java.io.FileInputStream
import java.math.BigDecimal
import com.ffii.fpsms.modules.stock.entity.Inventory
import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository
import com.ffii.fpsms.modules.stock.entity.InventoryRepository
import com.ffii.fpsms.modules.stock.entity.StockIn
import com.ffii.fpsms.modules.stock.entity.StockInLineRepository
import com.ffii.fpsms.modules.stock.entity.StockInRepository
import com.ffii.fpsms.modules.stock.enums.StockTakeLineStatus
import java.io.FileWriter
import java.io.PrintWriter
import com.ffii.fpsms.modules.stock.service.StockTakeService
import com.ffii.fpsms.modules.stock.web.model.SaveStockTakeRequest
import com.ffii.fpsms.modules.stock.enums.StockTakeStatus
import com.ffii.fpsms.modules.stock.service.StockInService
import com.ffii.fpsms.modules.stock.service.StockTakeLineService
import com.ffii.fpsms.modules.stock.web.model.SaveStockInRequest
import com.ffii.fpsms.modules.stock.web.model.SaveStockTakeLineRequest
import org.springframework.context.annotation.Lazy
import com.ffii.fpsms.modules.stock.service.StockInLineService
import com.ffii.fpsms.modules.stock.web.model.SaveStockInLineRequest
import com.ffii.fpsms.modules.stock.web.model.StockInLineStatus
import com.ffii.fpsms.modules.stock.web.model.SaveInventoryLotLineForSil

@Service
open class ItemsService(
@@ -21,7 +51,167 @@ open class ItemsService(
private val itemsRepository: ItemsRepository,
private val qcCheckRepository: QcCheckRepository,
private val qcItemsRepository: QcItemRepository, private val qcCategoryRepository: QcCategoryRepository,
private val inventoryRepository: InventoryRepository,
private val warehouseService: WarehouseService,
private val stockTakeService: StockTakeService,
@Lazy private val stockInService: StockInService,
private val stockInRepository: StockInRepository,
private val stockTakeLineService: StockTakeLineService,
@Lazy private val itemUomService: ItemUomService,
private val stockInLineService: StockInLineService,
private val stockInLineRepository: StockInLineRepository,
private val inventoryLotLineRepository: InventoryLotLineRepository,
): AbstractBaseEntityService<Items, Long, ItemsRepository>(jdbcDao, itemsRepository) {

private val excelImportPath: String = System.getProperty("user.home") + "/Downloads/StockTakeImport/"

private fun assignStockInNo(): String {
val prefix = "SI"
val midfix = CodeGenerator.DEFAULT_MIDFIX
val latestCode = stockInRepository.findLatestCodeByPrefix("${prefix}-${midfix}")
return CodeGenerator.generateNo(prefix = prefix, latestCode = latestCode)
}

inner class PatchLogWriter(private val logFilePath: String) {
private val logFile = File(logFilePath)
private val logWriter = PrintWriter(FileWriter(logFile, true))
private val timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))

init {
writeHeader()
}

fun writeHeader() {
logWriter.println("=".repeat(80))
logWriter.println("PATCH ITEMS FROM EXCEL - LOG FILE")
logWriter.println("Start Time: $timestamp")
logWriter.println("=".repeat(80))
logWriter.println()
}

fun writeFileInfo(lastRow: Int, startRow: Int) {
logWriter.println("Excel File Information:")
logWriter.println(" Last Row: $lastRow")
logWriter.println(" Starting Row: ${startRow + 1} (0-indexed: $startRow)")
logWriter.println()
logWriter.println("-".repeat(80))
logWriter.println()
}

fun writeRowError(rowNum: Int, message: String) {
logWriter.println("Row ${rowNum + 1}: ERROR - $message")
}

fun writeRowSkipped(rowNum: Int, reason: String) {
logWriter.println("Row ${rowNum + 1}: SKIPPED - $reason")
}

fun writeItemPatched(
rowNum: Int,
itemCode: String,
itemId: Long?,
itemName: String?,
inventorySheet: String?,
type: String?,
storeId: String?,
storeLocation: String?,
warehouse: String?,
area: String?,
slot: String?,
locationCode: String?,
mtmsPickRoutingId: Int?
) {
logWriter.println("Row ${rowNum + 1}: SUCCESS - Patched item '$itemCode' (ID: $itemId, Name: $itemName)")
logWriter.println(" - inventorySheet: '$inventorySheet'")
logWriter.println(" - type: '$type'")
logWriter.println(" - store_id: '$storeId'")
logWriter.println(" - storeLocation: '$storeLocation'")
logWriter.println(" - warehouse: '$warehouse'")
logWriter.println(" - area: '$area'")
logWriter.println(" - slot: '$slot'")
logWriter.println(" - LocationCode: '$locationCode'")
logWriter.println(" - MTMSPickRoutingID: '$mtmsPickRoutingId'")
}

fun writeInventoryDeleted(rowNum: Int, inventoryId: Long?) {
logWriter.println(" INVENTORY: Deleted existing inventory (ID: $inventoryId)")
}

fun writeInventoryDeleteError(rowNum: Int, message: String) {
logWriter.println(" INVENTORY: WARNING - Failed to delete existing inventory - $message")
}

fun writeInventoryCreated(rowNum: Int, stockTakeCount: BigDecimal) {
logWriter.println(" INVENTORY: CREATED - onHandQty: $stockTakeCount, onHoldQty: 0")
}

fun writeInventoryError(rowNum: Int, message: String) {
logWriter.println(" INVENTORY: ERROR - $message")
}

fun writeInventoryIdNull(rowNum: Int) {
logWriter.println(" INVENTORY: ERROR - Item ID is null after save")
}

fun writeEmptyLine() {
logWriter.println()
}

fun writeSummary(
successCount: Int,
errorCount: Int,
inventoryCreatedCount: Int,
inventoryFailedCount: Int,
missingInventoryItems: List<String>,
errors: List<String>
) {
logWriter.println()
logWriter.println("=".repeat(80))
logWriter.println("SUMMARY")
logWriter.println("=".repeat(80))
logWriter.println("End Time: ${LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))}")
logWriter.println()
logWriter.println("Items Patched: $successCount")
logWriter.println("Items with Errors: $errorCount")
logWriter.println()
logWriter.println("Inventory Records Created: $inventoryCreatedCount")
logWriter.println("Inventory Records Failed: $inventoryFailedCount")
logWriter.println()

if (missingInventoryItems.isNotEmpty()) {
logWriter.println("Items Missing Inventory Records (${missingInventoryItems.size}):")
missingInventoryItems.forEach { logWriter.println(" - $it") }
logWriter.println()
}

if (errors.isNotEmpty()) {
logWriter.println("Errors Encountered (${errors.size}):")
errors.forEach { logWriter.println(" - $it") }
}

logWriter.println()
logWriter.println("=".repeat(80))
logWriter.println("END OF LOG")
logWriter.println("=".repeat(80))
}

fun close() {
logWriter.close()
}

fun getLogFilePath(): String {
return logFilePath
}
}

private fun createPatchLogWriter(): PatchLogWriter {
val timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"))
val logFileName = "${excelImportPath}patch_log_$timestamp.txt"
return PatchLogWriter(logFileName)
}



// do mapping with projection
open fun allItems(): List<Items> {
// TODO: Replace by actual logic
@@ -386,4 +576,537 @@ open class ItemsService(
)
return jdbcDao.queryForInts(sql.toString(), args);
}

// Get String Value
private fun getCellStringValue(cell: Cell?): String? {
return cell?.let {
when (it.cellType) {
CellType.STRING -> it.stringCellValue.trim()
CellType.NUMERIC -> it.numericCellValue.toString().trim()
CellType.BLANK -> null
else -> null
}
}?.takeIf { it.isNotBlank() }
}

@Throws(IOException::class)
@Transactional
open fun patchItemsFromExcel(workbook: Workbook?): String {
logger.info("--------- Start - Patch Items from Excel -------")

if (workbook == null) {
logger.error("No Excel Import")
return "Import Excel failure"
}

// Create log writer
val logWriter = createPatchLogWriter()

try {
val sheet: Sheet = workbook.getSheetAt(0)

// Column indices (0-indexed)
val COLUMN_INVENTORY_SHEET_INDEX = 4 // Column E
val COLUMN_CODE_INDEX = 7 // Column H
val COLUMN_TYPE_INDEX = 9 // Column J
val COLUMN_STORE_ID_INDEX = 13 // Column N
val COLUMN_STORE_LOCATION_INDEX = 14 // Column O
val COLUMN_WAREHOUSE_INDEX = 15 // Column P
val COLUMN_AREA_INDEX = 16 // Column Q
val COLUMN_SLOT_INDEX = 17 // Column R
val COLUMN_MTMS_PICK_ROUTING_ID_INDEX = 18 // Column S
val COLUMN_ON_HOLD_QTY_INDEX = 21 // Column V

val START_ROW_INDEX = 4 // Starting from row 4 (0-indexed)

var successCount = 0
var errorCount = 0
var inventoryCreatedCount = 0
var inventoryFailedCount = 0
val errors = mutableListOf<String>()
val missingInventoryItems = mutableListOf<String>()

logWriter.writeFileInfo(sheet.lastRowNum, START_ROW_INDEX)
logger.info("Last row: ${sheet.lastRowNum}")

// Create StockTake (one for entire import)
val startTime = LocalDateTime.now()
val stockTakeCode = stockTakeService.assignStockTakeNo()
val saveStockTakeReq = SaveStockTakeRequest(
code = stockTakeCode,
planStart = startTime,
planEnd = startTime,
actualStart = startTime,
actualEnd = null,
status = StockTakeStatus.PENDING.value,
remarks = "Created from Excel import - Patch Items"
)
val savedStockTake = stockTakeService.saveStockTake(saveStockTakeReq)
logger.info("Created StockTake: ${savedStockTake.code} (ID: ${savedStockTake.id})")

// Create StockIn (one for entire import, linked to StockTake)
val stockInCode = assignStockInNo()
val saveStockInReq = SaveStockInRequest(
code = stockInCode, // Use generated code
stockTakeId = savedStockTake.id
)
val savedStockInResponse = stockInService.create(saveStockInReq)
val savedStockIn = savedStockInResponse.entity as StockIn
logger.info("Created StockIn: ${savedStockIn.code} (ID: ${savedStockIn.id})")

//Tracking duplicates for stock take lines
val processedItemIds = mutableMapOf<Long, Int>()

for (i in START_ROW_INDEX..sheet.lastRowNum) {
val row = sheet.getRow(i)

if (row == null) {
continue
}

// Get item code - Debug
val itemCode = try {
val cell = row.getCell(COLUMN_CODE_INDEX)

logger.info("=== DEBUG Row ${i + 1} ===")
logger.info("Reading from Column H (index ${COLUMN_CODE_INDEX})")

if (cell == null) {
logger.warn("Row ${i + 1}: Cell is NULL")
} else {
logger.info("Row ${i + 1}: Cell type = ${cell.cellType}")
val cellI = row.getCell(COLUMN_CODE_INDEX + 1)
logger.info("Column I (index ${COLUMN_CODE_INDEX + 1}) = '${getCellStringValue(cellI)}'")
}

val extractedCode = getCellStringValue(cell)?.trim()
logger.info("Extracted code = '$extractedCode'")
logger.info("=== END DEBUG Row ${i + 1} ===")

extractedCode
} catch (e: Exception) {
logger.error("Import Error (Row ${i + 1} - Code Error): ${e.message}")
logWriter.writeRowError(i, "Failed to read code - ${e.message}")
errorCount++
errors.add("Row ${i + 1}: Failed to read code - ${e.message}")
continue
}

if (itemCode.isNullOrBlank()) {
logger.warn("Row ${i + 1}: Empty code, skipping")
logWriter.writeRowSkipped(i, "Empty code")
continue
}

// Find item by code
val item = try {
itemsRepository.findByCodeAndDeletedFalse(itemCode)
} catch (e: Exception) {
logger.error("Import Error (Row ${i + 1} - Find Item Error): ${e.message}")
logWriter.writeRowError(i, "Failed to find item with code '$itemCode' - ${e.message}")
errorCount++
errors.add("Row ${i + 1}: Failed to find item with code '$itemCode' - ${e.message}")
continue
}

if (item == null) {
logger.warn("Row ${i + 1}: Item with code '$itemCode' not found, skipping")
logWriter.writeRowSkipped(i, "Item with code '$itemCode' not found in database")
errorCount++
errors.add("Row ${i + 1}: Item with code '$itemCode' not found")
continue
}

if (processedItemIds.containsKey(item.id!!)) {
val previousRow = processedItemIds[item.id!!]!! + 1
logger.warn("Row ${i + 1}: Item '$itemCode' was already processed at row $previousRow - Creating duplicate StockTakeLine")
logWriter.writeRowError(i, "WARNING: Item '$itemCode' appears multiple times (first at row $previousRow)")
}
processedItemIds[item.id!!] = i // Track this item

// Create StockTakeLine (for each row/item)
// Read quantity from Column V (same as inventory)
val qty = try {
val cell = row.getCell(COLUMN_ON_HOLD_QTY_INDEX)
when {
cell == null -> BigDecimal.ZERO
cell.cellType == CellType.NUMERIC -> cell.numericCellValue.toBigDecimal()
cell.cellType == CellType.STRING -> {
val strValue = cell.stringCellValue.trim()
if (strValue.isNotBlank()) strValue.toBigDecimalOrNull() ?: BigDecimal.ZERO
else BigDecimal.ZERO
}
else -> BigDecimal.ZERO
}
} catch (e: Exception) {
logger.warn("Row ${i + 1}: Failed to read quantity from column V - ${e.message}")
BigDecimal.ZERO
}

// Get UOM for the item
val uom = itemUomService.findStockUnitByItemId(item.id!!)?.uom

// Create StockTakeLine
val saveStockTakeLineReq = SaveStockTakeLineRequest(
stockTakeId = savedStockTake.id,
initialQty = qty,
finalQty = qty,
uomId = uom?.id,
status = StockTakeLineStatus.PENDING.value,
remarks = null,
inventoryLotLineId = null // Will be set later in Step 8
)
val savedStockTakeLine = try {
stockTakeLineService.saveStockTakeLine(saveStockTakeLineReq)
} catch (e: Exception) {
logger.error("Row ${i + 1}: Failed to create StockTakeLine for item '$itemCode' - ${e.message}")
logWriter.writeRowError(i, "Failed to create StockTakeLine - ${e.message}")
errorCount++
errors.add("Row ${i + 1}: Failed to create StockTakeLine - ${e.message}")
continue
}
logger.info("Row ${i + 1}: Created StockTakeLine (ID: ${savedStockTakeLine.id}) for item '$itemCode'")

//Create StockInLine (for each row/item)
// Get warehouse from item's LocationCode
val locationCode = item.LocationCode
val warehouse = if (locationCode != null) {
try {
warehouseService.findByCode(locationCode)
} catch (e: Exception) {
logger.warn("Row ${i + 1}: Failed to find warehouse with LocationCode '$locationCode' - ${e.message}")
null
}
} else {
logger.warn("Row ${i + 1}: Item '$itemCode' has no LocationCode, warehouse will be null")
null
}

// Calculate expiry date (+30 days)
val expiryDate = LocalDateTime.now().plusDays(30).toLocalDate()

// Generate lotNo: LT-YYYYMMDD-itemCode
val dateStr = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"))
val lotNo = "LT-$dateStr-$itemCode"

// Generate dnNo: DN-YYYYMMDD-itemCode
val dnNo = "DN-$dateStr-$itemCode"

// Create StockInLine
val saveStockInLineReq = SaveStockInLineRequest(
stockInId = savedStockIn.id,
itemId = item.id!!,
acceptedQty = qty,
acceptQty = qty,
expiryDate = expiryDate,
warehouseId = warehouse?.id,
stockTakeLineId = savedStockTakeLine.id,
dnNo = dnNo, // Set dnNo
qcAccept = true,
status = StockInLineStatus.PENDING.status
)

val savedStockInLine = try {
stockInLineService.create(saveStockInLineReq)
} catch (e: Exception) {
logger.error("Row ${i + 1}: Failed to create StockInLine for item '$itemCode' - ${e.message}")
logWriter.writeRowError(i, "Failed to create StockInLine - ${e.message}")
errorCount++
errors.add("Row ${i + 1}: Failed to create StockInLine - ${e.message}")
continue
}
logger.info("Row ${i + 1}: Created StockInLine (ID: ${savedStockInLine.id}) for item '$itemCode'")

// Set lotNo on StockInLine (after creation, as it's not in SaveStockInLineRequest)
val stockInLineEntity = savedStockInLine.id?.let {
stockInLineRepository.findById(it).orElse(null)
}
if (stockInLineEntity != null) {
stockInLineEntity.lotNo = lotNo
stockInLineRepository.saveAndFlush(stockInLineEntity)
logger.info("Row ${i + 1}: Set lotNo '$lotNo' on StockInLine (ID: ${stockInLineEntity.id})")
} else {
logger.warn("Row ${i + 1}: Could not find StockInLine entity to set lotNo")
}

val inventoryLotLines = if (qty > BigDecimal.ZERO && warehouse?.id != null) {
mutableListOf(
SaveInventoryLotLineForSil(
qty = qty, // Quantity from Column V
warehouseId = warehouse.id
)
)
} else {
logger.warn("Row ${i + 1}: Cannot create inventoryLotLines - qty: $qty, warehouse: ${warehouse?.id}")
null
}

// Update StockInLine to RECEIVED status (this will trigger InventoryLot & InventoryLotLine creation)
saveStockInLineReq.apply {
id = savedStockInLine.id
status = StockInLineStatus.RECEIVED.status
this.dnNo = dnNo
this.inventoryLotLines = inventoryLotLines
}
val finalStockInLine = try {
stockInLineService.update(saveStockInLineReq)
} catch (e: Exception) {
logger.error("Row ${i + 1}: Failed to update StockInLine to RECEIVED for item '$itemCode' - ${e.message}")
logWriter.writeRowError(i, "Failed to update StockInLine to RECEIVED - ${e.message}")
errorCount++
errors.add("Row ${i + 1}: Failed to update StockInLine to RECEIVED - ${e.message}")
continue
}
logger.info("Row ${i + 1}: Updated StockInLine to RECEIVED (ID: ${finalStockInLine.id}) for item '$itemCode' with lotNo: $lotNo, dnNo: $dnNo")

val inventoryLotLine = if (finalStockInLine.id != null && warehouse?.id != null) {
try {
inventoryLotLineRepository.findByInventoryLotStockInLineIdAndWarehouseId(
inventoryLotStockInLineId = finalStockInLine.id!!,
warehouseId = warehouse.id!!
)
} catch (e: Exception) {
logger.warn("Row ${i + 1}: Failed to find InventoryLotLine for StockInLine ${finalStockInLine.id} and warehouse ${warehouse.id} - ${e.message}")
null
}
} else {
logger.warn("Row ${i + 1}: Cannot find InventoryLotLine - finalStockInLine.id: ${finalStockInLine.id}, warehouse.id: ${warehouse?.id}")
null
}

if (inventoryLotLine != null) {
val updateStockTakeLineReq = SaveStockTakeLineRequest(
id = savedStockTakeLine.id,
stockTakeId = savedStockTake.id,
initialQty = savedStockTakeLine.initialQty,
finalQty = savedStockTakeLine.finalQty,
uomId = savedStockTakeLine.uom?.id,
status = StockTakeLineStatus.COMPLETED.value,
completeDate = LocalDateTime.now(),
inventoryLotLineId = inventoryLotLine.id,
remarks = savedStockTakeLine.remarks
)
try {
stockTakeLineService.saveStockTakeLine(updateStockTakeLineReq)
logger.info("Row ${i + 1}: Updated StockTakeLine (ID: ${savedStockTakeLine.id}) to COMPLETED with inventoryLotLineId: ${inventoryLotLine.id}")
} catch (e: Exception) {
logger.error("Row ${i + 1}: Failed to update StockTakeLine for item '$itemCode' - ${e.message}")
logWriter.writeRowError(i, "Failed to update StockTakeLine - ${e.message}")
// Don't continue here - this is not critical, just log the error
}
} else {
logger.warn("Row ${i + 1}: InventoryLotLine not found, StockTakeLine will not be updated with inventoryLotLineId")
logWriter.writeRowError(i, "InventoryLotLine not found for StockTakeLine update")
}

// Update fields - Replace existing data with Excel data
try {

// Save the item
val savedItem = itemsRepository.saveAndFlush(item)

logger.info("Row ${i + 1}: After save - Saved item ID: ${savedItem.id}")

successCount++
logger.info("Row ${i + 1}: Successfully patched item '${item.name}' (code: $itemCode)")

// Write to log file
logWriter.writeItemPatched(
rowNum = i,
itemCode = itemCode,
itemId = savedItem.id,
itemName = item.name,
inventorySheet = item.inventorySheet,
type = item.type,
storeId = item.store_id,
storeLocation = item.storeLocation,
warehouse = item.warehouse,
area = item.area,
slot = item.slot,
locationCode = item.LocationCode,
mtmsPickRoutingId = item.MTMSPickRoutingID
)

// Delete old inventory record if exists, then create new one
try {
val itemId = savedItem.id
if (itemId == null) {
logger.error("Row ${i + 1}: Item ID is null after save, cannot create inventory for item '$itemCode'")
logWriter.writeInventoryIdNull(i)
missingInventoryItems.add("Row ${i + 1}: Item '$itemCode' - Item ID is null")
inventoryFailedCount++
} else {
// Check if inventory exists
val existingInventory = try {
inventoryRepository.findByItemId(itemId).orElse(null)
} catch (e: Exception) {
logger.warn("Row ${i + 1}: Failed to find existing inventory for item '$itemCode' (ID: $itemId) - ${e.message}")
null
}

// Delete existing inventory if it exists
if (existingInventory != null) {
try {
inventoryRepository.delete(existingInventory)
inventoryRepository.flush()
logger.info("Row ${i + 1}: Deleted existing inventory record for item '${item.name}' (code: $itemCode)")
logWriter.writeInventoryDeleted(i, existingInventory.id)
} catch (e: Exception) {
logger.warn("Row ${i + 1}: Failed to delete existing inventory for item '$itemCode' - ${e.message}")
logWriter.writeInventoryDeleteError(i, e.message ?: "Unknown error")
}
}

// Read quantity from Column V (will be used for onHandQty)
val stockTakeCount = try {
val cell = row.getCell(COLUMN_ON_HOLD_QTY_INDEX)
when {
cell == null -> BigDecimal.ZERO
cell.cellType == CellType.NUMERIC -> cell.numericCellValue.toBigDecimal()
cell.cellType == CellType.STRING -> {
val strValue = cell.stringCellValue.trim()
if (strValue.isNotBlank()) strValue.toBigDecimalOrNull() ?: BigDecimal.ZERO
else BigDecimal.ZERO
}
else -> BigDecimal.ZERO
}
} catch (e: Exception) {
logger.warn("Row ${i + 1}: Failed to read quantity from column V - ${e.message}")
BigDecimal.ZERO
}

// Create new inventory record
try {
val inventory = Inventory()
inventory.item = savedItem
inventory.onHandQty = stockTakeCount // Column V value goes to onHandQty
inventory.onHoldQty = BigDecimal.ZERO // onHoldQty is always 0
inventory.unavailableQty = BigDecimal.ZERO
inventory.price = BigDecimal.ZERO
inventory.cpu = BigDecimal.ZERO
inventory.cpm = BigDecimal.ZERO
inventory.status = "active"

inventoryRepository.saveAndFlush(inventory)
inventoryCreatedCount++
logger.info("Row ${i + 1}: Created new inventory record for item '${item.name}' (code: $itemCode) with onHandQty: $stockTakeCount")
logWriter.writeInventoryCreated(i, stockTakeCount)

} catch (e: Exception) {
inventoryFailedCount++
logger.error("Row ${i + 1}: Failed to create inventory record for item '$itemCode' (ID: $itemId) - ${e.message}", e)
logWriter.writeInventoryError(i, "Failed to create - ${e.message}")
missingInventoryItems.add("Row ${i + 1}: Item '$itemCode' - ${e.message}")
}
}
} catch (e: Exception) {
inventoryFailedCount++
logger.error("Row ${i + 1}: Unexpected error during inventory creation for item '$itemCode' - ${e.message}", e)
logWriter.writeInventoryError(i, "Unexpected error - ${e.message}")
missingInventoryItems.add("Row ${i + 1}: Item '$itemCode' - ${e.message}")
}

logWriter.writeEmptyLine()

} catch (e: Exception) {
logger.error("Import Error (Row ${i + 1} - Save Error): ${e.message}")
logWriter.writeRowError(i, "Failed to save item '$itemCode' - ${e.message}")
errorCount++
errors.add("Row ${i + 1}: Failed to save item '$itemCode' - ${e.message}")
}
}

// Update StockTake to COMPLETED
val endTime = LocalDateTime.now()
saveStockTakeReq.apply {
id = savedStockTake.id
actualEnd = endTime
status = StockTakeStatus.COMPLETED.value
}
stockTakeService.saveStockTake(saveStockTakeReq)
logger.info("Updated StockTake to COMPLETED: ${savedStockTake.code}")

// Write summary
logWriter.writeSummary(
successCount = successCount,
errorCount = errorCount,
inventoryCreatedCount = inventoryCreatedCount,
inventoryFailedCount = inventoryFailedCount,
missingInventoryItems = missingInventoryItems,
errors = errors
)

logger.info("--------- End - Patch Items from Excel -------")
logger.info("Success: $successCount, Errors: $errorCount")
logger.info("Inventory Created: $inventoryCreatedCount, Inventory Failed: $inventoryFailedCount")
logger.info("Log file saved to: ${logWriter.getLogFilePath()}")

if (errors.isNotEmpty()) {
logger.error("Errors encountered: ${errors.joinToString("\n")}")
}

return "Patch Excel completed. Success: $successCount, Errors: $errorCount. Inventory Created: $inventoryCreatedCount, Failed: $inventoryFailedCount. Log file: ${logWriter.getLogFilePath()}"
} finally {
logWriter.close()
}
}


/**
* Patch items from Excel file located in configured folder
* @param fileName The name of the Excel file (e.g., "items_patch.xlsx")
* @return Result message with success/error count
*/
@Throws(IOException::class)
@Transactional
open fun patchItemsFromExcelFileName(fileName: String): String {
val filePath = "$excelImportPath$fileName"
logger.info("Reading Excel file from: $filePath")

val workbook: Workbook? = try {
val file = File(filePath)

if (!file.exists()) {
logger.error("File not found: $filePath")
return "File not found: $filePath"
}

if (!file.isFile) {
logger.error("Path is not a file: $filePath")
return "Path is not a file: $filePath"
}

if (!file.canRead()) {
logger.error("File is not readable: $filePath")
return "File is not readable: $filePath"
}

logger.info("Successfully located file: $filePath")
XSSFWorkbook(FileInputStream(file))
} catch (e: java.io.FileNotFoundException) {
logger.error("File not found: $filePath - ${e.message}")
return "File not found: $filePath"
} catch (e: Exception) {
logger.error("Failed to read Excel file: ${e.message}", e)
return "Failed to read Excel file: ${e.message}"
}

return try {
if (workbook == null) {
"Failed to read Excel workbook"
} else {
patchItemsFromExcel(workbook)
}
} catch (e: Exception) {
logger.error("Error processing Excel file: ${e.message}", e)
"Error processing Excel file: ${e.message}"
} finally {
// Close workbook to free resources
try {
workbook?.close()
} catch (e: Exception) {
logger.warn("Error closing workbook: ${e.message}")
}
}
}
}

+ 21
- 0
src/main/java/com/ffii/fpsms/modules/master/web/ItemsController.kt View File

@@ -13,12 +13,17 @@ import jakarta.servlet.http.HttpServletRequest
import jakarta.validation.Valid
import org.springframework.web.bind.annotation.*
import java.util.Collections.emptyList
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.http.ResponseEntity

@RestController
@RequestMapping("/items")
class ItemsController(
private val itemsService: ItemsService
) {
private val logger: Logger = LoggerFactory.getLogger(ItemsController::class.java)

@GetMapping
fun allItems(): List<Items> {
return itemsService.allItems()
@@ -122,4 +127,20 @@ fun getItemsWithDetailsByPage(request: HttpServletRequest): RecordsRes<Map<Strin

return RecordsRes(paginatedList, fullList.size)
}

@PostMapping("/patch/file")
fun patchItemsFromExcelFile(@RequestParam("fileName") fileName: String): ResponseEntity<*> {
return try {
if (fileName.isBlank()) {
return ResponseEntity.badRequest().body("File name cannot be empty")
}

logger.info("Request to patch items from file: $fileName")
val result = itemsService.patchItemsFromExcelFileName(fileName)
ResponseEntity.ok(result)
} catch (e: Exception) {
logger.error("Error patching from file: ${e.message}", e)
ResponseEntity.badRequest().body("Error: ${e.message}")
}
}
}

+ 5
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/StockInRepository.kt View File

@@ -16,4 +16,9 @@ interface StockInRepository : AbstractRepository<StockIn, Long> {
fun findByStockTakeIdAndDeletedFalse(stockTakeId: Long): StockIn?

fun findByIdAndDeletedIsFalse(id: Serializable): StockIn?

@Query("""
select si.code from StockIn si where si.code like :prefix% and si.deleted = false order by si.code desc limit 1
""")
fun findLatestCodeByPrefix(prefix: String): String?
}

+ 1
- 1
src/main/java/com/ffii/fpsms/modules/stock/service/StockInService.kt View File

@@ -31,7 +31,7 @@ open class StockInService(
stockIn.apply {
status = StockInStatus.PENDING.status
orderDate = LocalDateTime.now()
code = LocalDateTime.now().toString().take(19)
code = request.code ?: LocalDateTime.now().toString().take(19)
}
if (request.purchaseOrderId != null) {
val purchaseOrder : PurchaseOrder = purchaseOrderRepository.findByIdAndDeletedFalse(request.purchaseOrderId).orElseThrow();


+ 2
- 1
src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt View File

@@ -32,6 +32,7 @@ import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRecord
import com.ffii.fpsms.modules.deliveryOrder.enums.DeliveryOrderStatus
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRecordRepository
import com.ffii.fpsms.modules.common.CodeGenerator
import org.springframework.context.annotation.Lazy

@Service
open class StockOutLineService(
@@ -44,7 +45,7 @@ open class StockOutLineService(
private val itemUomRespository: ItemUomRespository,
private val pickOrderRepository: PickOrderRepository,
private val inventoryLotLineRepository: InventoryLotLineRepository,
private val suggestedPickLotService: SuggestedPickLotService,
@Lazy private val suggestedPickLotService: SuggestedPickLotService,
private val doPickOrderRepository: DoPickOrderRepository,
private val doPickOrderRecordRepository: DoPickOrderRecordRepository,
private val deliveryOrderRepository: DeliveryOrderRepository,


+ 3
- 2
src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeService.kt View File

@@ -22,6 +22,7 @@ import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.math.BigDecimal
import java.time.LocalDateTime
import org.springframework.context.annotation.Lazy

@Service
class StockTakeService(
@@ -30,8 +31,8 @@ class StockTakeService(
val warehouseService: WarehouseService,
val stockInService: StockInService,
val stockInLineService: StockInLineService,
val itemsService: ItemsService,
val itemUomService: ItemUomService,
@Lazy val itemsService: ItemsService,
@Lazy val itemUomService: ItemUomService,
val stockTakeLineService: StockTakeLineService,
val inventoryLotLineRepository: InventoryLotLineRepository,
) {


+ 5
- 0
src/main/resources/db/changelog/changes/20251127_01_KelvinY/01_add_inventorySheet_to_items.sql View File

@@ -0,0 +1,5 @@
-- liquibase formatted sql
-- changeset KelvinY:add_inventorySheet_to_items

ALTER TABLE `fpsmsdb`.`items`
ADD COLUMN `inventorySheet` VARCHAR(255) NULL;

Loading…
Cancel
Save