Subversion Repositories SmartDukaan

Rev

Rev 37704 | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 37704 Rev 37771
Line 1... Line 1...
1
package com.smartdukaan.cron.scheduled.purchaseorder;
1
package com.smartdukaan.cron.scheduled.purchaseorder;
2
 
2
 
-
 
3
import com.spice.profitmandi.common.model.ProfitMandiConstants;
-
 
4
import com.spice.profitmandi.dao.entity.auth.AuthUser;
3
import com.spice.profitmandi.dao.entity.inventory.Vendor;
5
import com.spice.profitmandi.dao.entity.inventory.InventoryWarehouse;
-
 
6
import com.spice.profitmandi.dao.entity.transaction.Order;
4
import com.spice.profitmandi.dao.entity.warehouse.Supplier;
7
import com.spice.profitmandi.dao.entity.warehouse.Supplier;
-
 
8
import com.spice.profitmandi.dao.entity.warehouse.WarehouseLineItem;
5
import com.spice.profitmandi.dao.entity.warehouse.WarehousePurchaseOrder;
9
import com.spice.profitmandi.dao.entity.warehouse.WarehousePurchaseOrder;
-
 
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;
6
import com.spice.profitmandi.dao.repository.warehouse.SupplierRepository;
16
import com.spice.profitmandi.dao.repository.warehouse.SupplierRepository;
-
 
17
import com.spice.profitmandi.dao.repository.warehouse.WarehouseLineItemRepository;
7
import com.spice.profitmandi.dao.repository.warehouse.WarehousePurchaseOrderRepository;
18
import com.spice.profitmandi.dao.repository.warehouse.WarehousePurchaseOrderRepository;
-
 
19
import com.spice.profitmandi.dao.repository.warehouse.WarehouseSupplierInvoiceRepository;
-
 
20
import com.spice.profitmandi.service.mail.MailOutboxService;
8
import in.shop2020.purchase.POStatus;
21
import in.shop2020.purchase.POStatus;
9
import org.apache.logging.log4j.core.config.Scheduled;
22
import org.apache.logging.log4j.LogManager;
-
 
23
import org.apache.logging.log4j.Logger;
10
import org.springframework.beans.factory.annotation.Autowired;
24
import org.springframework.beans.factory.annotation.Autowired;
11
import org.springframework.stereotype.Component;
25
import org.springframework.stereotype.Component;
12
import org.springframework.transaction.annotation.Transactional;
26
import org.springframework.transaction.annotation.Transactional;
13
 
27
 
14
import java.time.LocalDate;
28
import java.time.LocalDate;
-
 
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;
15
import java.util.List;
36
import java.util.List;
-
 
37
import java.util.Map;
-
 
38
import java.util.Objects;
16
import java.util.Set;
39
import java.util.Set;
17
import java.util.stream.Collectors;
40
import java.util.stream.Collectors;
18
 
41
 
19
@Component
42
@Component
20
@Transactional(rollbackFor = Throwable.class)
43
@Transactional(rollbackFor = Throwable.class)
21
public class POScheduler {
44
public class POScheduler {
-
 
45
 
-
 
46
    private static final Logger LOGGER = LogManager.getLogger(POScheduler.class);
-
 
47
 
-
 
48
    private static final int VENDOR_PO_OPEN_DAYS = 6;
22
    //Auto close non-internal vendor POs greater than seven days
49
    private static final int MOVEMENT_PO_ALERT_DAYS = 4;
-
 
50
 
23
    @Autowired
51
    @Autowired
24
    WarehousePurchaseOrderRepository warehousePurchaseOrderRepository;
52
    WarehousePurchaseOrderRepository warehousePurchaseOrderRepository;
25
    @Autowired
53
    @Autowired
26
    SupplierRepository supplierRepository;
54
    SupplierRepository supplierRepository;
-
 
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;
27
 
67
 
28
 
-
 
-
 
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.
29
    public void autoClosePurchaseOrders() {
70
    public void autoClosePurchaseOrders() {
30
        System.out.println("At autoclose pos");
71
        Set<Integer> internalSupplierIds = this.internalSupplierIds();
31
        List<WarehousePurchaseOrder> warehousePurchaseOrderList = warehousePurchaseOrderRepository.selectAllByStatus(POStatus.READY);
-
 
32
        warehousePurchaseOrderList.addAll(warehousePurchaseOrderRepository.selectAllByStatus(POStatus.PARTIALLY_FULFILLED));
-
 
33
        //Also ignore internal pos
-
 
34
        Set<Integer> internalSupplierIds = supplierRepository.selectAll().stream().filter(x->x.isInternal()).map(x->x.getId()).collect(Collectors.toSet());
-
 
35
 
-
 
36
        // Aged from getOpenSince(), not createdAt: a PO reopened because its receipt was delayed gets the same fresh
-
 
37
        // window a new one gets, instead of being closed straight back on the next sweep for being older than the
72
        LocalDate cutoff = LocalDate.now().minusDays(VENDOR_PO_OPEN_DAYS);
38
        // window it was reopened past.
-
 
39
        List<WarehousePurchaseOrder> posToClose = warehousePurchaseOrderList.stream()
73
        List<WarehousePurchaseOrder> posToClose = this.openPurchaseOrders().stream()
40
                .filter(x->!internalSupplierIds.contains(x.getSupplierId()))
74
                .filter(po -> !internalSupplierIds.contains(po.getSupplierId()))
41
                .filter(x -> x.getOpenSince().isBefore(LocalDate.now().minusDays(6).atStartOfDay()))
75
                .filter(po -> po.getCreatedAt().toLocalDate().isBefore(cutoff))
42
                .collect(Collectors.toList());
76
                .collect(Collectors.toList());
-
 
77
        posToClose.forEach(po -> po.setStatus(POStatus.CLOSED));
-
 
78
        LOGGER.info("Auto-closed {} vendor POs", posToClose.size());
-
 
79
    }
-
 
80
 
-
 
81
    // Mails the warehouse team every movement PO still open after MOVEMENT_PO_ALERT_DAYS, with what is holding it.
43
        //Close internal POS in four days
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);
44
        posToClose.addAll(warehousePurchaseOrderList.stream().
86
        List<WarehousePurchaseOrder> overdue = this.openPurchaseOrders().stream()
45
                filter(x->internalSupplierIds.contains(x.getSupplierId())).
87
                .filter(po -> internalSuppliers.containsKey(po.getSupplierId()))
46
                filter(x->x.getOpenSince().isBefore(LocalDate.now().minusDays(4).atStartOfDay())).collect(Collectors.toList()));
88
                .filter(po -> !po.getCreatedAt().toLocalDate().isAfter(cutoff))
-
 
89
                .sorted(Comparator.comparing(WarehousePurchaseOrder::getWarehouseId).thenComparing(WarehousePurchaseOrder::getCreatedAt))
-
 
90
                .collect(Collectors.toList());
-
 
91
        if (overdue.isEmpty()) {
-
 
92
            LOGGER.info("No movement PO open beyond {} days", MOVEMENT_PO_ALERT_DAYS);
-
 
93
            return;
-
 
94
        }
-
 
95
 
-
 
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
        }
-
 
103
 
-
 
104
        Map<Integer, String> warehouseNames = new HashMap<>();
-
 
105
        StringBuilder rows = new StringBuilder();
47
        for (WarehousePurchaseOrder warehousePurchaseOrder : posToClose) {
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));
-
 
130
    }
-
 
131
 
48
            //System.out.println(warehousePurchaseOrder);
132
    private String holdingReason(WarehousePurchaseOrder po) {
-
 
133
        if (po.getTransactionId() == null || po.getTransactionId() <= 0) {
49
            warehousePurchaseOrder.setStatus(POStatus.CLOSED);
134
            return "Receive-only PO: invoice not yet received";
50
        }
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
    }
51
 
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
    }
52
 
171
 
-
 
172
    private Set<Integer> internalSupplierIds() {
-
 
173
        return supplierRepository.selectAll().stream().filter(Supplier::isInternal).map(Supplier::getId).collect(Collectors.toSet());
53
    }
174
    }
54
}
175
}