Просмотр исходного кода

Merge remote-tracking branch 'origin/master'

# Conflicts:
#	src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt
master
CANCERYS\kw093 2 дней назад
Родитель
Сommit
601e5f6fe1
16 измененных файлов: 502 добавлений и 47 удалений
  1. +2
    -2
      src/main/java/com/ffii/fpsms/m18/service/M18DeliveryOrderService.kt
  2. +12
    -3
      src/main/java/com/ffii/fpsms/m18/service/M18PurchaseOrderService.kt
  3. +13
    -2
      src/main/java/com/ffii/fpsms/modules/common/scheduler/service/SchedulerService.kt
  4. +27
    -4
      src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleRepository.kt
  5. +1
    -0
      src/main/java/com/ffii/fpsms/modules/master/entity/projections/PrinterCombo.kt
  6. +2
    -0
      src/main/java/com/ffii/fpsms/modules/master/entity/projections/ProdScheduleInfo.kt
  7. +249
    -20
      src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt
  8. +20
    -1
      src/main/java/com/ffii/fpsms/modules/master/web/ProductionScheduleController.kt
  9. +13
    -12
      src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt
  10. +9
    -0
      src/main/java/com/ffii/fpsms/modules/user/req/UpdateUserReq.java
  11. +76
    -0
      src/main/java/com/ffii/fpsms/modules/user/service/UserQrCodeService.kt
  12. +2
    -1
      src/main/java/com/ffii/fpsms/modules/user/service/UserService.java
  13. +3
    -1
      src/main/java/com/ffii/fpsms/modules/user/service/pojo/UserRecord.java
  14. +5
    -0
      src/main/java/com/ffii/fpsms/modules/user/web/ExportUserQrCodeRequest.kt
  15. +26
    -1
      src/main/java/com/ffii/fpsms/modules/user/web/UserController.java
  16. +42
    -0
      src/main/resources/qrCodeHandle/qrCodeHandle.jrxml

+ 2
- 2
src/main/java/com/ffii/fpsms/m18/service/M18DeliveryOrderService.kt Просмотреть файл

@@ -63,7 +63,7 @@ open class M18DeliveryOrderService(
val dateTo = request.modifiedDateTo?.let {
LocalDateTime.parse(it, formatter).toLocalDate().toString()
}
val lastModifyDateConds =
val lastDateConds =
//"lastModifyDate=largerOrEqual=${request.modifiedDateFrom ?: lastModifyDateStart}=and=lastModifyDate=lessOrEqual=${request.modifiedDateTo ?: lastModifyDateEnd}"
"dDate=largerOrEqual=${dateFrom ?: lastModifyDateStart}=and=dDate=lessOrEqual=${dateTo ?: lastModifyDateEnd}"

@@ -74,7 +74,7 @@ open class M18DeliveryOrderService(
"venId=equal=",
"=or="
)
val shopPoConds = "(${shopPoBuyers})=and=(${shopPoSupplier})=and=(${lastModifyDateConds})"
val shopPoConds = "(${shopPoBuyers})=and=(${shopPoSupplier})=and=(${lastDateConds})"
println("shopPoConds: ${shopPoConds}")
val shopPoParams = M18PurchaseOrderListRequest(
params = null,


+ 12
- 3
src/main/java/com/ffii/fpsms/m18/service/M18PurchaseOrderService.kt Просмотреть файл

@@ -24,6 +24,7 @@ import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.time.LocalDateTime
import kotlin.reflect.full.memberProperties
import java.time.format.DateTimeFormatter

@Service
open class M18PurchaseOrderService(
@@ -57,8 +58,16 @@ open class M18PurchaseOrderService(
// Include material po, oem po
open fun getPurchaseOrdersWithType(request: M18CommonRequest): M18PurchaseOrderListResponseWithType? {
val purchaseOrders = M18PurchaseOrderListResponseWithType(mutableListOf())
val lastModifyDateConds =
"lastModifyDate=largerOrEqual=${request.modifiedDateFrom ?: lastModifyDateStart}=and=lastModifyDate=lessOrEqual=${request.modifiedDateTo ?: lastModifyDateEnd}"
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
val dateFrom = request.modifiedDateFrom?.let {
LocalDateTime.parse(it, formatter).toLocalDate().toString()
}
val dateTo = request.modifiedDateTo?.let {
LocalDateTime.parse(it, formatter).toLocalDate().toString()
}
val lastDateConds =
//"lastModifyDate=largerOrEqual=${request.modifiedDateFrom ?: lastModifyDateStart}=and=lastModifyDate=lessOrEqual=${request.modifiedDateTo ?: lastModifyDateEnd}"
"dDate=largerOrEqual=${dateFrom ?: lastModifyDateStart}=and=dDate=lessOrEqual=${dateTo ?: lastModifyDateEnd}"
// Material PO
val materialPoBuyers =
commonUtils.listToString(listOf(m18Config.BEID_PP, m18Config.BEID_PF), "beId=equal=", "=or=")
@@ -67,7 +76,7 @@ open class M18PurchaseOrderService(
"venId=unequal=",
"=or="
)
val materialPoConds = "(${materialPoBuyers})=and=(${materialPoSupplierNot})=and=(${lastModifyDateConds})"
val materialPoConds = "(${materialPoBuyers})=and=(${materialPoSupplierNot})=and=(${lastDateConds})"
println("materialPoConds: ${materialPoConds}")
val materialPoParams = M18PurchaseOrderListRequest(
params = null,


+ 13
- 2
src/main/java/com/ffii/fpsms/modules/common/scheduler/service/SchedulerService.kt Просмотреть файл

@@ -153,13 +153,24 @@ open class SchedulerService(
logger.info("Daily Scheduler - PO")
val currentTime = LocalDateTime.now()
val today = currentTime.toLocalDate().atStartOfDay()
val yesterday = today.minusDays(1L)
/* val yesterday = today.minusDays(1L)
val request = M18CommonRequest(
modifiedDateTo = today.format(dataStringFormat),
modifiedDateFrom = yesterday.format(dataStringFormat)
)*/
val tmr = today.plusDays(1L)
var request = M18CommonRequest(
modifiedDateTo = tmr.format(dataStringFormat),
modifiedDateFrom = tmr.format(dataStringFormat)
)
m18PurchaseOrderService.savePurchaseOrders(request);
m18DeliveryOrderService.saveDeliveryOrders(request);

//dDate from tmr to tmr
var requestDO = M18CommonRequest(
modifiedDateTo = tmr.format(dataStringFormat),
modifiedDateFrom = tmr.format(dataStringFormat)
)
m18DeliveryOrderService.saveDeliveryOrders(requestDO);
// logger.info("today: ${today.format(dataStringFormat)}")
// logger.info("yesterday: ${yesterday.format(dataStringFormat)}")
}


+ 27
- 4
src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleRepository.kt Просмотреть файл

@@ -52,7 +52,7 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule,
and (:schedulePeriodTo = '' or datediff(schedulePeriodTo, coalesce(:schedulePeriodTo, schedulePeriodTo)) = 0)
and (:totalEstProdCount is null or :totalEstProdCount = '' or totalEstProdCount = :totalEstProdCount)
and (coalesce(:types) is null or type in :types)
order by id desc;
order by id ASC;
""",
countQuery =
"""
@@ -83,7 +83,7 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule,
and (:schedulePeriodTo = '' or datediff(schedulePeriodTo, coalesce(:schedulePeriodTo, schedulePeriodTo)) = 0)
and (:totalEstProdCount is null or :totalEstProdCount = '' or totalEstProdCount = :totalEstProdCount)
and (coalesce(:types) is null or type in :types)
order by id desc;
order by id ASC;
""",
)
fun findProdScheduleInfoByPage(
@@ -121,7 +121,7 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule,
and (:produceAt = '' or datediff(produceAt, coalesce(:produceAt, produceAt)) = 0)
and (:totalEstProdCount is null or :totalEstProdCount = '' or totalEstProdCount = :totalEstProdCount)
and (coalesce(:types) is null or type in :types)
order by id desc;
order by id ASC;
"""
)
fun findProdScheduleInfoByProduceAtByPage(
@@ -207,10 +207,31 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule,
prod.itemPriority,
prod.batchNeed,
prod.stockQty,
prod.outputQty,
prod.prodQty,
prod.avgQtyLastMonth,
json_arrayagg(
json_object('id', prod.pslId, 'bomMaterials', prod.bomMaterials, 'jobNo', prod.jobNo, 'code', prod.code, 'name', prod.name, 'type', prod.type, 'demandQty', prod.demandQty, 'bomOutputQty', prod.bomOutputQty, 'uomName', prod.uomName, 'prodTimeInMinute', prod.prodTimeInMinute, 'priority', prod.priority, 'approved', prod.approved, 'proportion', prod.proportion, 'daysLeft', prod.daysLeft, 'needNoOfJobOrder', prod.needNoOfJobOrder, 'batchNeed', prod.batchNeed, 'avgQtyLastMonth', prod.avgQtyLastMonth, 'stockQty', prod.stockQty)
json_object('id', prod.pslId,
'bomMaterials', prod.bomMaterials,
'jobNo', prod.jobNo,
'code', prod.code,
'name', prod.name,
'type', prod.type,
'demandQty', prod.demandQty,
'bomOutputQty', prod.bomOutputQty,
'uomName', prod.uomName,
'prodTimeInMinute', prod.prodTimeInMinute,
'priority', prod.priority,
'approved', prod.approved,
'proportion', prod.proportion,
'daysLeft', prod.daysLeft,
'needNoOfJobOrder', prod.needNoOfJobOrder,
'batchNeed', prod.batchNeed,
'avgQtyLastMonth', prod.avgQtyLastMonth,
'prodQty', prod.prodQty,
'outputQty', prod.outputQty,
'stockQty', prod.stockQty)
) as prodScheduleLines
from (
select
@@ -226,6 +247,8 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule,
psl.itemPriority,
psl.batchNeed,
psl.stockQty,
psl.outputQty,
psl.prodQty,
pm.bomMaterials,
pm.bomOutputQty,
uc.udfudesc as uomName,


+ 1
- 0
src/main/java/com/ffii/fpsms/modules/master/entity/projections/PrinterCombo.kt Просмотреть файл

@@ -10,6 +10,7 @@ interface PrinterCombo {
val value: String;
val code: String?;
val name: String?;
val type: String?;
val description: String?;
val ip: String?;
val port: Int?;


+ 2
- 0
src/main/java/com/ffii/fpsms/modules/master/entity/projections/ProdScheduleInfo.kt Просмотреть файл

@@ -50,6 +50,8 @@ data class DetailedProdScheduleLineInfo(
val daysLeft: BigDecimal?,
val onHandQty: BigDecimal?,
val stockQty: BigDecimal?,
val outputQty: BigDecimal?,
val prodQty: BigDecimal?,
val lastMonthAvgSales: BigDecimal?,
val avgQtyLastMonth: BigDecimal?,
val needNoOfJobOrder: BigDecimal?,


+ 249
- 20
src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt Просмотреть файл

@@ -46,6 +46,10 @@ import kotlin.collections.component2
import kotlin.jvm.optionals.getOrNull
import kotlin.math.ceil
import kotlin.comparisons.maxOf
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.apache.poi.ss.usermodel.FillPatternType
import org.apache.poi.ss.usermodel.IndexedColors
import java.io.ByteArrayOutputStream

@Service
open class ProductionScheduleService(
@@ -336,7 +340,7 @@ open class ProductionScheduleService(
open fun saveProdScheduleLine(request: ReleaseProdScheduleLineRequest): MessageResponse {
val prodScheduleLine = request.id.let { productionScheduleLineRepository.findById(it).getOrNull() } ?: throw NoSuchElementException()
val prodSchedule = prodScheduleLine.productionSchedule
// Update Prod Schedule Type
prodSchedule.apply {
type = "manual"
@@ -351,7 +355,7 @@ open class ProductionScheduleService(
productionScheduleLineRepository.saveAndFlush(prodScheduleLine)

val bomMaterials = prodScheduleLine.id?.let { productionScheduleLineRepository.getBomMaterials(it) }
return MessageResponse(
id = request.id,
name = null,
@@ -422,11 +426,11 @@ open class ProductionScheduleService(
logger.info("bom?.outputQty: ${bom.outputQty} ${bom.outputQtyUom}")

logger.info("[releaseProdSchedule] prodScheduleLine.needNoOfJobOrder:" + prodScheduleLine.needNoOfJobOrder)
repeat(prodScheduleLine.needNoOfJobOrder) {
//repeat(prodScheduleLine.needNoOfJobOrder) {
// 6. Create Job Order
val joRequest = CreateJobOrderRequest(
bomId = bom.id, // bom is guaranteed non-null here
reqQty = bom.outputQty, // bom is guaranteed non-null here
reqQty = bom.outputQty?.multiply(BigDecimal.valueOf(prodScheduleLine.batchNeed.toLong())),
approverId = approver?.id,
// CRUCIAL FIX: Use the line ID, not the parent schedule ID
@@ -442,8 +446,8 @@ open class ProductionScheduleService(
// 7. Create related job order data
jobOrderBomMaterialService.createJobOrderBomMaterialsByJoId(createdJobOrderId)
jobOrderProcessService.createJobOrderProcessesByJoId(createdJobOrderId)
productProcessService.createProductProcessByJobOrderId(createdJobOrderId)
}
productProcessService.createProductProcessByJobOrderId(createdJobOrderId, prodScheduleLine.itemPriority.toInt())
//}
}

@@ -491,22 +495,45 @@ open class ProductionScheduleService(
logger.info("bom?.outputQty:" + bom?.outputQty + "" + bom?.outputQtyUom);

logger.info("prodScheduleLine.needNoOfJobOrder:" + prodScheduleLine.needNoOfJobOrder)
repeat(prodScheduleLine.needNoOfJobOrder) {
// Create Job Order
val prodScheduleLineId = prodScheduleLine.id!!

try {
jobOrderService.jobOrderDetailByPsId(prodScheduleLineId)
} catch (e: NoSuchElementException) {
// 3. Fetch BOM, handling nullability safely
val item = prodScheduleLine.item

val itemId = item.id
?: throw IllegalStateException("Item ID is missing for Production Schedule Line $prodScheduleLineId.")

val bom = bomService.findByItemId(itemId)
?: throw NoSuchElementException("BOM not found for Item ID $itemId.")

val approver = SecurityUtils.getUser().getOrNull() // Get approver once

val joRequest = CreateJobOrderRequest(
bomId = bom?.id,
reqQty = bom?.outputQty,
bomId = bom.id, // bom is guaranteed non-null here
reqQty = bom.outputQty?.multiply(BigDecimal.valueOf(prodScheduleLine.batchNeed.toLong())),
approverId = approver?.id,
prodScheduleLineId = request.id
// CRUCIAL FIX: Use the line ID, not the parent schedule ID
prodScheduleLineId = prodScheduleLine.id!!
)

// Assuming createJobOrder returns the created Job Order (jo)
val jo = jobOrderService.createJobOrder(joRequest)

logger.info("jo created:" + jo.id!!)
jobOrderBomMaterialService.createJobOrderBomMaterialsByJoId(jo.id!!)
jobOrderProcessService.createJobOrderProcessesByJoId(jo.id!!)
productProcessService.createProductProcessByJobOrderId(jo.id!!)
val createdJobOrderId = jo.id
?: throw IllegalStateException("Job Order creation failed: returned object ID is null.")

// 7. Create related job order data
jobOrderBomMaterialService.createJobOrderBomMaterialsByJoId(createdJobOrderId)
jobOrderProcessService.createJobOrderProcessesByJoId(createdJobOrderId)
productProcessService.createProductProcessByJobOrderId(createdJobOrderId, prodScheduleLine.itemPriority.toInt())

}
// Get Latest Data
@@ -593,7 +620,7 @@ open class ProductionScheduleService(
CEIL((i.avgQtyLastMonth * 1.9 - stockQty) / i.outputQty)
ELSE 0
END AS batchNeed,
markDark + markFloat + markDense + markAS as priority,
25 + 25 + markDark + markFloat + markDense + markAS + markTimeSequence + markComplexity as priority,
i.*
FROM
(SELECT
@@ -627,14 +654,29 @@ open class ProductionScheduleService(
inventory.onHandQty,
bom.itemId,
bom.id AS bomId,

CASE WHEN bom.isDark = 5 THEN 11
WHEN bom.isDark = 3 THEN 6
WHEN bom.isDark = 1 THEN 2
ELSE 0 END as markDark,
CASE WHEN bom.isFloat = 3 THEN 11

CASE WHEN bom.isFloat = 5 THEN 11
WHEN bom.isFloat = 3 THEN 6
WHEN bom.isFloat = 1 THEN 2
ELSE 0 END as markFloat,

CASE WHEN bom.isDense = 5 THEN 11
WHEN bom.isDense = 3 THEN 6
WHEN bom.isDense = 1 THEN 2
ELSE 0 END as markDense,

bom.timeSequence as markTimeSequence,

bom.complexity as markComplexity,

CASE WHEN bom.allergicSubstances = 5 THEN 11
ELSE 0 END as markAS,

inventory.id AS inventoryId
FROM
bom
@@ -663,6 +705,7 @@ open class ProductionScheduleService(
needNoOfJobOrder = (row["needNoOfJobOrder"] as Number).toLong()
daysLeft = (row["daysLeft"] as Number).toDouble()
batchNeed = row["batchNeed"] as Number
priority = row["priority"] as Number

}
}
@@ -696,7 +739,7 @@ open class ProductionScheduleService(
val allergicSubstances = maxOf(record.allergicSubstances.toDouble(), 0.0)

val priority = isDark + isFloat + isDense + allergicSubstances
record.itemPriority = priority
record.itemPriority = record.priority
productionPriorityMap.put(record, priority)
}

@@ -761,6 +804,10 @@ open class ProductionScheduleService(
record.needNoOfJobOrder = 0
record.batchNeed = 0
}
}else{
record.needQty = 0.0
record.needNoOfJobOrder = 0
record.batchNeed = 0
}

logger.info(record.name + " record.batchNeed: " + record.batchNeed + " record.stockQty:" + record.stockQty + " record.daysLeft:" + record.daysLeft)
@@ -831,7 +878,7 @@ open class ProductionScheduleService(
savedItem.type = "detailed"
savedItem.assignDate = LocalDateTime.now().dayOfMonth.toLong();
savedItem.weightingRef = detailedScheduleObj.needQty
savedItem.itemPriority = detailedScheduleObj.needQty.toLong()
savedItem.itemPriority = detailedScheduleObj.priority.toLong()
savedItem.outputQty = detailedScheduleObj.outputQty
savedItem.avgQtyLastMonth = detailedScheduleObj.avgQtyLastMonth
savedItem.stockQty = detailedScheduleObj.stockQty
@@ -862,11 +909,15 @@ open class ProductionScheduleService(
open var stockQty: Double = 0.0
open var daysLeft: Double = 0.0
open var batchNeed: Number = 0
open var priority: Number = 0

override fun toString(): String {
return "NeedQtyRecord(name=${name}," +
" avgQtyLastMonth=$avgQtyLastMonth" +
" stockQty=$stockQty" +
" daysLeft=$daysLeft" +
" batchNeed=$batchNeed" +
" priority=$priority" +
" itemId=$id" +
" needNoOfJobOrder=$needNoOfJobOrder" +
" outputQty=$outputQty" +
@@ -1286,4 +1337,182 @@ open class ProductionScheduleService(
}

}

fun exportProdScheduleToExcel(lines: List<Map<String, Any>>, lineMats: List<Map<String, Any>>): ByteArray {
val workbook = XSSFWorkbook()
// 1. Group Production Lines by Date
val groupedData = lines.groupBy {
val produceAt = it["produceAt"]
when (produceAt) {
is LocalDateTime -> produceAt.toLocalDate().toString()
is java.sql.Timestamp -> produceAt.toLocalDateTime().toLocalDate().toString()
else -> produceAt?.toString()?.substring(0, 10) ?: "Unknown_Date"
}
}

// 2. Define Header Style
val headerStyle = workbook.createCellStyle().apply {
fillForegroundColor = IndexedColors.GREY_25_PERCENT.index
fillPattern = FillPatternType.SOLID_FOREGROUND
val font = workbook.createFont()
font.bold = true
setFont(font)
}

// 3. Create Production Worksheets
groupedData.forEach { (dateKey, dailyLines) ->
val sheetName = dateKey.replace("[/\\\\?*:\\[\\]]".toRegex(), "-")
val sheet = workbook.createSheet(sheetName)

val headers = listOf("Item Name", "Avg Qty Last Month", "Stock Qty", "Days Left", "Output Qty", "Batch Need", "Priority")
val headerRow = sheet.createRow(0)
headers.forEachIndexed { i, title ->
val cell = headerRow.createCell(i)
cell.setCellValue(title)
cell.setCellStyle(headerStyle)
}

dailyLines.forEachIndexed { index, line ->
val row = sheet.createRow(index + 1)
row.createCell(0).setCellValue(line["itemName"]?.toString() ?: "")
row.createCell(1).setCellValue(asDouble(line["avgQtyLastMonth"]))
row.createCell(2).setCellValue(asDouble(line["stockQty"]))
row.createCell(3).setCellValue(asDouble(line["daysLeft"]))
row.createCell(4).setCellValue(asDouble(line["outputdQty"])) // Note: Matching your snippet's "outputdQty" key
row.createCell(5).setCellValue(asDouble(line["batchNeed"]))
row.createCell(6).setCellValue(asDouble(line["itemPriority"]))
}

for (i in headers.indices) { sheet.autoSizeColumn(i) }
}

// 4. Create Material Summary Worksheet
val matSheet = workbook.createSheet("Material Summary")
val matHeaders = listOf(
"Mat Code", "Mat Name", "Required Qty", "Total Qty Need",
"UoM", "Purchased Qty", "On Hand Qty", "Unavailable Qty",
"Related Item Code", "Related Item Name"
)

val matHeaderRow = matSheet.createRow(0)
matHeaders.forEachIndexed { i, title ->
matHeaderRow.createCell(i).apply {
setCellValue(title)
setCellStyle(headerStyle)
}
}

lineMats.forEachIndexed { index, rowData ->
val row = matSheet.createRow(index + 1)
val totalNeed = asDouble(rowData["totalMatQtyNeed"])
val purchased = asDouble(rowData["purchasedQty"])
val onHand = asDouble(rowData["onHandQty"])
// Calculation: Required Qty = totalMatQtyNeed - purchasedQty - onHandQty (minimum 0)
val requiredQty = (totalNeed - purchased - onHand).coerceAtLeast(0.0)

row.createCell(0).setCellValue(rowData["matCode"]?.toString() ?: "")
row.createCell(1).setCellValue(rowData["matName"]?.toString() ?: "")
row.createCell(2).setCellValue(requiredQty)
row.createCell(3).setCellValue(totalNeed)
row.createCell(4).setCellValue(rowData["uomName"]?.toString() ?: "")
row.createCell(5).setCellValue(purchased)
row.createCell(6).setCellValue(onHand)
row.createCell(7).setCellValue(asDouble(rowData["unavailableQty"]))
row.createCell(8).setCellValue(rowData["itemCode"]?.toString() ?: "")
row.createCell(9).setCellValue(rowData["itemName"]?.toString() ?: "")
}

for (i in matHeaders.indices) { matSheet.autoSizeColumn(i) }

// 5. Finalize and Return
val out = ByteArrayOutputStream()
workbook.use { it.write(out) }
return out.toByteArray()
}

private fun asDouble(value: Any?): Double {
return when (value) {
is Number -> value.toDouble()
is String -> value.toDoubleOrNull() ?: 0.0
else -> 0.0
}
}

//====================細排相關 START====================//
open fun searchExportProdSchedule(fromDate: LocalDate): List<Map<String, Any>> {

val args = mapOf(
"fromDate" to fromDate,
)

val sql = """
select
it.code as itemCode,
it.name as itemName,
ps.produceAt,
psl.*
from production_schedule_line psl
left join production_schedule ps on psl.prodScheduleId = ps.id
left join items it on psl.itemId = it.id

where ps.produceAt >= :fromDate

and ps.id = (select id from production_schedule where produceAt = ps.produceAt order by id desc limit 1)

order by ps.produceAt asc, psl.itemPriority desc, itemCode asc
""";

return jdbcDao.queryForList(sql, args);
}


open fun searchExportProdScheduleMaterial(fromDate: LocalDate): List<Map<String, Any>> {

val args = mapOf(
"fromDate" to fromDate,
)

val sql = """
select
group_concat(distinct it.code) as itemCode,
group_concat(distinct it.name) as itemName,
itm.code as matCode,
itm.name as matName,
sum(bm.qty * psl.batchNeed) as totalMatQtyNeed,
iv.onHandQty,
iv.unavailableQty,
(select sum(qty) from purchase_order_line
left join purchase_order on purchase_order_line.purchaseOrderId = purchase_order.id
where purchase_order_line.itemId = itm.id and date(purchase_order.estimatedArrivalDate) >= date(now()) and purchase_order.completeDate is null) as purchasedQty,
bm.uomName,
min(ps.produceAt) as toProdcueFrom,
max(ps.produceAt) as toProdcueAt,
psl.*
from production_schedule_line psl
left join production_schedule ps on psl.prodScheduleId = ps.id
left join items it on psl.itemId = it.id
left join bom on it.id = bom.itemId
left join bom_material bm on bom.id = bm.bomId
left join items itm on bm.itemId = itm.id
left join inventory iv on itm.id = iv.itemId
where ps.produceAt >= :fromDate
and ps.id = (select id from production_schedule where produceAt = ps.produceAt order by id desc limit 1)
-- and it.code = 'PP2236'
-- order by ps.produceAt asc, psl.itemPriority desc, itemCode asc
group by matCode
""";

return jdbcDao.queryForList(sql, args);
}
}

+ 20
- 1
src/main/java/com/ffii/fpsms/modules/master/web/ProductionScheduleController.kt Просмотреть файл

@@ -23,7 +23,10 @@ import java.time.format.DateTimeFormatter
import java.util.HashMap
import kotlin.collections.component1
import kotlin.collections.component2

import org.springframework.http.ResponseEntity
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.GetMapping

@RestController
@RequestMapping("/productionSchedule")
@@ -228,4 +231,20 @@ class ProductionScheduleController(
throw RuntimeException("Error generate schedule: ${e.message}", e)
}
}

@PostMapping(
value = ["/export-prod-schedule"],
produces = ["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"]
)
fun exportProdSchedule(): ResponseEntity<ByteArray> {
val data = productionScheduleService.searchExportProdSchedule(LocalDate.now())
val dataMat = productionScheduleService.searchExportProdScheduleMaterial(LocalDate.now())
val excelContent = productionScheduleService.exportProdScheduleToExcel(data, dataMat)
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=production_schedule.xlsx")
.contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
.body(excelContent)
}
}

+ 13
- 12
src/main/java/com/ffii/fpsms/modules/productProcess/service/ProductProcessService.kt Просмотреть файл

@@ -665,7 +665,7 @@ open class ProductProcessService(
} else {
"insufficient"
}
jobOrderLineInfo(
id = line.id?:0,
itemId = itemId,
@@ -685,7 +685,7 @@ open class ProductProcessService(
}
}

open fun createProductProcessByJobOrderId(jobOrderId: Long, productionPriority: Int?=50): MessageResponse {
open fun createProductProcessByJobOrderId(jobOrderId: Long): MessageResponse {
val jobOrder = jobOrderRepository.findById(jobOrderId).orElse(null)
val bom = bomRepository.findById(jobOrder?.bom?.id ?: 0L).orElse(null)
// val bom = jobOrder.bom.let { bomRepository.findById(it).orElse(null) }
@@ -702,6 +702,7 @@ open class ProductProcessService(
this.status = ProductProcessStatus.PENDING
this.date = jobOrder?.planEnd?.toLocalDate()
this.bom = bom

this.productionPriority = productionPriority
}

@@ -920,10 +921,10 @@ open class ProductProcessService(
}
open fun getJobOrderProcessLineDetail(productProcessLineId: Long): JobOrderProcessLineDetailResponse {
val productProcessLine = productProcessLineRepository.findById(productProcessLineId).orElse(null)
val bomProcessId = productProcessLine?.bomProcess?.id
println("bomProcessId ${bomProcessId}")
val bomProcess: BomProcess? = bomProcessId?.let {
val bomProcess: BomProcess? = bomProcessId?.let {
bomProcessRepository.findAll().firstOrNull { it.id == bomProcessId }
}
println("bomProcess ${bomProcess?.id}")
@@ -1024,12 +1025,12 @@ open class ProductProcessService(
println("📋 Service: ProductProcessLine EndTime: ${productProcessLine.endTime}")
// 更新状态为 "Pass"
updateProductProcessLineStatus(productProcessLineId, "Pass")
// 检查是否所有 lines 都完成(Completed 或 Pass)
// 注意:这里应该传入 productProcessId,而不是 productProcessLineId
val productProcessId = productProcessLine?.productProcess?.id ?: 0L
ifAllLinesCompletedOrPassed(productProcessId)
return MessageResponse(
id = productProcessLineId,
code = "200",
@@ -1349,20 +1350,20 @@ open class ProductProcessService(
open fun ifAllLinesCompletedOrPassed(productProcessId: Long): MessageResponse {
// 获取所有 product process lines
val allproductProcessLines = productProcessLineRepository.findByProductProcess_Id(productProcessId)
// 检查是否所有 lines 都是 "Completed" 或 "Pass"
if(allproductProcessLines.all { it.status == "Completed" || it.status == "Pass" }) {
// 更新 product process 的 endTime 和状态
updateProductProcessEndTime(productProcessId)
updateProductProcessStatus(productProcessId, ProductProcessStatus.COMPLETED)
val productProcess = productProcessRepository.findById(productProcessId).orElse(null)
val jobOrder = jobOrderRepository.findById(productProcess?.jobOrder?.id ?: 0L).orElse(null)
if(jobOrder != null) {
jobOrder.status = JobOrderStatus.STORING
jobOrderRepository.save(jobOrder)
stockInLineService.create(
SaveStockInLineRequest(
itemId = productProcess?.item?.id ?: 0L,
@@ -1377,7 +1378,7 @@ open class ProductProcessService(
)
}
}
return MessageResponse(
id = productProcessId,
code = "200",
@@ -1387,7 +1388,7 @@ open class ProductProcessService(
errorPosition = null,
)
}
open fun SaveProductProcessIssueTime(request: SaveProductProcessIssueTimeRequest): MessageResponse {
println("📋 Service: Saving ProductProcess Issue Time: ${request.productProcessLineId}")
val productProcessLine = productProcessLineRepository.findById(request.productProcessLineId).orElse(null)


+ 9
- 0
src/main/java/com/ffii/fpsms/modules/user/req/UpdateUserReq.java Просмотреть файл

@@ -22,6 +22,7 @@ public class UpdateUserReq {
private LocalDate expiryDate;
private String locale;
private String remarks;
private String staffNo;


// @NotNull
@@ -126,4 +127,12 @@ public class UpdateUserReq {
this.remarks = remarks;
}

public String getStaffNo() { // Add getter
return staffNo;
}

public void setStaffNo(String staffNo) { // Add setter
this.staffNo = staffNo;
}

}

+ 76
- 0
src/main/java/com/ffii/fpsms/modules/user/service/UserQrCodeService.kt Просмотреть файл

@@ -0,0 +1,76 @@
package com.ffii.fpsms.modules.user.service

import com.ffii.core.utils.PdfUtils
import com.ffii.core.utils.QrCodeUtil
import com.ffii.fpsms.modules.user.entity.UserRepository
import com.ffii.fpsms.modules.user.web.ExportUserQrCodeRequest
import net.sf.jasperreports.engine.JasperCompileManager
import net.sf.jasperreports.engine.JasperPrint
import net.sf.jasperreports.engine.export.JRPdfExporter
import net.sf.jasperreports.export.SimpleExporterInput
import net.sf.jasperreports.export.SimpleOutputStreamExporterOutput
import org.springframework.core.io.ClassPathResource
import org.springframework.stereotype.Service
import java.io.FileNotFoundException
import java.awt.GraphicsEnvironment
import kotlinx.serialization.json.Json
import kotlinx.serialization.encodeToString

@Service
class UserQrCodeService(
private val userRepository: UserRepository
) {

fun exportUserQrCode(request: ExportUserQrCodeRequest): Map<String, Any> {
val QRCODE_HANDLE_PDF = "qrCodeHandle/qrCodeHandle.jrxml"
val resource = ClassPathResource(QRCODE_HANDLE_PDF)
if (!resource.exists()) {
throw FileNotFoundException("Report file not found: $QRCODE_HANDLE_PDF")
}
val inputStream = resource.inputStream
val qrCodeHandleReport = JasperCompileManager.compileReport(inputStream)
val users = userRepository.findAllById(request.userIds)
val fields = mutableListOf<MutableMap<String, Any>>()
for (user in users) {
val field = mutableMapOf<String, Any>()
val staffNo = user.staffNo ?: ""
val username = user.username ?: "N/A"
val qrContentMap = mapOf("staffNo" to staffNo)
val qrCodeContent = Json.encodeToString(qrContentMap)
val qrCodeImage = QrCodeUtil.generateQRCodeImage(qrCodeContent)
field["username"] = username
field["staffNo"] = staffNo.ifEmpty { "N/A" }
field["qrCode"] = qrCodeImage
fields.add(field)
}
val params: MutableMap<String, Any> = mutableMapOf()
// Configure for Chinese character support
// Try to find a Chinese-supporting font
val availableFonts = GraphicsEnvironment.getLocalGraphicsEnvironment().availableFontFamilyNames
val chineseFont = availableFonts.find {
it.contains("SimSun", ignoreCase = true) ||
it.contains("Microsoft YaHei", ignoreCase = true) ||
it.contains("STSong", ignoreCase = true) ||
it.contains("SimHei", ignoreCase = true)
} ?: "Arial Unicode MS" // Fallback
params["net.sf.jasperreports.default.pdf.encoding"] = "Identity-H"
params["net.sf.jasperreports.default.pdf.embedded"] = true
params["net.sf.jasperreports.default.pdf.font.name"] = chineseFont
return mapOf(
"report" to PdfUtils.fillReport(qrCodeHandleReport, fields, params),
"fileName" to (users.firstOrNull()?.username ?: "user_qrcode")
)
}
}

+ 2
- 1
src/main/java/com/ffii/fpsms/modules/user/service/UserService.java Просмотреть файл

@@ -111,7 +111,8 @@ public class UserService extends AbstractBaseEntityService<User, Long, UserRepos
+ " u.email,"
+ " u.phone1,"
+ " u.phone2,"
+ " u.remarks "
+ " u.remarks,"
+ " u.staffNo"
+ " FROM `user` u"
+ " left join user_group ug on u.id = ug.userId"
+ " where u.deleted = false");


+ 3
- 1
src/main/java/com/ffii/fpsms/modules/user/service/pojo/UserRecord.java Просмотреть файл

@@ -24,6 +24,7 @@ public class UserRecord {
private String phone1;
private String phone2;
private String remarks;
private String staffNo;

public Integer getId() {
return id;
@@ -151,5 +152,6 @@ public class UserRecord {
public void setRemarks(String remarks) {
this.remarks = remarks;
}

public String getStaffNo() { return staffNo; }
public void setStaffNo(String staffNo) { this.staffNo = staffNo; }
}

+ 5
- 0
src/main/java/com/ffii/fpsms/modules/user/web/ExportUserQrCodeRequest.kt Просмотреть файл

@@ -0,0 +1,5 @@
package com.ffii.fpsms.modules.user.web

data class ExportUserQrCodeRequest(
val userIds: List<Long>
)

+ 26
- 1
src/main/java/com/ffii/fpsms/modules/user/web/UserController.java Просмотреть файл

@@ -42,6 +42,14 @@ import com.ffii.fpsms.modules.user.req.UpdateUserReq;
import com.ffii.fpsms.modules.user.service.UserService;
import com.ffii.fpsms.modules.user.service.res.LoadUserRes;

import com.ffii.fpsms.modules.user.web.ExportUserQrCodeRequest;
import com.ffii.fpsms.modules.user.service.UserQrCodeService;
import jakarta.servlet.http.HttpServletResponse;
import net.sf.jasperreports.engine.JasperExportManager;
import net.sf.jasperreports.engine.JasperPrint;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;

@@ -53,14 +61,17 @@ public class UserController{
private UserService userService;
private PasswordEncoder passwordEncoder;
private SettingsService settingsService;
private UserQrCodeService userQrCodeService;
public UserController(
UserService userService,
PasswordEncoder passwordEncoder,
SettingsService settingsService) {
SettingsService settingsService,
UserQrCodeService userQrCodeService) {
this.userService = userService;
this.passwordEncoder = passwordEncoder;
this.settingsService = settingsService;
this.userQrCodeService = userQrCodeService;
}
// @Operation(summary = "list user", responses = { @ApiResponse(responseCode = "200"),
@@ -229,6 +240,20 @@ public class UserController{
return new PasswordRule(settingsService);
}

@PostMapping("/export-qrcode")
public void exportQrCode(@Valid @RequestBody ExportUserQrCodeRequest request, HttpServletResponse response)
throws Exception {
response.setCharacterEncoding("utf-8");
response.setContentType("application/pdf");
OutputStream out = response.getOutputStream();
Map<String, Object> pdf = userQrCodeService.exportUserQrCode(request);
JasperPrint jasperPrint = (JasperPrint) pdf.get("report");
String fileName = (String) pdf.get("fileName");
response.addHeader("Content-Disposition", "attachment; filename=\"" + fileName + "_qrcode.pdf\"");
out.write(JasperExportManager.exportReportToPdf(jasperPrint));
out.flush();
}

public static class AdminChangePwdReq {
private Long id;
@NotBlank


+ 42
- 0
src/main/resources/qrCodeHandle/qrCodeHandle.jrxml Просмотреть файл

@@ -0,0 +1,42 @@
<?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="qrCodeHandle" pageWidth="595" pageHeight="842" columnWidth="555" leftMargin="20" rightMargin="20" topMargin="80" bottomMargin="20" uuid="c2f7cd27-3725-47ce-ac85-d8a38dc906fa">
<property name="com.jaspersoft.studio.data.defaultdataadapter" value="One Empty Record"/>
<property name="com.jaspersoft.studio.unit." value="pixel"/>
<property name="com.jaspersoft.studio.unit.pageHeight" value="pixel"/>
<property name="com.jaspersoft.studio.unit.pageWidth" value="pixel"/>
<property name="com.jaspersoft.studio.unit.topMargin" value="pixel"/>
<property name="com.jaspersoft.studio.unit.bottomMargin" value="pixel"/>
<property name="com.jaspersoft.studio.unit.leftMargin" value="pixel"/>
<property name="com.jaspersoft.studio.unit.rightMargin" value="pixel"/>
<property name="com.jaspersoft.studio.unit.columnWidth" value="pixel"/>
<property name="com.jaspersoft.studio.unit.columnSpacing" value="pixel"/>
<queryString>
<![CDATA[]]>
</queryString>
<field name="username" class="java.lang.String"/>
<field name="staffNo" class="java.lang.String"/>
<field name="qrCode" class="java.awt.Image"/>
<background>
<band splitType="Stretch"/>
</background>
<detail>
<band height="670" splitType="Stretch">
<textField isStretchWithOverflow="true" isBlankWhenNull="false">
<reportElement stretchType="RelativeToTallestObject" x="37" y="0" width="480" height="120" uuid="e3faf8de-84ba-423f-b6cf-84ba21598686">
<property name="com.jaspersoft.studio.unit.x" value="px"/>
<property name="com.jaspersoft.studio.unit.width" value="px"/>
<property name="com.jaspersoft.studio.unit.height" value="px"/>
</reportElement>
<textElement textAlignment="Center" verticalAlignment="Middle">
<font size="54" isBold="true" fontName="微軟正黑體" pdfEncoding="Identity-H" isPdfEmbedded="true"/>
</textElement>
<textFieldExpression><![CDATA[$F{username}]]></textFieldExpression>
</textField>
<image>
<reportElement x="27" y="120" width="500" height="500" uuid="b1a8ee23-9f0f-4014-9996-e0025222dcd2"/>
<imageExpression><![CDATA[$F{qrCode}]]></imageExpression>
</image>
</band>
</detail>
</jasperReport>

Загрузка…
Отмена
Сохранить