| 37827 |
amit |
1 |
package com.smartdukaan.cron.migrations;
|
|
|
2 |
|
|
|
3 |
import com.spice.profitmandi.common.exception.ProfitMandiBusinessException;
|
|
|
4 |
import com.spice.profitmandi.dao.entity.auth.AuthUser;
|
|
|
5 |
import com.spice.profitmandi.dao.entity.catalog.Item;
|
| 37832 |
amit |
6 |
import com.spice.profitmandi.dao.entity.transaction.Order;
|
| 37827 |
amit |
7 |
import com.spice.profitmandi.dao.entity.warehouse.Supplier;
|
|
|
8 |
import com.spice.profitmandi.dao.entity.warehouse.WarehousePurchaseOrder;
|
|
|
9 |
import com.spice.profitmandi.dao.model.CreateWarehousePurchaseOrderModel;
|
|
|
10 |
import com.spice.profitmandi.dao.model.InternalMovementAllocationModel;
|
|
|
11 |
import com.spice.profitmandi.dao.model.InternalMovementAvailabilityModel;
|
|
|
12 |
import com.spice.profitmandi.dao.model.ItemWisePurchaseModel;
|
|
|
13 |
import com.spice.profitmandi.dao.repository.auth.AuthRepository;
|
|
|
14 |
import com.spice.profitmandi.dao.repository.catalog.ItemRepository;
|
| 37832 |
amit |
15 |
import com.spice.profitmandi.dao.repository.transaction.OrderRepository;
|
| 37827 |
amit |
16 |
import com.spice.profitmandi.dao.repository.warehouse.SupplierRepository;
|
|
|
17 |
import com.spice.profitmandi.dao.repository.warehouse.WarehousePurchaseOrderRepository;
|
|
|
18 |
import com.spice.profitmandi.service.warehouse.InternalMovementPricingService;
|
|
|
19 |
import com.spice.profitmandi.service.warehouse.PurchaseOrderService;
|
| 37832 |
amit |
20 |
import in.shop2020.model.v1.order.OrderStatus;
|
|
|
21 |
import in.shop2020.purchase.POStatus;
|
| 37827 |
amit |
22 |
import org.apache.poi.ss.usermodel.CellType;
|
|
|
23 |
import org.apache.poi.ss.usermodel.Row;
|
|
|
24 |
import org.apache.poi.xssf.usermodel.XSSFRow;
|
|
|
25 |
import org.apache.poi.xssf.usermodel.XSSFSheet;
|
|
|
26 |
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
|
|
27 |
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
28 |
import org.springframework.stereotype.Component;
|
|
|
29 |
import org.springframework.transaction.annotation.Transactional;
|
|
|
30 |
|
|
|
31 |
import java.io.FileInputStream;
|
|
|
32 |
import java.io.InputStream;
|
|
|
33 |
import java.time.LocalDate;
|
| 37832 |
amit |
34 |
import java.time.LocalDateTime;
|
| 37827 |
amit |
35 |
import java.util.ArrayList;
|
| 37832 |
amit |
36 |
import java.util.Collections;
|
| 37827 |
amit |
37 |
import java.util.Comparator;
|
|
|
38 |
import java.util.LinkedHashMap;
|
|
|
39 |
import java.util.List;
|
|
|
40 |
import java.util.Map;
|
|
|
41 |
import java.util.function.Function;
|
|
|
42 |
import java.util.stream.Collectors;
|
|
|
43 |
import java.util.stream.IntStream;
|
|
|
44 |
|
|
|
45 |
/**
|
|
|
46 |
* Raises an internal movement PO from a PO-screen upload file (itemId, quantity on the first sheet) the way the PO
|
|
|
47 |
* screen does - createPurchaseOrder, recorded as the operator, dated today - which also creates and pays the internal
|
|
|
48 |
* order that MovementBiller then bills. {@link #preview} shows the POs it would raise without writing anything.
|
|
|
49 |
*/
|
|
|
50 |
@Component
|
|
|
51 |
public class MovementPoCreator {
|
|
|
52 |
|
|
|
53 |
// createPurchaseOrder returns the first PO; its cost-split and margin-scheme siblings are raised right after it.
|
|
|
54 |
private static final int SPLIT_ID_WINDOW = 50;
|
|
|
55 |
|
|
|
56 |
@Autowired
|
|
|
57 |
private PurchaseOrderService purchaseOrderService;
|
|
|
58 |
@Autowired
|
|
|
59 |
private InternalMovementPricingService internalMovementPricingService;
|
|
|
60 |
@Autowired
|
|
|
61 |
private SupplierRepository supplierRepository;
|
|
|
62 |
@Autowired
|
|
|
63 |
private ItemRepository itemRepository;
|
|
|
64 |
@Autowired
|
|
|
65 |
private WarehousePurchaseOrderRepository warehousePurchaseOrderRepository;
|
|
|
66 |
@Autowired
|
|
|
67 |
private AuthRepository authRepository;
|
| 37832 |
amit |
68 |
@Autowired
|
|
|
69 |
private OrderRepository orderRepository;
|
| 37827 |
amit |
70 |
|
|
|
71 |
public static class Preview {
|
|
|
72 |
public String supplierName;
|
|
|
73 |
public final List<String> problems = new ArrayList<>();
|
|
|
74 |
/** One entry per PO that would be raised, each its priced lines. */
|
|
|
75 |
public List<List<InternalMovementAllocationModel>> purchaseOrders = new ArrayList<>();
|
|
|
76 |
public Map<Integer, Item> items = new LinkedHashMap<>();
|
|
|
77 |
}
|
|
|
78 |
|
|
|
79 |
/** itemId -> quantity, in file order. Same layout the PO screen's bulk upload reads. */
|
|
|
80 |
public static Map<Integer, Integer> readFile(String path) throws Exception {
|
|
|
81 |
Map<Integer, Integer> quantities = new LinkedHashMap<>();
|
|
|
82 |
try (InputStream in = new FileInputStream(path); XSSFWorkbook workbook = new XSSFWorkbook(in)) {
|
|
|
83 |
workbook.setMissingCellPolicy(Row.MissingCellPolicy.RETURN_BLANK_AS_NULL);
|
|
|
84 |
XSSFSheet sheet = workbook.getSheetAt(0);
|
|
|
85 |
for (int rowNumber = 1; rowNumber <= sheet.getLastRowNum(); rowNumber++) {
|
|
|
86 |
XSSFRow row = sheet.getRow(rowNumber);
|
|
|
87 |
if (row == null || row.getCell(0) == null) {
|
|
|
88 |
break;
|
|
|
89 |
}
|
|
|
90 |
if (row.getCell(0).getCellTypeEnum() != CellType.NUMERIC || row.getCell(1) == null
|
|
|
91 |
|| row.getCell(1).getCellTypeEnum() != CellType.NUMERIC) {
|
|
|
92 |
throw new ProfitMandiBusinessException("row", rowNumber + 1, "itemId and quantity must be numbers");
|
|
|
93 |
}
|
|
|
94 |
int itemId = (int) row.getCell(0).getNumericCellValue();
|
|
|
95 |
int quantity = (int) row.getCell(1).getNumericCellValue();
|
|
|
96 |
if (quantity <= 0) {
|
|
|
97 |
throw new ProfitMandiBusinessException("row", rowNumber + 1, "quantity must be positive");
|
|
|
98 |
}
|
|
|
99 |
if (quantities.put(itemId, quantity) != null) {
|
|
|
100 |
throw new ProfitMandiBusinessException("itemId", itemId, "Item " + itemId + " appears twice");
|
|
|
101 |
}
|
|
|
102 |
}
|
|
|
103 |
}
|
|
|
104 |
return quantities;
|
|
|
105 |
}
|
|
|
106 |
|
|
|
107 |
/** Everything that would stop the PO, and the POs it would be raised as. Writes nothing. */
|
|
|
108 |
@Transactional(readOnly = true, rollbackFor = Throwable.class)
|
|
|
109 |
public Preview preview(int warehouseId, int supplierId, Map<Integer, Integer> quantities) throws ProfitMandiBusinessException {
|
|
|
110 |
Preview preview = new Preview();
|
|
|
111 |
Supplier supplier = supplierRepository.selectById(supplierId);
|
|
|
112 |
if (supplier == null || !supplier.isInternal()) {
|
|
|
113 |
preview.problems.add("supplier " + supplierId + " is not an internal supplier");
|
|
|
114 |
return preview;
|
|
|
115 |
}
|
|
|
116 |
preview.supplierName = supplier.getName();
|
|
|
117 |
if (supplier.getInternalWarehouseId() != null && supplier.getInternalWarehouseId() == warehouseId) {
|
|
|
118 |
preview.problems.add(supplier.getName() + " cannot move stock to its own warehouse");
|
|
|
119 |
}
|
|
|
120 |
try {
|
|
|
121 |
purchaseOrderService.checkSupplierMappedToWarehouse(supplier, warehouseId);
|
|
|
122 |
} catch (ProfitMandiBusinessException e) {
|
|
|
123 |
preview.problems.add(e.getCode());
|
|
|
124 |
}
|
|
|
125 |
|
|
|
126 |
List<Item> items = itemRepository.selectByIds(new ArrayList<>(quantities.keySet()));
|
|
|
127 |
preview.items = items.stream().collect(Collectors.toMap(Item::getId, Function.identity(), (a, b) -> a, LinkedHashMap::new));
|
|
|
128 |
List<Integer> unknown = quantities.keySet().stream().filter(x -> !preview.items.containsKey(x)).collect(Collectors.toList());
|
|
|
129 |
if (!unknown.isEmpty()) {
|
|
|
130 |
preview.problems.add("unknown item ids " + unknown);
|
|
|
131 |
}
|
|
|
132 |
|
|
|
133 |
Map<Integer, InternalMovementAvailabilityModel> availability = internalMovementPricingService.describeAvailability(supplier, items);
|
|
|
134 |
for (Map.Entry<Integer, Integer> line : quantities.entrySet()) {
|
|
|
135 |
InternalMovementAvailabilityModel movable = availability.get(line.getKey());
|
|
|
136 |
int canMove = movable == null ? 0 : movable.getMovableAltogether();
|
|
|
137 |
if (line.getValue() > canMove) {
|
|
|
138 |
preview.problems.add("item " + line.getKey() + " asks " + line.getValue() + ", only " + canMove
|
|
|
139 |
+ " can move (in stock " + (movable == null ? 0 : movable.getInStock()) + ")");
|
|
|
140 |
}
|
|
|
141 |
}
|
|
|
142 |
if (preview.problems.isEmpty()) {
|
|
|
143 |
preview.purchaseOrders = purchaseOrderService.previewMovementPurchaseOrders(supplier, toPurchaseItems(quantities));
|
|
|
144 |
}
|
|
|
145 |
return preview;
|
|
|
146 |
}
|
|
|
147 |
|
|
|
148 |
/** Raises the PO and its siblings, all or nothing. Returns their PO numbers in the order raised. */
|
|
|
149 |
@Transactional(rollbackFor = Throwable.class)
|
|
|
150 |
public List<String> create(int warehouseId, int supplierId, Map<Integer, Integer> quantities, String operatorEmail)
|
|
|
151 |
throws Exception {
|
|
|
152 |
Preview preview = this.preview(warehouseId, supplierId, quantities);
|
|
|
153 |
if (!preview.problems.isEmpty()) {
|
|
|
154 |
throw new ProfitMandiBusinessException("file", supplierId, String.join("; ", preview.problems));
|
|
|
155 |
}
|
|
|
156 |
AuthUser operator = authRepository.selectByEmailOrMobile(operatorEmail);
|
|
|
157 |
if (operator == null) {
|
|
|
158 |
throw new ProfitMandiBusinessException("email", operatorEmail, "No auth user for " + operatorEmail);
|
|
|
159 |
}
|
|
|
160 |
CreateWarehousePurchaseOrderModel model = new CreateWarehousePurchaseOrderModel();
|
|
|
161 |
model.setWarehouseId(warehouseId);
|
|
|
162 |
model.setVendorId(supplierId);
|
|
|
163 |
model.setPoDate(LocalDate.now());
|
|
|
164 |
model.setCreatedBy(operator.getFullName());
|
|
|
165 |
model.setRequesterAuthId(operator.getId());
|
|
|
166 |
model.setItems(toPurchaseItems(quantities));
|
|
|
167 |
|
|
|
168 |
int firstPoId = purchaseOrderService.createPurchaseOrder(model);
|
|
|
169 |
List<Integer> window = IntStream.range(firstPoId, firstPoId + SPLIT_ID_WINDOW).boxed().collect(Collectors.toList());
|
|
|
170 |
List<WarehousePurchaseOrder> raised = warehousePurchaseOrderRepository.selectAll(window).stream()
|
|
|
171 |
.filter(x -> x.getSupplierId() == supplierId && x.getWarehouseId() == warehouseId
|
|
|
172 |
&& operator.getFullName().equals(x.getCreatedBy())
|
|
|
173 |
&& x.getCreatedAt() != null && LocalDate.now().equals(x.getCreatedAt().toLocalDate()))
|
|
|
174 |
.sorted(Comparator.comparingInt(WarehousePurchaseOrder::getId))
|
|
|
175 |
.collect(Collectors.toList());
|
|
|
176 |
if (raised.size() != preview.purchaseOrders.size()) {
|
|
|
177 |
throw new ProfitMandiBusinessException("po", firstPoId, "Expected " + preview.purchaseOrders.size()
|
|
|
178 |
+ " PO(s) but found " + raised.size() + " near PO id " + firstPoId + " - rolled back, check by hand");
|
|
|
179 |
}
|
|
|
180 |
return raised.stream().map(WarehousePurchaseOrder::getPoNumber).collect(Collectors.toList());
|
|
|
181 |
}
|
|
|
182 |
|
| 37832 |
amit |
183 |
/**
|
|
|
184 |
* Closes a movement PO the way the PO screen's Close does (PurchaseOrderController.closePuchaseOrder): refuses if
|
|
|
185 |
* any order has moved past submitted for processing, otherwise refunds every live order and pre-closes the PO.
|
|
|
186 |
* Returns what it did, or would do when {@code live} is false.
|
|
|
187 |
*/
|
|
|
188 |
@Transactional(rollbackFor = Throwable.class)
|
|
|
189 |
public String close(String poNumber, String closedBy, boolean live) throws Exception {
|
|
|
190 |
List<WarehousePurchaseOrder> found = warehousePurchaseOrderRepository.selectAllByPONumbers(Collections.singletonList(poNumber));
|
|
|
191 |
if (found.size() != 1) {
|
|
|
192 |
throw new ProfitMandiBusinessException("po", poNumber, "No single PO numbered " + poNumber);
|
|
|
193 |
}
|
|
|
194 |
WarehousePurchaseOrder po = found.get(0);
|
|
|
195 |
if (po.getTransactionId() == null || po.getTransactionId() <= 0) {
|
|
|
196 |
throw new ProfitMandiBusinessException("po", poNumber, poNumber + " raised no movement order");
|
|
|
197 |
}
|
|
|
198 |
List<Order> orders = orderRepository.selectAllByTransactionId(po.getTransactionId());
|
|
|
199 |
int liveOrders = 0;
|
|
|
200 |
int units = 0;
|
|
|
201 |
for (Order order : orders) {
|
|
|
202 |
if (order.getRefundTimestamp() != null) {
|
|
|
203 |
continue;
|
|
|
204 |
}
|
|
|
205 |
if (!OrderStatus.SUBMITTED_FOR_PROCESSING.equals(order.getStatus())) {
|
|
|
206 |
throw new ProfitMandiBusinessException("po", poNumber, "Order " + order.getId() + " is already "
|
|
|
207 |
+ order.getStatus() + " - its stock has left, so " + poNumber + " cannot be closed");
|
|
|
208 |
}
|
|
|
209 |
liveOrders++;
|
|
|
210 |
units += order.getLineItem().getQuantity();
|
|
|
211 |
}
|
|
|
212 |
String summary = poNumber + " (txn " + po.getTransactionId() + ", status " + po.getStatus() + "): " + liveOrders
|
|
|
213 |
+ " order(s), " + units + " unit(s) to refund, then PRECLOSED";
|
|
|
214 |
if (!live) {
|
|
|
215 |
return "WOULD CLOSE " + summary;
|
|
|
216 |
}
|
|
|
217 |
for (Order order : orders) {
|
|
|
218 |
if (order.getRefundTimestamp() == null) {
|
|
|
219 |
orderRepository.refundOrder(order.getId(), closedBy, "Movement purchase order " + poNumber + " closed",
|
|
|
220 |
OrderStatus.CANCELLED_DUE_TO_LOW_INVENTORY);
|
|
|
221 |
}
|
|
|
222 |
}
|
|
|
223 |
po.setStatus(POStatus.PRECLOSED);
|
|
|
224 |
po.setUpdatedAt(LocalDateTime.now());
|
|
|
225 |
return "CLOSED " + summary;
|
|
|
226 |
}
|
|
|
227 |
|
| 37827 |
amit |
228 |
private static List<ItemWisePurchaseModel> toPurchaseItems(Map<Integer, Integer> quantities) {
|
|
|
229 |
List<ItemWisePurchaseModel> items = new ArrayList<>();
|
|
|
230 |
for (Map.Entry<Integer, Integer> line : quantities.entrySet()) {
|
|
|
231 |
ItemWisePurchaseModel item = new ItemWisePurchaseModel();
|
|
|
232 |
item.setItemId(line.getKey());
|
|
|
233 |
item.setAmendedQty(line.getValue());
|
|
|
234 |
items.add(item);
|
|
|
235 |
}
|
|
|
236 |
return items;
|
|
|
237 |
}
|
|
|
238 |
}
|