| @@ -77,8 +77,8 @@ public class SecurityConfig { | |||||
| /** | /** | ||||
| * FP-MTMS Version Checklist | Functions Ref. No. 67 | v1.0.0 | 2026-08-13 | * FP-MTMS Version Checklist | Functions Ref. No. 67 | v1.0.0 | 2026-08-13 | ||||
| * FP-MTMS Version Checklist | Functions Ref. No. 51 | v1.0.1 | 2026-08-06 | * FP-MTMS Version Checklist | Functions Ref. No. 51 | v1.0.1 | 2026-08-06 | ||||
| * FP-MTMS Version Checklist | Functions Ref. No. 3 | v1.0.6 | 2026-09-07 | |||||
| * (stockAdjustment/submit → INVENTORY_ADJUST only) | |||||
| * FP-MTMS Version Checklist | Functions Ref. No. 3 | v1.0.7 | 2026-09-08 | |||||
| * (stockAdjustment/submit and GET /latestRemarks → INVENTORY_ADJUST) | |||||
| */ | */ | ||||
| @Bean | @Bean | ||||
| @Order(1) | @Order(1) | ||||
| @@ -119,6 +119,8 @@ public class SecurityConfig { | |||||
| .hasAnyAuthority("TESTING", "ADMIN") | .hasAnyAuthority("TESTING", "ADMIN") | ||||
| .requestMatchers(HttpMethod.POST, "/stockAdjustment/submit") | .requestMatchers(HttpMethod.POST, "/stockAdjustment/submit") | ||||
| .hasAuthority("INVENTORY_ADJUST") | .hasAuthority("INVENTORY_ADJUST") | ||||
| .requestMatchers(HttpMethod.GET, "/stockAdjustment/latestRemarks") | |||||
| .hasAuthority("INVENTORY_ADJUST") | |||||
| .requestMatchers(HttpMethod.GET, "/inventoryLotLine/trace") | .requestMatchers(HttpMethod.GET, "/inventoryLotLine/trace") | ||||
| .hasAuthority("ITEM_TRACING") | .hasAuthority("ITEM_TRACING") | ||||
| .requestMatchers(HttpMethod.GET, "/inventoryLotLine/trace/location/**") | .requestMatchers(HttpMethod.GET, "/inventoryLotLine/trace/location/**") | ||||
| @@ -216,7 +216,6 @@ class StockLotOnhandReportController( | |||||
| "樓層", "倉庫", "區域", "儲位", | "樓層", "倉庫", "區域", "儲位", | ||||
| "最後異動日", "最後異動(入庫/出庫)", | "最後異動日", "最後異動(入庫/出庫)", | ||||
| "批號結餘", "單位均價", "庫存總價值", | "批號結餘", "單位均價", "庫存總價值", | ||||
| "備註", | |||||
| ) | ) | ||||
| val totalColumns = headers.size | val totalColumns = headers.size | ||||
| var rowIndex = 0 | var rowIndex = 0 | ||||
| @@ -301,7 +300,6 @@ class StockLotOnhandReportController( | |||||
| setNumberCell(r, 12, m["lotQtyRaw"], styles.number) | setNumberCell(r, 12, m["lotQtyRaw"], styles.number) | ||||
| setNumberCell(r, 13, m["avgUnitPriceRaw"], styles.number) | setNumberCell(r, 13, m["avgUnitPriceRaw"], styles.number) | ||||
| setNumberCell(r, 14, m["stockValueRaw"], styles.number) | setNumberCell(r, 14, m["stockValueRaw"], styles.number) | ||||
| setTextCell(r, 15, m["remarks"], styles.text) | |||||
| } | } | ||||
| } | } | ||||
| @@ -310,7 +308,7 @@ class StockLotOnhandReportController( | |||||
| sheet.setAutoFilter(CellRangeAddress(headerRowIndex, lastRowIndex, 0, totalColumns - 1)) | sheet.setAutoFilter(CellRangeAddress(headerRowIndex, lastRowIndex, 0, totalColumns - 1)) | ||||
| } | } | ||||
| sheet.createFreezePane(0, headerRowIndex + 1) | sheet.createFreezePane(0, headerRowIndex + 1) | ||||
| intArrayOf(14, 28, 14, 8, 20, 12, 10, 12, 10, 10, 12, 20, 12, 12, 14, 28) | |||||
| intArrayOf(14, 28, 14, 8, 20, 12, 10, 12, 10, 10, 12, 20, 12, 12, 14) | |||||
| .forEachIndexed { i, w -> sheet.setColumnWidth(i, w * 256) } | .forEachIndexed { i, w -> sheet.setColumnWidth(i, w * 256) } | ||||
| val out = ByteArrayOutputStream() | val out = ByteArrayOutputStream() | ||||
| @@ -31,6 +31,9 @@ open class StockAdjustmentRecord : BaseEntity<Long>() { | |||||
| @Column(name = "outQty", precision = 14, scale = 2) | @Column(name = "outQty", precision = 14, scale = 2) | ||||
| open var outQty: BigDecimal? = null | open var outQty: BigDecimal? = null | ||||
| @Column(name = "remarks", length = 500) | |||||
| open var remarks: String? = null | |||||
| @JsonBackReference | @JsonBackReference | ||||
| @ManyToOne | @ManyToOne | ||||
| @JoinColumn(name = "stockInLineId") | @JoinColumn(name = "stockInLineId") | ||||
| @@ -1,7 +1,20 @@ | |||||
| package com.ffii.fpsms.modules.stock.entity | package com.ffii.fpsms.modules.stock.entity | ||||
| import com.ffii.core.support.AbstractRepository | import com.ffii.core.support.AbstractRepository | ||||
| import org.springframework.data.jpa.repository.Query | |||||
| import org.springframework.data.repository.query.Param | |||||
| import org.springframework.stereotype.Repository | import org.springframework.stereotype.Repository | ||||
| @Repository | @Repository | ||||
| interface StockAdjustmentRecordRepository : AbstractRepository<StockAdjustmentRecord, Long> | |||||
| interface StockAdjustmentRecordRepository : AbstractRepository<StockAdjustmentRecord, Long> { | |||||
| @Query( | |||||
| """ | |||||
| SELECT r FROM StockAdjustmentRecord r | |||||
| WHERE r.item.id = :itemId | |||||
| AND r.deleted = false | |||||
| AND r.remarks IS NOT NULL | |||||
| ORDER BY r.id DESC | |||||
| """ | |||||
| ) | |||||
| fun findRemarkRecordsByItemId(@Param("itemId") itemId: Long): List<StockAdjustmentRecord> | |||||
| } | |||||
| @@ -4,6 +4,7 @@ import com.ffii.fpsms.modules.master.web.models.MessageResponse | |||||
| import com.ffii.fpsms.modules.stock.entity.InventoryLotLine | import com.ffii.fpsms.modules.stock.entity.InventoryLotLine | ||||
| import com.ffii.fpsms.modules.stock.entity.InventoryLotLineRepository | 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.StockAdjustmentRequest | ||||
| import com.ffii.fpsms.modules.stock.web.model.StockAdjustmentRemarksResponse | |||||
| import com.ffii.fpsms.modules.stock.web.model.StockInRequest | import com.ffii.fpsms.modules.stock.web.model.StockInRequest | ||||
| import com.ffii.fpsms.modules.stock.web.model.StockOutRequest | import com.ffii.fpsms.modules.stock.web.model.StockOutRequest | ||||
| import org.springframework.stereotype.Service | import org.springframework.stereotype.Service | ||||
| @@ -24,6 +25,7 @@ open class StockAdjustmentService( | |||||
| private val stockAdjustmentRecordRepository: StockAdjustmentRecordRepository | private val stockAdjustmentRecordRepository: StockAdjustmentRecordRepository | ||||
| ) { | ) { | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 3 | v1.0.7 | 2026-09-08 */ | |||||
| @Transactional | @Transactional | ||||
| open fun submit(request: StockAdjustmentRequest): MessageResponse { | open fun submit(request: StockAdjustmentRequest): MessageResponse { | ||||
| val originalById = request.originalLines.filter { it.id > 0 }.associateBy { it.id } | val originalById = request.originalLines.filter { it.id > 0 }.associateBy { it.id } | ||||
| @@ -36,7 +38,7 @@ open class StockAdjustmentService( | |||||
| inventoryLotLineId = line.id, | inventoryLotLineId = line.id, | ||||
| type = "ADJ" | type = "ADJ" | ||||
| ) | ) | ||||
| saveAdjustmentRecordForStockOut(stockOutLine) | |||||
| saveAdjustmentRecordForStockOut(stockOutLine, line.remarks) | |||||
| } | } | ||||
| for (current in request.currentLines) { | for (current in request.currentLines) { | ||||
| // Branch 2: New entry — createStockIn (OPEN or ADJ) | // Branch 2: New entry — createStockIn (OPEN or ADJ) | ||||
| @@ -56,7 +58,7 @@ open class StockAdjustmentService( | |||||
| warehouseId = current.warehouseId | warehouseId = current.warehouseId | ||||
| ) | ) | ||||
| ) | ) | ||||
| saveAdjustmentRecordForStockIn(stockInLine) | |||||
| saveAdjustmentRecordForStockIn(stockInLine, current.remarks) | |||||
| continue | continue | ||||
| } | } | ||||
| @@ -74,7 +76,7 @@ open class StockAdjustmentService( | |||||
| stockInRequest, | stockInRequest, | ||||
| inventoryLotLine | inventoryLotLine | ||||
| ) | ) | ||||
| saveAdjustmentRecordForStockIn(stockInLine) | |||||
| saveAdjustmentRecordForStockIn(stockInLine, current.remarks) | |||||
| } else { | } else { | ||||
| // Branch 3 (qty down): adjustment outbound only (not pick createStockOut) | // Branch 3 (qty down): adjustment outbound only (not pick createStockOut) | ||||
| val stockOutLine = stockOutLineService.createStockOutForAdjustment( | val stockOutLine = stockOutLineService.createStockOutForAdjustment( | ||||
| @@ -84,7 +86,7 @@ open class StockAdjustmentService( | |||||
| type = "ADJ" | type = "ADJ" | ||||
| ) | ) | ||||
| ) | ) | ||||
| saveAdjustmentRecordForStockOut(stockOutLine) | |||||
| saveAdjustmentRecordForStockOut(stockOutLine, current.remarks) | |||||
| } | } | ||||
| } | } | ||||
| @@ -98,6 +100,19 @@ open class StockAdjustmentService( | |||||
| ) | ) | ||||
| } | } | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 3 | v1.0.7 | 2026-09-08 */ | |||||
| open fun findLatestRemarksByItemId(itemId: Long): List<StockAdjustmentRemarksResponse> { | |||||
| val seenLotNos = HashSet<String>() | |||||
| val result = mutableListOf<StockAdjustmentRemarksResponse>() | |||||
| for (row in stockAdjustmentRecordRepository.findRemarkRecordsByItemId(itemId)) { | |||||
| val remarks = normalizeRemarks(row.remarks) ?: continue | |||||
| val lotNo = row.lotNo?.trim().orEmpty() | |||||
| if (lotNo.isEmpty() || !seenLotNos.add(lotNo)) continue | |||||
| result.add(StockAdjustmentRemarksResponse(lotNo = lotNo, remarks = remarks)) | |||||
| } | |||||
| return result | |||||
| } | |||||
| private fun buildStockInRequestFromExistingLotLine( | private fun buildStockInRequestFromExistingLotLine( | ||||
| inventoryLotLine: InventoryLotLine, | inventoryLotLine: InventoryLotLine, | ||||
| acceptedQty: BigDecimal | acceptedQty: BigDecimal | ||||
| @@ -131,7 +146,7 @@ open class StockAdjustmentService( | |||||
| warehouseId = warehouseId | warehouseId = warehouseId | ||||
| ) | ) | ||||
| } | } | ||||
| private fun saveAdjustmentRecordForStockIn(stockInLine: StockInLine) { | |||||
| private fun saveAdjustmentRecordForStockIn(stockInLine: StockInLine, remarks: String?) { | |||||
| val item = stockInLine.item ?: return | val item = stockInLine.item ?: return | ||||
| val record = StockAdjustmentRecord().apply { | val record = StockAdjustmentRecord().apply { | ||||
| this.item = item | this.item = item | ||||
| @@ -140,13 +155,14 @@ open class StockAdjustmentService( | |||||
| this.lotNo = stockInLine.lotNo | this.lotNo = stockInLine.lotNo | ||||
| this.inQty = stockInLine.acceptedQty | this.inQty = stockInLine.acceptedQty | ||||
| this.outQty = null | this.outQty = null | ||||
| this.remarks = normalizeRemarks(remarks) | |||||
| this.stockInLine = stockInLine | this.stockInLine = stockInLine | ||||
| this.stockOutLine = null | this.stockOutLine = null | ||||
| } | } | ||||
| stockAdjustmentRecordRepository.save(record) | stockAdjustmentRecordRepository.save(record) | ||||
| } | } | ||||
| private fun saveAdjustmentRecordForStockOut(stockOutLine: StockOutLine) { | |||||
| private fun saveAdjustmentRecordForStockOut(stockOutLine: StockOutLine, remarks: String?) { | |||||
| val item = stockOutLine.item ?: return | val item = stockOutLine.item ?: return | ||||
| val lotNo = stockOutLine.inventoryLotLine?.inventoryLot?.lotNo | val lotNo = stockOutLine.inventoryLotLine?.inventoryLot?.lotNo | ||||
| val record = StockAdjustmentRecord().apply { | val record = StockAdjustmentRecord().apply { | ||||
| @@ -156,9 +172,13 @@ open class StockAdjustmentService( | |||||
| this.lotNo = lotNo | this.lotNo = lotNo | ||||
| this.inQty = null | this.inQty = null | ||||
| this.outQty = BigDecimal.valueOf(stockOutLine.qty ?: 0.0) | this.outQty = BigDecimal.valueOf(stockOutLine.qty ?: 0.0) | ||||
| this.remarks = normalizeRemarks(remarks) | |||||
| this.stockInLine = null | this.stockInLine = null | ||||
| this.stockOutLine = stockOutLine | this.stockOutLine = stockOutLine | ||||
| } | } | ||||
| stockAdjustmentRecordRepository.save(record) | stockAdjustmentRecordRepository.save(record) | ||||
| } | } | ||||
| private fun normalizeRemarks(remarks: String?): String? = | |||||
| remarks?.trim()?.takeIf { it.isNotEmpty() } | |||||
| } | } | ||||
| @@ -3,6 +3,7 @@ package com.ffii.fpsms.modules.stock.web | |||||
| import com.ffii.fpsms.modules.master.web.models.MessageResponse | import com.ffii.fpsms.modules.master.web.models.MessageResponse | ||||
| import com.ffii.fpsms.modules.stock.service.StockAdjustmentService | import com.ffii.fpsms.modules.stock.service.StockAdjustmentService | ||||
| import com.ffii.fpsms.modules.stock.web.model.StockAdjustmentRequest | import com.ffii.fpsms.modules.stock.web.model.StockAdjustmentRequest | ||||
| import com.ffii.fpsms.modules.stock.web.model.StockAdjustmentRemarksResponse | |||||
| import jakarta.validation.Valid | import jakarta.validation.Valid | ||||
| import org.springframework.web.bind.annotation.* | import org.springframework.web.bind.annotation.* | ||||
| @@ -11,8 +12,15 @@ import org.springframework.web.bind.annotation.* | |||||
| class StockAdjustmentController( | class StockAdjustmentController( | ||||
| private val stockAdjustmentService: StockAdjustmentService | private val stockAdjustmentService: StockAdjustmentService | ||||
| ) { | ) { | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 3 | v1.0.7 | 2026-09-08 */ | |||||
| @PostMapping("/submit") | @PostMapping("/submit") | ||||
| fun submit(@Valid @RequestBody request: StockAdjustmentRequest): MessageResponse { | fun submit(@Valid @RequestBody request: StockAdjustmentRequest): MessageResponse { | ||||
| return stockAdjustmentService.submit(request) | return stockAdjustmentService.submit(request) | ||||
| } | } | ||||
| /** FP-MTMS Version Checklist | Functions Ref. No. 3 | v1.0.7 | 2026-09-08 */ | |||||
| @GetMapping("/latestRemarks") | |||||
| fun latestRemarks(@RequestParam itemId: Long): List<StockAdjustmentRemarksResponse> { | |||||
| return stockAdjustmentService.findLatestRemarksByItemId(itemId) | |||||
| } | |||||
| } | } | ||||
| @@ -14,11 +14,17 @@ data class StockAdjustmentLineRequest( | |||||
| val itemNo: String, | val itemNo: String, | ||||
| val expiryDate: String, | val expiryDate: String, | ||||
| val warehouseId: Long, | val warehouseId: Long, | ||||
| val uom: String? = null | |||||
| val uom: String? = null, | |||||
| val remarks: String? = null | |||||
| ) | ) | ||||
| data class StockAdjustmentRequest( | data class StockAdjustmentRequest( | ||||
| val itemId: Long, | val itemId: Long, | ||||
| val originalLines: List<StockAdjustmentLineRequest>, | val originalLines: List<StockAdjustmentLineRequest>, | ||||
| val currentLines: List<StockAdjustmentLineRequest> | val currentLines: List<StockAdjustmentLineRequest> | ||||
| ) | |||||
| data class StockAdjustmentRemarksResponse( | |||||
| val lotNo: String?, | |||||
| val remarks: String | |||||
| ) | ) | ||||
| @@ -0,0 +1,6 @@ | |||||
| --liquibase formatted sql | |||||
| --changeset fpsms:stock_adjustment_record_remarks | |||||
| --comment: Store stock adjustment reason/remarks | |||||
| ALTER TABLE `stock_adjustment_record` | |||||
| ADD COLUMN `remarks` VARCHAR(500) NULL AFTER `outQty`; | |||||