From 97a26b1a3b2dde0a46a1780b67dad4cf351cfeab Mon Sep 17 00:00:00 2001 From: "B.E.N.S.O.N" Date: Thu, 25 Jun 2026 14:05:39 +0800 Subject: [PATCH] =?UTF-8?q?QC=E6=B7=BB=E5=8A=A0=E6=BA=AB=E5=BA=A6/?= =?UTF-8?q?=E6=BF=95=E5=BA=A6=E5=84=B2=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mail/service/MailTemplateService.kt | 23 +- .../fpsms/modules/qc/entity/QcMeasureType.kt | 11 + .../modules/qc/entity/QcResultMeasurement.kt | 46 ++++ .../entity/QcResultMeasurementRepository.kt | 11 + .../qc/service/QcResultMeasurementService.kt | 187 +++++++++++++++ .../modules/qc/service/QcResultService.kt | 48 +++- .../modules/qc/web/QcResultController.kt | 6 +- .../qc/web/model/QcResultInfoResponse.kt | 24 ++ .../qc/web/model/SaveQcMeasurementRequest.kt | 9 + .../qc/web/model/SaveQcResultRequest.kt | 1 + .../report/service/ItemQcFailReportService.kt | 132 +++++++--- .../modules/report/service/ReportService.kt | 19 ++ .../report/web/ItemQcFailReportController.kt | 227 +++++++++++------- .../stock/service/StockInLineService.kt | 9 +- .../01_create_qc_result_measurement.sql | 25 ++ src/main/resources/jasper/ItemQCReport.jrxml | 36 ++- 16 files changed, 676 insertions(+), 138 deletions(-) create mode 100644 src/main/java/com/ffii/fpsms/modules/qc/entity/QcMeasureType.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/qc/entity/QcResultMeasurement.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/qc/entity/QcResultMeasurementRepository.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/qc/service/QcResultMeasurementService.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/qc/web/model/QcResultInfoResponse.kt create mode 100644 src/main/java/com/ffii/fpsms/modules/qc/web/model/SaveQcMeasurementRequest.kt create mode 100644 src/main/resources/db/changelog/changes/20260622_01_qc_result_measurement/01_create_qc_result_measurement.sql diff --git a/src/main/java/com/ffii/fpsms/modules/common/mail/service/MailTemplateService.kt b/src/main/java/com/ffii/fpsms/modules/common/mail/service/MailTemplateService.kt index 6b415be..651b782 100644 --- a/src/main/java/com/ffii/fpsms/modules/common/mail/service/MailTemplateService.kt +++ b/src/main/java/com/ffii/fpsms/modules/common/mail/service/MailTemplateService.kt @@ -5,6 +5,7 @@ import com.ffii.fpsms.modules.common.mail.entity.MailTemplateRepository import com.ffii.fpsms.modules.common.mail.web.models.DownloadMailTemplateResponse import com.ffii.fpsms.modules.common.mail.web.models.MailTemplateRequest import com.ffii.fpsms.modules.qc.service.QcResultService +import com.ffii.fpsms.modules.qc.web.model.QcResultInfoResponse import com.ffii.fpsms.modules.stock.entity.StockInLineRepository import com.ffii.fpsms.modules.stock.service.InventoryLotService import com.itextpdf.html2pdf.ConverterProperties @@ -14,6 +15,7 @@ import org.jsoup.Jsoup import org.springframework.stereotype.Service import java.io.ByteArrayOutputStream import java.math.BigDecimal +import java.time.LocalDateTime import java.time.format.DateTimeFormatter import kotlin.jvm.optionals.getOrNull @@ -33,6 +35,19 @@ open class MailTemplateService( val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd") + private fun formatFailedQcResultLine(result: QcResultInfoResponse): String { + val label = result.name?.takeIf { it.isNotBlank() } + ?: result.code?.takeIf { it.isNotBlank() } + ?: "N/A" + val criteria = result.description?.takeIf { it.isNotBlank() } ?: "" + val base = if (criteria.isNotBlank()) "$label - $criteria" else label + val measurementSuffix = result.measurementValue?.let { value -> + val unit = result.unit.orEmpty() + ";實測值:$value$unit" + }.orEmpty() + return base + measurementSuffix + } + fun allMailTemplates(): List { return mailTemplateRepository.findAllByDeletedIsFalse(); } @@ -166,19 +181,21 @@ open class MailTemplateService( val filteredResult = qcResults .groupBy { Pair(it.stockInLineId, it.qcItemId) } .mapValues { (_, group) -> - group.maxByOrNull { it.recordDate } + group.maxByOrNull { it.recordDate ?: LocalDateTime.MIN } } .values .filterNotNull() .filter { !it.qcPassed } if (filteredResult.isNotEmpty()) { - qcDate = formatter.format(filteredResult.maxOf { it.recordDate }) + filteredResult.mapNotNull { it.recordDate }.maxOrNull()?.let { latest -> + qcDate = formatter.format(latest) + } val tempDoc = Jsoup.parse("") val element = tempDoc.appendElement("ul") for (result in filteredResult) { element.appendElement("li") - .text("${result.code} - ${result.description}") + .text(formatFailedQcResultLine(result)) } tempDoc.outerHtml() } else { diff --git a/src/main/java/com/ffii/fpsms/modules/qc/entity/QcMeasureType.kt b/src/main/java/com/ffii/fpsms/modules/qc/entity/QcMeasureType.kt new file mode 100644 index 0000000..48fc85b --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/qc/entity/QcMeasureType.kt @@ -0,0 +1,11 @@ +package com.ffii.fpsms.modules.qc.entity + +object QcMeasureType { + const val TEMPERATURE = "TEMPERATURE" + const val HUMIDITY = "HUMIDITY" +} + +object QcFlowType { + const val IQC = "IQC" + const val EPQC = "EPQC" +} diff --git a/src/main/java/com/ffii/fpsms/modules/qc/entity/QcResultMeasurement.kt b/src/main/java/com/ffii/fpsms/modules/qc/entity/QcResultMeasurement.kt new file mode 100644 index 0000000..60934a4 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/qc/entity/QcResultMeasurement.kt @@ -0,0 +1,46 @@ +package com.ffii.fpsms.modules.qc.entity + +import com.ffii.core.entity.BaseEntity +import com.ffii.fpsms.modules.stock.entity.StockInLine +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.JoinColumn +import jakarta.persistence.ManyToOne +import jakarta.persistence.Table +import jakarta.validation.constraints.NotNull +import jakarta.validation.constraints.Size +import java.math.BigDecimal + +@Entity +@Table(name = "qc_result_measurement") +open class QcResultMeasurement : BaseEntity() { + @NotNull + @ManyToOne + @JoinColumn(name = "qcResultId", nullable = false) + open var qcResult: QcResult? = null + + @ManyToOne + @JoinColumn(name = "stockInLineId") + open var stockInLine: StockInLine? = null + + @NotNull + @Size(max = 45) + @Column(name = "qcType", nullable = false, length = 45) + open var qcType: String? = null + + @Size(max = 100) + @Column(name = "orderRefCode", length = 100) + open var orderRefCode: String? = null + + @NotNull + @Size(max = 45) + @Column(name = "measureType", nullable = false, length = 45) + open var measureType: String? = null + + @Column(name = "value", precision = 14, scale = 2) + open var value: BigDecimal? = null + + @Size(max = 20) + @Column(name = "unit", length = 20) + open var unit: String? = null +} diff --git a/src/main/java/com/ffii/fpsms/modules/qc/entity/QcResultMeasurementRepository.kt b/src/main/java/com/ffii/fpsms/modules/qc/entity/QcResultMeasurementRepository.kt new file mode 100644 index 0000000..c389de0 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/qc/entity/QcResultMeasurementRepository.kt @@ -0,0 +1,11 @@ +package com.ffii.fpsms.modules.qc.entity + +import com.ffii.core.support.AbstractRepository +import org.springframework.stereotype.Repository + +@Repository +interface QcResultMeasurementRepository : AbstractRepository { + fun findByQcResultIdAndMeasureTypeAndDeletedFalse(qcResultId: Long, measureType: String): QcResultMeasurement? + + fun findAllByQcResultIdInAndDeletedFalse(qcResultIds: Collection): List +} diff --git a/src/main/java/com/ffii/fpsms/modules/qc/service/QcResultMeasurementService.kt b/src/main/java/com/ffii/fpsms/modules/qc/service/QcResultMeasurementService.kt new file mode 100644 index 0000000..854568b --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/qc/service/QcResultMeasurementService.kt @@ -0,0 +1,187 @@ +package com.ffii.fpsms.modules.qc.service + +import com.ffii.core.support.JdbcDao +import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository +import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderRepository +import com.ffii.fpsms.modules.qc.entity.QcFlowType +import com.ffii.fpsms.modules.qc.entity.QcMeasureType +import com.ffii.fpsms.modules.qc.entity.QcResult +import com.ffii.fpsms.modules.qc.entity.QcResultMeasurement +import com.ffii.fpsms.modules.qc.entity.QcResultMeasurementRepository +import com.ffii.fpsms.modules.qc.web.model.SaveQcResultRequest +import com.ffii.fpsms.modules.stock.entity.StockInLine +import org.slf4j.LoggerFactory +import org.springframework.dao.DataAccessException +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +open class QcResultMeasurementService( + private val qcResultMeasurementRepository: QcResultMeasurementRepository, + private val purchaseOrderRepository: PurchaseOrderRepository, + private val jobOrderRepository: JobOrderRepository, + private val jdbcDao: JdbcDao, +) { + private val logger = LoggerFactory.getLogger(QcResultMeasurementService::class.java) + + @Volatile + private var measurementTableReady: Boolean? = null + + @Volatile + private var loggedMissingTableWarning: Boolean = false + + @Transactional + open fun syncMeasurement( + request: SaveQcResultRequest, + savedQcResult: QcResult, + stockInLine: StockInLine, + ) { + if (request.measurement == null) { + return + } + if (!isMeasurementStorageReady()) { + return + } + + try { + doSyncMeasurement(request, savedQcResult, stockInLine) + } catch (ex: DataAccessException) { + if (isMissingMeasurementTable(ex)) { + measurementTableReady = false + logMissingTableOnce() + return + } + throw ex + } + } + + private fun doSyncMeasurement( + request: SaveQcResultRequest, + savedQcResult: QcResult, + stockInLine: StockInLine, + ) { + val measurementRequest = request.measurement!! + val measureType = measurementRequest.measureType?.takeIf { it.isNotBlank() } + ?: QcMeasureType.TEMPERATURE + val existing = savedQcResult.id?.let { + qcResultMeasurementRepository.findByQcResultIdAndMeasureTypeAndDeletedFalse(it, measureType) + } + + val value = measurementRequest.value + if (value == null) { + existing?.let { + it.deleted = true + qcResultMeasurementRepository.saveAndFlush(it) + } + return + } + + val measurement = existing ?: QcResultMeasurement() + measurement.apply { + qcResult = savedQcResult + this.stockInLine = stockInLine + qcType = resolveQcType(stockInLine) + orderRefCode = resolveOrderRefCode(stockInLine) + this.measureType = measureType + this.value = value + unit = measurementRequest.unit?.takeIf { it.isNotBlank() } ?: defaultUnitFor(measureType) + deleted = false + } + qcResultMeasurementRepository.saveAndFlush(measurement) + } + + /** + * Returns false when Liquibase migration for [qc_result_measurement] has not been applied yet, + * so stock-in QC can complete without rolling back the parent transaction. + */ + private fun isMeasurementStorageReady(): Boolean { + if (measurementTableReady == true) { + return true + } + + return try { + val exists = jdbcDao.queryForInt( + """ + SELECT COUNT(*) + FROM information_schema.tables + WHERE table_schema = DATABASE() + AND table_name = 'qc_result_measurement' + """.trimIndent(), + ) > 0 + measurementTableReady = exists + if (!exists) { + logMissingTableOnce() + } + exists + } catch (ex: Exception) { + measurementTableReady = false + if (!loggedMissingTableWarning) { + logger.warn( + "Unable to verify qc_result_measurement table; skipping QC measurement sync until migration is applied.", + ex, + ) + loggedMissingTableWarning = true + } + false + } + } + + private fun logMissingTableOnce() { + if (loggedMissingTableWarning) { + return + } + logger.warn( + "qc_result_measurement table is not available; skipping QC measurement sync. " + + "Apply Liquibase migration 20260622_01_qc_result_measurement to persist temperature/humidity values.", + ) + loggedMissingTableWarning = true + } + + private fun isMissingMeasurementTable(ex: Throwable): Boolean { + var current: Throwable? = ex + while (current != null) { + val message = current.message?.lowercase().orEmpty() + if ( + message.contains("qc_result_measurement") && + (message.contains("doesn't exist") || + message.contains("does not exist") || + message.contains("unknown table") || + message.contains("invalid object name")) + ) { + return true + } + current = current.cause + } + return false + } + + open fun resolveQcType(stockInLine: StockInLine): String { + return if (stockInLine.jobOrder?.id != null) { + QcFlowType.EPQC + } else { + QcFlowType.IQC + } + } + + open fun resolveOrderRefCode(stockInLine: StockInLine): String? { + val jobOrderId = stockInLine.jobOrder?.id + if (jobOrderId != null) { + return stockInLine.jobOrder?.code + ?: jobOrderRepository.findById(jobOrderId).orElse(null)?.code + } + val purchaseOrderId = stockInLine.purchaseOrder?.id + if (purchaseOrderId != null) { + return stockInLine.purchaseOrder?.code + ?: purchaseOrderRepository.findById(purchaseOrderId).orElse(null)?.code + } + return null + } + + private fun defaultUnitFor(measureType: String): String? { + return when (measureType) { + QcMeasureType.TEMPERATURE -> "°C" + QcMeasureType.HUMIDITY -> "%" + else -> null + } + } +} diff --git a/src/main/java/com/ffii/fpsms/modules/qc/service/QcResultService.kt b/src/main/java/com/ffii/fpsms/modules/qc/service/QcResultService.kt index 76c38ed..a145d01 100644 --- a/src/main/java/com/ffii/fpsms/modules/qc/service/QcResultService.kt +++ b/src/main/java/com/ffii/fpsms/modules/qc/service/QcResultService.kt @@ -6,8 +6,10 @@ import com.ffii.fpsms.modules.master.entity.ItemsRepository import com.ffii.fpsms.modules.master.entity.QcItemRepository import com.ffii.fpsms.modules.master.web.models.MessageResponse import com.ffii.fpsms.modules.qc.entity.QcResult +import com.ffii.fpsms.modules.qc.entity.QcResultMeasurementRepository import com.ffii.fpsms.modules.qc.entity.QcResultRepository import com.ffii.fpsms.modules.qc.entity.projection.QcResultInfo +import com.ffii.fpsms.modules.qc.web.model.QcResultInfoResponse import com.ffii.fpsms.modules.qc.web.model.SaveQcResultRequest import com.ffii.fpsms.modules.stock.entity.StockInLineRepository import com.ffii.fpsms.modules.stock.entity.StockOutLIneRepository @@ -19,6 +21,8 @@ import java.io.IOException open class QcResultService( private val jdbcDao: JdbcDao, private val qcResultRepository: QcResultRepository, + private val qcResultMeasurementRepository: QcResultMeasurementRepository, + private val qcResultMeasurementService: QcResultMeasurementService, private val qcItemRepository: QcItemRepository, private val itemRepository: ItemsRepository, private val stockInLineRepository: StockInLineRepository, @@ -45,6 +49,9 @@ open class QcResultService( this.qcPassed = request.qcPassed } val savedQcResult = saveAndFlush(qcResult) + stockInLine?.let { sil -> + qcResultMeasurementService.syncMeasurement(request, savedQcResult, sil) + } return MessageResponse( id = savedQcResult.id, name = savedQcResult.qcItem!!.name, @@ -55,10 +62,43 @@ open class QcResultService( ) } - open fun getAllQcResultInfoByStockInLineId(stockInLineId: Long): List { - return qcResultRepository.findQcResultInfoByStockInLineIdAndDeletedFalse(stockInLineId) + open fun getAllQcResultInfoByStockInLineId(stockInLineId: Long): List { + return enrichWithMeasurements( + qcResultRepository.findQcResultInfoByStockInLineIdAndDeletedFalse(stockInLineId) + ) + } + open fun getAllQcResultInfoByStockOutLineId(stockOutLineId: Long): List { + return enrichWithMeasurements( + qcResultRepository.findQcResultInfoByStockOutLineIdAndDeletedFalse(stockOutLineId) + ) } - open fun getAllQcResultInfoByStockOutLineId(stockOutLineId: Long): List { - return qcResultRepository.findQcResultInfoByStockOutLineIdAndDeletedFalse(stockOutLineId) + + private fun enrichWithMeasurements(results: List): List { + if (results.isEmpty()) return emptyList() + val measurementsByQcResultId = qcResultMeasurementRepository + .findAllByQcResultIdInAndDeletedFalse(results.map { it.id }) + .associateBy { it.qcResult?.id } + return results.map { info -> + val measurement = measurementsByQcResultId[info.id] + QcResultInfoResponse( + id = info.id, + qcItemId = info.qcItemId, + code = info.code, + name = info.name, + description = info.description, + remarks = info.remarks, + stockInLineId = info.stockInLineId, + stockOutLineId = info.stockOutLineId, + escalationLogId = info.escalationLogId, + failQty = info.failQty, + qcPassed = info.qcPassed, + recordDate = info.recordDate, + measurementValue = measurement?.value, + measureType = measurement?.measureType, + unit = measurement?.unit, + qcType = measurement?.qcType, + orderRefCode = measurement?.orderRefCode, + ) + } } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/qc/web/QcResultController.kt b/src/main/java/com/ffii/fpsms/modules/qc/web/QcResultController.kt index cdf6944..e71f7a6 100644 --- a/src/main/java/com/ffii/fpsms/modules/qc/web/QcResultController.kt +++ b/src/main/java/com/ffii/fpsms/modules/qc/web/QcResultController.kt @@ -1,8 +1,8 @@ package com.ffii.fpsms.modules.qc.web import com.ffii.fpsms.modules.master.web.models.MessageResponse -import com.ffii.fpsms.modules.qc.entity.projection.QcResultInfo import com.ffii.fpsms.modules.qc.service.QcResultService +import com.ffii.fpsms.modules.qc.web.model.QcResultInfoResponse import com.ffii.fpsms.modules.qc.web.model.SaveQcResultRequest import jakarta.validation.Valid import org.springframework.web.bind.annotation.* @@ -19,12 +19,12 @@ class QcResultController( } @GetMapping("/{stockInLineId}") - fun getAllQcResultInfoByStockInLineId(@PathVariable stockInLineId: Long): List { + fun getAllQcResultInfoByStockInLineId(@PathVariable stockInLineId: Long): List { return qcResultService.getAllQcResultInfoByStockInLineId(stockInLineId) } @GetMapping("/pick-order/{stockOutLineId}") - fun getAllQcResultInfoByStockOutLineId(@PathVariable stockOutLineId: Long): List { + fun getAllQcResultInfoByStockOutLineId(@PathVariable stockOutLineId: Long): List { return qcResultService.getAllQcResultInfoByStockOutLineId(stockOutLineId) } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/qc/web/model/QcResultInfoResponse.kt b/src/main/java/com/ffii/fpsms/modules/qc/web/model/QcResultInfoResponse.kt new file mode 100644 index 0000000..0969d87 --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/qc/web/model/QcResultInfoResponse.kt @@ -0,0 +1,24 @@ +package com.ffii.fpsms.modules.qc.web.model + +import java.math.BigDecimal +import java.time.LocalDateTime + +data class QcResultInfoResponse( + val id: Long, + val qcItemId: Long?, + val code: String?, + val name: String?, + val description: String?, + val remarks: String?, + val stockInLineId: Long?, + val stockOutLineId: Long?, + val escalationLogId: Long?, + val failQty: Double?, + val qcPassed: Boolean, + val recordDate: LocalDateTime?, + val measurementValue: BigDecimal? = null, + val measureType: String? = null, + val unit: String? = null, + val qcType: String? = null, + val orderRefCode: String? = null, +) diff --git a/src/main/java/com/ffii/fpsms/modules/qc/web/model/SaveQcMeasurementRequest.kt b/src/main/java/com/ffii/fpsms/modules/qc/web/model/SaveQcMeasurementRequest.kt new file mode 100644 index 0000000..b35c3ba --- /dev/null +++ b/src/main/java/com/ffii/fpsms/modules/qc/web/model/SaveQcMeasurementRequest.kt @@ -0,0 +1,9 @@ +package com.ffii.fpsms.modules.qc.web.model + +import java.math.BigDecimal + +data class SaveQcMeasurementRequest( + val measureType: String? = null, + val value: BigDecimal? = null, + val unit: String? = null, +) diff --git a/src/main/java/com/ffii/fpsms/modules/qc/web/model/SaveQcResultRequest.kt b/src/main/java/com/ffii/fpsms/modules/qc/web/model/SaveQcResultRequest.kt index 344f3ba..30a84c6 100644 --- a/src/main/java/com/ffii/fpsms/modules/qc/web/model/SaveQcResultRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/qc/web/model/SaveQcResultRequest.kt @@ -10,4 +10,5 @@ data class SaveQcResultRequest( val qcPassed: Boolean, val type: String?, val remarks: String?, + val measurement: SaveQcMeasurementRequest? = null, ) diff --git a/src/main/java/com/ffii/fpsms/modules/report/service/ItemQcFailReportService.kt b/src/main/java/com/ffii/fpsms/modules/report/service/ItemQcFailReportService.kt index ff49878..94a9038 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/service/ItemQcFailReportService.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/service/ItemQcFailReportService.kt @@ -8,15 +8,30 @@ open class ItemQcFailReportService( private val jdbcDao: JdbcDao, ) { fun searchItemQcFailReport( - stockCategory: String?, // items.type (可逗号分隔) - itemCode: String?, // items.code (可逗号分隔, LIKE) - lastInDateStart: String?, // stock_in_line.receiptDate >= - lastInDateEnd: String?, // stock_in_line.receiptDate < + stockCategory: String?, + itemCode: String?, + lastInDateStart: String?, + lastInDateEnd: String?, + qcType: String?, + includeMeasurable: String?, + includeOther: String?, + measurableScope: String?, ): List> { + val measurable = resolveIncludeMeasurable(includeMeasurable) + val other = isTruthy(includeOther) + val scope = resolveMeasurableScope(measurableScope, measurable, other) + + if (!measurable && !other) { + return emptyList() + } + val args = mutableMapOf() val stockCategorySql = buildMultiValueExactClause(stockCategory, "it.type", "stockCategory", args) val itemCodeSql = buildMultiValueLikeClause(itemCode, "it.code", "itemCode", args) + val qcTypeSql = buildQcTypeClause(qcType) + val qcItemScopeSql = buildQcItemScopeClause(measurable, other) + val measuredValueSql = buildMeasuredValueClause(measurable, scope) val lastInDateStartSql = if (!lastInDateStart.isNullOrBlank()) { val formattedDate = lastInDateStart.replace("/", "-") @@ -39,10 +54,8 @@ open class ItemQcFailReportService( COALESCE(sil.lotNo, il.lotNo, '') AS lotNo, COALESCE(DATE_FORMAT(COALESCE(sil.expiryDate, il.expiryDate), '%Y-%m-%d'), '') AS expiryDate, - /* 用 stock_in_line.jobOrderId 来判 IQC/EPQC */ CASE WHEN sil.jobOrderId IS NOT NULL THEN 'EPQC' ELSE 'IQC' END AS qcType, - /* QC Template Used:取 qc_category.code (例如 A1/B4/X1) */ COALESCE( qc.name, (SELECT name @@ -50,33 +63,37 @@ open class ItemQcFailReportService( WHERE isDefault = 1 AND deleted = 0 LIMIT 1), - qc.name, '' ) AS qcTemplate, - /* QC Criteria with Defect:优先用 qc_item_category.description,否则用 qc_item */ COALESCE(qic.description, qi.description, qi.name, '') AS qcDefectCriteria, - /* Lot Qty / Defect Qty:按 jrxml 字段类型输出 String */ - TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT( COALESCE( - CASE WHEN sil.purchaseOrderLineId IS NOT NULL - THEN sil.acceptedQty - WHEN iu_purchase.id IS NOT NULL AND iu.id IS NOT NULL - THEN sil.acceptedQty * (iu_purchase.ratioN / NULLIF(iu_purchase.ratioD, 0)) / (iu.ratioN / NULLIF(iu.ratioD, 0)) - ELSE sil.acceptedQty END, 0), 2))) AS lotQty, + TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT( + COALESCE( + CASE WHEN sil.purchaseOrderLineId IS NOT NULL + THEN sil.acceptedQty + WHEN iu_purchase.id IS NOT NULL AND iu.id IS NOT NULL + THEN sil.acceptedQty * (iu_purchase.ratioN / NULLIF(iu_purchase.ratioD, 0)) / (iu.ratioN / NULLIF(iu.ratioD, 0)) + ELSE sil.acceptedQty END, 0), 2))) AS lotQty, TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(COALESCE(qr.failQty, 0), 2))) AS defectQty, - /* Ref Data (e.g temp):目前库表只有 qc_result.remarks,可先放这里 */ - COALESCE(qr.remarks, '') AS refData, + CASE + WHEN qrm.value IS NOT NULL THEN + CONCAT( + TRIM(TRAILING '.' FROM TRIM(TRAILING '0' FROM FORMAT(qrm.value, 2))), + COALESCE(qrm.unit, '') + ) + ELSE '' + END AS measuredValue, - /* Remarks:若你之后有独立字段再补 */ - '' AS remark, + COALESCE(qr.remarks, '') AS remark, - /* Order Ref No:IQC 用 purchase_order.code;EPQC 用 job_order.code */ CASE WHEN sil.jobOrderId IS NOT NULL THEN COALESCE(jo.code, '') ELSE COALESCE(po.code, '') - END AS orderRefNo + END AS orderRefNo, + + qr.qcPassed AS qcPassed FROM qc_result qr INNER JOIN stock_in_line sil @@ -85,6 +102,9 @@ open class ItemQcFailReportService( INNER JOIN items it ON qr.itemId = it.id AND it.deleted = 0 + LEFT JOIN qc_result_measurement qrm + ON qrm.qcResultId = qr.id + AND qrm.deleted = 0 LEFT JOIN item_uom iu ON it.id = iu.itemId AND iu.stockUnit = 1 @@ -105,19 +125,19 @@ open class ItemQcFailReportService( ON sil.jobOrderId = jo.id AND jo.deleted = 0 - LEFT JOIN items_qc_category_mapping iqcm + LEFT JOIN items_qc_category_mapping iqcm ON iqcm.itemId = it.id - AND iqcm.type = (CASE WHEN sil.jobOrderId IS NOT NULL THEN 'EPQC' ELSE 'IQC' END) + AND iqcm.type = (CASE WHEN sil.jobOrderId IS NOT NULL THEN 'EPQC' ELSE 'IQC' END) LEFT JOIN qc_category qc ON qc.id = iqcm.qcCategoryId - AND qc.deleted = 0 + AND qc.deleted = 0 LEFT JOIN qc_item qi ON qi.id = qr.qcItemId - AND qi.deleted = 0 + AND qi.deleted = 0 LEFT JOIN qc_item_category qic ON qic.qcItemId = qi.id - AND qic.qcCategoryId = COALESCE( + AND qic.qcCategoryId = COALESCE( iqcm.qcCategoryId, (SELECT id FROM qc_category @@ -128,13 +148,16 @@ open class ItemQcFailReportService( WHERE qr.deleted = 0 - AND qr.qcPassed = 0 + $qcItemScopeSql + $measuredValueSql + $qcTypeSql $stockCategorySql $itemCodeSql $lastInDateStartSql $lastInDateEndSql ORDER BY + qr.qcPassed DESC, it.type, it.code, COALESCE(sil.lotNo, il.lotNo, ''), @@ -142,9 +165,58 @@ open class ItemQcFailReportService( COALESCE(qic.`order`, 9999), COALESCE(qi.code, '') """.trimIndent() - val result = jdbcDao.queryForList(sql, args) + return jdbcDao.queryForList(sql, args) + } + + private fun resolveIncludeMeasurable(includeMeasurable: String?): Boolean { + return isTruthy(includeMeasurable) + } - return result + private fun isTruthy(value: String?): Boolean { + return value?.trim()?.lowercase() in listOf("true", "1", "yes") + } + + private fun buildQcItemScopeClause(includeMeasurable: Boolean, includeOther: Boolean): String { + val parts = mutableListOf() + if (includeMeasurable) { + parts.add("qi.name IN ('溫度', '濕度')") + } + if (includeOther) { + parts.add("(qi.name NOT IN ('溫度', '濕度') OR qi.name IS NULL)") + } + return "AND (${parts.joinToString(" OR ")})" + } + + private fun resolveMeasurableScope( + measurableScope: String?, + includeMeasurable: Boolean, + includeOther: Boolean, + ): String { + if (!measurableScope.isNullOrBlank()) { + return measurableScope.trim().lowercase() + } + return if (includeMeasurable && !includeOther) "with" else "all" + } + + /** + * with = only temp/humidity rows with a filled measured value; + * all = all temp/humidity rows; + * without is handled by includeMeasurable=false on the API layer. + */ + private fun buildMeasuredValueClause(includeMeasurable: Boolean, measurableScope: String): String { + if (!includeMeasurable) return "" + return when (measurableScope) { + "with" -> "AND qrm.value IS NOT NULL" + else -> "" + } + } + + private fun buildQcTypeClause(qcType: String?): String { + return when (qcType?.trim()?.uppercase()) { + "IQC" -> "AND sil.jobOrderId IS NULL" + "EPQC" -> "AND sil.jobOrderId IS NOT NULL" + else -> "" + } } private fun buildMultiValueLikeClause( @@ -182,4 +254,4 @@ open class ItemQcFailReportService( } return "AND (${conditions.joinToString(" OR ")})" } -} \ No newline at end of file +} diff --git a/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt b/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt index be1ebab..845af94 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt @@ -1716,5 +1716,24 @@ return result val jasperPrint = JasperFillManager.fillReport(jasperReport, params, dataSource) return JasperExportManager.exportReportToPdf(jasperPrint) } + + fun mergePdfBytes(parts: List): ByteArray { + if (parts.isEmpty()) return ByteArray(0) + if (parts.size == 1) return parts[0] + + val out = java.io.ByteArrayOutputStream() + val writer = com.itextpdf.kernel.pdf.PdfWriter(out) + val pdfDoc = com.itextpdf.kernel.pdf.PdfDocument(writer) + val merger = com.itextpdf.kernel.utils.PdfMerger(pdfDoc) + parts.forEach { bytes -> + val src = com.itextpdf.kernel.pdf.PdfDocument( + com.itextpdf.kernel.pdf.PdfReader(java.io.ByteArrayInputStream(bytes)) + ) + merger.merge(src, 1, src.numberOfPages) + src.close() + } + pdfDoc.close() + return out.toByteArray() + } } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/report/web/ItemQcFailReportController.kt b/src/main/java/com/ffii/fpsms/modules/report/web/ItemQcFailReportController.kt index 577cc28..8565087 100644 --- a/src/main/java/com/ffii/fpsms/modules/report/web/ItemQcFailReportController.kt +++ b/src/main/java/com/ffii/fpsms/modules/report/web/ItemQcFailReportController.kt @@ -2,10 +2,6 @@ package com.ffii.fpsms.modules.report.web import org.springframework.http.* import org.springframework.web.bind.annotation.* -import java.time.LocalDate -import java.time.LocalTime -import java.time.format.DateTimeFormatter -import java.math.BigDecimal import com.ffii.fpsms.modules.report.service.ItemQcFailReportService import com.ffii.fpsms.modules.report.service.ReportService import org.apache.poi.ss.usermodel.BorderStyle @@ -13,7 +9,6 @@ import org.apache.poi.ss.usermodel.FillPatternType import org.apache.poi.ss.usermodel.HorizontalAlignment import org.apache.poi.ss.usermodel.IndexedColors import org.apache.poi.ss.usermodel.VerticalAlignment -import org.apache.poi.ss.usermodel.DataFormat import org.apache.poi.ss.usermodel.Workbook import org.apache.poi.ss.util.WorkbookUtil import org.apache.poi.xssf.usermodel.XSSFWorkbook @@ -32,43 +27,47 @@ class ItemQcFailReportController( @RequestParam(required = false) itemCode: String?, @RequestParam(required = false) lastInDateStart: String?, @RequestParam(required = false) lastInDateEnd: String?, + @RequestParam(required = false) qcType: String?, + @RequestParam(required = false, defaultValue = "true") includeMeasurable: String?, + @RequestParam(required = false, defaultValue = "false") includeOther: String?, + @RequestParam(required = false, defaultValue = "all") measurableScope: String?, ): ResponseEntity { - val parameters = mutableMapOf() - - parameters["stockCategory"] = stockCategory ?: "All" - parameters["stockSubCategory"] = stockCategory ?: "All" - parameters["itemNo"] = itemCode ?: "All" - parameters["year"] = java.time.LocalDate.now().year.toString() - parameters["reportDate"] = java.time.LocalDate.now().format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd")) - parameters["reportTime"] = java.time.LocalTime.now().format(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")) - - parameters["storeLocation"] = "" - parameters["balanceFilterStart"] = "" - parameters["balanceFilterEnd"] = "" - parameters["lastInDateStart"] = lastInDateStart ?: "" - parameters["lastInDateEnd"] = lastInDateEnd ?: "" - parameters["lastOutDateStart"] = "" - parameters["lastOutDateEnd"] = "" - val dbData = itemQcFailReportService.searchItemQcFailReport( stockCategory = stockCategory, itemCode = itemCode, lastInDateStart = lastInDateStart, lastInDateEnd = lastInDateEnd, + qcType = qcType, + includeMeasurable = includeMeasurable, + includeOther = includeOther, + measurableScope = measurableScope, ) - val pdfBytes = reportService.createPdfResponse( - "/jasper/ItemQCReport.jrxml", - parameters, - dbData - ) + val passedRows = dbData.filter { isQcPassed(it) } + val failedRows = dbData.filter { !isQcPassed(it) } - val headers = org.springframework.http.HttpHeaders().apply { - contentType = org.springframework.http.MediaType.APPLICATION_PDF - setContentDispositionFormData("attachment", "ItemQCFailReport.pdf") - set("filename", "ItemQCFailReport.pdf") + val pdfParts = mutableListOf() + if (passedRows.isNotEmpty()) { + pdfParts.add(createItemQcPdf(stockCategory, itemCode, lastInDateStart, lastInDateEnd, qcType, passedRows, "合格")) } - return org.springframework.http.ResponseEntity(pdfBytes, headers, org.springframework.http.HttpStatus.OK) + if (failedRows.isNotEmpty()) { + pdfParts.add(createItemQcPdf(stockCategory, itemCode, lastInDateStart, lastInDateEnd, qcType, failedRows, "不合格")) + } + + val pdfBytes = when { + pdfParts.isEmpty() -> createItemQcPdf( + stockCategory, itemCode, lastInDateStart, lastInDateEnd, qcType, emptyList(), "合格" + ) + pdfParts.size == 1 -> pdfParts[0] + else -> reportService.mergePdfBytes(pdfParts) + } + + val headers = HttpHeaders().apply { + contentType = MediaType.APPLICATION_PDF + setContentDispositionFormData("attachment", "ItemQCReport.pdf") + set("filename", "ItemQCReport.pdf") + } + return ResponseEntity(pdfBytes, headers, HttpStatus.OK) } @GetMapping("/print-item-qc-fail-excel") @@ -77,40 +76,117 @@ class ItemQcFailReportController( @RequestParam(required = false) itemCode: String?, @RequestParam(required = false) lastInDateStart: String?, @RequestParam(required = false) lastInDateEnd: String?, + @RequestParam(required = false) qcType: String?, + @RequestParam(required = false, defaultValue = "true") includeMeasurable: String?, + @RequestParam(required = false, defaultValue = "false") includeOther: String?, + @RequestParam(required = false, defaultValue = "all") measurableScope: String?, ): ResponseEntity { val dbData = itemQcFailReportService.searchItemQcFailReport( stockCategory = stockCategory, itemCode = itemCode, lastInDateStart = lastInDateStart, lastInDateEnd = lastInDateEnd, + qcType = qcType, + includeMeasurable = includeMeasurable, + includeOther = includeOther, + measurableScope = measurableScope, ) - val reportTitle = "庫存品質檢測報告" - val excelBytes = createItemQcFailExcel( - dbData = dbData, - reportTitle = reportTitle, + val passedRows = dbData.filter { isQcPassed(it) } + val failedRows = dbData.filter { !isQcPassed(it) } + + val excelBytes = createItemQcMeasurementExcel( + passedRows = passedRows, + failedRows = failedRows, lastInDateStart = lastInDateStart, - lastInDateEnd = lastInDateEnd + lastInDateEnd = lastInDateEnd, + qcType = qcType, ) - val headers = org.springframework.http.HttpHeaders().apply { - contentType = org.springframework.http.MediaType.parseMediaType( + val headers = HttpHeaders().apply { + contentType = MediaType.parseMediaType( "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" ) - setContentDispositionFormData("attachment", "ItemQCFailReport.xlsx") - set("filename", "ItemQCFailReport.xlsx") + setContentDispositionFormData("attachment", "ItemQCReport.xlsx") + set("filename", "ItemQCReport.xlsx") } - return org.springframework.http.ResponseEntity(excelBytes, headers, org.springframework.http.HttpStatus.OK) + return ResponseEntity(excelBytes, headers, HttpStatus.OK) } - private fun createItemQcFailExcel( - dbData: List>, - reportTitle: String, + private fun createItemQcPdf( + stockCategory: String?, + itemCode: String?, + lastInDateStart: String?, + lastInDateEnd: String?, + qcType: String?, + rows: List>, + reportSection: String, + ): ByteArray { + val parameters = mutableMapOf() + parameters["stockCategory"] = stockCategory ?: "All" + parameters["stockSubCategory"] = stockCategory ?: "All" + parameters["itemNo"] = itemCode ?: "All" + parameters["year"] = java.time.LocalDate.now().year.toString() + parameters["reportDate"] = java.time.LocalDate.now() + .format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd")) + parameters["reportTime"] = java.time.LocalTime.now() + .format(java.time.format.DateTimeFormatter.ofPattern("HH:mm:ss")) + parameters["storeLocation"] = "" + parameters["balanceFilterStart"] = "" + parameters["balanceFilterEnd"] = "" + parameters["lastInDateStart"] = lastInDateStart ?: "" + parameters["lastInDateEnd"] = lastInDateEnd ?: "" + parameters["lastOutDateStart"] = "" + parameters["lastOutDateEnd"] = "" + parameters["reportSection"] = reportSection + parameters["qcTypeLabel"] = resolveQcTypeLabel(qcType) + + return reportService.createPdfResponse( + "/jasper/ItemQCReport.jrxml", + parameters, + rows, + ) + } + + private fun resolveQcTypeLabel(qcType: String?): String { + return when (qcType?.trim()?.uppercase()) { + "IQC" -> "IQC" + "EPQC" -> "EPQC" + else -> "全部" + } + } + + private fun isQcPassed(row: Map): Boolean { + return when (val value = row["qcPassed"]) { + is Boolean -> value + is Number -> value.toInt() == 1 + else -> value?.toString() in listOf("1", "true", "TRUE") + } + } + + private fun createItemQcMeasurementExcel( + passedRows: List>, + failedRows: List>, lastInDateStart: String?, - lastInDateEnd: String? + lastInDateEnd: String?, + qcType: String?, ): ByteArray { val workbook: Workbook = XSSFWorkbook() - val safeSheetName = WorkbookUtil.createSafeSheetName(reportTitle) + writeQcSheet(workbook, "合格", passedRows, lastInDateStart, lastInDateEnd, qcType) + writeQcSheet(workbook, "不合格", failedRows, lastInDateStart, lastInDateEnd, qcType) + return workbookToByteArray(workbook) + } + + private fun writeQcSheet( + workbook: Workbook, + sheetLabel: String, + dbData: List>, + lastInDateStart: String?, + lastInDateEnd: String?, + qcType: String?, + ) { + val reportTitle = "庫存品質檢測報告" + val safeSheetName = WorkbookUtil.createSafeSheetName(sheetLabel) val sheet = workbook.createSheet(safeSheetName) var rowIndex = 0 @@ -126,14 +202,12 @@ class ItemQcFailReportController( } titleStyle.setFont(titleFont) - // Title row run { val titleRowIndex = rowIndex++ val row = sheet.createRow(titleRowIndex) val cell = row.createCell(0) - cell.setCellValue(reportTitle) + cell.setCellValue("$reportTitle ($sheetLabel)") cell.cellStyle = titleStyle - // Merge title across columns so the text won't be blocked by narrow col A. sheet.addMergedRegion( org.apache.poi.ss.util.CellRangeAddress( titleRowIndex, @@ -145,13 +219,13 @@ class ItemQcFailReportController( row.heightInPoints = 24f } - // Info row run { val row = sheet.createRow(rowIndex++) val cell = row.createCell(0) val startTxt = lastInDateStart ?: "" val endTxt = lastInDateEnd ?: "" - cell.setCellValue("QC 不合格日期: ${startTxt} - ${endTxt}") + val qcTypeTxt = resolveQcTypeLabel(qcType) + cell.setCellValue("QC 檢測日期: ${startTxt} - ${endTxt} QC 類型: $qcTypeTxt") } val headerStyle = workbook.createCellStyle().apply { @@ -218,15 +292,14 @@ class ItemQcFailReportController( "到期日", "QC 類型", "QC 範本", - "不合格標準", + "檢測標準", "批量", "不合格數量", - "參考資料", + "實測值", "備註", "訂單/工單" ) - // Header row run { val row = sheet.createRow(rowIndex++) headers.forEachIndexed { col, h -> @@ -234,12 +307,12 @@ class ItemQcFailReportController( cell.setCellValue(h) cell.cellStyle = headerStyle } - // Increase header row height so wrapped header text won't be blocked/truncated. row.heightInPoints = 35f } dbData.forEach { r -> val row = sheet.createRow(rowIndex++) + fun writeText(col: Int, value: Any?) { val cell = row.createCell(col) cell.setCellValue(value?.toString() ?: "") @@ -248,7 +321,6 @@ class ItemQcFailReportController( fun writeNumber(col: Int, value: Any?) { val raw = value?.toString()?.trim() ?: "" - // Some SQL strings may end with "."; Excel would display it as "4." otherwise. val cleaned = raw.removeSuffix(".") val bd = cleaned.toBigDecimalOrNull() val cell = row.createCell(col) @@ -259,7 +331,6 @@ class ItemQcFailReportController( } else { val stripped = bd.stripTrailingZeros() if (stripped.scale() <= 0) { - // Render integer without decimal dot. cell.setCellValue(stripped.toDouble()) cell.cellStyle = integerNumberStyle } else { @@ -269,7 +340,6 @@ class ItemQcFailReportController( } } - // Keys must match ItemQcFailReportService SQL aliases writeText(0, r["stockSubCategory"]) writeText(1, r["itemNo"]) writeText(2, r["itemName"]) @@ -279,7 +349,6 @@ class ItemQcFailReportController( writeText(6, r["qcType"]) writeText(7, r["qcTemplate"]) val defectCriteria = r["qcDefectCriteria"] - // Wrap this column because values can be long. run { val cell = row.createCell(8) cell.setCellValue(defectCriteria?.toString() ?: "") @@ -287,49 +356,25 @@ class ItemQcFailReportController( } writeNumber(9, r["lotQty"]) writeNumber(10, r["defectQty"]) - writeText(11, r["refData"]) + writeText(11, r["measuredValue"]) writeText(12, r["remark"]) writeText(13, r["orderRefNo"]) - // If "不合格標準" is long, increase row height so wrapped text is visible. val defectText = defectCriteria?.toString() ?: "" - // Estimate wrap lines using the known column width (approx chars per line). - // This avoids ugly/uneven row heights from simple length thresholds. val charsPerLine = 38.0 val lines = maxOf( 1, kotlin.math.ceil(defectText.length / charsPerLine).toInt() ) - val baseHeight = 18f // single-line - val lineHeight = 13.5f // additional lines + val baseHeight = 18f + val lineHeight = 13.5f row.heightInPoints = (baseHeight + (lines - 1) * lineHeight).coerceIn(18f, 110f) } - // Column widths: keep reasonable defaults, then auto-size a bit (optional) - run { - // Set explicit column widths (Excel units = 1/256th of a character width). - val widths = intArrayOf( - 12, // stockSubCategory - 16, // itemNo - 18, // itemName - 10, // unit - 14, // lotNo - 12, // expiryDate - 10, // qcType - 20, // qcTemplate - 42, // qcDefectCriteria (wrap) - 12, // lotQty - 14, // defectQty - 22, // refData - 16, // remark - 18, // orderRefNo - ) - for (col in 0 until widths.size) { - sheet.setColumnWidth(col, widths[col] * 256) - } + val widths = intArrayOf(12, 16, 18, 10, 14, 12, 10, 20, 42, 12, 14, 14, 16, 18) + for (col in widths.indices) { + sheet.setColumnWidth(col, widths[col] * 256) } - - return workbookToByteArray(workbook) } private fun workbookToByteArray(workbook: Workbook): ByteArray { @@ -339,5 +384,3 @@ class ItemQcFailReportController( return out.toByteArray() } } - - diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt index 75bb2f6..6fb83bb 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt @@ -11,6 +11,7 @@ import com.ffii.fpsms.modules.master.web.models.MessageResponse import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderLineRepository import com.ffii.fpsms.modules.qc.entity.QcResult import com.ffii.fpsms.modules.qc.entity.QcResultRepository +import com.ffii.fpsms.modules.qc.service.QcResultMeasurementService import com.ffii.fpsms.modules.stock.entity.* import com.ffii.fpsms.modules.stock.sql.StockSql.SQL.INVENTORY_COUNT import net.sf.jasperreports.engine.JasperCompileManager @@ -82,6 +83,7 @@ open class StockInLineService( private val polRepository: PurchaseOrderLineRepository, private val qcItemsRepository: QcItemRepository, private val qcResultRepository: QcResultRepository, + private val qcResultMeasurementService: QcResultMeasurementService, private val escalationLogService: EscalationLogService, private val escalationLogRepository: EscalationLogRepository, private val stockInService: StockInService, @@ -514,7 +516,6 @@ open class StockInLineService( val qcResultEntries = request.qcResult!!.map { val qcItem = qcItemsRepository.findById(it.qcItemId).orElseThrow() val item = itemRepository.findById(stockInLine.item!!.id!!).orElseThrow() - val qcResult = QcResult(); QcResult().apply { this.qcItem = qcItem @@ -527,7 +528,11 @@ open class StockInLineService( this.escalationLog = escalationLogService.find(escLogId).getOrNull() } } - return qcResultRepository.saveAllAndFlush(qcResultEntries) + val savedResults = qcResultRepository.saveAllAndFlush(qcResultEntries) + request.qcResult!!.zip(savedResults).forEach { (req, saved) -> + qcResultMeasurementService.syncMeasurement(req, saved, stockInLine) + } + return savedResults } return null } diff --git a/src/main/resources/db/changelog/changes/20260622_01_qc_result_measurement/01_create_qc_result_measurement.sql b/src/main/resources/db/changelog/changes/20260622_01_qc_result_measurement/01_create_qc_result_measurement.sql new file mode 100644 index 0000000..399141c --- /dev/null +++ b/src/main/resources/db/changelog/changes/20260622_01_qc_result_measurement/01_create_qc_result_measurement.sql @@ -0,0 +1,25 @@ +--liquibase formatted sql + +--changeset fpsms:create qc_result_measurement +CREATE TABLE `qc_result_measurement` +( + `id` INT NOT NULL AUTO_INCREMENT, + `created` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `createdBy` VARCHAR(30) NULL, + `version` INT NOT NULL DEFAULT 0, + `modified` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + `modifiedBy` VARCHAR(30) NULL, + `deleted` TINYINT(1) NOT NULL DEFAULT 0, + `qcResultId` INT NOT NULL, + `stockInLineId` INT NULL, + `qcType` VARCHAR(45) NOT NULL, + `orderRefCode` VARCHAR(100) NULL, + `measureType` VARCHAR(45) NOT NULL, + `value` DECIMAL(14, 2) NULL, + `unit` VARCHAR(20) NULL, + CONSTRAINT PK_QC_RESULT_MEASUREMENT PRIMARY KEY (`id`), + CONSTRAINT FK_QCRM_TO_QC_RESULT_ON_QC_RESULT_ID + FOREIGN KEY (`qcResultId`) REFERENCES `qc_result` (`id`), + CONSTRAINT FK_QCRM_TO_STOCK_IN_LINE_ON_STOCK_IN_LINE_ID + FOREIGN KEY (`stockInLineId`) REFERENCES `stock_in_line` (`id`) +); diff --git a/src/main/resources/jasper/ItemQCReport.jrxml b/src/main/resources/jasper/ItemQCReport.jrxml index 5aa3a2e..a6c47ca 100644 --- a/src/main/resources/jasper/ItemQCReport.jrxml +++ b/src/main/resources/jasper/ItemQCReport.jrxml @@ -35,6 +35,12 @@ + + + + + + @@ -48,11 +54,12 @@ - + + @@ -109,7 +116,7 @@ - + @@ -271,7 +278,7 @@ - + @@ -280,6 +287,27 @@ + + + + + + + + + + + + + + + + + + + + + @@ -331,7 +359,7 @@ - +