Quellcode durchsuchen

QC添加溫度/濕度儲存

production
B.E.N.S.O.N vor 1 Tag
Ursprung
Commit
97a26b1a3b
16 geänderte Dateien mit 676 neuen und 138 gelöschten Zeilen
  1. +20
    -3
      src/main/java/com/ffii/fpsms/modules/common/mail/service/MailTemplateService.kt
  2. +11
    -0
      src/main/java/com/ffii/fpsms/modules/qc/entity/QcMeasureType.kt
  3. +46
    -0
      src/main/java/com/ffii/fpsms/modules/qc/entity/QcResultMeasurement.kt
  4. +11
    -0
      src/main/java/com/ffii/fpsms/modules/qc/entity/QcResultMeasurementRepository.kt
  5. +187
    -0
      src/main/java/com/ffii/fpsms/modules/qc/service/QcResultMeasurementService.kt
  6. +44
    -4
      src/main/java/com/ffii/fpsms/modules/qc/service/QcResultService.kt
  7. +3
    -3
      src/main/java/com/ffii/fpsms/modules/qc/web/QcResultController.kt
  8. +24
    -0
      src/main/java/com/ffii/fpsms/modules/qc/web/model/QcResultInfoResponse.kt
  9. +9
    -0
      src/main/java/com/ffii/fpsms/modules/qc/web/model/SaveQcMeasurementRequest.kt
  10. +1
    -0
      src/main/java/com/ffii/fpsms/modules/qc/web/model/SaveQcResultRequest.kt
  11. +102
    -30
      src/main/java/com/ffii/fpsms/modules/report/service/ItemQcFailReportService.kt
  12. +19
    -0
      src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt
  13. +135
    -92
      src/main/java/com/ffii/fpsms/modules/report/web/ItemQcFailReportController.kt
  14. +7
    -2
      src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt
  15. +25
    -0
      src/main/resources/db/changelog/changes/20260622_01_qc_result_measurement/01_create_qc_result_measurement.sql
  16. +32
    -4
      src/main/resources/jasper/ItemQCReport.jrxml

+ 20
- 3
src/main/java/com/ffii/fpsms/modules/common/mail/service/MailTemplateService.kt Datei anzeigen

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


+ 11
- 0
src/main/java/com/ffii/fpsms/modules/qc/entity/QcMeasureType.kt Datei anzeigen

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

+ 46
- 0
src/main/java/com/ffii/fpsms/modules/qc/entity/QcResultMeasurement.kt Datei anzeigen

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

+ 11
- 0
src/main/java/com/ffii/fpsms/modules/qc/entity/QcResultMeasurementRepository.kt Datei anzeigen

@@ -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<QcResultMeasurement, Long> {
fun findByQcResultIdAndMeasureTypeAndDeletedFalse(qcResultId: Long, measureType: String): QcResultMeasurement?

fun findAllByQcResultIdInAndDeletedFalse(qcResultIds: Collection<Long>): List<QcResultMeasurement>
}

+ 187
- 0
src/main/java/com/ffii/fpsms/modules/qc/service/QcResultMeasurementService.kt Datei anzeigen

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

+ 44
- 4
src/main/java/com/ffii/fpsms/modules/qc/service/QcResultService.kt Datei anzeigen

@@ -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<QcResultInfo> {
return qcResultRepository.findQcResultInfoByStockInLineIdAndDeletedFalse(stockInLineId)
open fun getAllQcResultInfoByStockInLineId(stockInLineId: Long): List<QcResultInfoResponse> {
return enrichWithMeasurements(
qcResultRepository.findQcResultInfoByStockInLineIdAndDeletedFalse(stockInLineId)
)
}
open fun getAllQcResultInfoByStockOutLineId(stockOutLineId: Long): List<QcResultInfoResponse> {
return enrichWithMeasurements(
qcResultRepository.findQcResultInfoByStockOutLineIdAndDeletedFalse(stockOutLineId)
)
}
open fun getAllQcResultInfoByStockOutLineId(stockOutLineId: Long): List<QcResultInfo> {
return qcResultRepository.findQcResultInfoByStockOutLineIdAndDeletedFalse(stockOutLineId)

private fun enrichWithMeasurements(results: List<QcResultInfo>): List<QcResultInfoResponse> {
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,
)
}
}
}

+ 3
- 3
src/main/java/com/ffii/fpsms/modules/qc/web/QcResultController.kt Datei anzeigen

@@ -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<QcResultInfo> {
fun getAllQcResultInfoByStockInLineId(@PathVariable stockInLineId: Long): List<QcResultInfoResponse> {
return qcResultService.getAllQcResultInfoByStockInLineId(stockInLineId)
}

@GetMapping("/pick-order/{stockOutLineId}")
fun getAllQcResultInfoByStockOutLineId(@PathVariable stockOutLineId: Long): List<QcResultInfo> {
fun getAllQcResultInfoByStockOutLineId(@PathVariable stockOutLineId: Long): List<QcResultInfoResponse> {
return qcResultService.getAllQcResultInfoByStockOutLineId(stockOutLineId)
}
}

+ 24
- 0
src/main/java/com/ffii/fpsms/modules/qc/web/model/QcResultInfoResponse.kt Datei anzeigen

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

+ 9
- 0
src/main/java/com/ffii/fpsms/modules/qc/web/model/SaveQcMeasurementRequest.kt Datei anzeigen

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

+ 1
- 0
src/main/java/com/ffii/fpsms/modules/qc/web/model/SaveQcResultRequest.kt Datei anzeigen

@@ -10,4 +10,5 @@ data class SaveQcResultRequest(
val qcPassed: Boolean,
val type: String?,
val remarks: String?,
val measurement: SaveQcMeasurementRequest? = null,
)

+ 102
- 30
src/main/java/com/ffii/fpsms/modules/report/service/ItemQcFailReportService.kt Datei anzeigen

@@ -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<Map<String, Any>> {
val measurable = resolveIncludeMeasurable(includeMeasurable)
val other = isTruthy(includeOther)
val scope = resolveMeasurableScope(measurableScope, measurable, other)

if (!measurable && !other) {
return emptyList()
}

val args = mutableMapOf<String, Any>()

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<String>()
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 ")})"
}
}
}

+ 19
- 0
src/main/java/com/ffii/fpsms/modules/report/service/ReportService.kt Datei anzeigen

@@ -1716,5 +1716,24 @@ return result
val jasperPrint = JasperFillManager.fillReport(jasperReport, params, dataSource)
return JasperExportManager.exportReportToPdf(jasperPrint)
}

fun mergePdfBytes(parts: List<ByteArray>): 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()
}
}

+ 135
- 92
src/main/java/com/ffii/fpsms/modules/report/web/ItemQcFailReportController.kt Datei anzeigen

@@ -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<ByteArray> {
val parameters = mutableMapOf<String, Any>()

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<ByteArray>()
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<ByteArray> {
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<Map<String, Any>>,
reportTitle: String,
private fun createItemQcPdf(
stockCategory: String?,
itemCode: String?,
lastInDateStart: String?,
lastInDateEnd: String?,
qcType: String?,
rows: List<Map<String, Any>>,
reportSection: String,
): ByteArray {
val parameters = mutableMapOf<String, Any>()
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<String, Any>): 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<Map<String, Any>>,
failedRows: List<Map<String, Any>>,
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<Map<String, Any>>,
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()
}
}



+ 7
- 2
src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt Datei anzeigen

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


+ 25
- 0
src/main/resources/db/changelog/changes/20260622_01_qc_result_measurement/01_create_qc_result_measurement.sql Datei anzeigen

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

+ 32
- 4
src/main/resources/jasper/ItemQCReport.jrxml Datei anzeigen

@@ -35,6 +35,12 @@
<parameter name="lastOutDateEnd" class="java.lang.String">
<parameterDescription><![CDATA["lastOutDateStart"]]></parameterDescription>
</parameter>
<parameter name="reportSection" class="java.lang.String">
<defaultValueExpression><![CDATA[""]]></defaultValueExpression>
</parameter>
<parameter name="qcTypeLabel" class="java.lang.String">
<defaultValueExpression><![CDATA["全部"]]></defaultValueExpression>
</parameter>
<queryString>
<![CDATA[]]>
</queryString>
@@ -48,11 +54,12 @@
<field name="qcDefectCriteria" class="java.lang.String"/>
<field name="lotQty" class="java.lang.String"/>
<field name="defectQty" class="java.lang.String"/>
<field name="refData" class="java.lang.String"/>
<field name="measuredValue" class="java.lang.String"/>
<field name="remark" class="java.lang.String"/>
<field name="orderRefNo" class="java.lang.String"/>
<field name="stockSubCategory" class="java.lang.String"/>
<group name="Group1" keepTogether="true">
<groupExpression><![CDATA[$F{itemNo}]]></groupExpression>
<groupHeader>
<band height="18">
<property name="com.jaspersoft.studio.layout" value="com.jaspersoft.studio.editor.layout.FreeLayout"/>
@@ -109,7 +116,7 @@
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="10"/>
</textElement>
<text><![CDATA[備注]]></text>
<text><![CDATA[實測值]]></text>
</staticText>
<staticText>
<reportElement x="90" y="80" width="70" height="18" uuid="7db4a800-8573-408c-baad-f4f4885625c9">
@@ -271,7 +278,7 @@
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="12" isBold="true"/>
</textElement>
<text><![CDATA[最後入倉日期:]]></text>
<text><![CDATA[QC 檢測日期:]]></text>
</staticText>
<textField>
<reportElement x="100" y="53" width="560" height="23" uuid="96afd73f-53ed-42cd-9911-fdaac017d65b"/>
@@ -280,6 +287,27 @@
</textElement>
<textFieldExpression><![CDATA[$P{lastInDateStart} + " 至 " + $P{lastInDateEnd}]]></textFieldExpression>
</textField>
<textField isBlankWhenNull="true">
<reportElement x="322" y="23" width="158" height="20" uuid="a1b2c3d4-e5f6-7890-abcd-ef1234567890"/>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="12" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$P{reportSection} != null && !$P{reportSection}.isEmpty() ? "(" + $P{reportSection} + ")" : ""]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="450" y="53" width="70" height="23" uuid="b2c3d4e5-f6a7-8901-bcde-f12345678901"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="12" isBold="true"/>
</textElement>
<text><![CDATA[QC 類型:]]></text>
</staticText>
<textField>
<reportElement x="520" y="53" width="80" height="23" uuid="c3d4e5f6-a7b8-9012-cdef-123456789012"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="12" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$P{qcTypeLabel}]]></textFieldExpression>
</textField>
</band>
</pageHeader>
<detail>
@@ -331,7 +359,7 @@
<textElement textAlignment="Center" verticalAlignment="Top">
<font fontName="微軟正黑體"/>
</textElement>
<textFieldExpression><![CDATA[$F{refData}]]></textFieldExpression>
<textFieldExpression><![CDATA[$F{measuredValue}]]></textFieldExpression>
</textField>
<textField textAdjust="StretchHeight">
<reportElement x="690" y="0" width="108" height="17" uuid="a5e52468-0ed0-4dfb-a305-2873273101c0"/>


Laden…
Abbrechen
Speichern