Subversion Repositories SmartDukaan

Rev

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

package com.smartdukaan.cron.migrations;

import com.spice.profitmandi.common.enumuration.FofoType;
import com.spice.profitmandi.common.enumuration.ItemType;
import com.spice.profitmandi.common.exception.ProfitMandiBusinessException;
import com.spice.profitmandi.common.model.GrnModel;
import com.spice.profitmandi.common.model.InvoiceItemModel;
import com.spice.profitmandi.common.model.WarehouseInvoiceModel;
import com.spice.profitmandi.dao.entity.auth.AuthUser;
import com.spice.profitmandi.dao.entity.catalog.Item;
import com.spice.profitmandi.dao.entity.fofo.FofoStore;
import com.spice.profitmandi.dao.entity.transaction.LineItem;
import com.spice.profitmandi.dao.entity.transaction.Order;
import com.spice.profitmandi.dao.entity.warehouse.WarehousePurchaseOrder;
import com.spice.profitmandi.dao.entity.warehouse.WarehouseSupplierInvoice;
import com.spice.profitmandi.dao.enumuration.warehouse.WarehouseInvoiceStatus;
import com.spice.profitmandi.dao.repository.auth.AuthRepository;
import com.spice.profitmandi.dao.repository.catalog.ItemRepository;
import com.spice.profitmandi.dao.repository.dtr.FofoStoreRepository;
import com.spice.profitmandi.dao.repository.transaction.OrderRepository;
import com.spice.profitmandi.dao.repository.warehouse.WarehousePurchaseOrderRepository;
import com.spice.profitmandi.dao.repository.warehouse.WarehouseSupplierInvoiceRepository;
import com.spice.profitmandi.service.warehouse.InvoiceService;
import com.spice.profitmandi.service.warehouse.PurchaseOrderService;
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.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Receives one internal transfer invoice into the destination warehouse, following the same sequence the
 * portal performs: record the supplier invoice, record its items, then GRN it through
 * PurchaseOrderService.grnPoModels - the call behind the portal's Create GRN button.
 *
 * The Excel upload path (addPORowModels) is deliberately not used. It persists a supplier invoice without a
 * status, which the column does not allow, so that route cannot complete a receipt at all.
 *
 * Kept in its own bean on purpose: InternalGrnTask calls this through the Spring proxy, so the transaction
 * below is actually applied. Calling it from inside the driver would be a self invocation, the proxy would
 * be bypassed and the work would run with no transaction at all.
 */
@Component
public class InternalGrnReceiver {

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

    /** The portal stores the scanned supplier invoice against the receipt; an internal transfer has none. */
    private static final int NO_INVOICE_DOCUMENT = 0;

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private ItemRepository itemRepository;

    @Autowired
    private FofoStoreRepository fofoStoreRepository;

    @Autowired
    private AuthRepository authRepository;

    @Autowired
    private WarehousePurchaseOrderRepository warehousePurchaseOrderRepository;

    @Autowired
    private WarehouseSupplierInvoiceRepository warehouseSupplierInvoiceRepository;

    @Autowired
    private InvoiceService invoiceService;

    @Autowired
    private PurchaseOrderService purchaseOrderService;

    /** One item of an invoice, gathered across the orders that share it. */
    private static class ReceiptLine {
        private int itemId;
        private boolean serialized;
        private float rate;
        private int qty;
        private final List<String> serialNumbers = new ArrayList<>();
    }

    /**
     * Each invoice commits or rolls back on its own so one bad invoice cannot undo those already received.
     *
     * @return true when the invoice was received (or would be, on a dry run), false when it was skipped.
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Throwable.class)
    public boolean grnInvoice(String invoiceNumber, String operatorEmail, boolean dryRun) throws ProfitMandiBusinessException {
        List<Order> orders = orderRepository.selectByInvoiceNumber(invoiceNumber);
        if (orders.isEmpty()) {
            LOGGER.info("SKIP    {} - no orders for this invoice", invoiceNumber);
            return false;
        }

        // Only our own stock moving between our own warehouses - never auto-receive a partner's goods.
        FofoStore buyer = fofoStoreRepository.selectByRetailerId(orders.get(0).getRetailerId());
        if (buyer == null || !FofoType.INTERNAL.equals(buyer.getFofoType())) {
            LOGGER.info("SKIP    {} - buyer {} is not an INTERNAL store", invoiceNumber, orders.get(0).getRetailerId());
            return false;
        }

        WarehousePurchaseOrder purchaseOrder = resolvePurchaseOrder(invoiceNumber, orders);
        if (purchaseOrder == null) {
            return false;
        }

        WarehouseSupplierInvoice existing = warehouseSupplierInvoiceRepository
                .selectAllBySupplierInvoice(purchaseOrder.getSupplierId(), invoiceNumber);
        if (existing != null) {
            LOGGER.info("SKIP    {} - already received (invoice id {}, status {})",
                    invoiceNumber, existing.getId(), existing.getStatus());
            return false;
        }

        // grnPoModels takes one entry per item, carrying every serial of that item, so gather the orders by item.
        Map<Integer, ReceiptLine> linesByItem = new LinkedHashMap<>();
        for (Order order : orders) {
            LineItem lineItem = order.getLineItem();
            if (lineItem == null) {
                LOGGER.info("SKIP    {} - order {} has no line item", invoiceNumber, order.getId());
                return false;
            }
            Item item = itemRepository.selectById(lineItem.getItemId());
            if (item == null) {
                LOGGER.info("SKIP    {} - item {} not found", invoiceNumber, lineItem.getItemId());
                return false;
            }

            boolean serialized = ItemType.SERIALIZED.equals(item.getType());
            int quantity = lineItem.getQuantity();

            // A line item carries one serial per unit, comma separated - never a single serial for the line.
            // Handing the whole field over as one serial stores a string no scan can match and records the
            // line as one unit, so the serials are split out and counted against the quantity here.
            List<String> serialNumbers = new ArrayList<>();
            if (serialized) {
                String serialField = lineItem.getSerialNumber();
                if (serialField != null) {
                    for (String serialNumber : serialField.split(",")) {
                        if (!serialNumber.trim().isEmpty()) {
                            serialNumbers.add(serialNumber.trim());
                        }
                    }
                }
                if (serialNumbers.size() != quantity) {
                    LOGGER.info("SKIP    {} - serialized item {} on order {} has {} serial(s) for {} unit(s)",
                            invoiceNumber, item.getId(), order.getId(), serialNumbers.size(), quantity);
                    return false;
                }
            }

            ReceiptLine line = linesByItem.get(lineItem.getItemId());
            if (line == null) {
                line = new ReceiptLine();
                line.itemId = lineItem.getItemId();
                line.serialized = serialized;
                line.rate = lineItem.getUnitPrice();
                linesByItem.put(line.itemId, line);
            }
            line.qty += quantity;
            line.serialNumbers.addAll(serialNumbers);
        }

        int numItems = 0;
        int serialisedUnits = 0;
        BigDecimal value = BigDecimal.ZERO;
        for (ReceiptLine line : linesByItem.values()) {
            numItems += line.qty;
            serialisedUnits += line.serialNumbers.size();
            value = value.add(BigDecimal.valueOf(line.rate).multiply(BigDecimal.valueOf(line.qty)));
        }

        if (dryRun) {
            LOGGER.info("WOULD GRN {} - po {} (id {}), warehouse {}, supplier {}, {} item(s), {} unit(s), {} serialized, value {}",
                    invoiceNumber, purchaseOrder.getPoNumber(), purchaseOrder.getId(), purchaseOrder.getWarehouseId(),
                    purchaseOrder.getSupplierId(), linesByItem.size(), numItems, serialisedUnits, value.toPlainString());
            for (ReceiptLine line : linesByItem.values()) {
                LOGGER.info("          item {} qty {} rate {} serials {}",
                        line.itemId, line.qty, line.rate,
                        line.serialNumbers.isEmpty() ? "-" : line.serialNumbers);
            }
            return true;
        }

        // 1. Record the supplier invoice, as the portal's Receive Invoice screen does.
        AuthUser operator = authRepository.selectByEmailOrMobile(operatorEmail);
        WarehouseSupplierInvoice invoice = new WarehouseSupplierInvoice();
        invoice.setInvoiceNumber(invoiceNumber);
        invoice.setInvoiceDate(orders.get(0).getBillingTimestamp());
        invoice.setSupplierId(purchaseOrder.getSupplierId());
        invoice.setWarehouseId(purchaseOrder.getWarehouseId());
        invoice.setCreateDate(LocalDateTime.now());
        invoice.setReceivedFrom(operator.getFullName());
        invoice.setNumItems(numItems);
        invoice.setTotalValue(value.floatValue());
        invoice.setStatus(WarehouseInvoiceStatus.init);
        invoice.setInvoiceDocId(NO_INVOICE_DOCUMENT);
        warehouseSupplierInvoiceRepository.persist(invoice);

        // 2. Record its items, as the Add Item screen does - the buying reports read these rows.
        WarehouseInvoiceModel invoiceModel = new WarehouseInvoiceModel();
        invoiceModel.setInvoiceId(invoice.getId());
        invoiceModel.setSupplierId(purchaseOrder.getSupplierId());
        invoiceModel.setWarehouseId(purchaseOrder.getWarehouseId());
        List<InvoiceItemModel> invoiceItems = new ArrayList<>();
        for (ReceiptLine line : linesByItem.values()) {
            InvoiceItemModel invoiceItem = new InvoiceItemModel();
            invoiceItem.setItemId(line.itemId);
            invoiceItem.setQty(line.qty);
            invoiceItem.setRate(line.rate);
            invoiceItems.add(invoiceItem);
        }
        invoiceModel.setInvoiceItems(invoiceItems);
        invoiceService.createInvoiceItem(invoiceModel);

        // 3. GRN it, exactly as the portal's Create GRN button does.
        List<GrnModel> grnModels = new ArrayList<>();
        for (ReceiptLine line : linesByItem.values()) {
            GrnModel grnModel = new GrnModel();
            grnModel.setPoId(purchaseOrder.getId());
            grnModel.setInvoiceId(invoice.getId());
            grnModel.setItemId(line.itemId);
            grnModel.setQty(line.qty);
            grnModel.setInvoicePrice(line.rate);
            grnModel.setSerialNumbers(line.serialized ? line.serialNumbers : new ArrayList<String>());
            grnModels.add(grnModel);
        }
        Map<Integer, Map<Integer, List<GrnModel>>> grnModelsMap = Collections.singletonMap(
                purchaseOrder.getId(), Collections.singletonMap(invoice.getId(), grnModels));
        purchaseOrderService.grnPoModels(operatorEmail, grnModelsMap);

        LOGGER.info("GRNED   {} - po {}, warehouse {}, invoice id {}, {} item(s), {} unit(s), {} serialized, value {}",
                invoiceNumber, purchaseOrder.getPoNumber(), purchaseOrder.getWarehouseId(), invoice.getId(),
                linesByItem.size(), numItems, serialisedUnits, value.toPlainString());
        return true;
    }

    /**
     * Creating an internal PO raises one transaction and records it on the order
     * (PurchaseOrderServiceImpl: warehousePurchaseOrder.setTransactionId), so that is the mapping back from
     * an invoice to its PO. order.purchase_order_id is not it - nothing populates it on this route.
     *
     * The invoice's orders all belong to that one transaction; if they ever did not, the PO is ambiguous and
     * the invoice is left alone rather than guessed at.
     */
    private WarehousePurchaseOrder resolvePurchaseOrder(String invoiceNumber, List<Order> orders) {
        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(), transactionIds);
            return null;
        }
        int transactionId = transactionIds.iterator().next();
        WarehousePurchaseOrder purchaseOrder = warehousePurchaseOrderRepository.selectByTransactionId(transactionId);
        if (purchaseOrder == null) {
            LOGGER.info("SKIP    {} - no purchase order mapped to transaction {}", invoiceNumber, transactionId);
            return null;
        }
        return purchaseOrder;
    }
}