| 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;
|
- |
|
| 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;
|
3 |
import org.apache.logging.log4j.LogManager;
|
| 20 |
import org.apache.logging.log4j.Logger;
|
4 |
import org.apache.logging.log4j.Logger;
|
| 21 |
import org.springframework.beans.factory.annotation.Autowired;
|
5 |
import org.springframework.beans.factory.annotation.Autowired;
|
| 22 |
import org.springframework.stereotype.Component;
|
6 |
import org.springframework.stereotype.Component;
|
| 23 |
import org.springframework.transaction.annotation.Propagation;
|
- |
|
| 24 |
import org.springframework.transaction.annotation.Transactional;
|
- |
|
| 25 |
|
7 |
|
| 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;
|
8 |
import java.util.List;
|
| 32 |
import java.util.Map;
|
- |
|
| 33 |
import java.util.Set;
|
- |
|
| 34 |
|
9 |
|
| 35 |
/**
|
10 |
/**
|
| 36 |
* Receives stock into the destination warehouse for internal transfer invoices that were billed but never
|
11 |
* 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
|
12 |
* GRNed. The receiving itself is InternalGrnReceiver, one invoice at a time.
|
| 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 |
*
|
13 |
*
|
| 41 |
* Deliberately narrow: only INTERNAL buyers, only invoices not already received, and a serialized line with
|
14 |
* This driver carries no transaction of its own. It must not, and it must not hold the receiving logic
|
| 42 |
* no serial number aborts its invoice rather than creating stock that cannot be traced to a unit.
|
15 |
* either: a transactional method called from within its own bean is a self invocation, which bypasses the
|
| - |
|
16 |
* Spring proxy and runs with no transaction at all.
|
| 43 |
*/
|
17 |
*/
|
| 44 |
@Component
|
18 |
@Component
|
| 45 |
public class InternalGrnTask {
|
19 |
public class InternalGrnTask {
|
| 46 |
|
20 |
|
| 47 |
private static final Logger LOGGER = LogManager.getLogger(InternalGrnTask.class);
|
21 |
private static final Logger LOGGER = LogManager.getLogger(InternalGrnTask.class);
|
| 48 |
|
22 |
|
| 49 |
@Autowired
|
23 |
@Autowired
|
| 50 |
private OrderRepository orderRepository;
|
24 |
private InternalGrnReceiver internalGrnReceiver;
|
| 51 |
|
25 |
|
| 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) {
|
26 |
public void grnInternalInvoices(List<String> invoiceNumbers, String operatorEmail, boolean dryRun) {
|
| 73 |
LOGGER.info("=== Internal GRN {} : {} invoice(s), operator {} ===",
|
27 |
LOGGER.info("=== Internal GRN {} : {} invoice(s), operator {} ===",
|
| 74 |
dryRun ? "DRY RUN (no writes)" : "LIVE RUN", invoiceNumbers.size(), operatorEmail);
|
28 |
dryRun ? "DRY RUN (no writes)" : "LIVE RUN", invoiceNumbers.size(), operatorEmail);
|
| 75 |
int received = 0;
|
29 |
int received = 0;
|
| 76 |
int skipped = 0;
|
30 |
int skipped = 0;
|
| 77 |
int failed = 0;
|
31 |
int failed = 0;
|
| 78 |
for (String invoiceNumber : invoiceNumbers) {
|
32 |
for (String invoiceNumber : invoiceNumbers) {
|
| 79 |
try {
|
33 |
try {
|
| 80 |
if (grnInvoice(invoiceNumber.trim(), operatorEmail, dryRun)) {
|
34 |
if (internalGrnReceiver.grnInvoice(invoiceNumber.trim(), operatorEmail, dryRun)) {
|
| 81 |
received++;
|
35 |
received++;
|
| 82 |
} else {
|
36 |
} else {
|
| 83 |
skipped++;
|
37 |
skipped++;
|
| 84 |
}
|
38 |
}
|
| 85 |
} catch (Exception e) {
|
39 |
} catch (Exception e) {
|
| Line 89... |
Line 43... |
| 89 |
}
|
43 |
}
|
| 90 |
LOGGER.info("=== Internal GRN {} complete : {} {}, {} skipped, {} failed ===",
|
44 |
LOGGER.info("=== Internal GRN {} complete : {} {}, {} skipped, {} failed ===",
|
| 91 |
dryRun ? "DRY RUN" : "LIVE RUN", received, dryRun ? "would be received" : "received",
|
45 |
dryRun ? "DRY RUN" : "LIVE RUN", received, dryRun ? "would be received" : "received",
|
| 92 |
skipped, failed);
|
46 |
skipped, failed);
|
| 93 |
}
|
47 |
}
|
| 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 |
}
|
48 |
}
|