Преглед изворни кода

fixing the around 16:30 logout problem;

Handle user put DO (m18 PO) sync in m18 sync page PO tab;
make the fg/semi fg report count by date rather than by month,
created a button to make qc accept faster,
added item code/name in search criteria of PO,
make 2 new version of PO Item label printout (can set 1 to print the old version in setting)
production
Fai Luk пре 7 часа
родитељ
комит
2c6648ba14
17 измењених фајлова са 932 додато и 54 уклоњено
  1. +10
    -7
      src/main/java/com/ffii/core/utils/QrCodeUtil.kt
  2. +50
    -18
      src/main/java/com/ffii/fpsms/m18/service/M18DeliveryOrderService.kt
  3. +38
    -16
      src/main/java/com/ffii/fpsms/m18/service/M18PurchaseOrderService.kt
  4. +3
    -0
      src/main/java/com/ffii/fpsms/modules/common/SettingNames.java
  5. +13
    -2
      src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderService.kt
  6. +2
    -0
      src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/PurchaseOrderController.kt
  7. +117
    -0
      src/main/java/com/ffii/fpsms/modules/report/service/SemiFGProductionAnalysisReportService.kt
  8. +249
    -0
      src/main/java/com/ffii/fpsms/modules/report/web/SemiFGProductionAnalysisReportController.kt
  9. +11
    -4
      src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt
  10. +63
    -0
      src/main/java/com/ffii/fpsms/modules/stock/service/PoItemLabelTemplate.kt
  11. +11
    -4
      src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt
  12. +3
    -2
      src/main/resources/application-prod.yml
  13. +2
    -1
      src/main/resources/application.yml
  14. +9
    -0
      src/main/resources/db/changelog/changes/20261001_po_item_label_version/01_setting.sql
  15. +7
    -0
      src/main/resources/db/changelog/changes/20261001_po_item_label_version/02_allow_version_3.sql
  16. +172
    -0
      src/main/resources/qrCodeLabel/poItemPDF_v2.jrxml
  17. +172
    -0
      src/main/resources/qrCodeLabel/poItemPDF_v3.jrxml

+ 10
- 7
src/main/java/com/ffii/core/utils/QrCodeUtil.kt Прегледај датотеку

@@ -1,18 +1,21 @@
package com.ffii.core.utils

import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import java.awt.image.BufferedImage
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.client.j2se.MatrixToImageWriter
import com.google.zxing.qrcode.QRCodeWriter

open class QrCodeUtil {
companion object {
fun generateQRCodeImage(barcodeText: String?): BufferedImage {
val barcodeWriter = QRCodeWriter()
val bitMatrix: BitMatrix = barcodeWriter.encode(barcodeText, BarcodeFormat.QR_CODE, 250, 250)
val img: BufferedImage = MatrixToImageWriter.toBufferedImage(bitMatrix)
return img
return generateQRCodeImage(barcodeText, 250, 4)
}

fun generateQRCodeImage(barcodeText: String?, size: Int, margin: Int): BufferedImage {
val hints = hashMapOf<EncodeHintType, Any>(EncodeHintType.MARGIN to margin)
val bitMatrix = QRCodeWriter().encode(barcodeText, BarcodeFormat.QR_CODE, size, size, hints)
return MatrixToImageWriter.toBufferedImage(bitMatrix)
}
}
}

+ 50
- 18
src/main/java/com/ffii/fpsms/m18/service/M18DeliveryOrderService.kt Прегледај датотеку

@@ -89,14 +89,8 @@ open class M18DeliveryOrderService(
""
}
// Shop PO
val shopPoBuyers = commonUtils.listToString(listOf(m18Config.BEID_TOA), "beId=equal=", "=or=")
val shopPoSupplier = commonUtils.listToString(
shopService.findM18VendorIdsByCodeRegexp(m18Config.SHOP_PO_SUPPLIER),
"venId=equal=",
"=or="
)
var shopPoConds = "(${shopPoBuyers})=and=(${shopPoSupplier})"
// Shop PO — same BE/supplier scope as [buildShopPoScopeConds]
var shopPoConds = buildShopPoScopeConds()

if (request.modifiedDateFrom != null && request.modifiedDateTo != null) {
shopPoConds += "=and=(${lastDateConds})"
@@ -160,9 +154,24 @@ open class M18DeliveryOrderService(
)
}

/**
* Shop PO (MTMS DO) BE/supplier scope used by scheduler and by-code sync:
* beId = TOA and venId in [M18Config.SHOP_PO_SUPPLIER] (e.g. P06/P07).
*/
private fun buildShopPoScopeConds(): String {
val shopPoBuyers = commonUtils.listToString(listOf(m18Config.BEID_TOA), "beId=equal=", "=or=")
val shopPoSupplier = commonUtils.listToString(
shopService.findM18VendorIdsByCodeRegexp(m18Config.SHOP_PO_SUPPLIER),
"venId=equal=",
"=or="
)
return "(${shopPoBuyers})=and=(${shopPoSupplier})"
}

/**
* Sync a single M18 shop PO / delivery order by document [code], same search pattern as
* [com.ffii.fpsms.m18.service.M18PurchaseOrderService.savePurchaseOrderByCode].
* [com.ffii.fpsms.m18.service.M18PurchaseOrderService.savePurchaseOrderByCode], including
* the scheduler shop-PO BE/supplier filters (rejects material POs).
*
* @param isExtraSync when true, persist local `delivery_order.isExtra=true` (manual DO(加單) sync).
* No M18-side "加單" filtering is used.
@@ -173,15 +182,16 @@ open class M18DeliveryOrderService(
isExtraSync: Boolean = false,
newOnly: Boolean = false,
): SyncResult {
if (newOnly && deliveryOrderRepository.existsByCodeAndDeletedIsFalse(code)) {
val trimmed = code.trim()
if (newOnly && deliveryOrderRepository.existsByCodeAndDeletedIsFalse(trimmed)) {
return SyncResult(
totalProcessed = 1,
totalSuccess = 0,
totalFail = 0,
query = "skipped (newOnly=true): delivery_order.code already exists: $code",
query = "skipped (newOnly=true): delivery_order.code already exists: $trimmed",
)
}
val conds = "(code=equal=$code)"
val conds = "(code=equal=$trimmed)=and=${buildShopPoScopeConds()}"
val searchRequest = M18PurchaseOrderListRequest(
stSearch = "po",
params = null,
@@ -228,6 +238,7 @@ open class M18DeliveryOrderService(

val successList = mutableListOf<Long>()
val skippedList = mutableListOf<Long>()
val skippedNotPendingList = mutableListOf<String>()
val successDetailList = mutableListOf<Long>()
val failList = mutableListOf<Long>()
val failDetailList = mutableListOf<Long>()
@@ -291,6 +302,23 @@ open class M18DeliveryOrderService(
// logger.info("${doRefType}: Finding For Latest M18 Data Log...")
val latestDeliveryOrderLog =
m18DataLogService.findLatestM18DataLogWithSuccess(deliveryOrder.id, doRefType)
val existingDeliveryOrderForSync =
latestDeliveryOrderLog?.id?.let { deliveryOrderService.findByM18DataLogId(it) }
if (existingDeliveryOrderForSync != null &&
existingDeliveryOrderForSync.deleted != true &&
existingDeliveryOrderForSync.status != DeliveryOrderStatus.PENDING
) {
val statusValue = existingDeliveryOrderForSync.status?.value ?: "unknown"
logger.info(
"${doRefType}: Skipping M18 sync — local DO id=${existingDeliveryOrderForSync.id} " +
"code=${existingDeliveryOrderForSync.code} status=$statusValue " +
"(only pending may be overwritten; receiving=已放單). M18 ID: ${deliveryOrder.id}"
)
skippedNotPendingList.add(
"code=${existingDeliveryOrderForSync.code} status=$statusValue"
)
return@forEach
}
//logger.info(latestDeliveryOrderLog.toString())
// Save to m18_data_log table
// logger.info("${doRefType}: Saving for M18 Data Log...")
@@ -312,10 +340,9 @@ open class M18DeliveryOrderService(
// logger.info("${doRefType}: Saved M18 Data Log. ID: ${saveM18DeliveryOrderLog.id}")

try {
// Find the delivery_order if exist
// Find the delivery_order if exist (re-use lookup from pending guard above)
// logger.info("${doRefType}: Finding exising delivery order...")
val existingDeliveryOrder =
latestDeliveryOrderLog?.id?.let { deliveryOrderService.findByM18DataLogId(it) }
val existingDeliveryOrder = existingDeliveryOrderForSync
// logger.info("${doRefType}: Exising delivery order ID: ${existingDeliveryOrder?.id}")

// Save to delivery_order table
@@ -621,12 +648,17 @@ open class M18DeliveryOrderService(

logger.info("--------------------------------------------End - Saving M18 Delivery Order--------------------------------------------")

val skippedSuffix = if (skippedList.isNotEmpty()) " | skipped=${skippedList.size}" else ""
val skippedExistingSuffix = if (skippedList.isNotEmpty()) " | skipped=${skippedList.size}" else ""
val skippedNotPendingSuffix = if (skippedNotPendingList.isNotEmpty()) {
" | skipped (not pending, only pending may be overwritten): ${skippedNotPendingList.joinToString("; ")}"
} else {
""
}
return SyncResult(
totalProcessed = successList.size + failList.size + skippedList.size,
totalProcessed = successList.size + failList.size + skippedList.size + skippedNotPendingList.size,
totalSuccess = successList.size,
totalFail = failList.size,
query = (deliveryOrdersWithType?.query ?: "") + skippedSuffix,
query = (deliveryOrdersWithType?.query ?: "") + skippedExistingSuffix + skippedNotPendingSuffix,
)
}
}

+ 38
- 16
src/main/java/com/ffii/fpsms/m18/service/M18PurchaseOrderService.kt Прегледај датотеку

@@ -97,15 +97,8 @@ open class M18PurchaseOrderService(
""
}

// Material PO
val materialPoBuyers =
commonUtils.listToString(listOf(m18Config.BEID_PP, m18Config.BEID_PF), "beId=equal=", "=or=")
val materialPoSupplierNot = commonUtils.listToString(
shopService.findM18VendorIdsByCodeRegexp(m18Config.MATERIAL_PO_SUPPLIER_NOT),
"venId=unequal=",
"=or="
)
var materialPoConds = "(${materialPoBuyers})=and=(${materialPoSupplierNot}))"
// Material PO — same BE/supplier scope as [buildMaterialPoScopeConds]
var materialPoConds = buildMaterialPoScopeConds()
if (request.modifiedDateFrom != null && request.modifiedDateTo != null) {
materialPoConds += "=and=(${lastDateConds})"
@@ -233,11 +226,28 @@ open class M18PurchaseOrderService(
return if (beId?.toString() == m18Config.BEID_TOA) PurchaseOrderType.SHOP else PurchaseOrderType.MATERIAL
}

/**
* Material PO BE/supplier scope used by scheduler and by-code sync:
* beId in (PP, PF) and venId not in [M18Config.MATERIAL_PO_SUPPLIER_NOT] (e.g. P06/P07 shop suppliers).
*/
private fun buildMaterialPoScopeConds(): String {
val materialPoBuyers =
commonUtils.listToString(listOf(m18Config.BEID_PP, m18Config.BEID_PF), "beId=equal=", "=or=")
val materialPoSupplierNot = commonUtils.listToString(
shopService.findM18VendorIdsByCodeRegexp(m18Config.MATERIAL_PO_SUPPLIER_NOT),
"venId=unequal=",
"=or="
)
return "(${materialPoBuyers})=and=(${materialPoSupplierNot})"
}

open fun savePurchaseOrderByCode(code: String): SyncResult {
// Follow the scheduler's original approach:
// 1) use /search/search to find the PO id by code
// 2) then sync by PO id (this reuses the same create/update logic)
val conds = "(code=equal=$code)"
// Same approach as scheduler list sync, restricted to one document code:
// 1) /search/search with material-PO BE/supplier scope + code
// 2) sync by PO id (reuses create/update logic)
// Rejects shop POs (TOA / P06|P07) that belong to DO sync instead.
val trimmed = code.trim()
val conds = "(code=equal=$trimmed)=and=${buildMaterialPoScopeConds()}"
return saveFromPoListSearch(conds, searchPurchaseOrdersByConds(conds))
}

@@ -357,6 +367,7 @@ open class M18PurchaseOrderService(
val successDetailList = mutableListOf<Long>()
val failList = mutableListOf<Long>()
val failDetailList = mutableListOf<Long>()
val skippedNotPendingList = mutableListOf<String>()
val affectedItemIds = mutableSetOf<Long>()
val uomByM18IdCache = mutableMapOf<Long, com.ffii.fpsms.modules.master.entity.ItemUom?>()

@@ -398,11 +409,15 @@ open class M18PurchaseOrderService(
if (existingPurchaseOrderForSync != null &&
existingPurchaseOrderForSync.status != PurchaseOrderStatus.PENDING
) {
val statusValue = existingPurchaseOrderForSync.status?.value ?: "unknown"
logger.info(
"${poRefType}: Skipping M18 sync — local PO id=${existingPurchaseOrderForSync.id} " +
"code=${existingPurchaseOrderForSync.code} status=${existingPurchaseOrderForSync.status?.value} " +
"code=${existingPurchaseOrderForSync.code} status=$statusValue " +
"(only pending may be overwritten). M18 ID: $m18PurchaseOrderId"
)
skippedNotPendingList.add(
"code=${existingPurchaseOrderForSync.code} status=$statusValue"
)
return@forEach
}

@@ -729,11 +744,18 @@ open class M18PurchaseOrderService(

logger.info("--------------------------------------------End - Saving M18 Purchase Order--------------------------------------------")

val baseQuery = purchaseOrdersWithType?.query ?: ""
val skippedSuffix = if (skippedNotPendingList.isNotEmpty()) {
" | skipped (not pending, only pending may be overwritten): ${skippedNotPendingList.joinToString("; ")}"
} else {
""
}

return SyncResult(
totalProcessed = successList.size + failList.size,
totalProcessed = successList.size + failList.size + skippedNotPendingList.size,
totalSuccess = successList.size,
totalFail = failList.size,
query = purchaseOrdersWithType?.query ?: ""
query = baseQuery + skippedSuffix
)
}
}


+ 3
- 0
src/main/java/com/ffii/fpsms/modules/common/SettingNames.java Прегледај датотеку

@@ -135,4 +135,7 @@ public abstract class SettingNames {
/** JSON: last laser TCP send where printer returned receive (job order no., lot, itemId/stockInLineId, etc.) */
public static final String LASER_PRINT_LAST_RECEIVE_SUCCESS = "LASER_PRINT.lastReceiveSuccess";

/** PO / stock-in item label: 1 = original, 2 = enlarged, 3 = smaller QR with a larger item name. */
public static final String LABEL_PO_ITEM_VERSION = "LABEL.poItem.version";

}

+ 13
- 2
src/main/java/com/ffii/fpsms/modules/purchaseOrder/service/PurchaseOrderService.kt Прегледај датотеку

@@ -211,8 +211,19 @@ open fun getPoSummariesByIds(ids: List<Long>): List<PurchaseOrderSummary> {
sql.append(" group by po.id");
sql.append(" order by po.orderDate desc")
sql.append(" ) r")
if (args.containsKey("itemDetail")){
sql.append(" where r.itemDetail like :itemDetail ");
val outerFilters = mutableListOf<String>()
if (args.containsKey("itemDetail")) {
outerFilters.add("r.itemDetail like :itemDetail")
}
if (args.containsKey("itemCode")) {
outerFilters.add("r.itemCode like :itemCode")
}
if (args.containsKey("itemName")) {
outerFilters.add("r.itemName like :itemName")
}
if (outerFilters.isNotEmpty()) {
sql.append(" where ")
sql.append(outerFilters.joinToString(" AND "))
}
return sql.toString()
}


+ 2
- 0
src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/PurchaseOrderController.kt Прегледај датотеку

@@ -47,6 +47,8 @@ class PurchaseOrderController(
val criteriaArgs = CriteriaArgsBuilder.withRequest(request)
.addStringLike("supplier")
.addStringLike("code")
.addStringLike("itemCode")
.addStringLike("itemName")
.addString("status")
.addBoolean("escalated")
.addDate("orderDate")


+ 117
- 0
src/main/java/com/ffii/fpsms/modules/report/service/SemiFGProductionAnalysisReportService.kt Прегледај датотеку

@@ -2,6 +2,7 @@ package com.ffii.fpsms.modules.report.service

import org.springframework.stereotype.Service
import com.ffii.core.support.JdbcDao
import java.time.LocalDate

@Service
class SemiFGProductionAnalysisReportService(
@@ -199,6 +200,122 @@ class SemiFGProductionAnalysisReportService(
return jdbcDao.queryForList(sql, args)
}

/**
* Same filters as [searchSemiFGProductionAnalysisReport], one row per item and production date.
* Quantity is not pivoted; the Excel builder places each date in the requested range as its own column.
*/
fun searchSemiFGProductionAnalysisDaily(
stockCategory: String?,
stockSubCategory: String?,
itemCode: String?,
year: String?,
rangeStart: LocalDate,
rangeEnd: LocalDate,
): List<Map<String, Any>> {
val args = mutableMapOf<String, Any>()

val stockCategorySql = if (!itemCode.isNullOrBlank()) {
""
} else if (!stockCategory.isNullOrBlank() && stockCategory != "All" && !stockCategory.contains("All")) {
buildMultiValueExactClause(stockCategory, "it.type", "semiSc", args)
} else {
""
}
val stockSubCategorySql = buildMultiValueLikeClause(stockSubCategory, "ic.sub", "stockSubCategory", args)
val itemCodeSql = buildMultiValueLikeClause(itemCode, "it.code", "semiItem", args)

val yearSql = if (!year.isNullOrBlank() && year != "All") {
args["year"] = year
"AND YEAR(CASE WHEN si.productLotNo IS NOT NULL AND TRIM(si.productLotNo) <> '' THEN il.stockInDate ELSE si.productionDate END) = :year"
} else {
""
}

args["lastOutDateStart"] = rangeStart.toString()
args["lastOutDateEnd"] = rangeEnd.toString()
val lastOutDateStartSql =
"AND DATE(CASE WHEN si.productLotNo IS NOT NULL AND TRIM(si.productLotNo) <> '' THEN il.stockInDate ELSE si.productionDate END) >= DATE(:lastOutDateStart)"
val lastOutDateEndSql =
"AND DATE(CASE WHEN si.productLotNo IS NOT NULL AND TRIM(si.productLotNo) <> '' THEN il.stockInDate ELSE si.productionDate END) <= DATE(:lastOutDateEnd)"

val sql = """
WITH qr_agg AS (
SELECT
qr.stockInLineId,
MAX(CASE WHEN qr.qcPassed = 0 THEN 1 ELSE 0 END) AS qcFailed
FROM qc_result qr
WHERE qr.deleted = 0
GROUP BY qr.stockInLineId
),
base AS (
SELECT
COALESCE(it.code, '') AS itemNo,
COALESCE(it.name, '') AS itemName,
COALESCE(ic.sub, '') AS stockSubCategory,
COALESCE(uc.udfudesc, '') AS unitOfMeasure,
DATE(
CASE
WHEN si.productLotNo IS NOT NULL AND TRIM(si.productLotNo) <> '' THEN il.stockInDate
ELSE si.productionDate
END
) AS prodDate,
si.id AS stockInLineId,
CASE WHEN COALESCE(qr_agg.qcFailed, 0) = 1 THEN 0
ELSE COALESCE(si.acceptedQty, 0)
END AS linePutAwayQty
FROM stock_in_line si
INNER JOIN items it ON si.itemId = it.id
INNER JOIN bom b ON b.code = it.code AND b.deleted = false
LEFT JOIN inventory_lot il ON il.id = si.inventoryLotId AND il.deleted = false
LEFT JOIN qr_agg ON qr_agg.stockInLineId = si.id
LEFT JOIN item_category ic ON it.categoryId = ic.id
LEFT JOIN item_uom iu ON it.id = iu.itemId AND iu.stockUnit = true
LEFT JOIN uom_conversion uc ON iu.uomId = uc.id
WHERE si.deleted = false
AND si.productLotNo IS NOT NULL
AND TRIM(si.productLotNo) <> ''
AND (
CASE
WHEN si.productLotNo IS NOT NULL AND TRIM(si.productLotNo) <> '' THEN il.stockInDate
ELSE si.productionDate
END
) IS NOT NULL
AND si.jobOrderId IS NOT NULL
AND (si.status IS NULL OR si.status <> 'Pending')
$stockCategorySql
$stockSubCategorySql
$itemCodeSql
$yearSql
$lastOutDateStartSql
$lastOutDateEndSql
),
dedup AS (
SELECT
itemNo,
itemName,
stockSubCategory,
unitOfMeasure,
prodDate,
stockInLineId,
MAX(linePutAwayQty) AS linePutAwayQty
FROM base
GROUP BY itemNo, itemName, stockSubCategory, unitOfMeasure, prodDate, stockInLineId
)
SELECT
d.itemNo AS itemNo,
MAX(d.itemName) AS itemName,
MAX(d.unitOfMeasure) AS unitOfMeasure,
d.prodDate AS prodDate,
CAST(COALESCE(SUM(d.linePutAwayQty), 0) AS DECIMAL(18,2)) AS qty
FROM dedup d
GROUP BY d.itemNo, d.prodDate
HAVING COALESCE(SUM(d.linePutAwayQty), 0) > 0
ORDER BY d.itemNo, d.prodDate
""".trimIndent()

return jdbcDao.queryForList(sql, args)
}

/**
* Gets list of item codes (bom.code) with names based on stockCategory filter.
* Supports multiple categories separated by comma (e.g., "FG,WIP").


+ 249
- 0
src/main/java/com/ffii/fpsms/modules/report/web/SemiFGProductionAnalysisReportController.kt Прегледај датотеку

@@ -7,6 +7,7 @@ import java.io.ByteArrayOutputStream
import java.time.LocalDate
import java.time.LocalTime
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
import com.ffii.fpsms.modules.report.service.SemiFGProductionAnalysisReportService
import com.ffii.fpsms.modules.report.service.ReportService
import org.apache.poi.ss.usermodel.BorderStyle
@@ -173,6 +174,243 @@ class SemiFGProductionAnalysisReportController(
return ResponseEntity(excelBytes, headers, HttpStatus.OK)
}

@PostMapping("/print-semi-fg-production-analysis-excel-daily")
fun exportSemiFGProductionAnalysisReportExcelDaily(
@RequestBody(required = false) req: ReportItemFilterRequest?,
): ResponseEntity<ByteArray> {
val r = req ?: ReportItemFilterRequest()
val start = parseReportDate(r.lastOutDateStart)
val end = parseReportDate(r.lastOutDateEnd)
if (start == null || end == null) {
return jsonError(HttpStatus.BAD_REQUEST, "請先輸入完成生產日期(由、至)")
}
if (end.isBefore(start)) {
return jsonError(HttpStatus.BAD_REQUEST, "完成生產日期「由」不可晚於「至」")
}
val dayCount = ChronoUnit.DAYS.between(start, end) + 1
if (dayCount > MAX_DAILY_COLUMNS) {
return jsonError(HttpStatus.BAD_REQUEST, "日期範圍不可超過 $MAX_DAILY_COLUMNS 日")
}

val dbData = semiFGProductionAnalysisReportService.searchSemiFGProductionAnalysisDaily(
r.stockCategory,
r.stockSubCategory,
r.combinedItemCode(),
r.year,
start,
end,
)
val dates = generateSequence(start) { previous ->
val next = previous.plusDays(1)
if (next.isAfter(end)) null else next
}.toList()
val excelBytes = createSemiFGProductionAnalysisDailyExcel(
dbData = dbData,
reportTitle = "成品/半成品生產分析報告",
dates = dates,
)
val headers = HttpHeaders().apply {
contentType = MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
setContentDispositionFormData("attachment", "SemiFGProductionAnalysisReport_Daily.xlsx")
set("filename", "SemiFGProductionAnalysisReport_Daily.xlsx")
}
return ResponseEntity(excelBytes, headers, HttpStatus.OK)
}

private fun createSemiFGProductionAnalysisDailyExcel(
dbData: List<Map<String, Any>>,
reportTitle: String,
dates: List<LocalDate>,
): ByteArray {
val workbook = XSSFWorkbook()
val safeSheetName = WorkbookUtil.createSafeSheetName(reportTitle)
val sheet = workbook.createSheet(safeSheetName)
val dateFormat = DateTimeFormatter.ISO_LOCAL_DATE
val columnDateFormat = DateTimeFormatter.ofPattern(
if (dates.first().year == dates.last().year) "d/M" else "d/M/yy",
)
val fixedColumns = 3
val totalColumns = fixedColumns + dates.size + 1
var rowIndex = 0

val titleStyle = workbook.createCellStyle().apply {
alignment = HorizontalAlignment.CENTER
verticalAlignment = VerticalAlignment.CENTER
}
val titleFont = workbook.createFont().apply {
bold = true
fontHeightInPoints = 16
}
titleStyle.setFont(titleFont)

val infoStyle = workbook.createCellStyle().apply {
alignment = HorizontalAlignment.LEFT
verticalAlignment = VerticalAlignment.CENTER
}

val headerStyle = workbook.createCellStyle().apply {
alignment = HorizontalAlignment.CENTER
verticalAlignment = VerticalAlignment.CENTER
fillForegroundColor = IndexedColors.GREY_25_PERCENT.index
fillPattern = FillPatternType.SOLID_FOREGROUND
borderTop = BorderStyle.THIN
borderBottom = BorderStyle.THIN
borderLeft = BorderStyle.THIN
borderRight = BorderStyle.THIN
wrapText = true
}
val headerFont = workbook.createFont().apply { bold = true }
headerStyle.setFont(headerFont)

val textStyle = workbook.createCellStyle().apply {
alignment = HorizontalAlignment.LEFT
verticalAlignment = VerticalAlignment.CENTER
borderTop = BorderStyle.THIN
borderBottom = BorderStyle.THIN
borderLeft = BorderStyle.THIN
borderRight = BorderStyle.THIN
}

val numberStyle = workbook.createCellStyle().apply {
alignment = HorizontalAlignment.RIGHT
verticalAlignment = VerticalAlignment.CENTER
borderTop = BorderStyle.THIN
borderBottom = BorderStyle.THIN
borderLeft = BorderStyle.THIN
borderRight = BorderStyle.THIN
dataFormat = workbook.creationHelper.createDataFormat().getFormat("#,##0")
}

val dashStyle = workbook.createCellStyle().apply {
alignment = HorizontalAlignment.RIGHT
verticalAlignment = VerticalAlignment.CENTER
borderTop = BorderStyle.THIN
borderBottom = BorderStyle.THIN
borderLeft = BorderStyle.THIN
borderRight = BorderStyle.THIN
}

val titleRow = sheet.createRow(rowIndex++)
titleRow.heightInPoints = 28f
val titleCell = titleRow.createCell(0)
titleCell.setCellValue(reportTitle)
titleCell.cellStyle = titleStyle
if (totalColumns > 1) {
sheet.addMergedRegion(CellRangeAddress(0, 0, 0, totalColumns - 1))
}

val rangeLabel = "${dates.first().format(dateFormat)} 至 ${dates.last().format(dateFormat)}"
val infoRow = sheet.createRow(rowIndex++)
val infoCell = infoRow.createCell(0)
infoCell.setCellValue(
"報告日期:${LocalDate.now().format(dateFormat)} " +
"報告時間:${LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss"))} " +
"完成生產日期:$rangeLabel"
)
infoCell.cellStyle = infoStyle
if (totalColumns > 1) {
sheet.addMergedRegion(CellRangeAddress(1, 1, 0, totalColumns - 1))
}

rowIndex++

val headerRowIndex = rowIndex
val headerRow = sheet.createRow(rowIndex++)
headerRow.heightInPoints = 32f
val headers = buildList {
add("貨品編號")
add("貨品名稱")
add("單位")
dates.forEach { add(it.format(columnDateFormat)) }
add("上架總計")
}
headers.forEachIndexed { index, name ->
val cell = headerRow.createCell(index)
cell.setCellValue(name)
cell.cellStyle = headerStyle
}

val items = pivotDailyRows(dbData)
if (items.isEmpty()) {
val dataRow = sheet.createRow(rowIndex++)
setTextCell(dataRow, 0, "-", textStyle)
setTextCell(dataRow, 1, "-", textStyle)
setTextCell(dataRow, 2, "-", textStyle)
for (col in fixedColumns until totalColumns) {
setDashCell(dataRow, col, dashStyle)
}
} else {
items.forEach { item ->
val dataRow = sheet.createRow(rowIndex++)
setTextCell(dataRow, 0, item.itemNo.ifBlank { "-" }, textStyle)
setTextCell(dataRow, 1, item.itemName.ifBlank { "-" }, textStyle)
setTextCell(dataRow, 2, item.unitOfMeasure.ifBlank { "-" }, textStyle)
var total = 0.0
dates.forEachIndexed { index, date ->
val qty = item.qtyByDate[date] ?: 0.0
total += qty
setNumberCell(dataRow, fixedColumns + index, qty, numberStyle, dashStyle)
}
setNumberCell(dataRow, totalColumns - 1, total, numberStyle, dashStyle)
}
}

sheet.createFreezePane(fixedColumns, headerRowIndex + 1)
sheet.setColumnWidth(0, 18 * 256)
sheet.setColumnWidth(1, 24 * 256)
sheet.setColumnWidth(2, 12 * 256)
for (col in fixedColumns until totalColumns) {
sheet.setColumnWidth(col, 12 * 256)
}

val output = ByteArrayOutputStream()
workbook.use { it.write(output) }
return output.toByteArray()
}

private fun pivotDailyRows(dbData: List<Map<String, Any>>): List<DailyItemQty> {
val byItem = linkedMapOf<String, DailyItemQty>()
dbData.forEach { row ->
val itemNo = row["itemNo"]?.toString()?.trim().orEmpty()
val prodDate = toLocalDate(row["prodDate"]) ?: return@forEach
val qty = parseNumericForExcelChart(row["qty"])
val item = byItem.getOrPut(itemNo) {
DailyItemQty(
itemNo = itemNo,
itemName = row["itemName"]?.toString().orEmpty(),
unitOfMeasure = row["unitOfMeasure"]?.toString().orEmpty(),
qtyByDate = linkedMapOf(),
)
}
item.qtyByDate[prodDate] = (item.qtyByDate[prodDate] ?: 0.0) + qty
}
return byItem.values.toList()
}

private fun parseReportDate(raw: String?): LocalDate? {
if (raw.isNullOrBlank()) return null
val normalized = raw.trim().replace("/", "-")
return runCatching { LocalDate.parse(normalized) }.getOrNull()
?: runCatching { LocalDate.parse(normalized, DateTimeFormatter.ofPattern("dd-MM-yyyy")) }.getOrNull()
}

private fun toLocalDate(value: Any?): LocalDate? = when (value) {
is LocalDate -> value
is java.sql.Date -> value.toLocalDate()
is java.sql.Timestamp -> value.toLocalDateTime().toLocalDate()
is java.time.LocalDateTime -> value.toLocalDate()
is java.util.Date -> value.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate()
is String -> parseReportDate(value.take(10))
else -> null
}

private fun jsonError(status: HttpStatus, message: String): ResponseEntity<ByteArray> {
val escaped = message.replace("\\", "\\\\").replace("\"", "\\\"")
return ResponseEntity.status(status)
.contentType(MediaType.APPLICATION_JSON)
.body("""{"message":"$escaped"}""".toByteArray(Charsets.UTF_8))
}

private fun createSemiFGProductionAnalysisExcel(
dbData: List<Map<String, Any>>,
reportTitle: String,
@@ -517,4 +755,15 @@ class SemiFGProductionAnalysisReportController(
val itemCodesWithCategory = semiFGProductionAnalysisReportService.getSemiFGItemCodesWithCategory(stockCategory)
return ResponseEntity(itemCodesWithCategory, HttpStatus.OK)
}

private data class DailyItemQty(
val itemNo: String,
val itemName: String,
val unitOfMeasure: String,
val qtyByDate: MutableMap<LocalDate, Double>,
)

companion object {
private const val MAX_DAILY_COLUMNS = 366
}
}

+ 11
- 4
src/main/java/com/ffii/fpsms/modules/stock/service/InventoryLotLineService.kt Прегледај датотеку

@@ -2,7 +2,6 @@ package com.ffii.fpsms.modules.stock.service

import com.ffii.core.response.RecordsRes
import com.ffii.core.utils.PdfUtils
import com.ffii.core.utils.QrCodeUtil
import com.ffii.fpsms.modules.master.entity.ItemUomRespository
import com.ffii.fpsms.modules.master.entity.WarehouseRepository
import com.ffii.fpsms.modules.stock.entity.InventoryLotLine
@@ -44,6 +43,7 @@ import com.ffii.fpsms.modules.stock.web.model.WorkbenchItemLotsResponse
import com.ffii.fpsms.modules.jobOrder.service.JobOrderService
import com.ffii.fpsms.modules.jobOrder.web.model.ExportFGStockInLabelRequest
import com.ffii.fpsms.modules.master.service.PrinterService
import com.ffii.fpsms.modules.settings.service.SettingsService
import com.ffii.fpsms.modules.stock.web.model.PrintLabelForInventoryLotLineRequest
import com.ffii.fpsms.modules.jobOrder.web.model.PrintFGStockInLabelRequest
import com.ffii.core.utils.ZebraPrinterUtil
@@ -60,6 +60,7 @@ open class InventoryLotLineService(
private val stockInLineRepository: StockInLineRepository,
private val inventoryRepository: InventoryRepository,
private val printerService: PrinterService,
private val settingsService: SettingsService,
@Lazy
private val jobOrderService: JobOrderService
) {
@@ -247,7 +248,8 @@ open class InventoryLotLineService(
@Throws(IOException::class)
@Transactional
open fun exportStockInLineQrcode(request: LotLineToQrcode): Map<String, Any> {
val QRCODE_PDF = "qrCodeLabel/poItemPDF.jrxml"
val labelVersion = PoItemLabelTemplate.version(settingsService)
val QRCODE_PDF = PoItemLabelTemplate.path(labelVersion)
val resource = ClassPathResource(QRCODE_PDF)
if (!resource.exists()) {
println("here")
@@ -287,13 +289,18 @@ open class InventoryLotLineService(
field["acceptedQty"] = "%.2f".format(info.acceptedQty)

val stockItemUom = itemUomRespository.findBaseUnitByItemIdAndStockUnitIsTrueAndDeletedIsFalse(info.itemId)
field["uom"] = stockItemUom?.uom?.udfudesc ?: info.uom?.udfudesc?.toString() ?: "N/A"
val fullUom = stockItemUom?.uom?.udfudesc ?: info.uom?.udfudesc?.toString() ?: "N/A"
val shortUom = stockItemUom?.uom?.udfShortDesc ?: info.uom?.udfShortDesc
field["uom"] = fullUom
val (qtyLabel, qtyDetail) = PoItemLabelTemplate.qtyLines(field["acceptedQty"] as String, fullUom, shortUom)
field["qtyLabel"] = qtyLabel
field["qtyDetail"] = qtyDetail

field["productionDate"] = info.productionDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: ""
field["expiryDate"] = info.expiryDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: ""
field["lotNo"] = info.lotNo!!
field["supplier"] = info.supplier ?: "N/A"
val image = QrCodeUtil.generateQRCodeImage(qrCodeContent)
val image = PoItemLabelTemplate.qrImage(qrCodeContent, labelVersion)
field["qrCode"] = image
fields.add(field)
}


+ 63
- 0
src/main/java/com/ffii/fpsms/modules/stock/service/PoItemLabelTemplate.kt Прегледај датотеку

@@ -0,0 +1,63 @@
package com.ffii.fpsms.modules.stock.service

import com.ffii.core.utils.QrCodeUtil
import com.ffii.fpsms.modules.common.SettingNames
import com.ffii.fpsms.modules.settings.service.SettingsService
import java.awt.image.BufferedImage

object PoItemLabelTemplate {
const val V1 = "qrCodeLabel/poItemPDF.jrxml"
const val V2 = "qrCodeLabel/poItemPDF_v2.jrxml"
const val V3 = "qrCodeLabel/poItemPDF_v3.jrxml"

fun version(settingsService: SettingsService): Int {
val raw = settingsService.findByName(SettingNames.LABEL_PO_ITEM_VERSION)
.map { it.value }
.orElse("1")
?.trim()
return when (raw) {
"2" -> 2
"3" -> 3
else -> 1
}
}

fun path(version: Int): String = when (version) {
2 -> V2
3 -> V3
else -> V1
}

fun qrImage(content: String, version: Int): BufferedImage =
if (version == 1) QrCodeUtil.generateQRCodeImage(content)
else QrCodeUtil.generateQRCodeImage(content, 500, 1)

/** First line is qty + short unit. Second line is the full unit only when it differs. */
fun qtyLines(qtyRaw: String?, fullRaw: String?, shortRaw: String?): Pair<String, String> {
val qty = stripTrailingZeros(qtyRaw)
val full = blankUnit(fullRaw)
val short = blankUnit(shortRaw)
val same = short.isEmpty() || full.isEmpty() || short.equals(full, ignoreCase = true)
if (same) {
val unit = short.ifEmpty { full }
val line = when {
unit.isEmpty() -> qty
unit.first().isDigit() -> listOf(qty, unit).filter { it.isNotEmpty() }.joinToString(" ")
else -> qty + unit
}
return line to ""
}
return (qty + short) to full
}

private fun blankUnit(raw: String?): String {
val text = raw?.trim().orEmpty()
return if (text.isEmpty() || text.equals("N/A", ignoreCase = true)) "" else text
}

private fun stripTrailingZeros(raw: String?): String {
val text = raw?.trim().orEmpty()
if (!text.contains('.')) return text
return text.replace(Regex("0+$"), "").removeSuffix(".")
}
}

+ 11
- 4
src/main/java/com/ffii/fpsms/modules/stock/service/StockInLineService.kt Прегледај датотеку

@@ -3,7 +3,6 @@ package com.ffii.fpsms.modules.stock.service
import com.ffii.core.exception.BadRequestException
import com.ffii.core.support.AbstractBaseEntityService
import com.ffii.core.support.JdbcDao
import com.ffii.core.utils.QrCodeUtil
import com.ffii.fpsms.modules.common.CodeGenerator
import com.ffii.fpsms.modules.master.entity.ItemsRepository
import com.ffii.fpsms.modules.master.entity.QcItemRepository
@@ -33,6 +32,7 @@ import com.ffii.fpsms.modules.master.entity.WarehouseRepository
import com.ffii.fpsms.modules.master.service.ItemUomService
//import com.ffii.fpsms.modules.master.service.BomOutputQtyService
import com.ffii.fpsms.modules.master.service.PrinterService
import com.ffii.fpsms.modules.settings.service.SettingsService
import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrder
import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderLine
import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderRepository
@@ -98,6 +98,7 @@ open class StockInLineService(
private val itemUomRepository: ItemUomRespository,
private val itemUomService: ItemUomService,
private val printerService: PrinterService,
private val settingsService: SettingsService,
private val stockTakeLineRepository: StockTakeLineRepository,
private val inventoryLotLineService: InventoryLotLineService,
private val deliveryOrderRepository: DeliveryOrderRepository,
@@ -1208,7 +1209,8 @@ open class StockInLineService(
@Throws(IOException::class)
@Transactional
open fun exportStockInLineQrcode(request: ExportQrCodeRequest): Map<String, Any> {
val QRCODE_PDF = "qrCodeLabel/poItemPDF.jrxml"
val labelVersion = PoItemLabelTemplate.version(settingsService)
val QRCODE_PDF = PoItemLabelTemplate.path(labelVersion)
val resource = ClassPathResource(QRCODE_PDF)
if (!resource.exists()) {
println("here")
@@ -1242,12 +1244,17 @@ open class StockInLineService(
field["poCode"] = info.poCode ?: "N/A"
field["itemType"] = info.itemType ?: "N/A"
field["acceptedQty"] = "%.2f".format(acceptedQty)
field["uom"] = stockItemUom?.uom?.udfudesc ?: info.uom?.udfudesc?.toString() ?: "N/A"
val fullUom = stockItemUom?.uom?.udfudesc ?: info.uom?.udfudesc?.toString() ?: "N/A"
val shortUom = stockItemUom?.uom?.udfShortDesc ?: info.uom?.udfShortDesc
field["uom"] = fullUom
val (qtyLabel, qtyDetail) = PoItemLabelTemplate.qtyLines(field["acceptedQty"] as String, fullUom, shortUom)
field["qtyLabel"] = qtyLabel
field["qtyDetail"] = qtyDetail
field["productionDate"] = info.productionDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: ""
field["expiryDate"] = info.expiryDate?.format(DateTimeFormatter.ISO_LOCAL_DATE) ?: ""
field["lotNo"] = info.lotNo ?: "N/A"
field["supplier"] = info.supplier ?: "N/A"
val image = QrCodeUtil.generateQRCodeImage(qrCodeContent)
val image = PoItemLabelTemplate.qrImage(qrCodeContent, labelVersion)
field["qrCode"] = image
fields.add(field)
}


+ 3
- 2
src/main/resources/application-prod.yml Прегледај датотеку

@@ -3,9 +3,10 @@ fpsms:
monitoring:
enabled: true

# Session length in production; frontend can call /refresh-token before expiry to stay logged in.
# Session length in production; frontend refreshes via /refresh-token before expiry.
# Floor staff often work ~08:00–22:00 (~14h); 16h access covers one shift without mid-day kickout.
jwt:
expiration-minutes: 480 # 8 hours access token (was 30 min); increase if users need longer AFK
expiration-minutes: 960 # 16 hours access token (was 480 / 8h — users logged out ~16:30 after morning login)
refresh-expiration-days: 7
# Required for stable JWT across restarts; set JWT_SECRET in the deployment environment (long random string).
secret: ${JWT_SECRET:fpsms-dev-jwt-signing-secret-change-for-production-use-long-random-JWT_SECRET}


+ 2
- 1
src/main/resources/application.yml Прегледај датотеку

@@ -113,8 +113,9 @@ spring:
dialect:
storage_engine: innodb

# JWT: access token expiry and refresh token expiry. Frontend should call /refresh-token before access token expires.
# JWT: access token expiry and refresh token expiry. Frontend calls /refresh-token before access token expires.
# Signing key must be STABLE across server restarts (do not use a random key per boot). Override with env JWT_SECRET in production.
# Prod overrides to 16h access (floor shifts ~08:00–22:00); see application-prod.yml.
jwt:
expiration-minutes: 14400 # access token: 10 days (default); override in application-prod for shorter session
refresh-expiration-days: 30 # refresh token validity (days)


+ 9
- 0
src/main/resources/db/changelog/changes/20261001_po_item_label_version/01_setting.sql Прегледај датотеку

@@ -0,0 +1,9 @@
--liquibase formatted sql
--changeset fpsms:20261001_po_item_label_version

INSERT INTO `settings` (`name`, `value`, `category`, `type`)
SELECT 'LABEL.poItem.version', '1', 'LABEL', '1/2'
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM `settings` WHERE `name` = 'LABEL.poItem.version'
);

+ 7
- 0
src/main/resources/db/changelog/changes/20261001_po_item_label_version/02_allow_version_3.sql Прегледај датотеку

@@ -0,0 +1,7 @@
--liquibase formatted sql
--changeset fpsms:20261001_po_item_label_version_allow_3

UPDATE `settings`
SET `type` = '1/2/3'
WHERE `name` = 'LABEL.poItem.version'
AND `type` = '1/2';

+ 172
- 0
src/main/resources/qrCodeLabel/poItemPDF_v2.jrxml Прегледај датотеку

@@ -0,0 +1,172 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Jaspersoft Studio version 6.21.3.final using JasperReports Library version 6.21.3-4a3078d20785ebe464f18037d738d12fc98c13cf -->
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="poItemPDF" printOrder="Horizontal" pageWidth="425" pageHeight="283" orientation="Landscape" columnWidth="425" leftMargin="0" rightMargin="0" topMargin="0" bottomMargin="0" uuid="6cb85eb9-3573-42f3-a293-099db250aec0">
<property name="com.jaspersoft.studio.unit." value="pixel"/>
<parameter name="lotNo" class="java.lang.String"/>
<parameter name="itemName" class="java.lang.String"/>
<parameter name="itemNo" class="java.lang.String"/>
<parameter name="poCode" class="java.lang.String"/>
<parameter name="qrCode" class="java.awt.Image"/>
<parameter name="acceptedQty" class="java.math.BigDecimal"/>
<parameter name="uom" class="java.lang.String"/>
<parameter name="isTransfer" class="java.lang.String"/>
<queryString>
<![CDATA[]]>
</queryString>
<field name="lotNo" class="java.lang.String"/>
<field name="itemName" class="java.lang.String"/>
<field name="itemNo" class="java.lang.String"/>
<field name="acceptedQty" class="java.lang.String"/>
<field name="uom" class="java.lang.String"/>
<field name="qtyLabel" class="java.lang.String"/>
<field name="qtyDetail" class="java.lang.String"/>
<field name="qrCode" class="java.awt.Image"/>
<field name="poCode" class="java.lang.String"/>
<field name="supplier" class="java.lang.String"/>
<field name="productionDate" class="java.lang.String"/>
<field name="expiryDate" class="java.lang.String"/>
<background>
<band splitType="Stretch"/>
</background>
<detail>
<band height="279" splitType="Prevent">
<staticText>
<reportElement x="8" y="0" width="108" height="56" uuid="c62ac00a-c495-462c-93bc-97bdd3cdb395"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="18" isBold="true"/>
</textElement>
<text><![CDATA[收貨日期:]]></text>
</staticText>
<textField textAdjust="ScaleFont" isBlankWhenNull="true">
<reportElement x="116" y="0" width="301" height="56" uuid="74a5cfba-046d-42f4-92d7-113729514a6f"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="44" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$F{productionDate}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="8" y="56" width="108" height="56" uuid="32edcad1-23c1-4d41-8c9a-352d9e6ef377"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="18" isBold="true"/>
</textElement>
<text><![CDATA[到期日:]]></text>
</staticText>
<textField textAdjust="ScaleFont" isBlankWhenNull="true">
<reportElement x="116" y="56" width="301" height="56" uuid="97e4a9bb-fc84-4074-b523-ddf9eb9ed2d2"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="44" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$F{expiryDate}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="8" y="114" width="230" height="18" uuid="f5f145f1-123f-4fe5-90fa-8550b3d2eb63"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="10" isBold="true"/>
</textElement>
<text><![CDATA[批次編號:]]></text>
</staticText>
<textField textAdjust="ScaleFont" isBlankWhenNull="true">
<reportElement x="8" y="132" width="230" height="20" uuid="402f35fc-b11a-4bf1-982d-e12c622f38ea"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="18" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$F{lotNo}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="8" y="152" width="108" height="18" uuid="96d46fb1-939d-4f3e-b8f0-172b69b85e9f"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="10" isBold="true"/>
</textElement>
<text><![CDATA[物料編號:]]></text>
</staticText>
<staticText>
<reportElement x="120" y="152" width="118" height="18" uuid="53a2d7cf-bd50-4264-abee-346823cd49dd"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="10" isBold="true"/>
</textElement>
<text><![CDATA[數量:]]></text>
</staticText>
<textField textAdjust="ScaleFont" isBlankWhenNull="true">
<reportElement x="8" y="170" width="108" height="28" uuid="1f289946-b945-4f79-8515-212368b8be3b"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="24" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$F{itemNo}]]></textFieldExpression>
</textField>
<textField textAdjust="ScaleFont" isBlankWhenNull="true">
<reportElement x="120" y="170" width="120" height="18" uuid="629d9af8-c73f-4614-ba9d-a582e2ed4a29"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="14" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$F{qtyLabel}]]></textFieldExpression>
</textField>
<textField textAdjust="ScaleFont" isBlankWhenNull="true">
<reportElement x="120" y="188" width="120" height="12" uuid="7c1a0e44-6b2d-4a91-9f10-2d8e5b6c4a11">
<printWhenExpression><![CDATA[$F{qtyDetail} != null && !$F{qtyDetail}.trim().isEmpty()]]></printWhenExpression>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="8"/>
</textElement>
<textFieldExpression><![CDATA[$F{qtyDetail}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="8" y="200" width="230" height="16" uuid="5c3f9b03-222c-4c94-ab58-6bc86d44c5ce"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="10" isBold="true"/>
</textElement>
<text><![CDATA[物料名稱:]]></text>
</staticText>
<textField textAdjust="ScaleFont" isBlankWhenNull="true">
<reportElement x="8" y="216" width="230" height="30" uuid="e04b094d-2d16-4060-b6d9-2e6033a673ab"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="32" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$F{itemName}]]></textFieldExpression>
</textField>
<image scaleImage="FillFrame" hAlign="Center" vAlign="Middle">
<reportElement x="248" y="112" width="166" height="166" uuid="5dd35c73-973b-4f0d-9cad-13305a29a9fd"/>
<imageExpression><![CDATA[$F{qrCode}]]></imageExpression>
</image>
<staticText>
<reportElement x="8" y="248" width="42" height="14" uuid="c2041205-51f8-45bb-a1d1-e3de37eebed7"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="8"/>
</textElement>
<text><![CDATA[供應商:]]></text>
</staticText>
<textField textAdjust="ScaleFont" isBlankWhenNull="true">
<reportElement x="50" y="248" width="188" height="14" uuid="ab82ca39-8840-4471-b7c0-cdeec9cb5693"/>
<textElement verticalAlignment="Middle">
<font fontName="微軟正黑體" size="8"/>
</textElement>
<textFieldExpression><![CDATA[$F{supplier}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="8" y="262" width="76" height="14" uuid="1b1efafd-049a-432e-93c4-9662273011f7"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="8"/>
</textElement>
<text><![CDATA[採購訂單編號:]]></text>
</staticText>
<textField textAdjust="ScaleFont" isBlankWhenNull="true">
<reportElement x="84" y="262" width="154" height="14" uuid="b805e20a-9a36-45c8-8255-23b6cc58f47c"/>
<textElement verticalAlignment="Middle">
<font fontName="微軟正黑體" size="8"/>
</textElement>
<textFieldExpression><![CDATA[$P{poCode}]]></textFieldExpression>
</textField>
<textField>
<reportElement x="276" y="183" width="110" height="22" uuid="d66a64b0-3335-4846-bcb0-a67594520055">
<printWhenExpression><![CDATA[$P{isTransfer} != null && !$P{isTransfer}.toString().trim().isEmpty()]]></printWhenExpression>
</reportElement>
<box>
<pen lineWidth="2.0"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="11" isBold="true" isUnderline="true"/>
</textElement>
<textFieldExpression><![CDATA[$P{isTransfer}]]></textFieldExpression>
</textField>
</band>
</detail>
</jasperReport>

+ 172
- 0
src/main/resources/qrCodeLabel/poItemPDF_v3.jrxml Прегледај датотеку

@@ -0,0 +1,172 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Jaspersoft Studio version 6.21.3.final using JasperReports Library version 6.21.3-4a3078d20785ebe464f18037d738d12fc98c13cf -->
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="poItemPDF_v3" printOrder="Horizontal" pageWidth="425" pageHeight="283" orientation="Landscape" columnWidth="425" leftMargin="0" rightMargin="0" topMargin="0" bottomMargin="0" uuid="6cb85eb9-3573-42f3-a293-099db250aec0">
<property name="com.jaspersoft.studio.unit." value="pixel"/>
<parameter name="lotNo" class="java.lang.String"/>
<parameter name="itemName" class="java.lang.String"/>
<parameter name="itemNo" class="java.lang.String"/>
<parameter name="poCode" class="java.lang.String"/>
<parameter name="qrCode" class="java.awt.Image"/>
<parameter name="acceptedQty" class="java.math.BigDecimal"/>
<parameter name="uom" class="java.lang.String"/>
<parameter name="isTransfer" class="java.lang.String"/>
<queryString>
<![CDATA[]]>
</queryString>
<field name="lotNo" class="java.lang.String"/>
<field name="itemName" class="java.lang.String"/>
<field name="itemNo" class="java.lang.String"/>
<field name="acceptedQty" class="java.lang.String"/>
<field name="uom" class="java.lang.String"/>
<field name="qtyLabel" class="java.lang.String"/>
<field name="qtyDetail" class="java.lang.String"/>
<field name="qrCode" class="java.awt.Image"/>
<field name="poCode" class="java.lang.String"/>
<field name="supplier" class="java.lang.String"/>
<field name="productionDate" class="java.lang.String"/>
<field name="expiryDate" class="java.lang.String"/>
<background>
<band splitType="Stretch"/>
</background>
<detail>
<band height="279" splitType="Prevent">
<staticText>
<reportElement x="8" y="0" width="108" height="56" uuid="c62ac00a-c495-462c-93bc-97bdd3cdb395"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="18" isBold="true"/>
</textElement>
<text><![CDATA[收貨日期:]]></text>
</staticText>
<textField textAdjust="ScaleFont" isBlankWhenNull="true">
<reportElement x="116" y="0" width="301" height="56" uuid="74a5cfba-046d-42f4-92d7-113729514a6f"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="44" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$F{productionDate}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="8" y="56" width="108" height="56" uuid="32edcad1-23c1-4d41-8c9a-352d9e6ef377"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="18" isBold="true"/>
</textElement>
<text><![CDATA[到期日:]]></text>
</staticText>
<textField textAdjust="ScaleFont" isBlankWhenNull="true">
<reportElement x="116" y="56" width="301" height="56" uuid="97e4a9bb-fc84-4074-b523-ddf9eb9ed2d2"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="44" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$F{expiryDate}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="8" y="114" width="286" height="16" uuid="f5f145f1-123f-4fe5-90fa-8550b3d2eb63"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="10" isBold="true"/>
</textElement>
<text><![CDATA[批次編號:]]></text>
</staticText>
<textField textAdjust="ScaleFont" isBlankWhenNull="true">
<reportElement x="8" y="130" width="286" height="20" uuid="402f35fc-b11a-4bf1-982d-e12c622f38ea"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="18" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$F{lotNo}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="8" y="152" width="140" height="16" uuid="96d46fb1-939d-4f3e-b8f0-172b69b85e9f"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="10" isBold="true"/>
</textElement>
<text><![CDATA[物料編號:]]></text>
</staticText>
<staticText>
<reportElement x="152" y="152" width="142" height="16" uuid="53a2d7cf-bd50-4264-abee-346823cd49dd"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="10" isBold="true"/>
</textElement>
<text><![CDATA[數量:]]></text>
</staticText>
<textField textAdjust="ScaleFont" isBlankWhenNull="true">
<reportElement x="8" y="168" width="140" height="26" uuid="1f289946-b945-4f79-8515-212368b8be3b"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="22" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$F{itemNo}]]></textFieldExpression>
</textField>
<textField textAdjust="ScaleFont" isBlankWhenNull="true">
<reportElement x="152" y="168" width="142" height="16" uuid="629d9af8-c73f-4614-ba9d-a582e2ed4a29"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="14" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$F{qtyLabel}]]></textFieldExpression>
</textField>
<textField textAdjust="ScaleFont" isBlankWhenNull="true">
<reportElement x="152" y="184" width="142" height="12" uuid="7c1a0e44-6b2d-4a91-9f10-2d8e5b6c4a11">
<printWhenExpression><![CDATA[$F{qtyDetail} != null && !$F{qtyDetail}.trim().isEmpty()]]></printWhenExpression>
</reportElement>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="8"/>
</textElement>
<textFieldExpression><![CDATA[$F{qtyDetail}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="8" y="200" width="290" height="18" uuid="5c3f9b03-222c-4c94-ab58-6bc86d44c5ce"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="10" isBold="true"/>
</textElement>
<text><![CDATA[物料名稱:]]></text>
</staticText>
<textField textAdjust="ScaleFont" isBlankWhenNull="true">
<reportElement x="8" y="218" width="290" height="58" uuid="e04b094d-2d16-4060-b6d9-2e6033a673ab"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="48" isBold="true"/>
</textElement>
<textFieldExpression><![CDATA[$F{itemName}]]></textFieldExpression>
</textField>
<image scaleImage="FillFrame" hAlign="Center" vAlign="Middle">
<reportElement x="308" y="116" width="108" height="108" uuid="5dd35c73-973b-4f0d-9cad-13305a29a9fd"/>
<imageExpression><![CDATA[$F{qrCode}]]></imageExpression>
</image>
<staticText>
<reportElement x="308" y="226" width="108" height="12" uuid="c2041205-51f8-45bb-a1d1-e3de37eebed7"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="8"/>
</textElement>
<text><![CDATA[供應商:]]></text>
</staticText>
<textField textAdjust="ScaleFont" isBlankWhenNull="true">
<reportElement x="308" y="238" width="108" height="14" uuid="ab82ca39-8840-4471-b7c0-cdeec9cb5693"/>
<textElement verticalAlignment="Middle">
<font fontName="微軟正黑體" size="8"/>
</textElement>
<textFieldExpression><![CDATA[$F{supplier}]]></textFieldExpression>
</textField>
<staticText>
<reportElement x="308" y="252" width="108" height="12" uuid="1b1efafd-049a-432e-93c4-9662273011f7"/>
<textElement textAlignment="Left" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="8"/>
</textElement>
<text><![CDATA[採購訂單編號:]]></text>
</staticText>
<textField textAdjust="ScaleFont" isBlankWhenNull="true">
<reportElement x="308" y="264" width="108" height="14" uuid="b805e20a-9a36-45c8-8255-23b6cc58f47c"/>
<textElement verticalAlignment="Middle">
<font fontName="微軟正黑體" size="8"/>
</textElement>
<textFieldExpression><![CDATA[$P{poCode}]]></textFieldExpression>
</textField>
<textField>
<reportElement x="322" y="156" width="80" height="20" uuid="d66a64b0-3335-4846-bcb0-a67594520055">
<printWhenExpression><![CDATA[$P{isTransfer} != null && !$P{isTransfer}.toString().trim().isEmpty()]]></printWhenExpression>
</reportElement>
<box>
<pen lineWidth="2.0"/>
</box>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font fontName="微軟正黑體" size="11" isBold="true" isUnderline="true"/>
</textElement>
<textFieldExpression><![CDATA[$P{isTransfer}]]></textFieldExpression>
</textField>
</band>
</detail>
</jasperReport>

Loading…
Откажи
Сачувај