| @@ -0,0 +1,111 @@ | |||||
| package com.ffii.fpsms.modules.dashboard.service | |||||
| import com.ffii.fpsms.modules.dashboard.web.models.GoodsReceiptStatusResponse | |||||
| import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderRepository | |||||
| import com.ffii.fpsms.modules.qc.entity.QcResultRepository | |||||
| import com.ffii.fpsms.modules.stock.entity.StockInLineRepository | |||||
| import org.springframework.stereotype.Service | |||||
| import java.time.LocalDate | |||||
| @Service | |||||
| class GoodsReceiptStatusService( | |||||
| private val purchaseOrderRepository: PurchaseOrderRepository, | |||||
| private val stockInLineRepository: StockInLineRepository, | |||||
| private val qcResultRepository: QcResultRepository | |||||
| ) { | |||||
| private data class Agg( | |||||
| val supplierId: Long?, | |||||
| val supplierName: String, | |||||
| var expectedNoOfDelivery: Int = 0, | |||||
| val ordersReceivedAtDock: MutableSet<Long> = mutableSetOf(), | |||||
| var itemsInspected: Int = 0, | |||||
| var itemsWithIqcIssue: Int = 0, | |||||
| var itemsCompletedPutAway: Int = 0 | |||||
| ) | |||||
| fun getGoodsReceiptStatus(date: LocalDate): List<GoodsReceiptStatusResponse> { | |||||
| val from = date.atStartOfDay() | |||||
| val to = date.plusDays(1).atStartOfDay() | |||||
| val purchaseOrders = purchaseOrderRepository.findAllByEstimatedArrivalDateRange(from, to) | |||||
| val stockInLines = stockInLineRepository.findByReceiptDateAndDeletedFalse(date) | |||||
| val stockInLineIds = stockInLines.mapNotNull { it.id } | |||||
| val inspectedLineIdSet = if (stockInLineIds.isEmpty()) { | |||||
| emptySet() | |||||
| } else { | |||||
| qcResultRepository | |||||
| .findDistinctStockInLineIdsByStockInLineIdInAndDeletedFalse(stockInLineIds) | |||||
| .toSet() | |||||
| } | |||||
| val failedLineIdSet = if (stockInLineIds.isEmpty()) { | |||||
| emptySet() | |||||
| } else { | |||||
| qcResultRepository | |||||
| .findDistinctFailedStockInLineIdsByStockInLineIdInAndDeletedFalse(stockInLineIds) | |||||
| .toSet() | |||||
| } | |||||
| val bySupplier = mutableMapOf<Long?, Agg>() | |||||
| fun upsertAgg(supplierId: Long?, supplierName: String): Agg { | |||||
| return bySupplier.getOrPut(supplierId) { | |||||
| Agg(supplierId = supplierId, supplierName = supplierName) | |||||
| } | |||||
| } | |||||
| // Expected deliveries (by PO estimated arrival date) | |||||
| purchaseOrders.forEach { po -> | |||||
| val supplier = po.supplier | |||||
| val supplierId = supplier?.id | |||||
| val supplierName = supplier?.name ?: "N/A" | |||||
| upsertAgg(supplierId, supplierName).expectedNoOfDelivery += 1 | |||||
| } | |||||
| // Receiving / IQC / Put-away (by stock-in receipt date) | |||||
| stockInLines.forEach { sil -> | |||||
| val supplier = sil.purchaseOrder?.supplier | |||||
| val supplierId = supplier?.id | |||||
| val supplierName = supplier?.name ?: "N/A" | |||||
| val agg = upsertAgg(supplierId, supplierName) | |||||
| val silId = sil.id ?: return@forEach | |||||
| // Orders received at dock: count orders with DN + (supplier) lot no entered | |||||
| val poId = sil.purchaseOrder?.id | |||||
| if (poId != null && !sil.dnNo.isNullOrBlank() && !sil.productLotNo.isNullOrBlank()) { | |||||
| agg.ordersReceivedAtDock.add(poId) | |||||
| } | |||||
| // Items inspected: any IQC result recorded | |||||
| if (inspectedLineIdSet.contains(silId)) { | |||||
| agg.itemsInspected += 1 | |||||
| } | |||||
| // Items with IQC issue: any failed IQC criteria | |||||
| if (failedLineIdSet.contains(silId)) { | |||||
| agg.itemsWithIqcIssue += 1 | |||||
| } | |||||
| // Put-away completed at store: stock-in line completed | |||||
| if (sil.status.equals("completed", ignoreCase = true)) { | |||||
| agg.itemsCompletedPutAway += 1 | |||||
| } | |||||
| } | |||||
| return bySupplier.values | |||||
| .map { agg -> | |||||
| GoodsReceiptStatusResponse( | |||||
| supplierId = agg.supplierId, | |||||
| supplierName = agg.supplierName, | |||||
| expectedNoOfDelivery = agg.expectedNoOfDelivery, | |||||
| noOfOrdersReceivedAtDock = agg.ordersReceivedAtDock.size, | |||||
| noOfItemsInspected = agg.itemsInspected, | |||||
| noOfItemsWithIqcIssue = agg.itemsWithIqcIssue, | |||||
| noOfItemsCompletedPutAwayAtStore = agg.itemsCompletedPutAway | |||||
| ) | |||||
| } | |||||
| .sortedBy { it.supplierName } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,26 @@ | |||||
| package com.ffii.fpsms.modules.dashboard.web | |||||
| import com.ffii.fpsms.modules.dashboard.service.GoodsReceiptStatusService | |||||
| import com.ffii.fpsms.modules.dashboard.web.models.GoodsReceiptStatusResponse | |||||
| import org.springframework.format.annotation.DateTimeFormat | |||||
| import org.springframework.web.bind.annotation.GetMapping | |||||
| import org.springframework.web.bind.annotation.RequestMapping | |||||
| import org.springframework.web.bind.annotation.RequestParam | |||||
| import org.springframework.web.bind.annotation.RestController | |||||
| import java.time.LocalDate | |||||
| @RestController | |||||
| @RequestMapping("/dashboard") | |||||
| class DashboardController( | |||||
| private val goodsReceiptStatusService: GoodsReceiptStatusService | |||||
| ) { | |||||
| @GetMapping("/goods-receipt-status") | |||||
| fun getGoodsReceiptStatus( | |||||
| @RequestParam(required = false) | |||||
| @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) | |||||
| date: LocalDate? | |||||
| ): List<GoodsReceiptStatusResponse> { | |||||
| return goodsReceiptStatusService.getGoodsReceiptStatus(date ?: LocalDate.now()) | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,12 @@ | |||||
| package com.ffii.fpsms.modules.dashboard.web.models | |||||
| data class GoodsReceiptStatusResponse( | |||||
| val supplierId: Long?, | |||||
| val supplierName: String, | |||||
| val expectedNoOfDelivery: Int, | |||||
| val noOfOrdersReceivedAtDock: Int, | |||||
| val noOfItemsInspected: Int, | |||||
| val noOfItemsWithIqcIssue: Int, | |||||
| val noOfItemsCompletedPutAwayAtStore: Int | |||||
| ) | |||||
| @@ -6,6 +6,7 @@ import com.ffii.fpsms.modules.purchaseOrder.entity.projections.PurchaseOrderInfo | |||||
| import org.springframework.data.domain.Page | import org.springframework.data.domain.Page | ||||
| import org.springframework.stereotype.Repository | import org.springframework.stereotype.Repository | ||||
| import java.io.Serializable | import java.io.Serializable | ||||
| import java.time.LocalDateTime | |||||
| import java.util.Optional | import java.util.Optional | ||||
| import org.springframework.data.domain.Pageable | import org.springframework.data.domain.Pageable | ||||
| import org.springframework.data.jpa.repository.Query | import org.springframework.data.jpa.repository.Query | ||||
| @@ -19,5 +20,19 @@ interface PurchaseOrderRepository : AbstractRepository<PurchaseOrder, Long> { | |||||
| fun findByIdAndDeletedFalse(id: Long): Optional<PurchaseOrder> | fun findByIdAndDeletedFalse(id: Long): Optional<PurchaseOrder> | ||||
| @Query( | |||||
| """ | |||||
| SELECT po FROM PurchaseOrder po | |||||
| WHERE po.deleted = false | |||||
| AND po.estimatedArrivalDate IS NOT NULL | |||||
| AND po.estimatedArrivalDate >= :from | |||||
| AND po.estimatedArrivalDate < :to | |||||
| """ | |||||
| ) | |||||
| fun findAllByEstimatedArrivalDateRange( | |||||
| @Param("from") from: LocalDateTime, | |||||
| @Param("to") to: LocalDateTime | |||||
| ): List<PurchaseOrder> | |||||
| override fun findAll(pageable: Pageable): Page<PurchaseOrder> | override fun findAll(pageable: Pageable): Page<PurchaseOrder> | ||||
| } | } | ||||
| @@ -3,10 +3,37 @@ package com.ffii.fpsms.modules.qc.entity | |||||
| import com.ffii.core.support.AbstractRepository | import com.ffii.core.support.AbstractRepository | ||||
| import com.ffii.fpsms.modules.qc.entity.projection.QcResultInfo | import com.ffii.fpsms.modules.qc.entity.projection.QcResultInfo | ||||
| import com.ffii.fpsms.modules.stock.entity.StockInLine | import com.ffii.fpsms.modules.stock.entity.StockInLine | ||||
| import org.springframework.data.jpa.repository.Query | |||||
| import org.springframework.data.repository.query.Param | |||||
| import org.springframework.stereotype.Repository | import org.springframework.stereotype.Repository | ||||
| @Repository | @Repository | ||||
| interface QcResultRepository: AbstractRepository<QcResult, Long> { | interface QcResultRepository: AbstractRepository<QcResult, Long> { | ||||
| fun findQcResultInfoByStockInLineIdAndDeletedFalse(stockInLineId: Long): List<QcResultInfo> | fun findQcResultInfoByStockInLineIdAndDeletedFalse(stockInLineId: Long): List<QcResultInfo> | ||||
| fun findQcResultInfoByStockOutLineIdAndDeletedFalse(stockOutLineId: Long): List<QcResultInfo> | fun findQcResultInfoByStockOutLineIdAndDeletedFalse(stockOutLineId: Long): List<QcResultInfo> | ||||
| @Query( | |||||
| """ | |||||
| SELECT DISTINCT qr.stockInLine.id | |||||
| FROM QcResult qr | |||||
| WHERE qr.deleted = false | |||||
| AND qr.stockInLine.id IN :stockInLineIds | |||||
| """ | |||||
| ) | |||||
| fun findDistinctStockInLineIdsByStockInLineIdInAndDeletedFalse( | |||||
| @Param("stockInLineIds") stockInLineIds: List<Long> | |||||
| ): List<Long> | |||||
| @Query( | |||||
| """ | |||||
| SELECT DISTINCT qr.stockInLine.id | |||||
| FROM QcResult qr | |||||
| WHERE qr.deleted = false | |||||
| AND qr.stockInLine.id IN :stockInLineIds | |||||
| AND (qr.qcPassed = false OR COALESCE(qr.failQty, 0) > 0) | |||||
| """ | |||||
| ) | |||||
| fun findDistinctFailedStockInLineIdsByStockInLineIdInAndDeletedFalse( | |||||
| @Param("stockInLineIds") stockInLineIds: List<Long> | |||||
| ): List<Long> | |||||
| } | } | ||||