| @@ -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. 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 | |||
| @Order(1) | |||
| @@ -119,6 +119,8 @@ public class SecurityConfig { | |||
| .hasAnyAuthority("TESTING", "ADMIN") | |||
| .requestMatchers(HttpMethod.POST, "/stockAdjustment/submit") | |||
| .hasAuthority("INVENTORY_ADJUST") | |||
| .requestMatchers(HttpMethod.GET, "/stockAdjustment/latestRemarks") | |||
| .hasAuthority("INVENTORY_ADJUST") | |||
| .requestMatchers(HttpMethod.GET, "/inventoryLotLine/trace") | |||
| .hasAuthority("ITEM_TRACING") | |||
| .requestMatchers(HttpMethod.GET, "/inventoryLotLine/trace/location/**") | |||
| @@ -216,7 +216,6 @@ class StockLotOnhandReportController( | |||
| "樓層", "倉庫", "區域", "儲位", | |||
| "最後異動日", "最後異動(入庫/出庫)", | |||
| "批號結餘", "單位均價", "庫存總價值", | |||
| "備註", | |||
| ) | |||
| val totalColumns = headers.size | |||
| var rowIndex = 0 | |||
| @@ -301,7 +300,6 @@ class StockLotOnhandReportController( | |||
| setNumberCell(r, 12, m["lotQtyRaw"], styles.number) | |||
| setNumberCell(r, 13, m["avgUnitPriceRaw"], 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.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) } | |||
| val out = ByteArrayOutputStream() | |||
| @@ -31,6 +31,9 @@ open class StockAdjustmentRecord : BaseEntity<Long>() { | |||
| @Column(name = "outQty", precision = 14, scale = 2) | |||
| open var outQty: BigDecimal? = null | |||
| @Column(name = "remarks", length = 500) | |||
| open var remarks: String? = null | |||
| @JsonBackReference | |||
| @ManyToOne | |||
| @JoinColumn(name = "stockInLineId") | |||
| @@ -1,7 +1,20 @@ | |||
| package com.ffii.fpsms.modules.stock.entity | |||
| 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 | |||
| @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.InventoryLotLineRepository | |||
| 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.StockOutRequest | |||
| import org.springframework.stereotype.Service | |||
| @@ -24,6 +25,7 @@ open class StockAdjustmentService( | |||
| private val stockAdjustmentRecordRepository: StockAdjustmentRecordRepository | |||
| ) { | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 3 | v1.0.7 | 2026-09-08 */ | |||
| @Transactional | |||
| open fun submit(request: StockAdjustmentRequest): MessageResponse { | |||
| val originalById = request.originalLines.filter { it.id > 0 }.associateBy { it.id } | |||
| @@ -36,7 +38,7 @@ open class StockAdjustmentService( | |||
| inventoryLotLineId = line.id, | |||
| type = "ADJ" | |||
| ) | |||
| saveAdjustmentRecordForStockOut(stockOutLine) | |||
| saveAdjustmentRecordForStockOut(stockOutLine, line.remarks) | |||
| } | |||
| for (current in request.currentLines) { | |||
| // Branch 2: New entry — createStockIn (OPEN or ADJ) | |||
| @@ -56,7 +58,7 @@ open class StockAdjustmentService( | |||
| warehouseId = current.warehouseId | |||
| ) | |||
| ) | |||
| saveAdjustmentRecordForStockIn(stockInLine) | |||
| saveAdjustmentRecordForStockIn(stockInLine, current.remarks) | |||
| continue | |||
| } | |||
| @@ -74,7 +76,7 @@ open class StockAdjustmentService( | |||
| stockInRequest, | |||
| inventoryLotLine | |||
| ) | |||
| saveAdjustmentRecordForStockIn(stockInLine) | |||
| saveAdjustmentRecordForStockIn(stockInLine, current.remarks) | |||
| } else { | |||
| // Branch 3 (qty down): adjustment outbound only (not pick createStockOut) | |||
| val stockOutLine = stockOutLineService.createStockOutForAdjustment( | |||
| @@ -84,7 +86,7 @@ open class StockAdjustmentService( | |||
| 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( | |||
| inventoryLotLine: InventoryLotLine, | |||
| acceptedQty: BigDecimal | |||
| @@ -131,7 +146,7 @@ open class StockAdjustmentService( | |||
| warehouseId = warehouseId | |||
| ) | |||
| } | |||
| private fun saveAdjustmentRecordForStockIn(stockInLine: StockInLine) { | |||
| private fun saveAdjustmentRecordForStockIn(stockInLine: StockInLine, remarks: String?) { | |||
| val item = stockInLine.item ?: return | |||
| val record = StockAdjustmentRecord().apply { | |||
| this.item = item | |||
| @@ -140,13 +155,14 @@ open class StockAdjustmentService( | |||
| this.lotNo = stockInLine.lotNo | |||
| this.inQty = stockInLine.acceptedQty | |||
| this.outQty = null | |||
| this.remarks = normalizeRemarks(remarks) | |||
| this.stockInLine = stockInLine | |||
| this.stockOutLine = null | |||
| } | |||
| stockAdjustmentRecordRepository.save(record) | |||
| } | |||
| private fun saveAdjustmentRecordForStockOut(stockOutLine: StockOutLine) { | |||
| private fun saveAdjustmentRecordForStockOut(stockOutLine: StockOutLine, remarks: String?) { | |||
| val item = stockOutLine.item ?: return | |||
| val lotNo = stockOutLine.inventoryLotLine?.inventoryLot?.lotNo | |||
| val record = StockAdjustmentRecord().apply { | |||
| @@ -156,9 +172,13 @@ open class StockAdjustmentService( | |||
| this.lotNo = lotNo | |||
| this.inQty = null | |||
| this.outQty = BigDecimal.valueOf(stockOutLine.qty ?: 0.0) | |||
| this.remarks = normalizeRemarks(remarks) | |||
| this.stockInLine = null | |||
| this.stockOutLine = stockOutLine | |||
| } | |||
| 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.stock.service.StockAdjustmentService | |||
| import com.ffii.fpsms.modules.stock.web.model.StockAdjustmentRequest | |||
| import com.ffii.fpsms.modules.stock.web.model.StockAdjustmentRemarksResponse | |||
| import jakarta.validation.Valid | |||
| import org.springframework.web.bind.annotation.* | |||
| @@ -11,8 +12,15 @@ import org.springframework.web.bind.annotation.* | |||
| class StockAdjustmentController( | |||
| private val stockAdjustmentService: StockAdjustmentService | |||
| ) { | |||
| /** FP-MTMS Version Checklist | Functions Ref. No. 3 | v1.0.7 | 2026-09-08 */ | |||
| @PostMapping("/submit") | |||
| fun submit(@Valid @RequestBody request: StockAdjustmentRequest): MessageResponse { | |||
| 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 expiryDate: String, | |||
| val warehouseId: Long, | |||
| val uom: String? = null | |||
| val uom: String? = null, | |||
| val remarks: String? = null | |||
| ) | |||
| data class StockAdjustmentRequest( | |||
| val itemId: Long, | |||
| val originalLines: 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`; | |||