Subversion Repositories SmartDukaan

Rev

Go to most recent revision | 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.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.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 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.util.ArrayList;
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;

    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());
    }

    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;
    }
}