| 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 |
}
|