# Conflicts: # src/main/java/com/ffii/fpsms/modules/pickOrder/entity/TruckRepository.ktmaster
| @@ -165,7 +165,15 @@ open class M18MasterDataService( | |||||
| maxQty = null, | maxQty = null, | ||||
| m18Id = id, | m18Id = id, | ||||
| m18LastModifyDate = commonUtils.timestampToLocalDateTime(pro.lastModifyDate), | 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) | val savedItem = itemsService.saveItem(saveItemRequest) | ||||
| @@ -260,7 +268,15 @@ open class M18MasterDataService( | |||||
| maxQty = null, | maxQty = null, | ||||
| m18Id = item.id, | m18Id = item.id, | ||||
| m18LastModifyDate = commonUtils.timestampToLocalDateTime(pro.lastModifyDate), | 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) | val savedItem = itemsService.saveItem(saveItemRequest) | ||||
| @@ -172,10 +172,51 @@ interface JobOrderRepository : AbstractRepository<JobOrder, Long> { | |||||
| left join inventory_lot_line ill on ill.id = sol.inventoryLotLineId | left join inventory_lot_line ill on ill.id = sol.inventoryLotLineId | ||||
| left join inventory_lot il on il.id = ill.inventoryLotId | left join inventory_lot il on il.id = ill.inventoryLotId | ||||
| where jo.prodScheduleLineId = :prodScheduleLineId | where jo.prodScheduleLineId = :prodScheduleLineId | ||||
| and jo.actualEnd is null | |||||
| group by jo.id, uc2.udfudesc | group by jo.id, uc2.udfudesc | ||||
| limit 1 | limit 1 | ||||
| """ | """ | ||||
| ) | ) | ||||
| fun findJobOrderByProdScheduleLineId(prodScheduleLineId: Long): JobOrderDetailWithJsonString?; | 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?; | |||||
| } | } | ||||
| @@ -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 { | open fun jobOrderDetailByCode(code: String): JobOrderDetail { | ||||
| val sqlResult = jobOrderRepository.findJobOrderDetailByCode(code) ?: throw NoSuchElementException("Job Order not found: $code"); | val sqlResult = jobOrderRepository.findJobOrderDetailByCode(code) ?: throw NoSuchElementException("Job Order not found: $code"); | ||||
| @@ -6,6 +6,7 @@ import jakarta.persistence.Entity | |||||
| import jakarta.persistence.Table | import jakarta.persistence.Table | ||||
| import jakarta.validation.constraints.NotNull | import jakarta.validation.constraints.NotNull | ||||
| import jakarta.validation.constraints.Size | import jakarta.validation.constraints.Size | ||||
| import java.time.LocalDateTime | |||||
| @Table(name = "equipment_detail") | @Table(name = "equipment_detail") | ||||
| @Entity | @Entity | ||||
| @@ -15,6 +16,19 @@ open class EquipmentDetail : BaseEntity<Long>() { | |||||
| @Column(name = "equipmentCode", nullable = true, length = 255) | @Column(name = "equipmentCode", nullable = true, length = 255) | ||||
| open var equipmentCode: String? = null | 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) | @Size(max = 30) | ||||
| @NotNull | @NotNull | ||||
| @Column(name = "code", nullable = false, length = 30) | @Column(name = "code", nullable = false, length = 30) | ||||
| @@ -39,6 +39,7 @@ interface ProductionScheduleLineRepository : AbstractRepository<ProductionSchedu | |||||
| group by psl.id, bm.id, i.id, pp.proportion | group by psl.id, bm.id, i.id, pp.proportion | ||||
| """) | """) | ||||
| fun getBomMaterials(id: Long): List<DetailedProdScheduleLineBomMaterialInterface>? | fun getBomMaterials(id: Long): List<DetailedProdScheduleLineBomMaterialInterface>? | ||||
| @Query(""" | @Query(""" | ||||
| SELECT psl FROM ProductionScheduleLine psl | SELECT psl FROM ProductionScheduleLine psl | ||||
| JOIN psl.productionSchedule ps | JOIN psl.productionSchedule ps | ||||
| @@ -7,6 +7,7 @@ import org.springframework.data.domain.Pageable | |||||
| import org.springframework.data.jpa.repository.Query | import org.springframework.data.jpa.repository.Query | ||||
| import org.springframework.stereotype.Repository | import org.springframework.stereotype.Repository | ||||
| import java.time.LocalDateTime | import java.time.LocalDateTime | ||||
| import java.time.LocalDate | |||||
| @Repository | @Repository | ||||
| interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule, Long> { | interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule, Long> { | ||||
| @@ -117,8 +118,11 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule, | |||||
| where rn = 1 | where rn = 1 | ||||
| and produceAt is not null | and produceAt is not null | ||||
| and scheduleAt in (select max(ps2.scheduleAt) from production_schedule ps2 group by ps2.produceAt) | 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 (:totalEstProdCount is null or :totalEstProdCount = '' or totalEstProdCount = :totalEstProdCount) | ||||
| and (coalesce(:types) is null or type in :types) | and (coalesce(:types) is null or type in :types) | ||||
| order by id ASC; | order by id ASC; | ||||
| @@ -126,7 +130,7 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule, | |||||
| ) | ) | ||||
| fun findProdScheduleInfoByProduceAtByPage( | fun findProdScheduleInfoByProduceAtByPage( | ||||
| // scheduleAt: String?, | // scheduleAt: String?, | ||||
| produceAt: String?, | |||||
| produceAt: LocalDate, | |||||
| totalEstProdCount: Double?, | totalEstProdCount: Double?, | ||||
| types: List<String>?, | types: List<String>?, | ||||
| pageable: Pageable | pageable: Pageable | ||||
| @@ -200,6 +204,7 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule, | |||||
| select | select | ||||
| prod.id, | prod.id, | ||||
| prod.scheduleAt, | prod.scheduleAt, | ||||
| prod.produceAt, | |||||
| prod.totalFGType, | prod.totalFGType, | ||||
| prod.totalEstProdCount, | prod.totalEstProdCount, | ||||
| prod.daysLeft, | prod.daysLeft, | ||||
| @@ -237,6 +242,7 @@ interface ProductionScheduleRepository : AbstractRepository<ProductionSchedule, | |||||
| select | select | ||||
| ps.id, | ps.id, | ||||
| ps.scheduleAt, | ps.scheduleAt, | ||||
| ps.produceAt, | |||||
| ps.totalFGType, | ps.totalFGType, | ||||
| ps.totalEstProdCount, | ps.totalEstProdCount, | ||||
| psl.approverId is not null as approved, | psl.approverId is not null as approved, | ||||
| @@ -33,7 +33,7 @@ interface ShopRepository : AbstractRepository<Shop, Long> { | |||||
| @Query( | @Query( | ||||
| nativeQuery = true, | nativeQuery = true, | ||||
| value = """ | 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 | FROM shop s LEFT JOIN truck t ON s.id = t.shopId | ||||
| WHERE s.type = 'shop' | WHERE s.type = 'shop' | ||||
| AND s.deleted = false; | AND s.deleted = false; | ||||
| @@ -18,6 +18,7 @@ interface ProdScheduleInfo { | |||||
| // Detailed Production Schedule With Line | // Detailed Production Schedule With Line | ||||
| interface DetailedProdScheduleWithLineWithJsonString { | interface DetailedProdScheduleWithLineWithJsonString { | ||||
| val id: Long? | val id: Long? | ||||
| val produceAt: LocalDateTime? | |||||
| val scheduleAt: LocalDateTime? | val scheduleAt: LocalDateTime? | ||||
| val totalEstProdCount: BigDecimal? | val totalEstProdCount: BigDecimal? | ||||
| val totalFGType: Long? | val totalFGType: Long? | ||||
| @@ -27,6 +28,7 @@ interface DetailedProdScheduleWithLineWithJsonString { | |||||
| data class DetailedProdScheduleWithLine ( | data class DetailedProdScheduleWithLine ( | ||||
| val id: Long?, | val id: Long?, | ||||
| val produceAt: LocalDateTime?, | |||||
| val scheduleAt: LocalDateTime?, | val scheduleAt: LocalDateTime?, | ||||
| val totalEstProdCount: BigDecimal?, | val totalEstProdCount: BigDecimal?, | ||||
| val totalFGType: Long?, | val totalFGType: Long?, | ||||
| @@ -19,4 +19,5 @@ interface ShopAndTruck { | |||||
| val districtReference: Long? | val districtReference: Long? | ||||
| val Store_id: String? | val Store_id: String? | ||||
| val remark: String? | val remark: String? | ||||
| val truckId: Long? | |||||
| } | } | ||||
| @@ -7,6 +7,9 @@ import com.ffii.fpsms.modules.master.entity.EquipmentDetailRepository | |||||
| import org.springframework.stereotype.Service | import org.springframework.stereotype.Service | ||||
| import org.springframework.transaction.annotation.Transactional | import org.springframework.transaction.annotation.Transactional | ||||
| import com.ffii.fpsms.modules.master.web.models.NewEquipmentDetailRequest | import com.ffii.fpsms.modules.master.web.models.NewEquipmentDetailRequest | ||||
| import com.ffii.fpsms.modules.master.web.models.UpdateMaintenanceRequest | |||||
| import java.time.LocalDateTime | |||||
| @Service | @Service | ||||
| open class EquipmentDetailService( | open class EquipmentDetailService( | ||||
| private val jdbcDao: JdbcDao, | private val jdbcDao: JdbcDao, | ||||
| @@ -18,21 +21,54 @@ open class EquipmentDetailService( | |||||
| } | } | ||||
| open fun getEquipmentDetailsByPage(args: Map<String, Any>): List<Map<String, Any>> { | open fun getEquipmentDetailsByPage(args: Map<String, Any>): List<Map<String, Any>> { | ||||
| println("Search args: $args") | |||||
| val sql = StringBuilder( | 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")) { | if (args.containsKey("id")) { | ||||
| sql.append(" AND e.id like :id ") | sql.append(" AND e.id like :id ") | ||||
| } | } | ||||
| if (args.containsKey("name")) { | if (args.containsKey("name")) { | ||||
| sql.append(" AND e.name like :name ") | |||||
| sql.append(" AND LOWER(e.name) LIKE LOWER(:name) ") | |||||
| } | } | ||||
| if (args.containsKey("description")) { | if (args.containsKey("description")) { | ||||
| sql.append(" AND e.description like :description ") | sql.append(" AND e.description like :description ") | ||||
| } | } | ||||
| if (args.containsKey("repairAndMaintenanceStatus")) { | |||||
| sql.append(" AND e.repairAndMaintenanceStatus = :repairAndMaintenanceStatus ") | |||||
| } | |||||
| return jdbcDao.queryForList(sql.toString(), args) | return jdbcDao.queryForList(sql.toString(), args) | ||||
| } | } | ||||
| @@ -51,6 +87,42 @@ open class EquipmentDetailService( | |||||
| open fun findByDescription(description: String): EquipmentDetail? { | open fun findByDescription(description: String): EquipmentDetail? { | ||||
| return equipmentDetailRepository.findByDescriptionAndDeletedIsFalse(description) | 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 | @Transactional | ||||
| open fun saveEquipmentDetail(request: NewEquipmentDetailRequest): EquipmentDetail { | open fun saveEquipmentDetail(request: NewEquipmentDetailRequest): EquipmentDetail { | ||||
| @@ -18,26 +18,40 @@ open class EquipmentService( | |||||
| } | } | ||||
| open fun getEquipmentsByPage(args: Map<String, Any>): List<Map<String, Any>> { | 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? { | open fun findById(id: Long): Equipment? { | ||||
| return equipmentRepository.findByIdAndDeletedFalse(id) | return equipmentRepository.findByIdAndDeletedFalse(id) | ||||
| @@ -392,7 +392,10 @@ open class ItemsService( | |||||
| "i.id, " + | "i.id, " + | ||||
| "i.code, " + | "i.code, " + | ||||
| "i.name, " + | "i.name, " + | ||||
| "i.description " + | |||||
| "i.description, " + | |||||
| "i.type, " + | |||||
| "i.`LocationCode` as LocationCode, " + | |||||
| "i.`qcCategoryId` as qcCategoryId " + | |||||
| "FROM items i " + | "FROM items i " + | ||||
| "WHERE i.deleted = FALSE" | "WHERE i.deleted = FALSE" | ||||
| ); | ); | ||||
| @@ -512,6 +515,14 @@ open class ItemsService( | |||||
| this.qcCategory = qcCategory | this.qcCategory = qcCategory | ||||
| m18Id = request.m18Id ?: this.m18Id | m18Id = request.m18Id ?: this.m18Id | ||||
| m18LastModifyDate = request.m18LastModifyDate ?: this.m18LastModifyDate | 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") | logger.info("saving item: $item") | ||||
| val savedItem = itemsRepository.saveAndFlush(item) | val savedItem = itemsRepository.saveAndFlush(item) | ||||
| @@ -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.CreateJobOrderBomMaterialRequest | ||||
| import com.ffii.fpsms.modules.jobOrder.web.model.CreateJobOrderProcessRequest | import com.ffii.fpsms.modules.jobOrder.web.model.CreateJobOrderProcessRequest | ||||
| import com.ffii.fpsms.modules.jobOrder.web.model.CreateJobOrderRequest | 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.* | ||||
| import com.ffii.fpsms.modules.master.entity.projections.* | import com.ffii.fpsms.modules.master.entity.projections.* | ||||
| import com.ffii.fpsms.modules.master.web.models.MessageResponse | import com.ffii.fpsms.modules.master.web.models.MessageResponse | ||||
| @@ -46,10 +47,16 @@ import kotlin.collections.component2 | |||||
| import kotlin.jvm.optionals.getOrNull | import kotlin.jvm.optionals.getOrNull | ||||
| import kotlin.math.ceil | import kotlin.math.ceil | ||||
| import kotlin.comparisons.maxOf | 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.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.io.ByteArrayOutputStream | ||||
| import java.sql.Timestamp | |||||
| @Service | @Service | ||||
| open class ProductionScheduleService( | open class ProductionScheduleService( | ||||
| @@ -118,10 +125,15 @@ open class ProductionScheduleService( | |||||
| open fun allDetailedProdSchedulesByPage(request: SearchProdScheduleRequest): RecordsRes<ProdScheduleInfo> { | open fun allDetailedProdSchedulesByPage(request: SearchProdScheduleRequest): RecordsRes<ProdScheduleInfo> { | ||||
| val pageable = PageRequest.of(request.pageNum ?: 0, request.pageSize ?: 10); | 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( | val response = productionScheduleRepository.findProdScheduleInfoByProduceAtByPage( | ||||
| produceAt = request.produceAt, | |||||
| produceAt = produceAtDate!!, | |||||
| totalEstProdCount = request.totalEstProdCount, | totalEstProdCount = request.totalEstProdCount, | ||||
| // types = listOf("detailed", "manual"), | |||||
| types = request.types, | types = request.types, | ||||
| pageable = pageable | pageable = pageable | ||||
| ) | ) | ||||
| @@ -329,6 +341,7 @@ open class ProductionScheduleService( | |||||
| return DetailedProdScheduleWithLine( | return DetailedProdScheduleWithLine( | ||||
| id = sqlResult.id, | id = sqlResult.id, | ||||
| produceAt = sqlResult.produceAt, | |||||
| scheduleAt = sqlResult.scheduleAt, | scheduleAt = sqlResult.scheduleAt, | ||||
| totalEstProdCount = sqlResult.totalEstProdCount, | totalEstProdCount = sqlResult.totalEstProdCount, | ||||
| totalFGType = sqlResult.totalFGType, | totalFGType = sqlResult.totalFGType, | ||||
| @@ -397,57 +410,63 @@ open class ProductionScheduleService( | |||||
| val prodScheduleLine = productionScheduleLineRepository.findById(prodScheduleLineId).getOrNull() | val prodScheduleLine = productionScheduleLineRepository.findById(prodScheduleLineId).getOrNull() | ||||
| ?: throw NoSuchElementException("Production Schedule Line with ID $prodScheduleLineId not found.") | ?: 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.") | ?: 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 machineCap = 10000.0 | ||||
| var needQtyList = getNeedQty() | var needQtyList = getNeedQty() | ||||
| //remove the production schedule >= today | |||||
| clearTodayAndFutureProdSchedule() | |||||
| println("needQtyList - " + needQtyList); | println("needQtyList - " + needQtyList); | ||||
| //##### The 22000, 10000 machine cap just means the max warehouse storage qty, not production qty cap | //##### 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 | //##### 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 | //##### 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() | 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 { | val headerStyle = workbook.createCellStyle().apply { | ||||
| fillForegroundColor = IndexedColors.GREY_25_PERCENT.index | fillForegroundColor = IndexedColors.GREY_25_PERCENT.index | ||||
| fillPattern = FillPatternType.SOLID_FOREGROUND | fillPattern = FillPatternType.SOLID_FOREGROUND | ||||
| wrapText = true | |||||
| verticalAlignment = VerticalAlignment.CENTER | |||||
| val font = workbook.createFont() | val font = workbook.createFont() | ||||
| font.bold = true | font.bold = true | ||||
| setFont(font) | 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) -> | 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) | val headerRow = sheet.createRow(0) | ||||
| headers.forEachIndexed { i, title -> | 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 -> | dailyLines.forEachIndexed { index, line -> | ||||
| val row = sheet.createRow(index + 1) | 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 matSheet = workbook.createSheet("Material Summary") | ||||
| val matHeaders = listOf( | 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" | "Related Item Code", "Related Item Name" | ||||
| ) | ) | ||||
| @@ -1399,37 +1468,73 @@ open class ProductionScheduleService( | |||||
| matHeaders.forEachIndexed { i, title -> | matHeaders.forEachIndexed { i, title -> | ||||
| matHeaderRow.createCell(i).apply { | matHeaderRow.createCell(i).apply { | ||||
| setCellValue(title) | setCellValue(title) | ||||
| setCellStyle(headerStyle) | |||||
| cellStyle = headerStyle | |||||
| } | } | ||||
| } | } | ||||
| lineMats.forEachIndexed { index, rowData -> | lineMats.forEachIndexed { index, rowData -> | ||||
| val row = matSheet.createRow(index + 1) | val row = matSheet.createRow(index + 1) | ||||
| row.heightInPoints = 35f | |||||
| val totalNeed = asDouble(rowData["totalMatQtyNeed"]) | val totalNeed = asDouble(rowData["totalMatQtyNeed"]) | ||||
| val purchased = asDouble(rowData["purchasedQty"]) | val purchased = asDouble(rowData["purchasedQty"]) | ||||
| val onHand = asDouble(rowData["onHandQty"]) | val onHand = asDouble(rowData["onHandQty"]) | ||||
| // Calculation: Required Qty = totalMatQtyNeed - purchasedQty - onHandQty (minimum 0) | |||||
| val requiredQty = (totalNeed - purchased - onHand).coerceAtLeast(0.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() | val out = ByteArrayOutputStream() | ||||
| workbook.use { it.write(out) } | workbook.use { it.write(out) } | ||||
| workbook.close() | |||||
| return out.toByteArray() | return out.toByteArray() | ||||
| } | } | ||||
| @@ -1514,5 +1619,30 @@ open class ProductionScheduleService( | |||||
| return jdbcDao.queryForList(sql, args); | 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") | |||||
| } | |||||
| } | } | ||||
| @@ -10,6 +10,8 @@ import jakarta.validation.Valid | |||||
| import org.springframework.web.bind.annotation.* | import org.springframework.web.bind.annotation.* | ||||
| import java.util.Collections.emptyList | import java.util.Collections.emptyList | ||||
| import com.ffii.fpsms.modules.master.web.models.NewEquipmentDetailRequest | import com.ffii.fpsms.modules.master.web.models.NewEquipmentDetailRequest | ||||
| import com.ffii.fpsms.modules.master.web.models.UpdateMaintenanceRequest | |||||
| @RestController | @RestController | ||||
| @RequestMapping("/EquipmentDetail") | @RequestMapping("/EquipmentDetail") | ||||
| class EquipmentDetailController( | class EquipmentDetailController( | ||||
| @@ -43,31 +45,38 @@ fun getAllEquipmentDetailByPage( | |||||
| .addStringLike("code") | .addStringLike("code") | ||||
| .addStringLike("description") | .addStringLike("description") | ||||
| .addStringLike("id") | .addStringLike("id") | ||||
| .addStringLike("equipmentCode") | |||||
| .addBoolean("repairAndMaintenanceStatus") | |||||
| .build() | .build() | ||||
| val pageSize = request.getParameter("pageSize")?.toIntOrNull() ?: 10 | val pageSize = request.getParameter("pageSize")?.toIntOrNull() ?: 10 | ||||
| val pageNum = request.getParameter("pageNum")?.toIntOrNull() ?: 1 | val pageNum = request.getParameter("pageNum")?.toIntOrNull() ?: 1 | ||||
| // 方法名和变量名都要和 Service 保持一致 | |||||
| val fullList = equipmentDetailService.getEquipmentDetailsByPage(criteriaArgs) ?: emptyList() | val fullList = equipmentDetailService.getEquipmentDetailsByPage(criteriaArgs) ?: emptyList() | ||||
| val paginatedList = PagingUtils.getPaginatedList(fullList, pageSize, pageNum) | val paginatedList = PagingUtils.getPaginatedList(fullList, pageSize, pageNum) | ||||
| return RecordsRes(paginatedList as List<Map<String, Any>>, fullList.size) | return RecordsRes(paginatedList as List<Map<String, Any>>, fullList.size) | ||||
| } | } | ||||
| // 详情 | |||||
| @GetMapping("/details/{id}") | @GetMapping("/details/{id}") | ||||
| fun getEquipmentDetail(@PathVariable id: Long): EquipmentDetail? { | fun getEquipmentDetail(@PathVariable id: Long): EquipmentDetail? { | ||||
| return equipmentDetailService.findById(id) | return equipmentDetailService.findById(id) | ||||
| } | } | ||||
| @PutMapping("/update/{id}") | |||||
| fun updateMaintenanceAndRepair( | |||||
| @PathVariable id: Long, | |||||
| @RequestBody request: UpdateMaintenanceRequest | |||||
| ): EquipmentDetail { | |||||
| return equipmentDetailService.updateMaintenanceAndRepair(id, request) | |||||
| } | |||||
| /* | /* | ||||
| // 新增/编辑 | |||||
| @PostMapping("/save") | @PostMapping("/save") | ||||
| fun saveEquipmentDetail(@Valid @RequestBody equipmentDetail: NewEquipmentDetailRequest): EquipmentDetail { | fun saveEquipmentDetail(@Valid @RequestBody equipmentDetail: NewEquipmentDetailRequest): EquipmentDetail { | ||||
| return equipmentDetailService.saveEquipmentDetail(equipmentDetail) | return equipmentDetailService.saveEquipmentDetail(equipmentDetail) | ||||
| } | } | ||||
| */ | */ | ||||
| // 逻辑删除 | |||||
| @DeleteMapping("/delete/{id}") | @DeleteMapping("/delete/{id}") | ||||
| fun deleteEquipmentDetail(@PathVariable id: Long) { | fun deleteEquipmentDetail(@PathVariable id: Long) { | ||||
| equipmentDetailService.deleteEquipmentDetail(id) | equipmentDetailService.deleteEquipmentDetail(id) | ||||
| @@ -0,0 +1,6 @@ | |||||
| package com.ffii.fpsms.modules.master.web.models | |||||
| data class UpdateMaintenanceRequest( | |||||
| val repairAndMaintenanceStatus: Boolean?, | |||||
| val repairAndMaintenanceRemarks: String? | |||||
| ) | |||||
| @@ -45,6 +45,14 @@ data class NewItemRequest( | |||||
| val m18Id: Long?, | val m18Id: Long?, | ||||
| val m18LastModifyDate: LocalDateTime?, | val m18LastModifyDate: LocalDateTime?, | ||||
| val qcCategoryId: Long?, | 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 type: List<NewTypeRequest>?, | ||||
| // val uom: List<NewUomRequest>?, | // val uom: List<NewUomRequest>?, | ||||
| // val weightUnit: List<NewWeightUnitRequest>?, | // val weightUnit: List<NewWeightUnitRequest>?, | ||||
| @@ -51,4 +51,40 @@ fun findByShopIdAndStoreIdAndDayOfWeek( | |||||
| @Param("storeId") storeId: String, | @Param("storeId") storeId: String, | ||||
| @Param("dayOfWeekAbbr") dayOfWeekAbbr: String | @Param("dayOfWeekAbbr") dayOfWeekAbbr: String | ||||
| ): List<Truck> | ): 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> | |||||
| } | } | ||||
| @@ -12,7 +12,9 @@ import com.ffii.fpsms.modules.pickOrder.web.models.SaveTruckRequest | |||||
| import java.time.LocalTime | import java.time.LocalTime | ||||
| import java.time.format.DateTimeFormatter | import java.time.format.DateTimeFormatter | ||||
| import com.ffii.fpsms.modules.master.entity.ShopRepository | 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.SaveTruckLane | ||||
| import com.ffii.fpsms.modules.pickOrder.web.models.UpdateLoadingSequenceRequest | |||||
| import jakarta.transaction.Transactional | import jakarta.transaction.Transactional | ||||
| @@ -187,7 +189,7 @@ open class TruckService( | |||||
| } | } | ||||
| open fun findAllByShopId(shopId: Long): List<Truck> { | open fun findAllByShopId(shopId: Long): List<Truck> { | ||||
| return truckRepository.findAllByShopId(shopId) | |||||
| return truckRepository.findByShopIdAndDeletedFalse(shopId) | |||||
| } | } | ||||
| @Transactional | @Transactional | ||||
| @@ -212,7 +214,10 @@ open class TruckService( | |||||
| @Transactional | @Transactional | ||||
| open fun deleteById(id: Long): String { | 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" | return "Truck deleted successfully with id: $id" | ||||
| } | } | ||||
| @@ -241,4 +246,22 @@ open class TruckService( | |||||
| return truckRepository.save(truck) | 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) | |||||
| } | |||||
| } | } | ||||
| @@ -1,5 +1,6 @@ | |||||
| package com.ffii.fpsms.modules.pickOrder.web | 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.master.web.models.MessageResponse | ||||
| import com.ffii.fpsms.modules.pickOrder.entity.Truck | import com.ffii.fpsms.modules.pickOrder.entity.Truck | ||||
| import org.springframework.web.bind.ServletRequestBindingException | 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.entity.TruckRepository | ||||
| import com.ffii.fpsms.modules.pickOrder.web.models.SaveTruckLane | 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.deleteTruckLane | ||||
| import com.ffii.fpsms.modules.pickOrder.web.models.UpdateLoadingSequenceRequest | |||||
| import jakarta.validation.Valid | import jakarta.validation.Valid | ||||
| @RestController | @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 | |||||
| ) | |||||
| } | |||||
| } | |||||
| } | } | ||||
| @@ -24,3 +24,7 @@ data class SaveTruckLane( | |||||
| data class deleteTruckLane( | data class deleteTruckLane( | ||||
| val id: Long | val id: Long | ||||
| ) | ) | ||||
| data class UpdateLoadingSequenceRequest( | |||||
| val id: Long, | |||||
| val loadingSequence: Int | |||||
| ) | |||||