Rev 37704 | Blame | Compare with Previous | Last modification | View Log | RSS feed
package com.smartdukaan.cron.scheduled.purchaseorder;import com.spice.profitmandi.common.model.ProfitMandiConstants;import com.spice.profitmandi.dao.entity.auth.AuthUser;import com.spice.profitmandi.dao.entity.inventory.InventoryWarehouse;import com.spice.profitmandi.dao.entity.transaction.Order;import com.spice.profitmandi.dao.entity.warehouse.Supplier;import com.spice.profitmandi.dao.entity.warehouse.WarehouseLineItem;import com.spice.profitmandi.dao.entity.warehouse.WarehousePurchaseOrder;import com.spice.profitmandi.dao.entity.warehouse.WarehouseSupplierInvoice;import com.spice.profitmandi.dao.enumuration.cs.EscalationType;import com.spice.profitmandi.dao.enumuration.warehouse.WarehouseInvoiceStatus;import com.spice.profitmandi.dao.repository.cs.CsService;import com.spice.profitmandi.dao.repository.inventory.InventoryWarehouseRepository;import com.spice.profitmandi.dao.repository.transaction.OrderRepository;import com.spice.profitmandi.dao.repository.warehouse.SupplierRepository;import com.spice.profitmandi.dao.repository.warehouse.WarehouseLineItemRepository;import com.spice.profitmandi.dao.repository.warehouse.WarehousePurchaseOrderRepository;import com.spice.profitmandi.dao.repository.warehouse.WarehouseSupplierInvoiceRepository;import com.spice.profitmandi.service.mail.MailOutboxService;import in.shop2020.purchase.POStatus;import org.apache.logging.log4j.LogManager;import org.apache.logging.log4j.Logger;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import org.springframework.transaction.annotation.Transactional;import java.time.LocalDate;import java.time.format.DateTimeFormatter;import java.time.temporal.ChronoUnit;import java.util.ArrayList;import java.util.Arrays;import java.util.Collections;import java.util.Comparator;import java.util.HashMap;import java.util.List;import java.util.Map;import java.util.Objects;import java.util.Set;import java.util.stream.Collectors;@Component@Transactional(rollbackFor = Throwable.class)public class POScheduler {private static final Logger LOGGER = LogManager.getLogger(POScheduler.class);private static final int VENDOR_PO_OPEN_DAYS = 6;private static final int MOVEMENT_PO_ALERT_DAYS = 4;@AutowiredWarehousePurchaseOrderRepository warehousePurchaseOrderRepository;@AutowiredSupplierRepository supplierRepository;@AutowiredWarehouseLineItemRepository warehouseLineItemRepository;@AutowiredWarehouseSupplierInvoiceRepository warehouseSupplierInvoiceRepository;@AutowiredOrderRepository orderRepository;@AutowiredInventoryWarehouseRepository inventoryWarehouseRepository;@AutowiredCsService csService;@AutowiredMailOutboxService mailOutboxService;// Closes vendor POs that have been open longer than their window. Movements are never closed here: one stays open// until it is received, its orders are cancelled, or it is closed by hand - see sendOpenMovementDigest.public void autoClosePurchaseOrders() {Set<Integer> internalSupplierIds = this.internalSupplierIds();LocalDate cutoff = LocalDate.now().minusDays(VENDOR_PO_OPEN_DAYS);List<WarehousePurchaseOrder> posToClose = this.openPurchaseOrders().stream().filter(po -> !internalSupplierIds.contains(po.getSupplierId())).filter(po -> po.getCreatedAt().toLocalDate().isBefore(cutoff)).collect(Collectors.toList());posToClose.forEach(po -> po.setStatus(POStatus.CLOSED));LOGGER.info("Auto-closed {} vendor POs", posToClose.size());}// Mails the warehouse team every movement PO still open after MOVEMENT_PO_ALERT_DAYS, with what is holding it.public void sendOpenMovementDigest() {Map<Integer, Supplier> internalSuppliers = supplierRepository.selectAll().stream().filter(Supplier::isInternal).collect(Collectors.toMap(Supplier::getId, x -> x));LocalDate cutoff = LocalDate.now().minusDays(MOVEMENT_PO_ALERT_DAYS);List<WarehousePurchaseOrder> overdue = this.openPurchaseOrders().stream().filter(po -> internalSuppliers.containsKey(po.getSupplierId())).filter(po -> !po.getCreatedAt().toLocalDate().isAfter(cutoff)).sorted(Comparator.comparing(WarehousePurchaseOrder::getWarehouseId).thenComparing(WarehousePurchaseOrder::getCreatedAt)).collect(Collectors.toList());if (overdue.isEmpty()) {LOGGER.info("No movement PO open beyond {} days", MOVEMENT_PO_ALERT_DAYS);return;}String[] recipients = csService.getAuthUserIds(ProfitMandiConstants.TICKET_CATEGORY_WAREHOUSE,Arrays.asList(EscalationType.L1, EscalationType.L2)).stream().map(AuthUser::getEmailId).filter(Objects::nonNull).distinct().toArray(String[]::new);if (recipients.length == 0) {LOGGER.warn("No warehouse L1/L2 recipients for the open movement PO digest");return;}Map<Integer, String> warehouseNames = new HashMap<>();StringBuilder rows = new StringBuilder();for (WarehousePurchaseOrder po : overdue) {Supplier supplier = internalSuppliers.get(po.getSupplierId());List<WarehouseLineItem> lines = warehouseLineItemRepository.selectAllByPurchaseOrderId(po.getId());int openQty = lines.stream().filter(x -> !x.isDiscarded()).mapToInt(WarehouseLineItem::getUnfulfilledQuantity).sum();double openValue = lines.stream().filter(x -> !x.isDiscarded()).mapToDouble(x -> x.getUnfulfilledQuantity() * (double) x.getUnitPrice()).sum();rows.append("<tr><td>").append(po.getPoNumber()).append("</td><td>").append(this.warehouseName(supplier.getInternalWarehouseId(), warehouseNames)).append("</td><td>").append(this.warehouseName(po.getWarehouseId(), warehouseNames)).append("</td><td>").append(po.getCreatedAt().toLocalDate().format(DateTimeFormatter.ofPattern("dd-MMM-yy"))).append("</td><td>").append(ChronoUnit.DAYS.between(po.getCreatedAt().toLocalDate(), LocalDate.now())).append("</td><td>").append(openQty).append("</td><td>").append(String.format("%,.0f", openValue)).append("</td><td>").append(this.holdingReason(po)).append("</td></tr>");}String body = "<p>Movement purchase orders open for " + MOVEMENT_PO_ALERT_DAYS + " days or more. Receive the billed "+ "invoices against their own PO, or cancel orders that will not ship.</p>"+ "<table border='1' cellpadding='4' cellspacing='0' style='border-collapse:collapse;font-family:Arial;font-size:12px'>"+ "<tr style='background:#1F3864;color:#fff'><th>PO</th><th>From</th><th>To</th><th>PO date</th><th>Days open</th>"+ "<th>Open qty</th><th>Open value (Rs)</th><th>Holding it</th></tr>" + rows + "</table>";mailOutboxService.queueMailViaGoogle(recipients, null,"Open movement POs (" + overdue.size() + ") - " + LocalDate.now().format(DateTimeFormatter.ofPattern("dd-MMM-yyyy")),body, true, "POScheduler.sendOpenMovementDigest");LOGGER.info("Open movement PO digest queued: {} POs to {}", overdue.size(), Arrays.toString(recipients));}private String holdingReason(WarehousePurchaseOrder po) {if (po.getTransactionId() == null || po.getTransactionId() <= 0) {return "Receive-only PO: invoice not yet received";}List<Order> live = orderRepository.selectAllByTransactionId(po.getTransactionId()).stream().filter(o -> o.getRefundTimestamp() == null).collect(Collectors.toList());List<String> notBilled = live.stream().filter(o -> o.getBillingTimestamp() == null).map(o -> String.valueOf(o.getId())).collect(Collectors.toList());List<String> invoices = live.stream().map(Order::getInvoiceNumber).filter(Objects::nonNull).distinct().collect(Collectors.toList());Set<String> received = invoices.isEmpty() ? Collections.<String>emptySet(): warehouseSupplierInvoiceRepository.selectAllBySupplierInvoice(po.getSupplierId(), invoices).stream().filter(x -> WarehouseInvoiceStatus.received.equals(x.getStatus())).map(WarehouseSupplierInvoice::getInvoiceNumber).collect(Collectors.toSet());List<String> reasons = new ArrayList<>();if (!notBilled.isEmpty()) {reasons.add("Not billed: order " + String.join(", ", notBilled));}List<String> pending = invoices.stream().filter(x -> !received.contains(x)).collect(Collectors.toList());if (!pending.isEmpty()) {reasons.add("Billed, not received: " + String.join(", ", pending));}return reasons.isEmpty() ? "No live order - open quantity will not arrive; close the PO" : String.join("<br>", reasons);}private String warehouseName(Integer warehouseId, Map<Integer, String> cache) {if (warehouseId == null) {return "-";}return cache.computeIfAbsent(warehouseId, id -> {InventoryWarehouse warehouse = inventoryWarehouseRepository.selectById(id);return warehouse == null ? String.valueOf(id) : id + " " + warehouse.getDisplayName();});}private List<WarehousePurchaseOrder> openPurchaseOrders() {List<WarehousePurchaseOrder> open = new ArrayList<>(warehousePurchaseOrderRepository.selectAllByStatus(POStatus.READY));open.addAll(warehousePurchaseOrderRepository.selectAllByStatus(POStatus.PARTIALLY_FULFILLED));return open;}private Set<Integer> internalSupplierIds() {return supplierRepository.selectAll().stream().filter(Supplier::isInternal).map(Supplier::getId).collect(Collectors.toSet());}}