| @@ -213,6 +213,7 @@ open class M18PurchaseOrderService( | |||||
| val successDetailList = mutableListOf<Long>() | val successDetailList = mutableListOf<Long>() | ||||
| val failList = mutableListOf<Long>() | val failList = mutableListOf<Long>() | ||||
| val failDetailList = mutableListOf<Long>() | val failDetailList = mutableListOf<Long>() | ||||
| val affectedItemIds = mutableSetOf<Long>() | |||||
| val poRefType = "Purchase Order" | val poRefType = "Purchase Order" | ||||
| val poLineRefType = "Purchase Order Line" | val poLineRefType = "Purchase Order Line" | ||||
| @@ -409,6 +410,7 @@ open class M18PurchaseOrderService( | |||||
| successDetailList.add(line.id) | successDetailList.add(line.id) | ||||
| // logger.info("${poLineRefType}: Purchase order ID: ${purchaseOrderId} | M18 ID: ${purchaseOrder.id}") | // logger.info("${poLineRefType}: Purchase order ID: ${purchaseOrderId} | M18 ID: ${purchaseOrder.id}") | ||||
| logger.info("${poLineRefType}: Saved purchase order line. ID: ${savePurchaseOrderLineResponse.id} | M18 Line ID: ${line.id} | Purchase order ID: ${purchaseOrderId} | M18 ID: ${purchaseOrder.id}") | logger.info("${poLineRefType}: Saved purchase order line. ID: ${savePurchaseOrderLineResponse.id} | M18 Line ID: ${line.id} | Purchase order ID: ${purchaseOrderId} | M18 ID: ${purchaseOrder.id}") | ||||
| itemId?.let { affectedItemIds.add(it) } | |||||
| } catch (e: Exception) { | } catch (e: Exception) { | ||||
| failDetailList.add(line.id) | failDetailList.add(line.id) | ||||
| // logger.error("${poLineRefType}: Saving Failure!") | // logger.error("${poLineRefType}: Saving Failure!") | ||||
| @@ -428,8 +430,9 @@ open class M18PurchaseOrderService( | |||||
| // Mark as deleted any local PO lines that no longer exist in M18 (removed there) | // Mark as deleted any local PO lines that no longer exist in M18 (removed there) | ||||
| if (purchaseOrderId != null) { | if (purchaseOrderId != null) { | ||||
| val m18LineIds = pot.map { it.id }.toSet() | val m18LineIds = pot.map { it.id }.toSet() | ||||
| val markedDeleted = | |||||
| val (markedDeleted, deletedItemIds) = | |||||
| purchaseOrderLineService.markDeletedLinesNotInM18(purchaseOrderId, m18LineIds) | purchaseOrderLineService.markDeletedLinesNotInM18(purchaseOrderId, m18LineIds) | ||||
| affectedItemIds.addAll(deletedItemIds) | |||||
| if (markedDeleted > 0) { | if (markedDeleted > 0) { | ||||
| logger.info("${poLineRefType}: Marked $markedDeleted line(s) as deleted (not in M18). PO ID: $purchaseOrderId | M18 PO ID: ${purchaseOrder.id}") | logger.info("${poLineRefType}: Marked $markedDeleted line(s) as deleted (not in M18). PO ID: $purchaseOrderId | M18 PO ID: ${purchaseOrder.id}") | ||||
| } | } | ||||
| @@ -515,11 +518,17 @@ open class M18PurchaseOrderService( | |||||
| logger.error("Total Fail (${poLineRefType}) (${failDetailList.size}): $failDetailList") | logger.error("Total Fail (${poLineRefType}) (${failDetailList.size}): $failDetailList") | ||||
| // } | // } | ||||
| val feeMarked = purchaseOrderLineService.markDeletedLinesWithFeeItems() | |||||
| val (feeMarked, feeItemIds) = purchaseOrderLineService.markDeletedLinesWithFeeItems() | |||||
| affectedItemIds.addAll(feeItemIds) | |||||
| if (feeMarked > 0) { | if (feeMarked > 0) { | ||||
| logger.info("Marked $feeMarked PO line(s) as deleted (isFee items).") | logger.info("Marked $feeMarked PO line(s) as deleted (isFee items).") | ||||
| } | } | ||||
| if (affectedItemIds.isNotEmpty()) { | |||||
| itemsService.updateAverageUnitPriceForItems(affectedItemIds) | |||||
| logger.info("Average unit price updated for ${affectedItemIds.size} item(s) after PO sync.") | |||||
| } | |||||
| logger.info("--------------------------------------------End - Saving M18 Purchase Order--------------------------------------------") | logger.info("--------------------------------------------End - Saving M18 Purchase Order--------------------------------------------") | ||||
| return SyncResult( | return SyncResult( | ||||
| @@ -67,6 +67,15 @@ open class Items : BaseEntity<Long>() { | |||||
| @Column(name = "m18LastModifyDate") | @Column(name = "m18LastModifyDate") | ||||
| open var m18LastModifyDate: LocalDateTime? = null | open var m18LastModifyDate: LocalDateTime? = null | ||||
| @Column(name = "AverageUnitPrice", nullable = true, length = 255) | |||||
| open var averageUnitPrice: String? = null | |||||
| @Column(name = "LatestMarketUnitPrice", nullable = true) | |||||
| open var latestMarketUnitPrice: Double? = null | |||||
| @Column(name = "latestMupUpdatedDate", nullable = true) | |||||
| open var latestMupUpdatedDate: LocalDateTime? = null | |||||
| @JsonManagedReference | @JsonManagedReference | ||||
| @OneToMany(mappedBy = "item", cascade = [CascadeType.ALL], orphanRemoval = true) | @OneToMany(mappedBy = "item", cascade = [CascadeType.ALL], orphanRemoval = true) | ||||
| open var itemUoms: MutableList<ItemUom> = mutableListOf() | open var itemUoms: MutableList<ItemUom> = mutableListOf() | ||||
| @@ -44,7 +44,7 @@ open class Warehouse : BaseEntity<Long>() { | |||||
| @Column(name = "stockTakeSection", nullable = true, length = 255) | @Column(name = "stockTakeSection", nullable = true, length = 255) | ||||
| open var stockTakeSection: String? = null | open var stockTakeSection: String? = null | ||||
| @Column(name = "stockTakeSectionDescription", nullable = true, length = 255) | @Column(name = "stockTakeSectionDescription", nullable = true, length = 255) | ||||
| open var stockTakeSectionDescription: String? = null | open var stockTakeSectionDescription: String? = null | ||||
| } | } | ||||
| @@ -15,11 +15,13 @@ import java.time.LocalDateTime | |||||
| import java.time.format.DateTimeFormatter | import java.time.format.DateTimeFormatter | ||||
| import java.time.temporal.TemporalAdjusters | import java.time.temporal.TemporalAdjusters | ||||
| import kotlin.jvm.optionals.getOrNull | import kotlin.jvm.optionals.getOrNull | ||||
| import com.ffii.core.utils.ExcelUtils | |||||
| import org.apache.poi.ss.usermodel.Cell | import org.apache.poi.ss.usermodel.Cell | ||||
| import org.apache.poi.ss.usermodel.CellType | import org.apache.poi.ss.usermodel.CellType | ||||
| import org.apache.poi.ss.usermodel.Sheet | import org.apache.poi.ss.usermodel.Sheet | ||||
| import org.apache.poi.ss.usermodel.Workbook | import org.apache.poi.ss.usermodel.Workbook | ||||
| import org.apache.poi.xssf.usermodel.XSSFWorkbook | import org.apache.poi.xssf.usermodel.XSSFWorkbook | ||||
| import java.io.ByteArrayOutputStream | |||||
| import java.io.File | import java.io.File | ||||
| import java.io.FileInputStream | import java.io.FileInputStream | ||||
| import java.math.BigDecimal | import java.math.BigDecimal | ||||
| @@ -399,9 +401,15 @@ open class ItemsService( | |||||
| "i.description, " + | "i.description, " + | ||||
| "i.type, " + | "i.type, " + | ||||
| "i.`LocationCode` as LocationCode, " + | "i.`LocationCode` as LocationCode, " + | ||||
| "i.`qcCategoryId` as qcCategoryId " + | |||||
| "i.`qcCategoryId` as qcCategoryId, " + | |||||
| "i.AverageUnitPrice as averageUnitPrice, " + | |||||
| "i.LatestMarketUnitPrice as latestMarketUnitPrice, " + | |||||
| "DATE_FORMAT(i.latestMupUpdatedDate, '%Y-%m-%d %H:%i') as latestMupUpdatedDate, " + | |||||
| "uc_p.udfudesc as purchaseUnit " + | |||||
| "FROM items i " + | "FROM items i " + | ||||
| "WHERE i.deleted = FALSE" | |||||
| "LEFT JOIN item_uom iu_p ON iu_p.itemId = i.id AND iu_p.deleted = false AND iu_p.purchaseUnit = true " + | |||||
| "LEFT JOIN uom_conversion uc_p ON uc_p.id = iu_p.uomId " + | |||||
| "WHERE i.deleted = false" | |||||
| ); | ); | ||||
| if (args.containsKey("name")) { | if (args.containsKey("name")) { | ||||
| @@ -415,6 +423,141 @@ open class ItemsService( | |||||
| return jdbcDao.queryForList(sql.toString(), args); | return jdbcDao.queryForList(sql.toString(), args); | ||||
| } | } | ||||
| /** | |||||
| * Incremental update of AverageUnitPrice for the given items only. | |||||
| * Recomputes weighted average (SUM(up*qty)/SUM(qty)) from purchase_order_line | |||||
| * where po.orderDate >= '2026-01-01', then updates items.AverageUnitPrice. | |||||
| * Items with no qualifying PO lines get AverageUnitPrice = null. | |||||
| */ | |||||
| @Transactional | |||||
| open fun updateAverageUnitPriceForItems(itemIds: Set<Long>) { | |||||
| if (itemIds.isEmpty()) return | |||||
| val idList = itemIds.toList() | |||||
| val args = mapOf("itemIds" to idList) | |||||
| val avgSql = """ | |||||
| SELECT pol.itemId AS itemId, | |||||
| ROUND(CAST(SUM(pol.up * pol.qty) / NULLIF(SUM(pol.qty), 0) AS DECIMAL(14,2)), 2) AS avgUp | |||||
| FROM purchase_order_line pol | |||||
| JOIN purchase_order po ON po.id = pol.purchaseOrderId AND po.deleted = false | |||||
| WHERE pol.itemId IN (:itemIds) AND pol.deleted = false | |||||
| AND po.orderDate >= '2026-01-01' | |||||
| AND pol.up IS NOT NULL AND pol.qty > 0 | |||||
| GROUP BY pol.itemId | |||||
| """.trimIndent() | |||||
| val rows = jdbcDao.queryForList(avgSql, args) | |||||
| val avgByItem = rows.mapNotNull { row -> | |||||
| val id = (row["itemId"] as? Number)?.toLong() ?: return@mapNotNull null | |||||
| val avg = row["avgUp"] as? BigDecimal ?: return@mapNotNull null | |||||
| id to avg.toPlainString() | |||||
| }.toMap() | |||||
| idList.forEach { itemId -> | |||||
| val item = itemsRepository.findById(itemId).orElse(null) ?: return@forEach | |||||
| item.averageUnitPrice = avgByItem[itemId] | |||||
| itemsRepository.saveAndFlush(item) | |||||
| } | |||||
| } | |||||
| /** Column headers in Chinese for market unit price template/import */ | |||||
| private val MARKET_UNIT_PRICE_HEADERS_ZH = listOf("項目編號", "項目名稱", "採購單位", "最新市場價格") | |||||
| /** Accepted column 3 header (price column); 最新市場單位價格 allowed for backward compatibility. */ | |||||
| private val ACCEPTED_COL3_ALIASES = setOf("最新市場價格", "最新市場單位價格", "Latest Market Unit Price", "Latest Market Price", "Market Unit Price") | |||||
| /** | |||||
| * Generates Excel template for latest market unit price upload. | |||||
| * Columns: 項目編號, 項目名稱, 採購單位, 最新市場價格 (empty). | |||||
| */ | |||||
| open fun generateMarketUnitPriceTemplate(): ByteArray { | |||||
| val data = getItemsByPage(emptyMap()) | |||||
| val workbook = XSSFWorkbook() | |||||
| val sheet = workbook.createSheet("Market Unit Price") | |||||
| val headerRow = sheet.createRow(0) | |||||
| MARKET_UNIT_PRICE_HEADERS_ZH.forEachIndexed { index, header -> | |||||
| headerRow.createCell(index).setCellValue(header) | |||||
| } | |||||
| data.forEachIndexed { index, row -> | |||||
| val r = sheet.createRow(index + 1) | |||||
| r.createCell(0).setCellValue((row["code"]?.toString() ?: "").trim()) | |||||
| r.createCell(1).setCellValue((row["name"]?.toString() ?: "").trim()) | |||||
| r.createCell(2).setCellValue((row["purchaseUnit"]?.toString() ?: "").trim()) | |||||
| r.createCell(3).setCellValue("") // empty for user to fill | |||||
| } | |||||
| val baos = ByteArrayOutputStream() | |||||
| workbook.use { it.write(baos) } | |||||
| return baos.toByteArray() | |||||
| } | |||||
| /** | |||||
| * Imports latest market unit price from Excel. Validates header row matches expected format. | |||||
| * @return map with "success", "updated" count, "errors" list | |||||
| */ | |||||
| @Transactional | |||||
| open fun importMarketUnitPriceExcel(workbook: Workbook?): Map<String, Any> { | |||||
| val errors = mutableListOf<String>() | |||||
| if (workbook == null) { | |||||
| return mapOf("success" to false, "updated" to 0, "errors" to listOf("No file or invalid Excel file.")) | |||||
| } | |||||
| val sheet = workbook.getSheetAt(0) ?: return mapOf("success" to false, "updated" to 0, "errors" to listOf("Sheet not found.")) | |||||
| val headerRow = sheet.getRow(0) ?: return mapOf("success" to false, "updated" to 0, "errors" to listOf("Header row not found.")) | |||||
| // Validate header format: exact 4 columns 項目編號, 項目名稱, 採購單位, 最新市場價格 | |||||
| val actualHeaders = (0..3).map { ExcelUtils.getStringValue(headerRow.getCell(it)).trim() } | |||||
| val expectedCol0 = listOf("項目編號", "Item Code") | |||||
| val expectedCol1 = listOf("項目名稱", "Item Name") | |||||
| val expectedCol2 = listOf("採購單位", "Purchase Unit") | |||||
| val validFormat = (actualHeaders[0] in expectedCol0) | |||||
| && (actualHeaders[1] in expectedCol1) | |||||
| && (actualHeaders[2] in expectedCol2) | |||||
| && (actualHeaders[3] in ACCEPTED_COL3_ALIASES) | |||||
| if (!validFormat) { | |||||
| errors.add("Invalid format: first row must be exactly 項目編號, 項目名稱, 採購單位, 最新市場價格 (or English: Item Code, Item Name, Purchase Unit, Latest Market Price). Got: ${actualHeaders.joinToString(", ")}") | |||||
| return mapOf("success" to false, "updated" to 0, "errors" to errors) | |||||
| } | |||||
| if (sheet.lastRowNum < 1) { | |||||
| errors.add("No data rows: file must contain at least one data row after the header.") | |||||
| return mapOf("success" to false, "updated" to 0, "errors" to errors) | |||||
| } | |||||
| var updated = 0 | |||||
| val now = LocalDateTime.now() | |||||
| for (i in 1..sheet.lastRowNum) { | |||||
| val row = sheet.getRow(i) ?: continue | |||||
| val codeCell = row.getCell(0) | |||||
| if (codeCell == null) { | |||||
| errors.add("Row ${i + 1}: missing 項目編號 (column A).") | |||||
| continue | |||||
| } | |||||
| val code = ExcelUtils.getStringValue(codeCell).trim() | |||||
| val priceCell = row.getCell(3) | |||||
| val priceStr = if (priceCell != null) ExcelUtils.getStringValue(priceCell).trim() else "" | |||||
| if (code.isEmpty() && priceStr.isEmpty()) continue | |||||
| if (code.isEmpty()) { | |||||
| errors.add("Row ${i + 1}: 項目編號 (column A) is required when 最新市場價格 is filled.") | |||||
| continue | |||||
| } | |||||
| // Empty 最新市場價格 is okay: skip row and do not update | |||||
| if (priceStr.isEmpty()) continue | |||||
| val price = try { | |||||
| if (priceCell != null && priceCell.cellType == CellType.NUMERIC) ExcelUtils.getDoubleValue(priceCell) else priceStr.toDouble() | |||||
| } catch (_: NumberFormatException) { | |||||
| errors.add("Row ${i + 1}: invalid price '$priceStr' for item $code") | |||||
| continue | |||||
| } | |||||
| if (price < 0) { | |||||
| errors.add("Row ${i + 1}: price must be non-negative for item $code") | |||||
| continue | |||||
| } | |||||
| val item = findByCode(code) | |||||
| if (item == null) { | |||||
| errors.add("Row ${i + 1}: item not found for code '$code'") | |||||
| continue | |||||
| } | |||||
| item.latestMarketUnitPrice = price | |||||
| item.latestMupUpdatedDate = now | |||||
| itemsRepository.saveAndFlush(item) | |||||
| updated++ | |||||
| } | |||||
| return mapOf("success" to true, "updated" to updated, "errors" to errors) | |||||
| } | |||||
| open fun findById(id: Long): Items? { | open fun findById(id: Long): Items? { | ||||
| return itemsRepository.findByIdAndDeletedFalse(id); | return itemsRepository.findByIdAndDeletedFalse(id); | ||||
| } | } | ||||
| @@ -11,11 +11,15 @@ import com.ffii.fpsms.modules.master.web.models.MessageResponse | |||||
| import com.ffii.fpsms.modules.master.web.models.NewItemRequest | import com.ffii.fpsms.modules.master.web.models.NewItemRequest | ||||
| import jakarta.servlet.http.HttpServletRequest | import jakarta.servlet.http.HttpServletRequest | ||||
| import jakarta.validation.Valid | import jakarta.validation.Valid | ||||
| import org.springframework.http.HttpHeaders | |||||
| import org.springframework.http.MediaType | |||||
| import org.springframework.http.ResponseEntity | |||||
| import org.springframework.web.bind.annotation.* | import org.springframework.web.bind.annotation.* | ||||
| import org.springframework.web.multipart.MultipartFile | |||||
| import org.apache.poi.xssf.usermodel.XSSFWorkbook | |||||
| import java.util.Collections.emptyList | import java.util.Collections.emptyList | ||||
| import org.slf4j.Logger | import org.slf4j.Logger | ||||
| import org.slf4j.LoggerFactory | import org.slf4j.LoggerFactory | ||||
| import org.springframework.http.ResponseEntity | |||||
| @RestController | @RestController | ||||
| @RequestMapping("/items") | @RequestMapping("/items") | ||||
| @@ -132,5 +136,31 @@ fun getItemsWithDetailsByPage(request: HttpServletRequest): RecordsRes<Map<Strin | |||||
| return RecordsRes(paginatedList, fullList.size) | return RecordsRes(paginatedList, fullList.size) | ||||
| } | } | ||||
| @GetMapping("/marketUnitPrice/template") | |||||
| fun downloadMarketUnitPriceTemplate(): ResponseEntity<ByteArray> { | |||||
| val bytes = itemsService.generateMarketUnitPriceTemplate() | |||||
| val headers = HttpHeaders().apply { | |||||
| contentType = MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") | |||||
| setContentDispositionFormData("attachment", "market_unit_price_template.xlsx") | |||||
| } | |||||
| return ResponseEntity.ok().headers(headers).body(bytes) | |||||
| } | |||||
| @PostMapping("/marketUnitPrice/import") | |||||
| fun importMarketUnitPrice(@RequestParam("file") file: MultipartFile): ResponseEntity<Map<String, Any>> { | |||||
| if (file.isEmpty) { | |||||
| return ResponseEntity.badRequest().body(mapOf("success" to false, "updated" to 0, "errors" to listOf("No file uploaded."))) | |||||
| } | |||||
| val workbook = try { | |||||
| XSSFWorkbook(file.inputStream) | |||||
| } catch (e: Exception) { | |||||
| logger.warn("Failed to parse Excel file", e) | |||||
| return ResponseEntity.badRequest().body(mapOf("success" to false, "updated" to 0, "errors" to listOf("Invalid Excel file."))) | |||||
| } | |||||
| workbook.use { | |||||
| val result = itemsService.importMarketUnitPriceExcel(it) | |||||
| return ResponseEntity.ok(result) | |||||
| } | |||||
| } | |||||
| } | } | ||||
| @@ -37,21 +37,23 @@ open class PurchaseOrderLineService( | |||||
| /** | /** | ||||
| * Mark as deleted any local PO lines for this PO that were synced from M18 but whose M18 line id | * Mark as deleted any local PO lines for this PO that were synced from M18 but whose M18 line id | ||||
| * is not in the given set (i.e. the line was deleted in M18). | * is not in the given set (i.e. the line was deleted in M18). | ||||
| * @return number of lines marked as deleted | |||||
| * @return Pair of (number of lines marked as deleted, set of itemIds of those lines) | |||||
| */ | */ | ||||
| open fun markDeletedLinesNotInM18(purchaseOrderId: Long, m18LineIds: Set<Long>): Int { | |||||
| open fun markDeletedLinesNotInM18(purchaseOrderId: Long, m18LineIds: Set<Long>): Pair<Int, Set<Long>> { | |||||
| val linesFromM18 = | val linesFromM18 = | ||||
| purchaseOrderLineRepository.findAllByPurchaseOrderIdAndDeletedIsFalseAndM18DataLogIsNotNull(purchaseOrderId) | purchaseOrderLineRepository.findAllByPurchaseOrderIdAndDeletedIsFalseAndM18DataLogIsNotNull(purchaseOrderId) | ||||
| val affectedItemIds = mutableSetOf<Long>() | |||||
| var count = 0 | var count = 0 | ||||
| linesFromM18.forEach { line -> | linesFromM18.forEach { line -> | ||||
| val m18Id = line.m18DataLog?.m18Id ?: return@forEach | val m18Id = line.m18DataLog?.m18Id ?: return@forEach | ||||
| if (m18Id !in m18LineIds) { | if (m18Id !in m18LineIds) { | ||||
| line.item?.id?.let { affectedItemIds.add(it) } | |||||
| line.deleted = true | line.deleted = true | ||||
| purchaseOrderLineRepository.saveAndFlush(line) | purchaseOrderLineRepository.saveAndFlush(line) | ||||
| count++ | count++ | ||||
| } | } | ||||
| } | } | ||||
| return count | |||||
| return Pair(count, affectedItemIds) | |||||
| } | } | ||||
| open fun findAllPoLineInfoByPoId(poId: Long): List<PurchaseOrderLineInfo> { | open fun findAllPoLineInfoByPoId(poId: Long): List<PurchaseOrderLineInfo> { | ||||
| @@ -99,12 +101,16 @@ open class PurchaseOrderLineService( | |||||
| return savedPurchaseOrderLine | return savedPurchaseOrderLine | ||||
| } | } | ||||
| open fun markDeletedLinesWithFeeItems(): Int { | |||||
| /** | |||||
| * @return Pair of (number of lines marked as deleted, set of itemIds of those lines) | |||||
| */ | |||||
| open fun markDeletedLinesWithFeeItems(): Pair<Int, Set<Long>> { | |||||
| val feeLines = purchaseOrderLineRepository.findAllByDeletedIsFalseAndItemIsFeeTrue() | val feeLines = purchaseOrderLineRepository.findAllByDeletedIsFalseAndItemIsFeeTrue() | ||||
| val affectedItemIds = feeLines.mapNotNull { it.item?.id }.toSet() | |||||
| feeLines.forEach { line -> | feeLines.forEach { line -> | ||||
| line.deleted = true | line.deleted = true | ||||
| purchaseOrderLineRepository.saveAndFlush(line) | purchaseOrderLineRepository.saveAndFlush(line) | ||||
| } | } | ||||
| return feeLines.size | |||||
| return Pair(feeLines.size, affectedItemIds) | |||||
| } | } | ||||
| } | } | ||||
| @@ -2,6 +2,7 @@ package com.ffii.fpsms.modules.stock.entity.projection | |||||
| import org.springframework.beans.factory.annotation.Value | import org.springframework.beans.factory.annotation.Value | ||||
| import java.math.BigDecimal | import java.math.BigDecimal | ||||
| import java.time.LocalDateTime | |||||
| interface InventoryInfo{ | interface InventoryInfo{ | ||||
| val id: Long? | val id: Long? | ||||
| @@ -52,4 +53,8 @@ interface InventoryInfo{ | |||||
| @get:Value("#{target.currency?.name}") | @get:Value("#{target.currency?.name}") | ||||
| val currencyName: String? | val currencyName: String? | ||||
| val status: String? | val status: String? | ||||
| @get:Value("#{target.item.latestMarketUnitPrice}") | |||||
| val latestMarketUnitPrice: Double? | |||||
| @get:Value("#{target.item.latestMupUpdatedDate}") | |||||
| val latestMupUpdatedDate: LocalDateTime? | |||||
| } | } | ||||
| @@ -0,0 +1,5 @@ | |||||
| -- liquibase formatted sql | |||||
| -- changeset <yourname>:alter_items_latest_market_unit_price_to_decimal | |||||
| ALTER TABLE `fpsmsdb`.`items` | |||||
| MODIFY COLUMN `LatestMarketUnitPrice` DECIMAL(19,4) NULL; | |||||
| @@ -0,0 +1,5 @@ | |||||
| -- liquibase formatted sql | |||||
| -- changeset KelvinY:alter_items_latest_market_unit_price_two_decimals | |||||
| ALTER TABLE `fpsmsdb`.`items` | |||||
| MODIFY COLUMN `LatestMarketUnitPrice` DECIMAL(19,2) NULL; | |||||