Quellcode durchsuchen

Merge remote-tracking branch 'origin/master'

# Conflicts:
#	src/main/java/com/ffii/fpsms/modules/pickOrder/entity/TruckRepository.kt
master
CANCERYS\kw093 vor 3 Wochen
Ursprung
Commit
0409b59363
20 geänderte Dateien mit 586 neuen und 133 gelöschten Zeilen
  1. +18
    -2
      src/main/java/com/ffii/fpsms/m18/service/M18MasterDataService.kt
  2. +41
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderRepository.kt
  3. +19
    -0
      src/main/java/com/ffii/fpsms/modules/jobOrder/service/JobOrderService.kt
  4. +14
    -0
      src/main/java/com/ffii/fpsms/modules/master/entity/EquipmentDetail.kt
  5. +1
    -0
      src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLineRepository.kt
  6. +9
    -3
      src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleRepository.kt
  7. +1
    -1
      src/main/java/com/ffii/fpsms/modules/master/entity/ShopRepository.kt
  8. +2
    -0
      src/main/java/com/ffii/fpsms/modules/master/entity/projections/ProdScheduleInfo.kt
  9. +1
    -0
      src/main/java/com/ffii/fpsms/modules/master/entity/projections/ShopAndTruck.kt
  10. +76
    -4
      src/main/java/com/ffii/fpsms/modules/master/service/EquipmentDetailService.kt
  11. +33
    -19
      src/main/java/com/ffii/fpsms/modules/master/service/EquipmentService.kt
  12. +12
    -1
      src/main/java/com/ffii/fpsms/modules/master/service/ItemsService.kt
  13. +227
    -97
      src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt
  14. +13
    -4
      src/main/java/com/ffii/fpsms/modules/master/web/EquipmentDetailController.kt
  15. +6
    -0
      src/main/java/com/ffii/fpsms/modules/master/web/UpdateMaintenanceRequest.kt
  16. +8
    -0
      src/main/java/com/ffii/fpsms/modules/master/web/models/NewItemRequest.kt
  17. +36
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/entity/TruckRepository.kt
  18. +25
    -2
      src/main/java/com/ffii/fpsms/modules/pickOrder/service/TruckService.kt
  19. +40
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/web/TruckController.kt
  20. +4
    -0
      src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/SaveTruckRequest.kt

+ 18
- 2
src/main/java/com/ffii/fpsms/m18/service/M18MasterDataService.kt Datei anzeigen

@@ -165,7 +165,15 @@ open class M18MasterDataService(
maxQty = null,
m18Id = id,
m18LastModifyDate = commonUtils.timestampToLocalDateTime(pro.lastModifyDate),
qcCategoryId = null
qcCategoryId = null,
store_id = null,
warehouse = null,
area = null,
slot = null,
LocationCode = null,
isEgg = null,
isFee = null,
isBag = null
)

val savedItem = itemsService.saveItem(saveItemRequest)
@@ -260,7 +268,15 @@ open class M18MasterDataService(
maxQty = null,
m18Id = item.id,
m18LastModifyDate = commonUtils.timestampToLocalDateTime(pro.lastModifyDate),
qcCategoryId = null
qcCategoryId = null,
store_id = null,
warehouse = null,
area = null,
slot = null,
LocationCode = null,
isEgg = null,
isFee = null,
isBag = null
)

val savedItem = itemsService.saveItem(saveItemRequest)


+ 41
- 0
src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderRepository.kt Datei anzeigen

@@ -172,10 +172,51 @@ interface JobOrderRepository : AbstractRepository<JobOrder, Long> {
left join inventory_lot_line ill on ill.id = sol.inventoryLotLineId
left join inventory_lot il on il.id = ill.inventoryLotId
where jo.prodScheduleLineId = :prodScheduleLineId
and jo.actualEnd is null
group by jo.id, uc2.udfudesc
limit 1
"""
)
fun findJobOrderByProdScheduleLineId(prodScheduleLineId: Long): JobOrderDetailWithJsonString?;

@Query(
nativeQuery = true,
value = """
select
jo.id,
jo.code,
b.name,
jo.reqQty,
b.outputQtyUom as unit,
uc2.udfudesc as uom,
json_arrayagg(
json_object(
'id', jobm.id,
'code', i.code,
'name', i.name,
'lotNo', il.lotNo,
'reqQty', jobm.reqQty,
'uom', uc.udfudesc,
'status', jobm.status
)
) as pickLines,
jo.status
from job_order jo
left join bom b on b.id = jo.bomId
left join item_uom iu on b.itemId = iu.itemId and iu.salesUnit = true
left join uom_conversion uc2 on uc2.id = iu.uomId
left join job_order_bom_material jobm on jo.id = jobm.jobOrderId
left join items i on i.id = jobm.itemId
left join uom_conversion uc on uc.id = jobm.uomId
left join stock_out_line sol on sol.id = jobm.stockOutLineId
left join inventory_lot_line ill on ill.id = sol.inventoryLotLineId
left join inventory_lot il on il.id = ill.inventoryLotId
where b.itemId = :itemId
and jo.actualEnd is null
group by jo.id, uc2.udfudesc
limit 1
"""
)
fun findJobOrderByItemId(itemId: Long): JobOrderDetailWithJsonString?;

}

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

@@ -288,6 +288,25 @@ open class JobOrderService(
)
}

open fun jobOrderDetailByItemId(itemId: Long): JobOrderDetail {
val sqlResult = jobOrderRepository.findJobOrderByItemId(itemId) ?: throw NoSuchElementException("Job Order not found with itemId: $itemId");

val type = object : TypeToken<List<JobOrderDetailPickLine>>() {}.type
val jsonResult = sqlResult.pickLines?.let { GsonUtils.stringToJson<List<JobOrderDetailPickLine>>(it, type) }
return JobOrderDetail(
id = sqlResult.id,
code = sqlResult.code,
itemCode = sqlResult.itemCode,
name = sqlResult.name,
reqQty = sqlResult.reqQty,
uom = sqlResult.uom,
pickLines = jsonResult,
status = sqlResult.status,
shortUom = sqlResult.shortUom
)
}


open fun jobOrderDetailByCode(code: String): JobOrderDetail {
val sqlResult = jobOrderRepository.findJobOrderDetailByCode(code) ?: throw NoSuchElementException("Job Order not found: $code");



+ 14
- 0
src/main/java/com/ffii/fpsms/modules/master/entity/EquipmentDetail.kt Datei anzeigen

@@ -6,6 +6,7 @@ import jakarta.persistence.Entity
import jakarta.persistence.Table
import jakarta.validation.constraints.NotNull
import jakarta.validation.constraints.Size
import java.time.LocalDateTime

@Table(name = "equipment_detail")
@Entity
@@ -15,6 +16,19 @@ open class EquipmentDetail : BaseEntity<Long>() {
@Column(name = "equipmentCode", nullable = true, length = 255)
open var equipmentCode: String? = null

@Column(name = "repairAndMaintenanceStatus", nullable = true)
open var repairAndMaintenanceStatus: Boolean? = null

@Column(name = "latestRepairAndMaintenanceDate", nullable = true)
open var latestRepairAndMaintenanceDate: LocalDateTime? = null

@Column(name = "lastRepairAndMaintenanceDate", nullable = true)
open var lastRepairAndMaintenanceDate: LocalDateTime? = null

@Size(max = 255)
@Column(name = "repairAndMaintenanceRemarks", nullable = true, length = 255)
open var repairAndMaintenanceRemarks: String? = null

@Size(max = 30)
@NotNull
@Column(name = "code", nullable = false, length = 30)


+ 1
- 0
src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLineRepository.kt Datei anzeigen

@@ -39,6 +39,7 @@ interface ProductionScheduleLineRepository : AbstractRepository<ProductionSchedu
group by psl.id, bm.id, i.id, pp.proportion
""")
fun getBomMaterials(id: Long): List<DetailedProdScheduleLineBomMaterialInterface>?
@Query("""
SELECT psl FROM ProductionScheduleLine psl
JOIN psl.productionSchedule ps


+ 9
- 3
src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleRepository.kt Datei anzeigen

@@ -7,6 +7,7 @@ import org.springframework.data.domain.Pageable
import org.springframework.data.jpa.repository.Query
import org.springframework.stereotype.Repository
import java.time.LocalDateTime
import java.time.LocalDate

@Repository
interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule, Long> {
@@ -117,8 +118,11 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule,
where rn = 1
and produceAt is not null
and scheduleAt in (select max(ps2.scheduleAt) from production_schedule ps2 group by ps2.produceAt)
-- and (:scheduleAt = '' or datediff(scheduleAt, coalesce(:scheduleAt, scheduleAt)) = 0)
and (:produceAt = '' or datediff(produceAt, coalesce(:produceAt, produceAt)) = 0)
AND (
:produceAt IS NULL
OR :produceAt = ''
OR Date(produceAt) >= :produceAt
)
and (:totalEstProdCount is null or :totalEstProdCount = '' or totalEstProdCount = :totalEstProdCount)
and (coalesce(:types) is null or type in :types)
order by id ASC;
@@ -126,7 +130,7 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule,
)
fun findProdScheduleInfoByProduceAtByPage(
// scheduleAt: String?,
produceAt: String?,
produceAt: LocalDate,
totalEstProdCount: Double?,
types: List<String>?,
pageable: Pageable
@@ -200,6 +204,7 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule,
select
prod.id,
prod.scheduleAt,
prod.produceAt,
prod.totalFGType,
prod.totalEstProdCount,
prod.daysLeft,
@@ -237,6 +242,7 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule,
select
ps.id,
ps.scheduleAt,
ps.produceAt,
ps.totalFGType,
ps.totalEstProdCount,
psl.approverId is not null as approved,


+ 1
- 1
src/main/java/com/ffii/fpsms/modules/master/entity/ShopRepository.kt Datei anzeigen

@@ -33,7 +33,7 @@ interface ShopRepository : AbstractRepository<Shop, Long> {
@Query(
nativeQuery = true,
value = """
SELECT s.id, s.code, s.name, s.contactNo, s.contactEmail, s.contactName, s.addr1, s.addr2, s.addr3, s.type, t.TruckLanceCode, t.LoadingSequence, t.districtReference,t.Store_id, t.remark
SELECT s.id, s.code, s.name, s.contactNo, s.contactEmail, s.contactName, s.addr1, s.addr2, s.addr3, s.type, t.TruckLanceCode, t.DepartureTime as departureTime, t.LoadingSequence, t.districtReference, t.Store_id as Store_id, t.remark
FROM shop s LEFT JOIN truck t ON s.id = t.shopId
WHERE s.type = 'shop'
AND s.deleted = false;


+ 2
- 0
src/main/java/com/ffii/fpsms/modules/master/entity/projections/ProdScheduleInfo.kt Datei anzeigen

@@ -18,6 +18,7 @@ interface ProdScheduleInfo {
// Detailed Production Schedule With Line
interface DetailedProdScheduleWithLineWithJsonString {
val id: Long?
val produceAt: LocalDateTime?
val scheduleAt: LocalDateTime?
val totalEstProdCount: BigDecimal?
val totalFGType: Long?
@@ -27,6 +28,7 @@ interface DetailedProdScheduleWithLineWithJsonString {

data class DetailedProdScheduleWithLine (
val id: Long?,
val produceAt: LocalDateTime?,
val scheduleAt: LocalDateTime?,
val totalEstProdCount: BigDecimal?,
val totalFGType: Long?,


+ 1
- 0
src/main/java/com/ffii/fpsms/modules/master/entity/projections/ShopAndTruck.kt Datei anzeigen

@@ -19,4 +19,5 @@ interface ShopAndTruck {
val districtReference: Long?
val Store_id: String?
val remark: String?
val truckId: Long?
}

+ 76
- 4
src/main/java/com/ffii/fpsms/modules/master/service/EquipmentDetailService.kt Datei anzeigen

@@ -7,6 +7,9 @@ import com.ffii.fpsms.modules.master.entity.EquipmentDetailRepository
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import com.ffii.fpsms.modules.master.web.models.NewEquipmentDetailRequest
import com.ffii.fpsms.modules.master.web.models.UpdateMaintenanceRequest
import java.time.LocalDateTime

@Service
open class EquipmentDetailService(
private val jdbcDao: JdbcDao,
@@ -18,21 +21,54 @@ open class EquipmentDetailService(
}

open fun getEquipmentDetailsByPage(args: Map<String, Any>): List<Map<String, Any>> {
println("Search args: $args")
val sql = StringBuilder(
"SELECT e.id, e.code, e.name, e.description FROM equipment_detail e WHERE e.deleted = FALSE"
"""
SELECT
e.id AS id,
e.code AS code,
e.name AS name,
e.description AS description,
e.equipmentCode AS equipmentCode,
e.repairAndMaintenanceStatus AS repairAndMaintenanceStatus,
e.latestRepairAndMaintenanceDate AS latestRepairAndMaintenanceDate,
e.lastRepairAndMaintenanceDate AS lastRepairAndMaintenanceDate,
e.repairAndMaintenanceRemarks AS repairAndMaintenanceRemarks
FROM equipment_detail e
WHERE e.deleted = FALSE
"""
)
if (args.containsKey("code")) {
sql.append(" AND e.code like :code ")
// Handle combined search for code and equipmentCode (OR logic)
val searchTerm = args["equipmentCode"] as? String
val codeTerm = args["code"] as? String
if (searchTerm != null && codeTerm != null && searchTerm == codeTerm) {
// When both are provided with the same value, search using OR
sql.append(" AND (LOWER(e.code) LIKE LOWER(:equipmentCode) OR LOWER(e.equipmentCode) LIKE LOWER(:equipmentCode)) ")
} else {
// Otherwise, use individual conditions
if (codeTerm != null && (searchTerm == null || searchTerm != codeTerm)) {
sql.append(" AND LOWER(e.code) LIKE LOWER(:code) ")
}
if (searchTerm != null && (codeTerm == null || searchTerm != codeTerm)) {
sql.append(" AND LOWER(e.equipmentCode) LIKE LOWER(:equipmentCode) ")
}
}
if (args.containsKey("id")) {
sql.append(" AND e.id like :id ")
}
if (args.containsKey("name")) {
sql.append(" AND e.name like :name ")
sql.append(" AND LOWER(e.name) LIKE LOWER(:name) ")
}
if (args.containsKey("description")) {
sql.append(" AND e.description like :description ")
}
if (args.containsKey("repairAndMaintenanceStatus")) {
sql.append(" AND e.repairAndMaintenanceStatus = :repairAndMaintenanceStatus ")
}
return jdbcDao.queryForList(sql.toString(), args)
}

@@ -51,6 +87,42 @@ open class EquipmentDetailService(
open fun findByDescription(description: String): EquipmentDetail? {
return equipmentDetailRepository.findByDescriptionAndDeletedIsFalse(description)
}

@Transactional
open fun updateMaintenanceAndRepair(
id: Long,
request: UpdateMaintenanceRequest
): EquipmentDetail {
val equipmentDetail = findById(id)
?: throw IllegalArgumentException("Equipment detail not found with id: $id")
// Store the previous status and latest date before updating
val previousStatus = equipmentDetail.repairAndMaintenanceStatus
val previousLatestDate = equipmentDetail.latestRepairAndMaintenanceDate
// Update status and remarks
equipmentDetail.repairAndMaintenanceStatus = request.repairAndMaintenanceStatus
equipmentDetail.repairAndMaintenanceRemarks = request.repairAndMaintenanceRemarks
// Handle date updates based on status change
when {
// Changing from "是" (true) to "否" (false)
previousStatus == true && request.repairAndMaintenanceStatus == false -> {
// Save current latestRepairAndMaintenanceDate to lastRepairAndMaintenanceDate
equipmentDetail.lastRepairAndMaintenanceDate = previousLatestDate
// Update latestRepairAndMaintenanceDate to current time
equipmentDetail.latestRepairAndMaintenanceDate = LocalDateTime.now()
}
// Changing from "否" (false) to "是" (true)
// Keep dates unchanged - no action needed
previousStatus == false && request.repairAndMaintenanceStatus == true -> {
// Dates remain unchanged
}
// Other cases (null to true/false, or same status) - no date changes
}
return equipmentDetailRepository.saveAndFlush(equipmentDetail)
}
/*
@Transactional
open fun saveEquipmentDetail(request: NewEquipmentDetailRequest): EquipmentDetail {


+ 33
- 19
src/main/java/com/ffii/fpsms/modules/master/service/EquipmentService.kt Datei anzeigen

@@ -18,26 +18,40 @@ open class EquipmentService(
}

open fun getEquipmentsByPage(args: Map<String, Any>): List<Map<String, Any>> {
val sql = StringBuilder(
"SELECT e.id, e.code, e.name, e.description, e.equipmentTypeId FROM equipment e WHERE e.deleted = FALSE"
)
if (args.containsKey("code")) {
sql.append(" AND e.code like :code ")
}
if (args.containsKey("id")) {
sql.append(" AND e.id like :id ")
}
if (args.containsKey("name")) {
sql.append(" AND e.name like :name ")
}
if (args.containsKey("description")) {
sql.append(" AND e.description like :description ")
}
if (args.containsKey("equipmentTypeId")) {
sql.append(" AND e.equipmentTypeId like :equipmentTypeId ")
}
return jdbcDao.queryForList(sql.toString(), args)
val sql = StringBuilder(
"""
SELECT
e.id AS id,
e.code AS code,
e.name AS name,
e.description AS description,
e.equipmentTypeId AS equipmentTypeId,
ed.repairAndMaintenanceStatus AS repairAndMaintenanceStatus,
ed.latestRepairAndMaintenanceDate AS latestRepairAndMaintenanceDate,
ed.lastRepairAndMaintenanceDate AS lastRepairAndMaintenanceDate,
ed.repairAndMaintenanceRemarks AS repairAndMaintenanceRemarks
FROM equipment e
LEFT JOIN equipment_detail ed ON e.code = ed.equipmentCode
WHERE e.deleted = FALSE
"""
)
if (args.containsKey("code")) {
sql.append(" AND e.code like :code ")
}
if (args.containsKey("id")) {
sql.append(" AND e.id like :id ")
}
if (args.containsKey("name")) {
sql.append(" AND e.name like :name ")
}
if (args.containsKey("description")) {
sql.append(" AND e.description like :description ")
}
if (args.containsKey("equipmentTypeId")) {
sql.append(" AND e.equipmentTypeId like :equipmentTypeId ")
}
return jdbcDao.queryForList(sql.toString(), args)
}

open fun findById(id: Long): Equipment? {
return equipmentRepository.findByIdAndDeletedFalse(id)


+ 12
- 1
src/main/java/com/ffii/fpsms/modules/master/service/ItemsService.kt Datei anzeigen

@@ -392,7 +392,10 @@ open class ItemsService(
"i.id, " +
"i.code, " +
"i.name, " +
"i.description " +
"i.description, " +
"i.type, " +
"i.`LocationCode` as LocationCode, " +
"i.`qcCategoryId` as qcCategoryId " +
"FROM items i " +
"WHERE i.deleted = FALSE"
);
@@ -512,6 +515,14 @@ open class ItemsService(
this.qcCategory = qcCategory
m18Id = request.m18Id ?: this.m18Id
m18LastModifyDate = request.m18LastModifyDate ?: this.m18LastModifyDate
store_id = request.store_id
warehouse = request.warehouse
area = request.area
slot = request.slot
LocationCode = request.LocationCode
isEgg = request.isEgg ?: false
isFee = request.isFee ?: false
isBag = request.isBag ?: false
}
logger.info("saving item: $item")
val savedItem = itemsRepository.saveAndFlush(item)


+ 227
- 97
src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt Datei anzeigen

@@ -11,6 +11,7 @@ import com.ffii.fpsms.modules.jobOrder.service.JobOrderService
import com.ffii.fpsms.modules.jobOrder.web.model.CreateJobOrderBomMaterialRequest
import com.ffii.fpsms.modules.jobOrder.web.model.CreateJobOrderProcessRequest
import com.ffii.fpsms.modules.jobOrder.web.model.CreateJobOrderRequest
import com.ffii.fpsms.modules.jobOrder.entity.projections.JobOrderDetail
import com.ffii.fpsms.modules.master.entity.*
import com.ffii.fpsms.modules.master.entity.projections.*
import com.ffii.fpsms.modules.master.web.models.MessageResponse
@@ -46,10 +47,16 @@ 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

// === POI IMPORTS FOR EXCEL EXPORT WITH PRINT SETUP ===
import org.apache.poi.ss.usermodel.*
import org.apache.poi.ss.usermodel.IndexedColors
import org.apache.poi.ss.usermodel.FillPatternType
import org.apache.poi.ss.usermodel.VerticalAlignment
import org.apache.poi.xssf.usermodel.XSSFWorkbook
import org.apache.poi.xssf.usermodel.XSSFPrintSetup
import java.io.ByteArrayOutputStream
import java.sql.Timestamp

@Service
open class ProductionScheduleService(
@@ -118,10 +125,15 @@ open class ProductionScheduleService(
open fun allDetailedProdSchedulesByPage(request: SearchProdScheduleRequest): RecordsRes<ProdScheduleInfo> {
val pageable = PageRequest.of(request.pageNum ?: 0, request.pageSize ?: 10);

val produceAtDate: LocalDate = request.produceAt?.takeIf { it.isNotBlank() }
?.let {
LocalDate.parse(it.trim())
}
?: LocalDate.now()

val response = productionScheduleRepository.findProdScheduleInfoByProduceAtByPage(
produceAt = request.produceAt,
produceAt = produceAtDate!!,
totalEstProdCount = request.totalEstProdCount,
// types = listOf("detailed", "manual"),
types = request.types,
pageable = pageable
)
@@ -329,6 +341,7 @@ open class ProductionScheduleService(

return DetailedProdScheduleWithLine(
id = sqlResult.id,
produceAt = sqlResult.produceAt,
scheduleAt = sqlResult.scheduleAt,
totalEstProdCount = sqlResult.totalEstProdCount,
totalFGType = sqlResult.totalFGType,
@@ -397,57 +410,63 @@ open class ProductionScheduleService(
val prodScheduleLine = productionScheduleLineRepository.findById(prodScheduleLineId).getOrNull()
?: throw NoSuchElementException("Production Schedule Line with ID $prodScheduleLineId not found.")

try {
jobOrderService.jobOrderDetailByPsId(prodScheduleLineId)
} catch (e: NoSuchElementException) {
// 3. Fetch BOM, handling nullability safely
val item = prodScheduleLine.item
?: throw IllegalStateException("Item object is missing for Production Schedule Line $prodScheduleLineId.")
// 3. Fetch BOM, handling nullability safely
val item = prodScheduleLine.item
?: throw IllegalStateException("Item object is missing for Production Schedule Line $prodScheduleLineId.")

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

val bom = bomService.findByItemId(itemId)
try {
jobOrderService.jobOrderDetailByItemId(itemId)
logger.info("jobOrderDetailByItemId ok itemId:$itemId")
} catch (e: NoSuchElementException) {
//only do with no JO is working
logger.info("NoSuchElementException itemId:$itemId")
try {
jobOrderService.jobOrderDetailByItemId(itemId)
} catch (e: NoSuchElementException) {
val bom = bomService.findByItemId(itemId)
?: throw NoSuchElementException("BOM not found for Item ID $itemId.")

// 4. Update Prod Schedule Line fields
prodScheduleLine.apply {
// Use bom.outputQty, ensuring it's treated as Double for prodQty
prodQty = bom.outputQty?.toDouble()
?: throw IllegalStateException("BOM output quantity is null for Item ID $itemId.")
approverId = approver?.id
}
productionScheduleLineRepository.save(prodScheduleLine)

// 5. Logging (optional but kept)
logger.info("prodScheduleLine.prodQty: ${prodScheduleLine.prodQty}")
logger.info("bom?.outputQty: ${bom.outputQty} ${bom.outputQtyUom}")

logger.info("[releaseProdSchedule] prodScheduleLine.needNoOfJobOrder:" + prodScheduleLine.needNoOfJobOrder)
//repeat(prodScheduleLine.needNoOfJobOrder) {
// 6. Create Job Order
val joRequest = CreateJobOrderRequest(
bomId = bom.id, // 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
prodScheduleLineId = prodScheduleLine.id!!
)

// Assuming createJobOrder returns the created Job Order (jo)
val jo = jobOrderService.createJobOrder(joRequest)
// 4. Update Prod Schedule Line fields
prodScheduleLine.apply {
// Use bom.outputQty, ensuring it's treated as Double for prodQty
prodQty = bom.outputQty?.toDouble()
?: throw IllegalStateException("BOM output quantity is null for Item ID $itemId.")
approverId = approver?.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())
//}
productionScheduleLineRepository.save(prodScheduleLine)

// 5. Logging (optional but kept)
logger.info("prodScheduleLine.prodQty: ${prodScheduleLine.prodQty}")
logger.info("bom?.outputQty: ${bom.outputQty} ${bom.outputQtyUom}")

logger.info("[releaseProdSchedule] prodScheduleLine.needNoOfJobOrder:" + prodScheduleLine.needNoOfJobOrder)
//repeat(prodScheduleLine.needNoOfJobOrder) {
// 6. Create Job Order
val joRequest = CreateJobOrderRequest(
bomId = bom.id, // 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
prodScheduleLineId = prodScheduleLine.id!!
)

// Assuming createJobOrder returns the created Job Order (jo)
val jo = jobOrderService.createJobOrder(joRequest)
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())
//}
}
}

@@ -719,7 +738,11 @@ open class ProductionScheduleService(
var machineCap = 10000.0
var needQtyList = getNeedQty()
//remove the production schedule >= today
clearTodayAndFutureProdSchedule()

println("needQtyList - " + needQtyList);
//##### The 22000, 10000 machine cap just means the max warehouse storage qty, not production qty cap
//##### The total production qty of the date is 10000 due to machine cap
//##### search all items with bom to consider need or no need production
@@ -1338,60 +1361,106 @@ open class ProductionScheduleService(

}

fun exportProdScheduleToExcel(lines: List<Map<String, Any>>, lineMats: List<Map<String, Any>>): ByteArray {
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
// Header style
val headerStyle = workbook.createCellStyle().apply {
fillForegroundColor = IndexedColors.GREY_25_PERCENT.index
fillPattern = FillPatternType.SOLID_FOREGROUND
wrapText = true
verticalAlignment = VerticalAlignment.CENTER
val font = workbook.createFont()
font.bold = true
setFont(font)
}

// 3. Create Production Worksheets
// Body style
val wrapStyle = workbook.createCellStyle().apply {
wrapText = true
verticalAlignment = VerticalAlignment.TOP
}

// Group production lines by date
val groupedData = lines.groupBy {
val produceAt = it["produceAt"]
when (produceAt) {
is LocalDateTime -> produceAt.toLocalDate().toString()
is Timestamp -> produceAt.toLocalDateTime().toLocalDate().toString()
is String -> produceAt.take(10)
else -> produceAt?.toString()?.substring(0, 10) ?: "Unknown_Date"
}
}

// Production sheets (one per date)
groupedData.forEach { (dateKey, dailyLines) ->
val sheetName = dateKey.replace("[/\\\\?*:\\[\\]]".toRegex(), "-")
val sheet = workbook.createSheet(sheetName)
val safeSheetName = dateKey.replace(Regex("[/\\\\?*:\\[\\]]"), "-").take(31)
val sheet = workbook.createSheet(safeSheetName)

val headers = listOf(
"Item Name", "Avg Qty Last Month", "Stock Qty", "Days Left",
"Output Qty", "Batch Need", "Priority"
)

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

// Data rows
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"]))
row.heightInPoints = 35f // Slightly taller for portrait readability

row.createCell(0).apply { setCellValue(line["itemName"]?.toString() ?: ""); cellStyle = wrapStyle }
row.createCell(1).apply { setCellValue(asDouble(line["avgQtyLastMonth"])); cellStyle = wrapStyle }
row.createCell(2).apply { setCellValue(asDouble(line["stockQty"])); cellStyle = wrapStyle }
row.createCell(3).apply { setCellValue(asDouble(line["daysLeft"])); cellStyle = wrapStyle }
row.createCell(4).apply { setCellValue(asDouble(line["outputdQty"] ?: line["outputQty"])); cellStyle = wrapStyle }
row.createCell(5).apply { setCellValue(asDouble(line["batchNeed"])); cellStyle = wrapStyle }
row.createCell(6).apply { setCellValue(asDouble(line["itemPriority"])); cellStyle = wrapStyle }
}

for (i in headers.indices) { sheet.autoSizeColumn(i) }
// Auto-size with wider limits for portrait
for (i in headers.indices) {
sheet.autoSizeColumn(i)
val maxWidth = when (i) {
0 -> 35 * 256 // Item Name can be longer
else -> 18 * 256
}
if (sheet.getColumnWidth(i) > maxWidth) {
sheet.setColumnWidth(i, maxWidth)
}
}

// === PORTRAIT PRINT SETUP ===
val printSetup = sheet.printSetup
printSetup.paperSize = XSSFPrintSetup.A4_PAPERSIZE
printSetup.landscape = false // ← Portrait mode
printSetup.fitWidth = 1.toShort() // Crucial: scale to fit width
printSetup.fitHeight = 0.toShort() // Allow multiple pages tall

sheet.fitToPage = true
sheet.horizontallyCenter = true
sheet.setMargin(Sheet.LeftMargin, 0.5)
sheet.setMargin(Sheet.RightMargin, 0.5)
sheet.setMargin(Sheet.TopMargin, 0.7)
sheet.setMargin(Sheet.BottomMargin, 0.7)
}

// 4. Create Material Summary Worksheet
// === MATERIAL SUMMARY SHEET - PORTRAIT OPTIMIZED ===
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",
"Mat Code", "Mat Name", "Required Qty", "Total Qty Need",
"UoM", "Purchased Qty", "On Hand Qty", "Unavailable Qty",
"Related Item Code", "Related Item Name"
)

@@ -1399,37 +1468,73 @@ open class ProductionScheduleService(
matHeaders.forEachIndexed { i, title ->
matHeaderRow.createCell(i).apply {
setCellValue(title)
setCellStyle(headerStyle)
cellStyle = headerStyle
}
}

lineMats.forEachIndexed { index, rowData ->
val row = matSheet.createRow(index + 1)
row.heightInPoints = 35f

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() ?: "")
}
val values = listOf<Any>(
rowData["matCode"]?.toString() ?: "",
rowData["matName"]?.toString() ?: "",
requiredQty,
totalNeed,
rowData["uomName"]?.toString() ?: "",
purchased,
onHand,
asDouble(rowData["unavailableQty"]),
rowData["itemCode"]?.toString() ?: "",
rowData["itemName"]?.toString() ?: ""
)

for (i in matHeaders.indices) { matSheet.autoSizeColumn(i) }
values.forEachIndexed { i, value ->
val cell = row.createCell(i)
when (value) {
is String -> cell.setCellValue(value)
is Number -> cell.setCellValue(value.toDouble())
else -> cell.setCellValue("")
}
cell.cellStyle = wrapStyle
}
}

// 5. Finalize and Return
// Manual column widths optimized for PORTRAIT A4
matSheet.setColumnWidth(0, 16 * 256) // Mat Code
matSheet.setColumnWidth(1, 32 * 256) // Mat Name
matSheet.setColumnWidth(2, 14 * 256) // Required Qty
matSheet.setColumnWidth(3, 14 * 256) // Total Qty Need
matSheet.setColumnWidth(4, 10 * 256) // UoM
matSheet.setColumnWidth(5, 14 * 256) // Purchased Qty
matSheet.setColumnWidth(6, 14 * 256) // On Hand Qty
matSheet.setColumnWidth(7, 14 * 256) // Unavailable Qty
matSheet.setColumnWidth(8, 22 * 256) // Related Item Code
matSheet.setColumnWidth(9, 40 * 256) // Related Item Name (longest)

// Portrait print setup
val matPrintSetup = matSheet.printSetup
matPrintSetup.paperSize = XSSFPrintSetup.A4_PAPERSIZE
matPrintSetup.landscape = false // ← Portrait
matPrintSetup.fitWidth = 1.toShort()
matPrintSetup.fitHeight = 0.toShort()

matSheet.fitToPage = true
matSheet.horizontallyCenter = true
matSheet.setMargin(Sheet.LeftMargin, 0.5)
matSheet.setMargin(Sheet.RightMargin, 0.5)
matSheet.setMargin(Sheet.TopMargin, 0.7)
matSheet.setMargin(Sheet.BottomMargin, 0.7)

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

@@ -1514,5 +1619,30 @@ open class ProductionScheduleService(
return jdbcDao.queryForList(sql, args);
}

@Transactional
open fun clearTodayAndFutureProdSchedule() {
val deleteLinesSql = """
DELETE FROM production_schedule_line
WHERE prodScheduleId IN (
SELECT id FROM production_schedule
WHERE DATE(produceAt) >= DATE(NOW())
)
""".trimIndent()

val deleteSchedulesSql = """
DELETE FROM production_schedule
WHERE DATE(produceAt) >= DATE(NOW())
""".trimIndent()

// Execute child delete first
jdbcDao.executeUpdate(deleteLinesSql)

// Then delete parent schedules
jdbcDao.executeUpdate(deleteSchedulesSql)

// Optional: log the action (if you have logging setup)
// logger.info("Cleared all production schedules with produceAt >= today")
}
}

+ 13
- 4
src/main/java/com/ffii/fpsms/modules/master/web/EquipmentDetailController.kt Datei anzeigen

@@ -10,6 +10,8 @@ import jakarta.validation.Valid
import org.springframework.web.bind.annotation.*
import java.util.Collections.emptyList
import com.ffii.fpsms.modules.master.web.models.NewEquipmentDetailRequest
import com.ffii.fpsms.modules.master.web.models.UpdateMaintenanceRequest

@RestController
@RequestMapping("/EquipmentDetail")
class EquipmentDetailController(
@@ -43,31 +45,38 @@ fun getAllEquipmentDetailByPage(
.addStringLike("code")
.addStringLike("description")
.addStringLike("id")
.addStringLike("equipmentCode")
.addBoolean("repairAndMaintenanceStatus")
.build()

val pageSize = request.getParameter("pageSize")?.toIntOrNull() ?: 10
val pageNum = request.getParameter("pageNum")?.toIntOrNull() ?: 1

// 方法名和变量名都要和 Service 保持一致
val fullList = equipmentDetailService.getEquipmentDetailsByPage(criteriaArgs) ?: emptyList()
val paginatedList = PagingUtils.getPaginatedList(fullList, pageSize, pageNum)

return RecordsRes(paginatedList as List<Map<String, Any>>, fullList.size)
}

// 详情
@GetMapping("/details/{id}")
fun getEquipmentDetail(@PathVariable id: Long): EquipmentDetail? {
return equipmentDetailService.findById(id)
}

@PutMapping("/update/{id}")
fun updateMaintenanceAndRepair(
@PathVariable id: Long,
@RequestBody request: UpdateMaintenanceRequest
): EquipmentDetail {
return equipmentDetailService.updateMaintenanceAndRepair(id, request)
}
/*
// 新增/编辑
@PostMapping("/save")
fun saveEquipmentDetail(@Valid @RequestBody equipmentDetail: NewEquipmentDetailRequest): EquipmentDetail {
return equipmentDetailService.saveEquipmentDetail(equipmentDetail)
}
*/
// 逻辑删除
@DeleteMapping("/delete/{id}")
fun deleteEquipmentDetail(@PathVariable id: Long) {
equipmentDetailService.deleteEquipmentDetail(id)


+ 6
- 0
src/main/java/com/ffii/fpsms/modules/master/web/UpdateMaintenanceRequest.kt Datei anzeigen

@@ -0,0 +1,6 @@
package com.ffii.fpsms.modules.master.web.models

data class UpdateMaintenanceRequest(
val repairAndMaintenanceStatus: Boolean?,
val repairAndMaintenanceRemarks: String?
)

+ 8
- 0
src/main/java/com/ffii/fpsms/modules/master/web/models/NewItemRequest.kt Datei anzeigen

@@ -45,6 +45,14 @@ data class NewItemRequest(
val m18Id: Long?,
val m18LastModifyDate: LocalDateTime?,
val qcCategoryId: Long?,
val store_id: String?,
val warehouse: String?,
val area: String?,
val slot: String?,
val LocationCode: String?,
val isEgg: Boolean?,
val isFee: Boolean?,
val isBag: Boolean?,
// val type: List<NewTypeRequest>?,
// val uom: List<NewUomRequest>?,
// val weightUnit: List<NewWeightUnitRequest>?,


+ 36
- 0
src/main/java/com/ffii/fpsms/modules/pickOrder/entity/TruckRepository.kt Datei anzeigen

@@ -51,4 +51,40 @@ fun findByShopIdAndStoreIdAndDayOfWeek(
@Param("storeId") storeId: String,
@Param("dayOfWeekAbbr") dayOfWeekAbbr: String
): List<Truck>

@Query(
nativeQuery = true,
value = """
SELECT t.*
FROM truck t
INNER JOIN (
SELECT TruckLanceCode, remark, MIN(id) as min_id
FROM truck
WHERE deleted = false
AND TruckLanceCode IS NOT NULL
GROUP BY TruckLanceCode, remark
) AS unique_combos
ON t.id = unique_combos.min_id
WHERE t.deleted = false
ORDER BY t.TruckLanceCode, t.remark
"""
)
fun findAllUniqueTruckLanceCodeAndRemarkCombinations(): List<Truck>

@Query(
nativeQuery = true,
value = """
SELECT s.id as id, t.ShopCode as code, s.name as name, s.contactNo as contactNo,
s.contactEmail as contactEmail, s.contactName as contactName,
s.addr1 as addr1, s.addr2 as addr2, s.addr3 as addr3, s.type as type,
t.TruckLanceCode as truckLanceCode, t.DepartureTime as departureTime,
t.LoadingSequence as LoadingSequence, t.districtReference as districtReference,
t.Store_id as Store_id, t.remark as remark, t.id as truckId
FROM shop s INNER JOIN truck t ON s.id = t.shopId
WHERE t.TruckLanceCode = :truckLanceCode
AND t.deleted = false
AND s.deleted = false;
"""
)
fun findAllFromShopAndTruckByTruckLanceCodeAndDeletedFalse(@Param("truckLanceCode") truckLanceCode: String): List<ShopAndTruck>
}

+ 25
- 2
src/main/java/com/ffii/fpsms/modules/pickOrder/service/TruckService.kt Datei anzeigen

@@ -12,7 +12,9 @@ import com.ffii.fpsms.modules.pickOrder.web.models.SaveTruckRequest
import java.time.LocalTime
import java.time.format.DateTimeFormatter
import com.ffii.fpsms.modules.master.entity.ShopRepository
import com.ffii.fpsms.modules.master.entity.projections.ShopAndTruck
import com.ffii.fpsms.modules.pickOrder.web.models.SaveTruckLane
import com.ffii.fpsms.modules.pickOrder.web.models.UpdateLoadingSequenceRequest
import jakarta.transaction.Transactional


@@ -187,7 +189,7 @@ open class TruckService(
}

open fun findAllByShopId(shopId: Long): List<Truck> {
return truckRepository.findAllByShopId(shopId)
return truckRepository.findByShopIdAndDeletedFalse(shopId)
}

@Transactional
@@ -212,7 +214,10 @@ open class TruckService(

@Transactional
open fun deleteById(id: Long): String {
truckRepository.deleteById(id)
val deleteTruck = truckRepository.findById(id).orElseThrow().apply {
deleted = true
}
truckRepository.save(deleteTruck)
return "Truck deleted successfully with id: $id"
}

@@ -241,4 +246,22 @@ open class TruckService(
return truckRepository.save(truck)
}

open fun findAllUniqueTruckLanceCodeAndRemarkCombinations(): List<Truck> {
return truckRepository.findAllUniqueTruckLanceCodeAndRemarkCombinations()
}

open fun findAllFromShopAndTruckByTruckLanceCodeAndDeletedFalse(truckLanceCode: String): List<ShopAndTruck> {
return truckRepository.findAllFromShopAndTruckByTruckLanceCodeAndDeletedFalse(truckLanceCode)
}

@Transactional
open fun updateLoadingSequence(request: UpdateLoadingSequenceRequest): Truck {
val truck = truckRepository.findById(request.id).orElseThrow {
IllegalArgumentException("Truck not found with id: ${request.id}")
}
truck.loadingSequence = request.loadingSequence
return truckRepository.save(truck)
}

}

+ 40
- 0
src/main/java/com/ffii/fpsms/modules/pickOrder/web/TruckController.kt Datei anzeigen

@@ -1,5 +1,6 @@
package com.ffii.fpsms.modules.pickOrder.web

import com.ffii.fpsms.modules.master.entity.projections.ShopAndTruck
import com.ffii.fpsms.modules.master.web.models.MessageResponse
import com.ffii.fpsms.modules.pickOrder.entity.Truck
import org.springframework.web.bind.ServletRequestBindingException
@@ -15,6 +16,7 @@ import com.ffii.fpsms.modules.pickOrder.service.TruckService
import com.ffii.fpsms.modules.pickOrder.entity.TruckRepository
import com.ffii.fpsms.modules.pickOrder.web.models.SaveTruckLane
import com.ffii.fpsms.modules.pickOrder.web.models.deleteTruckLane
import com.ffii.fpsms.modules.pickOrder.web.models.UpdateLoadingSequenceRequest
import jakarta.validation.Valid

@RestController
@@ -173,4 +175,42 @@ class TruckController(
)
}
}

@GetMapping("/findAllUniqueTruckLanceCodeAndRemarkCombinations")
fun findAllUniqueTruckLanceCodeAndRemarkCombinations(): List<Truck> {
return truckService.findAllUniqueTruckLanceCodeAndRemarkCombinations()
}


@GetMapping("/findAllFromShopAndTruckByTruckLanceCodeAndDeletedFalse")
fun findAllFromShopAndTruckByTruckLanceCodeAndDeletedFalse(@RequestParam truckLanceCode: String): List<ShopAndTruck> {
return truckService.findAllFromShopAndTruckByTruckLanceCodeAndDeletedFalse(truckLanceCode)
}

@PostMapping("/updateLoadingSequence")
fun updateLoadingSequence(@Valid @RequestBody request: UpdateLoadingSequenceRequest): MessageResponse {
try {
val truck = truckService.updateLoadingSequence(request)
return MessageResponse(
id = truck.id,
name = truck.shopName,
code = truck.truckLanceCode,
type = "truck",
message = "Loading sequence updated successfully",
errorPosition = null,
entity = truck
)
} catch (e: Exception) {
return MessageResponse(
id = null,
name = null,
code = null,
type = "truck",
message = "Error: ${e.message}",
errorPosition = null,
entity = null
)
}
}

}

+ 4
- 0
src/main/java/com/ffii/fpsms/modules/pickOrder/web/models/SaveTruckRequest.kt Datei anzeigen

@@ -24,3 +24,7 @@ data class SaveTruckLane(
data class deleteTruckLane(
val id: Long
)
data class UpdateLoadingSequenceRequest(
val id: Long,
val loadingSequence: Int
)

Laden…
Abbrechen
Speichern