Subversion Repositories SmartDukaan

Rev

Rev 37614 | 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.enumuration.FofoType;
import com.spice.profitmandi.common.enumuration.ItemType;
import com.spice.profitmandi.common.exception.ProfitMandiBusinessException;
import com.spice.profitmandi.common.model.PORowModel;
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.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.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.List;
import java.util.Map;
import java.util.Set;

/**
 * Receives stock into the destination warehouse for internal transfer invoices that were billed but never
 * GRNed. Per invoice it builds the rows the Excel GRN upload would have carried and hands them to
 * PurchaseOrderService.addPORowModels, which creates the supplier invoice, the purchase and the inventory
 * items in one call - the same path the portal uses, so nothing here reimplements receiving.
 *
 * Deliberately narrow: only INTERNAL buyers, only invoices not already received, and a serialized line with
 * no serial number aborts its invoice rather than creating stock that cannot be traced to a unit.
 */
@Component
public class InternalGrnTask {

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

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private ItemRepository itemRepository;

    @Autowired
    private FofoStoreRepository fofoStoreRepository;

    @Autowired
    private WarehousePurchaseOrderRepository warehousePurchaseOrderRepository;

    @Autowired
    private WarehouseSupplierInvoiceRepository warehouseSupplierInvoiceRepository;

    @Autowired
    private PurchaseOrderService purchaseOrderService;

    /**
     * Drives the run without holding a transaction of its own, so one invoice failing cannot roll back the
     * invoices already received - each invoice commits or rolls back on its own inside grnInvoice.
     */
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void grnInternalInvoices(List<String> invoiceNumbers, String operatorEmail, boolean dryRun) {
        LOGGER.info("=== Internal GRN {} : {} invoice(s), operator {} ===",
                dryRun ? "DRY RUN (no writes)" : "LIVE RUN", invoiceNumbers.size(), operatorEmail);
        int received = 0;
        int skipped = 0;
        int failed = 0;
        for (String invoiceNumber : invoiceNumbers) {
            try {
                if (grnInvoice(invoiceNumber.trim(), operatorEmail, dryRun)) {
                    received++;
                } else {
                    skipped++;
                }
            } catch (Exception e) {
                failed++;
                LOGGER.error("FAILED  {} - {}", invoiceNumber, e.getMessage(), e);
            }
        }
        LOGGER.info("=== Internal GRN {} complete : {} {}, {} skipped, {} failed ===",
                dryRun ? "DRY RUN" : "LIVE RUN", received, dryRun ? "would be received" : "received",
                skipped, failed);
    }

    /**
     * @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;
        }

        // An internal PO reaches its orders through the transaction it raised, not through order.purchase_order_id.
        Set<Integer> transactionIds = new HashSet<>();
        for (Order order : orders) {
            transactionIds.add(order.getTransactionId());
        }
        Set<Integer> poIds = new HashSet<>();
        WarehousePurchaseOrder purchaseOrder = null;
        for (Integer transactionId : transactionIds) {
            WarehousePurchaseOrder po = warehousePurchaseOrderRepository.selectByTransactionId(transactionId);
            if (po != null && poIds.add(po.getId())) {
                purchaseOrder = po;
            }
        }
        if (purchaseOrder == null) {
            LOGGER.info("SKIP    {} - no purchase order found for transaction(s) {}", invoiceNumber, transactionIds);
            return false;
        }
        // addPORowModels resolves a single PO for the whole map, so an invoice spanning POs cannot be trusted to it.
        if (poIds.size() > 1) {
            LOGGER.info("SKIP    {} - spans {} purchase orders {}", invoiceNumber, poIds.size(), poIds);
            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;
        }

        LocalDateTime receivedDate = LocalDateTime.now();
        List<PORowModel> rows = new ArrayList<>();
        BigDecimal value = BigDecimal.ZERO;
        int serials = 0;
        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;
            }

            String serialNumber = lineItem.getSerialNumber();
            boolean serialized = ItemType.SERIALIZED.equals(item.getType());
            // Receiving a serialized unit without its serial would create stock no scan could ever match.
            if (serialized && (serialNumber == null || serialNumber.trim().isEmpty())) {
                LOGGER.info("SKIP    {} - serialized item {} on order {} has no serial number",
                        invoiceNumber, item.getId(), order.getId());
                return false;
            }
            if (serialized) {
                serials++;
            }

            PORowModel row = new PORowModel();
            row.setPoNumber(purchaseOrder.getPoNumber());
            row.setInvoiceNumber(invoiceNumber);
            row.setInvoiceDate(order.getBillingTimestamp());
            row.setReceivedDate(receivedDate);
            row.setItemId(lineItem.getItemId());
            row.setQuantity(lineItem.getQuantity());
            row.setUnitPrice(lineItem.getUnitPrice());
            row.setImeiNumber(serialized ? serialNumber.trim() : null);
            rows.add(row);

            value = value.add(BigDecimal.valueOf(lineItem.getUnitPrice())
                    .multiply(BigDecimal.valueOf(lineItem.getQuantity())));
        }

        if (dryRun) {
            LOGGER.info("WOULD GRN {} - po {} (id {}), warehouse {}, supplier {}, {} line(s), {} serialized, value {}, receivedDate {}",
                    invoiceNumber, purchaseOrder.getPoNumber(), purchaseOrder.getId(), purchaseOrder.getWarehouseId(),
                    purchaseOrder.getSupplierId(), rows.size(), serials, value.toPlainString(), receivedDate);
            for (PORowModel row : rows) {
                LOGGER.info("          item {} qty {} price {} serial {}",
                        row.getItemId(), row.getQuantity(), row.getUnitPrice(),
                        row.getImeiNumber() == null ? "-" : row.getImeiNumber());
            }
            return true;
        }

        // One PO and one invoice per call - addPORowModels applies the first PO it resolves to every entry.
        Map<String, Map<String, List<PORowModel>>> poRowModelsMap = Collections.singletonMap(
                purchaseOrder.getPoNumber(), Collections.singletonMap(invoiceNumber, rows));
        purchaseOrderService.addPORowModels(operatorEmail, poRowModelsMap);

        LOGGER.info("GRNED   {} - po {}, warehouse {}, {} line(s), {} serialized, value {}",
                invoiceNumber, purchaseOrder.getPoNumber(), purchaseOrder.getWarehouseId(),
                rows.size(), serials, value.toPlainString());
        return true;
    }
}