Subversion Repositories SmartDukaan

Rev

Rev 37828 | Go to most recent revision | Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
37825 amit 1
package com.smartdukaan.cron.migrations;
2
 
3
import com.spice.profitmandi.common.enumuration.ItemType;
4
import com.spice.profitmandi.common.exception.ProfitMandiBusinessException;
5
import com.spice.profitmandi.common.model.ProfitMandiConstants;
6
import com.spice.profitmandi.dao.entity.auth.AuthUser;
7
import com.spice.profitmandi.dao.entity.catalog.Item;
8
import com.spice.profitmandi.dao.entity.inventory.InventoryWarehouse;
9
import com.spice.profitmandi.dao.entity.inventory.SaholicInventorySnapshot;
10
import com.spice.profitmandi.dao.entity.transaction.Order;
11
import com.spice.profitmandi.dao.entity.warehouse.Supplier;
12
import com.spice.profitmandi.dao.entity.warehouse.WarehouseInventoryItem;
13
import com.spice.profitmandi.dao.entity.warehouse.WarehousePurchaseOrder;
14
import com.spice.profitmandi.dao.model.transaction.OrderBillingModel;
15
import com.spice.profitmandi.dao.repository.auth.AuthRepository;
16
import com.spice.profitmandi.dao.repository.inventory.InventoryWarehouseRepository;
17
import com.spice.profitmandi.dao.repository.transaction.OrderRepository;
18
import com.spice.profitmandi.dao.repository.warehouse.SupplierRepository;
19
import com.spice.profitmandi.dao.repository.warehouse.WarehouseInventoryItemRepository;
20
import com.spice.profitmandi.dao.repository.warehouse.WarehousePurchaseOrderRepository;
21
import com.spice.profitmandi.service.inventory.SaholicInventoryService;
22
import com.spice.profitmandi.service.transaction.TransactionService;
23
import com.spice.profitmandi.service.warehouse.WarehouseServiceImpl;
24
import in.shop2020.model.v1.inventory.InventoryType;
25
import in.shop2020.model.v1.inventory.WarehouseType;
26
import in.shop2020.model.v1.order.OrderStatus;
27
import org.apache.logging.log4j.LogManager;
28
import org.apache.logging.log4j.Logger;
29
import org.springframework.beans.factory.annotation.Autowired;
30
import org.springframework.stereotype.Component;
31
import org.springframework.transaction.annotation.Transactional;
32
 
33
import java.util.ArrayList;
34
import java.util.Collections;
35
import java.util.Comparator;
36
import java.util.HashMap;
37
import java.util.LinkedHashMap;
38
import java.util.List;
39
import java.util.Map;
40
import java.util.stream.Collectors;
41
 
42
/**
43
 * Bills the orders an internal movement PO raised, the way a biller does it on the order management screen: an order
44
 * parked on the Dummy (its brand has no source mapping) is first pointed at the vendor warehouse that holds the stock,
45
 * serialized units are taken oldest first from that vendor warehouse - only IMEIs whose last scan leaves them in stock -
46
 * and the whole transaction is billed as one invoice through acceptOrders / addBillingDetailsForGrouppedOrders.
47
 *
48
 * The plan is worked out the same way for a dry run and a live one, and the live run re-plans inside its transaction,
49
 * so nothing is billed unless every order of the transaction can be. The dry run makes no billing calls at all:
50
 * billing opens a REQUIRES_NEW transaction of its own (createLoanForBilling) that a rollback would not undo.
51
 */
52
@Component
53
public class MovementBiller {
54
 
55
    private static final Logger LOGGER = LogManager.getLogger(MovementBiller.class);
56
 
57
    // NIC rejects an IRN with more than 1000 item lines; one order is one line.
58
    private static final int MAX_INVOICE_LINES = 900;
59
 
60
    @Autowired
61
    private WarehousePurchaseOrderRepository warehousePurchaseOrderRepository;
62
    @Autowired
63
    private SupplierRepository supplierRepository;
64
    @Autowired
65
    private OrderRepository orderRepository;
66
    @Autowired
67
    private TransactionService transactionService;
68
    @Autowired
69
    private SaholicInventoryService saholicInventoryService;
70
    @Autowired
71
    private InventoryWarehouseRepository inventoryWarehouseRepository;
72
    @Autowired
73
    private WarehouseInventoryItemRepository warehouseInventoryItemRepository;
74
    @Autowired
75
    private AuthRepository authRepository;
76
 
77
    /** One invoice line: an order, the vendor warehouse it bills from and the IMEIs it takes. */
78
    static class Line {
79
        int orderId;
80
        int itemId;
81
        String item;
82
        int qty;
83
        int fromWarehouseId;
84
        boolean repoint;
85
        boolean serialized;
86
        List<String> imeis = new ArrayList<>();
87
    }
88
 
89
    static class Plan {
90
        int transactionId;
91
        String poNumber;
92
        int sourceWarehouseId;
93
        int destinationWarehouseId;
94
        List<Line> lines = new ArrayList<>();
95
        List<String> problems = new ArrayList<>();
96
    }
97
 
98
    @Transactional(readOnly = true, rollbackFor = Throwable.class)
99
    public Plan describe(int transactionId) throws ProfitMandiBusinessException {
100
        return this.plan(transactionId);
101
    }
102
 
103
    /** Bills the whole transaction or nothing. Returns the invoice number. */
104
    @Transactional(rollbackFor = Throwable.class)
105
    public String bill(int transactionId, String operatorEmail, int logisticsProviderId) throws Exception {
106
        AuthUser operator = authRepository.selectByEmailOrMobile(operatorEmail);
107
        if (operator == null) {
108
            throw new ProfitMandiBusinessException("email", operatorEmail, "No auth user for " + operatorEmail);
109
        }
110
        Plan plan = this.plan(transactionId);
111
        if (!plan.problems.isEmpty()) {
112
            throw new ProfitMandiBusinessException("transaction", transactionId, String.join("; ", plan.problems));
113
        }
114
 
115
        // Point Dummy-parked orders at the vendor warehouse holding the stock, splitting an order whose quantity
116
        // comes from more than one vendor warehouse. The remainder of a split keeps the Dummy until its own chunk.
117
        Map<Integer, Integer> remainderOf = new HashMap<>();
118
        for (Line line : plan.lines) {
119
            if (!line.repoint) {
120
                continue;
121
            }
122
            int orderId = remainderOf.getOrDefault(line.orderId, line.orderId);
123
            Order order = orderRepository.selectById(orderId);
124
            int orderQty = order.getLineItem().getQuantity();
125
            if (line.qty < orderQty) {
126
                Order remainder = transactionService.splitOrder(orderId, orderQty - line.qty);
127
                remainderOf.put(line.orderId, remainder.getId());
128
            }
129
            transactionService.changeFulfillmentWarehouse(orderId, line.fromWarehouseId);
130
            line.orderId = orderId;
131
        }
132
 
133
        List<OrderBillingModel> models = new ArrayList<>();
134
        for (Line line : plan.lines) {
135
            OrderBillingModel model = new OrderBillingModel();
136
            model.setOrderId(line.orderId);
137
            model.setQuantityToBill(line.qty);
138
            model.setSerialNumbersToBill(line.imeis.stream().map(String::toLowerCase).collect(Collectors.toList()));
139
            model.setLogisticsProviderId(logisticsProviderId);
140
            model.setDistance("");
141
            models.add(model);
142
        }
143
        transactionService.acceptOrders(models);
144
        String invoiceNumber = transactionService.addBillingDetailsForGrouppedOrders(models, plan.sourceWarehouseId,
145
                operator.getFirstName());
146
        if (invoiceNumber != null) {
147
            transactionService.checkOrderDispatchEligibility(transactionId, invoiceNumber);
148
        }
149
        LOGGER.info("BILLED   txn {} PO {} {} -> {} : invoice {}, {} line(s), {} unit(s)", transactionId, plan.poNumber,
150
                plan.sourceWarehouseId, plan.destinationWarehouseId, invoiceNumber, plan.lines.size(),
151
                plan.lines.stream().mapToInt(x -> x.qty).sum());
152
        return invoiceNumber;
153
    }
154
 
155
    private Plan plan(int transactionId) throws ProfitMandiBusinessException {
156
        Plan plan = new Plan();
157
        plan.transactionId = transactionId;
158
 
159
        WarehousePurchaseOrder po = warehousePurchaseOrderRepository.selectByTransactionId(transactionId);
160
        if (po == null) {
161
            plan.problems.add("no purchase order is raised on transaction " + transactionId);
162
            return plan;
163
        }
164
        plan.poNumber = po.getPoNumber();
165
        plan.destinationWarehouseId = po.getWarehouseId();
166
        Supplier supplier = supplierRepository.selectById(po.getSupplierId());
167
        if (supplier == null || !supplier.isInternal() || supplier.getInternalWarehouseId() == null
168
                || supplier.getInternalWarehouseId() <= 0) {
169
            plan.problems.add("PO " + po.getPoNumber() + " is not an internal movement");
170
            return plan;
171
        }
172
        plan.sourceWarehouseId = supplier.getInternalWarehouseId();
173
 
174
        List<Order> orders = orderRepository.selectAllByTransactionIdAndWarehouseId(transactionId, plan.sourceWarehouseId)
175
                .stream().filter(x -> OrderStatus.SUBMITTED_FOR_PROCESSING.equals(x.getStatus()))
176
                .sorted(Comparator.comparing(Order::getId)).collect(Collectors.toList());
177
        if (orders.isEmpty()) {
178
            plan.problems.add("nothing left to bill on transaction " + transactionId + " (no order awaiting billing)");
179
            return plan;
180
        }
181
 
182
        List<Integer> itemIds = orders.stream().map(x -> x.getLineItem().getItemId()).distinct().collect(Collectors.toList());
183
        Map<Integer, InventoryWarehouse> warehouses = new HashMap<>();
184
        // free = what a vendor warehouse can still give to an order parked elsewhere
185
        Map<String, Integer> free = new HashMap<>();
186
        Map<String, Integer> available = new HashMap<>();
187
        Map<Integer, List<Integer>> realWarehousesByItem = new HashMap<>();
188
        for (SaholicInventorySnapshot snapshot : saholicInventoryService.getByWarehouseIdItemIds(plan.sourceWarehouseId, itemIds)) {
189
            if (!this.isRealVendorWarehouse(snapshot.getWarehouseId(), plan.sourceWarehouseId, warehouses)) {
190
                continue;
191
            }
192
            String key = key(snapshot.getWarehouseId(), snapshot.getItemId());
193
            available.put(key, snapshot.getAvailability());
194
            free.put(key, Math.max(snapshot.getAvailability() - snapshot.getReserved(), 0));
195
            realWarehousesByItem.computeIfAbsent(snapshot.getItemId(), x -> new ArrayList<>()).add(snapshot.getWarehouseId());
196
        }
197
 
198
        Map<String, List<String>> serialPools = new HashMap<>();
199
        Map<String, Integer> lotPools = new HashMap<>();
200
        for (Order order : orders) {
201
            Item item = order.getLineItem().getItem();
202
            int itemId = item.getId();
203
            int qty = order.getLineItem().getQuantity();
204
            boolean serialized = ItemType.SERIALIZED.equals(item.getType());
205
            String description = item.getItemDescription() + " (" + itemId + ")";
206
 
207
            // where the units come from: the order's own vendor warehouse, or - when parked - the ones with free stock
208
            Map<Integer, Integer> chunks = new LinkedHashMap<>();
209
            boolean repoint = !this.isRealVendorWarehouse(order.getFulfilmentWarehouseId(), plan.sourceWarehouseId, warehouses);
210
            if (!repoint) {
211
                int fulfilment = order.getFulfilmentWarehouseId();
212
                if (available.getOrDefault(key(fulfilment, itemId), 0) < qty) {
213
                    plan.problems.add("order " + order.getId() + " " + description + ": billing snapshot of warehouse "
214
                            + fulfilment + " shows " + available.getOrDefault(key(fulfilment, itemId), 0) + ", needs " + qty);
215
                    continue;
216
                }
217
                chunks.put(fulfilment, qty);
218
            } else {
219
                List<Integer> candidates = new ArrayList<>(realWarehousesByItem.getOrDefault(itemId, Collections.emptyList()));
220
                candidates.sort(Comparator.comparing((Integer w) -> free.getOrDefault(key(w, itemId), 0)).reversed());
221
                int remaining = qty;
222
                for (Integer warehouseId : candidates) {
223
                    int take = Math.min(remaining, free.getOrDefault(key(warehouseId, itemId), 0));
224
                    if (take > 0) {
225
                        chunks.put(warehouseId, take);
226
                        free.put(key(warehouseId, itemId), free.get(key(warehouseId, itemId)) - take);
227
                        remaining -= take;
228
                    }
229
                    if (remaining == 0) {
230
                        break;
231
                    }
232
                }
233
                if (remaining > 0) {
234
                    plan.problems.add("order " + order.getId() + " " + description + ": only " + (qty - remaining)
235
                            + " of " + qty + " free in the source's vendor warehouses");
236
                    continue;
237
                }
238
            }
239
 
240
            for (Map.Entry<Integer, Integer> chunk : chunks.entrySet()) {
241
                Line line = new Line();
242
                line.orderId = order.getId();
243
                line.itemId = itemId;
244
                line.item = description;
245
                line.qty = chunk.getValue();
246
                line.fromWarehouseId = chunk.getKey();
247
                line.repoint = repoint;
248
                line.serialized = serialized;
249
                String poolKey = key(chunk.getKey(), itemId);
250
                if (serialized) {
251
                    List<String> pool = serialPools.computeIfAbsent(poolKey, x -> this.billableImeis(itemId, chunk.getKey()));
252
                    if (pool.size() < line.qty) {
253
                        plan.problems.add("order " + order.getId() + " " + description + ": " + pool.size()
254
                                + " billable IMEI(s) in warehouse " + chunk.getKey() + ", needs " + line.qty);
255
                        continue;
256
                    }
257
                    line.imeis.addAll(pool.subList(0, line.qty));
258
                    pool.subList(0, line.qty).clear();
259
                } else {
260
                    int lots = lotPools.computeIfAbsent(poolKey, x -> this.billableLotQuantity(itemId, chunk.getKey()));
261
                    if (lots < line.qty) {
262
                        plan.problems.add("order " + order.getId() + " " + description + ": " + lots
263
                                + " billable unit(s) in warehouse " + chunk.getKey() + ", needs " + line.qty);
264
                        continue;
265
                    }
266
                    lotPools.put(poolKey, lots - line.qty);
267
                }
268
                plan.lines.add(line);
269
            }
270
        }
271
        if (plan.lines.size() > MAX_INVOICE_LINES) {
272
            plan.problems.add(plan.lines.size() + " invoice lines exceed the " + MAX_INVOICE_LINES
273
                    + " allowed on one IRN; raise the movement as smaller POs");
274
        }
275
        return plan;
276
    }
277
 
278
    // Billing takes an IMEI only at quantity 1 with a last scan that leaves it in stock (WarehouseServiceImpl billing).
279
    private List<String> billableImeis(int itemId, int vendorWarehouseId) {
280
        return warehouseInventoryItemRepository.selectBillableStock(itemId, vendorWarehouseId).stream()
281
                .filter(x -> x.getCurrentQuantity() == 1 && x.getSerialNumber() != null && !x.getSerialNumber().trim().isEmpty())
282
                .filter(x -> WarehouseServiceImpl.GOOD_SCAN_TYPES.contains(x.getLastScanType()))
283
                .sorted(Comparator.comparing(WarehouseInventoryItem::getCreated, Comparator.nullsFirst(Comparator.naturalOrder())))
284
                .map(WarehouseInventoryItem::getSerialNumber)
285
                .collect(Collectors.toCollection(ArrayList::new));
286
    }
287
 
288
    private int billableLotQuantity(int itemId, int vendorWarehouseId) {
289
        return warehouseInventoryItemRepository.selectBillableStock(itemId, vendorWarehouseId).stream()
290
                .filter(x -> x.getCurrentQuantity() > 0)
291
                .mapToInt(WarehouseInventoryItem::getCurrentQuantity).sum();
292
    }
293
 
294
    // A vendor warehouse the source bills from: its own OURS/GOOD stock, not the Dummy phantom target.
295
    private boolean isRealVendorWarehouse(Integer warehouseId, int sourceWarehouseId, Map<Integer, InventoryWarehouse> cache) {
296
        if (warehouseId == null) {
297
            return false;
298
        }
299
        InventoryWarehouse warehouse = cache.computeIfAbsent(warehouseId, inventoryWarehouseRepository::selectById);
300
        return warehouse != null && warehouse.getBillingWarehouseId() == sourceWarehouseId
301
                && warehouse.getVendorId() != ProfitMandiConstants.DEFAULT_FALLBACK_VENDOR_ID
302
                && InventoryType.GOOD.equals(warehouse.getInventoryType())
303
                && WarehouseType.OURS.equals(warehouse.getWarehouseType());
304
    }
305
 
306
    private static String key(int warehouseId, int itemId) {
307
        return warehouseId + ":" + itemId;
308
    }
309
}