| 37613 |
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;
|
|
|
6 |
import com.spice.profitmandi.common.model.PORowModel;
|
|
|
7 |
import com.spice.profitmandi.dao.entity.catalog.Item;
|
|
|
8 |
import com.spice.profitmandi.dao.entity.fofo.FofoStore;
|
|
|
9 |
import com.spice.profitmandi.dao.entity.transaction.LineItem;
|
|
|
10 |
import com.spice.profitmandi.dao.entity.transaction.Order;
|
|
|
11 |
import com.spice.profitmandi.dao.entity.warehouse.WarehousePurchaseOrder;
|
|
|
12 |
import com.spice.profitmandi.dao.entity.warehouse.WarehouseSupplierInvoice;
|
|
|
13 |
import com.spice.profitmandi.dao.repository.catalog.ItemRepository;
|
|
|
14 |
import com.spice.profitmandi.dao.repository.dtr.FofoStoreRepository;
|
|
|
15 |
import com.spice.profitmandi.dao.repository.transaction.OrderRepository;
|
|
|
16 |
import com.spice.profitmandi.dao.repository.warehouse.WarehousePurchaseOrderRepository;
|
|
|
17 |
import com.spice.profitmandi.dao.repository.warehouse.WarehouseSupplierInvoiceRepository;
|
|
|
18 |
import com.spice.profitmandi.service.warehouse.PurchaseOrderService;
|
|
|
19 |
import org.apache.logging.log4j.LogManager;
|
|
|
20 |
import org.apache.logging.log4j.Logger;
|
|
|
21 |
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
22 |
import org.springframework.stereotype.Component;
|
|
|
23 |
import org.springframework.transaction.annotation.Propagation;
|
|
|
24 |
import org.springframework.transaction.annotation.Transactional;
|
|
|
25 |
|
|
|
26 |
import java.math.BigDecimal;
|
|
|
27 |
import java.time.LocalDateTime;
|
|
|
28 |
import java.util.ArrayList;
|
|
|
29 |
import java.util.Collections;
|
|
|
30 |
import java.util.HashSet;
|
|
|
31 |
import java.util.List;
|
|
|
32 |
import java.util.Map;
|
|
|
33 |
import java.util.Set;
|
|
|
34 |
|
|
|
35 |
/**
|
|
|
36 |
* Receives stock into the destination warehouse for internal transfer invoices that were billed but never
|
|
|
37 |
* GRNed. Per invoice it builds the rows the Excel GRN upload would have carried and hands them to
|
|
|
38 |
* PurchaseOrderService.addPORowModels, which creates the supplier invoice, the purchase and the inventory
|
|
|
39 |
* items in one call - the same path the portal uses, so nothing here reimplements receiving.
|
|
|
40 |
*
|
|
|
41 |
* Deliberately narrow: only INTERNAL buyers, only invoices not already received, and a serialized line with
|
|
|
42 |
* no serial number aborts its invoice rather than creating stock that cannot be traced to a unit.
|
|
|
43 |
*/
|
|
|
44 |
@Component
|
|
|
45 |
public class InternalGrnTask {
|
|
|
46 |
|
|
|
47 |
private static final Logger LOGGER = LogManager.getLogger(InternalGrnTask.class);
|
|
|
48 |
|
|
|
49 |
@Autowired
|
|
|
50 |
private OrderRepository orderRepository;
|
|
|
51 |
|
|
|
52 |
@Autowired
|
|
|
53 |
private ItemRepository itemRepository;
|
|
|
54 |
|
|
|
55 |
@Autowired
|
|
|
56 |
private FofoStoreRepository fofoStoreRepository;
|
|
|
57 |
|
|
|
58 |
@Autowired
|
|
|
59 |
private WarehousePurchaseOrderRepository warehousePurchaseOrderRepository;
|
|
|
60 |
|
|
|
61 |
@Autowired
|
|
|
62 |
private WarehouseSupplierInvoiceRepository warehouseSupplierInvoiceRepository;
|
|
|
63 |
|
|
|
64 |
@Autowired
|
|
|
65 |
private PurchaseOrderService purchaseOrderService;
|
|
|
66 |
|
|
|
67 |
/**
|
|
|
68 |
* Drives the run without holding a transaction of its own, so one invoice failing cannot roll back the
|
|
|
69 |
* invoices already received - each invoice commits or rolls back on its own inside grnInvoice.
|
|
|
70 |
*/
|
|
|
71 |
@Transactional(propagation = Propagation.NOT_SUPPORTED)
|
|
|
72 |
public void grnInternalInvoices(List<String> invoiceNumbers, String operatorEmail, boolean dryRun) {
|
|
|
73 |
LOGGER.info("=== Internal GRN {} : {} invoice(s), operator {} ===",
|
|
|
74 |
dryRun ? "DRY RUN (no writes)" : "LIVE RUN", invoiceNumbers.size(), operatorEmail);
|
|
|
75 |
int received = 0;
|
|
|
76 |
int skipped = 0;
|
|
|
77 |
int failed = 0;
|
|
|
78 |
for (String invoiceNumber : invoiceNumbers) {
|
|
|
79 |
try {
|
|
|
80 |
if (grnInvoice(invoiceNumber.trim(), operatorEmail, dryRun)) {
|
|
|
81 |
received++;
|
|
|
82 |
} else {
|
|
|
83 |
skipped++;
|
|
|
84 |
}
|
|
|
85 |
} catch (Exception e) {
|
|
|
86 |
failed++;
|
|
|
87 |
LOGGER.error("FAILED {} - {}", invoiceNumber, e.getMessage(), e);
|
|
|
88 |
}
|
|
|
89 |
}
|
|
|
90 |
LOGGER.info("=== Internal GRN {} complete : {} {}, {} skipped, {} failed ===",
|
|
|
91 |
dryRun ? "DRY RUN" : "LIVE RUN", received, dryRun ? "would be received" : "received",
|
|
|
92 |
skipped, failed);
|
|
|
93 |
}
|
|
|
94 |
|
|
|
95 |
/**
|
|
|
96 |
* @return true when the invoice was received (or would be, on a dry run), false when it was skipped.
|
|
|
97 |
*/
|
|
|
98 |
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Throwable.class)
|
|
|
99 |
public boolean grnInvoice(String invoiceNumber, String operatorEmail, boolean dryRun) throws ProfitMandiBusinessException {
|
|
|
100 |
List<Order> orders = orderRepository.selectByInvoiceNumber(invoiceNumber);
|
|
|
101 |
if (orders.isEmpty()) {
|
|
|
102 |
LOGGER.info("SKIP {} - no orders for this invoice", invoiceNumber);
|
|
|
103 |
return false;
|
|
|
104 |
}
|
|
|
105 |
|
|
|
106 |
// Only our own stock moving between our own warehouses - never auto-receive a partner's goods.
|
|
|
107 |
FofoStore buyer = fofoStoreRepository.selectByRetailerId(orders.get(0).getRetailerId());
|
|
|
108 |
if (buyer == null || !FofoType.INTERNAL.equals(buyer.getFofoType())) {
|
|
|
109 |
LOGGER.info("SKIP {} - buyer {} is not an INTERNAL store", invoiceNumber, orders.get(0).getRetailerId());
|
|
|
110 |
return false;
|
|
|
111 |
}
|
|
|
112 |
|
|
|
113 |
// An internal PO reaches its orders through the transaction it raised, not through order.purchase_order_id.
|
|
|
114 |
Set<Integer> transactionIds = new HashSet<>();
|
|
|
115 |
for (Order order : orders) {
|
|
|
116 |
transactionIds.add(order.getTransactionId());
|
|
|
117 |
}
|
|
|
118 |
Set<Integer> poIds = new HashSet<>();
|
|
|
119 |
WarehousePurchaseOrder purchaseOrder = null;
|
|
|
120 |
for (Integer transactionId : transactionIds) {
|
|
|
121 |
WarehousePurchaseOrder po = warehousePurchaseOrderRepository.selectByTransactionId(transactionId);
|
|
|
122 |
if (po != null && poIds.add(po.getId())) {
|
|
|
123 |
purchaseOrder = po;
|
|
|
124 |
}
|
|
|
125 |
}
|
|
|
126 |
if (purchaseOrder == null) {
|
|
|
127 |
LOGGER.info("SKIP {} - no purchase order found for transaction(s) {}", invoiceNumber, transactionIds);
|
|
|
128 |
return false;
|
|
|
129 |
}
|
|
|
130 |
// addPORowModels resolves a single PO for the whole map, so an invoice spanning POs cannot be trusted to it.
|
|
|
131 |
if (poIds.size() > 1) {
|
|
|
132 |
LOGGER.info("SKIP {} - spans {} purchase orders {}", invoiceNumber, poIds.size(), poIds);
|
|
|
133 |
return false;
|
|
|
134 |
}
|
|
|
135 |
|
|
|
136 |
WarehouseSupplierInvoice existing = warehouseSupplierInvoiceRepository
|
|
|
137 |
.selectAllBySupplierInvoice(purchaseOrder.getSupplierId(), invoiceNumber);
|
|
|
138 |
if (existing != null) {
|
|
|
139 |
LOGGER.info("SKIP {} - already received (invoice id {}, status {})",
|
|
|
140 |
invoiceNumber, existing.getId(), existing.getStatus());
|
|
|
141 |
return false;
|
|
|
142 |
}
|
|
|
143 |
|
|
|
144 |
LocalDateTime receivedDate = LocalDateTime.now();
|
|
|
145 |
List<PORowModel> rows = new ArrayList<>();
|
|
|
146 |
BigDecimal value = BigDecimal.ZERO;
|
|
|
147 |
int serials = 0;
|
|
|
148 |
for (Order order : orders) {
|
|
|
149 |
LineItem lineItem = order.getLineItem();
|
|
|
150 |
if (lineItem == null) {
|
|
|
151 |
LOGGER.info("SKIP {} - order {} has no line item", invoiceNumber, order.getId());
|
|
|
152 |
return false;
|
|
|
153 |
}
|
|
|
154 |
Item item = itemRepository.selectById(lineItem.getItemId());
|
|
|
155 |
if (item == null) {
|
|
|
156 |
LOGGER.info("SKIP {} - item {} not found", invoiceNumber, lineItem.getItemId());
|
|
|
157 |
return false;
|
|
|
158 |
}
|
|
|
159 |
|
|
|
160 |
String serialNumber = lineItem.getSerialNumber();
|
|
|
161 |
boolean serialized = ItemType.SERIALIZED.equals(item.getType());
|
|
|
162 |
// Receiving a serialized unit without its serial would create stock no scan could ever match.
|
|
|
163 |
if (serialized && (serialNumber == null || serialNumber.trim().isEmpty())) {
|
|
|
164 |
LOGGER.info("SKIP {} - serialized item {} on order {} has no serial number",
|
|
|
165 |
invoiceNumber, item.getId(), order.getId());
|
|
|
166 |
return false;
|
|
|
167 |
}
|
|
|
168 |
if (serialized) {
|
|
|
169 |
serials++;
|
|
|
170 |
}
|
|
|
171 |
|
|
|
172 |
PORowModel row = new PORowModel();
|
|
|
173 |
row.setPoNumber(purchaseOrder.getPoNumber());
|
|
|
174 |
row.setInvoiceNumber(invoiceNumber);
|
|
|
175 |
row.setInvoiceDate(order.getBillingTimestamp());
|
|
|
176 |
row.setReceivedDate(receivedDate);
|
|
|
177 |
row.setItemId(lineItem.getItemId());
|
|
|
178 |
row.setQuantity(lineItem.getQuantity());
|
|
|
179 |
row.setUnitPrice(lineItem.getUnitPrice());
|
|
|
180 |
row.setImeiNumber(serialized ? serialNumber.trim() : null);
|
|
|
181 |
rows.add(row);
|
|
|
182 |
|
|
|
183 |
value = value.add(BigDecimal.valueOf(lineItem.getUnitPrice())
|
|
|
184 |
.multiply(BigDecimal.valueOf(lineItem.getQuantity())));
|
|
|
185 |
}
|
|
|
186 |
|
|
|
187 |
if (dryRun) {
|
|
|
188 |
LOGGER.info("WOULD GRN {} - po {} (id {}), warehouse {}, supplier {}, {} line(s), {} serialized, value {}, receivedDate {}",
|
|
|
189 |
invoiceNumber, purchaseOrder.getPoNumber(), purchaseOrder.getId(), purchaseOrder.getWarehouseId(),
|
|
|
190 |
purchaseOrder.getSupplierId(), rows.size(), serials, value.toPlainString(), receivedDate);
|
|
|
191 |
for (PORowModel row : rows) {
|
|
|
192 |
LOGGER.info(" item {} qty {} price {} serial {}",
|
|
|
193 |
row.getItemId(), row.getQuantity(), row.getUnitPrice(),
|
|
|
194 |
row.getImeiNumber() == null ? "-" : row.getImeiNumber());
|
|
|
195 |
}
|
|
|
196 |
return true;
|
|
|
197 |
}
|
|
|
198 |
|
|
|
199 |
// One PO and one invoice per call - addPORowModels applies the first PO it resolves to every entry.
|
|
|
200 |
Map<String, Map<String, List<PORowModel>>> poRowModelsMap = Collections.singletonMap(
|
|
|
201 |
purchaseOrder.getPoNumber(), Collections.singletonMap(invoiceNumber, rows));
|
|
|
202 |
purchaseOrderService.addPORowModels(operatorEmail, poRowModelsMap);
|
|
|
203 |
|
|
|
204 |
LOGGER.info("GRNED {} - po {}, warehouse {}, {} line(s), {} serialized, value {}",
|
|
|
205 |
invoiceNumber, purchaseOrder.getPoNumber(), purchaseOrder.getWarehouseId(),
|
|
|
206 |
rows.size(), serials, value.toPlainString());
|
|
|
207 |
return true;
|
|
|
208 |
}
|
|
|
209 |
}
|