Selaa lähdekoodia

stock trf + stock adj

master
kelvin.yau 2 päivää sitten
vanhempi
commit
6130f4e7e1
10 muutettua tiedostoa jossa 360 lisäystä ja 4 poistoa
  1. +43
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockAdjustmentRecord.kt
  2. +7
    -0
      src/main/java/com/ffii/fpsms/modules/stock/entity/StockAdjustmentRecordRepository.kt
  3. +58
    -1
      src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt
  4. +164
    -0
      src/main/java/com/ffii/fpsms/modules/stock/service/StockAdjustmentService.kt
  5. +8
    -1
      src/main/java/com/ffii/fpsms/modules/stock/web/InventoryLotLineController.kt
  6. +18
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/StockAdjustmentController.kt
  7. +2
    -2
      src/main/java/com/ffii/fpsms/modules/stock/web/StockInLineController.kt
  8. +7
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/model/PrintLabelForInventoryLotLineRequest.kt
  9. +24
    -0
      src/main/java/com/ffii/fpsms/modules/stock/web/model/StockAdjustmentRequest.kt
  10. +29
    -0
      src/main/resources/db/changelog/changes/20260212_01_KelvinY/01_create_stock_adjustment_record.sql

+ 43
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/StockAdjustmentRecord.kt Näytä tiedosto

@@ -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<Long>() {

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

+ 7
- 0
src/main/java/com/ffii/fpsms/modules/stock/entity/StockAdjustmentRecordRepository.kt Näytä tiedosto

@@ -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<StockAdjustmentRecord, Long>

+ 58
- 1
src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt Näytä tiedosto

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


+ 164
- 0
src/main/java/com/ffii/fpsms/modules/stock/service/StockAdjustmentService.kt Näytä tiedosto

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

+ 8
- 1
src/main/java/com/ffii/fpsms/modules/stock/web/InventoryLotLineController.kt Näytä tiedosto

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


+ 18
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/StockAdjustmentController.kt Näytä tiedosto

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

+ 2
- 2
src/main/java/com/ffii/fpsms/modules/stock/web/StockInLineController.kt Näytä tiedosto

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

+ 7
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/model/PrintLabelForInventoryLotLineRequest.kt Näytä tiedosto

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

+ 24
- 0
src/main/java/com/ffii/fpsms/modules/stock/web/model/StockAdjustmentRequest.kt Näytä tiedosto

@@ -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<StockAdjustmentLineRequest>,
val currentLines: List<StockAdjustmentLineRequest>
)

+ 29
- 0
src/main/resources/db/changelog/changes/20260212_01_KelvinY/01_create_stock_adjustment_record.sql Näytä tiedosto

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

Ladataan…
Peruuta
Tallenna