diff --git a/src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderBomMaterial.kt b/src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderBomMaterial.kt index 45ecf90..50932a5 100644 --- a/src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderBomMaterial.kt +++ b/src/main/java/com/ffii/fpsms/modules/jobOrder/entity/JobOrderBomMaterial.kt @@ -28,7 +28,7 @@ open class JobOrderBomMaterial : BaseEntity() { @Column(name = "reqQty", nullable = false, precision = 14, scale = 2) open var reqQty: BigDecimal? = null - @NotNull +// @NotNull @ManyToOne @JoinColumn(name = "uomId", nullable = false) open var uom: UomConversion? = null diff --git a/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLineRepository.kt b/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLineRepository.kt index 9f74af2..19afb68 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLineRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleLineRepository.kt @@ -1,10 +1,41 @@ package com.ffii.fpsms.modules.master.entity import com.ffii.core.support.AbstractRepository +import com.ffii.fpsms.modules.master.entity.projections.DetailedProdScheduleLineBomMaterial +import com.ffii.fpsms.modules.master.entity.projections.DetailedProdScheduleLineBomMaterialInterface import org.springframework.data.jpa.repository.Query import org.springframework.stereotype.Repository import java.time.LocalDateTime @Repository interface ProductionScheduleLineRepository : AbstractRepository { + @Query(nativeQuery = true, + value = """ + with prod_prop as ( + select + psl.id as pslId, + round(coalesce(psl.prodQty, 0) / coalesce(b.outputQty, 1), 2) as proportion + from production_schedule_line psl + left join bom b on b.itemId = psl.itemId + where psl.id = :id and b.id is not null + ) + select + psl.id as pslId, + bm.id, + b.outputQty as bomOutputQty, + bmi.code, + bmi.name, + bmi.`type`, + coalesce(i.onHandQty, 0) - coalesce(i.onHoldQty, 0) - coalesce(i.unavailableQty, 0) as availableQty, + ceil(coalesce(bm.qty, 0) * pp.proportion) as demandQty + from production_schedule_line psl + left join prod_prop pp on pp.pslId = psl.id + left join bom b on b.itemId = psl.itemId + left join bom_material bm on bm.bomId = b.id + left join items bmi on bmi.id = bm.itemId + left join inventory i on i.itemId = bmi.id + where psl.id = :id and bmi.id is not null + group by psl.id, bm.id, i.id, pp.proportion + """) + fun getBomMaterials(id: Long): List? } \ No newline at end of file diff --git a/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleRepository.kt b/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleRepository.kt index 724b12b..0cd3288 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleRepository.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/entity/ProductionScheduleRepository.kt @@ -1,9 +1,7 @@ package com.ffii.fpsms.modules.master.entity import com.ffii.core.support.AbstractRepository -import com.ffii.fpsms.modules.master.entity.projections.DetailedProdScheduleWithLine -import com.ffii.fpsms.modules.master.entity.projections.DetailedProdScheduleWithLineWithJsonString -import com.ffii.fpsms.modules.master.entity.projections.ProdScheduleInfo +import com.ffii.fpsms.modules.master.entity.projections.* import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.data.jpa.repository.Query @@ -85,7 +83,7 @@ interface ProductionScheduleRepository : AbstractRepository?, - val priority: BigDecimal? + val priority: BigDecimal?, + val approved: Boolean?, + val proportion: BigDecimal? ) data class DetailedProdScheduleLineBomMaterial ( @@ -53,6 +56,15 @@ data class DetailedProdScheduleLineBomMaterial ( val demandQty: BigDecimal? ) +interface DetailedProdScheduleLineBomMaterialInterface { + val id: Long? + val code: String? + val name: String? + val type: String? + val availableQty: BigDecimal? + val demandQty: BigDecimal? +} + data class DetailedProdScheduleLineProdTime ( val equipName: String?, val totalMinutes: BigDecimal? diff --git a/src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt b/src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt index 74d6522..34af086 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/service/ProductionScheduleService.kt @@ -15,15 +15,23 @@ 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.ReleaseProdScheduleLineRequest import com.ffii.fpsms.modules.master.web.models.SearchProdScheduleRequest +import com.ffii.fpsms.modules.stock.entity.Inventory +import com.ffii.fpsms.modules.stock.entity.InventoryRepository +import com.ffii.fpsms.modules.stock.service.InventoryService import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement import com.google.gson.reflect.TypeToken import org.springframework.data.domain.PageRequest import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.lang.reflect.Type import java.math.BigDecimal +import java.math.RoundingMode import java.time.LocalDateTime import java.time.format.DateTimeFormatter -import java.util.* -import kotlin.NoSuchElementException import kotlin.collections.component1 import kotlin.collections.component2 import kotlin.jvm.optionals.getOrNull @@ -41,6 +49,9 @@ open class ProductionScheduleService( private val jobOrderBomMaterialService: JobOrderBomMaterialService, private val bomService: BomService, private val jobOrderProcessService: JobOrderProcessService, + private val inventoryService: InventoryService, + private val inventoryRepository: InventoryRepository, + private val itemUomService: ItemUomService ) : AbstractBaseEntityService( jdbcDao, productionScheduleRepository @@ -90,7 +101,7 @@ open class ProductionScheduleService( // BigDecimal.ONE // } - val proportion = BigDecimal(line.prodQty).divide(bm.bom?.outputQty ?: BigDecimal.ONE) + val proportion = BigDecimal(line.prodQty).divide(bm.bom?.outputQty ?: BigDecimal.ONE, 2, RoundingMode.HALF_UP) val demandQty = bm.qty?.times(proportion) ?: zero @@ -174,7 +185,7 @@ open class ProductionScheduleService( // BigDecimal.ONE // } - val proportion = BigDecimal(line.prodQty).divide(bm.bom?.outputQty ?: BigDecimal.ONE) + val proportion = BigDecimal(line.prodQty).divide(bm.bom?.outputQty ?: BigDecimal.ONE, 2, RoundingMode.HALF_UP) val demandQty = bm.qty?.times(proportion) ?: zero @@ -254,10 +265,19 @@ open class ProductionScheduleService( ) } + class BooleanTypeAdapter: JsonDeserializer { + override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Boolean { +// println(json) + return json.asInt == 1 + } + } + open fun detailedProdScheduleDetail(id: Long): DetailedProdScheduleWithLine { val sqlResult = productionScheduleRepository.findDetailedProdScheduleWithLine(id) ?: throw NoSuchElementException() - val gson = Gson() + val gson = GsonBuilder() + .registerTypeAdapter(Boolean::class.javaObjectType, BooleanTypeAdapter()) + .create() val type = object : TypeToken?>() {}.type val gsonResult: List? = gson.fromJson(sqlResult.prodScheduleLines, type) @@ -270,14 +290,41 @@ open class ProductionScheduleService( ) } + @Transactional(rollbackFor = [java.lang.Exception::class]) + open fun saveProdScheduleLine(request: ReleaseProdScheduleLineRequest): MessageResponse { + val prodScheduleLine = request.id.let { productionScheduleLineRepository.findById(it).getOrNull() } ?: throw NoSuchElementException() + + // Update Prod Schedule Line Prod qty + prodScheduleLine.apply { + prodQty = request.demandQty.toDouble() + } + productionScheduleLineRepository.saveAndFlush(prodScheduleLine) + + val bomMaterials = prodScheduleLine.id?.let { productionScheduleLineRepository.getBomMaterials(it) } + + return MessageResponse( + id = request.id, + name = null, + code = null, + type = null, + message = "Success", + entity = mapOf("bomMaterials" to bomMaterials), + errorPosition = null + ) + } + + @Transactional(rollbackFor = [java.lang.Exception::class]) open fun releaseProdScheduleLine(request: ReleaseProdScheduleLineRequest): MessageResponse { val prodScheduleLine = request.id.let { productionScheduleLineRepository.findById(it).getOrNull() } ?: throw NoSuchElementException() val bom = prodScheduleLine.item.id?.let { bomService.findByItemId(it) } val approver = SecurityUtils.getUser().getOrNull() - val proportion = request.demandQty.divide(bom?.outputQty ?: BigDecimal.ONE) + val proportion = request.demandQty.divide(bom?.outputQty ?: BigDecimal.ONE, 2, RoundingMode.HALF_UP) // Update Prod Schedule Line Prod qty - prodScheduleLine.apply { prodQty = request.demandQty.toDouble() } + prodScheduleLine.apply { + prodQty = request.demandQty.toDouble() + approverId = approver?.id + } productionScheduleLineRepository.save(prodScheduleLine) // Create Job Order @@ -291,16 +338,61 @@ open class ProductionScheduleService( val jo = jobOrderService.createJobOrder(joRequest) // Create Job Order Bom Materials - val jobmRequests = bom?.bomMaterials?.map { bm -> - CreateJobOrderBomMaterialRequest( - joId = jo.id, - itemId = bm.item?.id, - reqQty = bm.qty?.times(proportion) ?: BigDecimal.ZERO, - uomId = bm.uom?.id - ) - } - if (jobmRequests != null) { - jobOrderBomMaterialService.createJobOrderBomMaterials(jobmRequests) + if (bom?.bomMaterials != null) { + // Job Order Bom Material + val jobmRequests = bom.bomMaterials.map { bm -> + val demandQty = bm.qty?.times(proportion) ?: BigDecimal.ZERO + + val jobm = CreateJobOrderBomMaterialRequest( + joId = jo.id, + itemId = bm.item?.id, + reqQty = bm.qty?.times(proportion) ?: BigDecimal.ZERO, + uomId = bm.salesUnit?.id + ) + + jobm + } + + if (jobmRequests != null) { + jobOrderBomMaterialService.createJobOrderBomMaterials(jobmRequests) + } + + // Inventory + val inventories = bom.bomMaterials.map { bm -> + val demandQty = bm.qty?.times(proportion) ?: BigDecimal.ZERO + + var inventory = bm.item?.id?.let { inventoryRepository.findByItemId(it).getOrNull() } + if (inventory != null) { + inventory.apply { + this.onHoldQty = (this.onHoldQty ?: BigDecimal.ZERO).plus(demandQty) + } + } else { + if (bm.item != null) { + val itemUom = bm.item?.id?.let { itemUomService.findSalesUnitByItemId(it) } + inventory = Inventory().apply { + item = bm.item + onHandQty = BigDecimal.ZERO + unavailableQty = BigDecimal.ZERO + this.onHoldQty = demandQty + uom = itemUom?.uom + status = "unavailable" + } + } + } + + inventory + }.groupBy { it?.item } // Group by item + .mapNotNull { (item, invList) -> + if (invList.isNotEmpty()) { + invList[0]?.apply { + onHoldQty = invList.sumOf { it?.onHoldQty ?: BigDecimal.ZERO } + } + } else { + null + } + } + + inventoryRepository.saveAllAndFlush(inventories) } // Create Job Order Process @@ -316,12 +408,17 @@ open class ProductionScheduleService( jobOrderProcessService.createJobOrderProcesses(jopRequests) } + // Get Latest Data +// val bomMaterials = prodScheduleLine.id?.let { productionScheduleLineRepository.getBomMaterials(it) } + val latestDetail = prodScheduleLine.productionSchedule.id?.let { detailedProdScheduleDetail(it) } + return MessageResponse( id = request.id, name = null, - code = null, + code = jo.code, type = null, message = "Success", + entity = mapOf("prodScheduleLines" to latestDetail?.prodScheduleLines), errorPosition = null ) } diff --git a/src/main/java/com/ffii/fpsms/modules/master/web/ProductionScheduleController.kt b/src/main/java/com/ffii/fpsms/modules/master/web/ProductionScheduleController.kt index e98dfed..f101bcb 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/web/ProductionScheduleController.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/web/ProductionScheduleController.kt @@ -13,6 +13,7 @@ import com.ffii.fpsms.modules.master.web.models.MessageResponse import com.ffii.fpsms.modules.master.web.models.ReleaseProdScheduleLineRequest import com.ffii.fpsms.modules.master.web.models.SearchProdScheduleRequest import jakarta.servlet.http.HttpServletRequest +import jakarta.validation.Valid import org.springframework.web.bind.annotation.* import java.time.Duration import java.time.LocalDateTime @@ -54,15 +55,22 @@ class ProductionScheduleController( return productionScheduleService.detailedProdScheduleDetail(id) } + @PostMapping("/detail/detailed/save") + fun saveDetailedProdScheduleDetail(@Valid @RequestBody request: ReleaseProdScheduleLineRequest): MessageResponse { + return productionScheduleService.saveProdScheduleLine(request) + } + + @PostMapping("/detail/detailed/releaseLine") + fun releaseProdScheduleLine(@Valid @RequestBody request: ReleaseProdScheduleLineRequest): MessageResponse { + return productionScheduleService.releaseProdScheduleLine(request) + } + @GetMapping("/getRecordByPage") fun allProdSchedulesByPage(@ModelAttribute request: SearchProdScheduleRequest): RecordsRes { return productionScheduleService.allProdSchedulesByPage(request); } - @PostMapping("/releaseLine") - fun releaseProdScheduleLine(request: ReleaseProdScheduleLineRequest): MessageResponse { - return productionScheduleService.releaseProdScheduleLine(request) - } + @RequestMapping(value = ["/testDetailSchedule"], method = [RequestMethod.GET]) fun generateDetailSchedule(request: HttpServletRequest?): Int { diff --git a/src/main/java/com/ffii/fpsms/modules/master/web/models/ReleaseProdScheduleLineRequest.kt b/src/main/java/com/ffii/fpsms/modules/master/web/models/ReleaseProdScheduleLineRequest.kt index bb17df7..0f943aa 100644 --- a/src/main/java/com/ffii/fpsms/modules/master/web/models/ReleaseProdScheduleLineRequest.kt +++ b/src/main/java/com/ffii/fpsms/modules/master/web/models/ReleaseProdScheduleLineRequest.kt @@ -1,8 +1,11 @@ package com.ffii.fpsms.modules.master.web.models +import jakarta.validation.constraints.NotNull import java.math.BigDecimal data class ReleaseProdScheduleLineRequest( + @field:NotNull(message = "Id cannot be null") val id: Long, + @field:NotNull(message = "Demand Qty cannot be null") val demandQty: BigDecimal, ) diff --git a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/PurchaseOrderController.kt b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/PurchaseOrderController.kt index bd5d685..769ced1 100644 --- a/src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/PurchaseOrderController.kt +++ b/src/main/java/com/ffii/fpsms/modules/purchaseOrder/web/PurchaseOrderController.kt @@ -29,8 +29,8 @@ class PurchaseOrderController( // @RequestParam(required = false) pageNum: Int, // @RequestParam(required = false) pageSize: Int ): RecordsRes { - println("request") - println(request) +// println("request") +// println(request) val criteriaArgs = CriteriaArgsBuilder.withRequest(request) .addStringLike("code") .addString("status") diff --git a/src/main/java/com/ffii/fpsms/modules/stock/entity/Inventory.kt b/src/main/java/com/ffii/fpsms/modules/stock/entity/Inventory.kt index ab1bcd5..9641001 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/entity/Inventory.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/entity/Inventory.kt @@ -11,19 +11,19 @@ import java.math.BigDecimal @Entity @Table(name = "inventory") open class Inventory: BaseEntity(){ - @NotNull +// @NotNull @Column(name = "onHandQty") open var onHandQty: BigDecimal? = null - @NotNull +// @NotNull @Column(name = "onHoldQty") open var onHoldQty: BigDecimal? = null - @NotNull +// @NotNull @Column(name = "unavailableQty") open var unavailableQty: BigDecimal? = null - @NotNull +// @NotNull @Column(name = "price") open var price: BigDecimal? = null @@ -35,23 +35,23 @@ open class Inventory: BaseEntity(){ @JoinColumn(name = "currencyId") open var currency: Currency? = null - @NotNull +// @NotNull @Column(name = "cpu") // cost per unit open var cpu: BigDecimal? = null - @NotNull +// @NotNull @Column(name = "cpuUnit") open var cpuUnit: String? = null - @NotNull +// @NotNull @Column(name = "cpm") // cost per unit open var cpm: BigDecimal? = null - @NotNull +// @NotNull @Column(name = "cpmUnit") open var cpmUnit: String? = null - @NotNull +// @NotNull @ManyToOne @JoinColumn(name = "uomId") open var uom: UomConversion? = null diff --git a/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryService.kt b/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryService.kt index 6720d84..46640a2 100644 --- a/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryService.kt +++ b/src/main/java/com/ffii/fpsms/modules/stock/service/InventoryService.kt @@ -5,6 +5,9 @@ import com.ffii.core.support.JdbcDao import com.ffii.fpsms.modules.common.CodeGenerator import com.ffii.fpsms.modules.master.entity.Items import com.ffii.fpsms.modules.master.entity.ItemsRepository +import com.ffii.fpsms.modules.master.entity.UomConversionRepository +import com.ffii.fpsms.modules.master.service.ItemUomService +import com.ffii.fpsms.modules.master.service.UomConversionService import com.ffii.fpsms.modules.master.web.models.MessageResponse import com.ffii.fpsms.modules.stock.entity.Inventory import com.ffii.fpsms.modules.stock.entity.InventoryRepository @@ -12,14 +15,19 @@ import com.ffii.fpsms.modules.stock.entity.projection.InventoryInfo import com.ffii.fpsms.modules.stock.web.model.SaveInventoryRequest import org.springframework.stereotype.Service import java.io.IOException +import java.math.BigDecimal import java.time.LocalDate import java.time.format.DateTimeFormatter +import kotlin.jvm.optionals.getOrNull @Service open class InventoryService( private val jdbcDao: JdbcDao, private val inventoryRepository: InventoryRepository, private val itemsRepository: ItemsRepository, + private val uomConversionService: UomConversionService, + private val uomConversionRepository: UomConversionRepository, + private val itemUomService: ItemUomService, ): AbstractBaseEntityService(jdbcDao, inventoryRepository) { open fun allInventory(): List { // TODO: Replace by actual logic @@ -39,6 +47,7 @@ open class InventoryService( return inventoryRepository.findInventoryInfoByItemIdInAndDeletedIsFalse(itemIds); } + // @Throws(IOException::class) // open fun updateInventory(request: SaveInventoryRequest): MessageResponse { // // out need id diff --git a/src/main/resources/db/changelog/changes/20250714_01_cyril/01_update_job_order_bom_material.sql b/src/main/resources/db/changelog/changes/20250714_01_cyril/01_update_job_order_bom_material.sql new file mode 100644 index 0000000..2f1659f --- /dev/null +++ b/src/main/resources/db/changelog/changes/20250714_01_cyril/01_update_job_order_bom_material.sql @@ -0,0 +1,11 @@ +-- liquibase formatted sql +-- changeset cyril:update_job_order_bom_material + +ALTER TABLE `job_order_bom_material` + DROP FOREIGN KEY `FK_JOBM_TO_UOM_CONVERSION_ON_UOM_ID`; +ALTER TABLE `job_order_bom_material` + CHANGE COLUMN `uomId` `uomId` INT NULL ; +ALTER TABLE `job_order_bom_material` + ADD CONSTRAINT `FK_JOBM_TO_UOM_CONVERSION_ON_UOM_ID` + FOREIGN KEY (`uomId`) + REFERENCES `uom_conversion` (`id`); diff --git a/src/main/resources/db/changelog/changes/20250715_01_cyril/01_update_inventory_default.sql b/src/main/resources/db/changelog/changes/20250715_01_cyril/01_update_inventory_default.sql new file mode 100644 index 0000000..7ca3a1f --- /dev/null +++ b/src/main/resources/db/changelog/changes/20250715_01_cyril/01_update_inventory_default.sql @@ -0,0 +1,19 @@ +-- liquibase formatted sql +-- changeset cyril:update_inventory_default + +ALTER TABLE `inventory` + DROP FOREIGN KEY `FK_INVENTORY_TO_UOM_ON_UOM_ID`; +ALTER TABLE `inventory` + CHANGE COLUMN `onHandQty` `onHandQty` DECIMAL(14,2) NOT NULL DEFAULT '0.00' , + CHANGE COLUMN `price` `price` INT NULL DEFAULT 0 , + CHANGE COLUMN `cpu` `cpu` DECIMAL(14,2) NULL DEFAULT '0.00' , + CHANGE COLUMN `cpuUnit` `cpuUnit` VARCHAR(255) NULL DEFAULT 'HKD' , + CHANGE COLUMN `cpm` `cpm` DECIMAL(14,2) NULL DEFAULT '0.00' , + CHANGE COLUMN `cpmUnit` `cpmUnit` VARCHAR(255) NULL DEFAULT 'HKD' , + CHANGE COLUMN `uomId` `uomId` INT NULL , + CHANGE COLUMN `status` `status` VARCHAR(255) NOT NULL DEFAULT 'available' ; +ALTER TABLE `inventory` + ADD CONSTRAINT `FK_INVENTORY_TO_UOM_ON_UOM_ID` + FOREIGN KEY (`uomId`) + REFERENCES `uom_conversion` (`id`); +