Subversion Repositories SmartDukaan

Rev

Rev 37704 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
32907 amit.gupta 1
package com.smartdukaan.cron.scheduled.purchaseorder;
2
 
37771 amit 3
import com.spice.profitmandi.common.model.ProfitMandiConstants;
4
import com.spice.profitmandi.dao.entity.auth.AuthUser;
5
import com.spice.profitmandi.dao.entity.inventory.InventoryWarehouse;
6
import com.spice.profitmandi.dao.entity.transaction.Order;
32907 amit.gupta 7
import com.spice.profitmandi.dao.entity.warehouse.Supplier;
37771 amit 8
import com.spice.profitmandi.dao.entity.warehouse.WarehouseLineItem;
32907 amit.gupta 9
import com.spice.profitmandi.dao.entity.warehouse.WarehousePurchaseOrder;
37771 amit 10
import com.spice.profitmandi.dao.entity.warehouse.WarehouseSupplierInvoice;
11
import com.spice.profitmandi.dao.enumuration.cs.EscalationType;
12
import com.spice.profitmandi.dao.enumuration.warehouse.WarehouseInvoiceStatus;
13
import com.spice.profitmandi.dao.repository.cs.CsService;
14
import com.spice.profitmandi.dao.repository.inventory.InventoryWarehouseRepository;
15
import com.spice.profitmandi.dao.repository.transaction.OrderRepository;
32907 amit.gupta 16
import com.spice.profitmandi.dao.repository.warehouse.SupplierRepository;
37771 amit 17
import com.spice.profitmandi.dao.repository.warehouse.WarehouseLineItemRepository;
32907 amit.gupta 18
import com.spice.profitmandi.dao.repository.warehouse.WarehousePurchaseOrderRepository;
37771 amit 19
import com.spice.profitmandi.dao.repository.warehouse.WarehouseSupplierInvoiceRepository;
20
import com.spice.profitmandi.service.mail.MailOutboxService;
32907 amit.gupta 21
import in.shop2020.purchase.POStatus;
37771 amit 22
import org.apache.logging.log4j.LogManager;
23
import org.apache.logging.log4j.Logger;
32907 amit.gupta 24
import org.springframework.beans.factory.annotation.Autowired;
25
import org.springframework.stereotype.Component;
26
import org.springframework.transaction.annotation.Transactional;
27
 
28
import java.time.LocalDate;
37771 amit 29
import java.time.format.DateTimeFormatter;
30
import java.time.temporal.ChronoUnit;
31
import java.util.ArrayList;
32
import java.util.Arrays;
33
import java.util.Collections;
34
import java.util.Comparator;
35
import java.util.HashMap;
32907 amit.gupta 36
import java.util.List;
37771 amit 37
import java.util.Map;
38
import java.util.Objects;
32907 amit.gupta 39
import java.util.Set;
40
import java.util.stream.Collectors;
41
 
42
@Component
43
@Transactional(rollbackFor = Throwable.class)
44
public class POScheduler {
37771 amit 45
 
46
    private static final Logger LOGGER = LogManager.getLogger(POScheduler.class);
47
 
48
    private static final int VENDOR_PO_OPEN_DAYS = 6;
49
    private static final int MOVEMENT_PO_ALERT_DAYS = 4;
50
 
32907 amit.gupta 51
    @Autowired
52
    WarehousePurchaseOrderRepository warehousePurchaseOrderRepository;
53
    @Autowired
54
    SupplierRepository supplierRepository;
37771 amit 55
    @Autowired
56
    WarehouseLineItemRepository warehouseLineItemRepository;
57
    @Autowired
58
    WarehouseSupplierInvoiceRepository warehouseSupplierInvoiceRepository;
59
    @Autowired
60
    OrderRepository orderRepository;
61
    @Autowired
62
    InventoryWarehouseRepository inventoryWarehouseRepository;
63
    @Autowired
64
    CsService csService;
65
    @Autowired
66
    MailOutboxService mailOutboxService;
32907 amit.gupta 67
 
37771 amit 68
    // Closes vendor POs that have been open longer than their window. Movements are never closed here: one stays open
69
    // until it is received, its orders are cancelled, or it is closed by hand - see sendOpenMovementDigest.
32907 amit.gupta 70
    public void autoClosePurchaseOrders() {
37771 amit 71
        Set<Integer> internalSupplierIds = this.internalSupplierIds();
72
        LocalDate cutoff = LocalDate.now().minusDays(VENDOR_PO_OPEN_DAYS);
73
        List<WarehousePurchaseOrder> posToClose = this.openPurchaseOrders().stream()
74
                .filter(po -> !internalSupplierIds.contains(po.getSupplierId()))
75
                .filter(po -> po.getCreatedAt().toLocalDate().isBefore(cutoff))
76
                .collect(Collectors.toList());
77
        posToClose.forEach(po -> po.setStatus(POStatus.CLOSED));
78
        LOGGER.info("Auto-closed {} vendor POs", posToClose.size());
79
    }
32907 amit.gupta 80
 
37771 amit 81
    // Mails the warehouse team every movement PO still open after MOVEMENT_PO_ALERT_DAYS, with what is holding it.
82
    public void sendOpenMovementDigest() {
83
        Map<Integer, Supplier> internalSuppliers = supplierRepository.selectAll().stream().filter(Supplier::isInternal)
84
                .collect(Collectors.toMap(Supplier::getId, x -> x));
85
        LocalDate cutoff = LocalDate.now().minusDays(MOVEMENT_PO_ALERT_DAYS);
86
        List<WarehousePurchaseOrder> overdue = this.openPurchaseOrders().stream()
87
                .filter(po -> internalSuppliers.containsKey(po.getSupplierId()))
88
                .filter(po -> !po.getCreatedAt().toLocalDate().isAfter(cutoff))
89
                .sorted(Comparator.comparing(WarehousePurchaseOrder::getWarehouseId).thenComparing(WarehousePurchaseOrder::getCreatedAt))
32907 amit.gupta 90
                .collect(Collectors.toList());
37771 amit 91
        if (overdue.isEmpty()) {
92
            LOGGER.info("No movement PO open beyond {} days", MOVEMENT_PO_ALERT_DAYS);
93
            return;
32907 amit.gupta 94
        }
95
 
37771 amit 96
        String[] recipients = csService.getAuthUserIds(ProfitMandiConstants.TICKET_CATEGORY_WAREHOUSE,
97
                Arrays.asList(EscalationType.L1, EscalationType.L2)).stream()
98
                .map(AuthUser::getEmailId).filter(Objects::nonNull).distinct().toArray(String[]::new);
99
        if (recipients.length == 0) {
100
            LOGGER.warn("No warehouse L1/L2 recipients for the open movement PO digest");
101
            return;
102
        }
32907 amit.gupta 103
 
37771 amit 104
        Map<Integer, String> warehouseNames = new HashMap<>();
105
        StringBuilder rows = new StringBuilder();
106
        for (WarehousePurchaseOrder po : overdue) {
107
            Supplier supplier = internalSuppliers.get(po.getSupplierId());
108
            List<WarehouseLineItem> lines = warehouseLineItemRepository.selectAllByPurchaseOrderId(po.getId());
109
            int openQty = lines.stream().filter(x -> !x.isDiscarded()).mapToInt(WarehouseLineItem::getUnfulfilledQuantity).sum();
110
            double openValue = lines.stream().filter(x -> !x.isDiscarded())
111
                    .mapToDouble(x -> x.getUnfulfilledQuantity() * (double) x.getUnitPrice()).sum();
112
            rows.append("<tr><td>").append(po.getPoNumber()).append("</td><td>")
113
                    .append(this.warehouseName(supplier.getInternalWarehouseId(), warehouseNames)).append("</td><td>")
114
                    .append(this.warehouseName(po.getWarehouseId(), warehouseNames)).append("</td><td>")
115
                    .append(po.getCreatedAt().toLocalDate().format(DateTimeFormatter.ofPattern("dd-MMM-yy"))).append("</td><td>")
116
                    .append(ChronoUnit.DAYS.between(po.getCreatedAt().toLocalDate(), LocalDate.now())).append("</td><td>")
117
                    .append(openQty).append("</td><td>").append(String.format("%,.0f", openValue)).append("</td><td>")
118
                    .append(this.holdingReason(po)).append("</td></tr>");
119
        }
120
 
121
        String body = "<p>Movement purchase orders open for " + MOVEMENT_PO_ALERT_DAYS + " days or more. Receive the billed "
122
                + "invoices against their own PO, or cancel orders that will not ship.</p>"
123
                + "<table border='1' cellpadding='4' cellspacing='0' style='border-collapse:collapse;font-family:Arial;font-size:12px'>"
124
                + "<tr style='background:#1F3864;color:#fff'><th>PO</th><th>From</th><th>To</th><th>PO date</th><th>Days open</th>"
125
                + "<th>Open qty</th><th>Open value (Rs)</th><th>Holding it</th></tr>" + rows + "</table>";
126
        mailOutboxService.queueMailViaGoogle(recipients, null,
127
                "Open movement POs (" + overdue.size() + ") - " + LocalDate.now().format(DateTimeFormatter.ofPattern("dd-MMM-yyyy")),
128
                body, true, "POScheduler.sendOpenMovementDigest");
129
        LOGGER.info("Open movement PO digest queued: {} POs to {}", overdue.size(), Arrays.toString(recipients));
32907 amit.gupta 130
    }
37771 amit 131
 
132
    private String holdingReason(WarehousePurchaseOrder po) {
133
        if (po.getTransactionId() == null || po.getTransactionId() <= 0) {
134
            return "Receive-only PO: invoice not yet received";
135
        }
136
        List<Order> live = orderRepository.selectAllByTransactionId(po.getTransactionId()).stream()
137
                .filter(o -> o.getRefundTimestamp() == null).collect(Collectors.toList());
138
        List<String> notBilled = live.stream().filter(o -> o.getBillingTimestamp() == null)
139
                .map(o -> String.valueOf(o.getId())).collect(Collectors.toList());
140
        List<String> invoices = live.stream().map(Order::getInvoiceNumber).filter(Objects::nonNull).distinct().collect(Collectors.toList());
141
        Set<String> received = invoices.isEmpty() ? Collections.<String>emptySet()
142
                : warehouseSupplierInvoiceRepository.selectAllBySupplierInvoice(po.getSupplierId(), invoices).stream()
143
                .filter(x -> WarehouseInvoiceStatus.received.equals(x.getStatus()))
144
                .map(WarehouseSupplierInvoice::getInvoiceNumber).collect(Collectors.toSet());
145
        List<String> reasons = new ArrayList<>();
146
        if (!notBilled.isEmpty()) {
147
            reasons.add("Not billed: order " + String.join(", ", notBilled));
148
        }
149
        List<String> pending = invoices.stream().filter(x -> !received.contains(x)).collect(Collectors.toList());
150
        if (!pending.isEmpty()) {
151
            reasons.add("Billed, not received: " + String.join(", ", pending));
152
        }
153
        return reasons.isEmpty() ? "No live order - open quantity will not arrive; close the PO" : String.join("<br>", reasons);
154
    }
155
 
156
    private String warehouseName(Integer warehouseId, Map<Integer, String> cache) {
157
        if (warehouseId == null) {
158
            return "-";
159
        }
160
        return cache.computeIfAbsent(warehouseId, id -> {
161
            InventoryWarehouse warehouse = inventoryWarehouseRepository.selectById(id);
162
            return warehouse == null ? String.valueOf(id) : id + " " + warehouse.getDisplayName();
163
        });
164
    }
165
 
166
    private List<WarehousePurchaseOrder> openPurchaseOrders() {
167
        List<WarehousePurchaseOrder> open = new ArrayList<>(warehousePurchaseOrderRepository.selectAllByStatus(POStatus.READY));
168
        open.addAll(warehousePurchaseOrderRepository.selectAllByStatus(POStatus.PARTIALLY_FULFILLED));
169
        return open;
170
    }
171
 
172
    private Set<Integer> internalSupplierIds() {
173
        return supplierRepository.selectAll().stream().filter(Supplier::isInternal).map(Supplier::getId).collect(Collectors.toSet());
174
    }
32907 amit.gupta 175
}