Current File : /var/www/vinorea/modules/tvcmsstockinfo/tvcmsstockinfo.php
<?php
/**
 * 2007-2025 PrestaShop.
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Academic Free License (AFL 3.0)
 * that is bundled with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://opensource.org/licenses/afl-3.0.php
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@prestashop.com so we can send you a copy immediately.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
 * versions in the future. If you wish to customize PrestaShop for your
 * needs please refer to http://www.prestashop.com for more information.
 *
 *  @author    PrestaShop SA <contact@prestashop.com>
 *  @copyright 2007-2025 PrestaShop SA
 *  @license   http://opensource.org/licenses/afl-3.0.php  Academic Free License (AFL 3.0)
 *  International Registered Trademark & Property of PrestaShop SA
 */
if (!defined('_PS_VERSION_')) {
    exit;
}

class TvcmsStockInfo extends Module
{
    // show design Types
    protected static $indicatorDesigns = ['none', 'bar', 'bar.sm', 'bar2', 'signal', 'battery', 'ring', 'pie'];

    // module position
    protected static $productDisplayTypes = ['old_price', 'price', 'after_price'];

    // Varible List
    protected $configs = [
        'TV_DISPLAY_WHEN_BELOW_LIMIT' => 0,
        'TV_DISPLAY_IN_CATS' => '',
        'TV_DISPLAY_IN_PRODUCT_LISTS' => true,
        'TV_DISPLAY_ON_PRODUCT_PAGE' => true,
        'TV_DISPLAY_NR_OF_ITEMS' => true,
        'TV_INDICATOR_FULL_AT' => 300,
        'TV_INDICATOR_DESIGN' => 'bar',
        'TV_ENABLE_PROGRESSIVE_COLORS' => true,
    ];

    // Hook List
    protected $hooks = [
        'displayBackOfficeHeader',
        'displayProductListReviews',
        'displayProductPriceBlock',
        'header',
    ];

    public function __construct()
    {
        $this->name = 'tvcmsstockinfo';
        $this->tab = 'front_office_features';
        $this->version = '4.0.0';
        $this->author = 'ThemeVolty';
        $this->need_instance = 0;

        $this->bootstrap = true;
        parent::__construct();

        $this->displayName = $this->l('ThemeVolty - Stock Indicator');
        $this->description = $this->l('Display stock indicator on your products.');

        $this->ps_versions_compliancy = ['min' => '1.7', 'max' => _PS_VERSION_];
        $this->module_key = '';

        $this->confirmUninstall = $this->l('Warning: all the data saved in your database will be deleted.' .
            ' Are you sure you want uninstall this module?');
    }

    public function createVariable()
    {
        $all_categories = Category::getAllCategoriesName();
        unset($all_categories[0]);
        unset($all_categories[1]);

        $i = 1;
        $category_list = [];
        foreach ($all_categories as $category) {
            $category_list[] = $category['id_category'];
            ++$i;
        }

        $category_list = implode(',', $category_list);

        Configuration::updateValue('TV_DISPLAY_WHEN_BELOW_LIMIT', 0);
        Configuration::updateValue('TV_DISPLAY_IN_CATS', $category_list);
        Configuration::updateValue('TV_DISPLAY_IN_PRODUCT_LISTS', true);
        Configuration::updateValue('TV_DISPLAY_ON_PRODUCT_PAGE', true);
        Configuration::updateValue('TV_DISPLAY_NR_OF_ITEMS', 'true');
        Configuration::updateValue('TV_INDICATOR_FULL_AT', 300);
        Configuration::updateValue('TV_INDICATOR_DESIGN', 'bar');
        Configuration::updateValue('TV_ENABLE_PROGRESSIVE_COLORS', true);
    }

    public function install()
    {
        $this->installTab();
        $this->createVariable();

        return parent::install()
            && $this->registerHook('displayBackOfficeHeader')
            && $this->registerHook('displayHeader')
            && $this->registerHook('displayProductListStockIndicator')
            && $this->registerHook('displayProductPageStockIndicator');
        // && $this->registerHook('displayProductListReviews');
        // && $this->registerHook('displayProductPriceBlock');
    }

    public function installTab()
    {
        $response = true;

        // First check for parent tab
        $parentTabID = Tab::getIdFromClassName('AdminThemeVolty');

        if ($parentTabID) {
            $parentTab = new Tab($parentTabID);
        } else {
            $parentTab = new Tab();
            $parentTab->active = 1;
            $parentTab->name = [];
            $parentTab->class_name = 'AdminThemeVolty';
            foreach (Language::getLanguages() as $lang) {
                $parentTab->name[$lang['id_lang']] = 'ThemeVolty Extension';
            }
            $parentTab->id_parent = 0;
            $parentTab->module = $this->name;
            $response &= $parentTab->add();
        }

        // Check for parent tab2
        $parentTab_2ID = Tab::getIdFromClassName('AdminThemeVoltyModules');
        if ($parentTab_2ID) {
            $parentTab_2 = new Tab($parentTab_2ID);
        } else {
            $parentTab_2 = new Tab();
            $parentTab_2->active = 1;
            $parentTab_2->name = [];
            $parentTab_2->class_name = 'AdminThemeVoltyModules';
            foreach (Language::getLanguages() as $lang) {
                $parentTab_2->name[$lang['id_lang']] = 'ThemeVolty Configure';
            }
            $parentTab_2->id_parent = $parentTab->id;
            $parentTab_2->module = $this->name;
            $response &= $parentTab_2->add();
        }
        // Created tab
        $tab = new Tab();
        $tab->active = 1;
        $tab->class_name = 'Admin' . $this->name;
        $tab->name = [];
        foreach (Language::getLanguages() as $lang) {
            $tab->name[$lang['id_lang']] = 'Stock Indicator';
        }
        $tab->id_parent = $parentTab_2->id;
        $tab->module = $this->name;
        $response &= $tab->add();

        return $response;
    }

    public function uninstall()
    {
        $this->uninstallTab();

        $this->deleteVariable();

        return parent::uninstall();
    }

    public function deleteVariable()
    {
        Configuration::deleteByName('TV_DISPLAY_WHEN_BELOW_LIMIT');
        Configuration::deleteByName('TV_DISPLAY_IN_CATS');
        Configuration::deleteByName('TV_DISPLAY_IN_PRODUCT_LISTS');
        Configuration::deleteByName('TV_DISPLAY_ON_PRODUCT_PAGE');
        Configuration::deleteByName('TV_DISPLAY_NR_OF_ITEMS');
        Configuration::deleteByName('TV_INDICATOR_FULL_AT');
        Configuration::deleteByName('TV_INDICATOR_DESIGN');
        Configuration::deleteByName('TV_ENABLE_PROGRESSIVE_COLORS');
    }

    public function uninstallTab()
    {
        $id_tab = Tab::getIdFromClassName('Admin' . $this->name);
        $tab = new Tab($id_tab);
        $tab->delete();

        return true;
    }

    public function reset()
    {
        return $this->uninstall()
            && $this->install();
    }

    public function getContent()
    {
        $content = false === Tools::isSubmit('settings') ? '' : $this->processSettingsForm();

        $content .= $this->renderSettingsForm();
        $this->context->smarty->assign(['mainContent' => $content]);

        return $this->display(__FILE__, 'views/templates/admin/master.tpl');
    }

    // Create Form
    protected function formFactory($fields = [])
    {
        $helper = new HelperForm();

        // General settings
        $helper->show_toolbar = false;
        $helper->table = $this->table;
        $helper->module = $this;
        $helper->default_form_language = $this->context->language->id;
        $helper->allow_employee_form_lang = Configuration::get('PS_BO_ALLOW_EMPLOYEE_FORM_LANG', 0);

        $helper->identifier = $this->identifier;
        $helper->currentIndex = $this->context->link->getAdminLink('AdminModules', false)
             . '&configure=' . $this->name . '&tab_module=' . $this->tab . '&module_name=' . $this->name;
        $helper->token = Tools::getAdminTokenLite('AdminModules');

        foreach ($fields as $name => $value) {
            $helper->$name = $value;
        }

        if (!array_key_exists('languages', $helper->tpl_vars)) {
            $helper->tpl_vars['languages'] = $this->context->controller->getLanguages();
        }

        if (!array_key_exists('id_language', $helper->tpl_vars)) {
            $helper->tpl_vars['id_language'] = $this->context->language->id;
        }

        return $helper;
    }

    // get variable's value for form
    protected function renderSettingsForm()
    {
        $helper = $this->formFactory([
            'submit_action' => 'settings',
            'tpl_vars' => ['fields_value' => $this->getSettingsFormValues()],
        ]);

        return $helper->generateForm(
            [['form' => $this->getSettingsForm()]]
        );
    }

    protected function getSettingsFormValues()
    {
        $configs = [];

        // Skip configurations that can't be used
        // when assigning them to FormHelper "fields_value" field.
        $skip = ['TV_DISPLAY_IN_CATS'];

        foreach ($this->configs as $config => $default) {
            if (in_array($config, $skip)) {
                continue;
            }

            $value = Configuration::get($config);

            $configs[$config] = false !== $value ? $value : $default;
        }

        return $configs;
    }

    protected function getSettingsForm()
    {
        $this->context->smarty->assign($this->getCommonIndicatorViewVars());

        $TV_DISPLAY_NR_OF_ITEMS_DESC = $this->l('If the number of items is not displayed, you must select '
             . 'a visible stock indicator design . ');
        $TV_INDICATOR_FULL_AT_DESC = $this->l('The indicator will be displayed as full when the product '
             . 'stock is at the specified value . ');

        $all_category = Category::getAllCategoriesName();
        $value = [];

        foreach ($all_category as $category) {
            $value['id'] = $category['id_category'];
            $value['use_checkbox'] = true;

            $tmp = Configuration::get('TV_DISPLAY_IN_CATS');
            // $tmp = str_replace("[", "", $tmp);
            // $tmp = str_replace("]", "", $tmp);

            $selected_categories = explode(',', $tmp);
            $value['selected_categories'] = $selected_categories;
        }

        return [
            'legend' => ['title' => $this->l('Settings'), 'icon' => 'icon-cog'],
            'input' => [
                [
                    'type' => 'switch',
                    'label' => $this->l('Display indicator on the product page'),
                    'name' => 'TV_DISPLAY_ON_PRODUCT_PAGE',
                    'values' => $this->getYesOrNoValues(),
                    'col' => 3,
                ],
                [
                    'type' => 'switch',
                    'label' => $this->l('Display Indicator In Product Listings'),
                    'name' => 'TV_DISPLAY_IN_PRODUCT_LISTS',
                    'values' => $this->getYesOrNoValues(),
                    'col' => 3,
                ],
                [
                    'type' => 'categories',
                    'label' => $this->l('Display Indicator For Products In These Categories'),
                    'name' => 'TV_DISPLAY_IN_CATS',
                    'col' => 6,
                    'tree' => $value,
                ],
                [
                    'type' => 'switch',
                    'label' => $this->l('Display Number Of Items'),
                    'desc' => $TV_DISPLAY_NR_OF_ITEMS_DESC,
                    'name' => 'TV_DISPLAY_NR_OF_ITEMS',
                    'values' => $this->getYesOrNoValues(),
                    'col' => 3,
                ],
                [
                    'type' => 'text',
                    'label' => $this->l('Display Indicator When Stock Is Below'),
                    'desc' => $this->l('If 0 is specified, stock indicator will always be displayed . '),
                    'hint' => $this->l('Display indicator when product stock is below the specified value . '),
                    'name' => 'TV_DISPLAY_WHEN_BELOW_LIMIT',
                    'col' => 3,
                ],
                [
                    'type' => 'text',
                    'label' => $this->l('Mark Indicator As Full When Product Stock Is At'),
                    'desc' => $TV_INDICATOR_FULL_AT_DESC,
                    'name' => 'TV_INDICATOR_FULL_AT',
                    'col' => 3,
                ],
                [
                    'type' => 'design',
                    'label' => $this->l('Stock Indicator Design'),
                    'name' => 'TV_INDICATOR_DESIGN',
                    'values' => $this->getIndicatorDesigns(),
                ],
                [
                    'type' => 'switch',
                    'label' => $this->l('Enable Progressive Colors For Stock Levels'),
                    'name' => 'TV_ENABLE_PROGRESSIVE_COLORS',
                    'values' => $this->getYesOrNoValues(),
                    'col' => 3,
                ],
            ],
            'submit' => ['title' => $this->l('Save')],
        ];
    }

    protected function getYesOrNoValues()
    {
        static $switchValues;

        if (!$switchValues) {
            $switchValues = [
                ['id' => 'active_on', 'value' => 1, 'label' => $this->l('Yes')],
                ['id' => 'active_off', 'value' => 0, 'label' => $this->l('No')],
            ];
        }

        return $switchValues;
    }

    protected function processSettingsForm()
    {
        if (count($errors = $this->validateSettingsForm()) > 0) {
            $alerts = '';

            foreach ($errors as $error) {
                $alerts .= $this->displayError($error);
            }

            return $alerts;
        }

        $successMessage = $this->l('Configuration updated . ');
        $errorMessage = $this->l('Something went wrong, please try again . ');

        foreach (array_keys($this->configs) as $config) {
            $value = Tools::getValue($config);

            if ('TV_DISPLAY_IN_CATS' == $config) {
                if (!empty($value)) {
                    $value = implode(',', $value);
                    Configuration::updateValue($config, $value);
                } else {
                    $value = '';
                    Configuration::updateValue($config, $value);
                }
            } elseif ('TV_DISPLAY_WHEN_BELOW_LIMIT' == $config || 'TV_INDICATOR_FULL_AT' == $config) {
                if (!Configuration::updateValue($config, (int) $value)) {
                    return $this->displayError($errorMessage);
                }
            } else {
                if (!Configuration::updateValue($config, $value)) {
                    return $this->displayError($errorMessage);
                }
            }
        }

        return $this->displayConfirmation($successMessage);
    }

    protected function validateSettingsForm()
    {
        $errors = [];

        foreach (array_keys($this->configs) as $config) {
            $value = Tools::getValue($config);

            if ('TV_DISPLAY_IN_CATS' == $config) {
                // If no category was selected, "0" is returned, otherwise check if is array.
                if (0 != $value && !is_array($value)) {
                    $errors[] = $this->l('Invalid selected categories . ');
                }
            }

            if ('TV_DISPLAY_WHEN_BELOW_LIMIT' == $config && !Validate::isUnsignedInt($value)) {
                $errors[] = $this->l('Show below limit must be a positive number . ');
            }

            if ('TV_INDICATOR_FULL_AT' == $config) {
                if (!Validate::isInt($value)) {
                    $errors[] = $this->l('Mark indicator as full must be a number . ');
                } elseif ((int) $value < 5) {
                    $errors[] = $this->l('Mark indicator as full can\'t be lower than 5 . ');
                }
            }

            if ('TV_INDICATOR_DESIGN' == $config) {
                if (!in_array($value, $this->getIndicatorDesigns())
                    || (!(bool) Tools::getValue('TV_DISPLAY_NR_OF_ITEMS') && 'none' == $value)) {
                    $errors[] = $this->l('Invalid stock indicator design . ');
                }
            }
        }

        return $errors;
    }

    protected function getIndicatorDesigns()
    {
        return static::$indicatorDesigns;
    }

    protected function getProductPageDisplayTypes()
    {
        return static::$productDisplayTypes;
    }

    public function hookDisplayBackOfficeHeader($params)
    {
        if (!$this->isModuleAdminPage()) {
            return '';
        }
        if ($this->name == Tools::getValue('configure')) {
            $this->context->controller->addCSS([
               $this->getPathUri() . 'views/css/indicators.css',
               $this->getPathUri() . 'views/css/admin.css',
            ]);
        }
    }

    protected function isModuleAdminPage()
    {
        if ($this->context->controller instanceof AdminModulesController
            && (Tools::getValue('configure') == $this->name || Tools::getValue('module_name') == $this->name)) {
            return true;
        }

        return false;
    }

    public function hookHeader($params)
    {
        $this->context->controller->addCSS([
            $this->getPathUri() . 'views/css/indicators.css',
            $this->getPathUri() . 'views/css/front.css',
        ]);

        // $this->context->controller->addJS(array(
        //     $this->getPathUri() . 'views/js/front.js',
        // ));
    }

    public function hookdisplayProductListStockIndicator($params)
    {
        if (!$this->isDisplayableInProductList()
            || ($this->isCategoryPage() && !$this->isDisplayableInCurrentCategory())) {
            return '';
        }

        $product = $params['product'];

        $productId = (int) $product['id_product'];
        $quantity = (int) $product['quantity_all_versions'];
        $hasMixedQty = 0 !== (int) $product['cache_default_attribute'];

        if (!$this->isStockBelowLimit($quantity)) {
            return '';
        }

        return $this->context->smarty
            ->assign($this->getIndicatorViewVars($productId, $quantity, $hasMixedQty))
            ->fetch($this->getLocalPath() . 'views/templates/front/hook/product_list.tpl');
    }

    public function hookdisplayProductPageStockIndicator($params)
    {
        if (!$this->isDisplayableOnProductPage()) {
            return '';
        }

        $product = $params['product'];

        // In 1.6 this hook is used in the product list as well...
        // we don't want that, to distinguish between product list and product page
        // the product param is an object only when the hook is used on the product page.
        if (Tools::version_compare(_PS_VERSION_, '1.7', '<')) {
            if (!is_object($product)) {
                return;
            }

            // Product object quantity includes "all versions".
            $productId = (int) $product->id;
            $quantity = (int) $product->quantity;
            $hasMixedQty = 0 !== (int) $product->cache_default_attribute;
        } else {
            $productId = (int) $product['id'];
            $quantity = (int) $product['quantity'];

            // For 1.7, product combination quantities are loaded individually.
            $hasMixedQty = false;
        }

        if (!$this->isStockBelowLimit($quantity)) {
            return;
        }

        if (Tools::version_compare(_PS_VERSION_, '1.7', '<') && $hasMixedQty) {
            return $this->context->smarty
                ->assign($this->getIndicatorViewVarsForLegacyProductPage($productId))
                ->fetch($this->getLocalPath() . 'views/templates/front/hook/product_page_legacy.tpl');
        }

        $productId = (int) $product['id'];
        $quantity = (int) $product['quantity'];
        $hasMixedQty = false;

        return $this->context->smarty
            ->assign($this->getIndicatorViewVars($productId, $quantity, $hasMixedQty))
            ->fetch($this->getLocalPath() . 'views/templates/front/hook/product_page.tpl');
    }

    protected function isDisplayableInProductList()
    {
        return (bool) Configuration::get('TV_DISPLAY_IN_PRODUCT_LISTS');
    }

    protected function isDisplayableOnProductPage()
    {
        return (bool) Configuration::get('TV_DISPLAY_ON_PRODUCT_PAGE');
    }

    protected function isCategoryPage()
    {
        return $this->context->controller instanceof CategoryController;
    }

    protected function isDisplayableInCurrentCategory()
    {
        $category_list = Configuration::get('TV_DISPLAY_IN_CATS');
        if (empty($category_list)) {
            return false;
        }
        $category_list = explode(',', $category_list);
        $id = Tools::getValue('id_category');

        return in_array($id, $category_list);
    }

    protected function isStockBelowLimit($quantity)
    {
        $showBelowLimit = (int) Configuration::get('TV_DISPLAY_WHEN_BELOW_LIMIT');

        // If limit is 0, skip this check.
        if (0 == $showBelowLimit) {
            return true;
        }

        return $quantity < $showBelowLimit;
    }

    protected function getLevelByQuantity($quantity)
    {
        // If zero or below, level will always be 0.
        if ($quantity <= 0) {
            return 0;
        }

        $fullLimit = (int) Configuration::get('TV_INDICATOR_FULL_AT');

        // If equal or greater than the full value, level will always be 5.
        if ($quantity >= $fullLimit) {
            return 5;
        }

        // Determine what % of "full" is "quantity"
        $percentage = ($quantity / $fullLimit) * 100;

        // Divide the percentage by 20 so we can calculate the level
        // 100 / 20 is 5, "percentage" / 20 will return the stock level,
        // but since we may get 0 and we only use level 0 when
        // below or equal to zero, we increment the result by one.

        return (int) ($percentage / 20) + 1;
    }

    protected function getCommonIndicatorViewVars()
    {
        return [
            'viewsPath' => $this->getLocalPath() . 'views',
            'indicatorDesign' => Configuration::get('TV_INDICATOR_DESIGN'),
            'useProgressiveColors' => Configuration::get('TV_ENABLE_PROGRESSIVE_COLORS'),
            'isItemsDisplayable' => (bool) Configuration::get('TV_DISPLAY_NR_OF_ITEMS'),
            'stockIndicatorTrans' => [
                'stockStatus' => $this->l('Stock status'),
                'items' => $this->l(' items'),
                'mixedItems' => $this->l('Mixed items'),
            ],
        ];
    }

    protected function getIndicatorViewVars($productId, $quantity, $hasMixedQty = false)
    {
        if ($hasMixedQty) {
            $quantity = (int) ($quantity / $this->getProductVariantsCount($productId));
        }

        $level = $this->getLevelByQuantity($quantity);

        return array_merge(
            $this->getCommonIndicatorViewVars(),
            [
                'productItems' => $quantity,
                'hasMixedQty' => $hasMixedQty,
                'stockLevel' => $level,
                'stockLevelStatus' => $this->getStatusByQuantity($level),
            ]
        );
    }

    protected function getIndicatorViewVarsForLegacyProductPage($productId)
    {
        $productVariants = $this->getProductVariantsQuantity($productId);

        if (empty($productVariants)) {
            return compact('productVariants');
        }

        $viewVars = $this->getCommonIndicatorViewVars();

        foreach ($productVariants as $key => $variant) {
            $level = $this->getLevelByQuantity($variant['quantity']);
            $productVariants[$key]['stockLevel'] = $level;
            $productVariants[$key]['stockLevelStatus'] = $this->getStatusByQuantity($level);
        }

        return array_merge($viewVars, compact('productVariants'));
    }

    protected function getProductVariantsCount($productId)
    {
        $table = _DB_PREFIX_ . 'product_attribute';
        $join = _DB_PREFIX_ . 'product_attribute_shop';

        return (int) Db::getInstance()->getValue(
            "select count(pa.`id_product_attribute`) as `total`
             from `{$table}` pa inner join `{$join}` pas on pa.id_product_attribute = pas.id_product_attribute
             where pas.`id_shop` = " . (int) $this->context->shop->id . '
             and pa.`id_product` = ' . (int) $productId
        );
    }

    protected function getProductVariantsQuantity($productId)
    {
        $table = _DB_PREFIX_ . 'product_attribute';
        $join = _DB_PREFIX_ . 'product_attribute_shop';

        if (!$combinations = Db::getInstance()->executeS(
            "select pa.`id_product`, pa.`id_product_attribute`, pa.`quantity`
             from `{$table}` pa inner join `{$join}` pas on pa.id_product_attribute = pas.id_product_attribute
             where pas.`id_shop` = " . (int) $this->context->shop->id . '
             and pa.`id_product` = ' . (int) $productId
        )) {
            return [];
        }

        // Pulled straight from Product@getAttributesResume method.
        foreach ($combinations as $key => $row) {
            $cache_key = $row['id_product'] . '_' . $row['id_product_attribute'] . '_quantity';

            if (!Cache::isStored($cache_key)) {
                $result = StockAvailable::getQuantityAvailableByProduct(
                    (int) $row['id_product'],
                    (int) $row['id_product_attribute']
                );
                Cache::store($cache_key, $result);

                $combinations[$key]['quantity'] = $result;
            } else {
                $combinations[$key]['quantity'] = Cache::retrieve($cache_key);
            }
        }

        return $combinations;
    }

    protected function getStatusByQuantity($level)
    {
        static $levels;

        if (!$levels) {
            $levels = [
                0 => $this->l('Out of stock'),
                1 => $this->l('Very low'),
                2 => $this->l('Low'),
                3 => $this->l('Moderate'),
                4 => $this->l('High'),
                5 => $this->l('Very high'),
            ];
        }

        return $levels[$level];
    }
}