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