Subversion Repositories SmartDukaan

Rev

Rev 37827 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
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
}