PrestaShop Customisation

In this page I demonstrate how to customise PrestaShop to generate Stock Transfer slip.

First of all we need to create a module. Let PRESTASHOP_DIR be the PrestaShop installation directory. Create a directory under PRESTASHOP_DIR/modules called 'transfer'.

Secondly create the module file called transfer.php as below. Instead of subclassing Module class we will subclass the StockManagerModuleCore as this class already implements stockManager hook. For the Transfer Slip generation purpose we require two additional tables namely ps_transfer and ps_transfer_movement. The former generates a unique ID (Transfer ID) for the Transfer Slip whereas the latter associates the Transfer ID to the corresponding Stock Movement IDs (id_stock_mvt).

<?php
if (defined('_PS_VERSION_'))
{
    // Continue
}
else
{
    exit;
}

class Transfer extends StockManagerModuleCore
{
    public function __construct()
    {
        $this->name = 'transfer';
        $this->version = '1.0';
        $this->author = 'Eki Baskoro';
        $this->need_instance = 0;

        parent::__construct();

        $this->displayName = $this->l('Transfer Module');
        $this->description = $this->l('Enable transfer slip.');
        $this->confirmUninstall = $this->l('Are you sure you want to uninstall?');
        $this->stock_manager_class = 'MyStockManager';
    }

    public function install()
    {
        $sql = "CREATE TABLE IF NOT EXISTS`"._DB_PREFIX_."transfer` (`id_transfer` BIGINT(20) NOT NULL AUTO_INCREMENT, PRIMARY KEY(`id_transfer`))";
        $created = Db::getInstance()->Execute($sql);

        if ($created)
        {
            $sql = "CREATE TABLE IF NOT EXISTS`"._DB_PREFIX_."transfer_movement` (`id_transfer` BIGINT(20) NOT NULL, `id_stock_mvt` BIGINT(20), PRIMARY KEY (`id_transfer`, `id_stock_mvt`))";
            $created = Db::getInstance()->Execute($sql);

            if ($created)
            {
                $installed = parent::install();
                return $installed;
            }
            else
            {
                return false;
            }
        }
        else
        {
            return false;
        }
    }
    
    public function uninstall()
    {
        $sql = "DROP TABLE `"._DB_PREFIX_."transfer_movement`";
        $dropped = Db::getInstance()->Execute($sql);

        if ($dropped)
        {
            $sql = "DROP TABLE `"._DB_PREFIX_."transfer`";
            $dropped = Db::getInstance()->Execute($sql);

            if ($dropped)
            {
                $uninstalled = parent::uninstall();
                return $uninstalled;
            }
            else
            {
                return false;
            }
        }
        else
        {
            return false;
        }
    }
}
?>

Next we need to subclass the StockManagerCore. Create a file called MyStockManager.php as below. We override the transferBetweenWarehouses method to use our versions of removeProduct and addProduct methods which are replaced by takeProduct and transferProduct methods respectively. These methods collect the Stock Movement IDs which we then save to the ps_transfer_movement tables accordingly.

<?php
class MyStockManager extends StockManagerCore
{
    public function transferBetweenWarehouses($id_product, $id_product_attribute, $quantity, $id_warehouse_from, $id_warehouse_to, $usable_from = true, $usable_to = true)
    {
        $quantityToTransfer = $quantity;
        $quantityInSource = $this->getProductPhysicalQuantities($id_product, $id_product_attribute, array($id_warehouse_from), $usable_from);

        if ($quantityInSource >= $quantityToTransfer)
        {
            $noMovement = (($id_warehouse_from == $id_warehouse_to)
                && ($usable_from == $usable_to));

            if ($noMovement)
            {
                return false;
            }
            else
            {
                // Check if the given warehouses are available
                $source = new Warehouse($id_warehouse_from);
                $destination = new Warehouse($id_warehouse_to);

                $warehousesExist = Validate::isLoadedObject($source)
                    && Validate::isLoadedObject($destination);

                if ($warehousesExist)
                {
                    // Take stock from source
                    $stocks = $this->takeProduct($id_product, $id_product_attribute, $source, $quantityToTransfer, $usable_from);

                    $stockTakeOk = (count($stocks) > 0);

                    if ($stockTakeOk)
                    {
                        // Add stock to destination
                        foreach ($stocks as $stock)
                        {
                            $price = $stock['price_te'];

                            // Convert product price if necessary
                            $sourceCurrency = $source->id_currency;
                            $destinationCurrency = $destination->id_currency;
                            $conversionRequired = ($sourceCurrency != $destinationCurrency);

                            if ($conversionRequired)
                            {
                                $priceInDefaultCurrency = Tools::convertPrice($price, $sourceCurrency, false);
                                $price = Tools::convertPrice($priceInDefaultCurrency, $destinationCurrency, true);
                            }
                            else
                            {
                                // No need to worry then
                            }

                            $result = $this->transferProduct($id_product, $id_product_attribute, $destination, $stock['quantity'], $price, $usable_to);

                            $transferOk = ($result != false);

                            if ($transferOk)
                            {
                                $movementIds = array();

                                foreach ($stock['movementIds'] as $movementId)
                                {
                                    array_push($movementIds, $movementId);
                                }

                                $movementIds[] = $result;

                                $sql = 'INSERT INTO `'._DB_PREFIX_.'transfer` VALUES (DEFAULT)';
                                $inserted = Db::getInstance()->Execute($sql);

                                if ($inserted)
                                {
                                    $transferId = Db::getInstance()->Insert_ID();

                                    foreach ($movementIds as $movementId)
                                    {
                                        $sql = 'INSERT INTO `'._DB_PREFIX_.'transfer_movement` VALUES ('.$transferId.','.$movementId.')';
                                        $inserted = Db::getInstance()->Execute($sql);

                                        if ($inserted)
                                        {
                                            // Keep going
                                        }
                                        else
                                        {
                                            return false;
                                        }
                                    }
                                }
                                else
                                {
                                    return false;
                                }
                            }
                            else
                            {
                                return false;
                            }
                        }

                        return true;
                    }
                    else
                    {
                        return false;
                    }
                }
                else
                {
                    return false;
                }
            }
        }
        else
        {
            return false;
        }
    }

    public function takeProduct($id_product, $id_product_attribute = null, Warehouse $warehouse, $quantity, $is_usable = true, $id_order = null)
    {
        $takePossible = Validate::isLoadedObject($warehouse)
            && $quantity
            && $id_product;

        if ($takePossible)
        {
            $context = Context::getContext();

            // Special case of a pack
            if (Pack::isPack((int)$id_product))
            {
                // Gets items
                $language = Configuration::get('PS_LANG_DEFAULT');
                $productsPack = Pack::getItems((int)$id_product, $language);

                foreach ($productsPack as $productPack)
                {
                    $productId = $productPack->id;
                    $productAttribute = Product::getDefaultAttribute($productId, 1);
                    $advancedStockManagementEnabled = ($productPack->advanced_stock_management == 1);

                    if ($advancedStockManagementEnabled)
                    {
                        $packQuantity = $productPack->pack_quantity;
                        $this->takeProduct($productId, $productAttribute, $warehouse, $packQuantity * $quantity, $is_usable, $id_order);
                    }
                    else
                    {
                        // Do nothing
                    }
                }
            }
            else
            {
                // Gets total quantities in stock for the current product
                $warehouseIds = array($warehouse->id);
                $physicalQuantity = (int)$this->getProductPhysicalQuantities($id_product, $id_product_attribute, $warehouseIds, false);
                $usableQuantity = (int)$this->getProductPhysicalQuantities($id_product, $id_product_attribute, $warehouseIds, true);

                // Check quantity if we want to decrement unusable quantity
                if ($is_usable)
                {
                    $quantityInStock = $usableQuantity;
                }
                else
                {
                    $quantityInStock = $physicalQuantity - $usableQuantity;
                }

                // Checks if it is possible to remove the given quantity
                $quantityToTake = $quantity;

                if ($quantityToTake < $quantityInStock)
                {
                    $warehouseId = $warehouse->id;
                    $stockCollection = $this->getStockCollection($id_product, $id_product_attribute, $warehouseId);
                    $stockCollection->getAll();
                    $hasCollection = (count($stockCollection) > 0);

                    if ($hasCollection)
                    {
                        $stockHistory = array();
                        $movementParameters = array();
                        $stockParameters = array();
                        $quantityToDecrement = array();
                        $globalQuantityToDecrement = array();
                        $movementReason = Configuration::get('PS_STOCK_MVT_TRANSFER_FROM');
                        $result = array();

                        // Switch on MANAGEMENT_TYPE
                        $managementType = $warehouse->management_type;

                        switch ($managementType)
                        {
                            // Case CUMP mode
                            case 'WA':
                                // There is one and only one stock for a given
                                // product in a warehouse in this mode
                                $stock = $stockCollection->current();
                                $movementParameters = array(
                                    'id_stock' => $stock->id,
                                    'physical_quantity' => $quantity,
                                    'id_stock_mvt_reason' => $movementReason,
                                    'id_order' => $id_order,
                                    'price_te' => $stock->price_te,
                                    'last_wa' => $stock->price_te,
                                    'current_wa' => $stock->price_te,
                                    'id_employee' => $context->employee->id,
                                    'employee_firstname' => $context->employee->firstname,
                                    'employee_lastname' => $context->employee->lastname,
                                    'sign' => -1
                                );

                                $stockParameters = array(
                                    'physical_quantity' => ($stock->physical_quantity - $quantity),
                                    'usable_quantity' => ($is_usable? ($stock->usable_quantity - $quantity) : $stock->usable_quantity)
                                );

                                // Saves stock in warehouse
                                $stock->hydrate($stockParameters);
                                $stock->update();

                                // Saves stock movement
                                $stockMovement = new StockMvt();
                                $stockMovement->hydrate($movementParameters);
                                $stockMovement->save();

                                $result[$stock->id]['quantity'] = $quantity;
                                $result[$stock->id]['price_te'] = $stock->price_te;
                                $result[$stock->id]['movementIds'][] = $stockMovement->id;
                                
                                break;

                            case 'LIFO':
                            case 'FIFO':
                                // For each stock, parse its movment history
                                // to calculate the quantities left for each
                                // positive movement, according to the instant
                                // available quantities for this stock
                                foreach ($stockCollection as $stock)
                                {
                                    $quantityLeft = $stock->physical_quantity;

                                    if ($quantityLeft <= 0)
                                    {
                                        continue;
                                    }
                                    else
                                    {
                                        // Perform calculation
                                    }

                                    $query = 'SELECT sm.`id_stock_mvt`
                                        ,sm.`date_add`
                                        ,sm.`physical_quantity`
                                        ,IF ((sm2.`physical_quantity` IS NULL), sm.`physical_quantity`, (sm.`physical_quantity` - SUM(sm2.`physical_quantity`))) AS qty
                                        FROM `'._DB_PREFIX_.'stock_mvt` sm
                                        LEFT JOIN `'._DB_PREFIX_.'stock_mvt` sm2 ON sm2.`referer` = sm.`id_stock_mvt`
                                        WHERE sm.`sign` = 1
                                        AND sm.`id_stock` = '.(int)$stock->id.'
                                        GROUP BY sm.`id_stock_mvt`
                                        ORDER BY sm.`date_add` DESC';
                                    $resource = Db::getInstance(_PS_USE_SQL_SLAVE_)->query($sql);

                                    while ($row = Db::getInstance()->nextRow($resource))
                                    {
                                        // break - in FIFO mode we have to 
                                        // retrieve the oldest positive
                                        // movements for which there are left
                                        // quantities
                                        if (($managementType =='FIFO')
                                            && ($row['qty'] == 0))
                                        {
                                            break;
                                        }
                                        else
                                        {
                                            // Continue
                                        }

                                        // Converts date to timestamp
                                        $addedDate = $row['date_add'];
                                        $date = new DateTime($addedDate);
                                        $timestamp = $date->format('U');

                                        // History of the movement
                                        $movementId = $row['id_stock_mvt'];
                                        $stockHistory[$timestamp] = array(
                                            'id_stock' => $stock->id,
                                            'id_stock_mvt' => (int)$movementId,
                                            'qty' => (int)$row['qty']
                                        );

                                        // break - in LIFO mode checks only
                                        // the necessary history to handle the
                                        // global quantity for the current
                                        // stock
                                        if ($managementType == 'LIFO')
                                        {
                                            $quantityLeft -= (int)
                                            $row['physical_quantity'];

                                            if ($quantityLeft <= 0)
                                            {
                                                break;
                                            }
                                            else
                                            {
                                                // Continue
                                            }
                                        }
                                        else
                                        {
                                            // Do nothing special
                                        }
                                    }
                                }

                                if ($managementType == 'LIFO')
                                {
                                    // Orders stock history by timestamp to
                                    // get newest history first
                                    krsort($stockHistory);
                                }
                                else
                                {
                                    // Orders stock history by timestamp to
                                    // get oldest history first
                                    ksort($stockHistory);
                                }

                                // Check each stock to manage the real 
                                // quantity to decrement for each one of them
                                foreach ($stockHistory as $history)
                                {
                                    if ($history['qty'] >= 
                                        $globalQuantityToDecrement)
                                    {
                                        $stockId = $history['id_stock'];
                                        $movementId = $history['id_stock_mvt'];
                                        $quantityToDecrement[$stockId][$movementId] = $globalQuantityToDecrement;
                                        $globalQuantityToDecrement = 0;
                                    }
                                    else
                                    {
                                        $quantityToDecrement[$stockId][$movementId] = $history['qty'];
                                        $globalQuantityToDecrement -= $history['qty'];
                                    }

                                    if ($globalQuantityToDecrement <= 0)
                                    {
                                        break;
                                    }
                                    else
                                    {
                                        // Continue
                                    }
                                }

                                // For each stock decrement it and log the
                                // movement
                                foreach ($stockCollection as $stock)
                                {
                                    if (array_key_exists($stock->id, $quantityToDecrement)
                                        && is_array($quantityToDecrement[$stock_id]))
                                    {
                                        $totalQuantity = 0;

                                        foreach ($quantityToDecrement[$stock->id] as $id_mvt_referrer => $qte)
                                        {
                                            $movementParameters = array(
                                                'id_stock' => $stock->id,
                                                'physical_quantity' => $qte,
                                                'id_stock_mvt_reason' => $movementReason,
                                                'id_order' => $id_order,
                                                'price_te' => $stock->price_te,
                                                'sign' => -1,
                                                'referer' => $id_mvt_referrer,
                                                'id_employee' => $context->employee->id
                                            );

                                            // Saves stock movement
                                            $stockMovement = new StockMvt();
                                            $stockMovement->hydrate(
                                            $movementParameters);
                                            $stockMovement->save();

                                            $result[$stock->id]['movementIds'][] = $stockMovement->id;

                                            $totalQuantity += $qte;
                                        }

                                        $stockParameters = array(
                                            'physical_quantity' => ($stock->physical_quantity - $totalQuantity),
                                            'usable_quantity' => ($is_usable? ($stock->usable_quantity - $totalQuantity) : $stock->usable_quantity)
                                        );

                                        $result[$stock->id]['quantity'] = $totalQuantity;
                                        $result[$stock->id]['price_te'] = $stock->price_te;

                                        // Saves stock in warehouse
                                        $stock->hydrate($stockParameters);
                                        $stock->update();
                                    }
                                    else
                                    {
                                    // Do nothing
                                    }
                                }

                                break;
                        }
                    }
                    else
                    {
                        return array();
                    }

                    // If we remove a usable quantity execute hook
                    if ($is_usable)
                    {
                        Hook::exec('actionProductCoverage', array(
                            'id_product' => $id_product,
                            'id_product_attribute' => $id_product_attribute,
                            'warehouse' => $warehouse)
                        );
                    }
                    else
                    {
                        // No hook to be executed
                    }

                    return $result;
                }
                else
                {
                    return array();
                }
            }
        }
        else
        {
            return array();
        }
    }

    public function transferProduct($id_product, $id_product_attribute = 0, Warehouse $warehouse, $quantity, $price_te, $is_usable = true, $id_supply_order = null)
    {
        $transferPossible = Validate::isLoadedObject($warehouse)
            && $price_te
            && $quantity
            && $id_product;

        if ($transferPossible)
        {
            $price_te = (float)round($price_te, 6);
            $context = Context::getContext();
            $movementReason = Configuration::get('PS_STOCK_MVT_TRANSFER_TO');
            $movementParameters = array(
                'id_stock' => null,
                'physical_quantity' => $quantity,
                'id_stock_mvt_reason' => $movementReason,
                'id_supply_order' => $id_supply_order,
                'price_te' => $price_te,
                'last_wa' => null,
                'current_wa' => null,
                'id_employee' => $context->employee->id,
                'employee_firstname' => $context->employee->firstname,
                'employee_lastname' => $context->employee->lastname,
                'sign' => 1
            );
            $stockExists = false;
            $managementType = $warehouse->management_type;

            switch ($managementType)
            {
                case 'WA':
                    $stockCollection = $this->getStockCollection($id_product, $id_product_attribute, $warehouse->id);

                    if (count($stockCollection) > 0)
                    {
                        $stockExists = true;
                        $stock = $stockCollection->current();
                        $last_wa = $stock->price_te;
                        $current_wa = $this->calculateWA($stock, $quantity, $price_te);

                        $movementParameters['id_stock'] = $stock->id;
                        $movementParameters['last_wa'] = $last_wa;
                        $movementParameters['current_wa'] = $current_wa;

                        $stockParameters = array(
                            'physical_quantity' => ($stock->physical_quantity + $quantity),
                            'price_te' => $current_wa,
                            'usable_quantity' => ($is_usable? ($stock->usable_quantity + $quantity) : $stock->usable_quantity),
                            'id_warehouse' => $warehouse->id
                        );

                        // Saves stock in warehouse
                        $stock->hydrate($stockParameters);
                        $stock->update();
                    }
                    else
                    {
                        $stockExists = false;

                        $movementParameters['last_wa'] = 0;
                        $movementParameters['current_wa'] = $price_te;
                    }

                break;

                case 'FIFO':
                case 'LIFO':
                    $stockCollection = $this->getStockCollection($id_product, $id_product_attribute, $warehouse->id, $price_te);

                    if (count($stockCollection) > 0)
                    {
                        $stockExists = true;
                        $stock = $stockCollection->current();
                        $stockParameters = array(
                            'physical_quantity' => ($stock->physical_quantity + $quantity),
                            'usable_quantity' => ($is_usable? ($stock->usable_quantity + $quantity) : $stock->usable_quantity)
                        );

                        // Update stock in warehouse
                        $stock->hydrate($stockParameters);
                        $stock->update();

                        $movementParameters['id_stock'] = $stock->id;
                    }
                    else
                    {
                        // Do nothing
                    }

                break;

                default:
                    return false;
            }

            if ($stockExists)
            {
                // Do nothing
            }
            else
            {
                $stock = new Stock();
                $stockParameters = array(
                    'id_product_attribute' => $id_product_attribute,
                    'id_product' => $id_product,
                    'physical_quantity' => $quantity,
                    'price_te' => $price_te,
                    'usable_quantity' => ($is_usable? $quantity : 0),
                    'id_warehouse' => $warehouse->id
                );

                // Save stock in warehouse
                $stock->hydrate($stockParameters);
                $stock->add();
                $movementParameters['id_stock'] = $stock->id;
            }

            // Saves stock movement
            $stockMovement = new StockMvt();
            $stockMovement->hydrate($movementParameters);
            $stockMovement->add();

            return $stockMovement->id;
        }
        else
        {
            return false;
        }
    }
}
?>

Next we need to create the Transfer Slip PDF. To be continued...
Comments