| @@ -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.stereotype.Repository | |||
| import java.io.Serializable | |||
| import java.time.LocalDateTime | |||
| import java.util.Optional | |||
| import org.springframework.data.domain.Pageable | |||
| import org.springframework.data.jpa.repository.Query | |||
| @@ -19,5 +20,19 @@ interface PurchaseOrderRepository : AbstractRepository<PurchaseOrder, Long> { | |||
| 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> | |||
| } | |||
| @@ -3,10 +3,37 @@ package com.ffii.fpsms.modules.qc.entity | |||
| import com.ffii.core.support.AbstractRepository | |||
| import com.ffii.fpsms.modules.qc.entity.projection.QcResultInfo | |||
| 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 | |||
| @Repository | |||
| interface QcResultRepository: AbstractRepository<QcResult, Long> { | |||
| fun findQcResultInfoByStockInLineIdAndDeletedFalse(stockInLineId: 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> | |||
| } | |||