Subversion Repositories SmartDukaan

Rev

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

package com.smartdukaan.cron.migrations;

import com.spice.profitmandi.common.exception.ProfitMandiBusinessException;
import com.spice.profitmandi.dao.entity.auth.AuthUser;
import com.spice.profitmandi.dao.entity.catalog.Item;
import com.spice.profitmandi.dao.entity.transaction.Order;
import com.spice.profitmandi.dao.entity.warehouse.Supplier;
import com.spice.profitmandi.dao.entity.warehouse.WarehousePurchaseOrder;
import com.spice.profitmandi.dao.model.CreateWarehousePurchaseOrderModel;
import com.spice.profitmandi.dao.model.InternalMovementAllocationModel;
import com.spice.profitmandi.dao.model.InternalMovementAvailabilityModel;
import com.spice.profitmandi.dao.model.ItemWisePurchaseModel;
import com.spice.profitmandi.dao.repository.auth.AuthRepository;
import com.spice.profitmandi.dao.repository.catalog.ItemRepository;
import com.spice.profitmandi.dao.repository.transaction.OrderRepository;
import com.spice.profitmandi.dao.repository.warehouse.SupplierRepository;
import com.spice.profitmandi.dao.repository.warehouse.WarehousePurchaseOrderRepository;
import com.spice.profitmandi.service.warehouse.InternalMovementPricingService;
import com.spice.profitmandi.service.warehouse.PurchaseOrderService;
import in.shop2020.model.v1.order.OrderStatus;
import in.shop2020.purchase.POStatus;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.io.FileInputStream;
import java.io.InputStream;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

/**
 * Raises an internal movement PO from a PO-screen upload file (itemId, quantity on the first sheet) the way the PO
 * screen does - createPurchaseOrder, recorded as the operator, dated today - which also creates and pays the internal
 * order that MovementBiller then bills. {@link #preview} shows the POs it would raise without writing anything.
 */
@Component
public class MovementPoCreator {

    // createPurchaseOrder returns the first PO; its cost-split and margin-scheme siblings are raised right after it.
    private static final int SPLIT_ID_WINDOW = 50;

    @Autowired
    private PurchaseOrderService purchaseOrderService;
    @Autowired
    private InternalMovementPricingService internalMovementPricingService;
    @Autowired
    private SupplierRepository supplierRepository;
    @Autowired
    private ItemRepository itemRepository;
    @Autowired
    private WarehousePurchaseOrderRepository warehousePurchaseOrderRepository;
    @Autowired
    private AuthRepository authRepository;
    @Autowired
    private OrderRepository orderRepository;

    public static class Preview {
        public String supplierName;
        public final List<String> problems = new ArrayList<>();
        /** One entry per PO that would be raised, each its priced lines. */
        public List<List<InternalMovementAllocationModel>> purchaseOrders = new ArrayList<>();
        public Map<Integer, Item> items = new LinkedHashMap<>();
    }

    /** itemId -> quantity, in file order. Same layout the PO screen's bulk upload reads. */
    public static Map<Integer, Integer> readFile(String path) throws Exception {
        Map<Integer, Integer> quantities = new LinkedHashMap<>();
        try (InputStream in = new FileInputStream(path); XSSFWorkbook workbook = new XSSFWorkbook(in)) {
            workbook.setMissingCellPolicy(Row.MissingCellPolicy.RETURN_BLANK_AS_NULL);
            XSSFSheet sheet = workbook.getSheetAt(0);
            for (int rowNumber = 1; rowNumber <= sheet.getLastRowNum(); rowNumber++) {
                XSSFRow row = sheet.getRow(rowNumber);
                if (row == null || row.getCell(0) == null) {
                    break;
                }
                if (row.getCell(0).getCellTypeEnum() != CellType.NUMERIC || row.getCell(1) == null
                        || row.getCell(1).getCellTypeEnum() != CellType.NUMERIC) {
                    throw new ProfitMandiBusinessException("row", rowNumber + 1, "itemId and quantity must be numbers");
                }
                int itemId = (int) row.getCell(0).getNumericCellValue();
                int quantity = (int) row.getCell(1).getNumericCellValue();
                if (quantity <= 0) {
                    throw new ProfitMandiBusinessException("row", rowNumber + 1, "quantity must be positive");
                }
                if (quantities.put(itemId, quantity) != null) {
                    throw new ProfitMandiBusinessException("itemId", itemId, "Item " + itemId + " appears twice");
                }
            }
        }
        return quantities;
    }

    /** Everything that would stop the PO, and the POs it would be raised as. Writes nothing. */
    @Transactional(readOnly = true, rollbackFor = Throwable.class)
    public Preview preview(int warehouseId, int supplierId, Map<Integer, Integer> quantities) throws ProfitMandiBusinessException {
        Preview preview = new Preview();
        Supplier supplier = supplierRepository.selectById(supplierId);
        if (supplier == null || !supplier.isInternal()) {
            preview.problems.add("supplier " + supplierId + " is not an internal supplier");
            return preview;
        }
        preview.supplierName = supplier.getName();
        if (supplier.getInternalWarehouseId() != null && supplier.getInternalWarehouseId() == warehouseId) {
            preview.problems.add(supplier.getName() + " cannot move stock to its own warehouse");
        }
        try {
            purchaseOrderService.checkSupplierMappedToWarehouse(supplier, warehouseId);
        } catch (ProfitMandiBusinessException e) {
            preview.problems.add(e.getCode());
        }

        List<Item> items = itemRepository.selectByIds(new ArrayList<>(quantities.keySet()));
        preview.items = items.stream().collect(Collectors.toMap(Item::getId, Function.identity(), (a, b) -> a, LinkedHashMap::new));
        List<Integer> unknown = quantities.keySet().stream().filter(x -> !preview.items.containsKey(x)).collect(Collectors.toList());
        if (!unknown.isEmpty()) {
            preview.problems.add("unknown item ids " + unknown);
        }

        Map<Integer, InternalMovementAvailabilityModel> availability = internalMovementPricingService.describeAvailability(supplier, items);
        for (Map.Entry<Integer, Integer> line : quantities.entrySet()) {
            InternalMovementAvailabilityModel movable = availability.get(line.getKey());
            int canMove = movable == null ? 0 : movable.getMovableAltogether();
            if (line.getValue() > canMove) {
                preview.problems.add("item " + line.getKey() + " asks " + line.getValue() + ", only " + canMove
                        + " can move (in stock " + (movable == null ? 0 : movable.getInStock()) + ")");
            }
        }
        if (preview.problems.isEmpty()) {
            preview.purchaseOrders = purchaseOrderService.previewMovementPurchaseOrders(supplier, toPurchaseItems(quantities));
        }
        return preview;
    }

    /** Raises the PO and its siblings, all or nothing. Returns their PO numbers in the order raised. */
    @Transactional(rollbackFor = Throwable.class)
    public List<String> create(int warehouseId, int supplierId, Map<Integer, Integer> quantities, String operatorEmail)
            throws Exception {
        Preview preview = this.preview(warehouseId, supplierId, quantities);
        if (!preview.problems.isEmpty()) {
            throw new ProfitMandiBusinessException("file", supplierId, String.join("; ", preview.problems));
        }
        AuthUser operator = authRepository.selectByEmailOrMobile(operatorEmail);
        if (operator == null) {
            throw new ProfitMandiBusinessException("email", operatorEmail, "No auth user for " + operatorEmail);
        }
        CreateWarehousePurchaseOrderModel model = new CreateWarehousePurchaseOrderModel();
        model.setWarehouseId(warehouseId);
        model.setVendorId(supplierId);
        model.setPoDate(LocalDate.now());
        model.setCreatedBy(operator.getFullName());
        model.setRequesterAuthId(operator.getId());
        model.setItems(toPurchaseItems(quantities));

        int firstPoId = purchaseOrderService.createPurchaseOrder(model);
        List<Integer> window = IntStream.range(firstPoId, firstPoId + SPLIT_ID_WINDOW).boxed().collect(Collectors.toList());
        List<WarehousePurchaseOrder> raised = warehousePurchaseOrderRepository.selectAll(window).stream()
                .filter(x -> x.getSupplierId() == supplierId && x.getWarehouseId() == warehouseId
                        && operator.getFullName().equals(x.getCreatedBy())
                        && x.getCreatedAt() != null && LocalDate.now().equals(x.getCreatedAt().toLocalDate()))
                .sorted(Comparator.comparingInt(WarehousePurchaseOrder::getId))
                .collect(Collectors.toList());
        if (raised.size() != preview.purchaseOrders.size()) {
            throw new ProfitMandiBusinessException("po", firstPoId, "Expected " + preview.purchaseOrders.size()
                    + " PO(s) but found " + raised.size() + " near PO id " + firstPoId + " - rolled back, check by hand");
        }
        return raised.stream().map(WarehousePurchaseOrder::getPoNumber).collect(Collectors.toList());
    }

    /**
     * Closes a movement PO the way the PO screen's Close does (PurchaseOrderController.closePuchaseOrder): refuses if
     * any order has moved past submitted for processing, otherwise refunds every live order and pre-closes the PO.
     * Returns what it did, or would do when {@code live} is false.
     */
    @Transactional(rollbackFor = Throwable.class)
    public String close(String poNumber, String closedBy, boolean live) throws Exception {
        List<WarehousePurchaseOrder> found = warehousePurchaseOrderRepository.selectAllByPONumbers(Collections.singletonList(poNumber));
        if (found.size() != 1) {
            throw new ProfitMandiBusinessException("po", poNumber, "No single PO numbered " + poNumber);
        }
        WarehousePurchaseOrder po = found.get(0);
        if (po.getTransactionId() == null || po.getTransactionId() <= 0) {
            throw new ProfitMandiBusinessException("po", poNumber, poNumber + " raised no movement order");
        }
        List<Order> orders = orderRepository.selectAllByTransactionId(po.getTransactionId());
        int liveOrders = 0;
        int units = 0;
        for (Order order : orders) {
            if (order.getRefundTimestamp() != null) {
                continue;
            }
            if (!OrderStatus.SUBMITTED_FOR_PROCESSING.equals(order.getStatus())) {
                throw new ProfitMandiBusinessException("po", poNumber, "Order " + order.getId() + " is already "
                        + order.getStatus() + " - its stock has left, so " + poNumber + " cannot be closed");
            }
            liveOrders++;
            units += order.getLineItem().getQuantity();
        }
        String summary = poNumber + " (txn " + po.getTransactionId() + ", status " + po.getStatus() + "): " + liveOrders
                + " order(s), " + units + " unit(s) to refund, then PRECLOSED";
        if (!live) {
            return "WOULD CLOSE " + summary;
        }
        for (Order order : orders) {
            if (order.getRefundTimestamp() == null) {
                orderRepository.refundOrder(order.getId(), closedBy, "Movement purchase order " + poNumber + " closed",
                        OrderStatus.CANCELLED_DUE_TO_LOW_INVENTORY);
            }
        }
        po.setStatus(POStatus.PRECLOSED);
        po.setUpdatedAt(LocalDateTime.now());
        return "CLOSED " + summary;
    }

    private static List<ItemWisePurchaseModel> toPurchaseItems(Map<Integer, Integer> quantities) {
        List<ItemWisePurchaseModel> items = new ArrayList<>();
        for (Map.Entry<Integer, Integer> line : quantities.entrySet()) {
            ItemWisePurchaseModel item = new ItemWisePurchaseModel();
            item.setItemId(line.getKey());
            item.setAmendedQty(line.getValue());
            items.add(item);
        }
        return items;
    }
}