Subversion Repositories SmartDukaan

Rev

Blame | Last modification | View Log | RSS feed

package com.smartdukaan.cron.migrations;

import com.spice.profitmandi.common.exception.ProfitMandiBusinessException;
import com.spice.profitmandi.dao.entity.inventory.SaholicInventorySnapshot;
import com.spice.profitmandi.dao.entity.transaction.Order;
import com.spice.profitmandi.dao.entity.warehouse.WarehouseInventoryItem;
import com.spice.profitmandi.dao.entity.warehouse.WarehouseInvoiceItem;
import com.spice.profitmandi.dao.entity.warehouse.WarehouseLineItem;
import com.spice.profitmandi.dao.entity.warehouse.WarehousePurchase;
import com.spice.profitmandi.dao.entity.warehouse.WarehousePurchaseOrder;
import com.spice.profitmandi.dao.entity.warehouse.WarehouseScan;
import com.spice.profitmandi.dao.entity.warehouse.WarehouseSupplierInvoice;
import in.shop2020.warehouse.ScanType;
import com.spice.profitmandi.dao.repository.GenericRepository;
import com.spice.profitmandi.dao.repository.inventory.SaholicInventorySnapshotRepository;
import com.spice.profitmandi.dao.repository.transaction.OrderRepository;
import com.spice.profitmandi.dao.repository.warehouse.WarehouseInvoiceItemRepository;
import com.spice.profitmandi.dao.repository.warehouse.WarehouseLineItemRepository;
import com.spice.profitmandi.dao.repository.warehouse.WarehousePurchaseOrderRepository;
import com.spice.profitmandi.dao.repository.warehouse.WarehousePurchaseRepository;
import com.spice.profitmandi.dao.repository.warehouse.WarehouseScanRepository;
import com.spice.profitmandi.dao.repository.warehouse.WarehouseSupplierInvoiceRepository;
import in.shop2020.purchase.POStatus;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Undoes one internal GRN so the invoice can be received again cleanly.
 *
 * It removes only what receiving created - the scans, the inventory units, the invoice items, the purchase
 * and the supplier invoice - and puts back the two running figures receiving moved: the warehouse
 * availability it added to, and the quantity it took off the purchase order line.
 *
 * It refuses an invoice whose stock has been touched since. A unit that has been scanned out, split or
 * partly consumed is no longer ours to delete, and reversing around it would leave the warehouse holding
 * stock no record explains.
 *
 * Kept in its own bean so InternalGrnTask reaches it through the Spring proxy and the transaction below is
 * actually applied.
 */
@Component
public class InternalGrnReverser {

    private static final Logger LOGGER = LogManager.getLogger(InternalGrnReverser.class);

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private GenericRepository genericRepository;

    @Autowired
    private WarehousePurchaseOrderRepository warehousePurchaseOrderRepository;

    @Autowired
    private WarehouseSupplierInvoiceRepository warehouseSupplierInvoiceRepository;

    @Autowired
    private WarehousePurchaseRepository warehousePurchaseRepository;

    @Autowired
    private WarehouseInvoiceItemRepository warehouseInvoiceItemRepository;

    @Autowired
    private WarehouseLineItemRepository warehouseLineItemRepository;

    @Autowired
    private WarehouseScanRepository warehouseScanRepository;

    @Autowired
    private SaholicInventorySnapshotRepository saholicInventorySnapshotRepository;

    /**
     * @return true when the invoice was reversed (or would be, on a dry run), false when it was left alone.
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Throwable.class)
    public boolean reverseInvoice(String invoiceNumber, boolean dryRun) throws ProfitMandiBusinessException {
        List<Order> orders = orderRepository.selectByInvoiceNumber(invoiceNumber);
        if (orders.isEmpty()) {
            LOGGER.info("SKIP     {} - no orders for this invoice", invoiceNumber);
            return false;
        }

        // Resolved the same way receiving resolved it, so the reversal cannot land on a different PO.
        Set<Integer> transactionIds = new HashSet<>();
        for (Order order : orders) {
            transactionIds.add(order.getTransactionId());
        }
        if (transactionIds.size() > 1) {
            LOGGER.info("SKIP     {} - spans {} transactions", invoiceNumber, transactionIds.size());
            return false;
        }
        WarehousePurchaseOrder purchaseOrder =
                warehousePurchaseOrderRepository.selectByTransactionId(transactionIds.iterator().next());
        if (purchaseOrder == null) {
            LOGGER.info("SKIP     {} - no purchase order mapped to the transaction", invoiceNumber);
            return false;
        }

        WarehouseSupplierInvoice invoice = warehouseSupplierInvoiceRepository
                .selectAllBySupplierInvoice(purchaseOrder.getSupplierId(), invoiceNumber);
        if (invoice == null) {
            LOGGER.info("SKIP     {} - not received, nothing to reverse", invoiceNumber);
            return false;
        }

        List<WarehousePurchase> purchases = warehousePurchaseRepository.selectByInvoiceId(invoice.getId());
        List<WarehouseInventoryItem> inventoryItems = new ArrayList<>();
        for (WarehousePurchase purchase : purchases) {
            inventoryItems.addAll(genericRepository.<WarehouseInventoryItem>selectAllByEqualOrderByDesc(
                    WarehouseInventoryItem.class, "purchaseId", purchase.getId(), "id"));
        }

        // Anything that moved since receiving is not ours to undo.
        for (WarehouseInventoryItem inventoryItem : inventoryItems) {
            if (inventoryItem.getCurrentQuantity() != inventoryItem.getInitialQuantity()) {
                LOGGER.info("REFUSE   {} - inventory item {} has moved ({} of {} left), reverse it by hand",
                        invoiceNumber, inventoryItem.getId(), inventoryItem.getCurrentQuantity(),
                        inventoryItem.getInitialQuantity());
                return false;
            }
            if (!ScanType.PURCHASE.equals(inventoryItem.getLastScanType())) {
                LOGGER.info("REFUSE   {} - inventory item {} was last scanned {}, not PURCHASE",
                        invoiceNumber, inventoryItem.getId(), inventoryItem.getLastScanType());
                return false;
            }
        }

        // What receiving added to availability, keyed the way it added it.
        Map<String, Integer> availabilityToRelease = new LinkedHashMap<>();
        // What receiving took off each PO line.
        Map<Integer, Integer> quantityToRestore = new LinkedHashMap<>();
        int units = 0;
        for (WarehouseInventoryItem inventoryItem : inventoryItems) {
            String key = inventoryItem.getCurrentWarehouseId() + ":" + inventoryItem.getItemId();
            Integer released = availabilityToRelease.get(key);
            availabilityToRelease.put(key, (released == null ? 0 : released) + inventoryItem.getInitialQuantity());
            Integer restored = quantityToRestore.get(inventoryItem.getItemId());
            quantityToRestore.put(inventoryItem.getItemId(),
                    (restored == null ? 0 : restored) + inventoryItem.getInitialQuantity());
            units += inventoryItem.getInitialQuantity();
        }

        List<WarehouseInvoiceItem> invoiceItems = warehouseInvoiceItemRepository.selectByInvoiceId(invoice.getId());

        if (dryRun) {
            LOGGER.info("WOULD REVERSE {} - invoice id {}, po {}, {} purchase(s), {} inventory row(s), {} unit(s), {} invoice item(s)",
                    invoiceNumber, invoice.getId(), purchaseOrder.getPoNumber(), purchases.size(),
                    inventoryItems.size(), units, invoiceItems.size());
            for (WarehouseInventoryItem inventoryItem : inventoryItems) {
                LOGGER.info("          delete inventory {} item {} qty {} serial {}",
                        inventoryItem.getId(), inventoryItem.getItemId(), inventoryItem.getInitialQuantity(),
                        inventoryItem.getSerialNumber() == null ? "-" : inventoryItem.getSerialNumber());
            }
            for (Map.Entry<String, Integer> entry : availabilityToRelease.entrySet()) {
                String[] key = entry.getKey().split(":");
                SaholicInventorySnapshot snapshot = saholicInventorySnapshotRepository
                        .selectByWarehouseIdandItemId(Integer.valueOf(key[0]), Integer.valueOf(key[1]));
                LOGGER.info("          availability warehouse {} item {} : {} -> {}",
                        key[0], key[1], snapshot == null ? "absent" : snapshot.getAvailability(),
                        snapshot == null ? "absent" : (snapshot.getAvailability() - entry.getValue()));
            }
            for (Map.Entry<Integer, Integer> entry : quantityToRestore.entrySet()) {
                WarehouseLineItem lineItem = warehouseLineItemRepository
                        .selectByPurchaseOrderIdItemId(purchaseOrder.getId(), entry.getKey());
                LOGGER.info("          po line item {} unfulfilled : {} -> {}", entry.getKey(),
                        lineItem == null ? "absent" : lineItem.getUnfulfilledQuantity(),
                        lineItem == null ? "absent" : (lineItem.getUnfulfilledQuantity() + entry.getValue()));
            }
            if (POStatus.CLOSED.equals(purchaseOrder.getStatus())) {
                LOGGER.info("          po {} would be reopened to READY", purchaseOrder.getPoNumber());
            }
            return true;
        }

        // Give back the availability receiving added.
        for (Map.Entry<String, Integer> entry : availabilityToRelease.entrySet()) {
            String[] key = entry.getKey().split(":");
            SaholicInventorySnapshot snapshot = saholicInventorySnapshotRepository
                    .selectByWarehouseIdandItemId(Integer.valueOf(key[0]), Integer.valueOf(key[1]));
            if (snapshot != null) {
                snapshot.setAvailability(snapshot.getAvailability() - entry.getValue());
            }
        }

        // Put back the quantity receiving took off the PO lines.
        for (Map.Entry<Integer, Integer> entry : quantityToRestore.entrySet()) {
            WarehouseLineItem lineItem = warehouseLineItemRepository
                    .selectByPurchaseOrderIdItemId(purchaseOrder.getId(), entry.getKey());
            if (lineItem != null) {
                lineItem.setUnfulfilledQuantity(lineItem.getUnfulfilledQuantity() + entry.getValue());
            }
        }

        // A PO closed by this receipt is open again now that the quantity is back on it.
        if (POStatus.CLOSED.equals(purchaseOrder.getStatus())) {
            purchaseOrder.setStatus(POStatus.READY);
        }

        for (WarehouseInventoryItem inventoryItem : inventoryItems) {
            for (WarehouseScan scan : warehouseScanRepository.selectAll(inventoryItem.getId(), ScanType.PURCHASE)) {
                warehouseScanRepository.delete(scan);
            }
            genericRepository.delete(inventoryItem);
        }
        for (WarehouseInvoiceItem invoiceItem : invoiceItems) {
            warehouseInvoiceItemRepository.delete(invoiceItem);
        }
        for (WarehousePurchase purchase : purchases) {
            genericRepository.delete(purchase);
        }
        warehouseSupplierInvoiceRepository.delete(invoice);

        LOGGER.info("REVERSED {} - invoice id {}, po {}, {} inventory row(s), {} unit(s) released",
                invoiceNumber, invoice.getId(), purchaseOrder.getPoNumber(), inventoryItems.size(), units);
        return true;
    }
}