From 6130f4e7e19c2be712a15c547e9aa8ed4ad0abb7 Mon Sep 17 00:00:00 2001 From: "kelvin.yau" Date: Thu, 26 Feb 2026 05:44:47 +0800 Subject: [PATCH] stock trf + stock adj --- .../stock/entity/StockAdjustmentRecord.kt | 43 +++++ .../entity/StockAdjustmentRecordRepository.kt | 7 + .../stock/service/InventoryLotLineService.kt | 59 ++++++- .../stock/service/StockAdjustmentService.kt | 164 ++++++++++++++++++ .../stock/web/InventoryLotLineController.kt | 9 +- .../stock/web/StockAdjustmentController.kt | 18 ++ .../stock/web/StockInLineController.kt | 4 +- .../PrintLabelForInventoryLotLineRequest.kt | 7 + .../stock/web/model/StockAdjustmentRequest.kt | 24 +++ .../01_create_stock_adjustment_record.sql | 29 ++++ 10 files changed, 360 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/ffii/fpsms/modules/stock/entity/StockAdjustmentRecord.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/stock/entity/StockAdjustmentRecordRepository.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/stock/service/StockAdjustmentService.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/stock/web/StockAdjustmentController.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/stock/web/model/PrintLabelForInventoryLotLineRequest.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/stock/web/model/StockAdjustmentRequest.kt create mode 100644 src/main/resources/db/changelog/changes/20260212_01_KelvinY/01_create_stock_adjustment_record.sql diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockAdjustmentRecord.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockAdjustmentRecord.kt new file mode 100644 index 0000000..dc711d4 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockAdjustmentRecord.kt @@ -0,0 +1,43 @@ +package com.ffii.fpsms.modules.stock.entity + +import com.fasterxml.jackson.annotation.JsonBackReference +import com.ffii.core.entity.BaseEntity +import com.ffii.fpsms.modules.master.entity.Items +import jakarta.persistence.* +import jakarta.validation.constraints.NotNull +import java.math.BigDecimal + +@Entity +@Table(name = "stock_adjustment_record") +open class StockAdjustmentRecord : BaseEntity() { + + @NotNull + @ManyToOne + @JoinColumn(name = "itemId", nullable = false) + open var item: Items? = null + + @Column(name = "itemCode", length = 50) + open var itemCode: String? = null + + @Column(name = "itemName", length = 200) + open var itemName: String? = null + + @Column(name = "lotNo", length = 512) + open var lotNo: String? = null + + @Column(name = "inQty", precision = 14, scale = 2) + open var inQty: BigDecimal? = null + + @Column(name = "outQty", precision = 14, scale = 2) + open var outQty: BigDecimal? = null + + @JsonBackReference + @ManyToOne + @JoinColumn(name = "stockInLineId") + open var stockInLine: StockInLine? = null + + @JsonBackReference + @ManyToOne + @JoinColumn(name = "stockOutLineId") + open var stockOutLine: StockOutLine? = null +} \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/StockAdjustmentRecordRepository.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockAdjustmentRecordRepository.kt new file mode 100644 index 0000000..4a087bd --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/StockAdjustmentRecordRepository.kt @@ -0,0 +1,7 @@ +package com.ffii.fpsms.modules.stock.entity + +import com.ffii.core.support.AbstractRepository +import org.springframework.stereotype.Repository + +@Repository +interface StockAdjustmentRecordRepository : AbstractRepository \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt index 2e7d8a1..b6e1a26 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt @@ -40,6 +40,13 @@ import com.ffii.fpsms.modules.stock.web.model.ScannedLotInfo import com.ffii.fpsms.modules.stock.web.model.SameItemLotInfo import com.ffii.fpsms.modules.jobOrder.service.JobOrderService import com.ffii.fpsms.modules.jobOrder.web.model.ExportFGStockInLabelRequest +import com.ffii.fpsms.modules.master.service.PrinterService +import com.ffii.fpsms.modules.stock.web.model.PrintLabelForInventoryLotLineRequest +import com.ffii.fpsms.modules.jobOrder.web.model.PrintFGStockInLabelRequest +import com.ffii.core.utils.ZebraPrinterUtil +import net.sf.jasperreports.engine.JasperExportManager +import java.io.File +import net.sf.jasperreports.engine.JasperPrint @Service open class InventoryLotLineService( @@ -49,6 +56,7 @@ open class InventoryLotLineService( private val itemUomRespository: ItemUomRespository, private val stockInLineRepository: StockInLineRepository, private val inventoryRepository: InventoryRepository, + private val printerService: PrinterService, @Lazy private val jobOrderService: JobOrderService ) { @@ -217,7 +225,6 @@ open class InventoryLotLineService( // .minus(inventoryLotLine.outQty ?: zero) // .minus(inventoryLotLine.holdQty ?: zero) - // Accepted qty at stock-in, no conversion (stays e.g. 50000 even after stock out) field["acceptedQty"] = "%.2f".format(info.acceptedQty) val stockItemUom = itemUomRespository.findBaseUnitByItemIdAndStockUnitIsTrueAndDeletedIsFalse(info.itemId) @@ -267,6 +274,56 @@ open class InventoryLotLineService( } } + @Transactional + open fun printLabelForInventoryLotLine(request: PrintLabelForInventoryLotLineRequest) { + val printer = printerService.findById(request.printerId) + ?: throw NoSuchElementException("No such printer") + val inventoryLotLine = inventoryLotLineRepository.findById(request.inventoryLotLineId).orElseThrow() + val stockInLine = inventoryLotLine.inventoryLot?.stockInLine + + when { + stockInLine?.jobOrder != null -> { + jobOrderService.printFGStockInLabel( + PrintFGStockInLabelRequest( + stockInLineId = stockInLine.id!!, + printerId = request.printerId, + printQty = request.printQty + ) + ) + } + else -> { + val pdf = if (stockInLine?.stockTransferRecord != null) { + val targetLocation = stockInLine.stockTransferRecord?.targetLocation ?: "" + exportStockInLineQrcode( + LotLineToQrcode(inventoryLotLineId = request.inventoryLotLineId, isTransfer = "轉倉至 $targetLocation") + ) + } else { + exportStockInLineQrcode(LotLineToQrcode(inventoryLotLineId = request.inventoryLotLineId)) + } + val jasperPrint = pdf["report"] as JasperPrint + val tempPdfFile = File.createTempFile("print_job_", ".pdf") + try { + JasperExportManager.exportReportToPdfFile(jasperPrint, tempPdfFile.absolutePath) + val printQty = if (request.printQty == null || request.printQty <= 0) 1 else request.printQty + printer.ip?.let { ip -> + printer.port?.let { port -> + ZebraPrinterUtil.printPdfToZebra( + tempPdfFile, + ip, + port, + printQty, + ZebraPrinterUtil.PrintDirection.ROTATED, + printer.dpi + ) + } + } + } finally { + tempPdfFile.delete() + } + } + } + } + @Transactional open fun updateInventoryLotLineQuantities(request: UpdateInventoryLotLineQuantitiesRequest): MessageResponse { try { diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockAdjustmentService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockAdjustmentService.kt new file mode 100644 index 0000000..37f5afd --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockAdjustmentService.kt @@ -0,0 +1,164 @@ +package com.ffii.fpsms.modules.stock.service + +import com.ffii.fpsms.modules.master.web.models.MessageResponse +import com.ffii.fpsms.modules.stock.entity.InventoryLotLine +import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository +import com.ffii.fpsms.modules.stock.web.model.StockAdjustmentRequest +import com.ffii.fpsms.modules.stock.web.model.StockInRequest +import com.ffii.fpsms.modules.stock.web.model.StockOutRequest +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.math.BigDecimal +import java.time.LocalDate + +import com.ffii.fpsms.modules.stock.entity.StockAdjustmentRecord +import com.ffii.fpsms.modules.stock.entity.StockAdjustmentRecordRepository +import com.ffii.fpsms.modules.stock.entity.StockInLine +import com.ffii.fpsms.modules.stock.entity.StockOutLine + +@Service +open class StockAdjustmentService( + private val stockInLineService: StockInLineService, + private val stockOutLineService: StockOutLineService, + private val inventoryLotLineRepository: InventoryLotLineRepository, + private val stockAdjustmentRecordRepository: StockAdjustmentRecordRepository +) { + + @Transactional + open fun submit(request: StockAdjustmentRequest): MessageResponse { + val originalById = request.originalLines.filter { it.id > 0 }.associateBy { it.id } + val currentIds = request.currentLines.map { it.id }.toSet() + + // Branch 4: Removed entries — stock out full qty and mark unavailable + val removed = request.originalLines.filter { it.id > 0 && it.id !in currentIds } + for (line in removed) { + val stockOutLine = stockOutLineService.createStockOut( + StockOutRequest( + inventoryLotLineId = line.id, + qty = line.adjustedQty.toDouble(), + type = "ADJ" + ) + ) + saveAdjustmentRecordForStockOut(stockOutLine) + } + for (current in request.currentLines) { + // Branch 2: New entry — createStockIn (OPEN or ADJ) + if (current.isNew) { + val stockType = if (current.isOpeningInventory) "OPEN" else "ADJ" + val stockInLine = stockInLineService.createStockIn( + StockInRequest( + itemId = current.itemId, + itemNo = current.itemNo, + demandQty = null, + acceptedQty = current.adjustedQty, + expiryDate = LocalDate.parse(current.expiryDate), + lotNo = current.lotNo?.takeIf { it.isNotBlank() }, + productLotNo = current.productlotNo?.takeIf { it.isNotBlank() }, + dnNo = current.dnNo?.takeIf { it.isNotBlank() }, + type = stockType, + warehouseId = current.warehouseId + ) + ) + saveAdjustmentRecordForStockIn(stockInLine) + continue + } + + // Branch 1 & 3: Existing line — compare qty + val original = originalById[current.id] ?: continue + val diff = current.adjustedQty.subtract(original.adjustedQty) + if (diff.compareTo(BigDecimal.ZERO) == 0) continue // Branch 1: no change + + if (diff.compareTo(BigDecimal.ZERO) > 0) { + // Branch 2 (qty up): createStockIn + val inventoryLotLine = inventoryLotLineRepository.findById(current.id) + .orElseThrow { IllegalArgumentException("InventoryLotLine not found: ${current.id}") } + val stockInRequest = buildStockInRequestFromExistingLotLine(inventoryLotLine, diff) + val stockInLine = stockInLineService.createStockIn(stockInRequest) + saveAdjustmentRecordForStockIn(stockInLine) + } else { + // Branch 3 (qty down): createStockOut + val stockOutLine = stockOutLineService.createStockOut( + StockOutRequest( + inventoryLotLineId = current.id, + qty = diff.abs().toDouble(), + type = "ADJ" + ) + ) + saveAdjustmentRecordForStockOut(stockOutLine) + } + } + + return MessageResponse( + id = null, + name = "Stock Adjustment", + code = "STOCK_ADJUSTMENT_SUBMITTED", + type = "success", + message = "Stock adjustment completed successfully", + errorPosition = null + ) + } + + private fun buildStockInRequestFromExistingLotLine( + inventoryLotLine: InventoryLotLine, + acceptedQty: BigDecimal + ): StockInRequest { + val inventoryLot = inventoryLotLine.inventoryLot + ?: throw IllegalArgumentException("InventoryLotLine must have an associated InventoryLot") + val item = inventoryLot.item + ?: throw IllegalArgumentException("InventoryLot must have an associated item") + val itemId = item.id + ?: throw IllegalArgumentException("Item must have an id") + val itemCode = item.code + ?: throw IllegalArgumentException("Item must have a code") + val expiryDate = inventoryLot.expiryDate + ?: throw IllegalArgumentException("InventoryLot must have an expiryDate") + val lotNo = inventoryLot.lotNo + val productLotNo = inventoryLot.stockInLine?.productLotNo + val dnNo = inventoryLot.stockInLine?.dnNo + val warehouseId = inventoryLotLine.warehouse?.id + ?: throw IllegalArgumentException("InventoryLotLine must have a warehouse") + + return StockInRequest( + itemId = itemId, + itemNo = itemCode, + demandQty = null, + acceptedQty = acceptedQty, + expiryDate = expiryDate, + lotNo = lotNo, + productLotNo = productLotNo, + dnNo = dnNo, + type = "ADJ", + warehouseId = warehouseId + ) + } + private fun saveAdjustmentRecordForStockIn(stockInLine: StockInLine) { + val item = stockInLine.item ?: return + val record = StockAdjustmentRecord().apply { + this.item = item + this.itemCode = stockInLine.itemNo ?: item.code + this.itemName = item.name + this.lotNo = stockInLine.lotNo + this.inQty = stockInLine.acceptedQty + this.outQty = null + this.stockInLine = stockInLine + this.stockOutLine = null + } + stockAdjustmentRecordRepository.save(record) + } + + private fun saveAdjustmentRecordForStockOut(stockOutLine: StockOutLine) { + val item = stockOutLine.item ?: return + val lotNo = stockOutLine.inventoryLotLine?.inventoryLot?.lotNo + val record = StockAdjustmentRecord().apply { + this.item = item + this.itemCode = item.code + this.itemName = item.name + this.lotNo = lotNo + this.inQty = null + this.outQty = BigDecimal.valueOf(stockOutLine.qty ?: 0.0) + this.stockInLine = null + this.stockOutLine = stockOutLine + } + stockAdjustmentRecordRepository.save(record) + } +} \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/InventoryLotLineController.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/InventoryLotLineController.kt index c86d521..7cf521f 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/InventoryLotLineController.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/InventoryLotLineController.kt @@ -23,6 +23,7 @@ import java.io.UnsupportedEncodingException import java.math.BigDecimal import java.text.ParseException import com.ffii.fpsms.modules.master.web.models.MessageResponse +import com.ffii.fpsms.modules.stock.web.model.PrintLabelForInventoryLotLineRequest import com.ffii.fpsms.modules.stock.web.model.UpdateInventoryLotLineStatusRequest import com.ffii.fpsms.modules.stock.web.model.QrCodeAnalysisRequest import com.ffii.fpsms.modules.stock.web.model.QrCodeAnalysisResponse @@ -73,7 +74,7 @@ class InventoryLotLineController ( ) } - @PostMapping("/print-label") + @PostMapping("/download-label") @Throws(UnsupportedEncodingException::class, NoSuchMessageException::class, ParseException::class, Exception::class) fun printLabel(@Valid @RequestBody request: LotLineToQrcode, response: HttpServletResponse) { response.characterEncoding = "utf-8"; @@ -84,6 +85,12 @@ class InventoryLotLineController ( response.addHeader("filename", "${pdf["fileName"]}.pdf") out.write(JasperExportManager.exportReportToPdf(jasperPrint)); } + + @GetMapping("/print-label") + fun printLabel(@ModelAttribute request: PrintLabelForInventoryLotLineRequest) { + inventoryLotLineService.printLabelForInventoryLotLine(request) + } + @PostMapping("/updateStatus") fun updateInventoryLotLineStatus(@RequestBody request: UpdateInventoryLotLineStatusRequest): MessageResponse { println("=== DEBUG: updateInventoryLotLineStatus Controller ===") diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/StockAdjustmentController.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/StockAdjustmentController.kt new file mode 100644 index 0000000..c76f6ec --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/StockAdjustmentController.kt @@ -0,0 +1,18 @@ +package com.ffii.fpsms.modules.stock.web + +import com.ffii.fpsms.modules.master.web.models.MessageResponse +import com.ffii.fpsms.modules.stock.service.StockAdjustmentService +import com.ffii.fpsms.modules.stock.web.model.StockAdjustmentRequest +import jakarta.validation.Valid +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/stockAdjustment") +class StockAdjustmentController( + private val stockAdjustmentService: StockAdjustmentService +) { + @PostMapping("/submit") + fun submit(@Valid @RequestBody request: StockAdjustmentRequest): MessageResponse { + return stockAdjustmentService.submit(request) + } +} \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/StockInLineController.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/StockInLineController.kt index e793b2b..f9fbdca 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/web/StockInLineController.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/StockInLineController.kt @@ -43,7 +43,7 @@ class StockInLineController( return stockInLineService.update(newItem) } - @PostMapping("/print-label") + @PostMapping("/download-label") @Throws(UnsupportedEncodingException::class, NoSuchMessageException::class, ParseException::class, Exception::class) fun printLabel(@Valid @RequestBody request: ExportQrCodeRequest, response: HttpServletResponse) { response.characterEncoding = "utf-8"; @@ -55,7 +55,7 @@ class StockInLineController( out.write(JasperExportManager.exportReportToPdf(jasperPrint)); } - @GetMapping("/printQrCode") + @GetMapping("/print-label") fun printQrCode(@ModelAttribute request: PrintQrCodeForSilRequest) { stockInLineService.printQrCode(request) } diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/PrintLabelForInventoryLotLineRequest.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/PrintLabelForInventoryLotLineRequest.kt new file mode 100644 index 0000000..f255b86 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/PrintLabelForInventoryLotLineRequest.kt @@ -0,0 +1,7 @@ +package com.ffii.fpsms.modules.stock.web.model + +data class PrintLabelForInventoryLotLineRequest( + val inventoryLotLineId: Long, + val printerId: Long, + val printQty: Int? = null, +) \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockAdjustmentRequest.kt b/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockAdjustmentRequest.kt new file mode 100644 index 0000000..fc60c4b --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/stock/web/model/StockAdjustmentRequest.kt @@ -0,0 +1,24 @@ +package com.ffii.fpsms.modules.stock.web.model + +import java.math.BigDecimal + +data class StockAdjustmentLineRequest( + val id: Long, + val lotNo: String? = null, + val adjustedQty: BigDecimal, + val productlotNo: String? = null, + val dnNo: String? = null, + val isOpeningInventory: Boolean = false, + val isNew: Boolean = false, + val itemId: Long, + val itemNo: String, + val expiryDate: String, + val warehouseId: Long, + val uom: String? = null +) + +data class StockAdjustmentRequest( + val itemId: Long, + val originalLines: List, + val currentLines: List +) \ No newline at end of file diff --git a/src/main/resources/db/changelog/changes/20260212_01_KelvinY/01_create_stock_adjustment_record.sql b/src/main/resources/db/changelog/changes/20260212_01_KelvinY/01_create_stock_adjustment_record.sql new file mode 100644 index 0000000..8d0bf35 --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260212_01_KelvinY/01_create_stock_adjustment_record.sql @@ -0,0 +1,29 @@ +-- liquibase formatted sql +-- changeset KelvinY:create_stock_adjustment_record_table + +CREATE TABLE IF NOT EXISTS `stock_adjustment_record` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `createdBy` VARCHAR(255) DEFAULT NULL, + `version` INT NOT NULL DEFAULT 0, + `modified` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `modifiedBy` VARCHAR(255) DEFAULT NULL, + `deleted` TINYINT(1) NOT NULL DEFAULT 0, + + `itemId` INT NOT NULL, + `itemCode` VARCHAR(50) DEFAULT NULL, + `itemName` VARCHAR(200) DEFAULT NULL, + `lotNo` VARCHAR(512) DEFAULT NULL, + `inQty` DECIMAL(14,2) DEFAULT NULL, + `outQty` DECIMAL(14,2) DEFAULT NULL, + `stockInLineId` INT DEFAULT NULL, + `stockOutLineId` INT DEFAULT NULL, + + PRIMARY KEY (`id`), + INDEX `idx_stock_adjustment_record_itemId` (`itemId`), + INDEX `idx_stock_adjustment_record_stockInLineId` (`stockInLineId`), + INDEX `idx_stock_adjustment_record_stockOutLineId` (`stockOutLineId`), + CONSTRAINT `fk_stock_adjustment_record_item` FOREIGN KEY (`itemId`) REFERENCES `items` (`id`), + CONSTRAINT `fk_stock_adjustment_record_stock_in_line` FOREIGN KEY (`stockInLineId`) REFERENCES `stock_in_line` (`id`), + CONSTRAINT `fk_stock_adjustment_record_stock_out_line` FOREIGN KEY (`stockOutLineId`) REFERENCES `stock_out_line` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; \ No newline at end of file