浏览代码

Import Stock Take Excel Function

master
kelvin.yau 2 周前
父节点
当前提交
f95aff4618
共有 8 个文件被更改,包括 784 次插入4 次删除
  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 查看文件

@@ -34,6 +34,30 @@ open class Items : BaseEntity<Long>() {
@Column(name = "countryOfOrigin") @Column(name = "countryOfOrigin")
open var countryOfOrigin: String? = null 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") @Column(name = "maxQty")
open var maxQty: Double? = null open var maxQty: Double? = null




+ 723
- 0
src/main/java/com/ffii/fpsms/modules/master/service/ItemsService.kt 查看文件

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


import com.ffii.core.support.AbstractBaseEntityService import com.ffii.core.support.AbstractBaseEntityService
import com.ffii.core.support.JdbcDao 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.entity.*
import com.ffii.fpsms.modules.master.web.models.ItemQc import com.ffii.fpsms.modules.master.web.models.ItemQc
import com.ffii.fpsms.modules.master.web.models.ItemWithQcResponse 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.format.DateTimeFormatter
import java.time.temporal.TemporalAdjusters import java.time.temporal.TemporalAdjusters
import kotlin.jvm.optionals.getOrNull 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 @Service
open class ItemsService( open class ItemsService(
@@ -21,7 +51,167 @@ open class ItemsService(
private val itemsRepository: ItemsRepository, private val itemsRepository: ItemsRepository,
private val qcCheckRepository: QcCheckRepository, private val qcCheckRepository: QcCheckRepository,
private val qcItemsRepository: QcItemRepository, private val qcCategoryRepository: QcCategoryRepository, 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) { ): 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 // do mapping with projection
open fun allItems(): List<Items> { open fun allItems(): List<Items> {
// TODO: Replace by actual logic // TODO: Replace by actual logic
@@ -386,4 +576,537 @@ open class ItemsService(
) )
return jdbcDao.queryForInts(sql.toString(), args); 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 查看文件

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


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

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


return RecordsRes(paginatedList, fullList.size) 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 查看文件

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


fun findByIdAndDeletedIsFalse(id: Serializable): 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 查看文件

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


+ 2
- 1
src/main/java/com/ffii/fpsms/modules/stock/service/StockOutLineService.kt 查看文件

@@ -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.enums.DeliveryOrderStatus
import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRecordRepository import com.ffii.fpsms.modules.deliveryOrder.entity.DoPickOrderLineRecordRepository
import com.ffii.fpsms.modules.common.CodeGenerator import com.ffii.fpsms.modules.common.CodeGenerator
import org.springframework.context.annotation.Lazy


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


+ 3
- 2
src/main/java/com/ffii/fpsms/modules/stock/service/StockTakeService.kt 查看文件

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


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


+ 5
- 0
src/main/resources/db/changelog/changes/20251127_01_KelvinY/01_add_inventorySheet_to_items.sql 查看文件

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

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

正在加载...
取消
保存