Subversion Repositories SmartDukaan

Rev

Rev 37614 | Go to most recent revision | Show entire file | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

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