1<?php
2/**
3 * 2007-2016 PrestaShop
4 *
5 * thirty bees is an extension to the PrestaShop e-commerce software developed by PrestaShop SA
6 * Copyright (C) 2017-2018 thirty bees
7 *
8 * NOTICE OF LICENSE
9 *
10 * This source file is subject to the Open Software License (OSL 3.0)
11 * that is bundled with this package in the file LICENSE.txt.
12 * It is also available through the world-wide-web at this URL:
13 * http://opensource.org/licenses/osl-3.0.php
14 * If you did not receive a copy of the license and are unable to
15 * obtain it through the world-wide-web, please send an email
16 * to license@thirtybees.com so we can send you a copy immediately.
17 *
18 * DISCLAIMER
19 *
20 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
21 * versions in the future. If you wish to customize PrestaShop for your
22 * needs please refer to https://www.thirtybees.com for more information.
23 *
24 * @author    thirty bees <contact@thirtybees.com>
25 * @author    PrestaShop SA <contact@prestashop.com>
26 * @copyright 2017-2018 thirty bees
27 * @copyright 2007-2016 PrestaShop SA
28 * @license   http://opensource.org/licenses/osl-3.0.php  Open Software License (OSL 3.0)
29 *  PrestaShop is an internationally registered trademark & property of PrestaShop SA
30 */
31
32/**
33 * Class AdminProductsControllerCore
34 *
35 * @since 1.0.0
36 */
37class AdminProductsControllerCore extends AdminController
38{
39    // @codingStandardsIgnoreStart
40    /** @var int Max image size for upload
41     * As of 1.5 it is recommended to not set a limit to max image size
42     */
43    protected $max_file_size = null;
44    protected $max_image_size = null;
45
46    protected $_category;
47    /**
48     * @var string name of the tab to display
49     */
50    protected $tab_display;
51    protected $tab_display_module;
52
53    /**
54     * The order in the array decides the order in the list of tab. If an element's value is a number, it will be preloaded.
55     * The tabs are preloaded from the smallest to the highest number.
56     *
57     * @var array Product tabs.
58     */
59    protected $available_tabs = [];
60
61    /** @var string $default_tab */
62    protected $default_tab = 'Informations';
63
64    /** @var array $available_tabs_lang */
65    protected $available_tabs_lang = [];
66
67    /** @var string $position_identifier */
68    protected $position_identifier = 'id_product';
69
70    /** @var array $submitted_tabs */
71    protected $submitted_tabs;
72
73    /** @var int $id_current_category */
74    protected $id_current_category;
75
76    /** @var Product $object */
77    public $object;
78    // @codingStandardsIgnoreEnd
79
80    /**
81     * AdminProductsControllerCore constructor.
82     *
83     * @since 1.0.0
84     */
85    public function __construct()
86    {
87        $this->bootstrap = true;
88        $this->table = 'product';
89        $this->className = 'Product';
90        $this->lang = true;
91        $this->explicitSelect = true;
92        $this->bulk_actions = [
93            'delete' => [
94                'text'    => $this->l('Delete selected'),
95                'icon'    => 'icon-trash',
96                'confirm' => $this->l('Delete selected items?'),
97            ],
98        ];
99        if (!Tools::getValue('id_product')) {
100            $this->multishop_context_group = false;
101        }
102
103        parent::__construct();
104
105        $this->imageType = 'jpg';
106        $this->_defaultOrderBy = 'position';
107        $this->max_file_size = (int) (Configuration::get('PS_LIMIT_UPLOAD_FILE_VALUE') * 1000000);
108        $this->max_image_size = (int) Configuration::get('PS_PRODUCT_PICTURE_MAX_SIZE');
109        $this->allow_export = true;
110
111        // @since 1.5 : translations for tabs
112        $this->available_tabs_lang = [
113            'Informations'   => $this->l('Information'),
114            'Pack'           => $this->l('Pack'),
115            'VirtualProduct' => $this->l('Virtual Product'),
116            'Prices'         => $this->l('Prices'),
117            'Seo'            => $this->l('SEO'),
118            'Images'         => $this->l('Images'),
119            'Associations'   => $this->l('Associations'),
120            'Shipping'       => $this->l('Shipping'),
121            'Combinations'   => $this->l('Combinations'),
122            'Features'       => $this->l('Features'),
123            'Customization'  => $this->l('Customization'),
124            'Attachments'    => $this->l('Attachments'),
125            'Quantities'     => $this->l('Quantities'),
126            'Suppliers'      => $this->l('Suppliers'),
127            'Warehouses'     => $this->l('Warehouses'),
128        ];
129
130        $this->available_tabs = ['Quantities' => 6, 'Warehouses' => 14];
131        if ($this->context->shop->getContext() != Shop::CONTEXT_GROUP) {
132            $this->available_tabs = array_merge(
133                $this->available_tabs,
134                [
135                    'Informations'   => 0,
136                    'Pack'           => 7,
137                    'VirtualProduct' => 8,
138                    'Prices'         => 1,
139                    'Seo'            => 2,
140                    'Associations'   => 3,
141                    'Images'         => 9,
142                    'Shipping'       => 4,
143                    'Combinations'   => 5,
144                    'Features'       => 10,
145                    'Customization'  => 11,
146                    'Attachments'    => 12,
147                    'Suppliers'      => 13,
148                ]
149            );
150        }
151
152        // Sort the tabs that need to be preloaded by their priority number
153        asort($this->available_tabs, SORT_NUMERIC);
154
155        /* Adding tab if modules are hooked */
156        $modulesList = Hook::getHookModuleExecList('displayAdminProductsExtra');
157        if (is_array($modulesList) && count($modulesList) > 0) {
158            foreach ($modulesList as $m) {
159                $this->available_tabs['Module'.ucfirst($m['module'])] = 23;
160                $this->available_tabs_lang['Module'.ucfirst($m['module'])] = Module::getModuleName($m['module']);
161            }
162        }
163
164        if (Tools::getValue('reset_filter_category')) {
165            $this->context->cookie->id_category_products_filter = false;
166        }
167        if (Shop::isFeatureActive() && $this->context->cookie->id_category_products_filter) {
168            $category = new Category((int) $this->context->cookie->id_category_products_filter);
169            if (!$category->inShop()) {
170                $this->context->cookie->id_category_products_filter = false;
171                Tools::redirectAdmin($this->context->link->getAdminLink('AdminProducts'));
172            }
173        }
174        /* Join categories table */
175        if ($idCategory = (int) Tools::getValue('productFilter_cl!name')) {
176            $this->_category = new Category((int) $idCategory);
177            $_POST['productFilter_cl!name'] = $this->_category->name[$this->context->language->id];
178        } else {
179            if ($idCategory = (int) Tools::getValue('id_category')) {
180                $this->id_current_category = $idCategory;
181                $this->context->cookie->id_category_products_filter = $idCategory;
182            } elseif ($idCategory = $this->context->cookie->id_category_products_filter) {
183                $this->id_current_category = $idCategory;
184            }
185            if ($this->id_current_category) {
186                $this->_category = new Category((int) $this->id_current_category);
187            } else {
188                $this->_category = new Category();
189            }
190        }
191
192        $joinCategory = false;
193        if (Validate::isLoadedObject($this->_category) && empty($this->_filter)) {
194            $joinCategory = true;
195        }
196
197        $this->_join .= '
198		LEFT JOIN `'._DB_PREFIX_.'stock_available` sav ON (sav.`id_product` = a.`id_product` AND sav.`id_product_attribute` = 0
199		'.StockAvailable::addSqlShopRestriction(null, null, 'sav').') ';
200
201        $alias = 'sa';
202        $aliasImage = 'image_shop';
203
204        $idShop = Shop::isFeatureActive() && Shop::getContext() == Shop::CONTEXT_SHOP ? (int) $this->context->shop->id : 'a.id_shop_default';
205        $this->_join .= ' JOIN `'._DB_PREFIX_.'product_shop` sa ON (a.`id_product` = sa.`id_product` AND sa.id_shop = '.$idShop.')
206				LEFT JOIN `'._DB_PREFIX_.'category_lang` cl ON ('.$alias.'.`id_category_default` = cl.`id_category` AND b.`id_lang` = cl.`id_lang` AND cl.id_shop = '.$idShop.')
207				LEFT JOIN `'._DB_PREFIX_.'shop` shop ON (shop.id_shop = '.$idShop.')
208				LEFT JOIN `'._DB_PREFIX_.'image_shop` image_shop ON (image_shop.`id_product` = a.`id_product` AND image_shop.`cover` = 1 AND image_shop.id_shop = '.$idShop.')
209				LEFT JOIN `'._DB_PREFIX_.'image` i ON (i.`id_image` = image_shop.`id_image`)
210				LEFT JOIN `'._DB_PREFIX_.'product_download` pd ON (pd.`id_product` = a.`id_product` AND pd.`active` = 1)';
211
212        $this->_select .= 'shop.`name` AS `shopname`, a.`id_shop_default`, ';
213        $this->_select .= $aliasImage.'.`id_image` AS `id_image`, cl.`name` AS `name_category`, '.$alias.'.`price`, 0 AS `price_final`, a.`is_virtual`, pd.`nb_downloadable`, sav.`quantity` AS `sav_quantity`, '.$alias.'.`active`, IF(sav.`quantity`<=0, 1, 0) AS `badge_danger`';
214
215        if ($joinCategory) {
216            $this->_join .= ' INNER JOIN `'._DB_PREFIX_.'category_product` cp ON (cp.`id_product` = a.`id_product` AND cp.`id_category` = '.(int) $this->_category->id.') ';
217            $this->_select .= ' , cp.`position`, ';
218        }
219        $this->_use_found_rows = false;
220        $this->_group = '';
221
222        $this->fields_list = [];
223        $this->fields_list['id_product'] = [
224            'title' => $this->l('ID'),
225            'align' => 'center',
226            'class' => 'fixed-width-xs',
227            'type'  => 'int',
228        ];
229        $this->fields_list['image'] = [
230            'title'   => $this->l('Image'),
231            'align'   => 'center',
232            'image'   => 'p',
233            'orderby' => false,
234            'filter'  => false,
235            'search'  => false,
236        ];
237        $this->fields_list['name'] = [
238            'title'      => $this->l('Name'),
239            'filter_key' => 'b!name',
240        ];
241        $this->fields_list['reference'] = [
242            'title' => $this->l('Reference'),
243            'align' => 'left',
244        ];
245
246        if (Shop::isFeatureActive() && Shop::getContext() != Shop::CONTEXT_SHOP) {
247            $this->fields_list['shopname'] = [
248                'title'      => $this->l('Default shop'),
249                'filter_key' => 'shop!name',
250            ];
251        } else {
252            $this->fields_list['name_category'] = [
253                'title'      => $this->l('Category'),
254                'filter_key' => 'cl!name',
255            ];
256        }
257        $this->fields_list['price'] = [
258            'title'      => $this->l('Base price'),
259            'type'       => 'price',
260            'align'      => 'text-right',
261            'filter_key' => 'a!price',
262        ];
263        $this->fields_list['price_final'] = [
264            'title'        => $this->l('Final price'),
265            'type'         => 'price',
266            'align'        => 'text-right',
267            'havingFilter' => true,
268            'orderby'      => false,
269            'search'       => false,
270        ];
271
272        if (Configuration::get('PS_STOCK_MANAGEMENT')) {
273            $this->fields_list['sav_quantity'] = [
274                'title'        => $this->l('Quantity'),
275                'type'         => 'int',
276                'align'        => 'text-right',
277                'filter_key'   => 'sav!quantity',
278                'orderby'      => true,
279                'badge_danger' => true,
280                //'hint' => $this->l('This is the quantity available in the current shop/group.'),
281            ];
282        }
283
284        $this->fields_list['active'] = [
285            'title'      => $this->l('Status'),
286            'active'     => 'status',
287            'filter_key' => $alias.'!active',
288            'align'      => 'text-center',
289            'type'       => 'bool',
290            'class'      => 'fixed-width-sm',
291            'orderby'    => false,
292        ];
293
294        if ($joinCategory && (int) $this->id_current_category) {
295            $this->fields_list['position'] = [
296                'title'      => $this->l('Position'),
297                'filter_key' => 'cp!position',
298                'align'      => 'center',
299                'position'   => 'position',
300            ];
301        }
302    }
303
304    /**
305     * @param string $echo
306     * @param array  $tr
307     *
308     * @return string
309     *
310     * @since 1.0.0
311     */
312    public static function getQuantities($echo, $tr)
313    {
314        if ((int) $tr['is_virtual'] == 1 && $tr['nb_downloadable'] == 0) {
315            return '&infin;';
316        } else {
317            return $echo;
318        }
319    }
320
321    /**
322     * Build a categories tree
323     *
324     * @param int   $idObj
325     * @param array $indexedCategories Array with categories where product is indexed (in order to check checkbox)
326     * @param array $categories        Categories to list
327     * @param       $current
328     * @param null  $idCategory        Current category ID
329     * @param null  $idCategoryDefault
330     * @param array $hasSuite
331     *
332     * @return string
333     *
334     * @since 1.0.0
335     */
336    public static function recurseCategoryForInclude($idObj, $indexedCategories, $categories, $current, $idCategory = null, $idCategoryDefault = null, $hasSuite = [])
337    {
338        global $done;
339        static $irow;
340        $content = '';
341
342        if (!$idCategory) {
343            $idCategory = (int) Configuration::get('PS_ROOT_CATEGORY');
344        }
345
346        if (!isset($done[$current['infos']['id_parent']])) {
347            $done[$current['infos']['id_parent']] = 0;
348        }
349        $done[$current['infos']['id_parent']] += 1;
350
351        $todo = count($categories[$current['infos']['id_parent']]);
352        $doneC = $done[$current['infos']['id_parent']];
353
354        $level = $current['infos']['level_depth'] + 1;
355
356        $content .= '
357		<tr class="'.($irow++ % 2 ? 'alt_row' : '').'">
358			<td>
359				<input type="checkbox" name="categoryBox[]" class="categoryBox'.($idCategoryDefault == $idCategory ? ' id_category_default' : '').'" id="categoryBox_'.$idCategory.'" value="'.$idCategory.'"'.((in_array($idCategory, $indexedCategories) || ((int) (Tools::getValue('id_category')) == $idCategory && !(int) ($idObj))) ? ' checked="checked"' : '').' />
360			</td>
361			<td>
362				'.$idCategory.'
363			</td>
364			<td>';
365        for ($i = 2; $i < $level; $i++) {
366            $content .= '<img src="../img/admin/lvl_'.$hasSuite[$i - 2].'.gif" alt="" />';
367        }
368        $content .= '<img src="../img/admin/'.($level == 1 ? 'lv1.gif' : 'lv2_'.($todo == $doneC ? 'f' : 'b').'.gif').'" alt="" /> &nbsp;
369			<label for="categoryBox_'.$idCategory.'" class="t">'.stripslashes($current['infos']['name']).'</label></td>
370		</tr>';
371
372        if ($level > 1) {
373            $hasSuite[] = ($todo == $doneC ? 0 : 1);
374        }
375        if (isset($categories[$idCategory])) {
376            foreach ($categories[$idCategory] as $key => $row) {
377                if ($key != 'infos') {
378                    $content .= AdminProductsController::recurseCategoryForInclude($idObj, $indexedCategories, $categories, $categories[$idCategory][$key], $key, $idCategoryDefault, $hasSuite);
379                }
380            }
381        }
382
383        return $content;
384    }
385
386    /**
387     * @return void
388     *
389     * @since 1.0.0
390     */
391    public function setMedia()
392    {
393        parent::setMedia();
394
395        $boTheme = ((Validate::isLoadedObject($this->context->employee)
396            && $this->context->employee->bo_theme) ? $this->context->employee->bo_theme : 'default');
397
398        if (!file_exists(_PS_BO_ALL_THEMES_DIR_.$boTheme.DIRECTORY_SEPARATOR.'template')) {
399            $boTheme = 'default';
400        }
401
402        $this->addJs(__PS_BASE_URI__.$this->admin_webpath.'/themes/'.$boTheme.'/js/jquery.iframe-transport.js');
403        $this->addJs(__PS_BASE_URI__.$this->admin_webpath.'/themes/'.$boTheme.'/js/jquery.fileupload.js');
404        $this->addJs(__PS_BASE_URI__.$this->admin_webpath.'/themes/'.$boTheme.'/js/jquery.fileupload-process.js');
405        $this->addJs(__PS_BASE_URI__.$this->admin_webpath.'/themes/'.$boTheme.'/js/jquery.fileupload-validate.js');
406        $this->addJs(__PS_BASE_URI__.'js/vendor/spin.js');
407        $this->addJs(__PS_BASE_URI__.'js/vendor/ladda.js');
408    }
409
410    /**
411     * @param int         $idLang
412     * @param string|null $orderBy
413     * @param string|null $orderWay
414     * @param int         $start
415     * @param int|null    $limit
416     * @param int|null    $idLangShop
417     *
418     * @since 1.0.0
419     */
420    public function getList($idLang, $orderBy = null, $orderWay = null, $start = 0, $limit = null, $idLangShop = null)
421    {
422        $orderByPriceFinal = (empty($orderBy) ? ($this->context->cookie->__get($this->table.'Orderby') ? $this->context->cookie->__get($this->table.'Orderby') : 'id_'.$this->table) : $orderBy);
423        $orderWayPriceFinal = (empty($orderWay) ? ($this->context->cookie->__get($this->table.'Orderway') ? $this->context->cookie->__get($this->table.'Orderby') : 'ASC') : $orderWay);
424        if ($orderByPriceFinal == 'price_final') {
425            $orderBy = 'id_'.$this->table;
426            $orderWay = 'ASC';
427        }
428        parent::getList($idLang, $orderBy, $orderWay, $start, $limit, $this->context->shop->id);
429
430        /* update product quantity with attributes ...*/
431        $nb = count($this->_list);
432        if ($this->_list) {
433            $context = $this->context->cloneContext();
434            $context->shop = clone($context->shop);
435            /* update product final price */
436            for ($i = 0; $i < $nb; $i++) {
437                if ($this->context->shop->getContext() != Shop::CONTEXT_SHOP) {
438                    $context->shop = new Shop((int) $this->_list[$i]['id_shop_default']);
439                }
440
441                $decimals = 0;
442                if ($this->context->currency->decimals) {
443                    $decimals = Configuration::get('PS_PRICE_DISPLAY_PRECISION');
444                }
445                // convert price with the currency from context
446                $this->_list[$i]['price'] = Tools::convertPrice($this->_list[$i]['price'], $this->context->currency, true, $this->context);
447                $this->_list[$i]['price_tmp'] = Product::getPriceStatic(
448                    $this->_list[$i]['id_product'],
449                    true,
450                    null,
451                    $decimals,
452                    null,
453                    false,
454                    true,
455                    1,
456                    true,
457                    null,
458                    null,
459                    null,
460                    $nothing,
461                    true,
462                    true,
463                    $context
464                );
465            }
466        }
467
468        if ($orderByPriceFinal == 'price_final') {
469            if (strtolower($orderWayPriceFinal) == 'desc') {
470                uasort($this->_list, 'cmpPriceDesc');
471            } else {
472                uasort($this->_list, 'cmpPriceAsc');
473            }
474        }
475        for ($i = 0; $this->_list && $i < $nb; $i++) {
476            $this->_list[$i]['price_final'] = $this->_list[$i]['price_tmp'];
477            unset($this->_list[$i]['price_tmp']);
478        }
479    }
480
481    /**
482     * Ajax process get category tree
483     *
484     * @since 1.0.0
485     */
486    public function ajaxProcessGetCategoryTree()
487    {
488        $category = Tools::getValue('category', Category::getRootCategory()->id);
489        $fullTree = Tools::getValue('fullTree', 0);
490        $useCheckBox = Tools::getValue('useCheckBox', 1);
491        $selected = Tools::getValue('selected', []);
492        $idTree = Tools::getValue('type');
493        $inputName = str_replace(['[', ']'], '', Tools::getValue('inputName', null));
494
495        $tree = new HelperTreeCategories('subtree_associated_categories');
496        $tree->setTemplate('subtree_associated_categories.tpl')
497            ->setUseCheckBox($useCheckBox)
498            ->setUseSearch(true)
499            ->setIdTree($idTree)
500            ->setSelectedCategories($selected)
501            ->setFullTree($fullTree)
502            ->setChildrenOnly(true)
503            ->setNoJS(true)
504            ->setRootCategory($category);
505
506        if ($inputName) {
507            $tree->setInputName($inputName);
508        }
509
510        die($tree->render());
511    }
512
513    /**
514     * Ajax process get countries options
515     *
516     * @since 1.0.0
517     */
518    public function ajaxProcessGetCountriesOptions()
519    {
520        if (!$res = Country::getCountriesByIdShop((int) Tools::getValue('id_shop'), (int) $this->context->language->id)) {
521            return;
522        }
523
524        $tpl = $this->createTemplate('specific_prices_shop_update.tpl');
525        $tpl->assign(
526            [
527                'option_list' => $res,
528                'key_id'      => 'id_country',
529                'key_value'   => 'name',
530            ]
531        );
532
533        $this->content = $tpl->fetch();
534    }
535
536    /**
537     *
538     * Ajax process get currency options
539     *
540     * @since 1.0.0
541     */
542    public function ajaxProcessGetCurrenciesOptions()
543    {
544        if (!$res = Currency::getCurrenciesByIdShop((int) Tools::getValue('id_shop'))) {
545            return;
546        }
547
548        $tpl = $this->createTemplate('specific_prices_shop_update.tpl');
549        $tpl->assign(
550            [
551                'option_list' => $res,
552                'key_id'      => 'id_currency',
553                'key_value'   => 'name',
554            ]
555        );
556
557        $this->content = $tpl->fetch();
558    }
559
560    /**
561     * Ajax process get group options
562     *
563     * @since 1.0.0
564     */
565    public function ajaxProcessGetGroupsOptions()
566    {
567        if (!$res = Group::getGroups((int) $this->context->language->id, (int) Tools::getValue('id_shop'))) {
568            return;
569        }
570
571        $tpl = $this->createTemplate('specific_prices_shop_update.tpl');
572        $tpl->assign(
573            [
574                'option_list' => $res,
575                'key_id'      => 'id_group',
576                'key_value'   => 'name',
577            ]
578        );
579
580        $this->content = $tpl->fetch();
581    }
582
583    /**
584     * Process delete virtual product
585     *
586     * @since 1.0.0
587     */
588    public function processDeleteVirtualProduct()
589    {
590        $idProduct = (int) Tools::getValue('id_product');
591        $idProductDownload = ProductDownload::getIdFromIdProduct($idProduct);
592
593        if ($idProductDownload) {
594            $productDownload = new ProductDownload($idProductDownload);
595
596            if (!$productDownload->deleteFile()) {
597                $this->errors[] = Tools::displayError('Cannot delete file.');
598            } else {
599                $productDownload->active = false;
600                $productDownload->update();
601                $this->redirect_after = static::$currentIndex.'&id_product='.$idProduct.'&updateproduct&key_tab=VirtualProduct&conf=1&token='.$this->token;
602            }
603        }
604
605        $this->display = 'edit';
606        $this->tab_display = 'VirtualProduct';
607    }
608
609    /**
610     * Ajax process add attachment
611     *
612     * @since 1.0.0
613     */
614    public function ajaxProcessAddAttachment()
615    {
616        if ($this->tabAccess['edit'] === '0') {
617            $this->ajaxDie(json_encode(['error' => $this->l('You do not have the right permission')]));
618        }
619        if (isset($_FILES['attachment_file'])) {
620            if ((int) $_FILES['attachment_file']['error'] === 1) {
621                $_FILES['attachment_file']['error'] = [];
622
623                $maxUpload = (int) ini_get('upload_max_filesize');
624                $maxPost = (int) ini_get('post_max_size');
625                $uploadMb = min($maxUpload, $maxPost);
626                $_FILES['attachment_file']['error'][] = sprintf(
627                    $this->l('File %1$s exceeds the size allowed by the server. The limit is set to %2$d MB.'),
628                    '<b>'.$_FILES['attachment_file']['name'].'</b> ',
629                    '<b>'.$uploadMb.'</b>'
630                );
631            }
632
633            $_FILES['attachment_file']['error'] = [];
634
635            $isAttachmentNameValid = false;
636            $attachmentNames = Tools::getValue('attachment_name');
637            $attachmentDescriptions = Tools::getValue('attachment_description');
638
639            if (!isset($attachmentNames) || !$attachmentNames) {
640                $attachmentNames = [];
641            }
642
643            if (!isset($attachmentDescriptions) || !$attachmentDescriptions) {
644                $attachmentDescriptions = [];
645            }
646
647            foreach ($attachmentNames as $lang => $name) {
648                $language = Language::getLanguage((int) $lang);
649
650                if (mb_strlen($name) > 0) {
651                    $isAttachmentNameValid = true;
652                }
653
654                if (!Validate::isGenericName($name)) {
655                    $_FILES['attachment_file']['error'][] = sprintf(Tools::displayError('Invalid name for %s language'), $language['name']);
656                } elseif (mb_strlen($name) > 32) {
657                    $_FILES['attachment_file']['error'][] = sprintf(Tools::displayError('The name for %1s language is too long (%2d chars max).'), $language['name'], 32);
658                }
659            }
660
661            foreach ($attachmentDescriptions as $lang => $description) {
662                $language = Language::getLanguage((int) $lang);
663
664                if (!Validate::isCleanHtml($description)) {
665                    $_FILES['attachment_file']['error'][] = sprintf(Tools::displayError('Invalid description for %s language'), $language['name']);
666                }
667            }
668
669            if (!$isAttachmentNameValid) {
670                $_FILES['attachment_file']['error'][] = Tools::displayError('An attachment name is required.');
671            }
672
673            if (empty($_FILES['attachment_file']['error'])) {
674                if (is_uploaded_file($_FILES['attachment_file']['tmp_name'])) {
675                    if ($_FILES['attachment_file']['size'] > (Configuration::get('PS_ATTACHMENT_MAXIMUM_SIZE') * 1024 * 1024)) {
676                        $_FILES['attachment_file']['error'][] = sprintf(
677                            $this->l('The file is too large. Maximum size allowed is: %1$d kB. The file you are trying to upload is %2$d kB.'),
678                            (Configuration::get('PS_ATTACHMENT_MAXIMUM_SIZE') * 1024),
679                            number_format(($_FILES['attachment_file']['size'] / 1024), 2, '.', '')
680                        );
681                    } else {
682                        do {
683                            $uniqid = sha1(microtime());
684                        } while (file_exists(_PS_DOWNLOAD_DIR_.$uniqid));
685                        if (!copy($_FILES['attachment_file']['tmp_name'], _PS_DOWNLOAD_DIR_.$uniqid)) {
686                            $_FILES['attachment_file']['error'][] = $this->l('File copy failed');
687                        }
688                        @unlink($_FILES['attachment_file']['tmp_name']);
689                    }
690                } else {
691                    $_FILES['attachment_file']['error'][] = Tools::displayError('The file is missing.');
692                }
693
694                if (empty($_FILES['attachment_file']['error']) && isset($uniqid)) {
695                    $attachment = new Attachment();
696
697                    foreach ($attachmentNames as $lang => $name) {
698                        $attachment->name[(int) $lang] = $name;
699                    }
700
701                    foreach ($attachmentDescriptions as $lang => $description) {
702                        $attachment->description[(int) $lang] = $description;
703                    }
704
705                    $attachment->file = $uniqid;
706                    $attachment->mime = $_FILES['attachment_file']['type'];
707                    $attachment->file_name = $_FILES['attachment_file']['name'];
708
709                    if (empty($attachment->mime) || mb_strlen($attachment->mime) > 128) {
710                        $_FILES['attachment_file']['error'][] = Tools::displayError('Invalid file extension');
711                    }
712                    if (!Validate::isGenericName($attachment->file_name)) {
713                        $_FILES['attachment_file']['error'][] = Tools::displayError('Invalid file name');
714                    }
715                    if (mb_strlen($attachment->file_name) > 128) {
716                        $_FILES['attachment_file']['error'][] = Tools::displayError('The file name is too long.');
717                    }
718                    if (empty($this->errors)) {
719                        $res = $attachment->add();
720                        if (!$res) {
721                            $_FILES['attachment_file']['error'][] = Tools::displayError('This attachment was unable to be loaded into the database.');
722                        } else {
723                            $_FILES['attachment_file']['id_attachment'] = $attachment->id;
724                            $_FILES['attachment_file']['filename'] = $attachment->name[$this->context->employee->id_lang];
725                            $idProduct = (int) Tools::getValue($this->identifier);
726                            $res = $attachment->attachProduct($idProduct);
727                            if (!$res) {
728                                $_FILES['attachment_file']['error'][] = Tools::displayError('We were unable to associate this attachment to a product.');
729                            }
730                        }
731                    } else {
732                        $_FILES['attachment_file']['error'][] = Tools::displayError('Invalid file');
733                    }
734                }
735            }
736
737            $this->ajaxDie(json_encode($_FILES));
738        }
739    }
740
741    /**
742     * Process duplicate
743     *
744     * @since 1.0.0
745     */
746    public function processDuplicate()
747    {
748        if (Validate::isLoadedObject($product = new Product((int) Tools::getValue('id_product')))) {
749            $idProductOld = $product->id;
750            if (empty($product->price) && Shop::getContext() == Shop::CONTEXT_GROUP) {
751                $shops = ShopGroup::getShopsFromGroup(Shop::getContextShopGroupID());
752                foreach ($shops as $shop) {
753                    if ($product->isAssociatedToShop($shop['id_shop'])) {
754                        $productPrice = new Product($idProductOld, false, null, $shop['id_shop']);
755                        $product->price = $productPrice->price;
756                    }
757                }
758            }
759            unset($product->id);
760            unset($product->id_product);
761            $product->indexed = 0;
762            $product->active = 0;
763            if ($product->add()
764                && Category::duplicateProductCategories($idProductOld, $product->id)
765                && Product::duplicateSuppliers($idProductOld, $product->id)
766                && ($combinationImages = Product::duplicateAttributes($idProductOld, $product->id)) !== false
767                && GroupReduction::duplicateReduction($idProductOld, $product->id)
768                && Product::duplicateAccessories($idProductOld, $product->id)
769                && Product::duplicateFeatures($idProductOld, $product->id)
770                && Product::duplicateSpecificPrices($idProductOld, $product->id)
771                && Pack::duplicate($idProductOld, $product->id)
772                && Product::duplicateCustomizationFields($idProductOld, $product->id)
773                && Product::duplicateTags($idProductOld, $product->id)
774                && Product::duplicateDownload($idProductOld, $product->id)
775                && Product::duplicateAttachments($idProductOld, $product->id)
776            ) {
777                if ($product->hasAttributes()) {
778                    Product::updateDefaultAttribute($product->id);
779                } else {
780	                // Set stock quantity
781	                $quantityAttributeOld = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
782		                (new DbQuery())
783			                ->select('`quantity`')
784			                ->from('stock_available')
785			                ->where('`id_product` = '.(int) $idProductOld)
786			                ->where('`id_product_attribute` = 0')
787	                );
788	                StockAvailable::setQuantity((int) $product->id, 0, (int) $quantityAttributeOld);
789                }
790
791                if (!Tools::getValue('noimage') && !Image::duplicateProductImages($idProductOld, $product->id, $combinationImages)) {
792                    $this->errors[] = Tools::displayError('An error occurred while copying images.');
793                } else {
794                    Hook::exec('actionProductAdd', ['id_product' => (int) $product->id, 'product' => $product]);
795                    if (in_array($product->visibility, ['both', 'search']) && Configuration::get('PS_SEARCH_INDEXATION')) {
796                        Search::indexation(false, $product->id);
797                    }
798                    $this->redirect_after = static::$currentIndex.(Tools::getIsset('id_category') ? '&id_category='.(int) Tools::getValue('id_category') : '').'&conf=19&token='.$this->token;
799                }
800            } else {
801                $this->errors[] = Tools::displayError('An error occurred while creating an object.');
802            }
803        }
804    }
805
806    /**
807     * Process delete
808     *
809     * @since 1.0.0
810     */
811    public function processDelete()
812    {
813        if (Validate::isLoadedObject($object = $this->loadObject()) && isset($this->fieldImageSettings)) {
814            /** @var Product $object */
815            // check if request at least one object with noZeroObject
816            if (isset($object->noZeroObject) && count($taxes = call_user_func([$this->className, $object->noZeroObject])) <= 1) {
817                $this->errors[] = Tools::displayError('You need at least one object.').' <b>'.$this->table.'</b><br />'.Tools::displayError('You cannot delete all of the items.');
818            } else {
819                /*
820                 * @since 1.5.0
821                 * It is NOT possible to delete a product if there is/are currently:
822                 * - a physical stock for this product
823                 * - supply order(s) for this product
824                 */
825                if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT') && $object->advanced_stock_management) {
826                    $stockManager = StockManagerFactory::getManager();
827                    $physicalQuantity = $stockManager->getProductPhysicalQuantities($object->id, 0);
828                    $realQuantity = $stockManager->getProductRealQuantities($object->id, 0);
829                    if ($physicalQuantity > 0 || $realQuantity > $physicalQuantity) {
830                        $this->errors[] = Tools::displayError('You cannot delete this product because there is physical stock left.');
831                    }
832                }
833
834                if (!count($this->errors)) {
835                    if ($object->delete()) {
836                        $idCategory = (int) Tools::getValue('id_category');
837                        $categoryUrl = empty($idCategory) ? '' : '&id_category='.(int) $idCategory;
838                        Logger::addLog(sprintf($this->l('%s deletion', 'AdminTab', false, false), $this->className), 1, null, $this->className, (int) $object->id, true, (int) $this->context->employee->id);
839                        $this->redirect_after = static::$currentIndex.'&conf=1&token='.$this->token.$categoryUrl;
840                    } else {
841                        $this->errors[] = Tools::displayError('An error occurred during deletion.');
842                    }
843                }
844            }
845        } else {
846            $this->errors[] = Tools::displayError('An error occurred while deleting the object.').' <b>'.$this->table.'</b> '.Tools::displayError('(cannot load object)');
847        }
848    }
849
850    /**
851     * Load object
852     *
853     * @param bool $opt
854     *
855     * @return false|ObjectModel
856     *
857     * @since 1.0.0
858     */
859    protected function loadObject($opt = false)
860    {
861        $result = parent::loadObject($opt);
862        if ($result && Validate::isLoadedObject($this->object)) {
863            if (Shop::getContext() == Shop::CONTEXT_SHOP && Shop::isFeatureActive() && !$this->object->isAssociatedToShop()) {
864                $defaultProduct = new Product((int) $this->object->id, false, null, (int) $this->object->id_shop_default);
865                $def = ObjectModel::getDefinition($this->object);
866                foreach ($def['fields'] as $field_name => $row) {
867                    if (is_array($defaultProduct->$field_name)) {
868                        foreach ($defaultProduct->$field_name as $key => $value) {
869                            $this->object->{$field_name}[$key] = $value;
870                        }
871                    } else {
872                        $this->object->$field_name = $defaultProduct->$field_name;
873                    }
874                }
875            }
876            $this->object->loadStockData();
877        }
878
879        return $result;
880    }
881
882    /**
883     * Process image
884     *
885     * @since 1.0.0
886     */
887    public function processImage()
888    {
889        $idImage = (int) Tools::getValue('id_image');
890        $image = new Image((int) $idImage);
891        if (Validate::isLoadedObject($image)) {
892            /* Update product image/legend */
893            // @todo : move in processEditProductImage
894            if (Tools::getIsset('editImage')) {
895                if ($image->cover) {
896                    $_POST['cover'] = 1;
897                }
898
899                $_POST['id_image'] = $image->id;
900            } elseif (Tools::getIsset('coverImage')) {
901                /* Choose product cover image */
902                Image::deleteCover($image->id_product);
903                $image->cover = 1;
904                if (!$image->update()) {
905                    $this->errors[] = Tools::displayError('You cannot change the product\'s cover image.');
906                } else {
907                    $productId = (int) Tools::getValue('id_product');
908                    @unlink(_PS_TMP_IMG_DIR_.'product_'.$productId.'.jpg');
909                    @unlink(_PS_TMP_IMG_DIR_.'product_mini_'.$productId.'_'.$this->context->shop->id.'.jpg');
910                    $this->redirect_after = static::$currentIndex.'&id_product='.$image->id_product.'&id_category='.(Tools::getIsset('id_category') ? '&id_category='.(int) Tools::getValue('id_category') : '').'&action=Images&addproduct'.'&token='.$this->token;
911                }
912            } elseif (Tools::getIsset('imgPosition') && Tools::getIsset('imgDirection')) {
913                /* Choose product image position */
914                $image->updatePosition(Tools::getValue('imgDirection'), Tools::getValue('imgPosition'));
915                $this->redirect_after = static::$currentIndex.'&id_product='.$image->id_product.'&id_category='.(Tools::getIsset('id_category') ? '&id_category='.(int) Tools::getValue('id_category') : '').'&add'.$this->table.'&action=Images&token='.$this->token;
916            }
917        } else {
918            $this->errors[] = Tools::displayError('The image could not be found. ');
919        }
920    }
921
922    /**
923     * This function is never called at the moment (specific prices cannot be edited)
924     * @todo: What should we be doing with it, then?
925     *
926     * @since 1.0.0
927     */
928    public function processPricesModification()
929    {
930        $idSpecificPrices = Tools::getValue('spm_id_specific_price');
931        $idCombinations = Tools::getValue('spm_id_product_attribute');
932        $idShops = Tools::getValue('spm_id_shop');
933        $idCurrencies = Tools::getValue('spm_id_currency');
934        $idCountries = Tools::getValue('spm_id_country');
935        $idGroups = Tools::getValue('spm_id_group');
936        $idCustomers = Tools::getValue('spm_id_customer');
937        $prices = Tools::getValue('spm_price');
938        $fromQuantities = Tools::getValue('spm_from_quantity');
939        $reductions = Tools::getValue('spm_reduction');
940        $reductionTypes = Tools::getValue('spm_reduction_type');
941        $froms = Tools::getValue('spm_from');
942        $tos = Tools::getValue('spm_to');
943
944        foreach ($idSpecificPrices as $key => $idSpecificPrice) {
945            if ($reductionTypes[$key] == 'percentage' && ((float) $reductions[$key] <= 0 || (float) $reductions[$key] > 100)) {
946                $this->errors[] = Tools::displayError('Submitted reduction value (0-100) is out-of-range');
947            } elseif ($this->_validateSpecificPrice(
948                $idShops[$key],
949                $idCurrencies[$key],
950                $idCountries[$key],
951                $idGroups[$key],
952                $idCustomers[$key],
953                priceval($prices[$key]),
954                $fromQuantities[$key],
955                priceval($reductions[$key]),
956                $reductionTypes[$key],
957                $froms[$key],
958                $tos[$key],
959                $idCombinations[$key]
960            )) {
961                $specificPrice = new SpecificPrice((int) ($idSpecificPrice));
962                $specificPrice->id_shop = (int) $idShops[$key];
963                $specificPrice->id_product_attribute = (int) $idCombinations[$key];
964                $specificPrice->id_currency = (int) ($idCurrencies[$key]);
965                $specificPrice->id_country = (int) ($idCountries[$key]);
966                $specificPrice->id_group = (int) ($idGroups[$key]);
967                $specificPrice->id_customer = (int) $idCustomers[$key];
968                $specificPrice->price = priceval($prices[$key]);
969                $specificPrice->from_quantity = (int) ($fromQuantities[$key]);
970                $specificPrice->reduction = ($reductionTypes[$key] == 'percentage' ? ($reductions[$key] / 100) : priceval($reductions[$key]));
971                $specificPrice->reduction_type = !$reductions[$key] ? 'amount' : $reductionTypes[$key];
972                $specificPrice->from = !$froms[$key] ? '0000-00-00 00:00:00' : $froms[$key];
973                $specificPrice->to = !$tos[$key] ? '0000-00-00 00:00:00' : $tos[$key];
974                if (!$specificPrice->update()) {
975                    $this->errors[] = Tools::displayError('An error occurred while updating the specific price.');
976                }
977            }
978        }
979        if (!count($this->errors)) {
980            $this->redirect_after = static::$currentIndex.'&id_product='.(int) (Tools::getValue('id_product')).(Tools::getIsset('id_category') ? '&id_category='.(int) Tools::getValue('id_category') : '').'&update'.$this->table.'&action=Prices&token='.$this->token;
981        }
982    }
983
984    /**
985     * Validate a SpecificPrice
986     *
987     * @param     $idShop
988     * @param     $idCurrency
989     * @param     $idCountry
990     * @param     $idGroup
991     * @param     $idCustomer
992     * @param     $price
993     * @param     $fromQuantity
994     * @param     $reduction
995     * @param     $reductionType
996     * @param     $from
997     * @param     $to
998     * @param int $idCombination
999     *
1000     * @return bool
1001     *
1002     * @since 1.0.0
1003     */
1004    protected function _validateSpecificPrice($idShop, $idCurrency, $idCountry, $idGroup, $idCustomer, $price, $fromQuantity, $reduction, $reductionType, $from, $to, $idCombination = 0)
1005    {
1006        if (!Validate::isUnsignedId($idShop) || !Validate::isUnsignedId($idCurrency) || !Validate::isUnsignedId($idCountry) || !Validate::isUnsignedId($idGroup) || !Validate::isUnsignedId($idCustomer)) {
1007            $this->errors[] = Tools::displayError('Wrong IDs');
1008        } elseif ((!isset($price) && !isset($reduction)) || (isset($price) && !Validate::isNegativePrice($price)) || (isset($reduction) && !Validate::isPrice($reduction))) {
1009            $this->errors[] = Tools::displayError('Invalid price/discount amount');
1010        } elseif (!Validate::isUnsignedInt($fromQuantity)) {
1011            $this->errors[] = Tools::displayError('Invalid quantity');
1012        } elseif ($reduction && !Validate::isReductionType($reductionType)) {
1013            $this->errors[] = Tools::displayError('Please select a discount type (amount or percentage).');
1014        } elseif ($from && $to && (!Validate::isDateFormat($from) || !Validate::isDateFormat($to))) {
1015            $this->errors[] = Tools::displayError('The from/to date is invalid.');
1016        } elseif (SpecificPrice::exists((int) $this->object->id, $idCombination, $idShop, $idGroup, $idCountry, $idCurrency, $idCustomer, $fromQuantity, $from, $to, false)) {
1017            $this->errors[] = Tools::displayError('A specific price already exists for these parameters.');
1018        } else {
1019            return true;
1020        }
1021
1022        return false;
1023    }
1024
1025    /**
1026     * Ajax process delete SpecificPrice
1027     *
1028     * @since 1.0.0
1029     */
1030    public function ajaxProcessDeleteSpecificPrice()
1031    {
1032        if ($this->tabAccess['delete'] === '1') {
1033            $idSpecificPrice = (int) Tools::getValue('id_specific_price');
1034            if (!$idSpecificPrice || !Validate::isUnsignedId($idSpecificPrice)) {
1035                $error = Tools::displayError('The specific price ID is invalid.');
1036            } else {
1037                $specificPrice = new SpecificPrice((int) $idSpecificPrice);
1038                if (!$specificPrice->delete()) {
1039                    $error = Tools::displayError('An error occurred while attempting to delete the specific price.');
1040                }
1041            }
1042        } else {
1043            $error = Tools::displayError('You do not have permission to delete this.');
1044        }
1045
1046        if (isset($error)) {
1047            $json = [
1048                'status'  => 'error',
1049                'message' => $error,
1050            ];
1051        } else {
1052            $json = [
1053                'status'  => 'ok',
1054                'message' => $this->_conf[1],
1055            ];
1056        }
1057
1058        $this->ajaxDie(json_encode($json));
1059    }
1060
1061    /**
1062     * Process product customization
1063     *
1064     * @since 1.0.0
1065     */
1066    public function processProductCustomization()
1067    {
1068        if (Validate::isLoadedObject($product = new Product((int) Tools::getValue('id_product')))) {
1069            foreach ($_POST as $field => $value) {
1070                if (strncmp($field, 'label_', 6) == 0 && !Validate::isLabel($value)) {
1071                    $this->errors[] = Tools::displayError('The label fields defined are invalid.');
1072                }
1073            }
1074            if (empty($this->errors) && !$product->updateLabels()) {
1075                $this->errors[] = Tools::displayError('An error occurred while updating customization fields.');
1076            }
1077            if (empty($this->errors)) {
1078                $this->confirmations[] = $this->l('Update successful');
1079            }
1080        } else {
1081            $this->errors[] = Tools::displayError('A product must be created before adding customization.');
1082        }
1083    }
1084
1085    /**
1086     * Overrides parent for custom redirect link
1087     *
1088     * @since 1.0.0
1089     */
1090    public function processPosition()
1091    {
1092        /** @var Product $object */
1093        if (!Validate::isLoadedObject($object = $this->loadObject())) {
1094            $this->errors[] = Tools::displayError('An error occurred while updating the status for an object.').' <b>'.$this->table.'</b> '.Tools::displayError('(cannot load object)');
1095        } elseif (!$object->updatePosition((int) Tools::getValue('way'), (int) Tools::getValue('position'))) {
1096            $this->errors[] = Tools::displayError('Failed to update the position.');
1097        } else {
1098            $category = new Category((int) Tools::getValue('id_category'));
1099            if (Validate::isLoadedObject($category)) {
1100                Hook::exec('actionCategoryUpdate', ['category' => $category]);
1101            }
1102            $this->redirect_after = static::$currentIndex.'&'.$this->table.'Orderby=position&'.$this->table.'Orderway=asc&action=Customization&conf=5'.(($idCategory = (Tools::getIsset('id_category') ? (int) Tools::getValue('id_category') : '')) ? ('&id_category='.$idCategory) : '').'&token='.Tools::getAdminTokenLite('AdminProducts');
1103        }
1104    }
1105
1106    /**
1107     * Initialize processing
1108     *
1109     * @since 1.0.0
1110     */
1111    public function initProcess()
1112    {
1113        if (Tools::isSubmit('submitAddproductAndStay') || Tools::isSubmit('submitAddproduct')) {
1114            // Clean up possible product type changes.
1115            $typeProduct = (int) Tools::getValue('type_product');
1116            $idProduct = (int) Tools::getValue('id_product');
1117
1118            if ($typeProduct !== Product::PTYPE_PACK) {
1119                if (!Pack::deleteItems($idProduct)) {
1120                    $this->errors[] = Tools::displayError('Cannot delete product pack items.');
1121                };
1122            }
1123            if ($typeProduct !== Product::PTYPE_VIRTUAL) {
1124                $idProductDownload = ProductDownload::getIdFromIdProduct($idProduct, false);
1125
1126                if ($idProductDownload) {
1127                    $productDownload = new ProductDownload($idProductDownload);
1128                    if (!$productDownload->delete()) {
1129                        $this->errors[] = Tools::displayError('Cannot delete product download.');
1130                    }
1131                }
1132            }
1133        }
1134
1135        // Delete a product in the download folder
1136        if (Tools::getValue('deleteVirtualProduct')) {
1137            if ($this->tabAccess['delete'] === '1') {
1138                $this->action = 'deleteVirtualProduct';
1139            } else {
1140                $this->errors[] = Tools::displayError('You do not have permission to delete this.');
1141            }
1142        } elseif (Tools::isSubmit('submitAddProductAndPreview')) {
1143            // Product preview
1144            $this->display = 'edit';
1145            $this->action = 'save';
1146            if (Tools::getValue('id_product')) {
1147                $this->id_object = Tools::getValue('id_product');
1148                $this->object = new Product((int) Tools::getValue('id_product'));
1149            }
1150        } elseif (Tools::isSubmit('submitAttachments')) {
1151            if ($this->tabAccess['edit'] === '1') {
1152                $this->action = 'attachments';
1153                $this->tab_display = 'attachments';
1154            } else {
1155                $this->errors[] = Tools::displayError('You do not have permission to edit this.');
1156            }
1157        } elseif (Tools::getIsset('duplicate'.$this->table)) {
1158            // Product duplication
1159            if ($this->tabAccess['add'] === '1') {
1160                $this->action = 'duplicate';
1161            } else {
1162                $this->errors[] = Tools::displayError('You do not have permission to add this.');
1163            }
1164        } elseif (Tools::getValue('id_image') && Tools::getValue('ajax')) {
1165            // Product images management
1166            if ($this->tabAccess['edit'] === '1') {
1167                $this->action = 'image';
1168            } else {
1169                $this->errors[] = Tools::displayError('You do not have permission to edit this.');
1170            }
1171        } elseif (Tools::isSubmit('submitProductAttribute')) {
1172            // Product attributes management
1173            if ($this->tabAccess['edit'] === '1') {
1174                $this->action = 'productAttribute';
1175            } else {
1176                $this->errors[] = Tools::displayError('You do not have permission to edit this.');
1177            }
1178        } elseif (Tools::isSubmit('submitFeatures') || Tools::isSubmit('submitFeaturesAndStay')) {
1179            // Product features management
1180            if ($this->tabAccess['edit'] === '1') {
1181                $this->action = 'features';
1182            } else {
1183                $this->errors[] = Tools::displayError('You do not have permission to edit this.');
1184            }
1185        } elseif (Tools::isSubmit('submitPricesModification')) {
1186            // Product specific prices management NEVER USED
1187            if ($this->tabAccess['add'] === '1') {
1188                $this->action = 'pricesModification';
1189            } else {
1190                $this->errors[] = Tools::displayError('You do not have permission to add this.');
1191            }
1192        } elseif (Tools::isSubmit('deleteSpecificPrice')) {
1193            if ($this->tabAccess['delete'] === '1') {
1194                $this->action = 'deleteSpecificPrice';
1195            } else {
1196                $this->errors[] = Tools::displayError('You do not have permission to delete this.');
1197            }
1198        } elseif (Tools::isSubmit('submitSpecificPricePriorities')) {
1199            if ($this->tabAccess['edit'] === '1') {
1200                $this->action = 'specificPricePriorities';
1201                $this->tab_display = 'prices';
1202            } else {
1203                $this->errors[] = Tools::displayError('You do not have permission to edit this.');
1204            }
1205        } elseif (Tools::isSubmit('submitCustomizationConfiguration')) {
1206            // Customization management
1207            if ($this->tabAccess['edit'] === '1') {
1208                $this->action = 'customizationConfiguration';
1209                $this->tab_display = 'customization';
1210                $this->display = 'edit';
1211            } else {
1212                $this->errors[] = Tools::displayError('You do not have permission to edit this.');
1213            }
1214        } elseif (Tools::isSubmit('submitProductCustomization')) {
1215            if ($this->tabAccess['edit'] === '1') {
1216                $this->action = 'productCustomization';
1217                $this->tab_display = 'customization';
1218                $this->display = 'edit';
1219            } else {
1220                $this->errors[] = Tools::displayError('You do not have permission to edit this.');
1221            }
1222        } elseif (Tools::isSubmit('id_product')) {
1223            $postMaxSize = Tools::getMaxUploadSize(Configuration::get('PS_LIMIT_UPLOAD_FILE_VALUE') * 1024 * 1024);
1224            if ($postMaxSize && isset($_SERVER['CONTENT_LENGTH']) && $_SERVER['CONTENT_LENGTH'] && $_SERVER['CONTENT_LENGTH'] > $postMaxSize) {
1225                $this->errors[] = sprintf(Tools::displayError('The uploaded file exceeds the "Maximum size for a downloadable product" set in preferences (%1$dMB) or the post_max_size/ directive in php.ini (%2$dMB).'), number_format((Configuration::get('PS_LIMIT_UPLOAD_FILE_VALUE'))), ($postMaxSize / 1024 / 1024));
1226            }
1227        }
1228
1229        if (!$this->action) {
1230            parent::initProcess();
1231        } else {
1232            $this->id_object = (int) Tools::getValue($this->identifier);
1233        }
1234
1235        if (isset($this->available_tabs[Tools::getValue('key_tab')])) {
1236            $this->tab_display = Tools::getValue('key_tab');
1237        }
1238
1239        // Set tab to display if not decided already
1240        if (!$this->tab_display && $this->action) {
1241            if (in_array($this->action, array_keys($this->available_tabs))) {
1242                $this->tab_display = $this->action;
1243            }
1244        }
1245
1246        // And if still not set, use default
1247        if (!$this->tab_display) {
1248            if (in_array($this->default_tab, $this->available_tabs)) {
1249                $this->tab_display = $this->default_tab;
1250            } else {
1251                $this->tab_display = key($this->available_tabs);
1252            }
1253        }
1254    }
1255
1256    /**
1257     * Has the given tab been submitted?
1258     *
1259     * @param string $tabName
1260     *
1261     * @return bool
1262     *
1263     * @since 1.0.0
1264     */
1265    protected function isTabSubmitted($tabName)
1266    {
1267        if (!is_array($this->submitted_tabs)) {
1268            $this->submitted_tabs = Tools::getValue('submitted_tabs');
1269        }
1270
1271        if (is_array($this->submitted_tabs) && in_array($tabName, $this->submitted_tabs)) {
1272            return true;
1273        }
1274
1275        return false;
1276    }
1277
1278    /**
1279     * postProcess handle every checks before saving products information
1280     *
1281     * @return void
1282     *
1283     * @since 1.0.0
1284     */
1285    public function postProcess()
1286    {
1287        if (!$this->redirect_after) {
1288            parent::postProcess();
1289        }
1290
1291        if ($this->display == 'edit' || $this->display == 'add') {
1292            $this->addJqueryUI(
1293                [
1294                    'ui.core',
1295                    'ui.widget',
1296                ]
1297            );
1298
1299            $this->addjQueryPlugin(
1300                [
1301                    'autocomplete',
1302                    'tablednd',
1303                    'thickbox',
1304                    'ajaxfileupload',
1305                    'date',
1306                    'tagify',
1307                    'select2',
1308                    'validate',
1309                ]
1310            );
1311
1312            $this->addJS(
1313                [
1314                    _PS_JS_DIR_.'admin/products.js',
1315                    _PS_JS_DIR_.'admin/attributes.js',
1316                    _PS_JS_DIR_.'admin/price.js',
1317                    _PS_JS_DIR_.'tiny_mce/tiny_mce.js',
1318                    _PS_JS_DIR_.'admin/tinymce.inc.js',
1319                    _PS_JS_DIR_.'admin/dnd.js',
1320                    _PS_JS_DIR_.'jquery/ui/jquery.ui.progressbar.min.js',
1321                    _PS_JS_DIR_.'vendor/spin.js',
1322                    _PS_JS_DIR_.'vendor/ladda.js',
1323                ]
1324            );
1325
1326            $this->addJS(_PS_JS_DIR_.'jquery/plugins/select2/select2_locale_'.$this->context->language->iso_code.'.js');
1327            $this->addJS(_PS_JS_DIR_.'jquery/plugins/validate/localization/messages_'.$this->context->language->iso_code.'.js');
1328
1329            $this->addCSS(
1330                [
1331                    _PS_JS_DIR_.'jquery/plugins/timepicker/jquery-ui-timepicker-addon.css',
1332                ]
1333            );
1334        }
1335    }
1336
1337    /**
1338     * Ajax process delete ProductAttribute
1339     *
1340     * @since 1.0.0
1341     */
1342    public function ajaxProcessDeleteProductAttribute()
1343    {
1344        if (!Combination::isFeatureActive()) {
1345            return;
1346        }
1347
1348        if ($this->tabAccess['delete'] === '1') {
1349            $idProduct = (int) Tools::getValue('id_product');
1350            $idProductAttribute = (int) Tools::getValue('id_product_attribute');
1351
1352            if ($idProduct && Validate::isUnsignedId($idProduct) && Validate::isLoadedObject($product = new Product($idProduct))) {
1353                if (($dependsOnStock = StockAvailable::dependsOnStock($idProduct)) && StockAvailable::getQuantityAvailableByProduct($idProduct, $idProductAttribute)) {
1354                    $json = [
1355                        'status'  => 'error',
1356                        'message' => $this->l('It is not possible to delete a combination while it still has some quantities in the Advanced Stock Management. You must delete its stock first.'),
1357                    ];
1358                } else {
1359                    $product->deleteAttributeCombination((int) $idProductAttribute);
1360                    $product->checkDefaultAttributes();
1361                    Tools::clearColorListCache((int) $product->id);
1362                    if (!$product->hasAttributes()) {
1363                        $product->cache_default_attribute = 0;
1364                        $product->update();
1365                    } else {
1366                        Product::updateDefaultAttribute($idProduct);
1367                    }
1368
1369                    if ($dependsOnStock && !Stock::deleteStockByIds($idProduct, $idProductAttribute)) {
1370                        $json = [
1371                            'status'  => 'error',
1372                            'message' => $this->l('Error while deleting the stock'),
1373                        ];
1374                    } else {
1375                        $json = [
1376                            'status'               => 'ok',
1377                            'message'              => $this->_conf[1],
1378                            'id_product_attribute' => (int) $idProductAttribute,
1379                        ];
1380                    }
1381                }
1382            } else {
1383                $json = [
1384                    'status'  => 'error',
1385                    'message' => $this->l('You cannot delete this attribute.'),
1386                ];
1387            }
1388        } else {
1389            $json = [
1390                'status'  => 'error',
1391                'message' => $this->l('You do not have permission to delete this.'),
1392            ];
1393        }
1394
1395        $this->ajaxDie(json_encode($json));
1396    }
1397
1398    /**
1399     * Ajax process default ProductAttribute
1400     *
1401     * @since 1.0.0
1402     */
1403    public function ajaxProcessDefaultProductAttribute()
1404    {
1405        if ($this->tabAccess['edit'] === '1') {
1406            if (!Combination::isFeatureActive()) {
1407                return;
1408            }
1409
1410            if (Validate::isLoadedObject($product = new Product((int) Tools::getValue('id_product')))) {
1411                $product->deleteDefaultAttributes();
1412                $product->setDefaultAttribute((int) Tools::getValue('id_product_attribute'));
1413                $json = [
1414                    'status'  => 'ok',
1415                    'message' => $this->_conf[4],
1416                ];
1417            } else {
1418                $json = [
1419                    'status'  => 'error',
1420                    'message' => $this->l('You cannot make this the default attribute.'),
1421                ];
1422            }
1423
1424            $this->ajaxDie(json_encode($json));
1425        }
1426    }
1427
1428    /**
1429     * Ajax process edit ProductAttribute
1430     *
1431     * @since 1.0.0
1432     */
1433    public function ajaxProcessEditProductAttribute()
1434    {
1435        if ($this->tabAccess['edit'] === '1') {
1436            $idProduct = (int) Tools::getValue('id_product');
1437            $idProductAttribute = (int) Tools::getValue('id_product_attribute');
1438            if ($idProduct && Validate::isUnsignedId($idProduct) && Validate::isLoadedObject($product = new Product((int) $idProduct))) {
1439                $combinations = $product->getAttributeCombinationsById($idProductAttribute, $this->context->language->id);
1440                foreach ($combinations as $key => $combination) {
1441                    $combinations[$key]['attributes'][] = [$combination['group_name'], $combination['attribute_name'], $combination['id_attribute']];
1442                }
1443
1444                $this->ajaxDie(json_encode($combinations));
1445            }
1446        }
1447    }
1448
1449    /**
1450     * Ajax pre process
1451     *
1452     * @since 1.0.0
1453     */
1454    public function ajaxPreProcess()
1455    {
1456        if (Tools::getIsset('update'.$this->table) && Tools::getIsset('id_'.$this->table)) {
1457            $this->display = 'edit';
1458            $this->action = Tools::getValue('action');
1459        }
1460    }
1461
1462    /**
1463     * Ajax process update Product image shop association
1464     */
1465    public function ajaxProcessUpdateProductImageShopAsso()
1466    {
1467        $idProduct = Tools::getValue('id_product');
1468        if (($idImage = Tools::getValue('id_image')) && ($idShop = (int) Tools::getValue('id_shop'))) {
1469            if (Tools::getValue('active') == 'true') {
1470                $res = Db::getInstance()->execute('INSERT INTO '._DB_PREFIX_.'image_shop (`id_product`, `id_image`, `id_shop`, `cover`) VALUES('.(int) $idProduct.', '.(int) $idImage.', '.(int) $idShop.', NULL)');
1471            } else {
1472                $res = Db::getInstance()->execute('DELETE FROM '._DB_PREFIX_.'image_shop WHERE `id_image` = '.(int) $idImage.' AND `id_shop` = '.(int) $idShop);
1473            }
1474        }
1475
1476        // Clean covers in image table
1477        if (isset($idShop)) {
1478            $countCoverImage = Db::getInstance()->getValue(
1479                '
1480			SELECT COUNT(*) FROM '._DB_PREFIX_.'image i
1481			INNER JOIN '._DB_PREFIX_.'image_shop ish ON (i.id_image = ish.id_image AND ish.id_shop = '.(int) $idShop.')
1482			WHERE i.cover = 1 AND i.`id_product` = '.(int) $idProduct
1483            );
1484
1485            if (!$idImage) {
1486                $idImage = Db::getInstance()->getValue(
1487                    '
1488                    SELECT i.`id_image` FROM '._DB_PREFIX_.'image i
1489                    INNER JOIN '._DB_PREFIX_.'image_shop ish ON (i.id_image = ish.id_image AND ish.id_shop = '.(int) $idShop.')
1490                    WHERE i.`id_product` = '.(int) $idProduct
1491                );
1492            }
1493
1494            if ($countCoverImage < 1) {
1495                Db::getInstance()->execute('UPDATE '._DB_PREFIX_.'image i SET i.cover = 1 WHERE i.id_image = '.(int) $idImage.' AND i.`id_product` = '.(int) $idProduct.' LIMIT 1');
1496            }
1497
1498            // Clean covers in image_shop table
1499            $countCoverImageShop = Db::getInstance()->getValue(
1500                '
1501                SELECT COUNT(*)
1502                FROM '._DB_PREFIX_.'image_shop ish
1503                WHERE ish.`id_product` = '.(int) $idProduct.' AND ish.id_shop = '.(int) $idShop.' AND ish.cover = 1'
1504            );
1505            if ($countCoverImageShop < 1) {
1506                Db::getInstance()->execute('UPDATE '._DB_PREFIX_.'image_shop ish SET ish.cover = 1 WHERE ish.id_image = '.(int) $idImage.' AND ish.`id_product` = '.(int) $idProduct.' AND ish.id_shop =  '.(int) $idShop.' LIMIT 1');
1507            }
1508        }
1509
1510        if (isset($res) && $res) {
1511            $this->jsonConfirmation($this->_conf[27]);
1512        } else {
1513            $this->jsonError(Tools::displayError('An error occurred while attempting to associate this image with your shop. '));
1514        }
1515    }
1516
1517    /**
1518     * Ajax process update image position
1519     *
1520     * @since 1.0.0
1521     */
1522    public function ajaxProcessUpdateImagePosition()
1523    {
1524        if ($this->tabAccess['edit'] === '0') {
1525            $this->ajaxDie(json_encode(['error' => $this->l('You do not have the right permission')]));
1526        }
1527        $res = false;
1528        if ($json = Tools::getValue('json')) {
1529            // If there is an exception, at least the response is in JSON format.
1530            $this->json = true;
1531
1532            $res = true;
1533            $json = stripslashes($json);
1534            $images = json_decode($json, true);
1535            foreach ($images as $id => $position) {
1536                /*
1537                 * If the the image is not associated with the currently
1538                 * selected shop, the fields that are also in the image_shop
1539                 * table (like id_product and cover) cannot be loaded properly,
1540                 * so we have to load them separately.
1541                 */
1542                $img = new Image((int) $id);
1543                $def = $img::$definition;
1544                $sql = 'SELECT * FROM `' . _DB_PREFIX_ . $def['table'] . '` WHERE `' . $def['primary'] . '` = ' . (int) $id;
1545                $fields_from_table = Db::getInstance()->getRow($sql);
1546                foreach ($def['fields'] as $key => $value) {
1547                    if (!$value['lang']) {
1548                        $img->{$key} = $fields_from_table[$key];
1549                    }
1550                }
1551                $img->position = (int) $position;
1552                $res &= $img->update();
1553            }
1554        }
1555        if ($res) {
1556            $this->jsonConfirmation($this->_conf[25]);
1557        } else {
1558            $this->jsonError(Tools::displayError('An error occurred while attempting to move this picture.'));
1559        }
1560    }
1561
1562    /**
1563     * Ajax process update cover
1564     *
1565     * @since 1.0.0
1566     */
1567    public function ajaxProcessUpdateCover()
1568    {
1569        if ($this->tabAccess['edit'] === '0') {
1570            $this->ajaxDie(json_encode(['error' => $this->l('You do not have the right permission')]));
1571        }
1572        Image::deleteCover((int) Tools::getValue('id_product'));
1573        $id_image = (int) Tools::getValue('id_image');
1574
1575        /*
1576         * If the the image is not associated with the currently selected shop,
1577         * the fields that are also in the image_shop table (like id_product and
1578         * cover) cannot be loaded properly, so we have to load them separately.
1579         */
1580        $img = new Image($id_image);
1581        $def = $img::$definition;
1582        $sql = 'SELECT * FROM `' . _DB_PREFIX_ . $def['table'] . '` WHERE `' . $def['primary'] . '` = ' . $id_image;
1583        $fields_from_table = Db::getInstance()->getRow($sql);
1584        foreach ($def['fields'] as $key => $value) {
1585            if (!$value['lang']) {
1586                $img->{$key} = $fields_from_table[$key];
1587            }
1588        }
1589        $img->cover = 1;
1590
1591        @unlink(_PS_TMP_IMG_DIR_.'product_'.(int) $img->id_product.'.jpg');
1592        @unlink(_PS_TMP_IMG_DIR_.'product_mini_'.(int) $img->id_product.'_'.$this->context->shop->id.'.jpg');
1593
1594        if ($img->update()) {
1595            $this->jsonConfirmation($this->_conf[26]);
1596        } else {
1597            $this->jsonError(Tools::displayError('An error occurred while attempting to update the cover picture.'));
1598        }
1599    }
1600
1601    /**
1602     * Ajax process delete product image
1603     *
1604     * @since 1.0.0
1605     */
1606    public function ajaxProcessDeleteProductImage()
1607    {
1608        $this->display = 'content';
1609        $res = true;
1610        /* Delete product image */
1611        $image = new Image((int) Tools::getValue('id_image'));
1612        $this->content['id'] = $image->id;
1613        $res &= $image->delete();
1614        // if deleted image was the cover, change it to the first one
1615        if (!Image::getCover($image->id_product)) {
1616            $res &= Db::getInstance()->execute(
1617                '
1618			UPDATE `'._DB_PREFIX_.'image_shop` image_shop
1619			SET image_shop.`cover` = 1
1620			WHERE image_shop.`id_product` = '.(int) $image->id_product.'
1621			AND id_shop='.(int) $this->context->shop->id.' LIMIT 1'
1622            );
1623        }
1624
1625        if (!Image::getGlobalCover($image->id_product)) {
1626            $res &= Db::getInstance()->execute(
1627                '
1628			UPDATE `'._DB_PREFIX_.'image` i
1629			SET i.`cover` = 1
1630			WHERE i.`id_product` = '.(int) $image->id_product.' LIMIT 1'
1631            );
1632        }
1633
1634        if (file_exists(_PS_TMP_IMG_DIR_.'product_'.$image->id_product.'.jpg')) {
1635            $res &= @unlink(_PS_TMP_IMG_DIR_.'product_'.$image->id_product.'.jpg');
1636        }
1637        if (file_exists(_PS_TMP_IMG_DIR_.'product_mini_'.$image->id_product.'_'.$this->context->shop->id.'.jpg')) {
1638            $res &= @unlink(_PS_TMP_IMG_DIR_.'product_mini_'.$image->id_product.'_'.$this->context->shop->id.'.jpg');
1639        }
1640
1641        if ($res) {
1642            $this->jsonConfirmation($this->_conf[7]);
1643        } else {
1644            $this->jsonError(Tools::displayError('An error occurred while attempting to delete the product image.'));
1645        }
1646    }
1647
1648    /**
1649     * Add or update a product image
1650     *
1651     * @param Product $product Product object to add image
1652     * @param string  $method
1653     *
1654     * @return int|false
1655     *
1656     * @since 1.0.0
1657     */
1658    public function addProductImage($product, $method = 'auto')
1659    {
1660        /* Updating an existing product image */
1661        if ($idImage = (int) Tools::getValue('id_image')) {
1662            $image = new Image((int) $idImage);
1663            if (!Validate::isLoadedObject($image)) {
1664                $this->errors[] = Tools::displayError('An error occurred while loading the object image.');
1665            } else {
1666                if (($cover = Tools::getValue('cover')) == 1) {
1667                    Image::deleteCover($product->id);
1668                }
1669                $image->cover = $cover;
1670                $this->validateRules('Image');
1671                $this->copyFromPost($image, 'image');
1672                if (count($this->errors) || !$image->update()) {
1673                    $this->errors[] = Tools::displayError('An error occurred while updating the image.');
1674                } elseif (isset($_FILES['image_product']['tmp_name']) && $_FILES['image_product']['tmp_name'] != null) {
1675                    $this->copyImage($product->id, $image->id, $method);
1676                }
1677            }
1678        }
1679        if (isset($image) && Validate::isLoadedObject($image) && !file_exists(_PS_PROD_IMG_DIR_.$image->getExistingImgPath().'.'.$image->image_format)) {
1680            $image->delete();
1681        }
1682        if (count($this->errors)) {
1683            return false;
1684        }
1685        @unlink(_PS_TMP_IMG_DIR_.'product_'.$product->id.'.jpg');
1686        @unlink(_PS_TMP_IMG_DIR_.'product_mini_'.$product->id.'_'.$this->context->shop->id.'.jpg');
1687
1688        return ((isset($idImage) && is_int($idImage) && $idImage) ? $idImage : false);
1689    }
1690
1691    /**
1692     * @param Product|ObjectModel $object
1693     * @param string              $table
1694     *
1695     * @since 1.0.0
1696     */
1697    protected function copyFromPost(&$object, $table)
1698    {
1699        parent::copyFromPost($object, $table);
1700        if (get_class($object) != 'Product') {
1701            return;
1702        }
1703
1704        /* Additional fields */
1705        foreach (Language::getIDs(false) as $idLang) {
1706            if (isset($_POST['meta_keywords_'.$idLang])) {
1707                $_POST['meta_keywords_'.$idLang] = $this->_cleanMetaKeywords(mb_strtolower($_POST['meta_keywords_'.$idLang]));
1708                // preg_replace('/ *,? +,* /', ',', strtolower($_POST['meta_keywords_'.$id_lang]));
1709                $object->meta_keywords[$idLang] = $_POST['meta_keywords_'.$idLang];
1710            }
1711        }
1712        $_POST['width'] = empty($_POST['width']) ? '0' : $_POST['width'];
1713        $_POST['height'] = empty($_POST['height']) ? '0' : $_POST['height'];
1714        $_POST['depth'] = empty($_POST['depth']) ? '0' : $_POST['depth'];
1715        $_POST['weight'] = empty($_POST['weight']) ? '0' : $_POST['weight'];
1716
1717        if (Tools::getIsset('unit_price') != null) {
1718            $object->unit_price = priceval(Tools::getValue('unit_price'));
1719        }
1720        if (Tools::getIsset('ecotax') != null) {
1721            $object->ecotax = priceval(Tools::getValue('ecotax'));
1722        }
1723
1724        if ($this->isTabSubmitted('Informations')) {
1725            if ($this->checkMultishopBox('available_for_order', $this->context)) {
1726                $object->available_for_order = (int) Tools::getValue('available_for_order');
1727            }
1728
1729            if ($this->checkMultishopBox('show_price', $this->context)) {
1730                $object->show_price = $object->available_for_order ? 1 : (int) Tools::getValue('show_price');
1731            }
1732
1733            if ($this->checkMultishopBox('online_only', $this->context)) {
1734                $object->online_only = (int) Tools::getValue('online_only');
1735            }
1736        }
1737        if ($this->isTabSubmitted('Prices')) {
1738            $object->on_sale = (int) Tools::getValue('on_sale');
1739        }
1740    }
1741
1742    /**
1743     * Clean meta keywords
1744     *
1745     * @param array $keywords
1746     *
1747     * @return string
1748     */
1749    protected function _cleanMetaKeywords($keywords)
1750    {
1751        if (!empty($keywords) && $keywords != '') {
1752            $out = [];
1753            $words = explode(',', $keywords);
1754            foreach ($words as $wordItem) {
1755                $wordItem = trim($wordItem);
1756                if (!empty($wordItem) && $wordItem != '') {
1757                    $out[] = $wordItem;
1758                }
1759            }
1760
1761            return ((count($out) > 0) ? implode(',', $out) : '');
1762        } else {
1763            return '';
1764        }
1765    }
1766
1767    /**
1768     * @param string $field
1769     * @param null   $context
1770     *
1771     * @return bool
1772     *
1773     * @since 1.0.0
1774     */
1775    public function checkMultishopBox($field, $context = null)
1776    {
1777        static $checkbox = null;
1778        static $shopContext = null;
1779
1780        if ($context == null && $shopContext == null) {
1781            $context = $this->context;
1782        }
1783
1784        if ($shopContext == null) {
1785            $shopContext = $context->shop->getContext();
1786        }
1787
1788        if ($checkbox == null) {
1789            $checkbox = Tools::getValue('multishop_check', []);
1790        }
1791
1792        if ($shopContext == Shop::CONTEXT_SHOP) {
1793            return true;
1794        }
1795
1796        if (isset($checkbox[$field]) && $checkbox[$field] == 1) {
1797            return true;
1798        }
1799
1800        return false;
1801    }
1802
1803    /**
1804     * Copy a product image
1805     *
1806     * @param int    $idProduct Product Id for product image filename
1807     * @param int    $idImage   Image Id for product image filename
1808     * @param string $method
1809     *
1810     * @return void|false
1811     * @throws PrestaShopException
1812     */
1813    public function copyImage($idProduct, $idImage, $method = 'auto')
1814    {
1815        if (!isset($_FILES['image_product']['tmp_name'])) {
1816            return false;
1817        }
1818        if ($error = ImageManager::validateUpload($_FILES['image_product'])) {
1819            $this->errors[] = $error;
1820        } else {
1821            $highDpi = (bool) Configuration::get('PS_HIGHT_DPI');
1822
1823            $image = new Image($idImage);
1824
1825            if (!$newPath = $image->getPathForCreation()) {
1826                $this->errors[] = Tools::displayError('An error occurred while attempting to create a new folder.');
1827            }
1828            if (!($tmpName = tempnam(_PS_TMP_IMG_DIR_, 'PS')) || !move_uploaded_file($_FILES['image_product']['tmp_name'], $tmpName)) {
1829                $this->errors[] = Tools::displayError('An error occurred during the image upload process.');
1830            } elseif (!ImageManager::resize($tmpName, $newPath.'.'.$image->image_format)) {
1831                $this->errors[] = Tools::displayError('An error occurred while copying the image.');
1832            } elseif ($method == 'auto') {
1833                $imagesTypes = ImageType::getImagesTypes('products');
1834                foreach ($imagesTypes as $k => $imageType) {
1835                    if (!ImageManager::resize(
1836                        $tmpName,
1837                        $newPath.'-'.stripslashes($imageType['name']).'.'.$image->image_format,
1838                        (int) $imageType['width'],
1839                        (int) $imageType['height'],
1840                        $image->image_format
1841                    )) {
1842                        $this->errors[] = Tools::displayError('An error occurred while copying this image:').' '.stripslashes($imageType['name']);
1843                    } else {
1844                        if ($highDpi) {
1845                            ImageManager::resize(
1846                                $tmpName,
1847                                $newPath.'-'.stripslashes($imageType['name']).'2x.'.$image->image_format,
1848                                (int) $imageType['width'] * 2,
1849                                (int) $imageType['height'] * 2,
1850                                $image->image_format
1851                            );
1852                        }
1853
1854                        if (ImageManager::webpSupport()) {
1855                            ImageManager::resize(
1856                                $tmpName,
1857                                $newPath.'-'.stripslashes($imageType['name']).'.webp',
1858                                (int) $imageType['width'],
1859                                (int) $imageType['height'],
1860                                'webp'
1861                            );
1862
1863                            if ($highDpi) {
1864                                ImageManager::resize(
1865                                    $tmpName,
1866                                    $newPath.'-'.stripslashes($imageType['name']).'2x.webp',
1867                                    (int) $imageType['width'] * 2,
1868                                    (int) $imageType['height'] * 2,
1869                                    'webp'
1870                                );
1871                            }
1872                        }
1873
1874                        if ((int) Configuration::get('TB_IMAGES_LAST_UPD_PRODUCTS') < $idProduct) {
1875                            Configuration::updateValue('TB_IMAGES_LAST_UPD_PRODUCTS', $idProduct);
1876                        }
1877                    }
1878                }
1879            }
1880
1881            @unlink($tmpName);
1882            Hook::exec('actionWatermark', ['id_image' => $idImage, 'id_product' => $idProduct]);
1883        }
1884    }
1885
1886    /**
1887     * Process add
1888     *
1889     * @return bool|ObjectModel
1890     *
1891     * @since 1.0.0
1892     */
1893    public function processAdd()
1894    {
1895        $this->checkProduct();
1896
1897        if (!empty($this->errors)) {
1898            $this->display = 'add';
1899
1900            return false;
1901        }
1902
1903        $this->object = new $this->className();
1904        $this->_removeTaxFromEcotax();
1905        $this->copyFromPost($this->object, $this->table);
1906        if ($this->object->add()) {
1907            Logger::addLog(sprintf($this->l('%s addition', 'AdminTab', false, false), $this->className), 1, null, $this->className, (int) $this->object->id, true, (int) $this->context->employee->id);
1908            $this->addCarriers($this->object);
1909            $this->updateAccessories($this->object);
1910            $this->updatePackItems($this->object);
1911            $this->updateDownloadProduct($this->object);
1912
1913            if (Configuration::get('PS_FORCE_ASM_NEW_PRODUCT') && Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT') && $this->object->getType() != Product::PTYPE_VIRTUAL) {
1914                $this->object->advanced_stock_management = 1;
1915                $this->object->save();
1916                $idShops = Shop::getContextListShopID();
1917                foreach ($idShops as $idShop) {
1918                    StockAvailable::setProductDependsOnStock($this->object->id, true, (int) $idShop, 0);
1919                }
1920            }
1921
1922            if (empty($this->errors)) {
1923                $languages = Language::getLanguages(false);
1924                if ($this->isProductFieldUpdated('category_box') && !$this->object->updateCategories(Tools::getValue('categoryBox'))) {
1925                    $this->errors[] = Tools::displayError('An error occurred while linking the object.').' <b>'.$this->table.'</b> '.Tools::displayError('To categories');
1926                } elseif (!$this->updateTags($languages, $this->object)) {
1927                    $this->errors[] = Tools::displayError('An error occurred while adding tags.');
1928                } else {
1929                    Hook::exec('actionProductAdd', ['id_product' => (int) $this->object->id, 'product' => $this->object]);
1930                    if (in_array($this->object->visibility, ['both', 'search']) && Configuration::get('PS_SEARCH_INDEXATION')) {
1931                        Search::indexation(false, $this->object->id);
1932                    }
1933                }
1934
1935                if (Configuration::get('PS_DEFAULT_WAREHOUSE_NEW_PRODUCT') != 0 && Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT')) {
1936                    $warehouseLocationEntity = new WarehouseProductLocation();
1937                    $warehouseLocationEntity->id_product = $this->object->id;
1938                    $warehouseLocationEntity->id_product_attribute = 0;
1939                    $warehouseLocationEntity->id_warehouse = Configuration::get('PS_DEFAULT_WAREHOUSE_NEW_PRODUCT');
1940                    $warehouseLocationEntity->location = pSQL('');
1941                    $warehouseLocationEntity->save();
1942                }
1943
1944                // Apply groups reductions
1945                $this->object->setGroupReduction();
1946
1947                // Save and preview
1948                if (Tools::isSubmit('submitAddProductAndPreview')) {
1949                    $this->redirect_after = $this->getPreviewUrl($this->object);
1950                }
1951
1952                // Save and stay on same form
1953                if ($this->display == 'edit') {
1954                    $this->redirect_after = static::$currentIndex.'&id_product='.(int) $this->object->id.(Tools::getIsset('id_category') ? '&id_category='.(int) Tools::getValue('id_category') : '').'&updateproduct&conf=3&key_tab='.Tools::safeOutput(Tools::getValue('key_tab')).'&token='.$this->token;
1955                } else {
1956                    // Default behavior (save and back)
1957                    $this->redirect_after = static::$currentIndex.(Tools::getIsset('id_category') ? '&id_category='.(int) Tools::getValue('id_category') : '').'&conf=3&token='.$this->token;
1958                }
1959            } else {
1960                $this->object->delete();
1961                // if errors : stay on edit page
1962                $this->display = 'edit';
1963            }
1964        } else {
1965            $this->errors[] = Tools::displayError('An error occurred while creating an object.').' <b>'.$this->table.'</b>';
1966        }
1967
1968        return $this->object;
1969    }
1970
1971    /**
1972     * Check that a saved product is valid
1973     */
1974    public function checkProduct()
1975    {
1976        $className = 'Product';
1977        // @todo : the call_user_func seems to contains only statics values (className = 'Product')
1978        $rules = call_user_func([$this->className, 'getValidationRules'], $this->className);
1979        $defaultLanguage = new Language((int) Configuration::get('PS_LANG_DEFAULT'));
1980        $languages = Language::getLanguages(false);
1981
1982        // Check required fields
1983        foreach ($rules['required'] as $field) {
1984            if (!$this->isProductFieldUpdated($field)) {
1985                continue;
1986            }
1987
1988            if (($value = Tools::getValue($field)) == false && $value != '0') {
1989                if (Tools::getValue('id_'.$this->table) && $field == 'passwd') {
1990                    continue;
1991                }
1992                $this->errors[] = sprintf(
1993                    Tools::displayError('The %s field is required.'),
1994                    call_user_func([$className, 'displayFieldName'], $field, $className)
1995                );
1996            }
1997        }
1998
1999        // Check multilingual required fields
2000        foreach ($rules['requiredLang'] as $fieldLang) {
2001            if ($this->isProductFieldUpdated($fieldLang, $defaultLanguage->id) && !Tools::getValue($fieldLang.'_'.$defaultLanguage->id)) {
2002                $this->errors[] = sprintf(
2003                    Tools::displayError('This %1$s field is required at least in %2$s'),
2004                    call_user_func([$className, 'displayFieldName'], $fieldLang, $className),
2005                    $defaultLanguage->name
2006                );
2007            }
2008        }
2009
2010        // Check fields sizes
2011        foreach ($rules['size'] as $field => $maxLength) {
2012            if ($this->isProductFieldUpdated($field) && ($value = Tools::getValue($field)) && mb_strlen($value) > $maxLength) {
2013                $this->errors[] = sprintf(
2014                    Tools::displayError('The %1$s field is too long (%2$d chars max).'),
2015                    call_user_func([$className, 'displayFieldName'], $field, $className),
2016                    $maxLength
2017                );
2018            }
2019        }
2020
2021        if (Tools::getIsset('description_short') && $this->isProductFieldUpdated('description_short')) {
2022            $saveShort = Tools::getValue('description_short');
2023            $_POST['description_short'] = strip_tags(Tools::getValue('description_short'));
2024        }
2025
2026        // Check description short size without html
2027        $limit = (int) Configuration::get('PS_PRODUCT_SHORT_DESC_LIMIT');
2028        if ($limit <= 0) {
2029            $limit = 400;
2030        }
2031        foreach ($languages as $language) {
2032            if ($this->isProductFieldUpdated('description_short', $language['id_lang']) && ($value = Tools::getValue('description_short_'.$language['id_lang']))) {
2033                if (mb_strlen(strip_tags($value)) > $limit) {
2034                    $this->errors[] = sprintf(
2035                        Tools::displayError('This %1$s field (%2$s) is too long: %3$d chars max (current count %4$d).'),
2036                        call_user_func([$className, 'displayFieldName'], 'description_short'),
2037                        $language['name'],
2038                        $limit,
2039                        mb_strlen(strip_tags($value))
2040                    );
2041                }
2042            }
2043        }
2044
2045        // Check multilingual fields sizes
2046        foreach ($rules['sizeLang'] as $fieldLang => $maxLength) {
2047            foreach ($languages as $language) {
2048                $value = Tools::getValue($fieldLang.'_'.$language['id_lang']);
2049                if ($value && mb_strlen($value) > $maxLength) {
2050                    $this->errors[] = sprintf(
2051                        Tools::displayError('The %1$s field is too long (%2$d chars max).'),
2052                        call_user_func([$className, 'displayFieldName'], $fieldLang, $className),
2053                        $maxLength
2054                    );
2055                }
2056            }
2057        }
2058
2059        if ($this->isProductFieldUpdated('description_short') && isset($_POST['description_short'])) {
2060            $_POST['description_short'] = $saveShort;
2061        }
2062
2063        // Check fields validity
2064        foreach ($rules['validate'] as $field => $function) {
2065            if ($this->isProductFieldUpdated($field) && ($value = Tools::getValue($field))) {
2066                $res = true;
2067                if (mb_strtolower($function) == 'iscleanhtml') {
2068                    if (!Validate::$function($value, (int) Configuration::get('PS_ALLOW_HTML_IFRAME'))) {
2069                        $res = false;
2070                    }
2071                } elseif (!Validate::$function($value)) {
2072                    $res = false;
2073                }
2074
2075                if (!$res) {
2076                    $this->errors[] = sprintf(
2077                        Tools::displayError('The %s field is invalid.'),
2078                        call_user_func([$className, 'displayFieldName'], $field, $className)
2079                    );
2080                }
2081            }
2082        }
2083        // Check multilingual fields validity
2084        foreach ($rules['validateLang'] as $fieldLang => $function) {
2085            foreach ($languages as $language) {
2086                if ($this->isProductFieldUpdated($fieldLang, $language['id_lang']) && ($value = Tools::getValue($fieldLang.'_'.$language['id_lang']))) {
2087                    if (!Validate::$function($value, (int) Configuration::get('PS_ALLOW_HTML_IFRAME'))) {
2088                        $this->errors[] = sprintf(
2089                            Tools::displayError('The %1$s field (%2$s) is invalid.'),
2090                            call_user_func([$className, 'displayFieldName'], $fieldLang, $className),
2091                            $language['name']
2092                        );
2093                    }
2094                }
2095            }
2096        }
2097
2098        // Categories
2099        if ($this->isProductFieldUpdated('id_category_default') && (!Tools::isSubmit('categoryBox') || !count(Tools::getValue('categoryBox')))) {
2100            $this->errors[] = $this->l('Products must be in at least one category.');
2101        }
2102
2103        if ($this->isProductFieldUpdated('id_category_default') && (!is_array(Tools::getValue('categoryBox')) || !in_array(Tools::getValue('id_category_default'), Tools::getValue('categoryBox')))) {
2104            $this->errors[] = $this->l('This product must be in the default category.');
2105        }
2106
2107        // Tags
2108        foreach ($languages as $language) {
2109            if ($value = Tools::getValue('tags_'.$language['id_lang'])) {
2110                if (!Validate::isTagsList($value)) {
2111                    $this->errors[] = sprintf(
2112                        Tools::displayError('The tags list (%s) is invalid.'),
2113                        $language['name']
2114                    );
2115                }
2116            }
2117        }
2118    }
2119
2120    /**
2121     * Check if a field is edited (if the checkbox is checked)
2122     * This method will do something only for multishop with a context all / group
2123     *
2124     * @param string $field Name of field
2125     * @param int    $idLang
2126     *
2127     * @return bool
2128     */
2129    protected function isProductFieldUpdated($field, $idLang = null)
2130    {
2131        // Cache this condition to improve performances
2132        static $isActivated = null;
2133        if (is_null($isActivated)) {
2134            $isActivated = Shop::isFeatureActive() && Shop::getContext() != Shop::CONTEXT_SHOP && $this->id_object;
2135        }
2136
2137        if (!$isActivated) {
2138            return true;
2139        }
2140
2141        $def = ObjectModel::getDefinition($this->object);
2142        if (!$this->object->isMultiShopField($field) && is_null($idLang) && isset($def['fields'][$field])) {
2143            return true;
2144        }
2145
2146        if (is_null($idLang)) {
2147            return !empty($_POST['multishop_check'][$field]);
2148        } else {
2149            return !empty($_POST['multishop_check'][$field][$idLang]);
2150        }
2151    }
2152
2153    /**
2154     * Checking customs feature
2155     *
2156     * @since 1.0.0
2157     */
2158    protected function _removeTaxFromEcotax()
2159    {
2160        if ($ecotax = Tools::getValue('ecotax')) {
2161            $_POST['ecotax']
2162                = priceval($ecotax / (1 + Tax::getProductEcotaxRate() / 100));
2163        }
2164    }
2165
2166    protected function addCarriers($product = null)
2167    {
2168        if (!isset($product)) {
2169            $product = new Product((int) Tools::getValue('id_product'));
2170        }
2171
2172        if (Validate::isLoadedObject($product)) {
2173            $carriers = [];
2174
2175            if (Tools::getValue('selectedCarriers')) {
2176                $carriers = Tools::getValue('selectedCarriers');
2177            }
2178
2179            $product->setCarriers($carriers);
2180        }
2181    }
2182
2183    /**
2184     * Update product accessories
2185     *
2186     * @param object $product Product
2187     *
2188     * @since 1.0.0
2189     */
2190    public function updateAccessories($product)
2191    {
2192        $product->deleteAccessories();
2193        if ($accessories = Tools::getValue('inputAccessories')) {
2194            $accessoriesId = array_unique(explode('-', $accessories));
2195            if (count($accessoriesId)) {
2196                array_pop($accessoriesId);
2197                $product->changeAccessories($accessoriesId);
2198            }
2199        }
2200    }
2201
2202    /**
2203     * delete all items in pack, then check if type_product value is PTYPE_PACK.
2204     * if yes, add the pack items from input "inputPackItems"
2205     *
2206     * @param Product $product
2207     *
2208     * @return bool
2209     */
2210    public function updatePackItems($product)
2211    {
2212        Pack::deleteItems($product->id);
2213        // lines format: QTY x ID-QTY x ID
2214        if (Tools::getValue('type_product') == Product::PTYPE_PACK) {
2215            $product->setDefaultAttribute(0);//reset cache_default_attribute
2216            $items = Tools::getValue('inputPackItems');
2217            $lines = array_unique(explode('-', $items));
2218
2219            // lines is an array of string with format : QTYxIDxID_PRODUCT_ATTRIBUTE
2220            if (count($lines)) {
2221                foreach ($lines as $line) {
2222                    if (!empty($line)) {
2223                        $itemIdAttribute = 0;
2224                        count($array = explode('x', $line)) == 3 ? list($qty, $itemId, $itemIdAttribute) = $array : list($qty, $itemId) = $array;
2225                        if ($qty > 0 && isset($itemId)) {
2226                            if (Pack::isPack((int) $itemId || $product->id == (int) $itemId)) {
2227                                $this->errors[] = Tools::displayError('You can\'t add product packs into a pack');
2228                            } elseif (!Pack::addItem((int) $product->id, (int) $itemId, (int) $qty, (int) $itemIdAttribute)) {
2229                                $this->errors[] = Tools::displayError('An error occurred while attempting to add products to the pack.');
2230                            }
2231                        }
2232                    }
2233                }
2234            }
2235        }
2236    }
2237
2238    /**
2239     * Update product download
2240     *
2241     * @param Product $product
2242     * @param int     $edit     Deprecated in favor of autodetection.
2243     *
2244     * @return bool
2245     *
2246     * @since 1.0.3 Deprecate $edit in favor of autodetection.
2247     * @since 1.0.0
2248     */
2249    public function updateDownloadProduct($product, $edit = 999)
2250    {
2251        if ($edit !== 999) {
2252            Tools::displayParameterAsDeprecated('edit');
2253        }
2254
2255        $idProductDownload = ProductDownload::getIdFromIdProduct($product->id, false);
2256        if (!$idProductDownload) {
2257            $idProductDownload = (int) Tools::getValue('virtual_product_id');
2258        }
2259
2260        if (Tools::getValue('type_product') == Product::PTYPE_VIRTUAL
2261            && Tools::getValue('is_virtual_file') == 1) {
2262            if (isset($_FILES['virtual_product_file_uploader']) && $_FILES['virtual_product_file_uploader']['size'] > 0) {
2263                $filename = ProductDownload::getNewFilename();
2264                $helper = new HelperUploader('virtual_product_file_uploader');
2265                $helper->setPostMaxSize(Tools::getOctets(ini_get('upload_max_filesize')))
2266                    ->setSavePath(_PS_DOWNLOAD_DIR_)->upload($_FILES['virtual_product_file_uploader'], $filename);
2267            } else {
2268                $filename = Tools::getValue('virtual_product_filename', ProductDownload::getNewFilename());
2269            }
2270
2271            $product->setDefaultAttribute(0); //reset cache_default_attribute
2272
2273            $active = Tools::getValue('virtual_product_active');
2274            $isShareable = Tools::getValue('virtual_product_is_shareable');
2275            $name = Tools::getValue('virtual_product_name');
2276            $nbDays = Tools::getValue('virtual_product_nb_days');
2277            $nbDownloable = Tools::getValue('virtual_product_nb_downloable');
2278            $expirationDate = Tools::getValue('virtual_product_expiration_date');
2279            // This whould allow precision up to the second, not supported by
2280            // the datepicker in the GUI, yet.
2281            //if ($expirationDate
2282            //    && !preg_match('/\d{1,2}\:\d{1,2}/', $expirationDate)) {
2283            //    // No time given should mean the end of the day.
2284            //    $dateExpiration .= ' 23:59:59';
2285            //}
2286            if ($expirationDate) {
2287                // We want the end of the given day.
2288                $expirationDate .= ' 23:59:59';
2289            }
2290
2291            $download = new ProductDownload($idProductDownload);
2292            $download->id_product = (int) $product->id;
2293            $download->display_filename = $name;
2294            $download->filename = $filename;
2295            $download->date_expiration = $expirationDate;
2296            $download->nb_days_accessible = (int) $nbDays;
2297            $download->nb_downloadable = (int) $nbDownloable;
2298            $download->active = (int) $active;
2299            $download->is_shareable = (int) $isShareable;
2300            if ($download->save()) {
2301                return true;
2302            }
2303        } else {
2304            // Delete the download and its file.
2305            if ($idProductDownload) {
2306                $productDownload = new ProductDownload($idProductDownload);
2307
2308                return $productDownload->delete();
2309            }
2310        }
2311
2312        return false;
2313    }
2314
2315    /**
2316     * Update product tags
2317     *
2318     * @param array  $languages Array languages
2319     * @param object $product   Product
2320     *
2321     * @return bool Update result
2322     *
2323     * @since 1.0.0
2324     */
2325    public function updateTags($languages, $product)
2326    {
2327        $tagSuccess = true;
2328        /* Reset all tags for THIS product */
2329        if (!Tag::deleteTagsForProduct((int) $product->id)) {
2330            $this->errors[] = Tools::displayError('An error occurred while attempting to delete previous tags.');
2331        }
2332        /* Assign tags to this product */
2333        foreach ($languages as $language) {
2334            if ($value = Tools::getValue('tags_'.$language['id_lang'])) {
2335                $tagSuccess &= Tag::addTags($language['id_lang'], (int) $product->id, $value);
2336            }
2337        }
2338
2339        if (!$tagSuccess) {
2340            $this->errors[] = Tools::displayError('An error occurred while adding tags.');
2341        }
2342
2343        return $tagSuccess;
2344    }
2345
2346    /**
2347     * @param Product $product
2348     *
2349     * @return bool|string
2350     *
2351     * @since 1.0.0
2352     */
2353    public function getPreviewUrl(Product $product)
2354    {
2355        $idLang = Configuration::get('PS_LANG_DEFAULT', null, null, $this->context->shop->id);
2356
2357        if (!Validate::isLoadedObject($product) || !$product->id_category_default) {
2358            return $this->l('Unable to determine the preview URL. This product has not been linked with a category, yet.');
2359        }
2360
2361        if (!ShopUrl::getMainShopDomain()) {
2362            return false;
2363        }
2364
2365        $isRewriteActive = (bool) Configuration::get('PS_REWRITING_SETTINGS');
2366        $previewUrl = $this->context->link->getProductLink(
2367            $product,
2368            $this->getFieldValue($product, 'link_rewrite', $this->context->language->id),
2369            Category::getLinkRewrite($this->getFieldValue($product, 'id_category_default'), $this->context->language->id),
2370            null,
2371            $idLang,
2372            (int) $this->context->shop->id,
2373            0,
2374            $isRewriteActive
2375        );
2376
2377        if (!$product->active) {
2378            $adminDir = dirname($_SERVER['PHP_SELF']);
2379            $adminDir = substr($adminDir, strrpos($adminDir, '/') + 1);
2380            $previewUrl .= ((strpos($previewUrl, '?') === false) ? '?' : '&').'adtoken='.$this->token.'&ad='.$adminDir.'&id_employee='.(int) $this->context->employee->id;
2381        }
2382
2383        return $previewUrl;
2384    }
2385
2386    /**
2387     * @return bool|false|ObjectModel
2388     *
2389     * @since 1.0.0
2390     */
2391    public function processStatus()
2392    {
2393        $this->loadObject(true);
2394        if (!Validate::isLoadedObject($this->object)) {
2395            return false;
2396        }
2397        if (($error = $this->object->validateFields(false, true)) !== true) {
2398            $this->errors[] = $error;
2399        }
2400        if (($error = $this->object->validateFieldsLang(false, true)) !== true) {
2401            $this->errors[] = $error;
2402        }
2403
2404        if (count($this->errors)) {
2405            return false;
2406        }
2407
2408        $res = parent::processStatus();
2409
2410        $query = trim(Tools::getValue('bo_query'));
2411        $searchType = (int) Tools::getValue('bo_search_type');
2412
2413        if ($query) {
2414            $this->redirect_after = preg_replace('/[\?|&](bo_query|bo_search_type)=([^&]*)/i', '', $this->redirect_after);
2415            $this->redirect_after .= '&bo_query='.$query.'&bo_search_type='.$searchType;
2416        }
2417
2418        return $res;
2419    }
2420
2421    /**
2422     * Process update
2423     *
2424     * @return bool|Product
2425     *
2426     * @since 1.0.0
2427     */
2428    public function processUpdate()
2429    {
2430        $existingProduct = $this->object;
2431
2432        $this->checkProduct();
2433
2434        if (!empty($this->errors)) {
2435            $this->display = 'edit';
2436
2437            return false;
2438        }
2439
2440        $id = (int) Tools::getValue('id_'.$this->table);
2441        /* Update an existing product */
2442        if (isset($id) && !empty($id)) {
2443            /** @var Product $object */
2444            $object = new $this->className((int) $id);
2445            $this->object = $object;
2446
2447            if (Validate::isLoadedObject($object)) {
2448                $this->_removeTaxFromEcotax();
2449                $productTypeBefore = $object->getType();
2450                $this->copyFromPost($object, $this->table);
2451                $object->indexed = 0;
2452
2453                if (Shop::isFeatureActive() && Shop::getContext() != Shop::CONTEXT_SHOP) {
2454                    $object->setFieldsToUpdate((array) Tools::getValue('multishop_check', []));
2455                }
2456
2457                // Duplicate combinations if not associated to shop
2458                if ($this->context->shop->getContext() == Shop::CONTEXT_SHOP && !$object->isAssociatedToShop()) {
2459                    $isAssociatedToShop = false;
2460                    $combinations = Product::getProductAttributesIds($object->id);
2461                    if ($combinations) {
2462                        foreach ($combinations as $idCombination) {
2463                            $combination = new Combination((int) $idCombination['id_product_attribute']);
2464                            $defaultCombination = new Combination((int) $idCombination['id_product_attribute'], null, (int) $this->object->id_shop_default);
2465
2466                            $def = ObjectModel::getDefinition($defaultCombination);
2467                            foreach ($def['fields'] as $fieldName => $row) {
2468                                $combination->$fieldName = ObjectModel::formatValue($defaultCombination->$fieldName, $def['fields'][$fieldName]['type']);
2469                            }
2470
2471                            $combination->save();
2472                        }
2473                    }
2474                } else {
2475                    $isAssociatedToShop = true;
2476                }
2477
2478                if ($object->update()) {
2479                    // If the product doesn't exist in the current shop but exists in another shop
2480                    if (Shop::getContext() == Shop::CONTEXT_SHOP && !$existingProduct->isAssociatedToShop($this->context->shop->id)) {
2481                        $outOfStock = StockAvailable::outOfStock($existingProduct->id, $existingProduct->id_shop_default);
2482                        $dependsOnStock = StockAvailable::dependsOnStock($existingProduct->id, $existingProduct->id_shop_default);
2483                        StockAvailable::setProductOutOfStock((int) $this->object->id, $outOfStock, $this->context->shop->id);
2484                        StockAvailable::setProductDependsOnStock((int) $this->object->id, $dependsOnStock, $this->context->shop->id);
2485                    }
2486
2487                    Logger::addLog(sprintf($this->l('%s modification', 'AdminTab', false, false), $this->className), 1, null, $this->className, (int) $this->object->id, true, (int) $this->context->employee->id);
2488                    if (in_array($this->context->shop->getContext(), [Shop::CONTEXT_SHOP, Shop::CONTEXT_ALL])) {
2489                        if ($this->isTabSubmitted('Shipping')) {
2490                            $this->addCarriers();
2491                        }
2492                        if ($this->isTabSubmitted('Associations')) {
2493                            $this->updateAccessories($object);
2494                        }
2495                        if ($this->isTabSubmitted('Suppliers')) {
2496                            $this->processSuppliers();
2497                        }
2498                        if ($this->isTabSubmitted('Features')) {
2499                            $this->processFeatures();
2500                        }
2501                        if ($this->isTabSubmitted('Combinations')) {
2502                            $this->processProductAttribute();
2503                        }
2504                        if ($this->isTabSubmitted('Prices')) {
2505                            $this->processPriceAddition();
2506                            $this->processSpecificPricePriorities();
2507                        }
2508                        if ($this->isTabSubmitted('Customization')) {
2509                            $this->processCustomizationConfiguration();
2510                        }
2511                        if ($this->isTabSubmitted('Attachments')) {
2512                            $this->processAttachments();
2513                        }
2514                        if ($this->isTabSubmitted('Images')) {
2515                            $this->processImageLegends();
2516                        }
2517
2518                        $this->updatePackItems($object);
2519                        // Disallow avanced stock management if the product become a pack
2520                        if ($productTypeBefore == Product::PTYPE_SIMPLE && $object->getType() == Product::PTYPE_PACK) {
2521                            StockAvailable::setProductDependsOnStock((int) $object->id, false);
2522                        }
2523                        $this->updateDownloadProduct($object);
2524                        $this->updateTags(Language::getLanguages(false), $object);
2525
2526                        if ($this->isProductFieldUpdated('category_box') && !$object->updateCategories(Tools::getValue('categoryBox'))) {
2527                            $this->errors[] = Tools::displayError('An error occurred while linking the object.').' <b>'.$this->table.'</b> '.Tools::displayError('To categories');
2528                        }
2529                    }
2530
2531                    if ($this->isTabSubmitted('Warehouses')) {
2532                        $this->processWarehouses();
2533                    }
2534                    if (empty($this->errors)) {
2535                        if (in_array($object->visibility, ['both', 'search']) && Configuration::get('PS_SEARCH_INDEXATION')) {
2536                            Search::indexation(false, $object->id);
2537                        }
2538
2539                        // Save and preview
2540                        if (Tools::isSubmit('submitAddProductAndPreview')) {
2541                            $this->redirect_after = $this->getPreviewUrl($object);
2542                        } else {
2543                            $page = (int) Tools::getValue('page');
2544                            // Save and stay on same form
2545                            if ($this->display == 'edit') {
2546                                $this->confirmations[] = $this->l('Update successful');
2547                                $this->redirect_after = static::$currentIndex.'&id_product='.(int) $this->object->id
2548                                    .(Tools::getIsset('id_category') ? '&id_category='.(int) Tools::getValue('id_category') : '')
2549                                    .'&updateproduct&conf=4&key_tab='.Tools::safeOutput(Tools::getValue('key_tab')).($page > 1 ? '&page='.(int) $page : '').'&token='.$this->token;
2550                            } else {
2551                                // Default behavior (save and back)
2552                                $this->redirect_after = static::$currentIndex.(Tools::getIsset('id_category') ? '&id_category='.(int) Tools::getValue('id_category') : '').'&conf=4'.($page > 1 ? '&submitFilterproduct='.(int) $page : '').'&token='.$this->token;
2553                            }
2554                        }
2555                    } // if errors : stay on edit page
2556                    else {
2557                        $this->display = 'edit';
2558                    }
2559                } else {
2560                    if (!$isAssociatedToShop && isset($combinations) && $combinations) {
2561                        foreach ($combinations as $idCombination) {
2562                            $combination = new Combination((int) $idCombination['id_product_attribute']);
2563                            $combination->delete();
2564                        }
2565                    }
2566                    $this->errors[] = Tools::displayError('An error occurred while updating an object.').' <b>'.$this->table.'</b> ('.Db::getInstance()->getMsgError().')';
2567                }
2568            } else {
2569                $this->errors[] = Tools::displayError('An error occurred while updating an object.').' <b>'.$this->table.'</b> ('.Tools::displayError('The object cannot be loaded. ').')';
2570            }
2571
2572            return $object;
2573        }
2574    }
2575
2576    /**
2577     * Post treatment for suppliers
2578     *
2579     * @since 1.0.0
2580     */
2581    public function processSuppliers()
2582    {
2583        if ((int) Tools::getValue('supplier_loaded') === 1 && Validate::isLoadedObject($product = new Product((int) Tools::getValue('id_product')))) {
2584            // Get all id_product_attribute
2585            $attributes = $product->getAttributesResume($this->context->language->id);
2586            if (empty($attributes)) {
2587                $attributes[] = [
2588                    'id_product_attribute'  => 0,
2589                    'attribute_designation' => '',
2590                ];
2591            }
2592
2593            // Get all available suppliers
2594            $suppliers = Supplier::getSuppliers();
2595
2596            // Get already associated suppliers
2597            $associatedSuppliers = ProductSupplier::getSupplierCollection($product->id);
2598
2599            $suppliersToAssociate = [];
2600            $newDefaultSupplier = 0;
2601
2602            if (Tools::isSubmit('default_supplier')) {
2603                $newDefaultSupplier = (int) Tools::getValue('default_supplier');
2604            }
2605
2606            // Get new associations
2607            foreach ($suppliers as $supplier) {
2608                if (Tools::isSubmit('check_supplier_'.$supplier['id_supplier'])) {
2609                    $suppliersToAssociate[] = $supplier['id_supplier'];
2610                }
2611            }
2612
2613            // Delete already associated suppliers if needed
2614            foreach ($associatedSuppliers as $key => $associatedSupplier) {
2615                /** @var ProductSupplier $associatedSupplier */
2616                if (!in_array($associatedSupplier->id_supplier, $suppliersToAssociate)) {
2617                    $associatedSupplier->delete();
2618                    unset($associatedSuppliers[$key]);
2619                }
2620            }
2621
2622            // Associate suppliers
2623            foreach ($suppliersToAssociate as $id) {
2624                $toAdd = true;
2625                foreach ($associatedSuppliers as $as) {
2626                    /** @var ProductSupplier $as */
2627                    if ($id == $as->id_supplier) {
2628                        $toAdd = false;
2629                    }
2630                }
2631
2632                if ($toAdd) {
2633                    $productSupplier = new ProductSupplier();
2634                    $productSupplier->id_product = $product->id;
2635                    $productSupplier->id_product_attribute = 0;
2636                    $productSupplier->id_supplier = $id;
2637                    if ($this->context->currency->id) {
2638                        $productSupplier->id_currency = (int) $this->context->currency->id;
2639                    } else {
2640                        $productSupplier->id_currency = (int) Configuration::get('PS_CURRENCY_DEFAULT');
2641                    }
2642                    $productSupplier->save();
2643
2644                    $associatedSuppliers[] = $productSupplier;
2645                    foreach ($attributes as $attribute) {
2646                        if ((int) $attribute['id_product_attribute'] > 0) {
2647                            $productSupplier = new ProductSupplier();
2648                            $productSupplier->id_product = $product->id;
2649                            $productSupplier->id_product_attribute = (int) $attribute['id_product_attribute'];
2650                            $productSupplier->id_supplier = $id;
2651                            $productSupplier->save();
2652                        }
2653                    }
2654                }
2655            }
2656
2657            // Manage references and prices
2658            foreach ($attributes as $attribute) {
2659                foreach ($associatedSuppliers as $supplier) {
2660                    /** @var ProductSupplier $supplier */
2661                    if (Tools::isSubmit('supplier_reference_'.$product->id.'_'.$attribute['id_product_attribute'].'_'.$supplier->id_supplier) ||
2662                        (Tools::isSubmit('product_price_'.$product->id.'_'.$attribute['id_product_attribute'].'_'.$supplier->id_supplier) &&
2663                            Tools::isSubmit('product_price_currency_'.$product->id.'_'.$attribute['id_product_attribute'].'_'.$supplier->id_supplier))
2664                    ) {
2665                        $reference = pSQL(
2666                            Tools::getValue(
2667                                'supplier_reference_'.$product->id.'_'.$attribute['id_product_attribute'].'_'.$supplier->id_supplier,
2668                                ''
2669                            )
2670                        );
2671
2672                        $price = priceval(
2673                            Tools::getValue(
2674                                'product_price_'.$product->id.'_'.$attribute['id_product_attribute'].'_'.$supplier->id_supplier,
2675                                0
2676                            )
2677                        );
2678
2679                        $idCurrency = (int) Tools::getValue(
2680                            'product_price_currency_'.$product->id.'_'.$attribute['id_product_attribute'].'_'.$supplier->id_supplier,
2681                            0
2682                        );
2683
2684                        if ($idCurrency <= 0 || (!($result = Currency::getCurrency($idCurrency)) || empty($result))) {
2685                            $this->errors[] = Tools::displayError('The selected currency is not valid');
2686                        }
2687
2688                        // Save product-supplier data
2689                        $productSupplierId = (int) ProductSupplier::getIdByProductAndSupplier($product->id, $attribute['id_product_attribute'], $supplier->id_supplier);
2690
2691                        if (!$productSupplierId) {
2692                            $product->addSupplierReference($supplier->id_supplier, (int) $attribute['id_product_attribute'], $reference, (float) $price, (int) $idCurrency);
2693                            if ($product->id_supplier == $supplier->id_supplier) {
2694                                if ((int) $attribute['id_product_attribute'] > 0) {
2695                                    $data = [
2696                                        'supplier_reference' => pSQL($reference),
2697                                        'wholesale_price'    => Tools::convertPrice($price, $idCurrency),
2698                                    ];
2699                                    $where = '
2700										a.id_product = '.(int) $product->id.'
2701										AND a.id_product_attribute = '.(int) $attribute['id_product_attribute'];
2702                                    ObjectModel::updateMultishopTable('Combination', $data, $where);
2703                                } else {
2704                                    $product->wholesale_price = Tools::convertPrice($price, $idCurrency);
2705                                    $product->supplier_reference = pSQL($reference);
2706                                    $product->update();
2707                                }
2708                            }
2709                        } else {
2710                            $productSupplier = new ProductSupplier($productSupplierId);
2711                            $productSupplier->id_currency = (int) $idCurrency;
2712                            $productSupplier->product_supplier_price_te = $price;
2713                            $productSupplier->product_supplier_reference = pSQL($reference);
2714                            $productSupplier->update();
2715                        }
2716                    } elseif (Tools::isSubmit('supplier_reference_'.$product->id.'_'.$attribute['id_product_attribute'].'_'.$supplier->id_supplier)) {
2717                        //int attribute with default values if possible
2718                        if ((int) $attribute['id_product_attribute'] > 0) {
2719                            $productSupplier = new ProductSupplier();
2720                            $productSupplier->id_product = $product->id;
2721                            $productSupplier->id_product_attribute = (int) $attribute['id_product_attribute'];
2722                            $productSupplier->id_supplier = $supplier->id_supplier;
2723                            $productSupplier->save();
2724                        }
2725                    }
2726                }
2727            }
2728            // Manage defaut supplier for product
2729            if ($newDefaultSupplier != $product->id_supplier) {
2730                $this->object->id_supplier = $newDefaultSupplier;
2731                $this->object->update();
2732            }
2733        }
2734    }
2735
2736    /**
2737     * Process features
2738     *
2739     * @since 1.0.0
2740     */
2741    public function processFeatures()
2742    {
2743        if (!Feature::isFeatureActive()) {
2744            return;
2745        }
2746
2747        if (Validate::isLoadedObject($product = new Product((int) Tools::getValue('id_product')))) {
2748            // delete all objects
2749            $product->deleteFeatures();
2750
2751            // add new objects
2752            $languages = Language::getLanguages(false);
2753            foreach ($_POST as $key => $val) {
2754                if (preg_match('/^feature_([0-9]+)_value/i', $key, $match)) {
2755                    if ($val) {
2756                        $product->addFeaturesToDB($match[1], $val);
2757                    } else {
2758                        if ($defaultValue = $this->checkFeatures($languages, $match[1])) {
2759                            $idValue = $product->addFeaturesToDB($match[1], 0, 1);
2760                            foreach ($languages as $language) {
2761                                if ($cust = Tools::getValue('custom_'.$match[1].'_'.(int) $language['id_lang'])) {
2762                                    $product->addFeaturesCustomToDB($idValue, (int) $language['id_lang'], $cust);
2763                                } else {
2764                                    $product->addFeaturesCustomToDB($idValue, (int) $language['id_lang'], $defaultValue);
2765                                }
2766                            }
2767                        }
2768                    }
2769                }
2770            }
2771        } else {
2772            $this->errors[] = Tools::displayError('A product must be created before adding features.');
2773        }
2774    }
2775
2776    /**
2777     * Check features
2778     *
2779     * @param $languages
2780     * @param $featureId
2781     *
2782     * @return int|mixed
2783     *
2784     * @since 1.0.0
2785     */
2786    protected function checkFeatures($languages, $featureId)
2787    {
2788        $rules = call_user_func(['FeatureValue', 'getValidationRules'], 'FeatureValue');
2789        $feature = Feature::getFeature((int) Configuration::get('PS_LANG_DEFAULT'), $featureId);
2790
2791        foreach ($languages as $language) {
2792            if ($val = Tools::getValue('custom_'.$featureId.'_'.$language['id_lang'])) {
2793                $currentLanguage = new Language($language['id_lang']);
2794                if (mb_strlen($val) > $rules['sizeLang']['value']) {
2795                    $this->errors[] = sprintf(
2796                        Tools::displayError('The name for feature %1$s is too long in %2$s.'),
2797                        ' <b>'.$feature['name'].'</b>',
2798                        $currentLanguage->name
2799                    );
2800                } elseif (!call_user_func(['Validate', $rules['validateLang']['value']], $val)) {
2801                    $this->errors[] = sprintf(
2802                        Tools::displayError('A valid name required for feature. %1$s in %2$s.'),
2803                        ' <b>'.$feature['name'].'</b>',
2804                        $currentLanguage->name
2805                    );
2806                }
2807                if (count($this->errors)) {
2808                    return 0;
2809                }
2810                // Getting default language
2811                if ($language['id_lang'] == Configuration::get('PS_LANG_DEFAULT')) {
2812                    return $val;
2813                }
2814            }
2815        }
2816
2817        return 0;
2818    }
2819
2820    /**
2821     * Process product attribute
2822     *
2823     * @since 1.0.0
2824     */
2825    public function processProductAttribute()
2826    {
2827        // Don't process if the combination fields have not been submitted
2828        if (!Combination::isFeatureActive() || !Tools::getValue('attribute_combination_list')) {
2829            return;
2830        }
2831
2832        if (Validate::isLoadedObject($product = $this->object)) {
2833            if ($this->isProductFieldUpdated('attribute_price') && (!Tools::getIsset('attribute_price') || Tools::getIsset('attribute_price') == null)) {
2834                $this->errors[] = Tools::displayError('The price attribute is required.');
2835            }
2836            if (!Tools::getIsset('attribute_combination_list') || Tools::isEmpty(Tools::getValue('attribute_combination_list'))) {
2837                $this->errors[] = Tools::displayError('You must add at least one attribute.');
2838            }
2839
2840            $arrayChecks = [
2841                'reference'          => 'isReference',
2842                'supplier_reference' => 'isReference',
2843                'location'           => 'isReference',
2844                'ean13'              => 'isEan13',
2845                'upc'                => 'isUpc',
2846                'wholesale_price'    => 'isPrice',
2847                'price'              => 'isPrice',
2848                'ecotax'             => 'isPrice',
2849                'quantity'           => 'isInt',
2850                'weight'             => 'isUnsignedFloat',
2851                'unit_price_impact'  => 'isPrice',
2852                'default_on'         => 'isBool',
2853                'minimal_quantity'   => 'isUnsignedInt',
2854                'available_date'     => 'isDateFormat',
2855            ];
2856            foreach ($arrayChecks as $property => $check) {
2857                $key = 'attribute_'.$property;
2858                $value = Tools::getValue($key);
2859
2860                if ($check === 'isPrice') {
2861                    $value = priceval($value);
2862                }
2863                if ($value !== false
2864                    && ! call_user_func(['Validate', $check], $value)) {
2865                    $this->errors[] = sprintf(Tools::displayError('Field %s is not valid'), $property);
2866                }
2867            }
2868
2869            if (!count($this->errors)) {
2870                if (!isset($_POST['attribute_wholesale_price'])) {
2871                    $_POST['attribute_wholesale_price'] = 0;
2872                }
2873                if (!isset($_POST['attribute_price_impact'])) {
2874                    $_POST['attribute_price_impact'] = 0;
2875                }
2876                if (!isset($_POST['attribute_weight_impact'])) {
2877                    $_POST['attribute_weight_impact'] = 0;
2878                }
2879                if (!isset($_POST['attribute_ecotax'])) {
2880                    $_POST['attribute_ecotax'] = 0;
2881                }
2882                if (Tools::getValue('attribute_default')) {
2883                    $product->deleteDefaultAttributes();
2884                }
2885
2886                // Change existing one
2887                if (($idProductAttribute = (int) Tools::getValue('id_product_attribute')) || ($idProductAttribute = $product->productAttributeExists(Tools::getValue('attribute_combination_list'), false, null, true, true))) {
2888                    if ($this->tabAccess['edit'] === '1') {
2889                        if ($this->isProductFieldUpdated('available_date_attribute') && (Tools::getValue('available_date_attribute') != '' && !Validate::isDateFormat(Tools::getValue('available_date_attribute')))) {
2890                            $this->errors[] = Tools::displayError('Invalid date format.');
2891                        } else {
2892                            $product->updateAttribute(
2893                                (int) $idProductAttribute,
2894                                $this->isProductFieldUpdated('attribute_wholesale_price') ? priceval(Tools::getValue('attribute_wholesale_price')) : null,
2895                                $this->isProductFieldUpdated('attribute_price_impact') ? priceval(Tools::getValue('attribute_price')) * Tools::getValue('attribute_price_impact') : null,
2896                                $this->isProductFieldUpdated('attribute_weight_impact') ? Tools::getValue('attribute_weight') * Tools::getValue('attribute_weight_impact') : null,
2897                                $this->isProductFieldUpdated('attribute_unit_impact') ? Tools::getValue('attribute_unity') * Tools::getValue('attribute_unit_impact') : null,
2898                                $this->isProductFieldUpdated('attribute_ecotax') ? priceval(Tools::getValue('attribute_ecotax')) : null,
2899                                Tools::getValue('id_image_attr'),
2900                                Tools::getValue('attribute_reference'),
2901                                Tools::getValue('attribute_ean13'),
2902                                $this->isProductFieldUpdated('attribute_default') ? Tools::getValue('attribute_default') : null,
2903                                Tools::getValue('attribute_location'),
2904                                Tools::getValue('attribute_upc'),
2905                                $this->isProductFieldUpdated('attribute_minimal_quantity') ? Tools::getValue('attribute_minimal_quantity') : null,
2906                                $this->isProductFieldUpdated('available_date_attribute') ? Tools::getValue('available_date_attribute') : null,
2907                                false
2908                            );
2909                            StockAvailable::setProductDependsOnStock((int) $product->id, $product->depends_on_stock, null, (int) $idProductAttribute);
2910                            StockAvailable::setProductOutOfStock((int) $product->id, $product->out_of_stock, null, (int) $idProductAttribute);
2911                        }
2912                    } else {
2913                        $this->errors[] = Tools::displayError('You do not have permission to add this.');
2914                    }
2915                } // Add new
2916                else {
2917                    if ($this->tabAccess['add'] === '1') {
2918                        /** @var Product $product */
2919                        if ($product->productAttributeExists(Tools::getValue('attribute_combination_list'))) {
2920                            $this->errors[] = Tools::displayError('This combination already exists.');
2921                        } else {
2922                            $idProductAttribute = $product->addCombinationEntity(
2923                                priceval(Tools::getValue('attribute_wholesale_price')),
2924                                priceval(Tools::getValue('attribute_price')) * Tools::getValue('attribute_price_impact'),
2925                                Tools::getValue('attribute_weight') * Tools::getValue('attribute_weight_impact'),
2926                                Tools::getValue('attribute_unity') * Tools::getValue('attribute_unit_impact'),
2927                                priceval(Tools::getValue('attribute_ecotax')),
2928                                0,
2929                                Tools::getValue('id_image_attr'),
2930                                Tools::getValue('attribute_reference'),
2931                                null,
2932                                Tools::getValue('attribute_ean13'),
2933                                Tools::getValue('attribute_default'),
2934                                Tools::getValue('attribute_location'),
2935                                Tools::getValue('attribute_upc'),
2936                                Tools::getValue('attribute_minimal_quantity'),
2937                                [],
2938                                Tools::getValue('available_date_attribute')
2939                            );
2940                            StockAvailable::setProductDependsOnStock((int) $product->id, $product->depends_on_stock, null, (int) $idProductAttribute);
2941                            StockAvailable::setProductOutOfStock((int) $product->id, $product->out_of_stock, null, (int) $idProductAttribute);
2942                        }
2943                    } else {
2944                        $this->errors[] = Tools::displayError('You do not have permission to').'<hr>'.Tools::displayError('edit here.');
2945                    }
2946                }
2947                if (!count($this->errors)) {
2948                    $combination = new Combination((int) $idProductAttribute);
2949                    $combination->setAttributes(Tools::getValue('attribute_combination_list'));
2950
2951                    // images could be deleted before
2952                    $idImages = Tools::getValue('id_image_attr');
2953                    if (!empty($idImages)) {
2954                        $combination->setImages($idImages);
2955                    }
2956
2957                    $product->checkDefaultAttributes();
2958                    if (Tools::getValue('attribute_default')) {
2959                        Product::updateDefaultAttribute((int) $product->id);
2960                        if (isset($idProductAttribute)) {
2961                            $product->cache_default_attribute = (int) $idProductAttribute;
2962                        }
2963
2964                        if ($availableDate = Tools::getValue('available_date_attribute')) {
2965                            $product->setAvailableDate($availableDate);
2966                        } else {
2967                            $product->setAvailableDate();
2968                        }
2969                    }
2970                }
2971            }
2972        }
2973    }
2974
2975    /**
2976     * Process price addition
2977     *
2978     * @since 1.0.0
2979     */
2980    public function processPriceAddition()
2981    {
2982        if (!Tools::getIsset('submitPriceAddition')) {
2983            return;
2984        }
2985
2986        $idProduct = Tools::getValue('id_product');
2987        $idProductAttribute = Tools::getValue('sp_id_product_attribute');
2988        $idShop = Tools::getValue('sp_id_shop');
2989        $idCurrency = Tools::getValue('sp_id_currency');
2990        $idCountry = Tools::getValue('sp_id_country');
2991        $idGroup = Tools::getValue('sp_id_group');
2992        $idCustomer = Tools::getValue('sp_id_customer');
2993        $price = Tools::getValue('leave_bprice') ? '-1' : priceval(Tools::getValue('sp_price'));
2994        $fromQuantity = Tools::getValue('sp_from_quantity');
2995        $reduction = priceval(Tools::getValue('sp_reduction'));
2996        $reductionTax = priceval(Tools::getValue('sp_reduction_tax'));
2997        $reductionType = !$reduction ? 'amount' : Tools::getValue('sp_reduction_type');
2998        $reductionType = $reductionType == '-' ? 'amount' : $reductionType;
2999        $from = Tools::getValue('sp_from');
3000        if (!$from) {
3001            $from = '0000-00-00 00:00:00';
3002        }
3003        $to = Tools::getValue('sp_to');
3004        if (!$to) {
3005            $to = '0000-00-00 00:00:00';
3006        }
3007
3008        if (($price == '-1') && ((float) $reduction == '0')) {
3009            $this->errors[] = Tools::displayError('No reduction value has been submitted');
3010        } elseif ($to != '0000-00-00 00:00:00' && strtotime($to) < strtotime($from)) {
3011            $this->errors[] = Tools::displayError('Invalid date range');
3012        } elseif ($reductionType == 'percentage' && ((float) $reduction <= 0 || (float) $reduction > 100)) {
3013            $this->errors[] = Tools::displayError('Submitted reduction value (0-100) is out-of-range');
3014        } elseif ($this->_validateSpecificPrice($idShop, $idCurrency, $idCountry, $idGroup, $idCustomer, $price, $fromQuantity, $reduction, $reductionType, $from, $to, $idProductAttribute)) {
3015            $specificPrice = new SpecificPrice();
3016            $specificPrice->id_product = (int) $idProduct;
3017            $specificPrice->id_product_attribute = (int) $idProductAttribute;
3018            $specificPrice->id_shop = (int) $idShop;
3019            $specificPrice->id_currency = (int) ($idCurrency);
3020            $specificPrice->id_country = (int) ($idCountry);
3021            $specificPrice->id_group = (int) ($idGroup);
3022            $specificPrice->id_customer = (int) $idCustomer;
3023            $specificPrice->price = (float) ($price);
3024            $specificPrice->from_quantity = (int) ($fromQuantity);
3025            $specificPrice->reduction = (float) ($reductionType == 'percentage' ? $reduction / 100 : $reduction);
3026            $specificPrice->reduction_tax = $reductionTax;
3027            $specificPrice->reduction_type = $reductionType;
3028            $specificPrice->from = $from;
3029            $specificPrice->to = $to;
3030            if (!$specificPrice->add()) {
3031                $this->errors[] = Tools::displayError('An error occurred while updating the specific price.');
3032            }
3033        }
3034    }
3035
3036    /**
3037     * Process specific price priorities
3038     *
3039     * @since 1.0.0
3040     */
3041    public function processSpecificPricePriorities()
3042    {
3043        if (!($obj = $this->loadObject())) {
3044            return;
3045        }
3046        if (!$priorities = Tools::getValue('specificPricePriority')) {
3047            $this->errors[] = Tools::displayError('Please specify priorities.');
3048        } elseif (Tools::isSubmit('specificPricePriorityToAll')) {
3049            if (!SpecificPrice::setPriorities($priorities)) {
3050                $this->errors[] = Tools::displayError('An error occurred while updating priorities.');
3051            } else {
3052                $this->confirmations[] = $this->l('The price rule has successfully updated');
3053            }
3054        } elseif (!SpecificPrice::setSpecificPriority((int) $obj->id, $priorities)) {
3055            $this->errors[] = Tools::displayError('An error occurred while setting priorities.');
3056        }
3057    }
3058
3059    /**
3060     * Process customization configuration
3061     *
3062     * @since 1.0.0
3063     */
3064    public function processCustomizationConfiguration()
3065    {
3066        $product = $this->object;
3067        // Get the number of existing customization fields ($product->text_fields is the updated value, not the existing value)
3068        $currentCustomization = $product->getCustomizationFieldIds();
3069        $filesCount = 0;
3070        $textCount = 0;
3071        if (is_array($currentCustomization)) {
3072            foreach ($currentCustomization as $field) {
3073                if ($field['type'] == 1) {
3074                    $textCount++;
3075                } else {
3076                    $filesCount++;
3077                }
3078            }
3079        }
3080
3081        if (!$product->createLabels((int) $product->uploadable_files - $filesCount, (int) $product->text_fields - $textCount)) {
3082            $this->errors[] = Tools::displayError('An error occurred while creating customization fields.');
3083        }
3084        if (!count($this->errors) && !$product->updateLabels()) {
3085            $this->errors[] = Tools::displayError('An error occurred while updating customization fields.');
3086        }
3087        $product->customizable = ($product->uploadable_files > 0 || $product->text_fields > 0) ? 1 : 0;
3088        if (($product->uploadable_files != $filesCount || $product->text_fields != $textCount) && !count($this->errors) && !$product->update()) {
3089            $this->errors[] = Tools::displayError('An error occurred while updating the custom configuration.');
3090        }
3091    }
3092
3093    /**
3094     * Attach an existing attachment to the product
3095     *
3096     * @return void
3097     *
3098     * @since 1.0.0
3099     */
3100    public function processAttachments()
3101    {
3102        if ($id = (int) Tools::getValue($this->identifier)) {
3103            $attachments = trim(Tools::getValue('arrayAttachments'), ',');
3104            $attachments = explode(',', $attachments);
3105            if (!Attachment::attachToProduct($id, $attachments)) {
3106                $this->errors[] = Tools::displayError('An error occurred while saving product attachments.');
3107            }
3108        }
3109    }
3110
3111    /**
3112     * Process image legends
3113     *
3114     * @since 1.0.0
3115     */
3116    public function processImageLegends()
3117    {
3118        if (Tools::getValue('key_tab') == 'Images' && Tools::getValue('submitAddproductAndStay') == 'update_legends' && Validate::isLoadedObject($product = new Product((int) Tools::getValue('id_product')))) {
3119            $idImage = (int) Tools::getValue('id_caption');
3120            $idImages = [];
3121            if ($idImage) {
3122                // Caption is for one image only.
3123                $idImages[] = $idImage;
3124            } else {
3125                // Same caption for all images.
3126                $images = Image::getImages(null, $product->id);
3127                foreach ($images as $image) {
3128                    $idImages[] = $image['id_image'];
3129                }
3130            }
3131            $languageIds = Language::getIDs(false);
3132            foreach ($_POST as $key => $val) {
3133                if (preg_match('/^legend_([0-9]+)/', $key, $match)) {
3134                    foreach ($languageIds as $idLang) {
3135                        if ($idLang == $match[1]) {
3136                            foreach ($idImages as $idImage) {
3137                                // Insert missing entries, update already
3138                                // existing ones. As SQL features no 'insert if
3139                                // missing, update otherwise', try both.
3140                                Db::getInstance(_PS_USE_SQL_SLAVE_)->insert(
3141                                    'image_lang',
3142                                    [
3143                                        'id_image'  => $idImage,
3144                                        'id_lang'   => $idLang,
3145                                        'legend'    => $val,
3146                                    ],
3147                                    false,
3148                                    true,
3149                                    Db::INSERT_IGNORE
3150                                );
3151                                Db::getInstance(_PS_USE_SQL_SLAVE_)->update(
3152                                    'image_lang',
3153                                    [
3154                                        'legend'    => $val,
3155                                    ],
3156                                    '`id_image` = '.$idImage
3157                                    .' AND `id_lang` = '.$idLang
3158                                );
3159                            }
3160                        }
3161                    }
3162                }
3163            }
3164        }
3165    }
3166
3167    /**
3168     * Post treatment for warehouses
3169     *
3170     * @since 1.0.0
3171     */
3172    public function processWarehouses()
3173    {
3174        if ((int) Tools::getValue('warehouse_loaded') === 1 && Validate::isLoadedObject($product = new Product((int) $idProduct = Tools::getValue('id_product')))) {
3175            // Get all id_product_attribute
3176            $attributes = $product->getAttributesResume($this->context->language->id);
3177            if (empty($attributes)) {
3178                $attributes[] = [
3179                    'id_product_attribute'  => 0,
3180                    'attribute_designation' => '',
3181                ];
3182            }
3183
3184            // Get all available warehouses
3185            $warehouses = Warehouse::getWarehouses(true);
3186
3187            // Get already associated warehouses
3188            $associatedWarehousesCollection = WarehouseProductLocation::getCollection($product->id);
3189
3190            $elementsToManage = [];
3191
3192            // get form inforamtion
3193            foreach ($attributes as $attribute) {
3194                foreach ($warehouses as $warehouse) {
3195                    $key = $warehouse['id_warehouse'].'_'.$product->id.'_'.$attribute['id_product_attribute'];
3196
3197                    // get elements to manage
3198                    if (Tools::isSubmit('check_warehouse_'.$key)) {
3199                        $location = Tools::getValue('location_warehouse_'.$key, '');
3200                        $elementsToManage[$key] = $location;
3201                    }
3202                }
3203            }
3204
3205            // Delete entry if necessary
3206            foreach ($associatedWarehousesCollection as $awc) {
3207                /** @var WarehouseProductLocation $awc */
3208                if (!array_key_exists($awc->id_warehouse.'_'.$awc->id_product.'_'.$awc->id_product_attribute, $elementsToManage)) {
3209                    $awc->delete();
3210                }
3211            }
3212
3213            // Manage locations
3214            foreach ($elementsToManage as $key => $location) {
3215                $params = explode('_', $key);
3216
3217                $wplId = (int) WarehouseProductLocation::getIdByProductAndWarehouse((int) $params[1], (int) $params[2], (int) $params[0]);
3218
3219                if (empty($wplId)) {
3220                    //create new record
3221                    $warehouseLocationEntity = new WarehouseProductLocation();
3222                    $warehouseLocationEntity->id_product = (int) $params[1];
3223                    $warehouseLocationEntity->id_product_attribute = (int) $params[2];
3224                    $warehouseLocationEntity->id_warehouse = (int) $params[0];
3225                    $warehouseLocationEntity->location = pSQL($location);
3226                    $warehouseLocationEntity->save();
3227                } else {
3228                    $warehouseLocationEntity = new WarehouseProductLocation((int) $wplId);
3229
3230                    $location = pSQL($location);
3231
3232                    if ($location != $warehouseLocationEntity->location) {
3233                        $warehouseLocationEntity->location = pSQL($location);
3234                        $warehouseLocationEntity->update();
3235                    }
3236                }
3237            }
3238            StockAvailable::synchronize((int) $idProduct);
3239        }
3240    }
3241
3242    /**
3243     * Initialize content
3244     *
3245     * @param null $token
3246     *
3247     * @since 1.0.0
3248     */
3249    public function initContent($token = null)
3250    {
3251        if ($this->display == 'edit' || $this->display == 'add') {
3252            $this->fields_form = [];
3253
3254            // Check if Module
3255            if (substr($this->tab_display, 0, 6) == 'Module') {
3256                $this->tab_display_module = strtolower(substr($this->tab_display, 6, mb_strlen($this->tab_display) - 6));
3257                $this->tab_display = 'Modules';
3258            }
3259            if (method_exists($this, 'initForm'.$this->tab_display)) {
3260                $this->tpl_form = strtolower($this->tab_display).'.tpl';
3261            }
3262
3263            if ($this->ajax) {
3264                $this->content_only = true;
3265            } else {
3266                $productTabs = [];
3267
3268                // tab_display defines which tab to display first
3269                if (!method_exists($this, 'initForm'.$this->tab_display)) {
3270                    $this->tab_display = $this->default_tab;
3271                }
3272
3273                $advancedStockManagementActive = Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT');
3274                foreach ($this->available_tabs as $productTab => $value) {
3275                    // if it's the warehouses tab and advanced stock management is disabled, continue
3276                    if ($advancedStockManagementActive == 0 && $productTab == 'Warehouses') {
3277                        continue;
3278                    }
3279
3280                    $productTabs[$productTab] = [
3281                        'id'       => $productTab,
3282                        'selected' => (strtolower($productTab) == strtolower($this->tab_display) || (isset($this->tab_display_module) && 'module'.$this->tab_display_module == mb_strtolower($productTab))),
3283                        'name'     => $this->available_tabs_lang[$productTab],
3284                        'href'     => $this->context->link->getAdminLink('AdminProducts').'&id_product='.(int) Tools::getValue('id_product').'&action='.$productTab,
3285                    ];
3286                }
3287                $this->tpl_form_vars['product_tabs'] = $productTabs;
3288            }
3289        } else {
3290            if ($idCategory = (int) $this->id_current_category) {
3291                static::$currentIndex .= '&id_category='.(int) $this->id_current_category;
3292            }
3293
3294            // If products from all categories are displayed, we don't want to use sorting by position
3295            if (!$idCategory) {
3296                $this->_defaultOrderBy = $this->identifier;
3297                if ($this->context->cookie->{$this->table.'Orderby'} == 'position') {
3298                    unset($this->context->cookie->{$this->table.'Orderby'});
3299                    unset($this->context->cookie->{$this->table.'Orderway'});
3300                }
3301            }
3302            if (!$idCategory) {
3303                $idCategory = Configuration::get('PS_ROOT_CATEGORY');
3304            }
3305            $this->tpl_list_vars['is_category_filter'] = (bool) $this->id_current_category;
3306
3307            // Generate category selection tree
3308            $tree = new HelperTreeCategories('categories-tree', $this->l('Filter by category'));
3309            $tree->setAttribute('is_category_filter', (bool) $this->id_current_category)
3310                ->setAttribute('base_url', preg_replace('#&id_category=[0-9]*#', '', static::$currentIndex).'&token='.$this->token)
3311                ->setInputName('id-category')
3312                ->setRootCategory(Category::getRootCategory()->id)
3313                ->setSelectedCategories([(int) $idCategory]);
3314            $this->tpl_list_vars['category_tree'] = $tree->render();
3315
3316            // used to build the new url when changing category
3317            $this->tpl_list_vars['base_url'] = preg_replace('#&id_category=[0-9]*#', '', static::$currentIndex).'&token='.$this->token;
3318        }
3319        // @todo module free
3320        $this->tpl_form_vars['vat_number'] = file_exists(_PS_MODULE_DIR_.'vatnumber/ajax.php');
3321
3322        parent::initContent();
3323    }
3324
3325    /**
3326     * Render KPIs
3327     *
3328     * @return mixed
3329     *
3330     * @since 1.0.0
3331     */
3332    public function renderKpis()
3333    {
3334        $time = time();
3335        $kpis = [];
3336
3337        /* The data generation is located in AdminStatsControllerCore */
3338
3339        if (Configuration::get('PS_STOCK_MANAGEMENT')) {
3340            $helper = new HelperKpi();
3341            $helper->id = 'box-products-stock';
3342            $helper->icon = 'icon-archive';
3343            $helper->color = 'color1';
3344            $helper->title = $this->l('Out of stock items', null, null, false);
3345            if (ConfigurationKPI::get('PERCENT_PRODUCT_OUT_OF_STOCK') !== false) {
3346                $helper->value = ConfigurationKPI::get('PERCENT_PRODUCT_OUT_OF_STOCK');
3347            }
3348            $helper->source = $this->context->link->getAdminLink('AdminStats').'&ajax=1&action=getKpi&kpi=percent_product_out_of_stock';
3349            $helper->tooltip = $this->l('X% of your products for sale are out of stock.', null, null, false);
3350            $helper->refresh = (bool) (ConfigurationKPI::get('PERCENT_PRODUCT_OUT_OF_STOCK_EXPIRE') < $time);
3351            $helper->href = $this->context->link->getAdminLink('AdminProducts').'&productFilter_sav!quantity=0&productFilter_active=1&submitFilterproduct=1';
3352            $kpis[] = $helper->generate();
3353        }
3354
3355        $helper = new HelperKpi();
3356        $helper->id = 'box-avg-gross-margin';
3357        $helper->icon = 'icon-tags';
3358        $helper->color = 'color2';
3359        $helper->title = $this->l('Average Gross Margin %', null, null, false);
3360        if (ConfigurationKPI::get('PRODUCT_AVG_GROSS_MARGIN') !== false) {
3361            $helper->value = ConfigurationKPI::get('PRODUCT_AVG_GROSS_MARGIN');
3362        }
3363        $helper->source = $this->context->link->getAdminLink('AdminStats').'&ajax=1&action=getKpi&kpi=product_avg_gross_margin';
3364        $helper->tooltip = $this->l('Gross margin expressed in percentage assesses how cost-effectively you sell your goods. Out of $100, you will retain $X to cover profit and expenses.', null, null, false);
3365        $helper->refresh = (bool) (ConfigurationKPI::get('PRODUCT_AVG_GROSS_MARGIN_EXPIRE') < $time);
3366        $kpis[] = $helper->generate();
3367
3368        $helper = new HelperKpi();
3369        $helper->id = 'box-8020-sales-catalog';
3370        $helper->icon = 'icon-beaker';
3371        $helper->color = 'color3';
3372        $helper->title = $this->l('Purchased references', null, null, false);
3373        $helper->subtitle = $this->l('30 days', null, null, false);
3374        if (ConfigurationKPI::get('8020_SALES_CATALOG') !== false) {
3375            $helper->value = ConfigurationKPI::get('8020_SALES_CATALOG');
3376        }
3377        $helper->source = $this->context->link->getAdminLink('AdminStats').'&ajax=1&action=getKpi&kpi=8020_sales_catalog';
3378        $helper->tooltip = $this->l('X% of your references have been purchased for the past 30 days', null, null, false);
3379        $helper->refresh = (bool) (ConfigurationKPI::get('8020_SALES_CATALOG_EXPIRE') < $time);
3380        if (Module::isInstalled('statsbestproducts')) {
3381            $helper->href = $this->context->link->getAdminLink('AdminStats').'&module=statsbestproducts&datepickerFrom='.date('Y-m-d', strtotime('-30 days')).'&datepickerTo='.date('Y-m-d');
3382        }
3383        $kpis[] = $helper->generate();
3384
3385        $helper = new HelperKpi();
3386        $helper->id = 'box-disabled-products';
3387        $helper->icon = 'icon-off';
3388        $helper->color = 'color4';
3389        $helper->href = $this->context->link->getAdminLink('AdminProducts');
3390        $helper->title = $this->l('Disabled Products', null, null, false);
3391        if (ConfigurationKPI::get('DISABLED_PRODUCTS') !== false) {
3392            $helper->value = ConfigurationKPI::get('DISABLED_PRODUCTS');
3393        }
3394        $helper->source = $this->context->link->getAdminLink('AdminStats').'&ajax=1&action=getKpi&kpi=disabled_products';
3395        $helper->refresh = (bool) (ConfigurationKPI::get('DISABLED_PRODUCTS_EXPIRE') < $time);
3396        $helper->tooltip = $this->l('X% of your products are disabled and not visible to your customers', null, null, false);
3397        $helper->href = $this->context->link->getAdminLink('AdminProducts').'&productFilter_active=0&submitFilterproduct=1';
3398        $kpis[] = $helper->generate();
3399
3400        $helper = new HelperKpiRow();
3401        $helper->kpis = $kpis;
3402
3403        return $helper->generate();
3404    }
3405
3406    public function renderList()
3407    {
3408        $this->addRowAction('edit');
3409        $this->addRowAction('preview');
3410        $this->addRowAction('duplicate');
3411        $this->addRowAction('delete');
3412
3413        return parent::renderList();
3414    }
3415
3416    public function ajaxProcessProductManufacturers()
3417    {
3418        $manufacturers = Manufacturer::getManufacturers();
3419        $jsonArray = [];
3420
3421        if ($manufacturers) {
3422            foreach ($manufacturers as $manufacturer) {
3423                $tmp = ["optionValue" => $manufacturer['id_manufacturer'], "optionDisplay" => htmlspecialchars(trim($manufacturer['name']))];
3424                $jsonArray[] = json_encode($tmp);
3425            }
3426        }
3427
3428        die('['.implode(',', $jsonArray).']');
3429    }
3430
3431    public function initPageHeaderToolbar()
3432    {
3433        if (empty($this->display)) {
3434            $this->page_header_toolbar_btn['new_product'] = [
3435                'href' => static::$currentIndex.'&addproduct&token='.$this->token,
3436                'desc' => $this->l('Add new product', null, null, false),
3437                'icon' => 'process-icon-new',
3438            ];
3439        }
3440        if ($this->display == 'edit') {
3441            if (($product = $this->loadObject(true)) && $product->isAssociatedToShop()) {
3442                // adding button for preview this product
3443                if ($url_preview = $this->getPreviewUrl($product)) {
3444                    $this->page_header_toolbar_btn['preview'] = [
3445                        'short'  => $this->l('Preview', null, null, false),
3446                        'href'   => $url_preview,
3447                        'desc'   => $this->l('Preview', null, null, false),
3448                        'target' => true,
3449                        'class'  => 'previewUrl',
3450                    ];
3451                }
3452
3453                $js = (bool) Image::getImages($this->context->language->id, (int) $product->id) ?
3454                    'confirm_link(\'\', \''.$this->l('This will copy the images too. If you wish to proceed, click "Yes". If not, click "No".', null, true, false).'\', \''.$this->l('Yes', null, true, false).'\', \''.$this->l('No', null, true, false).'\', \''.$this->context->link->getAdminLink('AdminProducts', true).'&id_product='.(int) $product->id.'&duplicateproduct'.'\', \''.$this->context->link->getAdminLink('AdminProducts', true).'&id_product='.(int) $product->id.'&duplicateproduct&noimage=1'.'\')'
3455                    :
3456                    'document.location = \''.$this->context->link->getAdminLink('AdminProducts', true).'&id_product='.(int) $product->id.'&duplicateproduct&noimage=1'.'\'';
3457
3458                // adding button for duplicate this product
3459                if ($this->tabAccess['add']) {
3460                    $this->page_header_toolbar_btn['duplicate'] = [
3461                        'short'   => $this->l('Duplicate', null, null, false),
3462                        'desc'    => $this->l('Duplicate', null, null, false),
3463                        'confirm' => 1,
3464                        'js'      => $js,
3465                    ];
3466                }
3467
3468                // adding button for preview this product statistics
3469                if (Module::isEnabled('statsdata')) {
3470                    $this->page_header_toolbar_btn['stats'] = [
3471                        'short' => $this->l('Statistics', null, null, false),
3472                        'href'  => $this->context->link->getAdminLink('AdminStats').'&module=statsproduct&id_product='.(int) $product->id,
3473                        'desc'  => $this->l('Product sales', null, null, false),
3474                    ];
3475                }
3476
3477                // adding button for delete this product
3478                if ($this->tabAccess['delete']) {
3479                    $this->page_header_toolbar_btn['delete'] = [
3480                        'short'   => $this->l('Delete', null, null, false),
3481                        'href'    => $this->context->link->getAdminLink('AdminProducts').'&id_product='.(int) $product->id.'&deleteproduct',
3482                        'desc'    => $this->l('Delete this product', null, null, false),
3483                        'confirm' => 1,
3484                        'js'      => 'if (confirm(\''.$this->l('Delete product?', null, true, false).'\')){return true;}else{event.preventDefault();}',
3485                    ];
3486                }
3487            }
3488        }
3489        parent::initPageHeaderToolbar();
3490    }
3491
3492    public function initToolbar()
3493    {
3494        parent::initToolbar();
3495        if ($this->display == 'edit' || $this->display == 'add') {
3496            $this->toolbar_btn['save'] = [
3497                'short' => 'Save',
3498                'href'  => '#',
3499                'desc'  => $this->l('Save'),
3500            ];
3501
3502            $this->toolbar_btn['save-and-stay'] = [
3503                'short' => 'SaveAndStay',
3504                'href'  => '#',
3505                'desc'  => $this->l('Save and stay'),
3506            ];
3507
3508            // adding button for adding a new combination in Combination tab
3509            $this->toolbar_btn['newCombination'] = [
3510                'short' => 'New combination',
3511                'desc'  => $this->l('New combination'),
3512                'class' => 'toolbar-new',
3513            ];
3514        } elseif ($this->can_import) {
3515            $this->toolbar_btn['import'] = [
3516                'href' => $this->context->link->getAdminLink('AdminImport', true).'&import_type=products',
3517                'desc' => $this->l('Import'),
3518            ];
3519        }
3520
3521        $this->context->smarty->assign('toolbar_scroll', 1);
3522        $this->context->smarty->assign('show_toolbar', 1);
3523        $this->context->smarty->assign('toolbar_btn', $this->toolbar_btn);
3524    }
3525
3526    /**
3527     * renderForm contains all necessary initialization needed for all tabs
3528     *
3529     * @return string
3530     * @throws PrestaShopException
3531     */
3532    public function renderForm()
3533    {
3534        // This nice code (irony) is here to store the product name, because the row after will erase product name in multishop context
3535        $this->product_name = $this->object->name[$this->context->language->id];
3536
3537        if (!method_exists($this, 'initForm'.$this->tab_display)) {
3538            return '';
3539        }
3540
3541        $product = $this->object;
3542
3543        // Product for multishop
3544        $this->context->smarty->assign('bullet_common_field', '');
3545        if (Shop::isFeatureActive() && $this->display == 'edit') {
3546            if (Shop::getContext() != Shop::CONTEXT_SHOP) {
3547                $this->context->smarty->assign(
3548                    [
3549                        'display_multishop_checkboxes' => true,
3550                        'multishop_check'              => Tools::getValue('multishop_check'),
3551                    ]
3552                );
3553            }
3554
3555            if (Shop::getContext() != Shop::CONTEXT_ALL) {
3556                $this->context->smarty->assign('bullet_common_field', '<i class="icon-circle text-orange"></i>');
3557                $this->context->smarty->assign('display_common_field', true);
3558            }
3559        }
3560
3561        $this->tpl_form_vars['tabs_preloaded'] = $this->available_tabs;
3562
3563        $this->tpl_form_vars['product_type'] = (int) Tools::getValue('type_product', $product->getType());
3564
3565        $this->getLanguages();
3566
3567        $this->tpl_form_vars['id_lang_default'] = Configuration::get('PS_LANG_DEFAULT');
3568
3569        $this->tpl_form_vars['currentIndex'] = static::$currentIndex;
3570        $this->tpl_form_vars['display_multishop_checkboxes'] = (Shop::isFeatureActive() && Shop::getContext() != Shop::CONTEXT_SHOP && $this->display == 'edit');
3571        $this->fields_form = [''];
3572
3573        $this->tpl_form_vars['token'] = $this->token;
3574        $this->tpl_form_vars['combinationImagesJs'] = $this->getCombinationImagesJs();
3575        $this->tpl_form_vars['PS_ALLOW_ACCENTED_CHARS_URL'] = (int) Configuration::get('PS_ALLOW_ACCENTED_CHARS_URL');
3576        $this->tpl_form_vars['post_data'] = json_encode($_POST);
3577        $this->tpl_form_vars['save_error'] = !empty($this->errors);
3578        $this->tpl_form_vars['mod_evasive'] = Tools::apacheModExists('evasive');
3579        $this->tpl_form_vars['mod_security'] = Tools::apacheModExists('security');
3580        $this->tpl_form_vars['ps_force_friendly_product'] = Configuration::get('PS_FORCE_FRIENDLY_PRODUCT');
3581
3582        // autoload rich text editor (tiny mce)
3583        $this->tpl_form_vars['tinymce'] = true;
3584        $iso = $this->context->language->iso_code;
3585        $this->tpl_form_vars['iso'] = file_exists(_PS_CORE_DIR_.'/js/tiny_mce/langs/'.$iso.'.js') ? $iso : 'en';
3586        $this->tpl_form_vars['path_css'] = _THEME_CSS_DIR_;
3587        $this->tpl_form_vars['ad'] = __PS_BASE_URI__.basename(_PS_ADMIN_DIR_);
3588
3589        if (Validate::isLoadedObject(($this->object))) {
3590            $idProduct = (int) $this->object->id;
3591        } else {
3592            $idProduct = (int) Tools::getvalue('id_product');
3593        }
3594
3595        $page = (int) Tools::getValue('page');
3596
3597        $this->tpl_form_vars['form_action'] = $this->context->link->getAdminLink('AdminProducts').'&'.($idProduct ? 'updateproduct&id_product='.(int) $idProduct : 'addproduct').($page > 1 ? '&page='.(int) $page : '');
3598        $this->tpl_form_vars['id_product'] = $idProduct;
3599
3600        // Transform configuration option 'upload_max_filesize' in octets
3601        $uploadMaxFilesize = Tools::getOctets(ini_get('upload_max_filesize'));
3602
3603        // Transform configuration option 'upload_max_filesize' in MegaOctets
3604        $uploadMaxFilesize = ($uploadMaxFilesize / 1024) / 1024;
3605
3606        $this->tpl_form_vars['upload_max_filesize'] = $uploadMaxFilesize;
3607        $this->tpl_form_vars['country_display_tax_label'] = $this->context->country->display_tax_label;
3608        $this->tpl_form_vars['has_combinations'] = $this->object->hasAttributes();
3609        $this->product_exists_in_shop = true;
3610
3611        if ($this->display == 'edit' && Validate::isLoadedObject($product) && Shop::isFeatureActive() && Shop::getContext() == Shop::CONTEXT_SHOP && !$product->isAssociatedToShop($this->context->shop->id)) {
3612            $this->product_exists_in_shop = false;
3613            if ($this->tab_display == 'Informations') {
3614                $this->displayWarning($this->l('Warning: The product does not exist in this shop'));
3615            }
3616
3617            $defaultProduct = new Product();
3618            $definition = ObjectModel::getDefinition($product);
3619            foreach ($definition['fields'] as $fieldName => $field) {
3620                if (isset($field['shop']) && $field['shop']) {
3621                    $product->$fieldName = ObjectModel::formatValue($defaultProduct->$fieldName, $field['type']);
3622                }
3623            }
3624        }
3625
3626        // let's calculate this once for all
3627        if (!Validate::isLoadedObject($this->object) && Tools::getValue('id_product')) {
3628            $this->errors[] = 'Unable to load object';
3629        } else {
3630            $this->_displayDraftWarning($this->object->active);
3631
3632            // if there was an error while saving, we don't want to lose posted data
3633            if (!empty($this->errors)) {
3634                $this->copyFromPost($this->object, $this->table);
3635            }
3636
3637            $this->initPack($this->object);
3638            $this->{'initForm'.$this->tab_display}($this->object);
3639            $this->tpl_form_vars['product'] = $this->object;
3640
3641            if ($this->ajax) {
3642                if (!isset($this->tpl_form_vars['custom_form'])) {
3643                    throw new PrestaShopException('custom_form empty for action '.$this->tab_display);
3644                } else {
3645                    return $this->tpl_form_vars['custom_form'];
3646                }
3647            }
3648        }
3649
3650        $parent = parent::renderForm();
3651        $this->addJqueryPlugin(['autocomplete', 'fancybox', 'typewatch']);
3652
3653        return $parent;
3654    }
3655
3656    /**
3657     * Get combination images JS
3658     *
3659     * @return string
3660     *
3661     * @since 1.0.0
3662     */
3663    public function getCombinationImagesJS()
3664    {
3665        /** @var Product $obj */
3666        if (!($obj = $this->loadObject(true))) {
3667            return '';
3668        }
3669
3670        $content = 'var combination_images = new Array();';
3671        if (!$allCombinationImages = $obj->getCombinationImages($this->context->language->id)) {
3672            return $content;
3673        }
3674        foreach ($allCombinationImages as $idProductAttribute => $combinationImages) {
3675            $i = 0;
3676            $content .= 'combination_images['.(int) $idProductAttribute.'] = new Array();';
3677            foreach ($combinationImages as $combinationImage) {
3678                $content .= 'combination_images['.(int) $idProductAttribute.']['.$i++.'] = '.(int) $combinationImage['id_image'].';';
3679            }
3680        }
3681
3682        return $content;
3683    }
3684
3685    /**
3686     * Display draft warning
3687     *
3688     * @param $active
3689     *
3690     * @since 1.0.0
3691     */
3692    protected function _displayDraftWarning($active)
3693    {
3694        $content = '<div class="warn draft" style="'.($active ? 'display:none' : '').'">
3695				<span>'.$this->l('Your product will be saved as a draft.').'</span>
3696				<a href="#" class="btn btn-default pull-right" onclick="submitAddProductAndPreview()" ><i class="icon-external-link-sign"></i> '.$this->l('Save and preview').'</a>
3697				<input type="hidden" name="fakeSubmitAddProductAndPreview" id="fakeSubmitAddProductAndPreview" />
3698	 		</div>';
3699        $this->tpl_form_vars['draft_warning'] = $content;
3700    }
3701
3702    protected function initPack(Product $product)
3703    {
3704        $this->tpl_form_vars['is_pack'] = ($product->id && Pack::isPack($product->id)) || Tools::getValue('type_product') == Product::PTYPE_PACK;
3705        $product->packItems = Pack::getItems($product->id, $this->context->language->id);
3706
3707        $inputPackItems = '';
3708        if (Tools::getValue('inputPackItems')) {
3709            $inputPackItems = Tools::getValue('inputPackItems');
3710        } else {
3711            if (is_array($product->packItems)) {
3712                foreach ($product->packItems as $packItem) {
3713                    $inputPackItems .= $packItem->pack_quantity.'x'.$packItem->id.'-';
3714                }
3715            }
3716        }
3717        $this->tpl_form_vars['input_pack_items'] = $inputPackItems;
3718
3719        $inputNamepackItems = '';
3720        if (Tools::getValue('namePackItems')) {
3721            $inputNamepackItems = Tools::getValue('namePackItems');
3722        } else {
3723            if (is_array($product->packItems)) {
3724                foreach ($product->packItems as $packItem) {
3725                    $inputNamepackItems .= $packItem->pack_quantity.' x '.$packItem->name.'¤';
3726                }
3727            }
3728        }
3729        $this->tpl_form_vars['input_namepack_items'] = $inputNamepackItems;
3730    }
3731
3732    /**
3733     * @param Product $obj
3734     *
3735     * @throws Exception
3736     * @throws PrestaShopException
3737     * @throws SmartyException
3738     *
3739     * @since 1.0.0
3740     */
3741    public function initFormAssociations($obj)
3742    {
3743        $product = $obj;
3744        $data = $this->createTemplate($this->tpl_form);
3745        // Prepare Categories tree for display in Associations tab
3746        $root = Category::getRootCategory();
3747        $defaultCategory = $this->context->cookie->id_category_products_filter ? $this->context->cookie->id_category_products_filter : $this->context->shop->id_category;
3748        if (!$product->id || !$product->isAssociatedToShop()) {
3749            $selectedCat = Category::getCategoryInformations(Tools::getValue('categoryBox', [$defaultCategory]), $this->default_form_language);
3750        } else {
3751            if (Tools::isSubmit('categoryBox')) {
3752                $selectedCat = Category::getCategoryInformations(Tools::getValue('categoryBox', [$defaultCategory]), $this->default_form_language);
3753            } else {
3754                $selectedCat = Product::getProductCategoriesFull($product->id, $this->default_form_language);
3755            }
3756        }
3757
3758        // Multishop block
3759        $data->assign('feature_shop_active', Shop::isFeatureActive());
3760        $helper = new HelperForm();
3761        if ($this->object && $this->object->id) {
3762            $helper->id = $this->object->id;
3763        } else {
3764            $helper->id = null;
3765        }
3766        $helper->table = $this->table;
3767        $helper->identifier = $this->identifier;
3768
3769        // Accessories block
3770        $accessories = Product::getAccessoriesLight($this->context->language->id, $product->id);
3771
3772        if ($postAccessories = Tools::getValue('inputAccessories')) {
3773            $postAccessoriesTab = explode('-', $postAccessories);
3774            foreach ($postAccessoriesTab as $accessoryId) {
3775                if (!$this->haveThisAccessory($accessoryId, $accessories) && $accessory = Product::getAccessoryById($accessoryId)) {
3776                    $accessories[] = $accessory;
3777                }
3778            }
3779        }
3780        $data->assign('accessories', $accessories);
3781
3782        $product->manufacturer_name = Manufacturer::getNameById($product->id_manufacturer);
3783
3784        $categories = [];
3785        foreach ($selectedCat as $key => $category) {
3786            $categories[] = $key;
3787        }
3788
3789        $tree = new HelperTreeCategories('associated-categories-tree', 'Associated categories');
3790        $tree->setTemplate('tree_associated_categories.tpl')
3791            ->setHeaderTemplate('tree_associated_header.tpl')
3792            ->setRootCategory((int) $root->id)
3793            ->setUseCheckBox(true)
3794            ->setUseSearch(true)
3795            ->setSelectedCategories($categories);
3796
3797        $data->assign(
3798            [
3799                'default_category'    => $defaultCategory,
3800                'selected_cat_ids'    => implode(',', array_keys($selectedCat)),
3801                'selected_cat'        => $selectedCat,
3802                'id_category_default' => $product->getDefaultCategory(),
3803                'category_tree'       => $tree->render(),
3804                'product'             => $product,
3805                'link'                => $this->context->link,
3806                'is_shop_context'     => Shop::getContext() == Shop::CONTEXT_SHOP,
3807            ]
3808        );
3809
3810        $this->tpl_form_vars['custom_form'] = $data->fetch();
3811    }
3812
3813    /**
3814     * @param $accessoryId
3815     * @param $accessories
3816     *
3817     * @return bool
3818     *
3819     * @since 1.0.0
3820     */
3821    public function haveThisAccessory($accessoryId, $accessories)
3822    {
3823        foreach ($accessories as $accessory) {
3824            if ((int) $accessory['id_product'] == (int) $accessoryId) {
3825                return true;
3826            }
3827        }
3828
3829        return false;
3830    }
3831
3832    /**
3833     * @param Product $obj
3834     *
3835     * @throws Exception
3836     * @throws SmartyException
3837     *
3838     * @since 1.0.0
3839     */
3840    public function initFormPrices($obj)
3841    {
3842        $data = $this->createTemplate($this->tpl_form);
3843        $product = $obj;
3844        if ($obj->id) {
3845            $shops = Shop::getShops();
3846            $countries = Country::getCountries($this->context->language->id);
3847            $groups = Group::getGroups($this->context->language->id, true);
3848            $currencies = Currency::getCurrencies();
3849            $attributes = $obj->getAttributesGroups((int) $this->context->language->id);
3850            $combinations = [];
3851            foreach ($attributes as $attribute) {
3852                $combinations[$attribute['id_product_attribute']]['id_product_attribute'] = $attribute['id_product_attribute'];
3853                if (!isset($combinations[$attribute['id_product_attribute']]['attributes'])) {
3854                    $combinations[$attribute['id_product_attribute']]['attributes'] = '';
3855                }
3856                $combinations[$attribute['id_product_attribute']]['attributes'] .= $attribute['attribute_name'].' - ';
3857
3858                $combinations[$attribute['id_product_attribute']]['price'] = Tools::displayPrice(
3859                    Tools::convertPrice(
3860                        Product::getPriceStatic((int) $obj->id, false, $attribute['id_product_attribute']),
3861                        $this->context->currency
3862                    ),
3863                    $this->context->currency
3864                );
3865            }
3866            foreach ($combinations as &$combination) {
3867                $combination['attributes'] = rtrim($combination['attributes'], ' - ');
3868            }
3869            $data->assign(
3870                'specificPriceModificationForm',
3871                $this->_displaySpecificPriceModificationForm($this->context->currency, $shops, $currencies, $countries, $groups)
3872            );
3873
3874            $data->assign('ecotax_tax_excl', (float) $obj->ecotax);
3875            $this->_applyTaxToEcotax($obj);
3876
3877            $data->assign(
3878                [
3879                    'shops'          => $shops,
3880                    'admin_one_shop' => count($this->context->employee->getAssociatedShops()) == 1,
3881                    'currencies'     => $currencies,
3882                    'countries'      => $countries,
3883                    'groups'         => $groups,
3884                    'combinations'   => $combinations,
3885                    'multi_shop'     => Shop::isFeatureActive(),
3886                    'link'           => new Link(),
3887                    'pack'           => new Pack(),
3888                ]
3889            );
3890        } else {
3891            $this->displayWarning($this->l('You must save this product before adding specific pricing'));
3892            $product->id_tax_rules_group = (int) Product::getIdTaxRulesGroupMostUsed();
3893            $data->assign('ecotax_tax_excl', 0);
3894        }
3895
3896        $address = new Address();
3897        $address->id_country = (int) $this->context->country->id;
3898        $taxRulesGroups = TaxRulesGroup::getTaxRulesGroups(true);
3899        $taxRates = [
3900            0 => [
3901                'id_tax_rules_group' => 0,
3902                'rates'              => [0],
3903                'computation_method' => 0,
3904            ],
3905        ];
3906
3907        foreach ($taxRulesGroups as $taxRulesGroup) {
3908            $idTaxRulesGroup = (int) $taxRulesGroup['id_tax_rules_group'];
3909            $taxCalculator = TaxManagerFactory::getManager($address, $idTaxRulesGroup)->getTaxCalculator();
3910            $taxRates[$idTaxRulesGroup] = [
3911                'id_tax_rules_group' => $idTaxRulesGroup,
3912                'rates'              => [],
3913                'computation_method' => (int) $taxCalculator->computation_method,
3914            ];
3915
3916            if (isset($taxCalculator->taxes) && count($taxCalculator->taxes)) {
3917                foreach ($taxCalculator->taxes as $tax) {
3918                    $taxRates[$idTaxRulesGroup]['rates'][] = (float) $tax->rate;
3919                }
3920            } else {
3921                $taxRates[$idTaxRulesGroup]['rates'][] = 0;
3922            }
3923        }
3924
3925        // prices part
3926        $data->assign(
3927            [
3928                'link'                    => $this->context->link,
3929                'currency'                => $currency = $this->context->currency,
3930                'tax_rules_groups'        => $taxRulesGroups,
3931                'taxesRatesByGroup'       => $taxRates,
3932                'ecotaxTaxRate'           => Tax::getProductEcotaxRate(),
3933                'tax_exclude_taxe_option' => Tax::excludeTaxeOption(),
3934                'ps_use_ecotax'           => Configuration::get('PS_USE_ECOTAX'),
3935            ]
3936        );
3937
3938        $product->price = Tools::convertPrice($product->price, $this->context->currency, true, $this->context);
3939        if ($product->unit_price_ratio != 0) {
3940            $data->assign('unit_price', priceval(
3941                $product->price / $product->unit_price_ratio
3942            ));
3943        } else {
3944            $data->assign('unit_price', 0);
3945        }
3946        $data->assign('ps_tax', Configuration::get('PS_TAX'));
3947
3948        $data->assign('country_display_tax_label', $this->context->country->display_tax_label);
3949        $data->assign(
3950            [
3951                'currency', $this->context->currency,
3952                'product' => $product,
3953                'token'   => $this->token,
3954            ]
3955        );
3956
3957        $this->tpl_form_vars['custom_form'] = $data->fetch();
3958    }
3959
3960    /**
3961     * @param $defaultCurrency
3962     * @param $shops
3963     * @param $currencies
3964     * @param $countries
3965     * @param $groups
3966     *
3967     * @return string
3968     *
3969     * @since 1.0.0
3970     */
3971    protected function _displaySpecificPriceModificationForm($defaultCurrency, $shops, $currencies, $countries, $groups)
3972    {
3973        /** @var Product $obj */
3974        if (!($obj = $this->loadObject())) {
3975            return '';
3976        }
3977
3978        $page = (int) Tools::getValue('page');
3979        $content = '';
3980        $specificPrices = SpecificPrice::getByProductId((int) $obj->id);
3981        $specificPricePriorities = SpecificPrice::getPriority((int) $obj->id);
3982
3983        $tmp = [];
3984        foreach ($shops as $shop) {
3985            $tmp[$shop['id_shop']] = $shop;
3986        }
3987        $shops = $tmp;
3988        $tmp = [];
3989        foreach ($currencies as $currency) {
3990            $tmp[$currency['id_currency']] = $currency;
3991        }
3992        $currencies = $tmp;
3993
3994        $tmp = [];
3995        foreach ($countries as $country) {
3996            $tmp[$country['id_country']] = $country;
3997        }
3998        $countries = $tmp;
3999
4000        $tmp = [];
4001        foreach ($groups as $group) {
4002            $tmp[$group['id_group']] = $group;
4003        }
4004        $groups = $tmp;
4005
4006        $lengthBefore = strlen($content);
4007        if (is_array($specificPrices) && count($specificPrices)) {
4008            $i = 0;
4009            foreach ($specificPrices as $specificPrice) {
4010                $idCurrency = $specificPrice['id_currency'] ? $specificPrice['id_currency'] : $defaultCurrency->id;
4011                if (!isset($currencies[$idCurrency])) {
4012                    continue;
4013                }
4014
4015                $currentSpecificCurrency = $currencies[$idCurrency];
4016                if ($specificPrice['reduction_type'] == 'percentage') {
4017                    $impact = '- '.($specificPrice['reduction'] * 100).' %';
4018                } elseif ($specificPrice['reduction'] > 0) {
4019                    $impact = '- '.Tools::displayPrice(
4020                        $specificPrice['reduction'],
4021                        $currentSpecificCurrency
4022                    ).' ';
4023                    if ($specificPrice['reduction_tax']) {
4024                        $impact .= '('.$this->l('Tax incl.').')';
4025                    } else {
4026                        $impact .= '('.$this->l('Tax excl.').')';
4027                    }
4028                } else {
4029                    $impact = '--';
4030                }
4031
4032                if ($specificPrice['from'] == '0000-00-00 00:00:00' && $specificPrice['to'] == '0000-00-00 00:00:00') {
4033                    $period = $this->l('Unlimited');
4034                } else {
4035                    $period = $this->l('From').' '.($specificPrice['from'] != '0000-00-00 00:00:00' ? $specificPrice['from'] : '0000-00-00 00:00:00').'<br />'.$this->l('To').' '.($specificPrice['to'] != '0000-00-00 00:00:00' ? $specificPrice['to'] : '0000-00-00 00:00:00');
4036                }
4037                if ($specificPrice['id_product_attribute']) {
4038                    $combination = new Combination((int) $specificPrice['id_product_attribute']);
4039                    $attributes = $combination->getAttributesName((int) $this->context->language->id);
4040                    $attributesName = '';
4041                    foreach ($attributes as $attribute) {
4042                        $attributesName .= $attribute['name'].' - ';
4043                    }
4044                    $attributesName = rtrim($attributesName, ' - ');
4045                } else {
4046                    $attributesName = $this->l('All combinations');
4047                }
4048
4049                $rule = new SpecificPriceRule((int) $specificPrice['id_specific_price_rule']);
4050                $ruleName = ($rule->id ? $rule->name : '--');
4051
4052                if ($specificPrice['id_customer']) {
4053                    $customer = new Customer((int) $specificPrice['id_customer']);
4054                    if (Validate::isLoadedObject($customer)) {
4055                        $customerFullName = $customer->firstname.' '.$customer->lastname;
4056                    }
4057                    unset($customer);
4058                }
4059
4060                if (!$specificPrice['id_shop'] || in_array($specificPrice['id_shop'], Shop::getContextListShopID())) {
4061                    $content .= '
4062					<tr '.($i % 2 ? 'class="alt_row"' : '').'>
4063						<td>'.$ruleName.'</td>
4064						<td>'.$attributesName.'</td>';
4065
4066                    $canDeleteSpecificPrices = true;
4067                    if (Shop::isFeatureActive()) {
4068                        $idShopSp = $specificPrice['id_shop'];
4069                        $canDeleteSpecificPrices = (count($this->context->employee->getAssociatedShops()) > 1 && !$idShopSp) || $idShopSp;
4070                        $content .= '
4071						<td>'.($idShopSp ? $shops[$idShopSp]['name'] : $this->l('All shops')).'</td>';
4072                    }
4073                    $price = $specificPrice['price'];
4074                    $fixedPrice = '--';
4075                    if ((string) $price === (string) $obj->price
4076                        || $price == -1) {
4077                        Tools::displayPrice($price, $currentSpecificCurrency);
4078                    }
4079                    $content .= '
4080						<td>'.($specificPrice['id_currency'] ? $currencies[$specificPrice['id_currency']]['name'] : $this->l('All currencies')).'</td>
4081						<td>'.($specificPrice['id_country'] ? $countries[$specificPrice['id_country']]['name'] : $this->l('All countries')).'</td>
4082						<td>'.($specificPrice['id_group'] ? $groups[$specificPrice['id_group']]['name'] : $this->l('All groups')).'</td>
4083						<td title="'.$this->l('ID:').' '.$specificPrice['id_customer'].'">'.(isset($customerFullName) ? $customerFullName : $this->l('All customers')).'</td>
4084						<td>'.$fixedPrice.'</td>
4085						<td>'.$impact.'</td>
4086						<td>'.$period.'</td>
4087						<td>'.$specificPrice['from_quantity'].'</th>
4088						<td>'.((!$rule->id && $canDeleteSpecificPrices) ? '<a class="btn btn-default" name="delete_link" href="'.static::$currentIndex.'&id_product='.(int) Tools::getValue('id_product').'&action=deleteSpecificPrice&id_specific_price='.(int) ($specificPrice['id_specific_price']).'&token='.Tools::getValue('token').'"><i class="icon-trash"></i></a>' : '').'</td>
4089					</tr>';
4090                    $i++;
4091                    unset($customerFullName);
4092                }
4093            }
4094        }
4095
4096        if ($lengthBefore === strlen($content)) {
4097            $content .= '
4098				<tr>
4099					<td class="text-center" colspan="13"><i class="icon-warning-sign"></i>&nbsp;'.$this->l('No specific prices.').'</td>
4100				</tr>';
4101        }
4102
4103        $content .= '
4104				</tbody>
4105			</table>
4106			</div>
4107			<div class="panel-footer">
4108				<a href="'.$this->context->link->getAdminLink('AdminProducts').($page > 1 ? '&submitFilter'.$this->table.'='.(int) $page : '').'" class="btn btn-default"><i class="process-icon-cancel"></i> '.$this->l('Cancel').'</a>
4109				<button id="product_form_submit_btn"  type="submit" name="submitAddproduct" class="btn btn-default pull-right" disabled="disabled"><i class="process-icon-loading"></i> '.$this->l('Save').'</button>
4110				<button id="product_form_submit_btn"  type="submit" name="submitAddproductAndStay" class="btn btn-default pull-right" disabled="disabled"><i class="process-icon-loading"></i> '.$this->l('Save and stay').'</button>
4111			</div>
4112		</div>';
4113
4114        $content .= '
4115		<script type="text/javascript">
4116			var currencies = [];
4117			currencies[0] = [];
4118			currencies[0]["sign"] = "'.$defaultCurrency->sign.'";
4119			currencies[0]["format"] = '.intval($defaultCurrency->format).';
4120			';
4121        foreach ($currencies as $currency) {
4122            $content .= '
4123				currencies['.$currency['id_currency'].'] = new Array();
4124				currencies['.$currency['id_currency'].']["sign"] = "'.$currency['sign'].'";
4125				currencies['.$currency['id_currency'].']["format"] = '.intval($currency['format']).';
4126				';
4127        }
4128        $content .= '
4129		</script>
4130		';
4131
4132        // Not use id_customer
4133        if ($specificPricePriorities[0] == 'id_customer') {
4134            unset($specificPricePriorities[0]);
4135        }
4136        // Reindex array starting from 0
4137        $specificPricePriorities = array_values($specificPricePriorities);
4138
4139        $content .= '<div class="panel">
4140		<h3>'.$this->l('Priority management').'</h3>
4141		<div class="alert alert-info">
4142				'.$this->l('Sometimes one customer can fit into multiple price rules. Priorities allow you to define which rule applies to the customer.').'
4143		</div>';
4144
4145        $content .= '
4146		<div class="form-group">
4147			<label class="control-label col-lg-3" for="specificPricePriority1">'.$this->l('Priorities').'</label>
4148			<div class="input-group col-lg-9">
4149				<select id="specificPricePriority1" name="specificPricePriority[]">
4150					<option value="id_shop"'.($specificPricePriorities[0] == 'id_shop' ? ' selected="selected"' : '').'>'.$this->l('Shop').'</option>
4151					<option value="id_currency"'.($specificPricePriorities[0] == 'id_currency' ? ' selected="selected"' : '').'>'.$this->l('Currency').'</option>
4152					<option value="id_country"'.($specificPricePriorities[0] == 'id_country' ? ' selected="selected"' : '').'>'.$this->l('Country').'</option>
4153					<option value="id_group"'.($specificPricePriorities[0] == 'id_group' ? ' selected="selected"' : '').'>'.$this->l('Group').'</option>
4154				</select>
4155				<span class="input-group-addon"><i class="icon-chevron-right"></i></span>
4156				<select name="specificPricePriority[]">
4157					<option value="id_shop"'.($specificPricePriorities[1] == 'id_shop' ? ' selected="selected"' : '').'>'.$this->l('Shop').'</option>
4158					<option value="id_currency"'.($specificPricePriorities[1] == 'id_currency' ? ' selected="selected"' : '').'>'.$this->l('Currency').'</option>
4159					<option value="id_country"'.($specificPricePriorities[1] == 'id_country' ? ' selected="selected"' : '').'>'.$this->l('Country').'</option>
4160					<option value="id_group"'.($specificPricePriorities[1] == 'id_group' ? ' selected="selected"' : '').'>'.$this->l('Group').'</option>
4161				</select>
4162				<span class="input-group-addon"><i class="icon-chevron-right"></i></span>
4163				<select name="specificPricePriority[]">
4164					<option value="id_shop"'.($specificPricePriorities[2] == 'id_shop' ? ' selected="selected"' : '').'>'.$this->l('Shop').'</option>
4165					<option value="id_currency"'.($specificPricePriorities[2] == 'id_currency' ? ' selected="selected"' : '').'>'.$this->l('Currency').'</option>
4166					<option value="id_country"'.($specificPricePriorities[2] == 'id_country' ? ' selected="selected"' : '').'>'.$this->l('Country').'</option>
4167					<option value="id_group"'.($specificPricePriorities[2] == 'id_group' ? ' selected="selected"' : '').'>'.$this->l('Group').'</option>
4168				</select>
4169				<span class="input-group-addon"><i class="icon-chevron-right"></i></span>
4170				<select name="specificPricePriority[]">
4171					<option value="id_shop"'.($specificPricePriorities[3] == 'id_shop' ? ' selected="selected"' : '').'>'.$this->l('Shop').'</option>
4172					<option value="id_currency"'.($specificPricePriorities[3] == 'id_currency' ? ' selected="selected"' : '').'>'.$this->l('Currency').'</option>
4173					<option value="id_country"'.($specificPricePriorities[3] == 'id_country' ? ' selected="selected"' : '').'>'.$this->l('Country').'</option>
4174					<option value="id_group"'.($specificPricePriorities[3] == 'id_group' ? ' selected="selected"' : '').'>'.$this->l('Group').'</option>
4175				</select>
4176			</div>
4177		</div>
4178		<div class="form-group">
4179			<div class="col-lg-9 col-lg-offset-3">
4180				<p class="checkbox">
4181					<label for="specificPricePriorityToAll"><input type="checkbox" name="specificPricePriorityToAll" id="specificPricePriorityToAll" />'.$this->l('Apply to all products').'</label>
4182				</p>
4183			</div>
4184		</div>
4185		<div class="panel-footer">
4186				<a href="'.$this->context->link->getAdminLink('AdminProducts').($page > 1 ? '&submitFilter'.$this->table.'='.(int) $page : '').'" class="btn btn-default"><i class="process-icon-cancel"></i> '.$this->l('Cancel').'</a>
4187				<button id="product_form_submit_btn"  type="submit" name="submitAddproduct" class="btn btn-default pull-right" disabled="disabled"><i class="process-icon-loading"></i> '.$this->l('Save').'</button>
4188				<button id="product_form_submit_btn"  type="submit" name="submitAddproductAndStay" class="btn btn-default pull-right" disabled="disabled"><i class="process-icon-loading"></i> '.$this->l('Save and stay').'</button>
4189			</div>
4190		</div>
4191		';
4192
4193        return $content;
4194    }
4195
4196    /**
4197     * Apply tax to eco tax
4198     *
4199     * @param Product $product
4200     *
4201     * @since 1.0.0
4202     */
4203    protected function _applyTaxToEcotax($product)
4204    {
4205        if ($product->ecotax) {
4206            $product->ecotax = priceval(
4207                $product->ecotax * (1 + Tax::getProductEcotaxRate() / 100)
4208            );
4209        }
4210    }
4211
4212    /**
4213     * Initialize SEO form
4214     *
4215     * @param Product $product
4216     *
4217     * @since 1.0.0
4218     */
4219    public function initFormSeo($product)
4220    {
4221        if (!$this->default_form_language) {
4222            $this->getLanguages();
4223        }
4224
4225        $data = $this->createTemplate($this->tpl_form);
4226
4227        $context = $this->context;
4228        $rewrittenLinks = [];
4229        if (!Validate::isLoadedObject($product) || !$product->id_category_default) {
4230            foreach ($this->_languages as $language) {
4231                $rewrittenLinks[(int) $language['id_lang']] = [$this->l('Unable to determine the preview URL. This product has not been linked with a category, yet.')];
4232            }
4233        } else {
4234            foreach ($this->_languages as $language) {
4235                $rewrittenLinks[(int) $language['id_lang']] = explode(
4236                    '[REWRITE]',
4237                    $context->link->getProductLink($product->id, '[REWRITE]', (int) $product->id_category_default)
4238                );
4239            }
4240        }
4241
4242        $data->assign(
4243            [
4244                'product'               => $product,
4245                'languages'             => $this->_languages,
4246                'id_lang'               => $this->context->language->id,
4247                'ps_ssl_enabled'        => Configuration::get('PS_SSL_ENABLED'),
4248                'curent_shop_url'       => $this->context->shop->getBaseURL(),
4249                'default_form_language' => $this->default_form_language,
4250                'rewritten_links'       => $rewrittenLinks,
4251            ]
4252        );
4253
4254        $this->tpl_form_vars['custom_form'] = $data->fetch();
4255    }
4256
4257    /**
4258     * @param Product $product
4259     *
4260     * @throws Exception
4261     * @throws SmartyException
4262     *
4263     * @since 1.0.0
4264     */
4265    public function initFormPack($product)
4266    {
4267        $data = $this->createTemplate($this->tpl_form);
4268
4269        // If pack items have been submitted, we want to display them instead of the actuel content of the pack
4270        // in database. In case of a submit error, the posted data is not lost and can be sent again.
4271        if (Tools::getValue('namePackItems')) {
4272            $inputPackItems = Tools::getValue('inputPackItems');
4273            $inputNamepackItems = Tools::getValue('namePackItems');
4274            $packItems = $this->getPackItems();
4275        } else {
4276            $product->packItems = Pack::getItems($product->id, $this->context->language->id);
4277            $packItems = $this->getPackItems($product);
4278            $inputNamepackItems = '';
4279            $inputPackItems = '';
4280            foreach ($packItems as $packItem) {
4281                $inputPackItems .= $packItem['pack_quantity'].'x'.$packItem['id'].'x'.$packItem['id_product_attribute'].'-';
4282                $inputNamepackItems .= $packItem['pack_quantity'].' x '.$packItem['name'].'¤';
4283            }
4284        }
4285
4286        $data->assign(
4287            [
4288                'input_pack_items'     => $inputPackItems,
4289                'input_namepack_items' => $inputNamepackItems,
4290                'pack_items'           => $packItems,
4291                'product_type'         => (int) Tools::getValue('type_product', $product->getType()),
4292            ]
4293        );
4294
4295        $this->tpl_form_vars['custom_form'] = $data->fetch();
4296    }
4297
4298    /**
4299     * Get an array of pack items for display from the product object if specified, else from POST/GET values
4300     *
4301     * @param Product $product
4302     *
4303     * @return array of pack items
4304     *
4305     * @since 1.0.0
4306     */
4307    public function getPackItems($product = null)
4308    {
4309        $packItems = [];
4310
4311        if (!$product) {
4312            $namesInput = Tools::getValue('namePackItems');
4313            $idsInput = Tools::getValue('inputPackItems');
4314            if (!$namesInput || !$idsInput) {
4315                return [];
4316            }
4317            // ids is an array of string with format : QTYxID
4318            $ids = array_unique(explode('-', $idsInput));
4319            $names = array_unique(explode('¤', $namesInput));
4320
4321            if (!empty($ids)) {
4322                $length = count($ids);
4323                for ($i = 0; $i < $length; $i++) {
4324                    if (!empty($ids[$i]) && !empty($names[$i])) {
4325                        list($packItems[$i]['pack_quantity'], $packItems[$i]['id']) = explode('x', $ids[$i]);
4326                        $explodedName = explode('x', $names[$i]);
4327                        $packItems[$i]['name'] = $explodedName[1];
4328                    }
4329                }
4330            }
4331        } else {
4332            if (is_array($product->packItems)) {
4333                $i = 0;
4334                foreach ($product->packItems as $packItem) {
4335                    $packItems[$i]['id'] = $packItem->id;
4336                    $packItems[$i]['pack_quantity'] = $packItem->pack_quantity;
4337                    $packItems[$i]['name'] = $packItem->name;
4338                    $packItems[$i]['reference'] = $packItem->reference;
4339                    $packItems[$i]['id_product_attribute'] = isset($packItem->id_pack_product_attribute) && $packItem->id_pack_product_attribute ? $packItem->id_pack_product_attribute : 0;
4340                    $cover = $packItem->id_pack_product_attribute ? Product::getCombinationImageById($packItem->id_pack_product_attribute, $this->context->language->id) : Product::getCover($packItem->id);
4341                    $packItems[$i]['image'] = $this->context->link->getImageLink($packItem->link_rewrite, $cover['id_image'], 'home_default');
4342                    // @todo: don't rely on 'home_default'
4343                    //$path_to_image = _PS_IMG_DIR_.'p/'.Image::getImgFolderStatic($cover['id_image']).(int)$cover['id_image'].'.jpg';
4344                    //$pack_items[$i]['image'] = ImageManager::thumbnail($path_to_image, 'pack_mini_'.$pack_item->id.'_'.$this->context->shop->id.'.jpg', 120);
4345                    $i++;
4346                }
4347            }
4348        }
4349
4350        return $packItems;
4351    }
4352
4353    /**
4354     * Initialize virtual product form
4355     *
4356     * @param Product $product
4357     *
4358     * @since 1.0.0
4359     */
4360    public function initFormVirtualProduct($product)
4361    {
4362        $data = $this->createTemplate($this->tpl_form);
4363
4364        $currency = $this->context->currency;
4365
4366        $idProductDownload = ProductDownload::getIdFromIdProduct($product->id, false);
4367        // This might give an empty record, which is fine.
4368        $productDownload = new ProductDownload($idProductDownload);
4369
4370        if (!ProductDownload::checkWritableDir()) {
4371            $this->errors[] = Tools::displayError('Download repository is not writable.');
4372            $this->tab_display = 'VirtualProduct';
4373        }
4374
4375        if ($productDownload->id) {
4376            // Check the downloadable file.
4377            $fileNotRight = false;
4378            if (!$productDownload->filename) {
4379                $this->errors[] = Tools::displayError('A downloadable file is missing.');
4380                $fileNotRight = true;
4381            } else {
4382                if (!$productDownload->display_filename) {
4383                    $this->errors[] = Tools::displayError('A file name is required.');
4384                    $fileNotRight = true;
4385                }
4386                if (!$productDownload->checkFile()) {
4387                    $this->errors[] = Tools::displayError('File on the server is missing, should be').' '._PS_DOWNLOAD_DIR_.$productDownload->filename.'.';
4388                    $fileNotRight = true;
4389                }
4390            }
4391            if ($fileNotRight) {
4392                if ($productDownload->active) {
4393                    $productDownload->active = false;
4394                    $productDownload->update();
4395                }
4396
4397                $this->tab_display = 'VirtualProduct';
4398            }
4399        }
4400
4401        $product->productDownload = $productDownload;
4402
4403        $virtualProductFileUploader = new HelperUploader('virtual_product_file_uploader');
4404        $virtualProductFileUploader->setMultiple(false)->setUrl(
4405            $this->context->link->getAdminLink('AdminProducts').'&ajax=1&id_product='.(int) $product->id
4406            .'&action=AddVirtualProductFile'
4407        )->setPostMaxSize(Tools::getOctets(ini_get('upload_max_filesize')))
4408            ->setTemplate('virtual_product.tpl');
4409
4410        if ($productDownload->date_expiration !== '0000-00-00 00:00:00') {
4411            $productDownload->date_expiration = substr($productDownload->date_expiration, 0, 10);
4412        } else {
4413            $productDownload->date_expiration = '';
4414        }
4415
4416        $data->assign(
4417            [
4418                'product'                       => $product,
4419                'token'                         => $this->token,
4420                'currency'                      => $currency,
4421                'link'                          => $this->context->link,
4422                'is_file'                       => $product->productDownload->checkFile(),
4423                'virtual_product_file_uploader' => $virtualProductFileUploader->render(),
4424            ]
4425        );
4426        $data->assign($this->tpl_form_vars);
4427        $this->tpl_form_vars['product'] = $product;
4428        $this->tpl_form_vars['custom_form'] = $data->fetch();
4429    }
4430
4431    /**
4432     * @param Product $obj
4433     *
4434     * @throws Exception
4435     * @throws SmartyException
4436     */
4437    public function initFormCustomization($obj)
4438    {
4439        $data = $this->createTemplate($this->tpl_form);
4440
4441        if ((bool) $obj->id) {
4442            if ($this->product_exists_in_shop) {
4443                $labels = $obj->getCustomizationFields();
4444
4445                $hasFileLabels = (int) $this->getFieldValue($obj, 'uploadable_files');
4446                $hasTextLabels = (int) $this->getFieldValue($obj, 'text_fields');
4447
4448                $data->assign(
4449                    [
4450                        'obj'                 => $obj,
4451                        'table'               => $this->table,
4452                        'languages'           => $this->_languages,
4453                        'has_file_labels'     => $hasFileLabels,
4454                        'display_file_labels' => $this->_displayLabelFields($obj, $labels, $this->_languages, Configuration::get('PS_LANG_DEFAULT'), Product::CUSTOMIZE_FILE),
4455                        'has_text_labels'     => $hasTextLabels,
4456                        'display_text_labels' => $this->_displayLabelFields($obj, $labels, $this->_languages, Configuration::get('PS_LANG_DEFAULT'), Product::CUSTOMIZE_TEXTFIELD),
4457                        'uploadable_files'    => (int) ($this->getFieldValue($obj, 'uploadable_files') ? (int) $this->getFieldValue($obj, 'uploadable_files') : '0'),
4458                        'text_fields'         => (int) ($this->getFieldValue($obj, 'text_fields') ? (int) $this->getFieldValue($obj, 'text_fields') : '0'),
4459                    ]
4460                );
4461            } else {
4462                $this->displayWarning($this->l('You must save the product in this shop before adding customization.'));
4463            }
4464        } else {
4465            $this->displayWarning($this->l('You must save this product before adding customization.'));
4466        }
4467
4468        $this->tpl_form_vars['custom_form'] = $data->fetch();
4469    }
4470
4471    /**
4472     * @param $obj
4473     * @param $labels
4474     * @param $languages
4475     * @param $defaultLanguage
4476     * @param $type
4477     *
4478     * @return string
4479     *
4480     * @since 1.0.0
4481     */
4482    protected function _displayLabelFields(&$obj, &$labels, $languages, $defaultLanguage, $type)
4483    {
4484        $content = '';
4485        $type = (int) ($type);
4486        $labelGenerated = [Product::CUSTOMIZE_FILE => (isset($labels[Product::CUSTOMIZE_FILE]) ? count($labels[Product::CUSTOMIZE_FILE]) : 0), Product::CUSTOMIZE_TEXTFIELD => (isset($labels[Product::CUSTOMIZE_TEXTFIELD]) ? count($labels[Product::CUSTOMIZE_TEXTFIELD]) : 0)];
4487
4488        $fieldIds = $this->_getCustomizationFieldIds($labels, $labelGenerated, $obj);
4489        if (isset($labels[$type])) {
4490            foreach ($labels[$type] as $idCustomizationField => $label) {
4491                $content .= $this->_displayLabelField($label, $languages, $defaultLanguage, $type, $fieldIds, (int) ($idCustomizationField));
4492            }
4493        }
4494
4495        return $content;
4496    }
4497
4498    /**
4499     * @param $labels
4500     * @param $alreadyGenerated
4501     * @param $obj
4502     *
4503     * @return string
4504     *
4505     * @since 1.0.0
4506     */
4507    protected function _getCustomizationFieldIds($labels, $alreadyGenerated, $obj)
4508    {
4509        $customizableFieldIds = [];
4510        if (isset($labels[Product::CUSTOMIZE_FILE])) {
4511            foreach ($labels[Product::CUSTOMIZE_FILE] as $idCustomizationField => $label) {
4512                $customizableFieldIds[] = 'label_'.Product::CUSTOMIZE_FILE.'_'.(int) ($idCustomizationField);
4513            }
4514        }
4515        if (isset($labels[Product::CUSTOMIZE_TEXTFIELD])) {
4516            foreach ($labels[Product::CUSTOMIZE_TEXTFIELD] as $idCustomizationField => $label) {
4517                $customizableFieldIds[] = 'label_'.Product::CUSTOMIZE_TEXTFIELD.'_'.(int) ($idCustomizationField);
4518            }
4519        }
4520        $j = 0;
4521        for ($i = $alreadyGenerated[Product::CUSTOMIZE_FILE]; $i < (int) ($this->getFieldValue($obj, 'uploadable_files')); $i++) {
4522            $customizableFieldIds[] = 'newLabel_'.Product::CUSTOMIZE_FILE.'_'.$j++;
4523        }
4524        $j = 0;
4525        for ($i = $alreadyGenerated[Product::CUSTOMIZE_TEXTFIELD]; $i < (int) ($this->getFieldValue($obj, 'text_fields')); $i++) {
4526            $customizableFieldIds[] = 'newLabel_'.Product::CUSTOMIZE_TEXTFIELD.'_'.$j++;
4527        }
4528
4529        return implode('¤', $customizableFieldIds);
4530    }
4531
4532    /**
4533     * @param $label
4534     * @param $languages
4535     * @param $defaultLanguage
4536     * @param $type
4537     * @param $fieldIds
4538     * @param $idCustomizationField
4539     *
4540     * @return string
4541     *
4542     * @since 1.0.0
4543     */
4544    protected function _displayLabelField(&$label, $languages, $defaultLanguage, $type, $fieldIds, $idCustomizationField)
4545    {
4546        foreach ($languages as $language) {
4547            $inputValue[$language['id_lang']] = (isset($label[(int) ($language['id_lang'])])) ? $label[(int) ($language['id_lang'])]['name'] : '';
4548        }
4549
4550        $required = (isset($label[(int) ($language['id_lang'])])) ? $label[(int) ($language['id_lang'])]['required'] : false;
4551
4552        $template = $this->context->smarty->createTemplate(
4553            'controllers/products/input_text_lang.tpl',
4554            $this->context->smarty
4555        );
4556
4557        return '<div class="form-group">'
4558            .'<div class="col-lg-6">'
4559            .$template->assign(
4560                [
4561                    'languages'   => $languages,
4562                    'input_name'  => 'label_'.$type.'_'.(int) ($idCustomizationField),
4563                    'input_value' => $inputValue,
4564                ]
4565            )->fetch()
4566            .'</div>'
4567            .'<div class="col-lg-6">'
4568            .'<div class="checkbox">'
4569            .'<label for="require_'.$type.'_'.(int) ($idCustomizationField).'">'
4570            .'<input type="checkbox" name="require_'.$type.'_'.(int) ($idCustomizationField).'" id="require_'.$type.'_'.(int) ($idCustomizationField).'" value="1" '.($required ? 'checked="checked"' : '').'/>'
4571            .$this->l('Required')
4572            .'</label>'
4573            .'</div>'
4574            .'</div>'
4575            .'</div>';
4576    }
4577
4578    public function initFormAttachments($obj)
4579    {
4580        if (!$this->default_form_language) {
4581            $this->getLanguages();
4582        }
4583
4584        $data = $this->createTemplate($this->tpl_form);
4585        $data->assign('default_form_language', $this->default_form_language);
4586
4587        if ((bool) $obj->id) {
4588            if ($this->product_exists_in_shop) {
4589                $attachmentName = [];
4590                $attachmentDescription = [];
4591                foreach ($this->_languages as $language) {
4592                    $attachmentName[$language['id_lang']] = '';
4593                    $attachmentDescription[$language['id_lang']] = '';
4594                }
4595
4596                $isoTinyMce = $this->context->language->iso_code;
4597                $isoTinyMce = (file_exists(_PS_JS_DIR_.'tiny_mce/langs/'.$isoTinyMce.'.js') ? $isoTinyMce : 'en');
4598
4599                $attachmentUploader = new HelperUploader('attachment_file');
4600                $attachmentUploader
4601                    ->setMultiple(false)
4602                    ->setUseAjax(true)
4603                    ->setUrl($this->context->link->getAdminLink('AdminProducts').'&ajax=1&id_product='.(int) $obj->id.'&action=AddAttachment')
4604                    ->setPostMaxSize((Configuration::get('PS_ATTACHMENT_MAXIMUM_SIZE') * 1024 * 1024))
4605                    ->setTemplate('attachment_ajax.tpl');
4606
4607                $data->assign(
4608                    [
4609                        'obj'                    => $obj,
4610                        'table'                  => $this->table,
4611                        'ad'                     => __PS_BASE_URI__.basename(_PS_ADMIN_DIR_),
4612                        'iso_tiny_mce'           => $isoTinyMce,
4613                        'languages'              => $this->_languages,
4614                        'id_lang'                => $this->context->language->id,
4615                        'attach1'                => Attachment::getAttachments($this->context->language->id, $obj->id, true),
4616                        'attach2'                => Attachment::getAttachments($this->context->language->id, $obj->id, false),
4617                        'default_form_language'  => (int) Configuration::get('PS_LANG_DEFAULT'),
4618                        'attachment_name'        => $attachmentName,
4619                        'attachment_description' => $attachmentDescription,
4620                        'attachment_uploader'    => $attachmentUploader->render(),
4621                    ]
4622                );
4623            } else {
4624                $this->displayWarning($this->l('You must save the product in this shop before adding attachements.'));
4625            }
4626        } else {
4627            $this->displayWarning($this->l('You must save this product before adding attachements.'));
4628        }
4629
4630        $this->tpl_form_vars['custom_form'] = $data->fetch();
4631    }
4632
4633    /**
4634     * @param Product $product
4635     *
4636     * @throws Exception
4637     * @throws SmartyException
4638     *
4639     * @since 1.0.0
4640     */
4641    public function initFormInformations($product)
4642    {
4643        if (!$this->default_form_language) {
4644            $this->getLanguages();
4645        }
4646
4647        $data = $this->createTemplate($this->tpl_form);
4648
4649        $currency = $this->context->currency;
4650
4651        $data->assign(
4652            [
4653                'languages'             => $this->_languages,
4654                'default_form_language' => $this->default_form_language,
4655                'currency'              => $currency,
4656            ]
4657        );
4658        $this->object = $product;
4659        //$this->display = 'edit';
4660        $data->assign('product_name_redirected', Product::getProductName((int) $product->id_product_redirected, null, (int) $this->context->language->id));
4661        /*
4662        * Form for adding a virtual product like software, mp3, etc...
4663        */
4664        $productDownload = new ProductDownload();
4665        if ($idProductDownload = $productDownload->getIdFromIdProduct($this->getFieldValue($product, 'id'))) {
4666            $productDownload = new ProductDownload($idProductDownload);
4667        }
4668
4669        $product->{'productDownload'} = $productDownload;
4670
4671        $productProps = [];
4672        // global informations
4673        array_push($productProps, 'reference', 'ean13', 'upc', 'available_for_order', 'show_price', 'online_only', 'id_manufacturer');
4674
4675        // specific / detailled information
4676        array_push(
4677            $productProps,
4678            // physical product
4679            'width', 'height', 'weight', 'active',
4680            // virtual product
4681            'is_virtual', 'cache_default_attribute',
4682            // customization
4683            'uploadable_files', 'text_fields'
4684        );
4685        // prices
4686        array_push(
4687            $productProps,
4688            'price',
4689            'wholesale_price',
4690            'id_tax_rules_group',
4691            'unit_price_ratio',
4692            'on_sale',
4693            'unity',
4694            'minimum_quantity',
4695            'additional_shipping_cost',
4696            'available_now',
4697            'available_later',
4698            'available_date'
4699        );
4700
4701        if (Configuration::get('PS_USE_ECOTAX')) {
4702            array_push($productProps, 'ecotax');
4703        }
4704
4705        foreach ($productProps as $prop) {
4706            $product->$prop = $this->getFieldValue($product, $prop);
4707        }
4708
4709        $product->name['class'] = 'updateCurrentText';
4710        if (!$product->id || Configuration::get('PS_FORCE_FRIENDLY_PRODUCT')) {
4711            $product->name['class'] .= ' copy2friendlyUrl';
4712        }
4713
4714        $images = Image::getImages($this->context->language->id, $product->id);
4715
4716        if (is_array($images)) {
4717            foreach ($images as $k => $image) {
4718                $images[$k]['src'] = $this->context->link->getImageLink($product->link_rewrite[$this->context->language->id], $product->id.'-'.$image['id_image'], ImageType::getFormatedName('small'));
4719            }
4720            $data->assign('images', $images);
4721        }
4722        $data->assign('imagesTypes', ImageType::getImagesTypes('products'));
4723
4724        $product->tags = Tag::getProductTags($product->id);
4725
4726        $data->assign('product_type', (int) Tools::getValue('type_product', $product->getType()));
4727        $data->assign('is_in_pack', (int) Pack::isPacked($product->id));
4728
4729        $checkProductAssociationAjax = false;
4730        if (Shop::isFeatureActive() && Shop::getContext() != Shop::CONTEXT_ALL) {
4731            $checkProductAssociationAjax = true;
4732        }
4733
4734        // TinyMCE
4735        $isoTinyMce = $this->context->language->iso_code;
4736        $isoTinyMce = (file_exists(_PS_ROOT_DIR_.'/js/tiny_mce/langs/'.$isoTinyMce.'.js') ? $isoTinyMce : 'en');
4737        $data->assign(
4738            [
4739                'ad'                             => dirname($_SERVER['PHP_SELF']),
4740                'iso_tiny_mce'                   => $isoTinyMce,
4741                'check_product_association_ajax' => $checkProductAssociationAjax,
4742                'id_lang'                        => $this->context->language->id,
4743                'product'                        => $product,
4744                'token'                          => $this->token,
4745                'currency'                       => $currency,
4746                'link'                           => $this->context->link,
4747                'PS_PRODUCT_SHORT_DESC_LIMIT'    => Configuration::get('PS_PRODUCT_SHORT_DESC_LIMIT') ? Configuration::get('PS_PRODUCT_SHORT_DESC_LIMIT') : 400,
4748            ]
4749        );
4750        $data->assign($this->tpl_form_vars);
4751
4752        $this->tpl_form_vars['product'] = $product;
4753        $this->tpl_form_vars['custom_form'] = $data->fetch();
4754    }
4755
4756    /**
4757     * Initialize shipping form
4758     *
4759     * @param $obj
4760     *
4761     * @since 1.0.0
4762     */
4763    public function initFormShipping($obj)
4764    {
4765        $data = $this->createTemplate($this->tpl_form);
4766        $data->assign(
4767            [
4768                'product'                   => $obj,
4769                'ps_dimension_unit'         => Configuration::get('PS_DIMENSION_UNIT'),
4770                'ps_weight_unit'            => Configuration::get('PS_WEIGHT_UNIT'),
4771                'carrier_list'              => $this->getCarrierList(),
4772                'currency'                  => $this->context->currency,
4773                'country_display_tax_label' => $this->context->country->display_tax_label,
4774            ]
4775        );
4776        $this->tpl_form_vars['custom_form'] = $data->fetch();
4777    }
4778
4779    /**
4780     * Get carrier list
4781     *
4782     * @return array
4783     *
4784     * @since 1.0.0
4785     */
4786    protected function getCarrierList()
4787    {
4788        $carrierList = Carrier::getCarriers($this->context->language->id, false, false, false, null, Carrier::ALL_CARRIERS);
4789
4790        if ($product = $this->loadObject(true)) {
4791            /** @var Product $product */
4792            $carrierSelectedList = $product->getCarriers();
4793            foreach ($carrierList as &$carrier) {
4794                foreach ($carrierSelectedList as $carrierSelected) {
4795                    if ($carrierSelected['id_reference'] == $carrier['id_reference']) {
4796                        $carrier['selected'] = true;
4797                        continue;
4798                    }
4799                }
4800            }
4801        }
4802
4803        return $carrierList;
4804    }
4805
4806    /**
4807     * Ajax process add product image
4808     *
4809     * @since 1.0.0
4810     */
4811    public function ajaxProcessAddProductImage()
4812    {
4813        static::$currentIndex = 'index.php?tab=AdminProducts';
4814        $product = new Product((int) Tools::getValue('id_product'));
4815        $legends = Tools::getValue('legend');
4816
4817        if (!is_array($legends)) {
4818            $legends = (array) $legends;
4819        }
4820
4821        if (!Validate::isLoadedObject($product)) {
4822            $files = [];
4823            $files[0]['error'] = Tools::displayError('Cannot add image because product creation failed.');
4824        }
4825
4826        $imageUploader = new HelperImageUploader('file');
4827        $imageUploader->setAcceptTypes(['jpeg', 'gif', 'png', 'jpg'])->setMaxSize($this->max_image_size);
4828        $files = $imageUploader->process();
4829
4830        foreach ($files as &$file) {
4831            $image = new Image();
4832            $image->id_product = (int) ($product->id);
4833            $image->position = Image::getHighestPosition($product->id) + 1;
4834
4835            foreach ($legends as $key => $legend) {
4836                if (!empty($legend)) {
4837                    $image->legend[(int) $key] = $legend;
4838                }
4839            }
4840
4841            if (!Image::getCover($image->id_product)) {
4842                $image->cover = 1;
4843            } else {
4844                $image->cover = 0;
4845            }
4846
4847            if (($validate = $image->validateFieldsLang(false, true)) !== true) {
4848                $file['error'] = Tools::displayError($validate);
4849            }
4850
4851            if (isset($file['error']) && (!is_numeric($file['error']) || $file['error'] != 0)) {
4852                continue;
4853            }
4854
4855            if (!$image->add()) {
4856                $file['error'] = Tools::displayError('Error while creating additional image');
4857            } else {
4858                if (!$newPath = $image->getPathForCreation()) {
4859                    $file['error'] = Tools::displayError('An error occurred during new folder creation');
4860                    continue;
4861                }
4862
4863                $error = 0;
4864
4865                if (!ImageManager::resize($file['save_path'], $newPath.'.'.$image->image_format, null, null, 'jpg', false, $error)) {
4866                    switch ($error) {
4867                        case ImageManager::ERROR_FILE_NOT_EXIST:
4868                            $file['error'] = Tools::displayError('An error occurred while copying image, the file does not exist anymore.');
4869                            break;
4870
4871                        case ImageManager::ERROR_FILE_WIDTH:
4872                            $file['error'] = Tools::displayError('An error occurred while copying image, the file width is 0px.');
4873                            break;
4874
4875                        case ImageManager::ERROR_MEMORY_LIMIT:
4876                            $file['error'] = Tools::displayError('An error occurred while copying image, check your memory limit.');
4877                            break;
4878
4879                        default:
4880                            $file['error'] = Tools::displayError('An error occurred while copying image.');
4881                            break;
4882                    }
4883                    continue;
4884                } else {
4885                    $imagesTypes = ImageType::getImagesTypes('products');
4886                    $generateHighDpiImages = (bool) Configuration::get('PS_HIGHT_DPI');
4887
4888                    foreach ($imagesTypes as $imageType) {
4889                        if (!ImageManager::resize($file['save_path'], $newPath.'-'.stripslashes($imageType['name']).'.'.$image->image_format, $imageType['width'], $imageType['height'], $image->image_format)) {
4890                            $file['error'] = Tools::displayError('An error occurred while copying image:').' '.stripslashes($imageType['name']);
4891                            continue;
4892                        }
4893
4894                        if ($generateHighDpiImages) {
4895                            if (!ImageManager::resize($file['save_path'], $newPath.'-'.stripslashes($imageType['name']).'2x.'.$image->image_format, (int) $imageType['width'] * 2, (int) $imageType['height'] * 2, $image->image_format)) {
4896                                $file['error'] = Tools::displayError('An error occurred while copying image:').' '.stripslashes($imageType['name']);
4897                                continue;
4898                            }
4899                        }
4900                    }
4901                }
4902
4903                unlink($file['save_path']);
4904                //Necesary to prevent hacking
4905                unset($file['save_path']);
4906                Hook::exec('actionWatermark', ['id_image' => $image->id, 'id_product' => $product->id]);
4907
4908                if (!$image->update()) {
4909                    $file['error'] = Tools::displayError('Error while updating status');
4910                    continue;
4911                }
4912
4913                // Associate image to shop from context
4914                $shops = Shop::getContextListShopID();
4915                $image->associateTo($shops);
4916                $jsonShops = [];
4917
4918                foreach ($shops as $id_shop) {
4919                    $jsonShops[$id_shop] = true;
4920                }
4921
4922                $file['status'] = 'ok';
4923                $file['id'] = $image->id;
4924                $file['position'] = $image->position;
4925                $file['cover'] = $image->cover;
4926                $file['legend'] = $image->legend;
4927                $file['path'] = $image->getExistingImgPath();
4928                $file['shops'] = $jsonShops;
4929
4930                @unlink(_PS_TMP_IMG_DIR_.'product_'.(int) $product->id.'.jpg');
4931                @unlink(_PS_TMP_IMG_DIR_.'product_mini_'.(int) $product->id.'_'.$this->context->shop->id.'.jpg');
4932            }
4933        }
4934
4935        $this->ajaxDie(json_encode([$imageUploader->getName() => $files]));
4936    }
4937
4938    /**
4939     * @param Product $obj
4940     *
4941     * @throws Exception
4942     * @throws SmartyException
4943     *
4944     * @since 1.0.0
4945     */
4946    public function initFormImages($obj)
4947    {
4948        $data = $this->createTemplate($this->tpl_form);
4949
4950        if ((bool) $obj->id) {
4951            if ($this->product_exists_in_shop) {
4952                $data->assign('product', $this->loadObject());
4953
4954                $shops = false;
4955                if (Shop::isFeatureActive()) {
4956                    $shops = Shop::getShops();
4957                }
4958
4959                if ($shops) {
4960                    foreach ($shops as $key => $shop) {
4961                        if (!$obj->isAssociatedToShop($shop['id_shop'])) {
4962                            unset($shops[$key]);
4963                        }
4964                    }
4965                }
4966
4967                $data->assign('shops', $shops);
4968
4969                $images = Image::getImages(null, $obj->id);
4970                foreach ($images as $k => $image) {
4971                    $images[$k] = new Image($image['id_image']);
4972                }
4973
4974                if ($this->context->shop->getContext() == Shop::CONTEXT_SHOP) {
4975                    $currentShopId = (int) $this->context->shop->id;
4976                } else {
4977                    $currentShopId = 0;
4978                }
4979
4980                $languages = Language::getLanguages(true);
4981                $imageUploader = new HelperImageUploader('file');
4982                $imageUploader->setMultiple(!(Tools::getUserBrowser() == 'Apple Safari' && Tools::getUserPlatform() == 'Windows'))
4983                    ->setUseAjax(true)->setUrl($this->context->link->getAdminLink('AdminProducts').'&ajax=1&id_product='.(int) $obj->id.'&action=addProductImage');
4984
4985                $data->assign(
4986                    [
4987                        'countImages'         => count($images),
4988                        'id_product'          => (int) Tools::getValue('id_product'),
4989                        'id_category_default' => (int) $this->_category->id,
4990                        'images'              => $images,
4991                        'iso_lang'            => $languages[0]['iso_code'],
4992                        'token'               => $this->token,
4993                        'table'               => $this->table,
4994                        'max_image_size'      => $this->max_image_size / 1024 / 1024,
4995                        'currency'            => $this->context->currency,
4996                        'current_shop_id'     => $currentShopId,
4997                        'languages'           => $this->_languages,
4998                        'default_language'    => (int) Configuration::get('PS_LANG_DEFAULT'),
4999                        'image_uploader'      => $imageUploader->render(),
5000                    ]
5001                );
5002
5003                $type = ImageType::getByNameNType('%', 'products', 'height');
5004                if (isset($type['name'])) {
5005                    $data->assign('imageType', $type['name']);
5006                } else {
5007                    $data->assign('imageType', ImageType::getFormatedName('small'));
5008                }
5009            } else {
5010                $this->displayWarning($this->l('You must save the product in this shop before adding images.'));
5011            }
5012        } else {
5013            $this->displayWarning($this->l('You must save this product before adding images.'));
5014        }
5015
5016        $this->tpl_form_vars['custom_form'] = $data->fetch();
5017    }
5018
5019    /**
5020     * Initialize combinations form
5021     *
5022     * @param $obj
5023     *
5024     * @since 1.0.0
5025     */
5026    public function initFormCombinations($obj)
5027    {
5028        return $this->initFormAttributes($obj);
5029    }
5030
5031    /**
5032     * @param Product $product
5033     *
5034     * @throws Exception
5035     * @throws SmartyException
5036     */
5037    public function initFormAttributes($product)
5038    {
5039        $data = $this->createTemplate($this->tpl_form);
5040        if (!Combination::isFeatureActive()) {
5041            $this->displayWarning(
5042                $this->l('This feature has been disabled. ').
5043                ' <a href="index.php?tab=AdminPerformance&token='.Tools::getAdminTokenLite('AdminPerformance').'#featuresDetachables">'.$this->l('Performances').'</a>'
5044            );
5045        } elseif (Validate::isLoadedObject($product)) {
5046            if ($this->product_exists_in_shop) {
5047                if ($product->is_virtual) {
5048                    $data->assign('product', $product);
5049                    $this->displayWarning($this->l('A virtual product cannot have combinations.'));
5050                } else {
5051                    $attributeJs = [];
5052                    $attributes = Attribute::getAttributes($this->context->language->id, true);
5053                    foreach ($attributes as $k => $attribute) {
5054                        $attributeJs[$attribute['id_attribute_group']][$attribute['id_attribute']] = $attribute['name'];
5055                    }
5056                    foreach ($attributeJs as $k => $ajs) {
5057                        natsort($attributeJs[$k]);
5058                    }
5059
5060                    $currency = $this->context->currency;
5061
5062                    $data->assign('attributeJs', $attributeJs);
5063                    $data->assign('attributes_groups', AttributeGroup::getAttributesGroups($this->context->language->id));
5064
5065                    $data->assign('currency', $currency);
5066
5067                    $images = Image::getImages($this->context->language->id, $product->id);
5068
5069                    $data->assign('tax_exclude_option', Tax::excludeTaxeOption());
5070                    $data->assign('ps_weight_unit', Configuration::get('PS_WEIGHT_UNIT'));
5071
5072                    $data->assign('ps_use_ecotax', Configuration::get('PS_USE_ECOTAX'));
5073                    $data->assign('field_value_unity', $this->getFieldValue($product, 'unity'));
5074
5075                    $data->assign('reasons', $reasons = StockMvtReason::getStockMvtReasons($this->context->language->id));
5076                    $data->assign('ps_stock_mvt_reason_default', $psStockMvtReasonDefault = Configuration::get('PS_STOCK_MVT_REASON_DEFAULT'));
5077                    $data->assign('minimal_quantity', $this->getFieldValue($product, 'minimal_quantity') ? $this->getFieldValue($product, 'minimal_quantity') : 1);
5078                    $data->assign('available_date', ($this->getFieldValue($product, 'available_date') != 0) ? stripslashes(htmlentities($this->getFieldValue($product, 'available_date'), $this->context->language->id)) : '0000-00-00');
5079
5080                    $i = 0;
5081                    $type = ImageType::getByNameNType('%', 'products', 'height');
5082                    if (isset($type['name'])) {
5083                        $data->assign('imageType', $type['name']);
5084                    } else {
5085                        $data->assign('imageType', ImageType::getFormatedName('small'));
5086                    }
5087                    $data->assign('imageWidth', (isset($imageType['width']) ? (int) ($imageType['width']) : 64) + 25);
5088                    foreach ($images as $k => $image) {
5089                        $images[$k]['obj'] = new Image($image['id_image']);
5090                        ++$i;
5091                    }
5092                    $data->assign('images', $images);
5093
5094                    $data->assign($this->tpl_form_vars);
5095                    $data->assign(
5096                        [
5097                            'list'               => $this->renderListAttributes($product, $currency),
5098                            'product'            => $product,
5099                            'id_category'        => $product->getDefaultCategory(),
5100                            'token_generator'    => Tools::getAdminTokenLite('AdminAttributeGenerator'),
5101                            'combination_exists' => (Shop::isFeatureActive() && (Shop::getContextShopGroup()->share_stock) && count(AttributeGroup::getAttributesGroups($this->context->language->id)) > 0 && $product->hasAttributes()),
5102                        ]
5103                    );
5104                }
5105            } else {
5106                $this->displayWarning($this->l('You must save the product in this shop before adding combinations.'));
5107            }
5108        } else {
5109            $data->assign('product', $product);
5110            $this->displayWarning($this->l('You must save this product before adding combinations.'));
5111        }
5112
5113        $this->tpl_form_vars['custom_form'] = $data->fetch();
5114    }
5115
5116    /**
5117     * @param Product            $product
5118     * @param Currency|array|int $currency
5119     *
5120     * @return string
5121     *
5122     * @since 1.0.0
5123     */
5124    public function renderListAttributes($product, $currency)
5125    {
5126        $this->bulk_actions = ['delete' => ['text' => $this->l('Delete selected'), 'confirm' => $this->l('Delete selected items?')]];
5127        $this->addRowAction('edit');
5128        $this->addRowAction('default');
5129        $this->addRowAction('delete');
5130
5131        $defaultClass = 'highlighted';
5132
5133        $this->fields_list = [
5134            'attributes' => ['title' => $this->l('Attribute - value pair'), 'align' => 'left'],
5135            'price'      => ['title' => $this->l('Impact on price'), 'type' => 'price', 'align' => 'left'],
5136            'weight'     => ['title' => $this->l('Impact on weight'), 'align' => 'left'],
5137            'reference'  => ['title' => $this->l('Reference'), 'align' => 'left'],
5138            'ean13'      => ['title' => $this->l('EAN-13'), 'align' => 'left'],
5139            'upc'        => ['title' => $this->l('UPC'), 'align' => 'left'],
5140        ];
5141
5142        if ($product->id) {
5143            /* Build attributes combinations */
5144            $combinations = $product->getAttributeCombinations($this->context->language->id);
5145            $groups = [];
5146            $combArray = [];
5147            if (is_array($combinations)) {
5148                $combinationImages = $product->getCombinationImages($this->context->language->id);
5149                foreach ($combinations as $k => $combination) {
5150                    $priceToConvert = Tools::convertPrice($combination['price'], $currency);
5151                    $price = Tools::displayPrice($priceToConvert, $currency);
5152
5153                    $combArray[$combination['id_product_attribute']]['id_product_attribute'] = $combination['id_product_attribute'];
5154                    $combArray[$combination['id_product_attribute']]['attributes'][] = [$combination['group_name'], $combination['attribute_name'], $combination['id_attribute']];
5155                    $combArray[$combination['id_product_attribute']]['wholesale_price'] = $combination['wholesale_price'];
5156                    $combArray[$combination['id_product_attribute']]['price'] = $price;
5157                    $combArray[$combination['id_product_attribute']]['weight'] = $combination['weight'].Configuration::get('PS_WEIGHT_UNIT');
5158                    $combArray[$combination['id_product_attribute']]['unit_impact'] = $combination['unit_price_impact'];
5159                    $combArray[$combination['id_product_attribute']]['reference'] = $combination['reference'];
5160                    $combArray[$combination['id_product_attribute']]['ean13'] = $combination['ean13'];
5161                    $combArray[$combination['id_product_attribute']]['upc'] = $combination['upc'];
5162                    $combArray[$combination['id_product_attribute']]['id_image'] = isset($combinationImages[$combination['id_product_attribute']][0]['id_image']) ? $combinationImages[$combination['id_product_attribute']][0]['id_image'] : 0;
5163                    $combArray[$combination['id_product_attribute']]['available_date'] = strftime($combination['available_date']);
5164                    $combArray[$combination['id_product_attribute']]['default_on'] = $combination['default_on'];
5165                    if ($combination['is_color_group']) {
5166                        $groups[$combination['id_attribute_group']] = $combination['group_name'];
5167                    }
5168                }
5169            }
5170
5171            if (isset($combArray)) {
5172                foreach ($combArray as $id_product_attribute => $product_attribute) {
5173                    $list = '';
5174
5175                    /* In order to keep the same attributes order */
5176                    asort($product_attribute['attributes']);
5177
5178                    foreach ($product_attribute['attributes'] as $attribute) {
5179                        $list .= $attribute[0].' - '.$attribute[1].', ';
5180                    }
5181
5182                    $list = rtrim($list, ', ');
5183                    $combArray[$id_product_attribute]['image'] = $product_attribute['id_image'] ? new Image($product_attribute['id_image']) : false;
5184                    $combArray[$id_product_attribute]['available_date'] = $product_attribute['available_date'] != 0 ? date('Y-m-d', strtotime($product_attribute['available_date'])) : '0000-00-00';
5185                    $combArray[$id_product_attribute]['attributes'] = $list;
5186                    $combArray[$id_product_attribute]['name'] = $list;
5187
5188                    if ($product_attribute['default_on']) {
5189                        $combArray[$id_product_attribute]['class'] = $defaultClass;
5190                    }
5191                }
5192            }
5193        }
5194
5195        foreach ($this->actions_available as $action) {
5196            if (!in_array($action, $this->actions) && isset($this->$action) && $this->$action) {
5197                $this->actions[] = $action;
5198            }
5199        }
5200
5201        $helper = new HelperList();
5202        $helper->identifier = 'id_product_attribute';
5203        $helper->table_id = 'combinations-list';
5204        $helper->token = $this->token;
5205        $helper->currentIndex = static::$currentIndex;
5206        $helper->no_link = true;
5207        $helper->simple_header = true;
5208        $helper->show_toolbar = false;
5209        $helper->shopLinkType = $this->shopLinkType;
5210        $helper->actions = $this->actions;
5211        $helper->list_skip_actions = $this->list_skip_actions;
5212        $helper->colorOnBackground = true;
5213        $helper->override_folder = $this->tpl_folder.'combination/';
5214
5215        return $helper->generateList($combArray, $this->fields_list);
5216    }
5217
5218    /**
5219     * @param Product $obj
5220     *
5221     * @throws Exception
5222     * @throws SmartyException
5223     *
5224     * @since 1.0.0
5225     */
5226    public function initFormQuantities($obj)
5227    {
5228        if (!$this->default_form_language) {
5229            $this->getLanguages();
5230        }
5231
5232        $data = $this->createTemplate($this->tpl_form);
5233        $data->assign('default_form_language', $this->default_form_language);
5234
5235        if ($obj->id) {
5236            if ($this->product_exists_in_shop) {
5237                // Get all id_product_attribute
5238                $attributes = $obj->getAttributesResume($this->context->language->id);
5239                if (empty($attributes)) {
5240                    $attributes[] = [
5241                        'id_product_attribute'  => 0,
5242                        'attribute_designation' => '',
5243                    ];
5244                }
5245
5246                // Get available quantities
5247                $available_quantity = [];
5248                $product_designation = [];
5249
5250                foreach ($attributes as $attribute) {
5251                    // Get available quantity for the current product attribute in the current shop
5252                    $available_quantity[$attribute['id_product_attribute']] = isset($attribute['id_product_attribute']) && $attribute['id_product_attribute'] ? (int) $attribute['quantity'] : (int) $obj->quantity;
5253                    // Get all product designation
5254                    $product_designation[$attribute['id_product_attribute']] = rtrim(
5255                        $obj->name[$this->context->language->id].' - '.$attribute['attribute_designation'],
5256                        ' - '
5257                    );
5258                }
5259
5260                $show_quantities = true;
5261                $shop_context = Shop::getContext();
5262                $shop_group = new ShopGroup((int) Shop::getContextShopGroupID());
5263
5264                // if we are in all shops context, it's not possible to manage quantities at this level
5265                if (Shop::isFeatureActive() && $shop_context == Shop::CONTEXT_ALL) {
5266                    $show_quantities = false;
5267                } // if we are in group shop context
5268                elseif (Shop::isFeatureActive() && $shop_context == Shop::CONTEXT_GROUP) {
5269                    // if quantities are not shared between shops of the group, it's not possible to manage them at group level
5270                    if (!$shop_group->share_stock) {
5271                        $show_quantities = false;
5272                    }
5273                } // if we are in shop context
5274                elseif (Shop::isFeatureActive()) {
5275                    // if quantities are shared between shops of the group, it's not possible to manage them for a given shop
5276                    if ($shop_group->share_stock) {
5277                        $show_quantities = false;
5278                    }
5279                }
5280
5281                $data->assign('ps_stock_management', Configuration::get('PS_STOCK_MANAGEMENT'));
5282                $data->assign('has_attribute', $obj->hasAttributes());
5283                // Check if product has combination, to display the available date only for the product or for each combination
5284                if (Combination::isFeatureActive()) {
5285                    $data->assign('countAttributes', (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
5286                        (new DbQuery())
5287                        ->select('COUNT(`id_product`)')
5288                        ->from('product_attribute')
5289                        ->where('`id_product` = '.(int) $obj->id)
5290                    ));
5291                } else {
5292                    $data->assign('countAttributes', false);
5293                }
5294                // if advanced stock management is active, checks associations
5295                $advanced_stock_management_warning = false;
5296                if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT') && $obj->advanced_stock_management) {
5297                    $p_attributes = Product::getProductAttributesIds($obj->id);
5298                    $warehouses = [];
5299
5300                    if (!$p_attributes) {
5301                        $warehouses[] = Warehouse::getProductWarehouseList($obj->id, 0);
5302                    }
5303
5304                    foreach ($p_attributes as $p_attribute) {
5305                        $ws = Warehouse::getProductWarehouseList($obj->id, $p_attribute['id_product_attribute']);
5306                        if ($ws) {
5307                            $warehouses[] = $ws;
5308                        }
5309                    }
5310                    $warehouses = Tools::arrayUnique($warehouses);
5311
5312                    if (empty($warehouses)) {
5313                        $advanced_stock_management_warning = true;
5314                    }
5315                }
5316                if ($advanced_stock_management_warning) {
5317                    $this->displayWarning($this->l('If you wish to use the advanced stock management, you must:'));
5318                    $this->displayWarning('- '.$this->l('associate your products with warehouses.'));
5319                    $this->displayWarning('- '.$this->l('associate your warehouses with carriers.'));
5320                    $this->displayWarning('- '.$this->l('associate your warehouses with the appropriate shops.'));
5321                }
5322
5323                $pack_quantity = null;
5324                // if product is a pack
5325                if (Pack::isPack($obj->id)) {
5326                    $items = Pack::getItems((int) $obj->id, Configuration::get('PS_LANG_DEFAULT'));
5327
5328                    // gets an array of quantities (quantity for the product / quantity in pack)
5329                    $pack_quantities = [];
5330                    foreach ($items as $item) {
5331                        /** @var Product $item */
5332                        if (!$item->isAvailableWhenOutOfStock((int) $item->out_of_stock)) {
5333                            $pack_id_product_attribute = Product::getDefaultAttribute($item->id, 1);
5334                            $pack_quantities[] = Product::getQuantity($item->id, $pack_id_product_attribute) / ($item->pack_quantity !== 0 ? $item->pack_quantity : 1);
5335                        }
5336                    }
5337
5338                    // gets the minimum
5339                    if (count($pack_quantities)) {
5340                        $pack_quantity = $pack_quantities[0];
5341                        foreach ($pack_quantities as $value) {
5342                            if ($pack_quantity > $value) {
5343                                $pack_quantity = $value;
5344                            }
5345                        }
5346                    }
5347
5348                    if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT') && !Warehouse::getPackWarehouses((int) $obj->id)) {
5349                        $this->displayWarning($this->l('You must have a common warehouse between this pack and its product.'));
5350                    }
5351                }
5352
5353                $data->assign(
5354                    [
5355                        'attributes'              => $attributes,
5356                        'available_quantity'      => $available_quantity,
5357                        'pack_quantity'           => $pack_quantity,
5358                        'stock_management_active' => Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT'),
5359                        'product_designation'     => $product_designation,
5360                        'product'                 => $obj,
5361                        'show_quantities'         => $show_quantities,
5362                        'order_out_of_stock'      => Configuration::get('PS_ORDER_OUT_OF_STOCK'),
5363                        'pack_stock_type'         => Configuration::get('PS_PACK_STOCK_TYPE'),
5364                        'token_preferences'       => Tools::getAdminTokenLite('AdminPPreferences'),
5365                        'token'                   => $this->token,
5366                        'languages'               => $this->_languages,
5367                        'id_lang'                 => $this->context->language->id,
5368                    ]
5369                );
5370            } else {
5371                $this->displayWarning($this->l('You must save the product in this shop before managing quantities.'));
5372            }
5373        } else {
5374            $this->displayWarning($this->l('You must save this product before managing quantities.'));
5375        }
5376
5377        $this->tpl_form_vars['custom_form'] = $data->fetch();
5378    }
5379
5380    /**
5381     * @param Product $obj
5382     *
5383     * @throws Exception
5384     * @throws SmartyException
5385     *
5386     * @since 1.0.0
5387     */
5388    public function initFormSuppliers($obj)
5389    {
5390        $data = $this->createTemplate($this->tpl_form);
5391
5392        if ($obj->id) {
5393            if ($this->product_exists_in_shop) {
5394                // Get all id_product_attribute
5395                $attributes = $obj->getAttributesResume($this->context->language->id);
5396                if (empty($attributes)) {
5397                    $attributes[] = [
5398                        'id_product'            => $obj->id,
5399                        'id_product_attribute'  => 0,
5400                        'attribute_designation' => '',
5401                    ];
5402                }
5403
5404                $product_designation = [];
5405
5406                foreach ($attributes as $attribute) {
5407                    $product_designation[$attribute['id_product_attribute']] = rtrim(
5408                        $obj->name[$this->context->language->id].' - '.$attribute['attribute_designation'],
5409                        ' - '
5410                    );
5411                }
5412
5413                // Get all available suppliers
5414                $suppliers = Supplier::getSuppliers();
5415
5416                // Get already associated suppliers
5417                $associated_suppliers = ProductSupplier::getSupplierCollection($obj->id);
5418
5419                // Get already associated suppliers and force to retreive product declinaisons
5420                $product_supplier_collection = ProductSupplier::getSupplierCollection($obj->id, false);
5421
5422                $default_supplier = 0;
5423
5424                foreach ($suppliers as &$supplier) {
5425                    $supplier['is_selected'] = false;
5426                    $supplier['is_default'] = false;
5427
5428                    foreach ($associated_suppliers as $associated_supplier) {
5429                        /** @var ProductSupplier $associated_supplier */
5430                        if ($associated_supplier->id_supplier == $supplier['id_supplier']) {
5431                            $associated_supplier->name = $supplier['name'];
5432                            $supplier['is_selected'] = true;
5433
5434                            if ($obj->id_supplier == $supplier['id_supplier']) {
5435                                $supplier['is_default'] = true;
5436                                $default_supplier = $supplier['id_supplier'];
5437                            }
5438                        }
5439                    }
5440                }
5441
5442                $data->assign(
5443                    [
5444                        'attributes'                      => $attributes,
5445                        'suppliers'                       => $suppliers,
5446                        'default_supplier'                => $default_supplier,
5447                        'associated_suppliers'            => $associated_suppliers,
5448                        'associated_suppliers_collection' => $product_supplier_collection,
5449                        'product_designation'             => $product_designation,
5450                        'currencies'                      => Currency::getCurrencies(false, true, true),
5451                        'product'                         => $obj,
5452                        'link'                            => $this->context->link,
5453                        'token'                           => $this->token,
5454                        'id_default_currency'             => Configuration::get('PS_CURRENCY_DEFAULT'),
5455                    ]
5456                );
5457            } else {
5458                $this->displayWarning($this->l('You must save the product in this shop before managing suppliers.'));
5459            }
5460        } else {
5461            $this->displayWarning($this->l('You must save this product before managing suppliers.'));
5462        }
5463
5464        $this->tpl_form_vars['custom_form'] = $data->fetch();
5465    }
5466
5467    /**
5468     * @param Product $obj
5469     *
5470     * @throws Exception
5471     * @throws SmartyException
5472     *
5473     * @since 1.0.0
5474     */
5475    public function initFormWarehouses($obj)
5476    {
5477        $data = $this->createTemplate($this->tpl_form);
5478
5479        if ($obj->id) {
5480            if ($this->product_exists_in_shop) {
5481                // Get all id_product_attribute
5482                $attributes = $obj->getAttributesResume($this->context->language->id);
5483                if (empty($attributes)) {
5484                    $attributes[] = [
5485                        'id_product'            => $obj->id,
5486                        'id_product_attribute'  => 0,
5487                        'attribute_designation' => '',
5488                    ];
5489                }
5490
5491                $product_designation = [];
5492
5493                foreach ($attributes as $attribute) {
5494                    $product_designation[$attribute['id_product_attribute']] = rtrim(
5495                        $obj->name[$this->context->language->id].' - '.$attribute['attribute_designation'],
5496                        ' - '
5497                    );
5498                }
5499
5500                // Get all available warehouses
5501                $warehouses = Warehouse::getWarehouses(true);
5502
5503                // Get already associated warehouses
5504                $associated_warehouses_collection = WarehouseProductLocation::getCollection($obj->id);
5505
5506                $data->assign(
5507                    [
5508                        'attributes'            => $attributes,
5509                        'warehouses'            => $warehouses,
5510                        'associated_warehouses' => $associated_warehouses_collection,
5511                        'product_designation'   => $product_designation,
5512                        'product'               => $obj,
5513                        'link'                  => $this->context->link,
5514                        'token'                 => $this->token,
5515                    ]
5516                );
5517            } else {
5518                $this->displayWarning($this->l('You must save the product in this shop before managing warehouses.'));
5519            }
5520        } else {
5521            $this->displayWarning($this->l('You must save this product before managing warehouses.'));
5522        }
5523
5524        $this->tpl_form_vars['custom_form'] = $data->fetch();
5525    }
5526
5527    /**
5528     * @param Product $obj
5529     *
5530     * @throws Exception
5531     * @throws SmartyException
5532     *
5533     * @since 1.0.0
5534     */
5535    public function initFormFeatures($obj)
5536    {
5537        if (!$this->default_form_language) {
5538            $this->getLanguages();
5539        }
5540
5541        $data = $this->createTemplate($this->tpl_form);
5542        $data->assign('default_form_language', $this->default_form_language);
5543        $data->assign('languages', $this->_languages);
5544
5545        if (!Feature::isFeatureActive()) {
5546            $this->displayWarning($this->l('This feature has been disabled. ').' <a href="index.php?tab=AdminPerformance&token='.Tools::getAdminTokenLite('AdminPerformance').'#featuresDetachables">'.$this->l('Performances').'</a>');
5547        } else {
5548            if ($obj->id) {
5549                if ($this->product_exists_in_shop) {
5550                    $features = Feature::getFeatures($this->context->language->id, (Shop::isFeatureActive() && Shop::getContext() == Shop::CONTEXT_SHOP));
5551
5552                    foreach ($features as $k => $tab_features) {
5553                        $features[$k]['current_item'] = false;
5554                        $features[$k]['val'] = [];
5555
5556                        $custom = true;
5557                        foreach ($obj->getFeatures() as $tab_products) {
5558                            if ($tab_products['id_feature'] == $tab_features['id_feature']) {
5559                                $features[$k]['current_item'] = $tab_products['id_feature_value'];
5560                            }
5561                        }
5562
5563                        $features[$k]['featureValues'] = FeatureValue::getFeatureValuesWithLang($this->context->language->id, (int) $tab_features['id_feature']);
5564                        if (count($features[$k]['featureValues'])) {
5565                            foreach ($features[$k]['featureValues'] as $value) {
5566                                if ($features[$k]['current_item'] == $value['id_feature_value']) {
5567                                    $custom = false;
5568                                }
5569                            }
5570                        }
5571
5572                        if ($custom) {
5573                            $feature_values_lang = FeatureValue::getFeatureValueLang($features[$k]['current_item']);
5574                            foreach ($feature_values_lang as $feature_value) {
5575                                $features[$k]['val'][$feature_value['id_lang']] = $feature_value;
5576                            }
5577                        }
5578                    }
5579
5580                    $data->assign('available_features', $features);
5581                    $data->assign('product', $obj);
5582                    $data->assign('link', $this->context->link);
5583                    $data->assign('default_form_language', $this->default_form_language);
5584                } else {
5585                    $this->displayWarning($this->l('You must save the product in this shop before adding features.'));
5586                }
5587            } else {
5588                $this->displayWarning($this->l('You must save this product before adding features.'));
5589            }
5590        }
5591        $this->tpl_form_vars['custom_form'] = $data->fetch();
5592    }
5593
5594    /**
5595     * Ajax process product quantity
5596     *
5597     * @return string|void
5598     *
5599     * @since 1.0.0
5600     */
5601    public function ajaxProcessProductQuantity()
5602    {
5603        if ($this->tabAccess['edit'] === '0') {
5604            $this->ajaxDie(json_encode(['error' => $this->l('You do not have the right permission')]));
5605        }
5606        if (!Tools::getValue('actionQty')) {
5607            $this->ajaxDie(json_encode(['error' => $this->l('Undefined action')]));
5608        }
5609
5610        $product = new Product((int) Tools::getValue('id_product'), true);
5611        switch (Tools::getValue('actionQty')) {
5612            case 'depends_on_stock':
5613                if (Tools::getValue('value') === false) {
5614                    $this->ajaxDie(json_encode(['error' => $this->l('Undefined value')]));
5615                }
5616                if ((int) Tools::getValue('value') != 0 && (int) Tools::getValue('value') != 1) {
5617                    $this->ajaxDie(json_encode(['error' => $this->l('Incorrect value')]));
5618                }
5619                if (!$product->advanced_stock_management && (int) Tools::getValue('value') == 1) {
5620                    $this->ajaxDie(json_encode(['error' => $this->l('Not possible if advanced stock management is disabled. ')]));
5621                }
5622                if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT') && (int) Tools::getValue('value') == 1 && (Pack::isPack($product->id) && !Pack::allUsesAdvancedStockManagement($product->id)
5623                        && ($product->pack_stock_type == 2 || $product->pack_stock_type == 1 ||
5624                            ($product->pack_stock_type == 3 && (Configuration::get('PS_PACK_STOCK_TYPE') == 1 || Configuration::get('PS_PACK_STOCK_TYPE') == 2))))
5625                ) {
5626                    $this->ajaxDie(
5627                        json_encode(
5628                            [
5629                                'error' => $this->l('You cannot use advanced stock management for this pack because').'<br />'.
5630                                    $this->l('- advanced stock management is not enabled for these products').'<br />'.
5631                                    $this->l('- you have chosen to decrement products quantities.'),
5632                            ]
5633                        )
5634                    );
5635                }
5636
5637                StockAvailable::setProductDependsOnStock($product->id, (int) Tools::getValue('value'));
5638                break;
5639
5640            case 'pack_stock_type':
5641                $value = Tools::getValue('value');
5642                if ($value === false) {
5643                    $this->ajaxDie(json_encode(['error' => $this->l('Undefined value')]));
5644                }
5645                if ((int) $value != 0 && (int) $value != 1
5646                    && (int) $value != 2 && (int) $value != 3
5647                ) {
5648                    $this->ajaxDie(json_encode(['error' => $this->l('Incorrect value')]));
5649                }
5650                if ($product->depends_on_stock && !Pack::allUsesAdvancedStockManagement($product->id) && ((int) $value == 1
5651                        || (int) $value == 2 || ((int) $value == 3 && (Configuration::get('PS_PACK_STOCK_TYPE') == 1 || Configuration::get('PS_PACK_STOCK_TYPE') == 2)))
5652                ) {
5653                    $this->ajaxDie(
5654                        json_encode(
5655                            [
5656                                'error' => $this->l('You cannot use this stock management option because:').'<br />'.
5657                                    $this->l('- advanced stock management is not enabled for these products').'<br />'.
5658                                    $this->l('- advanced stock management is enabled for the pack'),
5659                            ]
5660                        )
5661                    );
5662                }
5663
5664                Product::setPackStockType($product->id, $value);
5665                break;
5666
5667            case 'out_of_stock':
5668                if (Tools::getValue('value') === false) {
5669                    $this->ajaxDie(json_encode(['error' => $this->l('Undefined value')]));
5670                }
5671                if (!in_array((int) Tools::getValue('value'), [0, 1, 2])) {
5672                    $this->ajaxDie(json_encode(['error' => $this->l('Incorrect value')]));
5673                }
5674
5675                StockAvailable::setProductOutOfStock($product->id, (int) Tools::getValue('value'));
5676                break;
5677
5678            case 'set_qty':
5679                if (Tools::getValue('value') === false || (!is_numeric(trim(Tools::getValue('value'))))) {
5680                    $this->ajaxDie(json_encode(['error' => $this->l('Undefined value')]));
5681                }
5682                if (Tools::getValue('id_product_attribute') === false) {
5683                    $this->ajaxDie(json_encode(['error' => $this->l('Undefined id product attribute')]));
5684                }
5685
5686                StockAvailable::setQuantity($product->id, (int) Tools::getValue('id_product_attribute'), (int) Tools::getValue('value'));
5687                Hook::exec('actionProductUpdate', ['id_product' => (int) $product->id, 'product' => $product]);
5688
5689                // Catch potential echo from modules
5690                $error = ob_get_contents();
5691                if (!empty($error)) {
5692                    ob_end_clean();
5693                    $this->ajaxDie(json_encode(['error' => $error]));
5694                }
5695                break;
5696            case 'advanced_stock_management' :
5697                if (Tools::getValue('value') === false) {
5698                    $this->ajaxDie(json_encode(['error' => $this->l('Undefined value')]));
5699                }
5700                if ((int) Tools::getValue('value') != 1 && (int) Tools::getValue('value') != 0) {
5701                    $this->ajaxDie(json_encode(['error' => $this->l('Incorrect value')]));
5702                }
5703                if (!Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT') && (int) Tools::getValue('value') == 1) {
5704                    $this->ajaxDie(json_encode(['error' => $this->l('Not possible if advanced stock management is disabled. ')]));
5705                }
5706
5707                $product->setAdvancedStockManagement((int) Tools::getValue('value'));
5708                if (StockAvailable::dependsOnStock($product->id) == 1 && (int) Tools::getValue('value') == 0) {
5709                    StockAvailable::setProductDependsOnStock($product->id, 0);
5710                }
5711                break;
5712
5713        }
5714        $this->ajaxDie(json_encode(['error' => false]));
5715    }
5716
5717    /**
5718     * AdminProducts display hook
5719     *
5720     * @param $obj
5721     *
5722     * @throws PrestaShopException
5723     *
5724     * @since 1.0.0
5725     */
5726    public function initFormModules($obj)
5727    {
5728        $idModule = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue(
5729            (new DbQuery())
5730            ->select('`id_module`')
5731            ->from('module')
5732            ->where('`name` = \''.pSQL($this->tab_display_module).'\'')
5733        );
5734        $this->tpl_form_vars['custom_form'] = Hook::exec('displayAdminProductsExtra', [], (int) $idModule);
5735    }
5736
5737    public function getL($key)
5738    {
5739        $trad = [
5740            'Default category:'                                                 => $this->l('Default category'),
5741            'Catalog:'                                                          => $this->l('Catalog'),
5742            'Consider changing the default category.'                           => $this->l('Consider changing the default category.'),
5743            'ID'                                                                => $this->l('ID'),
5744            'Name'                                                              => $this->l('Name'),
5745            'Mark all checkbox(es) of categories in which product is to appear' => $this->l('Mark the checkbox of each categories in which this product will appear.'),
5746        ];
5747
5748        return $trad[$key];
5749    }
5750
5751    /**
5752     * Ajax process product name check
5753     *
5754     * @since 1.0.0
5755     */
5756    public function ajaxProcessCheckProductName()
5757    {
5758        if ($this->tabAccess['view'] === '1') {
5759            $search = Tools::getValue('q');
5760            $id_lang = Tools::getValue('id_lang');
5761            $limit = Tools::getValue('limit');
5762            if ($this->context->shop->getContext() != Shop::CONTEXT_SHOP) {
5763                $result = false;
5764            } else {
5765                $result = Db::getInstance()->executeS(
5766                    '
5767					SELECT DISTINCT pl.`name`, p.`id_product`, pl.`id_shop`
5768					FROM `'._DB_PREFIX_.'product` p
5769					LEFT JOIN `'._DB_PREFIX_.'product_shop` ps ON (ps.id_product = p.id_product AND ps.id_shop ='.(int) $this->context->shop->id.')
5770					LEFT JOIN `'._DB_PREFIX_.'product_lang` pl
5771						ON (pl.`id_product` = p.`id_product` AND pl.`id_lang` = '.(int) $id_lang.')
5772					WHERE pl.`name` LIKE "%'.pSQL($search).'%" AND ps.id_product IS NULL
5773					GROUP BY pl.`id_product`
5774					LIMIT '.(int) $limit
5775                );
5776            }
5777            $this->ajaxDie(json_encode($result));
5778        }
5779    }
5780
5781    /**
5782     * Ajax process update positions
5783     *
5784     * @since 1.0.0
5785     */
5786    public function ajaxProcessUpdatePositions()
5787    {
5788        if ($this->tabAccess['edit'] === '1') {
5789            $way = (int) (Tools::getValue('way'));
5790            $id_product = (int) Tools::getValue('id_product');
5791            $id_category = (int) Tools::getValue('id_category');
5792            $positions = Tools::getValue('product');
5793            $page = (int) Tools::getValue('page');
5794            $selected_pagination = (int) Tools::getValue('selected_pagination');
5795
5796            if (is_array($positions)) {
5797                foreach ($positions as $position => $value) {
5798                    $pos = explode('_', $value);
5799
5800                    if ((isset($pos[1]) && isset($pos[2])) && ($pos[1] == $id_category && (int) $pos[2] === $id_product)) {
5801                        if ($page > 1) {
5802                            $position = $position + (($page - 1) * $selected_pagination);
5803                        }
5804
5805                        if ($product = new Product((int) $pos[2])) {
5806                            if (isset($position) && $product->updatePosition($way, $position)) {
5807                                $category = new Category((int) $id_category);
5808                                if (Validate::isLoadedObject($category)) {
5809                                    hook::Exec('categoryUpdate', ['category' => $category]);
5810                                }
5811                                echo 'ok position '.(int) $position.' for product '.(int) $pos[2]."\r\n";
5812                            } else {
5813                                echo '{"hasError" : true, "errors" : "Can not update product '.(int) $id_product.' to position '.(int) $position.' "}';
5814                            }
5815                        } else {
5816                            echo '{"hasError" : true, "errors" : "This product ('.(int) $id_product.') can t be loaded"}';
5817                        }
5818
5819                        break;
5820                    }
5821                }
5822            }
5823        }
5824    }
5825
5826    /**
5827     * Ajax process publish product
5828     *
5829     * @since 1.0.0
5830     */
5831    public function ajaxProcessPublishProduct()
5832    {
5833        if ($this->tabAccess['edit'] === '1') {
5834            if ($id_product = (int) Tools::getValue('id_product')) {
5835                $bo_product_url = dirname($_SERVER['PHP_SELF']).'/index.php?tab=AdminProducts&id_product='.$id_product.'&updateproduct&token='.$this->token;
5836
5837                if (Tools::getValue('redirect')) {
5838                    die($bo_product_url);
5839                }
5840
5841                $product = new Product((int) $id_product);
5842                if (!Validate::isLoadedObject($product)) {
5843                    die('error: invalid id');
5844                }
5845
5846                $product->active = 1;
5847
5848                if ($product->save()) {
5849                    die($bo_product_url);
5850                } else {
5851                    die('error: saving');
5852                }
5853            }
5854        }
5855    }
5856
5857    /**
5858     * Display preview link
5859     *
5860     * @param null $token
5861     * @param      $id
5862     * @param null $name
5863     *
5864     * @return string
5865     *
5866     * @since 1.0.0
5867     */
5868    public function displayPreviewLink($token = null, $id, $name = null)
5869    {
5870        $tpl = $this->createTemplate('helpers/list/list_action_preview.tpl');
5871        if (!array_key_exists('Bad SQL query', static::$cache_lang)) {
5872            static::$cache_lang['Preview'] = $this->l('Preview', 'Helper');
5873        }
5874
5875        $tpl->assign(
5876            [
5877                'href'   => $this->getPreviewUrl(new Product((int) $id)),
5878                'action' => static::$cache_lang['Preview'],
5879            ]
5880        );
5881
5882        return $tpl->fetch();
5883    }
5884
5885    protected function processBulkDelete()
5886    {
5887        if ($this->tabAccess['delete'] === '1') {
5888            if (is_array($this->boxes) && !empty($this->boxes)) {
5889                $object = new $this->className();
5890
5891                if (isset($object->noZeroObject) &&
5892                    // Check if all object will be deleted
5893                    (count(call_user_func([$this->className, $object->noZeroObject])) <= 1 || count($_POST[$this->table.'Box']) == count(call_user_func([$this->className, $object->noZeroObject])))
5894                ) {
5895                    $this->errors[] = Tools::displayError('You need at least one object.').' <b>'.$this->table.'</b><br />'.Tools::displayError('You cannot delete all of the items.');
5896                } else {
5897                    $success = 1;
5898                    $products = Tools::getValue($this->table.'Box');
5899                    if (is_array($products) && ($count = count($products))) {
5900                        // Deleting products can be quite long on a cheap server. Let's say 1.5 seconds by product (I've seen it!).
5901                        if (intval(ini_get('max_execution_time')) < round($count * 1.5)) {
5902                            ini_set('max_execution_time', round($count * 1.5));
5903                        }
5904
5905                        if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT')) {
5906                            $stockManager = StockManagerFactory::getManager();
5907                        }
5908
5909                        foreach ($products as $id_product) {
5910                            $product = new Product((int) $id_product);
5911                            /*
5912                             * @since 1.5.0
5913                             * It is NOT possible to delete a product if there are currently:
5914                             * - physical stock for this product
5915                             * - supply order(s) for this product
5916                             */
5917                            if (Configuration::get('PS_ADVANCED_STOCK_MANAGEMENT') && $product->advanced_stock_management) {
5918                                $physical_quantity = $stockManager->getProductPhysicalQuantities($product->id, 0);
5919                                $real_quantity = $stockManager->getProductRealQuantities($product->id, 0);
5920                                if ($physical_quantity > 0 || $real_quantity > $physical_quantity) {
5921                                    $this->errors[] = sprintf(Tools::displayError('You cannot delete the product #%d because there is physical stock left.'), $product->id);
5922                                }
5923                            }
5924                            if (!count($this->errors)) {
5925                                if ($product->delete()) {
5926                                    Logger::addLog(sprintf($this->l('%s deletion', 'AdminTab', false, false), $this->className), 1, null, $this->className, (int) $product->id, true, (int) $this->context->employee->id);
5927                                } else {
5928                                    $success = false;
5929                                }
5930                            } else {
5931                                $success = 0;
5932                            }
5933                        }
5934                    }
5935
5936                    if ($success) {
5937                        $id_category = (int) Tools::getValue('id_category');
5938                        $category_url = empty($id_category) ? '' : '&id_category='.(int) $id_category;
5939                        $this->redirect_after = static::$currentIndex.'&conf=2&token='.$this->token.$category_url;
5940                    } else {
5941                        $this->errors[] = Tools::displayError('An error occurred while deleting this selection.');
5942                    }
5943                }
5944            } else {
5945                $this->errors[] = Tools::displayError('You must select at least one element to delete.');
5946            }
5947        } else {
5948            $this->errors[] = Tools::displayError('You do not have permission to delete this.');
5949        }
5950    }
5951
5952    /**
5953     * Update shop association
5954     *
5955     * @param int $idObject
5956     *
5957     * @since 1.0.0
5958     *
5959     * @return void
5960     */
5961    protected function updateAssoShop($idObject)
5962    {
5963        return;
5964    }
5965
5966    /**
5967     * Get final price
5968     *
5969     * @param $specificPrice
5970     * @param $productPrice
5971     * @param $taxRate
5972     *
5973     * @return float
5974     *
5975     * @since 1.0.0
5976     * @deprecated 1.1.0 Nowhere in use. Remove entirely for 1.2.0.
5977     */
5978    protected function _getFinalPrice($specificPrice, $productPrice, $taxRate)
5979    {
5980        Tools::displayAsDeprecated('Use Product->getPrice() directly.');
5981
5982        $decimals = 0;
5983        if ($this->context->currency->decimals) {
5984            $decimals = Configuration::get('PS_PRICE_DISPLAY_PRECISION');
5985        }
5986
5987        return $this->object->getPrice(
5988            false,
5989            $specificPrice['id_product_attribute'],
5990            $decimals
5991        );
5992    }
5993
5994    /**
5995     * Display product unavailable warning
5996     *
5997     * @since 1.0.0
5998     * @deprecated 1.1.0 Nowhere in use. Remove entirely for 1.2.0.
5999     */
6000    protected function _displayUnavailableProductWarning()
6001    {
6002        Tools::displayAsDeprecated();
6003
6004        $content = '<div class="alert">
6005			<span>'.$this->l('Your product will be saved as a draft.').'</span>
6006				<a href="#" class="btn btn-default pull-right" onclick="submitAddProductAndPreview()" ><i class="icon-external-link-sign"></i> '.$this->l('Save and preview').'</a>
6007				<input type="hidden" name="fakeSubmitAddProductAndPreview" id="fakeSubmitAddProductAndPreview" />
6008			</div>';
6009        $this->tpl_form_vars['warning_unavailable_product'] = $content;
6010    }
6011}
6012