Subversion Repositories SmartDukaan

Rev

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

Rev Author Line No. Line
37614 amit 1
package com.smartdukaan.cron.migrations;
2
 
3
import com.spice.profitmandi.common.enumuration.FofoType;
4
import com.spice.profitmandi.common.enumuration.ItemType;
5
import com.spice.profitmandi.common.exception.ProfitMandiBusinessException;
37615 amit 6
import com.spice.profitmandi.common.model.GrnModel;
7
import com.spice.profitmandi.common.model.InvoiceItemModel;
8
import com.spice.profitmandi.common.model.WarehouseInvoiceModel;
9
import com.spice.profitmandi.dao.entity.auth.AuthUser;
37614 amit 10
import com.spice.profitmandi.dao.entity.catalog.Item;
11
import com.spice.profitmandi.dao.entity.fofo.FofoStore;
12
import com.spice.profitmandi.dao.entity.transaction.LineItem;
13
import com.spice.profitmandi.dao.entity.transaction.Order;
14
import com.spice.profitmandi.dao.entity.warehouse.WarehousePurchaseOrder;
15
import com.spice.profitmandi.dao.entity.warehouse.WarehouseSupplierInvoice;
37615 amit 16
import com.spice.profitmandi.dao.enumuration.warehouse.WarehouseInvoiceStatus;
17
import com.spice.profitmandi.dao.repository.auth.AuthRepository;
37614 amit 18
import com.spice.profitmandi.dao.repository.catalog.ItemRepository;
19
import com.spice.profitmandi.dao.repository.dtr.FofoStoreRepository;
20
import com.spice.profitmandi.dao.repository.transaction.OrderRepository;
21
import com.spice.profitmandi.dao.repository.warehouse.WarehousePurchaseOrderRepository;
22
import com.spice.profitmandi.dao.repository.warehouse.WarehouseSupplierInvoiceRepository;
37615 amit 23
import com.spice.profitmandi.service.warehouse.InvoiceService;
37614 amit 24
import com.spice.profitmandi.service.warehouse.PurchaseOrderService;
25
import org.apache.logging.log4j.LogManager;
26
import org.apache.logging.log4j.Logger;
27
import org.springframework.beans.factory.annotation.Autowired;
28
import org.springframework.stereotype.Component;
29
import org.springframework.transaction.annotation.Propagation;
30
import org.springframework.transaction.annotation.Transactional;
31
 
32
import java.math.BigDecimal;
33
import java.time.LocalDateTime;
34
import java.util.ArrayList;
35
import java.util.Collections;
36
import java.util.HashSet;
37615 amit 37
import java.util.LinkedHashMap;
37614 amit 38
import java.util.List;
39
import java.util.Map;
40
import java.util.Set;
41
 
42
/**
37615 amit 43
 * Receives one internal transfer invoice into the destination warehouse, following the same sequence the
44
 * portal performs: record the supplier invoice, record its items, then GRN it through
45
 * PurchaseOrderService.grnPoModels - the call behind the portal's Create GRN button.
37614 amit 46
 *
37615 amit 47
 * The Excel upload path (addPORowModels) is deliberately not used. It persists a supplier invoice without a
48
 * status, which the column does not allow, so that route cannot complete a receipt at all.
49
 *
37614 amit 50
 * Kept in its own bean on purpose: InternalGrnTask calls this through the Spring proxy, so the transaction
51
 * below is actually applied. Calling it from inside the driver would be a self invocation, the proxy would
52
 * be bypassed and the work would run with no transaction at all.
53
 */
54
@Component
55
public class InternalGrnReceiver {
56
 
57
    private static final Logger LOGGER = LogManager.getLogger(InternalGrnReceiver.class);
58
 
37615 amit 59
    /** The portal stores the scanned supplier invoice against the receipt; an internal transfer has none. */
60
    private static final int NO_INVOICE_DOCUMENT = 0;
61
 
37614 amit 62
    @Autowired
63
    private OrderRepository orderRepository;
64
 
65
    @Autowired
66
    private ItemRepository itemRepository;
67
 
68
    @Autowired
69
    private FofoStoreRepository fofoStoreRepository;
70
 
71
    @Autowired
37615 amit 72
    private AuthRepository authRepository;
73
 
74
    @Autowired
37614 amit 75
    private WarehousePurchaseOrderRepository warehousePurchaseOrderRepository;
76
 
77
    @Autowired
78
    private WarehouseSupplierInvoiceRepository warehouseSupplierInvoiceRepository;
79
 
80
    @Autowired
37615 amit 81
    private InvoiceService invoiceService;
82
 
83
    @Autowired
37614 amit 84
    private PurchaseOrderService purchaseOrderService;
85
 
37615 amit 86
    /** One item of an invoice, gathered across the orders that share it. */
87
    private static class ReceiptLine {
88
        private int itemId;
89
        private boolean serialized;
90
        private float rate;
91
        private int qty;
92
        private final List<String> serialNumbers = new ArrayList<>();
93
    }
94
 
37614 amit 95
    /**
96
     * Each invoice commits or rolls back on its own so one bad invoice cannot undo those already received.
97
     *
98
     * @return true when the invoice was received (or would be, on a dry run), false when it was skipped.
99
     */
100
    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Throwable.class)
101
    public boolean grnInvoice(String invoiceNumber, String operatorEmail, boolean dryRun) throws ProfitMandiBusinessException {
102
        List<Order> orders = orderRepository.selectByInvoiceNumber(invoiceNumber);
103
        if (orders.isEmpty()) {
104
            LOGGER.info("SKIP    {} - no orders for this invoice", invoiceNumber);
105
            return false;
106
        }
107
 
108
        // Only our own stock moving between our own warehouses - never auto-receive a partner's goods.
109
        FofoStore buyer = fofoStoreRepository.selectByRetailerId(orders.get(0).getRetailerId());
110
        if (buyer == null || !FofoType.INTERNAL.equals(buyer.getFofoType())) {
111
            LOGGER.info("SKIP    {} - buyer {} is not an INTERNAL store", invoiceNumber, orders.get(0).getRetailerId());
112
            return false;
113
        }
114
 
37615 amit 115
        WarehousePurchaseOrder purchaseOrder = resolvePurchaseOrder(invoiceNumber, orders);
37614 amit 116
        if (purchaseOrder == null) {
117
            return false;
118
        }
119
 
120
        WarehouseSupplierInvoice existing = warehouseSupplierInvoiceRepository
121
                .selectAllBySupplierInvoice(purchaseOrder.getSupplierId(), invoiceNumber);
122
        if (existing != null) {
123
            LOGGER.info("SKIP    {} - already received (invoice id {}, status {})",
124
                    invoiceNumber, existing.getId(), existing.getStatus());
125
            return false;
126
        }
127
 
37615 amit 128
        // grnPoModels takes one entry per item, carrying every serial of that item, so gather the orders by item.
129
        Map<Integer, ReceiptLine> linesByItem = new LinkedHashMap<>();
37614 amit 130
        for (Order order : orders) {
131
            LineItem lineItem = order.getLineItem();
132
            if (lineItem == null) {
133
                LOGGER.info("SKIP    {} - order {} has no line item", invoiceNumber, order.getId());
134
                return false;
135
            }
136
            Item item = itemRepository.selectById(lineItem.getItemId());
137
            if (item == null) {
138
                LOGGER.info("SKIP    {} - item {} not found", invoiceNumber, lineItem.getItemId());
139
                return false;
140
            }
141
 
142
            boolean serialized = ItemType.SERIALIZED.equals(item.getType());
37616 amit 143
            int quantity = lineItem.getQuantity();
144
 
145
            // A line item carries one serial per unit, comma separated - never a single serial for the line.
146
            // Handing the whole field over as one serial stores a string no scan can match and records the
147
            // line as one unit, so the serials are split out and counted against the quantity here.
148
            List<String> serialNumbers = new ArrayList<>();
149
            if (serialized) {
150
                String serialField = lineItem.getSerialNumber();
151
                if (serialField != null) {
152
                    for (String serialNumber : serialField.split(",")) {
153
                        if (!serialNumber.trim().isEmpty()) {
154
                            serialNumbers.add(serialNumber.trim());
155
                        }
156
                    }
157
                }
158
                if (serialNumbers.size() != quantity) {
159
                    LOGGER.info("SKIP    {} - serialized item {} on order {} has {} serial(s) for {} unit(s)",
160
                            invoiceNumber, item.getId(), order.getId(), serialNumbers.size(), quantity);
161
                    return false;
162
                }
37614 amit 163
            }
37615 amit 164
 
165
            ReceiptLine line = linesByItem.get(lineItem.getItemId());
166
            if (line == null) {
167
                line = new ReceiptLine();
168
                line.itemId = lineItem.getItemId();
169
                line.serialized = serialized;
170
                line.rate = lineItem.getUnitPrice();
171
                linesByItem.put(line.itemId, line);
172
            }
37616 amit 173
            line.qty += quantity;
174
            line.serialNumbers.addAll(serialNumbers);
37615 amit 175
        }
37614 amit 176
 
37615 amit 177
        int numItems = 0;
178
        int serialisedUnits = 0;
179
        BigDecimal value = BigDecimal.ZERO;
180
        for (ReceiptLine line : linesByItem.values()) {
181
            numItems += line.qty;
182
            serialisedUnits += line.serialNumbers.size();
183
            value = value.add(BigDecimal.valueOf(line.rate).multiply(BigDecimal.valueOf(line.qty)));
37614 amit 184
        }
185
 
186
        if (dryRun) {
37615 amit 187
            LOGGER.info("WOULD GRN {} - po {} (id {}), warehouse {}, supplier {}, {} item(s), {} unit(s), {} serialized, value {}",
37614 amit 188
                    invoiceNumber, purchaseOrder.getPoNumber(), purchaseOrder.getId(), purchaseOrder.getWarehouseId(),
37615 amit 189
                    purchaseOrder.getSupplierId(), linesByItem.size(), numItems, serialisedUnits, value.toPlainString());
190
            for (ReceiptLine line : linesByItem.values()) {
191
                LOGGER.info("          item {} qty {} rate {} serials {}",
192
                        line.itemId, line.qty, line.rate,
193
                        line.serialNumbers.isEmpty() ? "-" : line.serialNumbers);
37614 amit 194
            }
195
            return true;
196
        }
197
 
37615 amit 198
        // 1. Record the supplier invoice, as the portal's Receive Invoice screen does.
199
        AuthUser operator = authRepository.selectByEmailOrMobile(operatorEmail);
200
        WarehouseSupplierInvoice invoice = new WarehouseSupplierInvoice();
201
        invoice.setInvoiceNumber(invoiceNumber);
202
        invoice.setInvoiceDate(orders.get(0).getBillingTimestamp());
203
        invoice.setSupplierId(purchaseOrder.getSupplierId());
204
        invoice.setWarehouseId(purchaseOrder.getWarehouseId());
205
        invoice.setCreateDate(LocalDateTime.now());
206
        invoice.setReceivedFrom(operator.getFullName());
207
        invoice.setNumItems(numItems);
208
        invoice.setTotalValue(value.floatValue());
209
        invoice.setStatus(WarehouseInvoiceStatus.init);
210
        invoice.setInvoiceDocId(NO_INVOICE_DOCUMENT);
211
        warehouseSupplierInvoiceRepository.persist(invoice);
37614 amit 212
 
37615 amit 213
        // 2. Record its items, as the Add Item screen does - the buying reports read these rows.
214
        WarehouseInvoiceModel invoiceModel = new WarehouseInvoiceModel();
215
        invoiceModel.setInvoiceId(invoice.getId());
216
        invoiceModel.setSupplierId(purchaseOrder.getSupplierId());
217
        invoiceModel.setWarehouseId(purchaseOrder.getWarehouseId());
218
        List<InvoiceItemModel> invoiceItems = new ArrayList<>();
219
        for (ReceiptLine line : linesByItem.values()) {
220
            InvoiceItemModel invoiceItem = new InvoiceItemModel();
221
            invoiceItem.setItemId(line.itemId);
222
            invoiceItem.setQty(line.qty);
223
            invoiceItem.setRate(line.rate);
224
            invoiceItems.add(invoiceItem);
225
        }
226
        invoiceModel.setInvoiceItems(invoiceItems);
227
        invoiceService.createInvoiceItem(invoiceModel);
228
 
229
        // 3. GRN it, exactly as the portal's Create GRN button does.
230
        List<GrnModel> grnModels = new ArrayList<>();
231
        for (ReceiptLine line : linesByItem.values()) {
232
            GrnModel grnModel = new GrnModel();
233
            grnModel.setPoId(purchaseOrder.getId());
234
            grnModel.setInvoiceId(invoice.getId());
235
            grnModel.setItemId(line.itemId);
236
            grnModel.setQty(line.qty);
237
            grnModel.setInvoicePrice(line.rate);
238
            grnModel.setSerialNumbers(line.serialized ? line.serialNumbers : new ArrayList<String>());
239
            grnModels.add(grnModel);
240
        }
241
        Map<Integer, Map<Integer, List<GrnModel>>> grnModelsMap = Collections.singletonMap(
242
                purchaseOrder.getId(), Collections.singletonMap(invoice.getId(), grnModels));
243
        purchaseOrderService.grnPoModels(operatorEmail, grnModelsMap);
244
 
245
        LOGGER.info("GRNED   {} - po {}, warehouse {}, invoice id {}, {} item(s), {} unit(s), {} serialized, value {}",
246
                invoiceNumber, purchaseOrder.getPoNumber(), purchaseOrder.getWarehouseId(), invoice.getId(),
247
                linesByItem.size(), numItems, serialisedUnits, value.toPlainString());
37614 amit 248
        return true;
249
    }
37615 amit 250
 
251
    /**
252
     * Creating an internal PO raises one transaction and records it on the order
253
     * (PurchaseOrderServiceImpl: warehousePurchaseOrder.setTransactionId), so that is the mapping back from
254
     * an invoice to its PO. order.purchase_order_id is not it - nothing populates it on this route.
255
     *
256
     * The invoice's orders all belong to that one transaction; if they ever did not, the PO is ambiguous and
257
     * the invoice is left alone rather than guessed at.
258
     */
259
    private WarehousePurchaseOrder resolvePurchaseOrder(String invoiceNumber, List<Order> orders) {
260
        Set<Integer> transactionIds = new HashSet<>();
261
        for (Order order : orders) {
262
            transactionIds.add(order.getTransactionId());
263
        }
264
        if (transactionIds.size() > 1) {
265
            LOGGER.info("SKIP    {} - spans {} transactions {}", invoiceNumber, transactionIds.size(), transactionIds);
266
            return null;
267
        }
268
        int transactionId = transactionIds.iterator().next();
269
        WarehousePurchaseOrder purchaseOrder = warehousePurchaseOrderRepository.selectByTransactionId(transactionId);
270
        if (purchaseOrder == null) {
271
            LOGGER.info("SKIP    {} - no purchase order mapped to transaction {}", invoiceNumber, transactionId);
272
            return null;
273
        }
274
        return purchaseOrder;
275
    }
37614 amit 276
}