diff --git a/controllers/.htaccess b/controllers/.htaccess new file mode 100644 index 00000000..3de9e400 --- /dev/null +++ b/controllers/.htaccess @@ -0,0 +1,10 @@ +# Apache 2.2 + + Order deny,allow + Deny from all + + +# Apache 2.4 + + Require all denied + diff --git a/controllers/admin/AdminAccessController.php b/controllers/admin/AdminAccessController.php new file mode 100644 index 00000000..3bb1226b --- /dev/null +++ b/controllers/admin/AdminAccessController.php @@ -0,0 +1,235 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +/** + * @property Profile $object + */ +class AdminAccessControllerCore extends AdminController +{ + /** @var array : Black list of id_tab that do not have access */ + public $accesses_black_list = []; + + public function __construct() + { + $this->bootstrap = true; + $this->show_toolbar = false; + $this->table = 'access'; + $this->className = 'Profile'; + $this->multishop_context = Shop::CONTEXT_ALL; + $this->lang = false; + $this->context = Context::getContext(); + + // Blacklist AdminLogin + $this->accesses_black_list[] = Tab::getIdFromClassName('AdminLogin'); + + parent::__construct(); + } + + /** + * AdminController::renderForm() override. + * + * @see AdminController::renderForm() + */ + public function renderForm() + { + $current_profile = (int) $this->getCurrentProfileId(); + $profiles = Profile::getProfiles($this->context->language->id); + $tabs = Tab::getTabs($this->context->language->id); + + $accesses = []; + foreach ($profiles as $profile) { + $accesses[$profile['id_profile']] = Profile::getProfileAccesses($profile['id_profile']); + } + + // Deleted id_tab that do not have access + foreach ($tabs as $key => $tab) { + // Don't allow permissions for unnamed tabs (ie. AdminLogin) + if (empty($tab['name'])) { + unset($tabs[$key]); + } + + foreach ($this->accesses_black_list as $id_tab) { + if ($tab['id_tab'] == (int) $id_tab) { + unset($tabs[$key]); + } + } + } + + $modules = []; + foreach ($profiles as $profile) { + $modules[$profile['id_profile']] = Module::getModulesAccessesByIdProfile($profile['id_profile']); + uasort($modules[$profile['id_profile']], [$this, 'sortModuleByName']); + } + + $this->fields_form = ['']; + $this->tpl_form_vars = [ + 'profiles' => $profiles, + 'accesses' => $accesses, + 'id_tab_parentmodule' => (int) Tab::getIdFromClassName('AdminParentModules'), + 'id_tab_module' => (int) Tab::getIdFromClassName('AdminModules'), + 'tabs' => $this->displayTabs($tabs), + 'current_profile' => (int) $current_profile, + 'admin_profile' => (int) _PS_ADMIN_PROFILE_, + 'access_edit' => $this->access('edit'), + 'perms' => ['view', 'add', 'edit', 'delete'], + 'id_perms' => ['view' => 0, 'add' => 1, 'edit' => 2, 'delete' => 3, 'all' => 4], + 'modules' => $modules, + 'link' => $this->context->link, + 'employee_profile_id' => (int) $this->context->employee->id_profile, + ]; + + return parent::renderForm(); + } + + /** + * AdminController::initContent() override. + * + * @see AdminController::initContent() + */ + public function initContent() + { + $this->display = 'edit'; + + if (!$this->loadObject(true)) { + return; + } + + $this->content .= $this->renderForm(); + + $this->context->smarty->assign([ + 'content' => $this->content, + ]); + } + + public function initToolbarTitle() + { + $this->toolbar_title = array_unique($this->breadcrumbs); + } + + public function initPageHeaderToolbar() + { + parent::initPageHeaderToolbar(); + unset($this->page_header_toolbar_btn['cancel']); + } + + public function ajaxProcessUpdateAccess() + { + if (_PS_MODE_DEMO_) { + throw new PrestaShopException($this->trans('This functionality has been disabled.', [], 'Admin.Notifications.Error')); + } + if ($this->access('edit') != '1') { + throw new PrestaShopException($this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error')); + } + + if (Tools::isSubmit('submitAddAccess')) { + $access = new Access(); + $perm = Tools::getValue('perm'); + if (!in_array($perm, ['view', 'add', 'edit', 'delete', 'all'])) { + throw new PrestaShopException('permission does not exist'); + } + + $enabled = (int) Tools::getValue('enabled'); + $id_tab = (int) Tools::getValue('id_tab'); + $id_profile = (int) Tools::getValue('id_profile'); + $addFromParent = (int) Tools::getValue('addFromParent'); + + die($access->updateLgcAccess((int) $id_profile, $id_tab, $perm, $enabled, $addFromParent)); + } + } + + public function ajaxProcessUpdateModuleAccess() + { + if (_PS_MODE_DEMO_) { + throw new PrestaShopException($this->trans('This functionality has been disabled.', [], 'Admin.Notifications.Error')); + } + if ($this->access('edit') != '1') { + throw new PrestaShopException($this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error')); + } + + if (Tools::isSubmit('changeModuleAccess')) { + $access = new Access(); + $perm = Tools::getValue('perm'); + $enabled = (int) Tools::getValue('enabled'); + $id_module = (int) Tools::getValue('id_module'); + $id_profile = (int) Tools::getValue('id_profile'); + + if (!in_array($perm, ['view', 'configure', 'uninstall'])) { + throw new PrestaShopException('permission does not exist'); + } + + die($access->updateLgcModuleAccess((int) $id_profile, $id_module, $perm, $enabled)); + } + } + + /** + * Get the current profile id. + * + * @return int the $_GET['profile'] if valid, else 1 (the first profile id) + */ + public function getCurrentProfileId() + { + return (isset($_GET['id_profile']) && !empty($_GET['id_profile']) && is_numeric($_GET['id_profile'])) ? (int) $_GET['id_profile'] : 1; + } + + /** + * @param array $a module data + * @param array $b module data + * + * @return int + */ + protected function sortModuleByName($a, $b) + { + $moduleAName = isset($a['name']) ? $a['name'] : null; + $moduleBName = isset($b['name']) ? $b['name'] : null; + + return strnatcmp($moduleAName, $moduleBName); + } + + /** + * return human readable Tabs hierarchy for display. + */ + protected function displayTabs(array $tabs) + { + $tabsTree = $this->getChildrenTab($tabs); + + return $tabsTree; + } + + protected function getChildrenTab(array &$tabs, $id_parent = 0) + { + $children = []; + foreach ($tabs as &$tab) { + $id = $tab['id_tab']; + + if ($tab['id_parent'] == $id_parent) { + $children[$id] = $tab; + $children[$id]['children'] = $this->getChildrenTab($tabs, $id); + } + } + + return $children; + } +} diff --git a/controllers/admin/AdminAddressesController.php b/controllers/admin/AdminAddressesController.php new file mode 100644 index 00000000..28872bca --- /dev/null +++ b/controllers/admin/AdminAddressesController.php @@ -0,0 +1,571 @@ + + * @copyright 2007-2019 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property Address $object + */ +class AdminAddressesControllerCore extends AdminController +{ + /** @var array countries list */ + protected $countries_array = array(); + + public function __construct() + { + $this->bootstrap = true; + $this->required_database = true; + $this->required_fields = array('company', 'address2', 'postcode', 'other', 'phone', 'phone_mobile', 'vat_number', 'dni'); + $this->table = 'address'; + $this->className = 'CustomerAddress'; + $this->lang = false; + $this->addressType = 'customer'; + $this->explicitSelect = true; + + parent::__construct(); + + $this->addRowAction('edit'); + $this->addRowAction('delete'); + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Notifications.Info'), + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Info'), + 'icon' => 'icon-trash', + ), + ); + + $this->allow_export = true; + + if (!Tools::getValue('realedit')) { + $this->deleted = true; + } + + $countries = Country::getCountries($this->context->language->id); + foreach ($countries as $country) { + $this->countries_array[$country['id_country']] = $country['name']; + } + + $this->fields_list = array( + 'id_address' => array( + 'title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ), + 'firstname' => array( + 'title' => $this->trans('First Name', array(), 'Admin.Global'), + 'filter_key' => 'a!firstname', + 'maxlength' => 30, + ), + 'lastname' => array( + 'title' => $this->trans('Last Name', array(), 'Admin.Global'), + 'filter_key' => 'a!lastname', + 'maxlength' => 30, + ), + 'address1' => array( + 'title' => $this->trans('Address', array(), 'Admin.Global'), + ), + 'postcode' => array( + 'title' => $this->trans('Zip/postal code', array(), 'Admin.Global'), + 'align' => 'right', + ), + 'city' => array( + 'title' => $this->trans('City', array(), 'Admin.Global'), + ), + 'country' => array( + 'title' => $this->trans('Country', array(), 'Admin.Global'), + 'type' => 'select', + 'list' => $this->countries_array, + 'filter_key' => 'cl!id_country', + ), + ); + + $this->_select = 'cl.`name` as country'; + $this->_join = ' + LEFT JOIN `' . _DB_PREFIX_ . 'country_lang` cl ON (cl.`id_country` = a.`id_country` AND cl.`id_lang` = ' . (int) $this->context->language->id . ') + LEFT JOIN `' . _DB_PREFIX_ . 'customer` c ON a.id_customer = c.id_customer + '; + $this->_where = 'AND a.id_customer != 0 ' . Shop::addSqlRestriction(Shop::SHARE_CUSTOMER, 'c'); + $this->_use_found_rows = false; + } + + public function initToolbar() + { + parent::initToolbar(); + + if (!$this->display && $this->can_import) { + $this->toolbar_btn['import'] = array( + 'href' => $this->context->link->getAdminLink('AdminImport', true, array(), array('import_type' => 'addresses')), + 'desc' => $this->trans('Import', array(), 'Admin.Actions'), + ); + } + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_address'] = array( + 'href' => $this->context->link->getAdminLink('AdminAddresses', true, array(), array('addaddress' => 1)), + 'desc' => $this->trans('Add new address', array(), 'Admin.Orderscustomers.Feature'), + 'icon' => 'process-icon-new', + ); + } + + parent::initPageHeaderToolbar(); + } + + public function renderForm() + { + $this->fields_form = array( + 'legend' => array( + 'title' => $this->trans('Addresses', array(), 'Admin.Orderscustomers.Feature'), + 'icon' => 'icon-envelope-alt', + ), + 'input' => array( + array( + 'type' => 'text_customer', + 'label' => $this->trans('Customer', array(), 'Admin.Global'), + 'name' => 'id_customer', + 'required' => false, + ), + array( + 'type' => 'text', + 'label' => $this->trans('Identification number', array(), 'Admin.Orderscustomers.Feature'), + 'name' => 'dni', + 'required' => false, + 'col' => '4', + 'hint' => $this->trans('The national ID card number of this person, or a unique tax identification number.', array(), 'Admin.Orderscustomers.Feature'), + ), + array( + 'type' => 'text', + 'label' => $this->trans('Address alias', array(), 'Admin.Orderscustomers.Feature'), + 'name' => 'alias', + 'required' => true, + 'col' => '4', + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'textarea', + 'label' => $this->trans('Other', array(), 'Admin.Global'), + 'name' => 'other', + 'required' => false, + 'cols' => 15, + 'rows' => 3, + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ), + array( + 'type' => 'hidden', + 'name' => 'id_order', + ), + array( + 'type' => 'hidden', + 'name' => 'address_type', + ), + array( + 'type' => 'hidden', + 'name' => 'back', + ), + ), + 'submit' => array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ), + ); + + $this->fields_value['address_type'] = (int) Tools::getValue('address_type', 1); + + $id_customer = (int) Tools::getValue('id_customer'); + if (!$id_customer && Validate::isLoadedObject($this->object)) { + $id_customer = $this->object->id_customer; + } + if ($id_customer) { + $customer = new Customer((int) $id_customer); + } + + $this->tpl_form_vars = array( + 'customer' => isset($customer) ? $customer : null, + 'customer_view_url' => $this->context->link->getAdminLink('AdminCustomers', true, [], [ + 'viewcustomer' => 1, + 'id_customer' => $id_customer, + ]), + 'back_url' => urldecode(Tools::getValue('back')), + ); + + // Order address fields depending on country format + $addresses_fields = $this->processAddressFormat(); + // we use delivery address + $addresses_fields = $addresses_fields['dlv_all_fields']; + + // get required field + $required_fields = AddressFormat::getFieldsRequired(); + + // Merge with field required + $addresses_fields = array_unique(array_merge($addresses_fields, $required_fields)); + + $temp_fields = array(); + + foreach ($addresses_fields as $addr_field_item) { + if ($addr_field_item == 'company') { + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('Company', array(), 'Admin.Global'), + 'name' => 'company', + 'required' => in_array('company', $required_fields), + 'col' => '4', + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' <>;=#{}', + ); + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('VAT number', array(), 'Admin.Orderscustomers.Feature'), + 'col' => '2', + 'name' => 'vat_number', + 'required' => in_array('vat_number', $required_fields), + ); + } elseif ($addr_field_item == 'lastname') { + if (isset($customer) && + !Tools::isSubmit('submit' . strtoupper($this->table)) && + Validate::isLoadedObject($customer) && + !Validate::isLoadedObject($this->object)) { + $default_value = $customer->lastname; + } else { + $default_value = ''; + } + + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('Last Name', array(), 'Admin.Global'), + 'name' => 'lastname', + 'required' => true, + 'col' => '4', + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' 0-9!<>,;?=+()@#"�{}_$%:', + 'default_value' => $default_value, + ); + } elseif ($addr_field_item == 'firstname') { + if (isset($customer) && + !Tools::isSubmit('submit' . strtoupper($this->table)) && + Validate::isLoadedObject($customer) && + !Validate::isLoadedObject($this->object)) { + $default_value = $customer->firstname; + } else { + $default_value = ''; + } + + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('First Name', array(), 'Admin.Global'), + 'name' => 'firstname', + 'required' => true, + 'col' => '4', + 'hint' => $this->trans('Invalid characters:', array(), 'Admin.Notifications.Info') . ' 0-9!<>,;?=+()@#"�{}_$%:', + 'default_value' => $default_value, + ); + } elseif ($addr_field_item == 'address1') { + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('Address', array(), 'Admin.Global'), + 'name' => 'address1', + 'col' => '6', + 'required' => true, + ); + } elseif ($addr_field_item == 'address2') { + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('Address', array(), 'Admin.Global') . ' (2)', + 'name' => 'address2', + 'col' => '6', + 'required' => in_array('address2', $required_fields), + ); + } elseif ($addr_field_item == 'postcode') { + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('Zip/postal code', array(), 'Admin.Global'), + 'name' => 'postcode', + 'col' => '2', + 'required' => true, + ); + } elseif ($addr_field_item == 'city') { + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('City', array(), 'Admin.Global'), + 'name' => 'city', + 'col' => '4', + 'required' => true, + ); + } elseif ($addr_field_item == 'country' || $addr_field_item == 'Country:name') { + $temp_fields[] = array( + 'type' => 'select', + 'label' => $this->trans('Country', array(), 'Admin.Global'), + 'name' => 'id_country', + 'required' => in_array('Country:name', $required_fields) || in_array('country', $required_fields), + 'col' => '4', + 'default_value' => (int) $this->context->country->id, + 'options' => array( + 'query' => Country::getCountries($this->context->language->id), + 'id' => 'id_country', + 'name' => 'name', + ), + ); + $temp_fields[] = array( + 'type' => 'select', + 'label' => $this->trans('State', array(), 'Admin.Global'), + 'name' => 'id_state', + 'required' => false, + 'col' => '4', + 'options' => array( + 'query' => array(), + 'id' => 'id_state', + 'name' => 'name', + ), + ); + } elseif ($addr_field_item == 'phone') { + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('Home phone', array(), 'Admin.Global'), + 'name' => 'phone', + 'required' => in_array('phone', $required_fields), + 'col' => '4', + ); + } elseif ($addr_field_item == 'phone_mobile') { + $temp_fields[] = array( + 'type' => 'text', + 'label' => $this->trans('Mobile phone', array(), 'Admin.Global'), + 'name' => 'phone_mobile', + 'required' => in_array('phone_mobile', $required_fields), + 'col' => '4', + ); + } + } + + // merge address format with the rest of the form + array_splice($this->fields_form['input'], 3, 0, $temp_fields); + + return parent::renderForm(); + } + + public function processSave() + { + if (Tools::getValue('submitFormAjax')) { + $this->redirect_after = false; + } + + // Transform e-mail in id_customer for parent processing + if (Validate::isEmail(Tools::getValue('email'))) { + $customer = new Customer(); + $customer->getByEmail(Tools::getValue('email'), null, false); + if (Validate::isLoadedObject($customer)) { + $_POST['id_customer'] = $customer->id; + } else { + $this->errors[] = $this->trans('This email address is not registered.', array(), 'Admin.Orderscustomers.Notification'); + } + } elseif ($id_customer = Tools::getValue('id_customer')) { + $customer = new Customer((int) $id_customer); + if (Validate::isLoadedObject($customer)) { + $_POST['id_customer'] = $customer->id; + } else { + $this->errors[] = $this->trans('This customer ID is not recognized.', array(), 'Admin.Orderscustomers.Notification'); + } + } else { + $this->errors[] = $this->trans('This email address is not valid. Please use an address like bob@example.com.', array(), 'Admin.Orderscustomers.Notification'); + } + if (Country::isNeedDniByCountryId(Tools::getValue('id_country')) && !Tools::getValue('dni')) { + $this->errors[] = $this->trans('The identification number is incorrect or has already been used.', array(), 'Admin.Orderscustomers.Notification'); + } + + /* If the selected country does not contain states */ + $id_state = (int) Tools::getValue('id_state'); + $id_country = (int) Tools::getValue('id_country'); + $country = new Country((int) $id_country); + if ($country && !(int) $country->contains_states && $id_state) { + $this->errors[] = $this->trans('You have selected a state for a country that does not contain states.', array(), 'Admin.Orderscustomers.Notification'); + } + + /* If the selected country contains states, then a state have to be selected */ + if ((int) $country->contains_states && !$id_state) { + $this->errors[] = $this->trans('An address located in a country containing states must have a state selected.', array(), 'Admin.Orderscustomers.Notification'); + } + + $postcode = Tools::getValue('postcode'); + /* Check zip code format */ + if ($country->zip_code_format && !$country->checkZipCode($postcode)) { + $this->errors[] = $this->trans('Your Zip/postal code is incorrect.', array(), 'Admin.Notifications.Error') . '' . $this->trans('It must be entered as follows:', array(), 'Admin.Notifications.Error') . ' ' . str_replace('C', $country->iso_code, str_replace('N', '0', str_replace('L', 'A', $country->zip_code_format))); + } elseif (empty($postcode) && $country->need_zip_code) { + $this->errors[] = $this->trans('A Zip/postal code is required.', array(), 'Admin.Notifications.Error'); + } elseif ($postcode && !Validate::isPostCode($postcode)) { + $this->errors[] = $this->trans('The Zip/postal code is invalid.', array(), 'Admin.Notifications.Error'); + } + + /* If this address come from order's edition and is the same as the other one (invoice or delivery one) + ** we delete its id_address to force the creation of a new one */ + if ((int) Tools::getValue('id_order')) { + $this->_redirect = false; + if (isset($_POST['address_type'])) { + $_POST['id_address'] = ''; + $this->id_object = null; + } + } + + // Check the requires fields which are settings in the BO + $address = new Address(); + $this->errors = array_merge($this->errors, $address->validateFieldsRequiredDatabase()); + + $return = false; + if (empty($this->errors)) { + $return = parent::processSave(); + } else { + // if we have errors, we stay on the form instead of going back to the list + $this->display = 'edit'; + } + + /* Reassignation of the order's new (invoice or delivery) address */ + $address_type = (int) Tools::getValue('address_type') == 2 ? 'invoice' : 'delivery'; + + if ($this->action == 'save' && ($id_order = (int) Tools::getValue('id_order')) && !count($this->errors) && !empty($address_type)) { + if (!Db::getInstance()->execute('UPDATE ' . _DB_PREFIX_ . 'orders SET `id_address_' . bqSQL($address_type) . '` = ' . (int) $this->object->id . ' WHERE `id_order` = ' . (int) $id_order)) { + $this->errors[] = $this->trans('An error occurred while linking this address to its order.', array(), 'Admin.Orderscustomers.Notification'); + } else { + //update order shipping cost + $order = new Order($id_order); + $order->refreshShippingCost(); + + // update cart + $cart = Cart::getCartByOrderId($id_order); + if (Validate::isLoadedObject($cart)) { + if ($address_type == 'invoice') { + $cart->id_address_invoice = (int) $this->object->id; + } else { + $cart->id_address_delivery = (int) $this->object->id; + } + $cart->update(); + } + // redirect + Tools::redirectAdmin(urldecode(Tools::getValue('back')) . '&conf=4'); + } + } + + return $return; + } + + public function processAdd() + { + if (Tools::getValue('submitFormAjax')) { + $this->redirect_after = false; + } + + return parent::processAdd(); + } + + /** + * Get Address formats used by the country where the address id retrieved from POST/GET is. + * + * @return array address formats + */ + protected function processAddressFormat() + { + $tmp_addr = new CustomerAddress((int) Tools::getValue('id_address')); + + $selected_country = ($tmp_addr && $tmp_addr->id_country) ? $tmp_addr->id_country : (int) Configuration::get('PS_COUNTRY_DEFAULT'); + $adr_fields = AddressFormat::getOrderedAddressFields($selected_country, false, true); + + $all_fields = array(); + $out = array(); + + foreach ($adr_fields as $fields_line) { + foreach (explode(' ', $fields_line) as $field_item) { + $all_fields[] = trim($field_item); + } + } + + foreach (array('inv', 'dlv') as $adr_type) { + $out[$adr_type . '_adr_fields'] = $adr_fields; + $out[$adr_type . '_all_fields'] = $all_fields; + } + + return $out; + } + + /** + * Method called when an ajax request is made. + * + * @see AdminController::postProcess() + */ + public function ajaxProcess() + { + if (Tools::isSubmit('email')) { + $email = pSQL(Tools::getValue('email')); + $customer = Customer::searchByName($email); + if (!empty($customer)) { + $customer = $customer['0']; + echo json_encode(array('infos' => pSQL($customer['firstname']) . '_' . pSQL($customer['lastname']) . '_' . pSQL($customer['company']))); + } + } + + if (Tools::isSubmit('dni_required')) { + echo json_encode(['dni_required' => Address::dniRequired((int) Tools::getValue('id_country'))]); + } + + die; + } + + /** + * Object Delete. + */ + public function processDelete() + { + if (Validate::isLoadedObject($object = $this->loadObject())) { + /** @var Address $object */ + if (!$object->isUsed()) { + $this->deleted = false; + } + } + + $res = parent::processDelete(); + + if ($back = Tools::getValue('back')) { + $this->redirect_after = urldecode($back) . '&conf=1'; + } + + return $res; + } + + /** + * Delete multiple items. + * + * @return bool true if succcess + */ + protected function processBulkDelete() + { + if (is_array($this->boxes) && !empty($this->boxes)) { + $deleted = false; + foreach ($this->boxes as $id) { + $to_delete = new Address((int) $id); + if ($to_delete->isUsed()) { + $deleted = true; + + break; + } + } + $this->deleted = $deleted; + } + + return parent::processBulkDelete(); + } +} diff --git a/controllers/admin/AdminAttachmentsController.php b/controllers/admin/AdminAttachmentsController.php new file mode 100644 index 00000000..26b5c1a5 --- /dev/null +++ b/controllers/admin/AdminAttachmentsController.php @@ -0,0 +1,264 @@ + + * @copyright 2007-2019 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property Attachment $object + */ +class AdminAttachmentsControllerCore extends AdminController +{ + public $bootstrap = true; + + protected $product_attachements = array(); + + public function __construct() + { + $this->table = 'attachment'; + $this->className = 'Attachment'; + $this->lang = true; + + $this->addRowAction('edit'); + $this->addRowAction('view'); + $this->addRowAction('delete'); + + $this->_select = 'IFNULL(virtual_product_attachment.products, 0) as products'; + $this->_join = 'LEFT JOIN (SELECT id_attachment, COUNT(*) as products FROM ' . _DB_PREFIX_ . 'product_attachment GROUP BY id_attachment) virtual_product_attachment ON a.id_attachment = virtual_product_attachment.id_attachment'; + $this->_use_found_rows = false; + + parent::__construct(); + + $this->fields_list = array( + 'id_attachment' => array( + 'title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ), + 'name' => array( + 'title' => $this->trans('Name', array(), 'Admin.Global'), + ), + 'file' => array( + 'title' => $this->trans('File', array(), 'Admin.Global'), + 'orderby' => false, + 'search' => false, + ), + 'file_size' => array( + 'title' => $this->trans('Size', array(), 'Admin.Global'), + 'callback' => 'displayHumanReadableSize', + ), + 'products' => array( + 'title' => $this->trans('Associated with', array(), 'Admin.Catalog.Feature'), + 'suffix' => $this->trans('product(s)', array(), 'Admin.Catalog.Feature'), + 'filter_key' => 'virtual_product_attachment!products', + ), + ); + + $this->bulk_actions = array( + 'delete' => array( + 'text' => $this->trans('Delete selected', array(), 'Admin.Notifications.Info'), + 'icon' => 'icon-trash', + 'confirm' => $this->trans('Delete selected items?', array(), 'Admin.Notifications.Info'), + ), + ); + } + + public function setMedia($isNewTheme = false) + { + parent::setMedia($isNewTheme); + + $this->addJs(_PS_JS_DIR_ . '/admin/attachments.js'); + Media::addJsDefL('confirm_text', $this->trans('This file is associated with the following products, do you really want to delete it?', array(), 'Admin.Catalog.Notification')); + } + + public static function displayHumanReadableSize($size) + { + return Tools::formatBytes($size); + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_attachment'] = array( + 'href' => self::$currentIndex . '&addattachment&token=' . $this->token, + 'desc' => $this->trans('Add new file', array(), 'Admin.Catalog.Feature'), + 'icon' => 'process-icon-new', + ); + } + + parent::initPageHeaderToolbar(); + } + + public function renderView() + { + if (($obj = $this->loadObject(true)) && Validate::isLoadedObject($obj)) { + $link = $this->context->link->getPageLink('attachment', true, null, 'id_attachment=' . $obj->id); + Tools::redirectLink($link); + } + + return $this->displayWarning($this->trans('File not found', array(), 'Admin.Catalog.Notification')); + } + + public function renderForm() + { + if (($obj = $this->loadObject(true)) && Validate::isLoadedObject($obj)) { + /** @var Attachment $obj */ + $link = $this->context->link->getPageLink('attachment', true, null, 'id_attachment=' . $obj->id); + + if (file_exists(_PS_DOWNLOAD_DIR_ . $obj->file)) { + $size = round(filesize(_PS_DOWNLOAD_DIR_ . $obj->file) / 1024); + } + } + + $this->fields_form = array( + 'legend' => array( + 'title' => $this->trans('Add new file', array(), 'Admin.Catalog.Feature'), + 'icon' => 'icon-paper-clip', + ), + 'input' => array( + array( + 'type' => 'text', + 'label' => $this->trans('Filename', array(), 'Admin.Global'), + 'name' => 'name', + 'required' => true, + 'lang' => true, + 'col' => 4, + ), + array( + 'type' => 'textarea', + 'label' => $this->trans('Description', array(), 'Admin.Global'), + 'name' => 'description', + 'lang' => true, + 'col' => 6, + ), + array( + 'type' => 'file', + 'file' => isset($link) ? $link : null, + 'size' => isset($size) ? $size : null, + 'label' => $this->trans('File', array(), 'Admin.Global'), + 'name' => 'file', + 'required' => true, + 'col' => 6, + ), + ), + 'submit' => array( + 'title' => $this->trans('Save', array(), 'Admin.Actions'), + ), + ); + + return parent::renderForm(); + } + + public function getList($id_lang, $order_by = null, $order_way = null, $start = 0, $limit = null, $id_lang_shop = false) + { + parent::getList((int) $id_lang, $order_by, $order_way, $start, $limit, $id_lang_shop); + + if (count($this->_list)) { + $this->product_attachements = Attachment::getProductAttached((int) $id_lang, $this->_list); + + $list_product_list = array(); + foreach ($this->_list as $list) { + $product_list = ''; + + if (isset($this->product_attachements[$list['id_attachment']])) { + foreach ($this->product_attachements[$list['id_attachment']] as $product) { + $product_list .= $product . ', '; + } + + $product_list = rtrim($product_list, ', '); + } + + $list_product_list[$list['id_attachment']] = $product_list; + } + + // Assign array in list_action_delete.tpl + $this->tpl_delete_link_vars = array( + 'product_list' => $list_product_list, + 'product_attachements' => $this->product_attachements, + ); + } + } + + public function postProcess() + { + if (_PS_MODE_DEMO_) { + $this->errors[] = $this->trans('This functionality has been disabled.', array(), 'Admin.Notifications.Error'); + + return; + } + + if (Tools::isSubmit('submitAdd' . $this->table)) { + $id = (int) Tools::getValue('id_attachment'); + if ($id && $a = new Attachment($id)) { + $_POST['file'] = $a->file; + $_POST['mime'] = $a->mime; + } + if (!count($this->errors)) { + if (isset($_FILES['file']) && is_uploaded_file($_FILES['file']['tmp_name'])) { + if ($_FILES['file']['size'] > (Configuration::get('PS_ATTACHMENT_MAXIMUM_SIZE') * 1024 * 1024)) { + $this->errors[] = $this->trans( + 'The file is too large. Maximum size allowed is: %1$d kB. The file you are trying to upload is %2$d kB.', + array( + '%1$d' => (Configuration::get('PS_ATTACHMENT_MAXIMUM_SIZE') * 1024), + '%2$d' => number_format(($_FILES['file']['size'] / 1024), 2, '.', ''), + ), + 'Admin.Notifications.Error' + ); + } else { + do { + $uniqid = sha1(microtime()); + } while (file_exists(_PS_DOWNLOAD_DIR_ . $uniqid)); + if (!move_uploaded_file($_FILES['file']['tmp_name'], _PS_DOWNLOAD_DIR_ . $uniqid)) { + $this->errors[] = $this->trans('Failed to copy the file.', array(), 'Admin.Catalog.Notification'); + } + $_POST['file_name'] = $_FILES['file']['name']; + @unlink($_FILES['file']['tmp_name']); + if (!count($this->errors) && isset($a) && file_exists(_PS_DOWNLOAD_DIR_ . $a->file)) { + unlink(_PS_DOWNLOAD_DIR_ . $a->file); + } + $_POST['file'] = $uniqid; + $_POST['mime'] = $_FILES['file']['type']; + } + } elseif (array_key_exists('file', $_FILES) && (int) $_FILES['file']['error'] === 1) { + $max_upload = (int) ini_get('upload_max_filesize'); + $max_post = (int) ini_get('post_max_size'); + $upload_mb = min($max_upload, $max_post); + $this->errors[] = $this->trans( + 'The file %file% exceeds the size allowed by the server. The limit is set to %size% MB.', + array('%file%' => '' . $_FILES['file']['name'] . ' ', '%size%' => '' . $upload_mb . ''), + 'Admin.Catalog.Notification' + ); + } elseif (!isset($a) || (isset($a) && !file_exists(_PS_DOWNLOAD_DIR_ . $a->file))) { + $this->errors[] = $this->trans('Upload error. Please check your server configurations for the maximum upload size allowed.', array(), 'Admin.Catalog.Notification'); + } + } + $this->validateRules(); + } + $return = parent::postProcess(); + if (!$return && isset($uniqid) && file_exists(_PS_DOWNLOAD_DIR_ . $uniqid)) { + unlink(_PS_DOWNLOAD_DIR_ . $uniqid); + } + + return $return; + } +} diff --git a/controllers/admin/AdminAttributeGeneratorController.php b/controllers/admin/AdminAttributeGeneratorController.php new file mode 100644 index 00000000..8ec6ac96 --- /dev/null +++ b/controllers/admin/AdminAttributeGeneratorController.php @@ -0,0 +1,273 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ +@ini_set('max_execution_time', 3600); + +/** + * @property Product $object + */ +class AdminAttributeGeneratorControllerCore extends AdminController +{ + protected $combinations = []; + + /** @var Product */ + protected $product; + + public function __construct() + { + $this->bootstrap = true; + $this->table = 'product_attribute'; + $this->className = 'Product'; + $this->multishop_context_group = false; + + parent::__construct(); + } + + public function setMedia($isNewTheme = false) + { + parent::setMedia($isNewTheme); + $this->addJS(_PS_JS_DIR_ . 'admin/attributes.js'); + } + + protected function addAttribute($attributes, $price = 0, $weight = 0) + { + foreach ($attributes as $attribute) { + $price += (float) preg_replace('/[^0-9.-]/', '', str_replace(',', '.', Tools::getValue('price_impact_' . (int) $attribute))); + $weight += (float) preg_replace('/[^0-9.]/', '', str_replace(',', '.', Tools::getValue('weight_impact_' . (int) $attribute))); + } + if ($this->product->id) { + return [ + 'id_product' => (int) $this->product->id, + 'price' => (float) $price, + 'weight' => (float) $weight, + 'ecotax' => 0, + 'quantity' => (int) Tools::getValue('quantity'), + 'reference' => pSQL($_POST['reference']), + 'default_on' => 0, + 'available_date' => '0000-00-00', + ]; + } + + return []; + } + + public static function createCombinations($list) + { + if (count($list) <= 1) { + return count($list) ? array_map(function ($v) { return [$v]; }, $list[0]) : $list; + } + $res = []; + $first = array_pop($list); + foreach ($first as $attribute) { + $tab = AdminAttributeGeneratorController::createCombinations($list); + foreach ($tab as $to_add) { + $res[] = is_array($to_add) ? array_merge($to_add, [$attribute]) : [$to_add, $attribute]; + } + } + + return $res; + } + + public function initProcess() + { + if (!defined('PS_MASS_PRODUCT_CREATION')) { + define('PS_MASS_PRODUCT_CREATION', true); + } + + if (Tools::isSubmit('generate')) { + if ($this->access('edit')) { + $this->action = 'generate'; + } else { + $this->errors[] = $this->trans('You do not have permission to add this.', [], 'Admin.Notifications.Error'); + } + } + parent::initProcess(); + } + + public function postProcess() + { + $this->product = new Product((int) Tools::getValue('id_product')); + $this->product->loadStockData(); + parent::postProcess(); + } + + public function processGenerate() + { + if (!is_array(Tools::getValue('options'))) { + $this->errors[] = $this->trans('Please select at least one attribute.', [], 'Admin.Catalog.Notification'); + } else { + $tab = array_values(Tools::getValue('options')); + if (count($tab) && Validate::isLoadedObject($this->product)) { + AdminAttributeGeneratorController::setAttributesImpacts($this->product->id, $tab); + $this->combinations = array_values(AdminAttributeGeneratorController::createCombinations($tab)); + $values = array_values(array_map([$this, 'addAttribute'], $this->combinations)); + + // @since 1.5.0 + if ($this->product->depends_on_stock == 0) { + $attributes = Product::getProductAttributesIds($this->product->id, true); + foreach ($attributes as $attribute) { + StockAvailable::removeProductFromStockAvailable($this->product->id, $attribute['id_product_attribute'], Context::getContext()->shop); + } + } + + SpecificPriceRule::disableAnyApplication(); + + $this->product->deleteProductAttributes(); + $this->product->generateMultipleCombinations($values, $this->combinations); + + // Reset cached default attribute for the product and get a new one + Product::getDefaultAttribute($this->product->id, 0, true); + Product::updateDefaultAttribute($this->product->id); + + // @since 1.5.0 + if ($this->product->depends_on_stock == 0) { + $attributes = Product::getProductAttributesIds($this->product->id, true); + $quantity = (int) Tools::getValue('quantity'); + foreach ($attributes as $attribute) { + if (Shop::getContext() == Shop::CONTEXT_ALL) { + $shops_list = Shop::getShops(); + if (is_array($shops_list)) { + foreach ($shops_list as $current_shop) { + if (isset($current_shop['id_shop']) && (int) $current_shop['id_shop'] > 0) { + StockAvailable::setQuantity($this->product->id, (int) $attribute['id_product_attribute'], $quantity, (int) $current_shop['id_shop']); + } + } + } + } else { + StockAvailable::setQuantity($this->product->id, (int) $attribute['id_product_attribute'], $quantity); + } + } + } else { + StockAvailable::synchronize($this->product->id); + } + + SpecificPriceRule::enableAnyApplication(); + SpecificPriceRule::applyAllRules([(int) $this->product->id]); + + Tools::redirectAdmin($this->context->link->getAdminLink('AdminProducts') . '&id_product=' . (int) Tools::getValue('id_product') . '&updateproduct&key_tab=Combinations&conf=4'); + } else { + $this->errors[] = $this->trans('Unable to initialize these parameters. A combination is missing or an object cannot be loaded.', [], 'Admin.Catalog.Notification'); + } + } + } + + protected static function setAttributesImpacts($id_product, $tab) + { + $attributes = []; + foreach ($tab as $group) { + foreach ($group as $attribute) { + $price = preg_replace('/[^0-9.]/', '', str_replace(',', '.', Tools::getValue('price_impact_' . (int) $attribute))); + $weight = preg_replace('/[^0-9.]/', '', str_replace(',', '.', Tools::getValue('weight_impact_' . (int) $attribute))); + $attributes[] = '(' . (int) $id_product . ', ' . (int) $attribute . ', ' . (float) $price . ', ' . (float) $weight . ')'; + } + } + + return Db::getInstance()->execute(' + INSERT INTO `' . _DB_PREFIX_ . 'attribute_impact` (`id_product`, `id_attribute`, `price`, `weight`) + VALUES ' . implode(',', $attributes) . ' + ON DUPLICATE KEY UPDATE `price` = VALUES(price), `weight` = VALUES(weight)'); + } + + public function initGroupTable() + { + $combinations_groups = $this->product->getAttributesGroups($this->context->language->id); + $attributes = []; + $impacts = Product::getAttributesImpacts($this->product->id); + foreach ($combinations_groups as &$combination) { + $target = &$attributes[$combination['id_attribute_group']][$combination['id_attribute']]; + $target = $combination; + if (isset($impacts[$combination['id_attribute']])) { + $target['price'] = $impacts[$combination['id_attribute']]['price']; + $target['weight'] = $impacts[$combination['id_attribute']]['weight']; + } + } + $this->context->smarty->assign([ + 'currency_sign' => $this->context->currency->sign, + 'weight_unit' => Configuration::get('PS_WEIGHT_UNIT'), + 'attributes' => $attributes, + ]); + } + + public function initPageHeaderToolbar() + { + parent::initPageHeaderToolbar(); + + $this->page_header_toolbar_title = $this->trans('Attributes generator', [], 'Admin.Catalog.Feature'); + $this->page_header_toolbar_btn['back'] = [ + 'href' => $this->context->link->getAdminLink('AdminProducts') . '&id_product=' . (int) Tools::getValue('id_product') . '&updateproduct&key_tab=Combinations', + 'desc' => $this->trans('Back to the product', [], 'Admin.Catalog.Feature'), + ]; + } + + public function initBreadcrumbs($tab_id = null, $tabs = null) + { + $this->display = 'generator'; + + return parent::initBreadcrumbs(); + } + + public function initContent() + { + if (!Combination::isFeatureActive()) { + $adminPerformanceUrl = $this->context->link->getAdminLink('AdminPerformance'); + + $url = '' . + $this->trans('Performance', [], 'Admin.Global') . ''; + $this->displayWarning($this->trans('This feature has been disabled. You can activate it here: %link%.', ['%link%' => $url], 'Admin.Catalog.Notification')); + + return; + } + + // Init toolbar + $this->initPageHeaderToolbar(); + $this->initGroupTable(); + + $attributes = Attribute::getAttributes(Context::getContext()->language->id, true); + $attribute_js = []; + + foreach ($attributes as $k => $attribute) { + $attribute_js[$attribute['id_attribute_group']][$attribute['id_attribute']] = $attribute['name']; + } + + $attribute_groups = AttributeGroup::getAttributesGroups($this->context->language->id); + $this->product = new Product((int) Tools::getValue('id_product')); + + $this->context->smarty->assign([ + 'tax_rates' => $this->product->getTaxesRate(), + 'generate' => isset($_POST['generate']) && !count($this->errors), + 'combinations_size' => count($this->combinations), + 'product_name' => $this->product->name[$this->context->language->id], + 'product_reference' => $this->product->reference, + 'url_generator' => self::$currentIndex . '&id_product=' . (int) Tools::getValue('id_product') . '&attributegenerator&token=' . Tools::getValue('token'), + 'attribute_groups' => $attribute_groups, + 'attribute_js' => $attribute_js, + 'toolbar_btn' => $this->toolbar_btn, + 'toolbar_scroll' => true, + 'show_page_header_toolbar' => $this->show_page_header_toolbar, + 'page_header_toolbar_title' => $this->page_header_toolbar_title, + 'page_header_toolbar_btn' => $this->page_header_toolbar_btn, + ]); + } +} diff --git a/controllers/admin/AdminAttributesGroupsController.php b/controllers/admin/AdminAttributesGroupsController.php new file mode 100644 index 00000000..78644687 --- /dev/null +++ b/controllers/admin/AdminAttributesGroupsController.php @@ -0,0 +1,974 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +/** + * @property AttributeGroup $object + */ +class AdminAttributesGroupsControllerCore extends AdminController +{ + public $bootstrap = true; + protected $id_attribute; + protected $position_identifier = 'id_attribute_group'; + protected $attribute_name; + + public function __construct() + { + $this->bootstrap = true; + $this->table = 'attribute_group'; + $this->list_id = 'attribute_group'; + $this->identifier = 'id_attribute_group'; + $this->className = 'AttributeGroup'; + $this->lang = true; + $this->_defaultOrderBy = 'position'; + + parent::__construct(); + + $this->fields_list = [ + 'id_attribute_group' => [ + 'title' => $this->trans('ID', [], 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ], + 'name' => [ + 'title' => $this->trans('Name', [], 'Admin.Global'), + 'filter_key' => 'b!name', + 'align' => 'left', + ], + 'count_values' => [ + 'title' => $this->trans('Values', [], 'Admin.Catalog.Feature'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + 'orderby' => false, + 'search' => false, + ], + 'position' => [ + 'title' => $this->trans('Position', [], 'Admin.Global'), + 'filter_key' => 'a!position', + 'position' => 'position', + 'align' => 'center', + 'class' => 'fixed-width-xs', + ], + ]; + + $this->bulk_actions = [ + 'delete' => [ + 'text' => $this->trans('Delete selected', [], 'Admin.Notifications.Info'), + 'icon' => 'icon-trash', + 'confirm' => $this->trans('Delete selected items?', [], 'Admin.Notifications.Info'), + ], + ]; + $this->fieldImageSettings = ['name' => 'texture', 'dir' => 'co']; + + $this->image_dir = 'co'; + } + + /** + * AdminController::renderList() override. + * + * @see AdminController::renderList() + */ + public function renderList() + { + $this->addRowAction('view'); + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + return parent::renderList(); + } + + public function renderView() + { + if (($id = (int) Tools::getValue('id_attribute_group'))) { + $this->table = 'attribute'; + $this->className = 'Attribute'; + $this->identifier = 'id_attribute'; + $this->position_identifier = 'id_attribute'; + $this->position_group_identifier = 'id_attribute_group'; + $this->list_id = 'attribute_values'; + $this->lang = true; + + $this->context->smarty->assign([ + 'current' => self::$currentIndex . '&id_attribute_group=' . (int) $id . '&viewattribute_group', + ]); + + if (!Validate::isLoadedObject($obj = new AttributeGroup((int) $id))) { + $this->errors[] = $this->trans('An error occurred while updating the status for an object.', [], 'Admin.Catalog.Notification') . + ' ' . $this->table . ' ' . + $this->trans('(cannot load object)', [], 'Admin.Catalog.Notification'); + + return; + } + + $this->attribute_name = $obj->name; + $this->fields_list = [ + 'id_attribute' => [ + 'title' => $this->trans('ID', [], 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ], + 'name' => [ + 'title' => $this->trans('Value', [], 'Admin.Catalog.Feature'), + 'width' => 'auto', + 'filter_key' => 'b!name', + ], + ]; + + if ($obj->group_type == 'color') { + $this->fields_list['color'] = [ + 'title' => $this->trans('Color', [], 'Admin.Catalog.Feature'), + 'filter_key' => 'a!color', + ]; + } + + $this->fields_list['position'] = [ + 'title' => $this->trans('Position', [], 'Admin.Global'), + 'filter_key' => 'a!position', + 'position' => 'position', + 'class' => 'fixed-width-md', + ]; + + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + $this->_where = 'AND a.`id_attribute_group` = ' . (int) $id; + $this->_orderBy = 'position'; + + self::$currentIndex = self::$currentIndex . '&id_attribute_group=' . (int) $id . '&viewattribute_group'; + $this->processFilter(); + + return parent::renderList(); + } + } + + /** + * AdminController::renderForm() override. + * + * @see AdminController::renderForm() + */ + public function renderForm() + { + $this->table = 'attribute_group'; + $this->identifier = 'id_attribute_group'; + + $group_type = [ + [ + 'id' => 'select', + 'name' => $this->trans('Drop-down list', [], 'Admin.Global'), + ], + [ + 'id' => 'radio', + 'name' => $this->trans('Radio buttons', [], 'Admin.Global'), + ], + [ + 'id' => 'color', + 'name' => $this->trans('Color or texture', [], 'Admin.Catalog.Feature'), + ], + ]; + + $this->fields_form = [ + 'legend' => [ + 'title' => $this->trans('Attributes', [], 'Admin.Catalog.Feature'), + 'icon' => 'icon-info-sign', + ], + 'input' => [ + [ + 'type' => 'text', + 'label' => $this->trans('Name', [], 'Admin.Global'), + 'name' => 'name', + 'lang' => true, + 'required' => true, + 'col' => '4', + 'hint' => $this->trans('Your internal name for this attribute.', [], 'Admin.Catalog.Help') . ' ' . $this->trans('Invalid characters:', [], 'Admin.Notifications.Info') . ' <>;=#{}', + ], + [ + 'type' => 'text', + 'label' => $this->trans('Public name', [], 'Admin.Catalog.Feature'), + 'name' => 'public_name', + 'lang' => true, + 'required' => true, + 'col' => '4', + 'hint' => $this->trans('The public name for this attribute, displayed to the customers.', [], 'Admin.Catalog.Help') . ' ' . $this->trans('Invalid characters:', [], 'Admin.Notifications.Info') . ' <>;=#{}', + ], + [ + 'type' => 'select', + 'label' => $this->trans('Attribute type', [], 'Admin.Catalog.Feature'), + 'name' => 'group_type', + 'required' => true, + 'options' => [ + 'query' => $group_type, + 'id' => 'id', + 'name' => 'name', + ], + 'col' => '2', + 'hint' => $this->trans('The way the attribute\'s values will be presented to the customers in the product\'s page.', [], 'Admin.Catalog.Help'), + ], + ], + ]; + + if (Shop::isFeatureActive()) { + $this->fields_form['input'][] = [ + 'type' => 'shop', + 'label' => $this->trans('Shop association', [], 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + ]; + } + + $this->fields_form['submit'] = [ + 'title' => $this->trans('Save', [], 'Admin.Actions'), + ]; + + if (!($obj = $this->loadObject(true))) { + return; + } + + return parent::renderForm(); + } + + public function renderFormAttributes() + { + $attributes_groups = AttributeGroup::getAttributesGroups($this->context->language->id); + + $this->table = 'attribute'; + $this->identifier = 'id_attribute'; + + $this->show_form_cancel_button = true; + $this->fields_form = [ + 'legend' => [ + 'title' => $this->trans('Values', [], 'Admin.Global'), + 'icon' => 'icon-info-sign', + ], + 'input' => [ + [ + 'type' => 'select', + 'label' => $this->trans('Attribute group', [], 'Admin.Catalog.Feature'), + 'name' => 'id_attribute_group', + 'required' => true, + 'options' => [ + 'query' => $attributes_groups, + 'id' => 'id_attribute_group', + 'name' => 'name', + ], + 'hint' => $this->trans('Choose the attribute group for this value.', [], 'Admin.Catalog.Help'), + ], + [ + 'type' => 'text', + 'label' => $this->trans('Value', [], 'Admin.Global'), + 'name' => 'name', + 'lang' => true, + 'required' => true, + 'hint' => $this->trans('Invalid characters:', [], 'Admin.Notifications.Info') . ' <>;=#{}', + ], + ], + ]; + + if (Shop::isFeatureActive()) { + // We get all associated shops for all attribute groups, because we will disable group shops + // for attributes that the selected attribute group don't support + $sql = 'SELECT id_attribute_group, id_shop FROM ' . _DB_PREFIX_ . 'attribute_group_shop'; + $associations = []; + foreach (Db::getInstance()->executeS($sql) as $row) { + $associations[$row['id_attribute_group']][] = $row['id_shop']; + } + + $this->fields_form['input'][] = [ + 'type' => 'shop', + 'label' => $this->trans('Shop association', [], 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + 'values' => Shop::getTree(), + ]; + } else { + $associations = []; + } + + $this->fields_form['shop_associations'] = json_encode($associations); + + $this->fields_form['input'][] = [ + 'type' => 'color', + 'label' => $this->trans('Color', [], 'Admin.Catalog.Feature'), + 'name' => 'color', + 'hint' => $this->trans('Choose a color with the color picker, or enter an HTML color (e.g. "lightblue", "#CC6600").', [], 'Admin.Catalog.Help'), + ]; + + $this->fields_form['input'][] = [ + 'type' => 'file', + 'label' => $this->trans('Texture', [], 'Admin.Catalog.Feature'), + 'name' => 'texture', + 'hint' => [ + $this->trans('Upload an image file containing the color texture from your computer.', [], 'Admin.Catalog.Help'), + $this->trans('This will override the HTML color!', [], 'Admin.Catalog.Help'), + ], + ]; + + $this->fields_form['input'][] = [ + 'type' => 'current_texture', + 'label' => $this->trans('Current texture', [], 'Admin.Catalog.Feature'), + 'name' => 'current_texture', + ]; + + $this->fields_form['input'][] = [ + 'type' => 'closediv', + 'name' => '', + ]; + + $this->fields_form['submit'] = [ + 'title' => $this->trans('Save', [], 'Admin.Actions'), + ]; + + $this->fields_form['buttons'] = [ + 'save-and-stay' => [ + 'title' => $this->trans('Save then add another value', [], 'Admin.Catalog.Feature'), + 'name' => 'submitAdd' . $this->table . 'AndStay', + 'type' => 'submit', + 'class' => 'btn btn-default pull-right', + 'icon' => 'process-icon-save', + ], + ]; + + $this->fields_value['id_attribute_group'] = (int) Tools::getValue('id_attribute_group'); + + // Override var of Controller + $this->table = 'attribute'; + $this->className = 'Attribute'; + $this->identifier = 'id_attribute'; + $this->lang = true; + $this->tpl_folder = 'attributes/'; + + // Create object Attribute + if (!$obj = new Attribute((int) Tools::getValue($this->identifier))) { + return; + } + + $str_attributes_groups = ''; + foreach ($attributes_groups as $attribute_group) { + $str_attributes_groups .= '"' . $attribute_group['id_attribute_group'] . '" : ' . ($attribute_group['group_type'] == 'color' ? '1' : '0') . ', '; + } + + $image = '../img/' . $this->fieldImageSettings['dir'] . '/' . (int) $obj->id . '.jpg'; + + $this->tpl_form_vars = [ + 'strAttributesGroups' => $str_attributes_groups, + 'colorAttributeProperties' => Validate::isLoadedObject($obj) && $obj->isColorAttribute(), + 'imageTextureExists' => file_exists(_PS_IMG_DIR_ . $this->fieldImageSettings['dir'] . '/' . (int) $obj->id . '.jpg'), + 'imageTexture' => $image, + 'imageTextureUrl' => Tools::safeOutput($_SERVER['REQUEST_URI']) . '&deleteImage=1', + ]; + + return parent::renderForm(); + } + + /** + * AdminController::init() override. + * + * @see AdminController::init() + */ + public function init() + { + if (Tools::isSubmit('updateattribute')) { + $this->display = 'editAttributes'; + } elseif (Tools::isSubmit('submitAddattribute')) { + $this->display = 'editAttributes'; + } elseif (Tools::isSubmit('submitAddattribute_group')) { + $this->display = 'add'; + } + + parent::init(); + } + + /** + * Override processAdd to change SaveAndStay button action. + * + * @see classes/AdminControllerCore::processUpdate() + */ + public function processAdd() + { + if ($this->table == 'attribute') { + /** @var AttributeGroup $object */ + $object = new $this->className(); + foreach (Language::getLanguages(false) as $language) { + if ($object->isAttribute( + (int) Tools::getValue('id_attribute_group'), + Tools::getValue('name_' . $language['id_lang']), + $language['id_lang'] + )) { + $this->errors['name_' . $language['id_lang']] = $this->trans( + 'The attribute value "%1$s" already exist for %2$s language', + [ + Tools::getValue('name_' . $language['id_lang']), + $language['name'], + ], + 'Admin.Catalog.Notification' + ); + } + } + + if (!empty($this->errors)) { + return $object; + } + } + + $object = parent::processAdd(); + + if (Tools::isSubmit('submitAdd' . $this->table . 'AndStay') && !count($this->errors)) { + if ($this->display == 'add') { + $this->redirect_after = self::$currentIndex . '&' . $this->identifier . '=&conf=3&update' . $this->table . '&token=' . $this->token; + } else { + $this->redirect_after = self::$currentIndex . '&id_attribute_group=' . (int) Tools::getValue('id_attribute_group') . '&conf=3&update' . $this->table . '&token=' . $this->token; + } + } + + if (count($this->errors)) { + $this->setTypeAttribute(); + } + + return $object; + } + + /** + * Override processUpdate to change SaveAndStay button action. + * + * @see classes/AdminControllerCore::processUpdate() + */ + public function processUpdate() + { + $object = parent::processUpdate(); + + if (Tools::isSubmit('submitAdd' . $this->table . 'AndStay') && !count($this->errors)) { + if ($this->display == 'add') { + $this->redirect_after = self::$currentIndex . '&' . $this->identifier . '=&conf=3&update' . $this->table . '&token=' . $this->token; + } else { + $this->redirect_after = self::$currentIndex . '&' . $this->identifier . '=&id_attribute_group=' . (int) Tools::getValue('id_attribute_group') . '&conf=3&update' . $this->table . '&token=' . $this->token; + } + } + + if (count($this->errors)) { + $this->setTypeAttribute(); + } + + if (Tools::isSubmit('updateattribute') || Tools::isSubmit('deleteattribute') || Tools::isSubmit('submitAddattribute') || Tools::isSubmit('submitBulkdeleteattribute')) { + Tools::clearColorListCache(); + } + + return $object; + } + + /** + * AdminController::initContent() override. + * + * @see AdminController::initContent() + */ + public function initContent() + { + if (Combination::isFeatureActive()) { + if ($this->display == 'edit' || $this->display == 'add') { + if (!($this->object = $this->loadObject(true))) { + return; + } + $this->content .= $this->renderForm(); + } elseif ($this->display == 'editAttributes') { + if (!$this->object = new Attribute((int) Tools::getValue('id_attribute'))) { + return; + } + + $this->content .= $this->renderFormAttributes(); + } elseif ($this->display != 'view' && !$this->ajax) { + $this->content .= $this->renderList(); + $this->content .= $this->renderOptions(); + /* reset all attributes filter */ + if (!Tools::getValue('submitFilterattribute_group', 0) && !Tools::getIsset('id_attribute_group')) { + $this->processResetFilters('attribute_values'); + } + } elseif ($this->display == 'view' && !$this->ajax) { + $this->content = $this->renderView(); + } + } else { + $adminPerformanceUrl = $this->context->link->getAdminLink('AdminPerformance'); + $url = '' . $this->trans('Performance', [], 'Admin.Global') . ''; + $this->displayWarning($this->trans('This feature has been disabled. You can activate it here: %link%.', ['%link%' => $url], 'Admin.Catalog.Notification')); + } + + $this->context->smarty->assign([ + 'table' => $this->table, + 'current' => self::$currentIndex, + 'token' => $this->token, + 'content' => $this->content, + ]); + } + + public function initPageHeaderToolbar() + { + if (Combination::isFeatureActive()) { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_attribute_group'] = [ + 'href' => self::$currentIndex . '&addattribute_group&token=' . $this->token, + 'desc' => $this->trans('Add new attribute', [], 'Admin.Catalog.Feature'), + 'icon' => 'process-icon-new', + ]; + $this->page_header_toolbar_btn['new_value'] = [ + 'href' => self::$currentIndex . '&updateattribute&id_attribute_group=' . (int) Tools::getValue('id_attribute_group') . '&token=' . $this->token, + 'desc' => $this->trans('Add new value', [], 'Admin.Catalog.Feature'), + 'icon' => 'process-icon-new', + ]; + } + } + + if ($this->display == 'view') { + $this->page_header_toolbar_btn['new_value'] = [ + 'href' => self::$currentIndex . '&updateattribute&id_attribute_group=' . (int) Tools::getValue('id_attribute_group') . '&token=' . $this->token, + 'desc' => $this->trans('Add new value', [], 'Admin.Catalog.Feature'), + 'icon' => 'process-icon-new', + ]; + } + + parent::initPageHeaderToolbar(); + } + + public function initToolbar() + { + switch ($this->display) { + // @todo defining default buttons + case 'add': + case 'edit': + case 'editAttributes': + // Default save button - action dynamically handled in javascript + $this->toolbar_btn['save'] = [ + 'href' => '#', + 'desc' => $this->trans('Save', [], 'Admin.Actions'), + ]; + + if ($this->display == 'editAttributes' && !$this->id_attribute) { + $this->toolbar_btn['save-and-stay'] = [ + 'short' => 'SaveAndStay', + 'href' => '#', + 'desc' => $this->trans('Save then add another value', [], 'Admin.Catalog.Help'), + 'force_desc' => true, + ]; + } + + $this->toolbar_btn['back'] = [ + 'href' => $this->context->link->getAdminLink('AdminAttributesGroups'), + 'desc' => $this->trans('Back to list', [], 'Admin.Actions'), + ]; + + break; + case 'view': + $this->toolbar_btn['newAttributes'] = [ + 'href' => $this->context->link->getAdminLink('AdminAttributesGroups', true, [], ['updateattribute' => 1, 'id_attribute_group' => (int) Tools::getValue('id_attribute_group')]), + 'desc' => $this->trans('Add New Values', [], 'Admin.Catalog.Feature'), + 'class' => 'toolbar-new', + ]; + + $this->toolbar_btn['back'] = [ + 'href' => $this->context->link->getAdminLink('AdminAttributesGroups'), + 'desc' => $this->trans('Back to list', [], 'Admin.Actions'), + ]; + + break; + default: // list + $this->toolbar_btn['new'] = [ + 'href' => $this->context->link->getAdminLink('AdminAttributesGroups', true, [], ['add' . $this->table => 1]), + 'desc' => $this->trans('Add New Attributes', [], 'Admin.Catalog.Feature'), + ]; + if ($this->can_import) { + $this->toolbar_btn['import'] = [ + 'href' => $this->context->link->getAdminLink('AdminImport', true, [], ['import_type' => 'combinations']), + 'desc' => $this->trans('Import', [], 'Admin.Actions'), + ]; + } + } + } + + public function initToolbarTitle() + { + $bread_extended = $this->breadcrumbs; + + switch ($this->display) { + case 'edit': + $bread_extended[] = $this->trans('Edit New Attribute', [], 'Admin.Catalog.Feature'); + + break; + + case 'add': + $bread_extended[] = $this->trans('Add New Attribute', [], 'Admin.Catalog.Feature'); + + break; + + case 'view': + if (Tools::getIsset('viewattribute_group')) { + if (($id = (int) Tools::getValue('id_attribute_group'))) { + if (Validate::isLoadedObject($obj = new AttributeGroup((int) $id))) { + $bread_extended[] = $obj->name[$this->context->employee->id_lang]; + } + } + } else { + $bread_extended[] = $this->attribute_name[$this->context->employee->id_lang]; + } + + break; + + case 'editAttributes': + if ($this->id_attribute) { + if (($id = (int) Tools::getValue('id_attribute_group'))) { + if (Validate::isLoadedObject($obj = new AttributeGroup((int) $id))) { + $bread_extended[] = '' . $obj->name[$this->context->employee->id_lang] . ''; + } + if (Validate::isLoadedObject($obj = new Attribute((int) $this->id_attribute))) { + $bread_extended[] = $this->trans( + 'Edit: %value%', + [ + '%value%' => $obj->name[$this->context->employee->id_lang], + ], + 'Admin.Catalog.Feature' + ); + } + } else { + $bread_extended[] = $this->trans('Edit Value', [], 'Admin.Catalog.Feature'); + } + } else { + $bread_extended[] = $this->trans('Add New Value', [], 'Admin.Catalog.Feature'); + } + + break; + } + + if (count($bread_extended) > 0) { + $this->addMetaTitle($bread_extended[count($bread_extended) - 1]); + } + + $this->toolbar_title = $bread_extended; + } + + public function initProcess() + { + $this->setTypeAttribute(); + + if (Tools::getIsset('viewattribute_group')) { + $this->list_id = 'attribute_values'; + + if (isset($_POST['submitReset' . $this->list_id])) { + $this->processResetFilters(); + } + } else { + $this->list_id = 'attribute_group'; + } + + parent::initProcess(); + + if ($this->table == 'attribute') { + $this->display = 'editAttributes'; + $this->id_attribute = (int) Tools::getValue('id_attribute'); + } + } + + protected function setTypeAttribute() + { + if (Tools::isSubmit('updateattribute') || Tools::isSubmit('deleteattribute') || Tools::isSubmit('submitAddattribute') || Tools::isSubmit('submitBulkdeleteattribute')) { + $this->table = 'attribute'; + $this->className = 'Attribute'; + $this->identifier = 'id_attribute'; + + if ($this->display == 'edit') { + $this->display = 'editAttributes'; + } + } + } + + public function processPosition() + { + if (Tools::getIsset('viewattribute_group')) { + $object = new Attribute((int) Tools::getValue('id_attribute')); + self::$currentIndex = self::$currentIndex . '&viewattribute_group'; + } else { + $object = new AttributeGroup((int) Tools::getValue('id_attribute_group')); + } + + if (!Validate::isLoadedObject($object)) { + $this->errors[] = $this->trans('An error occurred while updating the status for an object.', [], 'Admin.Notifications.Error') . + ' ' . $this->table . ' ' . $this->trans('(cannot load object)', [], 'Admin.Notifications.Error'); + } elseif (!$object->updatePosition((int) Tools::getValue('way'), (int) Tools::getValue('position'))) { + $this->errors[] = $this->trans('Failed to update the position.', [], 'Admin.Notifications.Error'); + } else { + $id_identifier_str = ($id_identifier = (int) Tools::getValue($this->identifier)) ? '&' . $this->identifier . '=' . $id_identifier : ''; + $redirect = self::$currentIndex . '&' . $this->table . 'Orderby=position&' . $this->table . 'Orderway=asc&conf=5' . $id_identifier_str . '&token=' . $this->token; + $this->redirect_after = $redirect; + } + + return $object; + } + + /** + * Call the right method for creating or updating object. + * + * @return mixed + */ + public function processSave() + { + if ($this->display == 'add' || $this->display == 'edit') { + $this->identifier = 'id_attribute_group'; + } + + if (!$this->id_object) { + return $this->processAdd(); + } else { + return $this->processUpdate(); + } + } + + public function postProcess() + { + if (!Combination::isFeatureActive()) { + return; + } + + if (!Tools::getValue($this->identifier) && (int) Tools::getValue('id_attribute') && !Tools::getValue('attributeOrderby')) { + // Override var of Controller + $this->table = 'attribute'; + $this->className = 'Attribute'; + $this->identifier = 'id_attribute'; + } + + /* set location with current index */ + if (Tools::getIsset('id_attribute_group') && Tools::getIsset('viewattribute_group')) { + self::$currentIndex = self::$currentIndex . '&id_attribute_group=' . (int) Tools::getValue('id_attribute_group', 0) . '&viewattribute_group'; + } + + // If it's an attribute, load object Attribute() + if (Tools::getValue('updateattribute') || Tools::isSubmit('deleteattribute') || Tools::isSubmit('submitAddattribute')) { + if (true !== $this->access('edit')) { + $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error'); + + return; + } elseif (!$object = new Attribute((int) Tools::getValue($this->identifier))) { + $this->errors[] = $this->trans('An error occurred while updating the status for an object.', [], 'Admin.Notifications.Error') . + ' ' . $this->table . ' ' . + $this->trans('(cannot load object)', [], 'Admin.Notifications.Error'); + + return; + } + + if (Tools::getValue('position') !== false && Tools::getValue('id_attribute')) { + $_POST['id_attribute_group'] = $object->id_attribute_group; + if (!$object->updatePosition((int) Tools::getValue('way'), (int) Tools::getValue('position'))) { + $this->errors[] = $this->trans('Failed to update the position.', [], 'Admin.Notifications.Error'); + } else { + Tools::redirectAdmin(self::$currentIndex . '&conf=5&token=' . Tools::getAdminTokenLite('AdminAttributesGroups') . '#details_details_' . $object->id_attribute_group); + } + } elseif (Tools::isSubmit('deleteattribute') && Tools::getValue('id_attribute')) { + if (!$object->delete()) { + $this->errors[] = $this->trans('Failed to delete the attribute.', [], 'Admin.Catalog.Notification'); + } else { + Tools::redirectAdmin(self::$currentIndex . '&conf=1&token=' . Tools::getAdminTokenLite('AdminAttributesGroups')); + } + } elseif (Tools::isSubmit('submitAddattribute')) { + Hook::exec('actionObjectAttributeAddBefore'); + $this->action = 'save'; + $id_attribute = (int) Tools::getValue('id_attribute'); + // Adding last position to the attribute if not exist + if ($id_attribute <= 0) { + $sql = 'SELECT `position`+1 + FROM `' . _DB_PREFIX_ . 'attribute` + WHERE `id_attribute_group` = ' . (int) Tools::getValue('id_attribute_group') . ' + ORDER BY position DESC'; + // set the position of the new group attribute in $_POST for postProcess() method + $_POST['position'] = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql); + } + $_POST['id_parent'] = 0; + $this->processSave($this->token); + } + } else { + if (Tools::isSubmit('submitBulkdelete' . $this->table)) { + if ($this->access('delete')) { + if (isset($_POST[$this->list_id . 'Box'])) { + /** @var AttributeGroup $object */ + $object = new $this->className(); + if ($object->deleteSelection($_POST[$this->list_id . 'Box'])) { + AttributeGroup::cleanPositions(); + Tools::redirectAdmin(self::$currentIndex . '&conf=2' . '&token=' . $this->token); + } + $this->errors[] = $this->trans('An error occurred while deleting this selection.', [], 'Admin.Notifications.Error'); + } else { + $this->errors[] = $this->trans('You must select at least one element to delete.', [], 'Admin.Notifications.Error'); + } + } else { + $this->errors[] = $this->trans('You do not have permission to delete this.', [], 'Admin.Notifications.Error'); + } + // clean position after delete + AttributeGroup::cleanPositions(); + } elseif (Tools::isSubmit('submitAdd' . $this->table)) { + Hook::exec('actionObjectAttributeGroupAddBefore'); + $id_attribute_group = (int) Tools::getValue('id_attribute_group'); + // Adding last position to the attribute if not exist + if ($id_attribute_group <= 0) { + $sql = 'SELECT `position`+1 + FROM `' . _DB_PREFIX_ . 'attribute_group` + ORDER BY position DESC'; + // set the position of the new group attribute in $_POST for postProcess() method + $_POST['position'] = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql); + } + // clean \n\r characters + foreach ($_POST as $key => $value) { + if (preg_match('/^name_/Ui', $key)) { + $_POST[$key] = str_replace('\n', '', str_replace('\r', '', $value)); + } + } + parent::postProcess(); + } else { + parent::postProcess(); + if (Tools::isSubmit('delete' . $this->table)) { + AttributeGroup::cleanPositions(); + } + } + } + } + + /** + * AdminController::getList() override. + * + * @see AdminController::getList() + * + * @param int $id_lang + * @param string|null $order_by + * @param string|null $order_way + * @param int $start + * @param int|null $limit + * @param int|bool $id_lang_shop + * + * @throws PrestaShopException + */ + public function getList($id_lang, $order_by = null, $order_way = null, $start = 0, $limit = null, $id_lang_shop = false) + { + parent::getList($id_lang, $order_by, $order_way, $start, $limit, $id_lang_shop); + + if ($this->display == 'view') { + foreach ($this->_list as &$list) { + if (file_exists(_PS_IMG_DIR_ . $this->fieldImageSettings['dir'] . '/' . (int) $list['id_attribute'] . '.jpg')) { + if (!isset($list['color']) || !is_array($list['color'])) { + $list['color'] = []; + } + $list['color']['texture'] = '../img/' . $this->fieldImageSettings['dir'] . '/' . (int) $list['id_attribute'] . '.jpg'; + } + } + } else { + $nb_items = count($this->_list); + for ($i = 0; $i < $nb_items; ++$i) { + $item = &$this->_list[$i]; + + $query = new DbQuery(); + $query->select('COUNT(a.id_attribute) as count_values'); + $query->from('attribute', 'a'); + $query->join(Shop::addSqlAssociation('attribute', 'a')); + $query->where('a.id_attribute_group =' . (int) $item['id_attribute_group']); + $query->groupBy('attribute_shop.id_shop'); + $query->orderBy('count_values DESC'); + $item['count_values'] = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($query); + unset($query); + } + } + } + + /** + * Overrides parent to delete items from sublist. + * + * @return mixed + */ + public function processBulkDelete() + { + // If we are deleting attributes instead of attribute_groups + if (Tools::getIsset('attributeBox')) { + $this->className = 'Attribute'; + $this->table = 'attribute'; + $this->boxes = Tools::getValue($this->table . 'Box'); + } + + $result = parent::processBulkDelete(); + // Restore vars + $this->className = 'AttributeGroup'; + $this->table = 'attribute_group'; + + return $result; + } + + /* Modify group attribute position */ + public function ajaxProcessUpdateGroupsPositions() + { + $way = (int) Tools::getValue('way'); + $id_attribute_group = (int) Tools::getValue('id_attribute_group'); + $positions = Tools::getValue('attribute_group'); + + $new_positions = []; + foreach ($positions as $v) { + if (count(explode('_', $v)) == 4) { + $new_positions[] = $v; + } + } + + foreach ($new_positions as $position => $value) { + $pos = explode('_', $value); + + if (isset($pos[2]) && (int) $pos[2] === $id_attribute_group) { + if ($group_attribute = new AttributeGroup((int) $pos[2])) { + if (isset($position) && $group_attribute->updatePosition($way, $position)) { + echo 'ok position ' . (int) $position . ' for attribute group ' . (int) $pos[2] . '\r\n'; + } else { + echo '{"hasError" : true, "errors" : "Can not update the ' . (int) $id_attribute_group . ' attribute group to position ' . (int) $position . ' "}'; + } + } else { + echo '{"hasError" : true, "errors" : "The (' . (int) $id_attribute_group . ') attribute group cannot be loaded."}'; + } + + break; + } + } + } + + /* Modify attribute position */ + public function ajaxProcessUpdateAttributesPositions() + { + $way = (int) Tools::getValue('way'); + $id_attribute = (int) Tools::getValue('id_attribute'); + $id_attribute_group = (int) Tools::getValue('id_attribute_group'); + $positions = Tools::getValue('attribute'); + + if (is_array($positions)) { + foreach ($positions as $position => $value) { + $pos = explode('_', $value); + + if ((isset($pos[1], $pos[2])) && (int) $pos[2] === $id_attribute) { + if ($attribute = new Attribute((int) $pos[2])) { + if (isset($position) && $attribute->updatePosition($way, $position)) { + echo 'ok position ' . (int) $position . ' for attribute ' . (int) $pos[2] . '\r\n'; + } else { + echo '{"hasError" : true, "errors" : "Can not update the ' . (int) $id_attribute . ' attribute to position ' . (int) $position . ' "}'; + } + } else { + echo '{"hasError" : true, "errors" : "The (' . (int) $id_attribute . ') attribute cannot be loaded"}'; + } + + break; + } + } + } + } +} diff --git a/controllers/admin/AdminCarrierWizardController.php b/controllers/admin/AdminCarrierWizardController.php new file mode 100644 index 00000000..6e25e5c4 --- /dev/null +++ b/controllers/admin/AdminCarrierWizardController.php @@ -0,0 +1,960 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +/** + * @property Carrier $object + */ +class AdminCarrierWizardControllerCore extends AdminController +{ + protected $wizard_access; + + public function __construct() + { + $this->bootstrap = true; + $this->display = 'view'; + $this->table = 'carrier'; + $this->identifier = 'id_carrier'; + $this->className = 'Carrier'; + $this->lang = false; + $this->deleted = true; + $this->step_number = 0; + $this->type_context = Shop::getContext(); + $this->old_context = Context::getContext(); + $this->multishop_context = Shop::CONTEXT_ALL; + $this->context = Context::getContext(); + + $this->fieldImageSettings = [ + 'name' => 'logo', + 'dir' => 's', + ]; + + parent::__construct(); + + $this->tabAccess = Profile::getProfileAccess($this->context->employee->id_profile, Tab::getIdFromClassName('AdminCarriers')); + } + + public function setMedia($isNewTheme = false) + { + parent::setMedia($isNewTheme); + $this->addJqueryPlugin('smartWizard'); + $this->addJqueryPlugin('typewatch'); + $this->addJs(_PS_JS_DIR_ . 'admin/carrier_wizard.js'); + } + + public function initWizard() + { + $this->wizard_steps = [ + 'name' => 'carrier_wizard', + 'steps' => [ + [ + 'title' => $this->trans('General settings', [], 'Admin.Shipping.Feature'), + ], + [ + 'title' => $this->trans('Shipping locations and costs', [], 'Admin.Shipping.Feature'), + ], + [ + 'title' => $this->trans('Size, weight, and group access', [], 'Admin.Shipping.Feature'), + ], + [ + 'title' => $this->trans('Summary', [], 'Admin.Global'), + ], ], + ]; + + if (Shop::isFeatureActive()) { + $multistore_step = [ + [ + 'title' => $this->trans('MultiStore', [], 'Admin.Global'), + ], + ]; + array_splice($this->wizard_steps['steps'], 1, 0, $multistore_step); + } + } + + public function renderView() + { + $this->initWizard(); + + if (Tools::getValue('id_carrier') && $this->access('edit')) { + $carrier = $this->loadObject(); + } elseif ($this->access('add')) { + $carrier = new Carrier(); + } + + if ((!$this->access('edit') && Tools::getValue('id_carrier')) || (!$this->access('add') && !Tools::getValue('id_carrier'))) { + $this->errors[] = $this->trans('You do not have permission to use this wizard.', [], 'Admin.Shipping.Notification'); + + return; + } + + $currency = $this->getActualCurrency(); + + $this->tpl_view_vars = [ + 'currency_sign' => $currency->sign, + 'PS_WEIGHT_UNIT' => Configuration::get('PS_WEIGHT_UNIT'), + 'enableAllSteps' => Validate::isLoadedObject($carrier), + 'wizard_steps' => $this->wizard_steps, + 'validate_url' => $this->context->link->getAdminLink('AdminCarrierWizard'), + 'carrierlist_url' => $this->context->link->getAdminLink('AdminCarriers') . '&conf=' . ((int) Validate::isLoadedObject($carrier) ? 4 : 3), + 'multistore_enable' => Shop::isFeatureActive(), + 'wizard_contents' => [ + 'contents' => [ + 0 => $this->renderStepOne($carrier), + 1 => $this->renderStepThree($carrier), + 2 => $this->renderStepFour($carrier), + 3 => $this->renderStepFive($carrier), + ], + ], + 'labels' => [ + 'next' => $this->trans('Next', [], 'Admin.Global'), + 'previous' => $this->trans('Previous', [], 'Admin.Global'), + 'finish' => $this->trans('Finish', [], 'Admin.Actions'), ], + ]; + + if (Shop::isFeatureActive()) { + array_splice($this->tpl_view_vars['wizard_contents']['contents'], 1, 0, [0 => $this->renderStepTwo($carrier)]); + } + + $this->context->smarty->assign([ + 'carrier_logo' => (Validate::isLoadedObject($carrier) && file_exists(_PS_SHIP_IMG_DIR_ . $carrier->id . '.jpg') ? _THEME_SHIP_DIR_ . $carrier->id . '.jpg' : false), + ]); + + $this->context->smarty->assign([ + 'logo_content' => $this->createTemplate('logo.tpl')->fetch(), + ]); + + $this->addjQueryPlugin(['ajaxfileupload']); + + return parent::renderView(); + } + + public function initBreadcrumbs($tab_id = null, $tabs = null) + { + if (Tools::getValue('id_carrier')) { + $this->display = 'edit'; + } else { + $this->display = 'add'; + } + + parent::initBreadcrumbs((int) Tab::getIdFromClassName('AdminCarriers')); + + $this->display = 'view'; + } + + public function initPageHeaderToolbar() + { + parent::initPageHeaderToolbar(); + + $this->page_header_toolbar_btn['cancel'] = [ + 'href' => $this->context->link->getAdminLink('AdminCarriers'), + 'desc' => $this->trans('Cancel', [], 'Admin.Actions'), + ]; + } + + public function renderStepOne($carrier) + { + $this->fields_form = [ + 'form' => [ + 'id_form' => 'step_carrier_general', + 'input' => [ + [ + 'type' => 'text', + 'label' => $this->trans('Carrier name', [], 'Admin.Shipping.Feature'), + 'name' => 'name', + 'required' => true, + 'hint' => [ + $this->trans('Allowed characters: letters, spaces and "%special_chars%".', ['%special_chars%' => '().-'], 'Admin.Shipping.Help'), + $this->trans('The carrier\'s name will be displayed during checkout.', [], 'Admin.Shipping.Help'), + $this->trans('For in-store pickup, enter 0 to replace the carrier name with your shop name.', [], 'Admin.Shipping.Help'), + ], + ], + [ + 'type' => 'text', + 'label' => $this->trans('Transit time', [], 'Admin.Shipping.Feature'), + 'name' => 'delay', + 'lang' => true, + 'required' => true, + 'maxlength' => 512, + 'hint' => $this->trans('The delivery time will be displayed during checkout.', [], 'Admin.Shipping.Help'), + ], + [ + 'type' => 'text', + 'label' => $this->trans('Speed grade', [], 'Admin.Shipping.Feature'), + 'name' => 'grade', + 'required' => false, + 'size' => 1, + 'hint' => $this->trans('Enter "0" for a longest shipping delay, or "9" for the shortest shipping delay.', [], 'Admin.Shipping.Help'), + ], + [ + 'type' => 'logo', + 'label' => $this->trans('Logo', [], 'Admin.Global'), + 'name' => 'logo', + ], + [ + 'type' => 'text', + 'label' => $this->trans('Tracking URL', [], 'Admin.Shipping.Feature'), + 'name' => 'url', + 'hint' => $this->trans('Delivery tracking URL: Type \'@\' where the tracking number should appear. It will be automatically replaced by the tracking number.', [], 'Admin.Shipping.Help'), + 'desc' => $this->trans('For example: \'http://example.com/track.php?num=@\' with \'@\' where the tracking number should appear.', [], 'Admin.Shipping.Help'), + ], + ], + ], + ]; + + $tpl_vars = ['max_image_size' => (int) Configuration::get('PS_PRODUCT_PICTURE_MAX_SIZE') / 1024 / 1024]; + $fields_value = $this->getStepOneFieldsValues($carrier); + + return $this->renderGenericForm(['form' => $this->fields_form], $fields_value, $tpl_vars); + } + + public function renderStepTwo($carrier) + { + $this->fields_form = [ + 'form' => [ + 'id_form' => 'step_carrier_shops', + 'force' => true, + 'input' => [ + [ + 'type' => 'shop', + 'label' => $this->trans('Shop association', [], 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + ], + ], + ], + ]; + $fields_value = $this->getStepTwoFieldsValues($carrier); + + return $this->renderGenericForm(['form' => $this->fields_form], $fields_value); + } + + public function renderStepThree($carrier) + { + $this->fields_form = [ + 'form' => [ + 'id_form' => 'step_carrier_ranges', + 'input' => [ + 'shipping_handling' => [ + 'type' => 'switch', + 'label' => $this->trans('Add handling costs', [], 'Admin.Shipping.Feature'), + 'name' => 'shipping_handling', + 'required' => false, + 'class' => 't', + 'is_bool' => true, + 'values' => [ + [ + 'id' => 'shipping_handling_on', + 'value' => 1, + 'label' => $this->trans('Enabled', [], 'Admin.Global'), + ], + [ + 'id' => 'shipping_handling_off', + 'value' => 0, + 'label' => $this->trans('Disabled', [], 'Admin.Global'), + ], + ], + 'hint' => $this->trans('Include the handling costs (as set in Shipping > Preferences) in the final carrier price.', [], 'Admin.Shipping.Help'), + ], + 'is_free' => [ + 'type' => 'switch', + 'label' => $this->trans('Free shipping', [], 'Admin.Shipping.Feature'), + 'name' => 'is_free', + 'required' => false, + 'class' => 't', + 'values' => [ + [ + 'id' => 'is_free_on', + 'value' => 1, + 'label' => '', + ], + [ + 'id' => 'is_free_off', + 'value' => 0, + 'label' => '', + ], + ], + ], + 'shipping_method' => [ + 'type' => 'radio', + 'label' => $this->trans('Billing', [], 'Admin.Shipping.Feature'), + 'name' => 'shipping_method', + 'required' => false, + 'class' => 't', + 'br' => true, + 'values' => [ + [ + 'id' => 'billing_price', + 'value' => Carrier::SHIPPING_METHOD_PRICE, + 'label' => $this->trans('According to total price.', [], 'Admin.Shipping.Feature'), + ], + [ + 'id' => 'billing_weight', + 'value' => Carrier::SHIPPING_METHOD_WEIGHT, + 'label' => $this->trans('According to total weight.', [], 'Admin.Shipping.Feature'), + ], + ], + ], + 'id_tax_rules_group' => [ + 'type' => 'select', + 'label' => $this->trans('Tax', [], 'Admin.Global'), + 'name' => 'id_tax_rules_group', + 'options' => [ + 'query' => TaxRulesGroup::getTaxRulesGroups(true), + 'id' => 'id_tax_rules_group', + 'name' => 'name', + 'default' => [ + 'label' => $this->trans('No tax', [], 'Admin.Global'), + 'value' => 0, + ], + ], + ], + 'range_behavior' => [ + 'type' => 'select', + 'label' => $this->trans('Out-of-range behavior', [], 'Admin.Shipping.Feature'), + 'name' => 'range_behavior', + 'options' => [ + 'query' => [ + [ + 'id' => 0, + 'name' => $this->trans('Apply the cost of the highest defined range', [], 'Admin.Shipping.Feature'), + ], + [ + 'id' => 1, + 'name' => $this->trans('Disable carrier', [], 'Admin.Shipping.Feature'), + ], + ], + 'id' => 'id', + 'name' => 'name', + ], + 'hint' => $this->trans('Out-of-range behavior occurs when no defined range matches the customer\'s cart (e.g. when the weight of the cart is greater than the highest weight limit defined by the weight ranges).', [], 'Admin.Shipping.Help'), + ], + 'zones' => [ + 'type' => 'zone', + 'name' => 'zones', + ], + ], + ], + ]; + + if (Configuration::get('PS_ATCP_SHIPWRAP')) { + unset($this->fields_form['form']['input']['id_tax_rules_group']); + } + + $tpl_vars = []; + $tpl_vars['PS_WEIGHT_UNIT'] = Configuration::get('PS_WEIGHT_UNIT'); + + $currency = $this->getActualCurrency(); + + $tpl_vars['currency_sign'] = $currency->sign; + + $fields_value = $this->getStepThreeFieldsValues($carrier); + + $this->getTplRangesVarsAndValues($carrier, $tpl_vars, $fields_value); + + return $this->renderGenericForm(['form' => $this->fields_form], $fields_value, $tpl_vars); + } + + /** + * @param Carrier $carrier + * + * @return string + */ + public function renderStepFour($carrier) + { + $this->fields_form = [ + 'form' => [ + 'id_form' => 'step_carrier_conf', + 'input' => [ + [ + 'type' => 'text', + 'label' => $this->trans('Maximum package width (%s)', ['%s' => Configuration::get('PS_DIMENSION_UNIT')], 'Admin.Shipping.Feature'), + 'name' => 'max_width', + 'required' => false, + 'hint' => $this->trans('Maximum width managed by this carrier. Set the value to "0", or leave this field blank to ignore.', [], 'Admin.Shipping.Help') . ' ' . $this->trans('The value must be an integer.', [], 'Admin.Shipping.Help'), + ], + [ + 'type' => 'text', + 'label' => $this->trans('Maximum package height (%s)', ['%s' => Configuration::get('PS_DIMENSION_UNIT')], 'Admin.Shipping.Feature'), + 'name' => 'max_height', + 'required' => false, + 'hint' => $this->trans('Maximum height managed by this carrier. Set the value to "0", or leave this field blank to ignore.', [], 'Admin.Shipping.Help') . ' ' . $this->trans('The value must be an integer.', [], 'Admin.Shipping.Help'), + ], + [ + 'type' => 'text', + 'label' => $this->trans('Maximum package depth (%s)', ['%s' => Configuration::get('PS_DIMENSION_UNIT')], 'Admin.Shipping.Feature'), + 'name' => 'max_depth', + 'required' => false, + 'hint' => $this->trans('Maximum depth managed by this carrier. Set the value to "0", or leave this field blank to ignore.', [], 'Admin.Shipping.Help') . ' ' . $this->trans('The value must be an integer.', [], 'Admin.Shipping.Help'), + ], + [ + 'type' => 'text', + 'label' => $this->trans('Maximum package weight (%s)', ['%s' => Configuration::get('PS_WEIGHT_UNIT')], 'Admin.Shipping.Feature'), + 'name' => 'max_weight', + 'required' => false, + 'hint' => $this->trans('Maximum weight managed by this carrier. Set the value to "0", or leave this field blank to ignore.', [], 'Admin.Shipping.Help'), + ], + [ + 'type' => 'group', + 'label' => $this->trans('Group access', [], 'Admin.Shipping.Feature'), + 'name' => 'groupBox', + 'values' => Group::getGroups(Context::getContext()->language->id), + 'hint' => $this->trans('Mark the groups that are allowed access to this carrier.', [], 'Admin.Shipping.Help'), + ], + ], + ], + ]; + + $fields_value = $this->getStepFourFieldsValues($carrier); + + // Added values of object Group + $carrier_groups = $carrier->getGroups(); + $carrier_groups_ids = []; + if (is_array($carrier_groups)) { + foreach ($carrier_groups as $carrier_group) { + $carrier_groups_ids[] = $carrier_group['id_group']; + } + } + + $groups = Group::getGroups($this->context->language->id); + + foreach ($groups as $group) { + $fields_value['groupBox_' . $group['id_group']] = Tools::getValue('groupBox_' . $group['id_group'], (in_array($group['id_group'], $carrier_groups_ids) || empty($carrier_groups_ids) && !$carrier->id)); + } + + return $this->renderGenericForm(['form' => $this->fields_form], $fields_value); + } + + public function renderStepFive($carrier) + { + $this->fields_form = [ + 'form' => [ + 'id_form' => 'step_carrier_summary', + 'input' => [ + [ + 'type' => 'switch', + 'label' => $this->trans('Enabled', [], 'Admin.Global'), + 'name' => 'active', + 'required' => false, + 'class' => 't', + 'is_bool' => true, + 'values' => [ + [ + 'id' => 'active_on', + 'value' => 1, + ], + [ + 'id' => 'active_off', + 'value' => 0, + ], + ], + 'hint' => $this->trans('Enable the carrier in the front office.', [], 'Admin.Shipping.Help'), + ], + ], + ], + ]; + $template = $this->createTemplate('controllers/carrier_wizard/summary.tpl'); + $fields_value = $this->getStepFiveFieldsValues($carrier); + $active_form = $this->renderGenericForm(['form' => $this->fields_form], $fields_value); + $active_form = str_replace(['
' . $content . '
php_value post_max_size 20M
+ + ' + . $this->trans('___________ CUSTOM ___________', [], 'Admin.Design.Feature') + . ''; + + /** @todo do something better with controllers */ + $controllers = Dispatcher::getControllers(_PS_FRONT_CONTROLLER_DIR_); + ksort($controllers); + + foreach ($file_list as $k => $v) { + if (!array_key_exists($v, $controllers)) { + $content .= '' . $v . ''; + } + } + + $content .= '' . $this->trans('____________ CORE ____________', [], 'Admin.Design.Feature') . ''; + + foreach ($controllers as $k => $v) { + $content .= '' . $k . ''; + } + + $modules_controllers_type = ['admin' => $this->trans('Admin modules controller', [], 'Admin.Design.Feature'), 'front' => $this->trans('Front modules controller', [], 'Admin.Design.Feature')]; + foreach ($modules_controllers_type as $type => $label) { + $content .= '____________ ' . $label . ' ____________'; + $all_modules_controllers = Dispatcher::getModuleControllers($type); + foreach ($all_modules_controllers as $module => $modules_controllers) { + foreach ($modules_controllers as $cont) { + $content .= 'module-' . $module . '-' . $cont . ''; + } + } + } + + $content .= ' +
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc lacinia in enim iaculis malesuada. Quisque congue fermentum leo et porta. Pellentesque a quam dui. Pellentesque sed augue id sem aliquet faucibus eu vel odio. Nullam non libero volutpat, pulvinar turpis non, gravida mauris. Nullam tincidunt id est at euismod. Quisque euismod quam in pellentesque mollis. Nulla suscipit porttitor massa, nec eleifend risus egestas in. Aenean luctus porttitor tempus. Morbi dolor leo, dictum id interdum vel, semper ac est. Maecenas justo augue, accumsan in velit nec, consectetur fringilla orci. Nunc ut ante erat. Curabitur dolor augue, eleifend a luctus non, aliquet a mi. Curabitur ultricies lectus in rhoncus sodales. Maecenas quis dictum erat. Suspendisse blandit lacus sed felis facilisis, in interdum quam congue.
', + ]; + + $this->fields_form = [ + 'legend' => [ + 'title' => 'patterns of helper form.tpl', + 'icon' => 'icon-edit', + ], + 'tabs' => [ + 'small' => 'Small Inputs', + 'large' => 'Large Inputs', + ], + 'description' => 'You can use image instead of icon for the title.', + 'input' => [ + [ + 'type' => 'text', + 'label' => 'simple input text', + 'name' => 'type_text', + ], + [ + 'type' => 'text', + 'label' => 'input text with desc', + 'name' => 'type_text_desc', + 'desc' => 'desc input text', + ], + [ + 'type' => 'text', + 'label' => 'required input text', + 'name' => 'type_text_required', + 'required' => true, + ], + [ + 'type' => 'text', + 'label' => 'input text with hint', + 'name' => 'type_text_hint', + 'hint' => 'hint input text', + ], + [ + 'type' => 'text', + 'label' => 'input text with prefix', + 'name' => 'type_text_prefix', + 'prefix' => 'prefix', + ], + [ + 'type' => 'text', + 'label' => 'input text with suffix', + 'name' => 'type_text_suffix', + 'suffix' => 'suffix', + ], + [ + 'type' => 'text', + 'label' => 'input text with placeholder', + 'name' => 'type_text_placeholder', + 'placeholder' => 'placeholder', + ], + [ + 'type' => 'text', + 'label' => 'input text with character counter', + 'name' => 'type_text_maxchar', + 'maxchar' => 30, + ], + [ + 'type' => 'text', + 'lang' => true, + 'label' => 'input text multilang', + 'name' => 'type_text_multilang', + ], + [ + 'type' => 'text', + 'label' => 'input readonly', + 'readonly' => true, + 'name' => 'type_text_readonly', + ], + [ + 'type' => 'text', + 'label' => 'input fixed-width-xs', + 'name' => 'type_text_xs', + 'class' => 'input fixed-width-xs', + ], + [ + 'type' => 'text', + 'label' => 'input fixed-width-sm', + 'name' => 'type_text_sm', + 'class' => 'input fixed-width-sm', + ], + [ + 'type' => 'text', + 'label' => 'input fixed-width-md', + 'name' => 'type_text_md', + 'class' => 'input fixed-width-md', + ], + [ + 'type' => 'text', + 'label' => 'input fixed-width-lg', + 'name' => 'type_text_lg', + 'class' => 'input fixed-width-lg', + ], + [ + 'type' => 'text', + 'label' => 'input fixed-width-xl', + 'name' => 'type_text_xl', + 'class' => 'input fixed-width-xl', + ], + [ + 'type' => 'text', + 'label' => 'input fixed-width-xxl', + 'name' => 'type_text_xxl', + 'class' => 'fixed-width-xxl', + ], + [ + 'type' => 'text', + 'label' => 'input fixed-width-sm', + 'name' => 'type_text_sm', + 'class' => 'input fixed-width-sm', + 'tab' => 'small', + ], + [ + 'type' => 'text', + 'label' => 'input fixed-width-md', + 'name' => 'type_text_md', + 'class' => 'input fixed-width-md', + 'tab' => 'small', + ], + [ + 'type' => 'text', + 'label' => 'input fixed-width-lg', + 'name' => 'type_text_lg', + 'class' => 'input fixed-width-lg', + 'tab' => 'large', + ], + [ + 'type' => 'text', + 'label' => 'input fixed-width-xl', + 'name' => 'type_text_xl', + 'class' => 'input fixed-width-xl', + 'tab' => 'large', + ], + [ + 'type' => 'text', + 'label' => 'input fixed-width-xxl', + 'name' => 'type_text_xxl', + 'class' => 'fixed-width-xxl', + 'tab' => 'large', + ], + [ + 'type' => 'free', + 'label' => 'About tabs', + 'name' => 'tab_note', + 'tab' => 'small', + ], + [ + 'type' => 'text', + 'label' => 'input fixed-width-md with prefix', + 'name' => 'type_text_md', + 'class' => 'input fixed-width-md', + 'prefix' => 'prefix', + ], + [ + 'type' => 'text', + 'label' => 'input fixed-width-md with sufix', + 'name' => 'type_text_md', + 'class' => 'input fixed-width-md', + 'suffix' => 'suffix', + ], + [ + 'type' => 'tags', + 'label' => 'input tags', + 'name' => 'type_text_tags', + ], + [ + 'type' => 'textbutton', + 'label' => 'input with button', + 'name' => 'type_textbutton', + 'button' => [ + 'label' => 'do something', + 'attributes' => [ + 'onclick' => 'alert(\'something done\');', + ], + ], + ], + [ + 'type' => 'select', + 'label' => 'select', + 'name' => 'type_select', + 'options' => [ + 'query' => Zone::getZones(), + 'id' => 'id_zone', + 'name' => 'name', + ], + ], + [ + 'type' => 'select', + 'label' => 'select with chosen', + 'name' => 'type_select_chosen', + 'class' => 'chosen', + 'options' => [ + 'query' => Country::getCountries((int) Context::getContext()->cookie->id_lang), + 'id' => 'id_zone', + 'name' => 'name', + ], + ], + [ + 'type' => 'select', + 'label' => 'select multiple with chosen', + 'name' => 'type_select_multiple_chosen', + 'class' => 'chosen', + 'multiple' => true, + 'options' => [ + 'query' => Country::getCountries((int) Context::getContext()->cookie->id_lang), + 'id' => 'id_zone', + 'name' => 'name', + ], + ], + [ + 'type' => 'radio', + 'label' => 'radios', + 'name' => 'type_radio', + 'values' => [ + [ + 'id' => 'type_male', + 'value' => 0, + 'label' => 'first', + ], + [ + 'id' => 'type_female', + 'value' => 1, + 'label' => 'second', + ], + [ + 'id' => 'type_neutral', + 'value' => 2, + 'label' => 'third', + ], + ], + ], + [ + 'type' => 'checkbox', + 'label' => 'checkbox', + 'name' => 'type_checkbox', + 'values' => [ + 'query' => Zone::getZones(), + 'id' => 'id_zone', + 'name' => 'name', + ], + ], + [ + 'type' => 'switch', + 'label' => 'switch', + 'name' => 'type_switch', + 'values' => [ + [ + 'id' => 'type_switch_on', + 'value' => 1, + ], + [ + 'id' => 'type_switch_off', + 'value' => 0, + ], + ], + ], + [ + 'type' => 'switch', + 'label' => 'switch disabled', + 'name' => 'type_switch_disabled', + 'disabled' => 'true', + 'values' => [ + [ + 'id' => 'type_switch_disabled_on', + 'value' => 1, + ], + [ + 'id' => 'type_switch_disabled_off', + 'value' => 0, + ], + ], + ], + [ + 'type' => 'textarea', + 'label' => 'text area (with autoresize)', + 'name' => 'type_textarea', + ], + [ + 'type' => 'textarea', + 'label' => 'text area with rich text editor', + 'name' => 'type_textarea_rte', + 'autoload_rte' => true, + ], + [ + 'type' => 'password', + 'label' => 'input password', + 'name' => 'type_password', + ], + [ + 'type' => 'birthday', + 'label' => 'input birthday', + 'name' => 'type_birthday', + 'options' => [ + 'days' => Tools::dateDays(), + 'months' => Tools::dateMonths(), + 'years' => Tools::dateYears(), + ], + ], + [ + 'type' => 'group', + 'label' => 'group', + 'name' => 'type_group', + 'values' => Group::getGroups(Context::getContext()->language->id), + ], + [ + 'type' => 'categories', + 'label' => 'tree categories', + 'name' => 'type_categories', + 'tree' => [ + 'root_category' => 1, + 'id' => 'id_category', + 'name' => 'name_category', + 'selected_categories' => [3], + ], + ], + [ + 'type' => 'file', + 'label' => 'input file', + 'name' => 'type_file', + ], + [ + 'type' => 'color', + 'label' => 'input color', + 'name' => 'type_color', + ], + [ + 'type' => 'date', + 'label' => 'input date', + 'name' => 'type_date', + ], + [ + 'type' => 'datetime', + 'label' => 'input date and time', + 'name' => 'type_datetime', + ], + [ + 'type' => 'html', + 'name' => 'html_data', + 'html_content' => '
+ ' . $this->trans('The "indexed" products have been analyzed by PrestaShop and will appear in the results of a front office search.', [], 'Admin.Shopparameters.Feature') . ' + ' . $this->trans('Indexed products', [], 'Admin.Shopparameters.Feature') . ' ' . (int) $indexed . ' / ' . (int) $total . '. +
+ ' . $this->trans('Building the product index may take a few minutes.', [], 'Admin.Shopparameters.Feature') . ' + ' . $this->trans('If your server stops before the process ends, you can resume the indexing by clicking "%add_missing_products_label%".', ['%add_missing_products_label%' => $this->trans('Add missing products to the index', [], 'Admin.Shopparameters.Feature')], 'Admin.Shopparameters.Feature') . ' +
+ ' . $this->trans('You can set a cron job that will rebuild your index using the following URL:', [], 'Admin.Shopparameters.Feature') . ' + + + ' . Tools::safeOutput($cron_url) . ' + +
Signaler un problème sur GitHub' + . ' Proposer une idée d\'amélioration sur GitHub', + 'fields' => [ + 'PS_SEARCH_START' => [ + 'title' => $this->trans('Search within word', [], 'Admin.Shopparameters.Feature'), + 'validation' => 'isBool', + 'cast' => 'intval', + 'type' => 'bool', + 'desc' => $this->trans( + 'By default, to search for “blouse”, you have to enter “blous”, “blo”, etc (beginning of the word) – but not “lous” (within the word).', + [], + 'Admin.Shopparameters.Help' + ) . '' . + $this->trans( + 'With this option enabled, it also gives the good result if you search for “lous”, “ouse”, or anything contained in the word.', + [], + 'Admin.Shopparameters.Help' + ), + 'hint' => [ + $this->trans( + 'Enable search within a whole word, rather than from its beginning only.', + [], + 'Admin.Shopparameters.Help' + ), + $this->trans( + 'It checks if the searched term is contained in the indexed word. This may be resource-consuming.', + [], + 'Admin.Shopparameters.Help' + ), + ], + ], + 'PS_SEARCH_END' => [ + 'title' => $this->trans('Search exact end match', [], 'Admin.Shopparameters.Feature'), + 'validation' => 'isBool', + 'cast' => 'intval', + 'type' => 'bool', + 'desc' => $this->trans( + 'By default, if you search "book", you will have "book", "bookcase" and "bookend".', + [], + 'Admin.Shopparameters.Help' + ) . '' . + $this->trans( + 'With this option enabled, it only gives one result “book”, as exact end of the indexed word is matching.', + [], + 'Admin.Shopparameters.Help' + ), + 'hint' => [ + $this->trans( + 'Enable more precise search with the end of the word.', + [], + 'Admin.Shopparameters.Help' + ), + $this->trans( + 'It checks if the searched term is the exact end of the indexed word.', + [], + 'Admin.Shopparameters.Help' + ), + ], + ], + 'PS_SEARCH_FUZZY' => [ + 'title' => $this->trans('Fuzzy search', [], 'Admin.Shopparameters.Feature'), + 'validation' => 'isBool', + 'cast' => 'intval', + 'type' => 'bool', + 'desc' => $this->trans( + 'By default, the fuzzy search is enabled. It means spelling errors are allowed, e.g. you can search for "bird" with words like "burd", "bard" or "beerd".', + [], + 'Admin.Shopparameters.Help' + ) . '' . + $this->trans( + 'Disabling this option will require exact spelling for the search to match results.', + [], + 'Admin.Shopparameters.Help' + ), + 'hint' => [ + $this->trans( + 'Enable approximate string matching.', + [], + 'Admin.Shopparameters.Help' + ), + ], + ], + 'PS_SEARCH_FUZZY_MAX_LOOP' => [ + 'title' => $this->trans( + 'Maximum approximate words allowed by fuzzy search', + [], + 'Admin.Shopparameters.Feature' + ), + 'hint' => $this->trans( + 'Note that this option is resource-consuming: the more you search, the longer it takes.', + [], + 'Admin.Shopparameters.Help' + ), + 'validation' => 'isUnsignedInt', + 'type' => 'text', + 'cast' => 'intval', + ], + 'PS_SEARCH_MAX_WORD_LENGTH' => [ + 'title' => $this->trans( + 'Maximum word length (in characters)', + [], + 'Admin.Shopparameters.Feature' + ), + 'hint' => $this->trans( + 'Only words fewer or equal to this maximum length will be searched.', + [], + 'Admin.Shopparameters.Help' + ), + 'desc' => $this->trans( + 'This parameter will only be used if the fuzzy search is activated: the lower the value, the more tolerant your search will be.', + [], + 'Admin.Shopparameters.Help' + ), + 'validation' => 'isUnsignedInt', + 'type' => 'text', + 'cast' => 'intval', + 'required' => true, + ], + 'PS_SEARCH_MINWORDLEN' => [ + 'title' => $this->trans( + 'Minimum word length (in characters)', + [], + 'Admin.Shopparameters.Feature' + ), + 'hint' => $this->trans( + 'Only words this size or larger will be indexed.', + [], + 'Admin.Shopparameters.Help' + ), + 'validation' => 'isUnsignedInt', + 'type' => 'text', + 'cast' => 'intval', + ], + 'PS_SEARCH_BLACKLIST' => [ + 'title' => $this->trans('Blacklisted words', [], 'Admin.Shopparameters.Feature'), + 'validation' => 'isGenericName', + 'hint' => $this->trans( + 'Please enter the index words separated by a "|".', + [], + 'Admin.Shopparameters.Help' + ), + 'type' => 'textareaLang', + ], + ], + 'submit' => ['title' => $this->trans('Save', [], 'Admin.Actions')], + ], + 'relevance' => [ + 'title' => $this->trans('Weight', [], 'Admin.Shopparameters.Feature'), + 'icon' => 'icon-cogs', + 'info' => $this->trans( + 'The "weight" represents its importance and relevance for the ranking of the products when completing a new search.', + [], + 'Admin.Shopparameters.Feature' + ) . ' + ' . $this->trans( + 'A word with a weight of eight will have four times more value than a word with a weight of two.', + [], + 'Admin.Shopparameters.Feature' + ) . ' + ' . $this->trans( + 'We advise you to set a greater weight for words which appear in the name or reference of a product. This will allow the search results to be as precise and relevant as possible.', + [], + 'Admin.Shopparameters.Feature' + ) . ' + ' . $this->trans( + 'Setting a weight to 0 will exclude that field from search index. Re-build of the entire index is required when changing to or from 0', + [], + 'Admin.Shopparameters.Feature' + ), + 'fields' => [ + 'PS_SEARCH_WEIGHT_PNAME' => [ + 'title' => $this->trans('Product name weight', [], 'Admin.Shopparameters.Feature'), + 'validation' => 'isUnsignedInt', + 'type' => 'text', + 'cast' => 'intval', + ], + 'PS_SEARCH_WEIGHT_REF' => [ + 'title' => $this->trans('Reference weight', [], 'Admin.Shopparameters.Feature'), + 'validation' => 'isUnsignedInt', + 'type' => 'text', + 'cast' => 'intval', + ], + 'PS_SEARCH_WEIGHT_SHORTDESC' => [ + 'title' => $this->trans( + 'Short description weight', + [], + 'Admin.Shopparameters.Feature' + ), + 'validation' => 'isUnsignedInt', + 'type' => 'text', + 'cast' => 'intval', + ], + 'PS_SEARCH_WEIGHT_DESC' => [ + 'title' => $this->trans('Description weight', [], 'Admin.Shopparameters.Feature'), + 'validation' => 'isUnsignedInt', + 'type' => 'text', + 'cast' => 'intval', + ], + 'PS_SEARCH_WEIGHT_CNAME' => [ + 'title' => $this->trans('Category weight', [], 'Admin.Shopparameters.Feature'), + 'validation' => 'isUnsignedInt', + 'type' => 'text', + 'cast' => 'intval', + ], + 'PS_SEARCH_WEIGHT_MNAME' => [ + 'title' => $this->trans('Brand weight', [], 'Admin.Shopparameters.Feature'), + 'validation' => 'isUnsignedInt', + 'type' => 'text', + 'cast' => 'intval', + ], + 'PS_SEARCH_WEIGHT_TAG' => [ + 'title' => $this->trans('Tags weight', [], 'Admin.Shopparameters.Feature'), + 'validation' => 'isUnsignedInt', + 'type' => 'text', + 'cast' => 'intval', + ], + 'PS_SEARCH_WEIGHT_ATTRIBUTE' => [ + 'title' => $this->trans('Attributes weight', [], 'Admin.Shopparameters.Feature'), + 'validation' => 'isUnsignedInt', + 'type' => 'text', + 'cast' => 'intval', + ], + 'PS_SEARCH_WEIGHT_FEATURE' => [ + 'title' => $this->trans('Features weight', [], 'Admin.Shopparameters.Feature'), + 'validation' => 'isUnsignedInt', + 'type' => 'text', + 'cast' => 'intval', + ], + ], + 'submit' => ['title' => $this->trans('Save', [], 'Admin.Actions')], + ], + ]; + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_alias'] = [ + 'href' => self::$currentIndex . '&addalias&token=' . $this->token, + 'desc' => $this->trans('Add new alias', [], 'Admin.Shopparameters.Feature'), + 'icon' => 'process-icon-new', + ]; + } + $this->identifier_name = 'alias'; + parent::initPageHeaderToolbar(); + if ($this->can_import) { + $this->toolbar_btn['import'] = [ + 'href' => $this->context->link->getAdminLink('AdminImport', true) . '&import_type=alias', + 'desc' => $this->trans('Import', [], 'Admin.Actions'), + ]; + } + } + + public function initProcess() + { + parent::initProcess(); + // This is a composite page, we don't want the "options" display mode + if ($this->display == 'options') { + $this->display = ''; + } + } + + /** + * Function used to render the options for this controller. + */ + public function renderOptions() + { + if ($this->fields_options && is_array($this->fields_options)) { + $helper = new HelperOptions($this); + $this->setHelperDisplay($helper); + $helper->toolbar_scroll = true; + $helper->toolbar_btn = ['save' => [ + 'href' => '#', + 'desc' => $this->trans('Save', [], 'Admin.Actions'), + ]]; + $helper->id = $this->id; + $helper->tpl_vars = $this->tpl_option_vars; + $options = $helper->generateOptions($this->fields_options); + + return $options; + } + } + + public function renderForm() + { + $this->fields_form = [ + 'legend' => [ + 'title' => $this->trans('Aliases', [], 'Admin.Shopparameters.Feature'), + 'icon' => 'icon-search', + ], + 'input' => [ + [ + 'type' => 'text', + 'label' => $this->trans('Alias', [], 'Admin.Shopparameters.Feature'), + 'name' => 'alias', + 'required' => true, + 'hint' => [ + $this->trans('Enter each alias separated by a comma (e.g. \'prestshop,preztashop,prestasohp\').', [], 'Admin.Shopparameters.Help'), + $this->trans('Forbidden characters: <>;=#{}', [], 'Admin.Shopparameters.Help'), + ], + ], + [ + 'type' => 'text', + 'label' => $this->trans('Result', [], 'Admin.Shopparameters.Feature'), + 'name' => 'search', + 'required' => true, + 'hint' => $this->trans('Search this word instead.', [], 'Admin.Shopparameters.Help'), + ], + ], + 'submit' => [ + 'title' => $this->trans('Save', [], 'Admin.Actions'), + ], + ]; + + $this->fields_value = ['alias' => $this->object->getAliases()]; + + return parent::renderForm(); + } + + public function processSave() + { + $search = (string) Tools::getValue('search'); + $string = (string) Tools::getValue('alias'); + $aliases = explode(',', $string); + if (empty($search) || empty($string)) { + $this->errors[] = $this->trans('Aliases and results are both required.', [], 'Admin.Shopparameters.Notification'); + } + if (!Validate::isValidSearch($search)) { + $this->errors[] = Tools::safeOutput($search) . ' ' . $this->trans('Is not a valid result', [], 'Admin.Shopparameters.Notification'); + } + foreach ($aliases as $alias) { + if (!Validate::isValidSearch($alias)) { + $this->errors[] = Tools::safeOutput($alias) . ' ' . $this->trans('Is not a valid alias', [], 'Admin.Shopparameters.Notification'); + } + } + + if (!count($this->errors)) { + foreach ($aliases as $alias) { + $obj = new Alias(null, trim($alias), trim($search)); + $obj->save(); + } + } + + if (empty($this->errors)) { + $this->confirmations[] = $this->trans('Creation successful', [], 'Admin.Shopparameters.Notification'); + } + } + + /** + * Retrieve a part of the cookie key for token check. (needs to be static). + * + * @return string Token + */ + private function getTokenForCron() + { + return substr( + _COOKIE_KEY_, + AdminSearchController::TOKEN_CHECK_START_POS, + AdminSearchController::TOKEN_CHECK_LENGTH + ); + } +} diff --git a/controllers/admin/AdminSearchController.php b/controllers/admin/AdminSearchController.php new file mode 100644 index 00000000..34f01c24 --- /dev/null +++ b/controllers/admin/AdminSearchController.php @@ -0,0 +1,525 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ +class AdminSearchControllerCore extends AdminController +{ + const TOKEN_CHECK_START_POS = 34; + const TOKEN_CHECK_LENGTH = 8; + + public function __construct() + { + $this->bootstrap = true; + parent::__construct(); + } + + /** + * {@inheritdoc} + */ + public function init() + { + if ($this->isCronTask() + && substr( + _COOKIE_KEY_, + static::TOKEN_CHECK_START_POS, + static::TOKEN_CHECK_LENGTH + ) === Tools::getValue('token') + ) { + $this->setAllowAnonymous(true); + } + + parent::init(); + } + + public function getTabSlug() + { + return 'ROLE_MOD_TAB_ADMINSEARCHCONF_'; + } + + public function postProcess() + { + $this->context = Context::getContext(); + $this->query = trim(Tools::getValue('bo_query')); + $searchType = (int) Tools::getValue('bo_search_type'); + + /* 1.6 code compatibility, as we use HelperList, we need to handle click to go to product */ + $action = Tools::getValue('action'); + if ($action == 'redirectToProduct') { + $id_product = (int) Tools::getValue('id_product'); + $link = $this->context->link->getAdminLink('AdminProducts', false, ['id_product' => $id_product]); + Tools::redirectAdmin($link); + } + + /* Handle empty search field */ + if (!empty($this->query)) { + if (!$searchType && strlen($this->query) > 1) { + $this->searchFeatures(); + } + + /* Product research */ + if (!$searchType || $searchType == 1) { + /* Handle product ID */ + if ($searchType == 1 && (int) $this->query && Validate::isUnsignedInt((int) $this->query)) { + if (($product = new Product($this->query)) && Validate::isLoadedObject($product)) { + Tools::redirectAdmin('index.php?tab=AdminProducts&id_product=' . (int) ($product->id) . '&token=' . Tools::getAdminTokenLite('AdminProducts')); + } + } + + /* Normal catalog search */ + $this->searchCatalog(); + } + + /* Customer */ + if (!$searchType || $searchType == 2 || $searchType == 6) { + if (!$searchType || $searchType == 2) { + /* Handle customer ID */ + if ($searchType && (int) $this->query && Validate::isUnsignedInt((int) $this->query)) { + if (($customer = new Customer($this->query)) && Validate::isLoadedObject($customer)) { + Tools::redirectAdmin($this->context->link->getAdminLink( + 'AdminCustomers', + true, + [], + [ + 'id_customer' => $customer->id, + 'viewcustomer' => 1, + ] + )); + } + } + + /* Normal customer search */ + $this->searchCustomer(); + } + + if ($searchType == 6) { + $this->searchIP(); + } + } + + /* Order */ + if (!$searchType || $searchType == 3) { + if (Validate::isUnsignedInt(trim($this->query)) && (int) $this->query && ($order = new Order((int) $this->query)) && Validate::isLoadedObject($order)) { + if ($searchType == 3) { + Tools::redirectAdmin('index.php?tab=AdminOrders&id_order=' . (int) $order->id . '&vieworder' . '&token=' . Tools::getAdminTokenLite('AdminOrders')); + } else { + $row = get_object_vars($order); + $row['id_order'] = $row['id']; + $customer = $order->getCustomer(); + $row['customer'] = $customer->firstname . ' ' . $customer->lastname; + $order_state = $order->getCurrentOrderState(); + $row['osname'] = $order_state->name[$this->context->language->id]; + $this->_list['orders'] = [$row]; + } + } else { + $orders = Order::getByReference($this->query); + $nb_orders = count($orders); + if ($nb_orders == 1 && $searchType == 3) { + Tools::redirectAdmin('index.php?tab=AdminOrders&id_order=' . (int) $orders[0]->id . '&vieworder' . '&token=' . Tools::getAdminTokenLite('AdminOrders')); + } elseif ($nb_orders) { + $this->_list['orders'] = []; + foreach ($orders as $order) { + /** @var Order $order */ + $row = get_object_vars($order); + $row['id_order'] = $row['id']; + $customer = $order->getCustomer(); + $row['customer'] = $customer->firstname . ' ' . $customer->lastname; + $order_state = $order->getCurrentOrderState(); + $row['osname'] = $order_state->name[$this->context->language->id]; + $this->_list['orders'][] = $row; + } + } elseif ($searchType == 3) { + $this->errors[] = $this->trans('No order was found with this ID:', [], 'Admin.Orderscustomers.Notification') . ' ' . Tools::htmlentitiesUTF8($this->query); + } + } + } + + /* Invoices */ + if ($searchType == 4) { + if (Validate::isOrderInvoiceNumber($this->query) && ($invoice = OrderInvoice::getInvoiceByNumber($this->query))) { + Tools::redirectAdmin($this->context->link->getAdminLink('AdminPdf') . '&submitAction=generateInvoicePDF&id_order=' . (int) ($invoice->id_order)); + } + $this->errors[] = $this->trans('No invoice was found with this ID:', [], 'Admin.Orderscustomers.Notification') . ' ' . Tools::htmlentitiesUTF8($this->query); + } + + /* Cart */ + if ($searchType == 5) { + if ((int) $this->query && Validate::isUnsignedInt((int) $this->query) && ($cart = new Cart($this->query)) && Validate::isLoadedObject($cart)) { + Tools::redirectAdmin('index.php?tab=AdminCarts&id_cart=' . (int) ($cart->id) . '&viewcart' . '&token=' . Tools::getAdminToken('AdminCarts' . (int) (Tab::getIdFromClassName('AdminCarts')) . (int) $this->context->employee->id)); + } + $this->errors[] = $this->trans('No cart was found with this ID:', [], 'Admin.Orderscustomers.Notification') . ' ' . Tools::htmlentitiesUTF8($this->query); + } + /* IP */ + // 6 - but it is included in the customer block + + /* Module search */ + if (!$searchType || $searchType == 7) { + /* Handle module name */ + if ($searchType == 7 && Validate::isModuleName($this->query) && ($module = Module::getInstanceByName($this->query)) && Validate::isLoadedObject($module)) { + Tools::redirectAdmin('index.php?tab=AdminModules&tab_module=' . $module->tab . '&module_name=' . $module->name . '&anchor=' . ucfirst($module->name) . '&token=' . Tools::getAdminTokenLite('AdminModules')); + } + + /* Normal catalog search */ + $this->searchModule(); + } + } + $this->display = 'view'; + } + + public function searchIP() + { + if (!ip2long(trim($this->query))) { + $this->errors[] = $this->trans('This is not a valid IP address:', [], 'Admin.Shopparameters.Notification') . ' ' . Tools::htmlentitiesUTF8($this->query); + + return; + } + $this->_list['customers'] = Customer::searchByIp($this->query); + } + + /** + * Search a specific string in the products and categories. + * + * @param string $query String to find in the catalog + */ + public function searchCatalog() + { + $this->context = Context::getContext(); + $this->_list['products'] = Product::searchByName($this->context->language->id, $this->query); + $this->_list['categories'] = Category::searchByName($this->context->language->id, $this->query); + } + + /** + * Search a specific name in the customers. + * + * @param string $query String to find in the catalog + */ + public function searchCustomer() + { + $this->_list['customers'] = Customer::searchByName($this->query); + } + + public function searchModule() + { + $this->_list['modules'] = []; + $all_modules = Module::getModulesOnDisk(true, true, Context::getContext()->employee->id); + foreach ($all_modules as $module) { + if (stripos($module->name, $this->query) !== false || stripos($module->displayName, $this->query) !== false || stripos($module->description, $this->query) !== false) { + $module->linkto = 'index.php?tab=AdminModules&tab_module=' . $module->tab . '&module_name=' . $module->name . '&anchor=' . ucfirst($module->name) . '&token=' . Tools::getAdminTokenLite('AdminModules'); + $this->_list['modules'][] = $module; + } + } + + if (!is_numeric(trim($this->query)) && !Validate::isEmail($this->query)) { + $iso_lang = Tools::strtolower(Context::getContext()->language->iso_code); + $iso_country = Tools::strtolower(Country::getIsoById(Configuration::get('PS_COUNTRY_DEFAULT'))); + if (($json_content = Tools::file_get_contents('https://api-addons.prestashop.com/' . _PS_VERSION_ . '/search/' . urlencode($this->query) . '/' . $iso_country . '/' . $iso_lang . '/')) != false) { + $results = json_decode($json_content, true); + if (isset($results['id'])) { + $this->_list['addons'] = [$results]; + } else { + $this->_list['addons'] = $results; + } + } + } + } + + /** + * Search a feature in all store. + * + * @param string $query String to find in the catalog + */ + public function searchFeatures() + { + $this->_list['features'] = []; + + global $_LANGADM; + if ($_LANGADM === null) { + return; + } + + $tabs = []; + $key_match = []; + $result = Db::getInstance()->executeS( + ' + SELECT class_name, name + FROM ' . _DB_PREFIX_ . 'tab t + INNER JOIN ' . _DB_PREFIX_ . 'tab_lang tl ON (t.id_tab = tl.id_tab AND tl.id_lang = ' . (int) $this->context->employee->id_lang . ') + WHERE active = 1' . (defined('_PS_HOST_MODE_') ? ' AND t.`hide_host_mode` = 0' : '') + ); + foreach ($result as $row) { + if (Access::isGranted('ROLE_MOD_TAB_' . strtoupper($row['class_name']) . '_READ', $this->context->employee->id_profile)) { + $tabs[strtolower($row['class_name'])] = $row['name']; + $key_match[strtolower($row['class_name'])] = $row['class_name']; + } + } + + $this->_list['features'] = []; + foreach ($_LANGADM as $key => $value) { + if (stripos($value, $this->query) !== false) { + $value = stripslashes($value); + $key = strtolower(substr($key, 0, -32)); + if (in_array($key, ['AdminTab', 'index'])) { + continue; + } + // if class name doesn't exists, just ignore it + if (!isset($tabs[$key])) { + continue; + } + if (!isset($this->_list['features'][$tabs[$key]])) { + $this->_list['features'][$tabs[$key]] = []; + } + $this->_list['features'][$tabs[$key]][] = ['link' => Context::getContext()->link->getAdminLink($key_match[$key]), 'value' => Tools::safeOutput($value)]; + } + } + } + + protected function initOrderList() + { + $this->fields_list['orders'] = [ + 'reference' => ['title' => $this->trans('Reference', [], 'Admin.Global'), 'align' => 'center', 'width' => 65], + 'id_order' => ['title' => $this->trans('ID', [], 'Admin.Global'), 'align' => 'center', 'width' => 25], + 'customer' => ['title' => $this->trans('Customer', [], 'Admin.Global')], + 'total_paid_tax_incl' => ['title' => $this->trans('Total', [], 'Admin.Global'), 'width' => 70, 'align' => 'right', 'type' => 'price', 'currency' => true], + 'payment' => ['title' => $this->trans('Payment', [], 'Admin.Global'), 'width' => 100], + 'osname' => ['title' => $this->trans('Status', [], 'Admin.Global'), 'width' => 280], + 'date_add' => ['title' => $this->trans('Date', [], 'Admin.Global'), 'width' => 130, 'align' => 'right', 'type' => 'datetime'], + ]; + } + + protected function initCustomerList() + { + $genders_icon = ['default' => 'unknown.gif']; + $genders = [0 => $this->trans('?', [], 'Admin.Global')]; + foreach (Gender::getGenders() as $gender) { + /* @var Gender $gender */ + $genders_icon[$gender->id] = '../genders/' . (int) $gender->id . '.jpg'; + $genders[$gender->id] = $gender->name; + } + $this->fields_list['customers'] = ([ + 'id_customer' => ['title' => $this->trans('ID', [], 'Admin.Global'), 'align' => 'center', 'width' => 25], + 'id_gender' => ['title' => $this->trans('Social title', [], 'Admin.Global'), 'align' => 'center', 'icon' => $genders_icon, 'list' => $genders, 'width' => 25], + 'firstname' => ['title' => $this->trans('First name', [], 'Admin.Global'), 'align' => 'left', 'width' => 150], + 'lastname' => ['title' => $this->trans('Name', [], 'Admin.Global'), 'align' => 'left', 'width' => 'auto'], + 'email' => ['title' => $this->trans('Email address', [], 'Admin.Global'), 'align' => 'left', 'width' => 250], + 'company' => ['title' => $this->trans('Company', [], 'Admin.Global'), 'align' => 'left', 'width' => 150], + 'birthday' => ['title' => $this->trans('Birth date', [], 'Admin.Global'), 'align' => 'center', 'type' => 'date', 'width' => 75], + 'date_add' => ['title' => $this->trans('Registration date', [], 'Admin.Shopparameters.Feature'), 'align' => 'center', 'type' => 'date', 'width' => 75], + 'orders' => ['title' => $this->trans('Orders', [], 'Admin.Global'), 'align' => 'center', 'width' => 50], + 'active' => ['title' => $this->trans('Enabled', [], 'Admin.Global'), 'align' => 'center', 'active' => 'status', 'type' => 'bool', 'width' => 25], + ]); + } + + protected function initProductList() + { + $this->show_toolbar = false; + $this->fields_list['products'] = [ + 'id_product' => ['title' => $this->trans('ID', [], 'Admin.Global'), 'width' => 25], + 'manufacturer_name' => ['title' => $this->trans('Brand', [], 'Admin.Global'), 'align' => 'center', 'width' => 200], + 'reference' => ['title' => $this->trans('Reference', [], 'Admin.Global'), 'align' => 'center', 'width' => 150], + 'name' => ['title' => $this->trans('Name', [], 'Admin.Global'), 'width' => 'auto'], + 'price_tax_excl' => ['title' => $this->trans('Price (tax excl.)', [], 'Admin.Catalog.Feature'), 'align' => 'right', 'type' => 'price', 'width' => 60], + 'price_tax_incl' => ['title' => $this->trans('Price (tax incl.)', [], 'Admin.Catalog.Feature'), 'align' => 'right', 'type' => 'price', 'width' => 60], + 'active' => ['title' => $this->trans('Active', [], 'Admin.Global'), 'width' => 70, 'active' => 'status', 'align' => 'center', 'type' => 'bool'], + ]; + } + + public function setMedia($isNewTheme = false) + { + parent::setMedia($isNewTheme); + $this->addJqueryPlugin('highlight'); + } + + /* Override because we don't want any buttons */ + public function initToolbar() + { + } + + public function initToolbarTitle() + { + $this->toolbar_title = $this->trans('Search results', [], 'Admin.Global'); + } + + public function renderView() + { + $this->tpl_view_vars['query'] = Tools::safeOutput($this->query); + $this->tpl_view_vars['show_toolbar'] = true; + + if (count($this->errors)) { + return parent::renderView(); + } else { + $nb_results = 0; + foreach ($this->_list as $list) { + if ($list != false) { + $nb_results += count($list); + } + } + $this->tpl_view_vars['nb_results'] = $nb_results; + + if ($this->isCountableAndNotEmpty($this->_list, 'features')) { + $this->tpl_view_vars['features'] = $this->_list['features']; + } + + if ($this->isCountableAndNotEmpty($this->_list, 'categories')) { + $categories = []; + foreach ($this->_list['categories'] as $category) { + $categories[] = Tools::getPath( + $this->context->link->getAdminLink('AdminCategories', false), + $category['id_category'] + ); + } + $this->tpl_view_vars['categories'] = $categories; + } + + if ($this->isCountableAndNotEmpty($this->_list, 'products')) { + $view = ''; + $this->initProductList(); + + $helper = new HelperList(); + $helper->shopLinkType = ''; + $helper->simple_header = true; + $helper->identifier = 'id_product'; + $helper->actions = ['edit']; + $helper->show_toolbar = false; + $helper->table = 'product'; + /* 1.6 code compatibility, as we use HelperList, we need to handle click to go to product, a better way need to be find */ + $helper->currentIndex = $this->context->link->getAdminLink('AdminSearch', false); + $helper->currentIndex .= '&action=redirectToProduct'; + + $query = trim(Tools::getValue('bo_query')); + $searchType = (int) Tools::getValue('bo_search_type'); + + if ($query) { + $helper->currentIndex .= '&bo_query=' . $query . '&bo_search_type=' . $searchType; + } + + $helper->token = Tools::getAdminTokenLite('AdminSearch'); + + if ($this->_list['products']) { + $view = $helper->generateList($this->_list['products'], $this->fields_list['products']); + } + + $this->tpl_view_vars['products'] = $view; + $this->tpl_view_vars['productsCount'] = count($this->_list['products']); + } + + if ($this->isCountableAndNotEmpty($this->_list, 'customers')) { + $view = ''; + $this->initCustomerList(); + + $helper = new HelperList(); + $helper->shopLinkType = ''; + $helper->simple_header = true; + $helper->identifier = 'id_customer'; + $helper->actions = ['edit', 'view']; + $helper->show_toolbar = false; + $helper->table = 'customer'; + $helper->currentIndex = $this->context->link->getAdminLink('AdminCustomers', false); + $helper->token = Tools::getAdminTokenLite('AdminCustomers'); + + foreach ($this->_list['customers'] as $key => $val) { + $this->_list['customers'][$key]['orders'] = Order::getCustomerNbOrders((int) $val['id_customer']); + } + + $view = $helper->generateList($this->_list['customers'], $this->fields_list['customers']); + $this->tpl_view_vars['customers'] = $view; + $this->tpl_view_vars['customerCount'] = count($this->_list['customers']); + } + + if ($this->isCountableAndNotEmpty($this->_list, 'orders')) { + $view = ''; + $this->initOrderList(); + + $helper = new HelperList(); + $helper->shopLinkType = ''; + $helper->simple_header = true; + $helper->identifier = 'id_order'; + $helper->actions = ['view']; + $helper->show_toolbar = false; + $helper->table = 'order'; + $helper->currentIndex = $this->context->link->getAdminLink('AdminOrders', false); + $helper->token = Tools::getAdminTokenLite('AdminOrders'); + + $view = $helper->generateList($this->_list['orders'], $this->fields_list['orders']); + $this->tpl_view_vars['orders'] = $view; + $this->tpl_view_vars['orderCount'] = count($this->_list['orders']); + } + + if ($this->isCountableAndNotEmpty($this->_list, 'modules')) { + $this->tpl_view_vars['modules'] = $this->_list['modules']; + } + + if ($this->isCountableAndNotEmpty($this->_list, 'addons')) { + $this->tpl_view_vars['addons'] = $this->_list['addons']; + } + + return parent::renderView(); + } + } + + /** + * Check if key is present in array, is countable and has data. + * + * @param array $array Array + * @param string $key Key + * + * @return bool + */ + protected function isCountableAndNotEmpty(array $array, $key) + { + return isset($array[$key]) && + is_countable($array[$key]) && + count($array[$key]); + } + + /** + * Request triggering the search indexation. + * + * Kept as GET request for backward compatibility purpose, but should be modified as POST when migrated. + * NOTE the token is different for that method, check the method checkToken() for more details. + */ + public function displayAjaxSearchCron() + { + if (!Tools::getValue('id_shop')) { + Context::getContext()->shop->setContext(Shop::CONTEXT_ALL); + } else { + Context::getContext()->shop->setContext(Shop::CONTEXT_SHOP, (int) Tools::getValue('id_shop')); + } + + // Considering the indexing task can be really long, we ask the PHP process to not stop before 2 hours. + ini_set('max_execution_time', 7200); + Search::indexation(Tools::getValue('full')); + if (Tools::getValue('redirect')) { + Tools::redirectAdmin($_SERVER['HTTP_REFERER'] . '&conf=4'); + } + } + + /** + * Check if a task is a cron task + * + * @return bool + */ + protected function isCronTask() + { + return Tools::isSubmit('action') && 'searchCron' === Tools::getValue('action'); + } +} diff --git a/controllers/admin/AdminSearchEnginesController.php b/controllers/admin/AdminSearchEnginesController.php new file mode 100644 index 00000000..4429431f --- /dev/null +++ b/controllers/admin/AdminSearchEnginesController.php @@ -0,0 +1,102 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +/** + * @property SearchEngine $object + */ +class AdminSearchEnginesControllerCore extends AdminController +{ + public function __construct() + { + $this->bootstrap = true; + $this->table = 'search_engine'; + $this->className = 'SearchEngine'; + $this->lang = false; + + parent::__construct(); + + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + if (!Tools::getValue('realedit')) { + $this->deleted = false; + } + + $this->bulk_actions = [ + 'delete' => [ + 'text' => $this->trans('Delete selected', [], 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', [], 'Admin.Notifications.Warning'), + 'icon' => 'icon-trash', + ], + ]; + + $this->fields_list = [ + 'id_search_engine' => ['title' => $this->trans('ID', [], 'Admin.Global'), 'width' => 25], + 'server' => ['title' => $this->trans('Server', [], 'Admin.Shopparameters.Feature')], + 'getvar' => ['title' => $this->trans('GET variable', [], 'Admin.Shopparameters.Feature'), 'width' => 100], + ]; + + $this->fields_form = [ + 'legend' => [ + 'title' => $this->trans('Referrer', [], 'Admin.Shopparameters.Feature'), + ], + 'input' => [ + [ + 'type' => 'text', + 'label' => $this->trans('Server', [], 'Admin.Shopparameters.Feature'), + 'name' => 'server', + 'size' => 20, + 'required' => true, + ], + [ + 'type' => 'text', + 'label' => $this->trans('$_GET variable', [], 'Admin.Shopparameters.Feature'), + 'name' => 'getvar', + 'size' => 40, + 'required' => true, + ], + ], + 'submit' => [ + 'title' => $this->trans('Save', [], 'Admin.Actions'), + ], + ]; + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_search_engine'] = [ + 'href' => self::$currentIndex . '&addsearch_engine&token=' . $this->token, + 'desc' => $this->trans('Add new search engine', [], 'Admin.Shopparameters.Feature'), + 'icon' => 'process-icon-new', + ]; + } + + $this->identifier_name = 'server'; + + parent::initPageHeaderToolbar(); + } +} diff --git a/controllers/admin/AdminShopController.php b/controllers/admin/AdminShopController.php new file mode 100644 index 00000000..c16c05eb --- /dev/null +++ b/controllers/admin/AdminShopController.php @@ -0,0 +1,864 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ +use PrestaShop\PrestaShop\Core\Addon\Theme\ThemeManagerBuilder; + +class AdminShopControllerCore extends AdminController +{ + public function __construct() + { + $this->bootstrap = true; + $this->table = 'shop'; + $this->className = 'Shop'; + $this->multishop_context = Shop::CONTEXT_ALL; + + parent::__construct(); + + $this->id_shop_group = (int) Tools::getValue('id_shop_group'); + + /* if $_GET['id_shop'] is transmitted, virtual url can be loaded in config.php, so we wether transmit shop_id in herfs */ + if ($this->id_shop = (int) Tools::getValue('shop_id')) { + $_GET['id_shop'] = $this->id_shop; + } + + $this->list_skip_actions['delete'] = [(int) Configuration::get('PS_SHOP_DEFAULT')]; + $this->fields_list = [ + 'id_shop' => [ + 'title' => $this->trans('Shop ID', [], 'Admin.Shopparameters.Feature'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ], + 'name' => [ + 'title' => $this->trans('Shop name', [], 'Admin.Shopparameters.Feature'), + 'filter_key' => 'a!name', + 'width' => 200, + ], + 'shop_group_name' => [ + 'title' => $this->trans('Shop group', [], 'Admin.Shopparameters.Feature'), + 'width' => 150, + 'filter_key' => 'gs!name', + ], + 'category_name' => [ + 'title' => $this->trans('Root category', [], 'Admin.Shopparameters.Feature'), + 'width' => 150, + 'filter_key' => 'cl!name', + ], + 'url' => [ + 'title' => $this->trans('Main URL for this shop', [], 'Admin.Shopparameters.Feature'), + 'havingFilter' => 'url', + ], + ]; + } + + public function getTabSlug() + { + return 'ROLE_MOD_TAB_ADMINSHOPGROUP_'; + } + + public function viewAccess($disable = false) + { + return Configuration::get('PS_MULTISHOP_FEATURE_ACTIVE'); + } + + public function initPageHeaderToolbar() + { + parent::initPageHeaderToolbar(); + + if (!$this->display && $this->id_shop_group) { + if ($this->id_object) { + $this->loadObject(); + } + + if (!$this->id_shop_group && $this->object && $this->object->id_shop_group) { + $this->id_shop_group = $this->object->id_shop_group; + } + + $this->page_header_toolbar_btn['edit'] = [ + 'desc' => $this->trans('Edit this shop group', [], 'Admin.Shopparameters.Feature'), + 'href' => $this->context->link->getAdminLink('AdminShopGroup') . '&updateshop_group&id_shop_group=' + . $this->id_shop_group, + ]; + + $this->page_header_toolbar_btn['new'] = [ + 'desc' => $this->trans('Add new shop', [], 'Admin.Shopparameters.Feature'), + 'href' => $this->context->link->getAdminLink('AdminShop') . '&add' . $this->table . '&id_shop_group=' + . $this->id_shop_group, + ]; + } + } + + public function initToolbar() + { + parent::initToolbar(); + + if ($this->display != 'edit' && $this->display != 'add') { + if ($this->id_object) { + $this->loadObject(); + } + + if (!$this->id_shop_group && $this->object && $this->object->id_shop_group) { + $this->id_shop_group = $this->object->id_shop_group; + } + + $this->toolbar_btn['new'] = [ + 'desc' => $this->trans('Add new shop', [], 'Admin.Shopparameters.Feature'), + 'href' => $this->context->link->getAdminLink('AdminShop') . '&add' . $this->table . '&id_shop_group=' + . $this->id_shop_group, + ]; + } + } + + public function initContent() + { + parent::initContent(); + + $this->addJqueryPlugin('cooki-plugin'); + $data = Shop::getTree(); + + foreach ($data as &$group) { + foreach ($group['shops'] as &$shop) { + $current_shop = new Shop($shop['id_shop']); + $urls = $current_shop->getUrls(); + + foreach ($urls as &$url) { + $title = $url['domain'] . $url['physical_uri'] . $url['virtual_uri']; + if (strlen($title) > 23) { + $title = substr($title, 0, 23) . '...'; + } + + $url['name'] = $title; + $shop['urls'][$url['id_shop_url']] = $url; + } + } + } + + $shops_tree = new HelperTreeShops('shops-tree', $this->trans('Multistore tree', [], 'Admin.Shopparameters.Feature')); + $shops_tree->setNodeFolderTemplate('shop_tree_node_folder.tpl')->setNodeItemTemplate('shop_tree_node_item.tpl') + ->setHeaderTemplate('shop_tree_header.tpl')->setActions([ + new TreeToolbarLink( + 'Collapse All', + '#', + '$(\'#' . $shops_tree->getId() . '\').tree(\'collapseAll\'); return false;', + 'icon-collapse-alt' + ), + new TreeToolbarLink( + 'Expand All', + '#', + '$(\'#' . $shops_tree->getId() . '\').tree(\'expandAll\'); return false;', + 'icon-expand-alt' + ), + ]) + ->setAttribute('url_shop_group', $this->context->link->getAdminLink('AdminShopGroup')) + ->setAttribute('url_shop', $this->context->link->getAdminLink('AdminShop')) + ->setAttribute('url_shop_url', $this->context->link->getAdminLink('AdminShopUrl')) + ->setData($data); + $shops_tree = $shops_tree->render(null, false, false); + + if ($this->display == 'edit') { + $this->toolbar_title[] = $this->object->name; + } elseif (!$this->display && $this->id_shop_group) { + $group = new ShopGroup($this->id_shop_group); + $this->toolbar_title[] = $group->name; + } + + $this->context->smarty->assign([ + 'toolbar_scroll' => 1, + 'toolbar_btn' => $this->toolbar_btn, + 'title' => $this->toolbar_title, + 'shops_tree' => $shops_tree, + ]); + } + + public function renderList() + { + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + $this->_select = 'gs.name shop_group_name, cl.name category_name, CONCAT(\'http://\', su.domain, su.physical_uri, su.virtual_uri) AS url'; + $this->_join = ' + LEFT JOIN `' . _DB_PREFIX_ . 'shop_group` gs + ON (a.id_shop_group = gs.id_shop_group) + LEFT JOIN `' . _DB_PREFIX_ . 'category_lang` cl + ON (a.id_category = cl.id_category AND cl.id_lang=' . (int) $this->context->language->id . ') + LEFT JOIN ' . _DB_PREFIX_ . 'shop_url su + ON a.id_shop = su.id_shop AND su.main = 1 + '; + $this->_group = 'GROUP BY a.id_shop'; + + if ($id_shop_group = (int) Tools::getValue('id_shop_group')) { + $this->_where = 'AND a.id_shop_group = ' . $id_shop_group; + } + + return parent::renderList(); + } + + public function displayAjaxGetCategoriesFromRootCategory() + { + if (Tools::isSubmit('id_category')) { + $selected_cat = [(int) Tools::getValue('id_category')]; + $children = Category::getChildren((int) Tools::getValue('id_category'), $this->context->language->id); + foreach ($children as $child) { + $selected_cat[] = $child['id_category']; + } + + $helper = new HelperTreeCategories('categories-tree', null, (int) Tools::getValue('id_category'), null, false); + $this->content = $helper->setSelectedCategories($selected_cat)->setUseSearch(true)->setUseCheckBox(true) + ->render(); + } + parent::displayAjax(); + } + + public function postProcess() + { + if (Tools::isSubmit('id_category_default')) { + $_POST['id_category'] = Tools::getValue('id_category_default'); + } + + if (Tools::isSubmit('submitAddshopAndStay') || Tools::isSubmit('submitAddshop')) { + $shop_group = new ShopGroup((int) Tools::getValue('id_shop_group')); + if ($shop_group->shopNameExists(Tools::getValue('name'), (int) Tools::getValue('id_shop'))) { + $this->errors[] = $this->trans('You cannot have two shops with the same name in the same group.', [], 'Admin.Advparameters.Notification'); + } + } + + if (count($this->errors)) { + return false; + } + + /** @var Shop|bool $result */ + $result = parent::postProcess(); + + if ($result != false && (Tools::isSubmit('submitAddshopAndStay') || Tools::isSubmit('submitAddshop')) && (int) $result->id_category != (int) Configuration::get('PS_HOME_CATEGORY', null, null, (int) $result->id)) { + Configuration::updateValue('PS_HOME_CATEGORY', (int) $result->id_category, false, null, (int) $result->id); + } + + if ($this->redirect_after) { + $this->redirect_after .= '&id_shop_group=' . $this->id_shop_group; + } + + return $result; + } + + public function processDelete() + { + if (!Validate::isLoadedObject($object = $this->loadObject())) { + $this->errors[] = $this->trans('Unable to load this shop.', [], 'Admin.Advparameters.Notification'); + } elseif (!Shop::hasDependency($object->id)) { + $result = Category::deleteCategoriesFromShop($object->id) && parent::processDelete(); + Tools::generateHtaccess(); + + return $result; + } else { + $this->errors[] = $this->trans('You cannot delete this shop (customer and/or order dependency).', [], 'Admin.Shopparameters.Notification'); + } + + return false; + } + + /** + * @param Shop $new_shop + * + * @return bool + */ + protected function afterAdd($new_shop) + { + $import_data = Tools::getValue('importData', []); + + // The root category should be at least imported + $new_shop->copyShopData((int) Tools::getValue('importFromShop'), $import_data); + + // copy default data + if (!Tools::getValue('useImportData') || (is_array($import_data) && !isset($import_data['group']))) { + $sql = 'INSERT INTO `' . _DB_PREFIX_ . 'group_shop` (`id_shop`, `id_group`) + VALUES + (' . (int) $new_shop->id . ', ' . (int) Configuration::get('PS_UNIDENTIFIED_GROUP') . '), + (' . (int) $new_shop->id . ', ' . (int) Configuration::get('PS_GUEST_GROUP') . '), + (' . (int) $new_shop->id . ', ' . (int) Configuration::get('PS_CUSTOMER_GROUP') . ') + '; + Db::getInstance()->execute($sql); + } + + return parent::afterAdd($new_shop); + } + + /** + * @param Shop $new_shop + * + * @return bool + */ + protected function afterUpdate($new_shop) + { + $categories = Tools::getValue('categoryBox'); + + if (!is_array($categories)) { + $this->errors[] = $this->trans('Please create some sub-categories for this root category.', [], 'Admin.Shopparameters.Notification'); + + return false; + } + + array_unshift($categories, Configuration::get('PS_ROOT_CATEGORY')); + + if (!Category::updateFromShop($categories, $new_shop->id)) { + $this->errors[] = $this->trans('You need to select at least the root category.', [], 'Admin.Shopparameters.Notification'); + } + if (Tools::getValue('useImportData') && ($import_data = Tools::getValue('importData')) && is_array($import_data)) { + $new_shop->copyShopData((int) Tools::getValue('importFromShop'), $import_data); + } + + if (Tools::isSubmit('submitAddshopAndStay') || Tools::isSubmit('submitAddshop')) { + $this->redirect_after = self::$currentIndex . '&shop_id=' . (int) $new_shop->id . '&conf=4&token=' . $this->token; + } + + return parent::afterUpdate($new_shop); + } + + public function getList($id_lang, $order_by = null, $order_way = null, $start = 0, $limit = null, $id_lang_shop = false) + { + if (Shop::getContext() == Shop::CONTEXT_GROUP) { + $this->_where .= ' AND a.id_shop_group = ' . (int) Shop::getContextShopGroupID(); + } + + parent::getList($id_lang, $order_by, $order_way, $start, $limit, $id_lang_shop); + $shop_delete_list = []; + + // don't allow to remove shop which have dependencies (customers / orders / ... ) + foreach ($this->_list as &$shop) { + if (Shop::hasDependency($shop['id_shop'])) { + $shop_delete_list[] = $shop['id_shop']; + } + } + $this->context->smarty->assign('shops_having_dependencies', $shop_delete_list); + } + + public function renderForm() + { + /** @var Shop $obj */ + if (!($obj = $this->loadObject(true))) { + return; + } + + $this->fields_form = [ + 'legend' => [ + 'title' => $this->trans('Shop', [], 'Admin.Global'), + 'icon' => 'icon-shopping-cart', + ], + 'identifier' => 'shop_id', + 'input' => [ + [ + 'type' => 'text', + 'label' => $this->trans('Shop name', [], 'Admin.Shopparameters.Feature'), + 'desc' => [ + $this->trans('This field does not refer to the shop name visible in the front office.', [], 'Admin.Shopparameters.Help'), + $this->trans('Follow [1]this link[/1] to edit the shop name used on the front office.', [ + '[1]' => '', + '[/1]' => '', + ], 'Admin.Shopparameters.Help'), ], + 'name' => 'name', + 'required' => true, + ], + ], + ]; + + $display_group_list = true; + if ($this->display == 'edit') { + $group = new ShopGroup($obj->id_shop_group); + if ($group->share_customer || $group->share_order || $group->share_stock) { + $display_group_list = false; + } + } + + if ($display_group_list) { + $options = []; + foreach (ShopGroup::getShopGroups() as $group) { + /** @var ShopGroup $group */ + if ($this->display == 'edit' && ($group->share_customer || $group->share_order || $group->share_stock) && ShopGroup::hasDependency($group->id)) { + continue; + } + + $options[] = [ + 'id_shop_group' => $group->id, + 'name' => $group->name, + ]; + } + + if ($this->display == 'add') { + $group_desc = $this->trans('Warning: You won\'t be able to change the group of this shop if this shop belongs to a group with one of these options activated: Share Customers, Share Quantities or Share Orders.', [], 'Admin.Shopparameters.Notification'); + } else { + $group_desc = $this->trans('You can only move your shop to a shop group with all "share" options disabled -- or to a shop group with no customers/orders.', [], 'Admin.Shopparameters.Notification'); + } + + $this->fields_form['input'][] = [ + 'type' => 'select', + 'label' => $this->trans('Shop group', [], 'Admin.Shopparameters.Feature'), + 'desc' => $group_desc, + 'name' => 'id_shop_group', + 'options' => [ + 'query' => $options, + 'id' => 'id_shop_group', + 'name' => 'name', + ], + ]; + } else { + $this->fields_form['input'][] = [ + 'type' => 'hidden', + 'name' => 'id_shop_group', + 'default' => $group->name, + ]; + $this->fields_form['input'][] = [ + 'type' => 'textShopGroup', + 'label' => $this->trans('Shop group', [], 'Admin.Shopparameters.Feature'), + 'desc' => $this->trans('You can\'t edit the shop group because the current shop belongs to a group with the "share" option enabled.', [], 'Admin.Shopparameters.Help'), + 'name' => 'id_shop_group', + 'value' => $group->name, + ]; + } + + $categories = Category::getRootCategories($this->context->language->id); + $this->fields_form['input'][] = [ + 'type' => 'select', + 'label' => $this->trans('Category root', [], 'Admin.Catalog.Feature'), + 'desc' => $this->trans('This is the root category of the store that you\'ve created. To define a new root category for your store, [1]please click here[/1].', [ + '[1]' => '', + '[/1]' => '', + ], 'Admin.Shopparameters.Help'), + 'name' => 'id_category', + 'options' => [ + 'query' => $categories, + 'id' => 'id_category', + 'name' => 'name', + ], + ]; + + if (Tools::isSubmit('id_shop')) { + $shop = new Shop((int) Tools::getValue('id_shop')); + $id_root = $shop->id_category; + } else { + $id_root = $categories[0]['id_category']; + } + + $id_shop = (int) Tools::getValue('id_shop'); + self::$currentIndex = self::$currentIndex . '&id_shop_group=' . (int) (Tools::getValue('id_shop_group') ? + Tools::getValue('id_shop_group') : (isset($obj->id_shop_group) ? $obj->id_shop_group : Shop::getContextShopGroupID())); + $shop = new Shop($id_shop); + $selected_cat = Shop::getCategories($id_shop); + + if (empty($selected_cat)) { + // get first category root and preselect all these children + $root_categories = Category::getRootCategories(); + $root_category = new Category($root_categories[0]['id_category']); + $children = $root_category->getAllChildren($this->context->language->id); + $selected_cat[] = $root_categories[0]['id_category']; + + foreach ($children as $child) { + $selected_cat[] = $child->id; + } + } + + if (Shop::getContext() == Shop::CONTEXT_SHOP && Tools::isSubmit('id_shop')) { + $root_category = new Category($shop->id_category); + } else { + $root_category = new Category($id_root); + } + + $this->fields_form['input'][] = [ + 'type' => 'categories', + 'name' => 'categoryBox', + 'label' => $this->trans('Associated categories', [], 'Admin.Catalog.Feature'), + 'tree' => [ + 'id' => 'categories-tree', + 'selected_categories' => $selected_cat, + 'root_category' => $root_category->id, + 'use_search' => true, + 'use_checkbox' => true, + ], + 'desc' => $this->trans('By selecting associated categories, you are choosing to share the categories between shops. Once associated between shops, any alteration of this category will impact every shop.', [], 'Admin.Shopparameters.Help'), + ]; + /*$this->fields_form['input'][] = array( + 'type' => 'switch', + 'label' => $this->trans('Enabled', array(), 'Admin.Global'), + 'name' => 'active', + 'required' => true, + 'is_bool' => true, + 'values' => array( + array( + 'id' => 'active_on', + 'value' => 1 + ), + array( + 'id' => 'active_off', + 'value' => 0 + ) + ), + 'desc' => $this->trans('Enable or disable your store?', array(), 'Admin.Shopparameters.Help') + );*/ + + $themes = (new ThemeManagerBuilder($this->context, Db::getInstance())) + ->buildRepository() + ->getList(); + + $this->fields_form['input'][] = [ + 'type' => 'theme', + 'label' => $this->trans('Theme', [], 'Admin.Design.Feature'), + 'name' => 'theme', + 'values' => $themes, + ]; + + $this->fields_form['submit'] = [ + 'title' => $this->trans('Save', [], 'Admin.Actions'), + ]; + + if (Shop::getTotalShops() > 1 && $obj->id) { + $disabled = ['active' => false]; + } else { + $disabled = false; + } + + $import_data = [ + 'carrier' => $this->trans('Carriers', [], 'Admin.Shipping.Feature'), + 'cms' => $this->trans('Pages', [], 'Admin.Design.Feature'), + 'contact' => $this->trans('Contact information', [], 'Admin.Advparameters.Feature'), + 'country' => $this->trans('Countries', [], 'Admin.Global'), + 'currency' => $this->trans('Currencies', [], 'Admin.Global'), + 'discount' => $this->trans('Discount prices', [], 'Admin.Advparameters.Feature'), + 'employee' => $this->trans('Employees', [], 'Admin.Advparameters.Feature'), + 'image' => $this->trans('Images', [], 'Admin.Global'), + 'lang' => $this->trans('Languages', [], 'Admin.Global'), + 'manufacturer' => $this->trans('Brands', [], 'Admin.Global'), + 'module' => $this->trans('Modules', [], 'Admin.Global'), + 'hook_module' => $this->trans('Module hooks', [], 'Admin.Advparameters.Feature'), + 'meta_lang' => $this->trans('Meta information', [], 'Admin.Advparameters.Feature'), + 'product' => $this->trans('Products', [], 'Admin.Global'), + 'product_attribute' => $this->trans('Product combinations', [], 'Admin.Advparameters.Feature'), + 'stock_available' => $this->trans('Available quantities for sale', [], 'Admin.Advparameters.Feature'), + 'store' => $this->trans('Stores', [], 'Admin.Global'), + 'warehouse' => $this->trans('Warehouses', [], 'Admin.Advparameters.Feature'), + 'webservice_account' => $this->trans('Webservice accounts', [], 'Admin.Advparameters.Feature'), + 'attribute_group' => $this->trans('Attribute groups', [], 'Admin.Advparameters.Feature'), + 'feature' => $this->trans('Features', [], 'Admin.Global'), + 'group' => $this->trans('Customer groups', [], 'Admin.Advparameters.Feature'), + 'tax_rules_group' => $this->trans('Tax rules groups', [], 'Admin.Advparameters.Feature'), + 'supplier' => $this->trans('Suppliers', [], 'Admin.Global'), + 'referrer' => $this->trans('Referrers/affiliates', [], 'Admin.Advparameters.Feature'), + 'zone' => $this->trans('Zones', [], 'Admin.International.Feature'), + 'cart_rule' => $this->trans('Cart rules', [], 'Admin.Advparameters.Feature'), + ]; + + // Hook for duplication of shop data + $modules_list = Hook::getHookModuleExecList('actionShopDataDuplication'); + if (is_array($modules_list) && count($modules_list) > 0) { + foreach ($modules_list as $m) { + $import_data['Module' . ucfirst($m['module'])] = Module::getModuleName($m['module']); + } + } + + asort($import_data); + + if (!$this->object->id) { + $this->fields_import_form = [ + 'radio' => [ + 'type' => 'radio', + 'label' => $this->trans('Import data', [], 'Admin.Advparameters.Feature'), + 'name' => 'useImportData', + 'value' => 1, + ], + 'select' => [ + 'type' => 'select', + 'name' => 'importFromShop', + 'label' => $this->trans('Choose the source shop', [], 'Admin.Advparameters.Feature'), + 'options' => [ + 'query' => Shop::getShops(false), + 'name' => 'name', + ], + ], + 'allcheckbox' => [ + 'type' => 'checkbox', + 'label' => $this->trans('Choose data to import', [], 'Admin.Advparameters.Feature'), + 'values' => $import_data, + ], + 'desc' => $this->trans('Use this option to associate data (products, modules, etc.) the same way for each selected shop.', [], 'Admin.Advparameters.Help'), + ]; + } + + if (!$obj->theme_name) { + $themes = (new ThemeManagerBuilder($this->context, Db::getInstance())) + ->buildRepository() + ->getList(); + $theme = array_pop($themes); + $theme_name = $theme->getName(); + } else { + $theme_name = $obj->theme_name; + } + + $this->fields_value = [ + 'id_shop_group' => (Tools::getValue('id_shop_group') ? Tools::getValue('id_shop_group') : + (isset($obj->id_shop_group)) ? $obj->id_shop_group : Shop::getContextShopGroupID()), + 'id_category' => (Tools::getValue('id_category') ? Tools::getValue('id_category') : + (isset($obj->id_category)) ? $obj->id_category : (int) Configuration::get('PS_HOME_CATEGORY')), + 'theme_name' => $theme_name, + ]; + + $ids_category = []; + $shops = Shop::getShops(false); + foreach ($shops as $shop) { + $ids_category[$shop['id_shop']] = $shop['id_category']; + } + + $this->tpl_form_vars = [ + 'disabled' => $disabled, + 'checked' => (Tools::getValue('addshop') !== false) ? true : false, + 'defaultShop' => (int) Configuration::get('PS_SHOP_DEFAULT'), + 'ids_category' => $ids_category, + ]; + if (isset($this->fields_import_form)) { + $this->tpl_form_vars = array_merge($this->tpl_form_vars, ['form_import' => $this->fields_import_form]); + } + + return parent::renderForm(); + } + + /** + * Object creation. + */ + public function processAdd() + { + if (!Tools::getValue('categoryBox') || !in_array(Tools::getValue('id_category'), Tools::getValue('categoryBox'))) { + $this->errors[] = $this->trans('You need to select at least the root category.', [], 'Admin.Advparameters.Notification'); + } + + if (Tools::isSubmit('id_category_default')) { + $_POST['id_category'] = (int) Tools::getValue('id_category_default'); + } + + /* Checking fields validity */ + $this->validateRules(); + + if (!count($this->errors)) { + /** @var Shop $object */ + $object = new $this->className(); + $this->copyFromPost($object, $this->table); + $this->beforeAdd($object); + if (!$object->add()) { + $this->errors[] = $this->trans('An error occurred while creating an object.', [], 'Admin.Notifications.Error') . + ' ' . $this->table . ' (' . Db::getInstance()->getMsgError() . ')'; + } elseif (($_POST[$this->identifier] = $object->id) && $this->postImage($object->id) && !count($this->errors) && $this->_redirect) { + // voluntary do affectation here + $parent_id = (int) Tools::getValue('id_parent', 1); + $this->afterAdd($object); + $this->updateAssoShop($object->id); + // Save and stay on same form + if (Tools::isSubmit('submitAdd' . $this->table . 'AndStay')) { + $this->redirect_after = self::$currentIndex . '&shop_id=' . (int) $object->id . '&conf=3&update' . $this->table . '&token=' . $this->token; + } + // Save and back to parent + if (Tools::isSubmit('submitAdd' . $this->table . 'AndBackToParent')) { + $this->redirect_after = self::$currentIndex . '&shop_id=' . (int) $parent_id . '&conf=3&token=' . $this->token; + } + // Default behavior (save and back) + if (empty($this->redirect_after)) { + $this->redirect_after = self::$currentIndex . ($parent_id ? '&shop_id=' . $object->id : '') . '&conf=3&token=' . $this->token; + } + } + } + + $this->errors = array_unique($this->errors); + if (count($this->errors) > 0) { + $this->display = 'add'; + + return; + } + + $object->associateSuperAdmins(); + + $categories = Tools::getValue('categoryBox'); + array_unshift($categories, Configuration::get('PS_ROOT_CATEGORY')); + Category::updateFromShop($categories, $object->id); + if (Tools::getValue('useImportData') && ($import_data = Tools::getValue('importData')) && is_array($import_data) && isset($import_data['product'])) { + ini_set('max_execution_time', 7200); // like searchcron.php + Search::indexation(true); + } + + return $object; + } + + public function displayEditLink($token, $id, $name = null) + { + if ($this->access('edit')) { + $tpl = $this->createTemplate('helpers/list/list_action_edit.tpl'); + if (!array_key_exists('Edit', self::$cache_lang)) { + self::$cache_lang['Edit'] = $this->trans('Edit', [], 'Admin.Actions'); + } + + $tpl->assign([ + 'href' => $this->context->link->getAdminLink('AdminShop') . '&shop_id=' . (int) $id . '&update' . $this->table, + 'action' => self::$cache_lang['Edit'], + 'id' => $id, + ]); + + return $tpl->fetch(); + } else { + return; + } + } + + public function initCategoriesAssociation($id_root = null) + { + if (null === $id_root) { + $id_root = Configuration::get('PS_ROOT_CATEGORY'); + } + $id_shop = (int) Tools::getValue('id_shop'); + $shop = new Shop($id_shop); + $selected_cat = Shop::getCategories($id_shop); + if (empty($selected_cat)) { + // get first category root and preselect all these children + $root_categories = Category::getRootCategories(); + $root_category = new Category($root_categories[0]['id_category']); + $children = $root_category->getAllChildren($this->context->language->id); + $selected_cat[] = $root_categories[0]['id_category']; + + foreach ($children as $child) { + $selected_cat[] = $child->id; + } + } + if (Shop::getContext() == Shop::CONTEXT_SHOP && Tools::isSubmit('id_shop')) { + $root_category = new Category($shop->id_category); + } else { + $root_category = new Category($id_root); + } + $root_category = ['id_category' => $root_category->id, 'name' => $root_category->name[$this->context->language->id]]; + + $helper = new Helper(); + + return $helper->renderCategoryTree($root_category, $selected_cat, 'categoryBox', false, true); + } + + public function ajaxProcessTree() + { + $tree = []; + $sql = 'SELECT g.id_shop_group, g.name as group_name, s.id_shop, s.name as shop_name, u.id_shop_url, u.domain, u.physical_uri, u.virtual_uri + FROM ' . _DB_PREFIX_ . 'shop_group g + LEFT JOIN ' . _DB_PREFIX_ . 'shop s ON g.id_shop_group = s.id_shop_group + LEFT JOIN ' . _DB_PREFIX_ . 'shop_url u ON u.id_shop = s.id_shop + ORDER BY g.name, s.name, u.domain'; + $results = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql); + foreach ($results as $row) { + $id_shop_group = $row['id_shop_group']; + $id_shop = $row['id_shop']; + $id_shop_url = $row['id_shop_url']; + + // Group list + if (!isset($tree[$id_shop_group])) { + $tree[$id_shop_group] = [ + 'data' => [ + 'title' => '' . $this->trans('Group', [], 'Admin.Global') . ' ' . $row['group_name'], + 'icon' => 'themes/' . $this->context->employee->bo_theme . '/img/tree-multishop-groups.png', + 'attr' => [ + 'href' => $this->context->link->getAdminLink('AdminShop') . '&id_shop_group=' . $id_shop_group, + 'title' => $this->trans('Click here to display the shops in the %name% shop group', ['%name%' => $row['group_name']], 'Admin.Advparameters.Help'), + ], + ], + 'attr' => [ + 'id' => 'tree-group-' . $id_shop_group, + ], + 'children' => [], + ]; + } + + // Shop list + if (!$id_shop) { + continue; + } + + if (!isset($tree[$id_shop_group]['children'][$id_shop])) { + $tree[$id_shop_group]['children'][$id_shop] = [ + 'data' => [ + 'title' => $row['shop_name'], + 'icon' => 'themes/' . $this->context->employee->bo_theme . '/img/tree-multishop-shop.png', + 'attr' => [ + 'href' => $this->context->link->getAdminLink('AdminShopUrl') . '&shop_id=' . (int) $id_shop, + 'title' => $this->trans('Click here to display the URLs of the %name% shop', ['%name%' => $row['shop_name']], 'Admin.Advparameters.Help'), + ], + ], + 'attr' => [ + 'id' => 'tree-shop-' . $id_shop, + ], + 'children' => [], + ]; + } + // Url list + if (!$id_shop_url) { + continue; + } + + if (!isset($tree[$id_shop_group]['children'][$id_shop]['children'][$id_shop_url])) { + $url = $row['domain'] . $row['physical_uri'] . $row['virtual_uri']; + if (strlen($url) > 23) { + $url = substr($url, 0, 23) . '...'; + } + + $tree[$id_shop_group]['children'][$id_shop]['children'][$id_shop_url] = [ + 'data' => [ + 'title' => $url, + 'icon' => 'themes/' . $this->context->employee->bo_theme . '/img/tree-multishop-url.png', + 'attr' => [ + 'href' => $this->context->link->getAdminLink('AdminShopUrl') . '&updateshop_url&id_shop_url=' . $id_shop_url, + 'title' => $row['domain'] . $row['physical_uri'] . $row['virtual_uri'], + ], + ], + 'attr' => [ + 'id' => 'tree-url-' . $id_shop_url, + ], + ]; + } + } + + // jstree need to have children as array and not object, so we use sort to get clean keys + // DO NOT REMOVE this code, even if it seems really strange ;) + sort($tree); + foreach ($tree as &$groups) { + sort($groups['children']); + foreach ($groups['children'] as &$shops) { + sort($shops['children']); + } + } + + $tree = [[ + 'data' => [ + 'title' => '' . $this->trans('Shop groups list', [], 'Admin.Advparameters.Feature') . '', + 'icon' => 'themes/' . $this->context->employee->bo_theme . '/img/tree-multishop-root.png', + 'attr' => [ + 'href' => $this->context->link->getAdminLink('AdminShopGroup'), + 'title' => $this->trans('Click here to display the list of shop groups', [], 'Admin.Advparameters.Help'), + ], + ], + 'attr' => [ + 'id' => 'tree-root', + ], + 'state' => 'open', + 'children' => $tree, + ]]; + + die(json_encode($tree)); + } +} diff --git a/controllers/admin/AdminShopGroupController.php b/controllers/admin/AdminShopGroupController.php new file mode 100644 index 00000000..f052db72 --- /dev/null +++ b/controllers/admin/AdminShopGroupController.php @@ -0,0 +1,364 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +/** + * @property ShopGroup $object + */ +class AdminShopGroupControllerCore extends AdminController +{ + public function __construct() + { + $this->bootstrap = true; + $this->table = 'shop_group'; + $this->className = 'ShopGroup'; + $this->lang = false; + $this->multishop_context = Shop::CONTEXT_ALL; + + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + parent::__construct(); + + if (!Tools::getValue('realedit')) { + $this->deleted = false; + } + + $this->show_toolbar = false; + + $this->fields_list = [ + 'id_shop_group' => [ + 'title' => $this->trans('ID', [], 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ], + 'name' => [ + 'title' => $this->trans('Shop group', [], 'Admin.Advparameters.Feature'), + 'width' => 'auto', + 'filter_key' => 'a!name', + ], + ]; + + $this->fields_options = [ + 'general' => [ + 'title' => $this->trans('Multistore options', [], 'Admin.Advparameters.Feature'), + 'fields' => [ + 'PS_SHOP_DEFAULT' => [ + 'title' => $this->trans('Default shop', [], 'Admin.Advparameters.Feature'), + 'cast' => 'intval', + 'type' => 'select', + 'identifier' => 'id_shop', + 'list' => Shop::getShops(), + 'visibility' => Shop::CONTEXT_ALL, + ], + ], + 'submit' => ['title' => $this->trans('Save', [], 'Admin.Actions')], + ], + ]; + } + + public function viewAccess($disable = false) + { + return Configuration::get('PS_MULTISHOP_FEATURE_ACTIVE'); + } + + public function initContent() + { + parent::initContent(); + + $this->addJqueryPlugin('cooki-plugin'); + $data = Shop::getTree(); + + foreach ($data as $key_group => &$group) { + foreach ($group['shops'] as $key_shop => &$shop) { + $current_shop = new Shop($shop['id_shop']); + $urls = $current_shop->getUrls(); + + foreach ($urls as $key_url => &$url) { + $title = $url['domain'] . $url['physical_uri'] . $url['virtual_uri']; + if (strlen($title) > 23) { + $title = substr($title, 0, 23) . '...'; + } + + $url['name'] = $title; + $shop['urls'][$url['id_shop_url']] = $url; + } + } + } + + $shops_tree = new HelperTreeShops('shops-tree', $this->trans('Multistore tree', [], 'Admin.Advparameters.Feature')); + $shops_tree->setNodeFolderTemplate('shop_tree_node_folder.tpl')->setNodeItemTemplate('shop_tree_node_item.tpl') + ->setHeaderTemplate('shop_tree_header.tpl')->setActions([ + new TreeToolbarLink( + 'Collapse All', + '#', + '$(\'#' . $shops_tree->getId() . '\').tree(\'collapseAll\'); return false;', + 'icon-collapse-alt' + ), + new TreeToolbarLink( + 'Expand All', + '#', + '$(\'#' . $shops_tree->getId() . '\').tree(\'expandAll\'); return false;', + 'icon-expand-alt' + ), + ]) + ->setAttribute('url_shop_group', $this->context->link->getAdminLink('AdminShopGroup')) + ->setAttribute('url_shop', $this->context->link->getAdminLink('AdminShop')) + ->setAttribute('url_shop_url', $this->context->link->getAdminLink('AdminShopUrl')) + ->setData($data); + $shops_tree = $shops_tree->render(null, false, false); + + if ($this->display == 'edit') { + $this->toolbar_title[] = $this->object->name; + } + + $this->context->smarty->assign([ + 'toolbar_scroll' => 1, + 'toolbar_btn' => $this->toolbar_btn, + 'title' => $this->toolbar_title, + 'shops_tree' => $shops_tree, + ]); + } + + public function initPageHeaderToolbar() + { + parent::initPageHeaderToolbar(); + + if ($this->display != 'add' && $this->display != 'edit') { + $this->page_header_toolbar_btn['new'] = [ + 'desc' => $this->trans('Add a new shop group', [], 'Admin.Advparameters.Feature'), + 'href' => self::$currentIndex . '&add' . $this->table . '&token=' . $this->token, + ]; + $this->page_header_toolbar_btn['new_2'] = [ + 'desc' => $this->trans('Add a new shop', [], 'Admin.Advparameters.Feature'), + 'href' => $this->context->link->getAdminLink('AdminShop') . '&addshop', + 'imgclass' => 'new_2', + 'icon' => 'process-icon-new', + ]; + } + } + + public function initToolbar() + { + parent::initToolbar(); + + if ($this->display != 'add' && $this->display != 'edit') { + $this->toolbar_btn['new'] = [ + 'desc' => $this->trans('Add a new shop group', [], 'Admin.Advparameters.Feature'), + 'href' => self::$currentIndex . '&add' . $this->table . '&token=' . $this->token, + ]; + } + } + + public function renderForm() + { + $this->fields_form = [ + 'legend' => [ + 'title' => $this->trans('Shop group', [], 'Admin.Advparameters.Feature'), + 'icon' => 'icon-shopping-cart', + ], + 'description' => $this->trans('Warning: Enabling the "share customers" and "share orders" options is not recommended. Once activated and orders are created, you will not be able to disable these options. If you need these options, we recommend using several categories rather than several shops.', [], 'Admin.Advparameters.Help'), + 'input' => [ + [ + 'type' => 'text', + 'label' => $this->trans('Shop group name', [], 'Admin.Advparameters.Feature'), + 'name' => 'name', + 'required' => true, + ], + [ + 'type' => 'switch', + 'label' => $this->trans('Share customers', [], 'Admin.Advparameters.Feature'), + 'name' => 'share_customer', + 'required' => true, + 'class' => 't', + 'is_bool' => true, + 'disabled' => ($this->id_object && $this->display == 'edit' && ShopGroup::hasDependency($this->id_object, 'customer')) ? true : false, + 'values' => [ + [ + 'id' => 'share_customer_on', + 'value' => 1, + ], + [ + 'id' => 'share_customer_off', + 'value' => 0, + ], + ], + 'desc' => $this->trans('Once this option is enabled, the shops in this group will share customers. If a customer registers in any one of these shops, the account will automatically be available in the others shops of this group.', [], 'Admin.Advparameters.Help') . '' . $this->trans('Warning: you will not be able to disable this option once you have registered customers.', [], 'Admin.Advparameters.Help'), + ], + [ + 'type' => 'switch', + 'label' => $this->trans('Share available quantities to sell', [], 'Admin.Advparameters.Feature'), + 'name' => 'share_stock', + 'required' => true, + 'class' => 't', + 'is_bool' => true, + 'values' => [ + [ + 'id' => 'share_stock_on', + 'value' => 1, + ], + [ + 'id' => 'share_stock_off', + 'value' => 0, + ], + ], + 'desc' => $this->trans('Share available quantities between shops of this group. When changing this option, all available products quantities will be reset to 0.', [], 'Admin.Advparameters.Feature'), + ], + [ + 'type' => 'switch', + 'label' => $this->trans('Share orders', [], 'Admin.Advparameters.Feature'), + 'name' => 'share_order', + 'required' => true, + 'class' => 't', + 'is_bool' => true, + 'disabled' => ($this->id_object && $this->display == 'edit' && ShopGroup::hasDependency($this->id_object, 'order')) ? true : false, + 'values' => [ + [ + 'id' => 'share_order_on', + 'value' => 1, + ], + [ + 'id' => 'share_order_off', + 'value' => 0, + ], + ], + 'desc' => $this->trans('Once this option is enabled (which is only possible if customers and available quantities are shared among shops), the customer\'s cart will be shared by all shops in this group. This way, any purchase started in one shop will be able to be completed in another shop from the same group.', [], 'Admin.Advparameters.Help') . '' . $this->trans('Warning: You will not be able to disable this option once you\'ve started to accept orders.', [], 'Admin.Advparameters.Help'), + ], + [ + 'type' => 'switch', + 'label' => $this->trans('Status', [], 'Admin.Global'), + 'name' => 'active', + 'required' => true, + 'class' => 't', + 'is_bool' => true, + 'values' => [ + [ + 'id' => 'active_on', + 'value' => 1, + ], + [ + 'id' => 'active_off', + 'value' => 0, + ], + ], + 'desc' => $this->trans('Enable or disable this shop group?', [], 'Admin.Advparameters.Help'), + ], + ], + 'submit' => [ + 'title' => $this->trans('Save', [], 'Admin.Actions'), + ], + ]; + + if (!($obj = $this->loadObject(true))) { + return; + } + + if (Shop::getTotalShops() > 1 && $obj->id) { + $disabled = [ + 'share_customer' => true, + 'share_stock' => true, + 'share_order' => true, + 'active' => false, + ]; + } else { + $disabled = false; + } + + $default_shop = new Shop(Configuration::get('PS_SHOP_DEFAULT')); + $this->tpl_form_vars = [ + 'disabled' => $disabled, + 'checked' => (Tools::getValue('addshop_group') !== false) ? true : false, + 'defaultGroup' => $default_shop->id_shop_group, + ]; + + $this->fields_value = [ + 'active' => true, + ]; + + return parent::renderForm(); + } + + public function getList($id_lang, $order_by = null, $order_way = null, $start = 0, $limit = null, $id_lang_shop = false) + { + parent::getList($id_lang, $order_by, $order_way, $start, $limit, $id_lang_shop); + $shop_group_delete_list = []; + + // test store authorized to remove + foreach ($this->_list as $shop_group) { + $shops = Shop::getShops(true, $shop_group['id_shop_group']); + if (!empty($shops)) { + $shop_group_delete_list[] = $shop_group['id_shop_group']; + } + } + $this->addRowActionSkipList('delete', $shop_group_delete_list); + } + + public function postProcess() + { + if (Tools::isSubmit('delete' . $this->table) || Tools::isSubmit('status') || Tools::isSubmit('status' . $this->table)) { + /** @var ShopGroup $object */ + $object = $this->loadObject(); + + if (ShopGroup::getTotalShopGroup() == 1) { + $this->errors[] = $this->trans('You cannot delete or disable the last shop group.', [], 'Admin.Notifications.Error'); + } elseif ($object->haveShops()) { + $this->errors[] = $this->trans('You cannot delete or disable a shop group in use.', [], 'Admin.Notifications.Error'); + } + + if (count($this->errors)) { + return false; + } + } + + return parent::postProcess(); + } + + protected function afterAdd($new_shop_group) + { + //Reset available quantitites + StockAvailable::resetProductFromStockAvailableByShopGroup($new_shop_group); + } + + protected function afterUpdate($new_shop_group) + { + //Reset available quantitites + StockAvailable::resetProductFromStockAvailableByShopGroup($new_shop_group); + } + + public function renderOptions() + { + if ($this->fields_options && is_array($this->fields_options)) { + $this->display = 'options'; + $this->show_toolbar = false; + $helper = new HelperOptions($this); + $this->setHelperDisplay($helper); + $helper->id = $this->id; + $helper->tpl_vars = $this->tpl_option_vars; + $options = $helper->generateOptions($this->fields_options); + + return $options; + } + } +} diff --git a/controllers/admin/AdminShopUrlController.php b/controllers/admin/AdminShopUrlController.php new file mode 100644 index 00000000..ddb64663 --- /dev/null +++ b/controllers/admin/AdminShopUrlController.php @@ -0,0 +1,574 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +/** + * @property ShopUrl $object + */ +class AdminShopUrlControllerCore extends AdminController +{ + public function __construct() + { + $this->bootstrap = true; + $this->table = 'shop_url'; + $this->className = 'ShopUrl'; + $this->lang = false; + $this->requiredDatabase = true; + $this->multishop_context = Shop::CONTEXT_ALL; + $this->bulk_actions = []; + + parent::__construct(); + + /* if $_GET['id_shop'] is transmitted, virtual url can be loaded in config.php, so we wether transmit shop_id in herfs */ + if ($this->id_shop = (int) Tools::getValue('shop_id')) { + $_GET['id_shop'] = $this->id_shop; + } else { + $this->id_shop = (int) Tools::getValue('id_shop'); + } + + if (!Tools::getValue('realedit')) { + $this->deleted = false; + } + + $this->fields_list = [ + 'id_shop_url' => [ + 'title' => $this->trans('Shop URL ID', [], 'Admin.Advparameters.Feature'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ], + 'shop_name' => [ + 'title' => $this->trans('Shop name', [], 'Admin.Advparameters.Feature'), + 'filter_key' => 's!name', + ], + 'url' => [ + 'title' => $this->trans('URL', [], 'Admin.Global'), + 'filter_key' => 'url', + 'havingFilter' => true, + 'remove_onclick' => true, + ], + 'main' => [ + 'title' => $this->trans('Is it the main URL?', [], 'Admin.Advparameters.Feature'), + 'align' => 'center', + 'activeVisu' => 'main', + 'active' => 'main', + 'type' => 'bool', + 'orderby' => false, + 'filter_key' => 'main', + 'class' => 'fixed-width-md', + ], + 'active' => [ + 'title' => $this->trans('Enabled', [], 'Admin.Global'), + 'align' => 'center', + 'active' => 'status', + 'type' => 'bool', + 'orderby' => false, + 'filter_key' => 'active', + 'class' => 'fixed-width-md', + ], + ]; + } + + public function viewAccess($disable = false) + { + return Configuration::get('PS_MULTISHOP_FEATURE_ACTIVE'); + } + + public function renderList() + { + $this->addRowActionSkipList('delete', [1]); + + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + $this->_select = 's.name AS shop_name, CONCAT(\'http://\', a.domain, a.physical_uri, a.virtual_uri) AS url'; + $this->_join = 'LEFT JOIN `' . _DB_PREFIX_ . 'shop` s ON (s.id_shop = a.id_shop)'; + + if ($id_shop = (int) Tools::getValue('id_shop')) { + $this->_where = 'AND a.id_shop = ' . $id_shop; + } + $this->_use_found_rows = false; + + return parent::renderList(); + } + + public function renderForm() + { + $update_htaccess = Tools::modRewriteActive() && ((file_exists('.htaccess') && is_writable('.htaccess')) || is_writable(dirname('.htaccess'))); + + $this->multiple_fieldsets = true; + if (!$update_htaccess) { + $desc_virtual_uri = [ + '' . $this->trans('If you want to add a virtual URL, you need to activate URL rewriting on your web server and enable Friendly URL option.', [], 'Admin.Advparameters.Help') . '', + ]; + } else { + $desc_virtual_uri = [ + $this->trans('You can use this option if you want to create a store with a URL that doesn\'t exist on your server (e.g. if you want your store to be available with the URL www.example.com/my-store/shoes/, you have to set shoes/ in this field, assuming that my-store/ is your Physical URL).', [], 'Admin.Advparameters.Help'), + '' . $this->trans('URL rewriting must be activated on your server to use this feature.', [], 'Admin.Advparameters.Help') . '', + ]; + } + $this->fields_form = [ + [ + 'form' => [ + 'legend' => [ + 'title' => $this->trans('URL options', [], 'Admin.Advparameters.Feature'), + 'icon' => 'icon-cogs', + ], + 'input' => [ + [ + 'type' => 'select', + 'label' => $this->trans('Shop', [], 'Admin.Global'), + 'name' => 'id_shop', + 'onchange' => 'checkMainUrlInfo(this.value);', + 'options' => [ + 'optiongroup' => [ + 'query' => Shop::getTree(), + 'label' => 'name', + ], + 'options' => [ + 'query' => 'shops', + 'id' => 'id_shop', + 'name' => 'name', + ], + ], + ], + [ + 'type' => 'switch', + 'label' => $this->trans('Is it the main URL for this shop?', [], 'Admin.Advparameters.Feature'), + 'name' => 'main', + 'is_bool' => true, + 'class' => 't', + 'values' => [ + [ + 'id' => 'main_on', + 'value' => 1, + ], + [ + 'id' => 'main_off', + 'value' => 0, + ], + ], + 'desc' => [ + $this->trans('If you set this URL as the Main URL for the selected shop, all URLs set to this shop will be redirected to this URL (you can only have one Main URL per shop).', [], 'Admin.Advparameters.Help'), + [ + 'text' => $this->trans('Since the selected shop has no main URL, you have to set this URL as the Main URL.', [], 'Admin.Advparameters.Help'), + 'id' => 'mainUrlInfo', + ], + [ + 'text' => $this->trans('The selected shop already has a Main URL. Therefore, if you set this one as the Main URL, the older Main URL will be set as a regular URL.', [], 'Admin.Advparameters.Help'), + 'id' => 'mainUrlInfoExplain', + ], + ], + ], + [ + 'type' => 'switch', + 'label' => $this->trans('Enabled', [], 'Admin.Global'), + 'name' => 'active', + 'required' => false, + 'is_bool' => true, + 'class' => 't', + 'values' => [ + [ + 'id' => 'active_on', + 'value' => 1, + ], + [ + 'id' => 'active_off', + 'value' => 0, + ], + ], + ], + ], + 'submit' => [ + 'title' => $this->trans('Save', [], 'Admin.Actions'), + ], + ], + ], + [ + 'form' => [ + 'legend' => [ + 'title' => $this->trans('Shop URL', [], 'Admin.Advparameters.Feature'), + 'icon' => 'icon-shopping-cart', + ], + 'input' => [ + [ + 'type' => 'text', + 'label' => $this->trans('Domain', [], 'Admin.Advparameters.Feature'), + 'name' => 'domain', + 'size' => 50, + ], + [ + 'type' => 'text', + 'label' => $this->trans('SSL Domain', [], 'Admin.Advparameters.Feature'), + 'name' => 'domain_ssl', + 'size' => 50, + ], + ], + 'submit' => [ + 'title' => $this->trans('Save', [], 'Admin.Actions'), + ], + ], + ], + ]; + + if (!defined('_PS_HOST_MODE_')) { + $this->fields_form[1]['form']['input'] = array_merge( + $this->fields_form[1]['form']['input'], + [ + [ + 'type' => 'text', + 'label' => $this->trans('Physical URL', [], 'Admin.Advparameters.Feature'), + 'name' => 'physical_uri', + 'desc' => $this->trans('This is the physical folder for your store on the web server. Leave this field empty if your store is installed on the root path. For instance, if your store is available at www.example.com/my-store/, you must input my-store/ in this field.', [], 'Admin.Advparameters.Help'), + 'size' => 50, + ], + ] + ); + } + + $this->fields_form[1]['form']['input'] = array_merge( + $this->fields_form[1]['form']['input'], + [ + [ + 'type' => 'text', + 'label' => $this->trans('Virtual URL', [], 'Admin.Advparameters.Feature'), + 'name' => 'virtual_uri', + 'desc' => $desc_virtual_uri, + 'size' => 50, + 'hint' => (!$update_htaccess) ? $this->trans('Warning: URL rewriting (e.g. mod_rewrite for Apache) seems to be disabled. If your Virtual URL doesn\'t work, please check with your hosting provider on how to activate URL rewriting.', [], 'Admin.Advparameters.Help') : null, + ], + [ + 'type' => 'text', + 'label' => $this->trans('Final URL', [], 'Admin.Advparameters.Feature'), + 'name' => 'final_url', + 'size' => 76, + 'readonly' => true, + ], + ] + ); + + if (!($obj = $this->loadObject(true))) { + return; + } + + self::$currentIndex = self::$currentIndex . ($obj->id ? '&shop_id=' . (int) $obj->id_shop : ''); + + $current_shop = Shop::initialize(); + + $list_shop_with_url = []; + foreach (Shop::getShops(false, null, true) as $id) { + $list_shop_with_url[$id] = (bool) count(ShopUrl::getShopUrls($id)); + } + + $this->tpl_form_vars = [ + 'js_shop_url' => json_encode($list_shop_with_url), + ]; + + $this->fields_value = [ + 'domain' => trim(Validate::isLoadedObject($obj) ? $this->getFieldValue($obj, 'domain') : $current_shop->domain), + 'domain_ssl' => trim(Validate::isLoadedObject($obj) ? $this->getFieldValue($obj, 'domain_ssl') : $current_shop->domain_ssl), + 'physical_uri' => trim(Validate::isLoadedObject($obj) ? $this->getFieldValue($obj, 'physical_uri') : $current_shop->physical_uri), + 'active' => trim(Validate::isLoadedObject($obj) ? $this->getFieldValue($obj, 'active') : true), + ]; + + return parent::renderForm(); + } + + public function initPageHeaderToolbar() + { + parent::initPageHeaderToolbar(); + + if ($this->display != 'add' && $this->display != 'edit') { + if ($this->id_object) { + $this->loadObject(); + } + + if (!$this->id_shop && $this->object && $this->object->id_shop) { + $this->id_shop = $this->object->id_shop; + } + + $this->page_header_toolbar_btn['edit'] = [ + 'desc' => $this->trans('Edit this shop', [], 'Admin.Advparameters.Feature'), + 'href' => $this->context->link->getAdminLink('AdminShop') . '&updateshop&shop_id=' . (int) $this->id_shop, + ]; + + $this->page_header_toolbar_btn['new'] = [ + 'desc' => $this->trans('Add a new URL', [], 'Admin.Advparameters.Feature'), + 'href' => $this->context->link->getAdminLink('AdminShopUrl') . '&add' . $this->table . '&shop_id=' . (int) $this->id_shop, + ]; + } + } + + public function initToolbar() + { + parent::initToolbar(); + + if ($this->display != 'add' && $this->display != 'edit') { + if ($this->id_object) { + $this->loadObject(); + } + + if (!$this->id_shop && $this->object && $this->object->id_shop) { + $this->id_shop = $this->object->id_shop; + } + + $this->toolbar_btn['new'] = [ + 'desc' => $this->trans('Add a new URL', [], 'Admin.Advparameters.Feature'), + 'href' => $this->context->link->getAdminLink('AdminShopUrl') . '&add' . $this->table . '&shop_id=' . (int) $this->id_shop, + ]; + } + } + + public function initContent() + { + parent::initContent(); + + $this->addJqueryPlugin('cooki-plugin'); + $data = Shop::getTree(); + + foreach ($data as &$group) { + foreach ($group['shops'] as &$shop) { + $current_shop = new Shop($shop['id_shop']); + $urls = $current_shop->getUrls(); + + foreach ($urls as &$url) { + $title = $url['domain'] . $url['physical_uri'] . $url['virtual_uri']; + if (strlen($title) > 23) { + $title = substr($title, 0, 23) . '...'; + } + + $url['name'] = $title; + $shop['urls'][$url['id_shop_url']] = $url; + } + } + } + + $shops_tree = new HelperTreeShops('shops-tree', $this->trans('Multistore tree', [], 'Admin.Advparameters.Feature')); + $shops_tree->setNodeFolderTemplate('shop_tree_node_folder.tpl')->setNodeItemTemplate('shop_tree_node_item.tpl') + ->setHeaderTemplate('shop_tree_header.tpl')->setActions([ + new TreeToolbarLink( + 'Collapse All', + '#', + '$(\'#' . $shops_tree->getId() . '\').tree(\'collapseAll\'); return false;', + 'icon-collapse-alt' + ), + new TreeToolbarLink( + 'Expand All', + '#', + '$(\'#' . $shops_tree->getId() . '\').tree(\'expandAll\'); return false;', + 'icon-expand-alt' + ), + ]) + ->setAttribute('url_shop_group', $this->context->link->getAdminLink('AdminShopGroup')) + ->setAttribute('url_shop', $this->context->link->getAdminLink('AdminShop')) + ->setAttribute('url_shop_url', $this->context->link->getAdminLink('AdminShopUrl')) + ->setData($data); + $shops_tree = $shops_tree->render(null, false, false); + + if (!$this->display && $this->id_shop) { + $shop = new Shop($this->id_shop); + $this->toolbar_title[] = $shop->name; + } + + $this->context->smarty->assign([ + 'toolbar_scroll' => 1, + 'toolbar_btn' => $this->toolbar_btn, + 'title' => $this->toolbar_title, + 'shops_tree' => $shops_tree, + ]); + } + + public function postProcess() + { + $token = Tools::getValue('token') ? Tools::getValue('token') : $this->token; + + $result = true; + + if ((Tools::isSubmit('status' . $this->table) || Tools::isSubmit('status')) && Tools::getValue($this->identifier)) { + if ($this->access('edit')) { + if (Validate::isLoadedObject($object = $this->loadObject())) { + /** @var ShopUrl $object */ + if ($object->main) { + $this->errors[] = $this->trans('You cannot disable the Main URL.', [], 'Admin.Notifications.Error'); + } elseif ($object->toggleStatus()) { + Tools::redirectAdmin(self::$currentIndex . '&conf=5&token=' . $token); + } else { + $this->errors[] = $this->trans('An error occurred while updating the status.', [], 'Admin.Notifications.Error'); + } + } else { + $this->errors[] = $this->trans('An error occurred while updating the status for an object.', [], 'Admin.Notifications.Error') . ' ' . $this->table . ' ' . $this->trans('(cannot load object)', [], 'Admin.Notifications.Error'); + } + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('main' . $this->table) && Tools::getValue($this->identifier)) { + if ($this->access('edit')) { + if (Validate::isLoadedObject($object = $this->loadObject())) { + /** @var ShopUrl $object */ + if (!$object->main) { + $result = $object->setMain(); + Tools::redirectAdmin(self::$currentIndex . '&conf=4&token=' . $token); + } else { + $this->errors[] = $this->trans('You cannot change a main URL to a non-main URL. You have to set another URL as your Main URL for the selected shop.', [], 'Admin.Notifications.Error'); + } + } else { + $this->errors[] = $this->trans('An error occurred while updating the status for an object.', [], 'Admin.Notifications.Error') . ' ' . $this->table . ' ' . $this->trans('(cannot load object)', [], 'Admin.Notifications.Error'); + } + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error'); + } + } else { + $result = parent::postProcess(); + } + + if ($this->redirect_after) { + $this->redirect_after .= '&shop_id=' . (int) $this->id_shop; + } + + return $result; + } + + public function processSave() + { + /** @var ShopUrl $object */ + $object = $this->loadObject(true); + if ($object->canAddThisUrl(Tools::getValue('domain'), Tools::getValue('domain_ssl'), Tools::getValue('physical_uri'), Tools::getValue('virtual_uri'))) { + $this->errors[] = $this->trans('A shop URL that uses this domain already exists.', [], 'Admin.Notifications.Error'); + } + + $unallowed = str_replace('/', '', Tools::getValue('virtual_uri')); + if ($unallowed == 'c' || $unallowed == 'img' || is_numeric($unallowed)) { + $this->errors[] = $this->trans( + 'A shop virtual URL cannot be "%URL%"', + [ + '%URL%' => $unallowed, + ], + 'Admin.Notifications.Error' + ); + } + $return = parent::processSave(); + if (!$this->errors) { + Tools::generateHtaccess(); + Tools::generateRobotsFile(); + Tools::clearSmartyCache(); + Media::clearCache(); + } + + return $return; + } + + public function processAdd() + { + /** @var ShopUrl $object */ + $object = $this->loadObject(true); + + if ($object->canAddThisUrl(Tools::getValue('domain'), Tools::getValue('domain_ssl'), Tools::getValue('physical_uri'), Tools::getValue('virtual_uri'))) { + $this->errors[] = $this->trans('A shop URL that uses this domain already exists.', [], 'Admin.Notifications.Error'); + } + + if (Tools::getValue('main') && !Tools::getValue('active')) { + $this->errors[] = $this->trans('You cannot disable the Main URL.', [], 'Admin.Notifications.Error'); + } + + return parent::processAdd(); + } + + public function processUpdate() + { + $this->redirect_shop_url = false; + $current_url = parse_url($_SERVER['REQUEST_URI']); + if (trim(dirname(dirname($current_url['path'])), '/') == trim($this->object->getBaseURI(), '/')) { + $this->redirect_shop_url = true; + } + + /** @var ShopUrl $object */ + $object = $this->loadObject(true); + + if ($object->main && !Tools::getValue('main')) { + $this->errors[] = $this->trans('You cannot change a main URL to a non-main URL. You have to set another URL as your Main URL for the selected shop.', [], 'Admin.Notifications.Error'); + } + + if (($object->main || Tools::getValue('main')) && !Tools::getValue('active')) { + $this->errors[] = $this->trans('You cannot disable the Main URL.', [], 'Admin.Notifications.Error'); + } + + return parent::processUpdate(); + } + + /** + * @param ShopUrl $object + */ + protected function afterUpdate($object) + { + if ($object->id && Tools::getValue('main')) { + $object->setMain(); + } + + if ($this->redirect_shop_url) { + $this->redirect_after = $this->context->link->getAdminLink('AdminShopUrl'); + } + } + + /** + * @param string $token + * @param int $id + * @param string $name + * + * @return mixed + */ + public function displayDeleteLink($token, $id, $name = null) + { + $tpl = $this->createTemplate('helpers/list/list_action_delete.tpl'); + + if (!array_key_exists('Delete', self::$cache_lang)) { + self::$cache_lang['Delete'] = $this->trans('Delete', [], 'Admin.Actions'); + } + + if (!array_key_exists('DeleteItem', self::$cache_lang)) { + self::$cache_lang['DeleteItem'] = $this->trans('Delete selected item?', [], 'Admin.Notifications.Warning'); + } + + if (!array_key_exists('Name', self::$cache_lang)) { + self::$cache_lang['Name'] = $this->trans('Name:', [], 'Admin.Global'); + } + + if (null !== $name) { + $name = '\n\n' . self::$cache_lang['Name'] . ' ' . $name; + } + + $data = [ + $this->identifier => $id, + 'href' => self::$currentIndex . '&' . $this->identifier . '=' . $id . '&delete' . $this->table . '&shop_id=' . (int) $this->id_shop . '&token=' . ($token != null ? $token : $this->token), + 'action' => self::$cache_lang['Delete'], + ]; + + if ($this->specificConfirmDelete !== false) { + $data['confirm'] = null !== $this->specificConfirmDelete ? '\r' . $this->specificConfirmDelete : self::$cache_lang['DeleteItem'] . $name; + } + + $tpl->assign(array_merge($this->tpl_delete_link_vars, $data)); + + return $tpl->fetch(); + } +} diff --git a/controllers/admin/AdminSlipController.php b/controllers/admin/AdminSlipController.php new file mode 100644 index 00000000..b739313a --- /dev/null +++ b/controllers/admin/AdminSlipController.php @@ -0,0 +1,202 @@ + + * @copyright 2007-2019 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ + +/** + * @property OrderSlip $object + */ +class AdminSlipControllerCore extends AdminController +{ + public function __construct() + { + $this->bootstrap = true; + $this->table = 'order_slip'; + $this->className = 'OrderSlip'; + + $this->_select = ' o.`id_shop`'; + $this->_join .= ' LEFT JOIN ' . _DB_PREFIX_ . 'orders o ON (o.`id_order` = a.`id_order`)'; + $this->_group = ' GROUP BY a.`id_order_slip`'; + + parent::__construct(); + + $this->fields_list = array( + 'id_order_slip' => array( + 'title' => $this->trans('ID', array(), 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ), + 'id_order' => array( + 'title' => $this->trans('Order ID', array(), 'Admin.Orderscustomers.Feature'), + 'align' => 'left', + 'class' => 'fixed-width-md', + 'filter_key' => 'a!id_order', + ), + 'date_add' => array( + 'title' => $this->trans('Date issued', array(), 'Admin.Orderscustomers.Feature'), + 'type' => 'date', + 'align' => 'right', + 'filter_key' => 'a!date_add', + 'havingFilter' => true, + ), + 'id_pdf' => array( + 'title' => $this->trans('PDF', array(), 'Admin.Global'), + 'align' => 'center', + 'callback' => 'printPDFIcons', + 'orderby' => false, + 'search' => false, + 'remove_onclick' => true, ), + ); + + $this->_select = 'a.id_order_slip AS id_pdf'; + $this->optionTitle = $this->trans('Slip', array(), 'Admin.Orderscustomers.Feature'); + + $this->fields_options = array( + 'general' => array( + 'title' => $this->trans('Credit slip options', array(), 'Admin.Orderscustomers.Feature'), + 'fields' => array( + 'PS_CREDIT_SLIP_PREFIX' => array( + 'title' => $this->trans('Credit slip prefix', array(), 'Admin.Orderscustomers.Feature'), + 'desc' => $this->trans('Prefix used for credit slips.', array(), 'Admin.Orderscustomers.Help'), + 'size' => 6, + 'type' => 'textLang', + ), + ), + 'submit' => array('title' => $this->trans('Save', array(), 'Admin.Actions')), + ), + ); + + $this->_where = Shop::addSqlRestriction(false, 'o'); + } + + public function initPageHeaderToolbar() + { + $this->page_header_toolbar_btn['generate_pdf'] = array( + 'href' => self::$currentIndex . '&token=' . $this->token, + 'desc' => $this->trans('Generate PDF', array(), 'Admin.Orderscustomers.Feature'), + 'icon' => 'process-icon-save-date', + ); + + parent::initPageHeaderToolbar(); + } + + public function renderForm() + { + $this->fields_form = array( + 'legend' => array( + 'title' => $this->trans('Print a PDF', array(), 'Admin.Orderscustomers.Feature'), + 'icon' => 'icon-print', + ), + 'input' => array( + array( + 'type' => 'date', + 'label' => $this->trans('From', array(), 'Admin.Global'), + 'name' => 'date_from', + 'maxlength' => 10, + 'required' => true, + 'hint' => $this->trans('Format: 2011-12-31 (inclusive).', array(), 'Admin.Orderscustomers.Help'), + ), + array( + 'type' => 'date', + 'label' => $this->trans('To', array(), 'Admin.Global'), + 'name' => 'date_to', + 'maxlength' => 10, + 'required' => true, + 'hint' => $this->trans('Format: 2012-12-31 (inclusive).', array(), 'Admin.Orderscustomers.Help'), + ), + ), + 'submit' => array( + 'title' => $this->trans('Generate PDF', array(), 'Admin.Orderscustomers.Feature'), + 'id' => 'submitPrint', + 'icon' => 'process-icon-download-alt', + ), + ); + + $this->fields_value = array( + 'date_from' => date('Y-m-d'), + 'date_to' => date('Y-m-d'), + ); + + $this->show_toolbar = false; + + return parent::renderForm(); + } + + public function postProcess() + { + if (Tools::getValue('submitAddorder_slip')) { + if (!Validate::isDate(Tools::getValue('date_from'))) { + $this->errors[] = $this->trans('Invalid "From" date', array(), 'Admin.Orderscustomers.Notification'); + } + if (!Validate::isDate(Tools::getValue('date_to'))) { + $this->errors[] = $this->trans('Invalid "To" date', array(), 'Admin.Orderscustomers.Notification'); + } + if (!count($this->errors)) { + $order_slips = OrderSlip::getSlipsIdByDate(Tools::getValue('date_from'), Tools::getValue('date_to')); + if (count($order_slips)) { + Tools::redirectAdmin($this->context->link->getAdminLink('AdminPdf') . '&submitAction=generateOrderSlipsPDF&date_from=' . urlencode(Tools::getValue('date_from')) . '&date_to=' . urlencode(Tools::getValue('date_to'))); + } + $this->errors[] = $this->trans('No order slips were found for this period.', array(), 'Admin.Orderscustomers.Notification'); + } + } else { + return parent::postProcess(); + } + } + + public function initContent() + { + $this->content .= $this->renderList(); + $this->content .= $this->renderForm(); + $this->content .= $this->renderOptions(); + + $this->context->smarty->assign(array( + 'content' => $this->content, + )); + } + + public function initToolbar() + { + parent::initToolbar(); + + $this->toolbar_btn['save-date'] = array( + 'href' => '#', + 'desc' => $this->trans('Generate PDF', array(), 'Admin.Orderscustomers.Feature'), + ); + } + + public function printPDFIcons($id_order_slip, $tr) + { + $order_slip = new OrderSlip((int) $id_order_slip); + if (!Validate::isLoadedObject($order_slip)) { + return ''; + } + + $this->context->smarty->assign(array( + 'order_slip' => $order_slip, + 'tr' => $tr, + )); + + return $this->createTemplate('_print_pdf_icon.tpl')->fetch(); + } +} diff --git a/controllers/admin/AdminSpecificPriceRuleController.php b/controllers/admin/AdminSpecificPriceRuleController.php new file mode 100644 index 00000000..8e6084e7 --- /dev/null +++ b/controllers/admin/AdminSpecificPriceRuleController.php @@ -0,0 +1,384 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +/** + * @property SpecificPriceRule $object + */ +class AdminSpecificPriceRuleControllerCore extends AdminController +{ + public $list_reduction_type; + + public function __construct() + { + $this->bootstrap = true; + $this->table = 'specific_price_rule'; + $this->className = 'SpecificPriceRule'; + $this->lang = false; + $this->multishop_context = Shop::CONTEXT_ALL; + + parent::__construct(); + + /* if $_GET['id_shop'] is transmitted, virtual url can be loaded in config.php, so we wether transmit shop_id in herfs */ + if ($this->id_shop = (int) Tools::getValue('shop_id')) { + $_GET['id_shop'] = $this->id_shop; + $_POST['id_shop'] = $this->id_shop; + } + + $this->list_reduction_type = [ + 'percentage' => $this->trans('Percentage', [], 'Admin.Global'), + 'amount' => $this->trans('Amount', [], 'Admin.Global'), + ]; + + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + $this->_select = 's.name shop_name, cul.name as currency_name, cl.name country_name, gl.name group_name'; + $this->_join = 'LEFT JOIN ' . _DB_PREFIX_ . 'shop s ON (s.id_shop = a.id_shop) + LEFT JOIN ' . _DB_PREFIX_ . 'currency_lang cul ON (cul.id_currency = a.id_currency AND cul.id_lang=' . (int) $this->context->language->id . ') + LEFT JOIN ' . _DB_PREFIX_ . 'country_lang cl ON (cl.id_country = a.id_country AND cl.id_lang=' . (int) $this->context->language->id . ') + LEFT JOIN ' . _DB_PREFIX_ . 'group_lang gl ON (gl.id_group = a.id_group AND gl.id_lang=' . (int) $this->context->language->id . ')'; + $this->_use_found_rows = false; + + $this->bulk_actions = [ + 'delete' => [ + 'text' => $this->trans('Delete selected', [], 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', [], 'Admin.Notifications.Warning'), + 'icon' => 'icon-trash', + ], + ]; + + $this->fields_list = [ + 'id_specific_price_rule' => [ + 'title' => $this->trans('ID', [], 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ], + 'name' => [ + 'title' => $this->trans('Name', [], 'Admin.Global'), + 'filter_key' => 'a!name', + 'width' => 'auto', + ], + 'shop_name' => [ + 'title' => $this->trans('Shop', [], 'Admin.Global'), + 'filter_key' => 's!name', + ], + 'id_currency' => [ + 'title' => $this->trans('Currency', [], 'Admin.Global'), + 'align' => 'center', + 'filter_key' => 'cul!name', + ], + 'country_name' => [ + 'title' => $this->trans('Country', [], 'Admin.Global'), + 'align' => 'center', + 'filter_key' => 'cl!name', + ], + 'group_name' => [ + 'title' => $this->trans('Group', [], 'Admin.Global'), + 'align' => 'center', + 'filter_key' => 'gl!name', + ], + 'from_quantity' => [ + 'title' => $this->trans('From quantity', [], 'Admin.Catalog.Feature'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ], + 'reduction_type' => [ + 'title' => $this->trans('Reduction type', [], 'Admin.Catalog.Feature'), + 'align' => 'center', + 'type' => 'select', + 'filter_key' => 'a!reduction_type', + 'list' => $this->list_reduction_type, + ], + 'reduction' => [ + 'title' => $this->trans('Reduction', [], 'Admin.Catalog.Feature'), + 'align' => 'center', + 'type' => 'decimal', + 'class' => 'fixed-width-xs', + ], + 'from' => [ + 'title' => $this->trans('Beginning', [], 'Admin.Catalog.Feature'), + 'align' => 'right', + 'type' => 'datetime', + 'filter_key' => 'a!from', + 'order_key' => 'a!from', + ], + 'to' => [ + 'title' => $this->trans('End', [], 'Admin.Catalog.Feature'), + 'align' => 'right', + 'type' => 'datetime', + 'filter_key' => 'a!to', + 'order_key' => 'a!to', + ], + ]; + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_specific_price_rule'] = [ + 'href' => self::$currentIndex . '&addspecific_price_rule&token=' . $this->token, + 'desc' => $this->trans('Add new catalog price rule', [], 'Admin.Catalog.Feature'), + 'icon' => 'process-icon-new', + ]; + } + + parent::initPageHeaderToolbar(); + } + + public function getList($id_lang, $order_by = null, $order_way = null, $start = 0, $limit = null, $id_lang_shop = false) + { + parent::getList($id_lang, $order_by, $order_way, $start, $limit, $id_lang_shop); + + foreach ($this->_list as $k => $list) { + if (null !== $this->_list[$k]['id_currency']) { + $currency = new Currency( + (int) $this->_list[$k]['id_currency'], + (int) $this->context->language->id, + (int) $this->context->shop->id + ); + $this->_list[$k]['id_currency'] = Validate::isLoadedObject($currency) ? $currency->getName() : null; + } + + if ($list['reduction_type'] == 'amount') { + $this->_list[$k]['reduction_type'] = $this->list_reduction_type['amount']; + } elseif ($list['reduction_type'] == 'percentage') { + $this->_list[$k]['reduction_type'] = $this->list_reduction_type['percentage']; + } + } + } + + public function renderForm() + { + if (!$this->object->id) { + $this->object->price = -1; + } + + $this->fields_form = [ + 'legend' => [ + 'title' => $this->trans('Catalog price rules', [], 'Admin.Catalog.Feature'), + 'icon' => 'icon-dollar', + ], + 'input' => [ + [ + 'type' => 'text', + 'label' => $this->trans('Name', [], 'Admin.Global'), + 'name' => 'name', + 'maxlength' => 255, + 'required' => true, + ], + [ + 'type' => 'select', + 'label' => $this->trans('Shop', [], 'Admin.Global'), + 'name' => 'shop_id', + 'options' => [ + 'query' => Shop::getShops(), + 'id' => 'id_shop', + 'name' => 'name', + ], + 'condition' => Shop::isFeatureActive(), + 'default_value' => Shop::getContextShopID(), + ], + [ + 'type' => 'select', + 'label' => $this->trans('Currency', [], 'Admin.Global'), + 'name' => 'id_currency', + 'options' => [ + 'query' => array_merge([0 => ['id_currency' => 0, 'name' => $this->trans('All currencies', [], 'Admin.Global')]], Currency::getCurrencies(false, true, true)), + 'id' => 'id_currency', + 'name' => 'name', + ], + ], + [ + 'type' => 'select', + 'label' => $this->trans('Country', [], 'Admin.Global'), + 'name' => 'id_country', + 'options' => [ + 'query' => array_merge([0 => ['id_country' => 0, 'name' => $this->trans('All countries', [], 'Admin.Global')]], Country::getCountries((int) $this->context->language->id)), + 'id' => 'id_country', + 'name' => 'name', + ], + ], + [ + 'type' => 'select', + 'label' => $this->trans('Group', [], 'Admin.Global'), + 'name' => 'id_group', + 'options' => [ + 'query' => array_merge([0 => ['id_group' => 0, 'name' => $this->trans('All groups', [], 'Admin.Global')]], Group::getGroups((int) $this->context->language->id)), + 'id' => 'id_group', + 'name' => 'name', + ], + ], + [ + 'type' => 'text', + 'label' => $this->trans('From quantity', [], 'Admin.Catalog.Feature'), + 'name' => 'from_quantity', + 'maxlength' => 10, + 'required' => true, + ], + [ + 'type' => 'text', + 'label' => $this->trans('Price (tax excl.)', [], 'Admin.Catalog.Feature'), + 'name' => 'price', + 'disabled' => ($this->object->price == -1 ? 1 : 0), + 'maxlength' => 10, + 'suffix' => $this->context->currency->getSign('right'), + ], + [ + 'type' => 'checkbox', + 'name' => 'leave_bprice', + 'values' => [ + 'query' => [ + [ + 'id' => 'on', + 'name' => $this->trans('Leave initial price', [], 'Admin.Catalog.Feature'), + 'val' => '1', + 'checked' => '1', + ], + ], + 'id' => 'id', + 'name' => 'name', + ], + ], + [ + 'type' => 'datetime', + 'label' => $this->trans('From', [], 'Admin.Global'), + 'name' => 'from', + ], + [ + 'type' => 'datetime', + 'label' => $this->trans('To', [], 'Admin.Global'), + 'name' => 'to', + ], + [ + 'type' => 'select', + 'label' => $this->trans('Reduction type', [], 'Admin.Catalog.Feature'), + 'name' => 'reduction_type', + 'options' => [ + 'query' => [['reduction_type' => 'amount', 'name' => $this->trans('Amount', [], 'Admin.Global')], ['reduction_type' => 'percentage', 'name' => $this->trans('Percentage', [], 'Admin.Global')]], + 'id' => 'reduction_type', + 'name' => 'name', + ], + ], + [ + 'type' => 'select', + 'label' => $this->trans('Reduction with or without taxes', [], 'Admin.Catalog.Feature'), + 'name' => 'reduction_tax', + 'align' => 'center', + 'options' => [ + 'query' => [ + ['lab' => $this->trans('Tax included', [], 'Admin.Global'), 'val' => 1], + ['lab' => $this->trans('Tax excluded', [], 'Admin.Global'), 'val' => 0], + ], + 'id' => 'val', + 'name' => 'lab', + ], + ], + [ + 'type' => 'text', + 'label' => $this->trans('Reduction', [], 'Admin.Catalog.Feature'), + 'name' => 'reduction', + 'required' => true, + ], + ], + 'submit' => [ + 'title' => $this->trans('Save', [], 'Admin.Actions'), + ], + ]; + if (($value = $this->getFieldValue($this->object, 'price')) != -1) { + $price = number_format($value, 6); + } else { + $price = ''; + } + + $this->fields_value = [ + 'price' => $price, + 'from_quantity' => (($value = $this->getFieldValue($this->object, 'from_quantity')) ? $value : 1), + 'reduction' => number_format((($value = $this->getFieldValue($this->object, 'reduction')) ? $value : 0), 6), + 'leave_bprice_on' => $price ? 0 : 1, + 'shop_id' => (($value = $this->getFieldValue($this->object, 'id_shop')) ? $value : 1), + ]; + + $attribute_groups = []; + $attributes = Attribute::getAttributes((int) $this->context->language->id); + foreach ($attributes as $attribute) { + if (!isset($attribute_groups[$attribute['id_attribute_group']])) { + $attribute_groups[$attribute['id_attribute_group']] = [ + 'id_attribute_group' => $attribute['id_attribute_group'], + 'name' => $attribute['attribute_group'], + ]; + } + $attribute_groups[$attribute['id_attribute_group']]['attributes'][] = [ + 'id_attribute' => $attribute['id_attribute'], + 'name' => $attribute['name'], + ]; + } + $features = Feature::getFeatures((int) $this->context->language->id); + foreach ($features as &$feature) { + $feature['values'] = FeatureValue::getFeatureValuesWithLang((int) $this->context->language->id, $feature['id_feature'], true); + } + + $this->tpl_form_vars = [ + 'manufacturers' => Manufacturer::getManufacturers(false, (int) $this->context->language->id, true, false, false, false, true), + 'suppliers' => Supplier::getSuppliers(), + 'attributes_group' => $attribute_groups, + 'features' => $features, + 'categories' => Category::getSimpleCategories((int) $this->context->language->id), + 'conditions' => $this->object->getConditions(), + 'is_multishop' => Shop::isFeatureActive(), + ]; + + return parent::renderForm(); + } + + public function processSave() + { + $_POST['price'] = Tools::getValue('leave_bprice_on') ? '-1' : Tools::getValue('price'); + if (Validate::isLoadedObject(($object = parent::processSave()))) { + /* @var SpecificPriceRule $object */ + $object->deleteConditions(); + foreach ($_POST as $key => $values) { + if (preg_match('/^condition_group_([0-9]+)$/Ui', $key, $condition_group)) { + $conditions = []; + foreach ($values as $value) { + $condition = explode('_', $value); + $conditions[] = ['type' => $condition[0], 'value' => $condition[1]]; + } + $object->addConditions($conditions); + } + } + $object->apply(); + + return $object; + } + } + + public function postProcess() + { + Tools::clearSmartyCache(); + + return parent::postProcess(); + } +} diff --git a/controllers/admin/AdminStatesController.php b/controllers/admin/AdminStatesController.php new file mode 100644 index 00000000..75ac3fb1 --- /dev/null +++ b/controllers/admin/AdminStatesController.php @@ -0,0 +1,299 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +/** + * @property State $object + */ +class AdminStatesControllerCore extends AdminController +{ + public function __construct() + { + $this->bootstrap = true; + $this->table = 'state'; + $this->className = 'State'; + $this->lang = false; + $this->requiredDatabase = true; + + parent::__construct(); + + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + if (!Tools::getValue('realedit')) { + $this->deleted = false; + } + + $this->bulk_actions = [ + 'delete' => ['text' => $this->trans('Delete selected', [], 'Admin.Actions'), 'confirm' => $this->trans('Delete selected items?', [], 'Admin.Notifications.Warning')], + 'AffectZone' => ['text' => $this->trans('Assign to a new zone', [], 'Admin.International.Feature')], + ]; + + $this->_select = 'z.`name` AS zone, cl.`name` AS country'; + $this->_join = ' + LEFT JOIN `' . _DB_PREFIX_ . 'zone` z ON (z.`id_zone` = a.`id_zone`) + LEFT JOIN `' . _DB_PREFIX_ . 'country_lang` cl ON (cl.`id_country` = a.`id_country` AND cl.id_lang = ' . (int) $this->context->language->id . ')'; + $this->_use_found_rows = false; + + $countries_array = $zones_array = []; + $this->zones = Zone::getZones(); + $this->countries = Country::getCountries($this->context->language->id, false, true, false); + foreach ($this->zones as $zone) { + $zones_array[$zone['id_zone']] = $zone['name']; + } + foreach ($this->countries as $country) { + $countries_array[$country['id_country']] = $country['name']; + } + + $this->fields_list = [ + 'id_state' => [ + 'title' => $this->trans('ID', [], 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ], + 'name' => [ + 'title' => $this->trans('Name', [], 'Admin.Global'), + 'filter_key' => 'a!name', + ], + 'iso_code' => [ + 'title' => $this->trans('ISO code', [], 'Admin.International.Feature'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ], + 'zone' => [ + 'title' => $this->trans('Zone', [], 'Admin.Global'), + 'type' => 'select', + 'list' => $zones_array, + 'filter_key' => 'z!id_zone', + 'filter_type' => 'int', + 'order_key' => 'zone', + ], + 'country' => [ + 'title' => $this->trans('Country', [], 'Admin.Global'), + 'type' => 'select', + 'list' => $countries_array, + 'filter_key' => 'cl!id_country', + 'filter_type' => 'int', + 'order_key' => 'country', + ], + 'active' => [ + 'title' => $this->trans('Enabled', [], 'Admin.Global'), + 'active' => 'status', + 'filter_key' => 'a!active', + 'align' => 'center', + 'type' => 'bool', + 'orderby' => false, + 'class' => 'fixed-width-sm', + ], + ]; + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_state'] = [ + 'href' => self::$currentIndex . '&addstate&token=' . $this->token, + 'desc' => $this->trans('Add new state', [], 'Admin.International.Feature'), + 'icon' => 'process-icon-new', + ]; + } + + parent::initPageHeaderToolbar(); + } + + public function renderList() + { + $this->tpl_list_vars['zones'] = Zone::getZones(); + $this->tpl_list_vars['REQUEST_URI'] = $_SERVER['REQUEST_URI']; + $this->tpl_list_vars['POST'] = $_POST; + + return parent::renderList(); + } + + public function renderForm() + { + $this->fields_form = [ + 'legend' => [ + 'title' => $this->trans('States', [], 'Admin.International.Feature'), + 'icon' => 'icon-globe', + ], + 'input' => [ + [ + 'type' => 'text', + 'label' => $this->trans('Name', [], 'Admin.Global'), + 'name' => 'name', + 'maxlength' => 32, + 'required' => true, + 'hint' => $this->trans('Provide the state name to be displayed in addresses and on invoices.', [], 'Admin.International.Help'), + ], + [ + 'type' => 'text', + 'label' => $this->trans('ISO code', [], 'Admin.International.Feature'), + 'name' => 'iso_code', + 'maxlength' => 7, + 'required' => true, + 'class' => 'uppercase', + 'hint' => $this->trans('1 to 4 letter ISO code.', [], 'Admin.International.Help') . ' ' . $this->trans('You can prefix it with the country ISO code if needed.', [], 'Admin.International.Help'), + ], + [ + 'type' => 'select', + 'label' => $this->trans('Country', [], 'Admin.Global'), + 'name' => 'id_country', + 'required' => true, + 'default_value' => (int) $this->context->country->id, + 'options' => [ + 'query' => Country::getCountries($this->context->language->id, false, true), + 'id' => 'id_country', + 'name' => 'name', + ], + 'hint' => $this->trans('Country where the state is located.', [], 'Admin.International.Help') . ' ' . $this->trans('Only the countries with the option "contains states" enabled are displayed.', [], 'Admin.International.Help'), + ], + [ + 'type' => 'select', + 'label' => $this->trans('Zone', [], 'Admin.Global'), + 'name' => 'id_zone', + 'required' => true, + 'options' => [ + 'query' => Zone::getZones(), + 'id' => 'id_zone', + 'name' => 'name', + ], + 'hint' => [ + $this->trans('Geographical region where this state is located.', [], 'Admin.International.Help'), + $this->trans('Used for shipping', [], 'Admin.International.Help'), + ], + ], + [ + 'type' => 'switch', + 'label' => $this->trans('Status', [], 'Admin.Global'), + 'name' => 'active', + 'required' => true, + 'values' => [ + [ + 'id' => 'active_on', + 'value' => 1, + 'label' => '', + ], + [ + 'id' => 'active_off', + 'value' => 0, + 'label' => '', + ], + ], + ], + ], + 'submit' => [ + 'title' => $this->trans('Save', [], 'Admin.Actions'), + ], + ]; + + return parent::renderForm(); + } + + public function postProcess() + { + if (Tools::isSubmit($this->table . 'Orderby') || Tools::isSubmit($this->table . 'Orderway')) { + $this->filter = true; + } + + // Idiot-proof controls + if (!Tools::getValue('id_' . $this->table)) { + if (Validate::isStateIsoCode(Tools::getValue('iso_code')) && State::getIdByIso(Tools::getValue('iso_code'), Tools::getValue('id_country'))) { + $this->errors[] = $this->trans('This ISO code already exists. You cannot create two states with the same ISO code.', [], 'Admin.International.Notification'); + } + } elseif (Validate::isStateIsoCode(Tools::getValue('iso_code'))) { + $id_state = State::getIdByIso(Tools::getValue('iso_code'), Tools::getValue('id_country')); + if ($id_state && $id_state != Tools::getValue('id_' . $this->table)) { + $this->errors[] = $this->trans('This ISO code already exists. You cannot create two states with the same ISO code.', [], 'Admin.International.Notification'); + } + } + + /* Delete state */ + if (Tools::isSubmit('delete' . $this->table)) { + if ($this->access('delete')) { + if (Validate::isLoadedObject($object = $this->loadObject())) { + /** @var State $object */ + if (!$object->isUsed()) { + if ($object->delete()) { + Tools::redirectAdmin(self::$currentIndex . '&conf=1&token=' . (Tools::getValue('token') ? Tools::getValue('token') : $this->token)); + } + $this->errors[] = $this->trans('An error occurred during deletion.', [], 'Admin.Notifications.Error'); + } else { + $this->errors[] = $this->trans('This state was used in at least one address. It cannot be removed.', [], 'Admin.International.Notification'); + } + } else { + $this->errors[] = $this->trans('An error occurred while deleting the object.', [], 'Admin.Notifications.Error') . ' ' . $this->table . ' ' . $this->trans('(cannot load object)', [], 'Admin.Notifications.Error'); + } + } else { + $this->errors[] = $this->trans('You do not have permission to delete this.', [], 'Admin.Notifications.Error'); + } + } + + if (!count($this->errors)) { + parent::postProcess(); + } + } + + protected function displayAjaxStates() + { + $states = Db::getInstance()->executeS(' + SELECT s.id_state, s.name + FROM ' . _DB_PREFIX_ . 'state s + LEFT JOIN ' . _DB_PREFIX_ . 'country c ON (s.`id_country` = c.`id_country`) + WHERE s.id_country = ' . (int) (Tools::getValue('id_country')) . ' AND s.active = 1 AND c.`contains_states` = 1 + ORDER BY s.`name` ASC'); + + if (is_array($states) && !empty($states)) { + $list = ''; + if ((bool) Tools::getValue('no_empty') != true) { + $empty_value = (Tools::isSubmit('empty_value')) ? Tools::getValue('empty_value') : '-'; + $list = '' . Tools::htmlentitiesUTF8($empty_value) . '' . "\n"; + } + + foreach ($states as $state) { + $list .= '' . $state['name'] . '' . "\n"; + } + } else { + $list = 'false'; + } + + die($list); + } + + /** + * Allow the assignation of zone only if the form is displayed. + */ + protected function processBulkAffectZone() + { + $zone_to_affect = Tools::getValue('zone_to_affect'); + if ($zone_to_affect && $zone_to_affect !== 0) { + parent::processBulkAffectZone(); + } + + if (Tools::getIsset('submitBulkAffectZonestate')) { + $this->tpl_list_vars['assign_zone'] = true; + } + } +} diff --git a/controllers/admin/AdminStatsController.php b/controllers/admin/AdminStatsController.php new file mode 100644 index 00000000..b2632fe4 --- /dev/null +++ b/controllers/admin/AdminStatsController.php @@ -0,0 +1,1115 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ +use PrestaShop\PrestaShop\Core\Addon\Module\ModuleManagerBuilder; +use PrestaShop\PrestaShop\Core\Addon\Theme\Theme; +use PrestaShop\PrestaShop\Core\Addon\Theme\ThemeManagerBuilder; + +class AdminStatsControllerCore extends AdminStatsTabController +{ + public static function getVisits($unique, $date_from, $date_to, $granularity = false) + { + $visits = ($granularity == false) ? 0 : []; + $moduleManagerBuilder = ModuleManagerBuilder::getInstance(); + $moduleManager = $moduleManagerBuilder->build(); + + /** @var Gapi $gapi */ + $gapi = $moduleManager->isInstalled('gapi') ? Module::getInstanceByName('gapi') : false; + if (Validate::isLoadedObject($gapi) && $gapi->isConfigured()) { + $metric = $unique ? 'visitors' : 'visits'; + if ($result = $gapi->requestReportData( + $granularity ? 'ga:date' : '', + 'ga:' . $metric, + $date_from, + $date_to, + null, + null, + 1, + 5000 + ) + ) { + foreach ($result as $row) { + if ($granularity == 'day') { + $visits[strtotime( + preg_replace('/^([0-9]{4})([0-9]{2})([0-9]{2})$/', '$1-$2-$3', $row['dimensions']['date']) + )] = $row['metrics'][$metric]; + } elseif ($granularity == 'month') { + if (!isset( + $visits[strtotime( + preg_replace( + '/^([0-9]{4})([0-9]{2})([0-9]{2})$/', + '$1-$2-01', + $row['dimensions']['date'] + ) + )] + ) + ) { + $visits[strtotime( + preg_replace( + '/^([0-9]{4})([0-9]{2})([0-9]{2})$/', + '$1-$2-01', + $row['dimensions']['date'] + ) + )] = 0; + } + $visits[strtotime( + preg_replace('/^([0-9]{4})([0-9]{2})([0-9]{2})$/', '$1-$2-01', $row['dimensions']['date']) + )] += $row['metrics'][$metric]; + } else { + $visits = $row['metrics'][$metric]; + } + } + } + } else { + if ($granularity == 'day') { + $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( + ' + SELECT date(`date_add`) as date, COUNT(' . ($unique ? 'DISTINCT id_guest' : '*') . ') as visits + FROM `' . _DB_PREFIX_ . 'connections` + WHERE `date_add` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" + ' . Shop::addSqlRestriction() . ' + GROUP BY date(`date_add`)' + ); + foreach ($result as $row) { + $visits[strtotime($row['date'])] = $row['visits']; + } + } elseif ($granularity == 'month') { + $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( + ' + SELECT LEFT(LAST_DAY(`date_add`), 7) as date, COUNT(' . ($unique ? 'DISTINCT id_guest' : '*') . ') as visits + FROM `' . _DB_PREFIX_ . 'connections` + WHERE `date_add` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" + ' . Shop::addSqlRestriction() . ' + GROUP BY LAST_DAY(`date_add`)' + ); + foreach ($result as $row) { + $visits[strtotime($row['date'] . '-01')] = $row['visits']; + } + } else { + $visits = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(' . ($unique ? 'DISTINCT id_guest' : '*') . ') as visits + FROM `' . _DB_PREFIX_ . 'connections` + WHERE `date_add` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" + ' . Shop::addSqlRestriction() + ); + } + } + + return $visits; + } + + public static function getAbandonedCarts($date_from, $date_to) + { + return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(DISTINCT id_guest) + FROM `' . _DB_PREFIX_ . 'cart` + WHERE `date_add` BETWEEN "' . pSQL($date_from) . '" AND "' . pSQL($date_to) . '" + AND NOT EXISTS (SELECT 1 FROM `' . _DB_PREFIX_ . 'orders` WHERE `' . _DB_PREFIX_ . 'orders`.id_cart = `' . _DB_PREFIX_ . 'cart`.id_cart) + ' . Shop::addSqlRestriction() + ); + } + + public static function getInstalledModules() + { + return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(DISTINCT m.`id_module`) + FROM `' . _DB_PREFIX_ . 'module` m + ' . Shop::addSqlAssociation('module', 'm') + ); + } + + public static function getDisabledModules() + { + return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(*) + FROM `' . _DB_PREFIX_ . 'module` m + ' . Shop::addSqlAssociation('module', 'm', false) . ' + WHERE module_shop.id_module IS NULL OR m.active = 0' + ); + } + + public static function getModulesToUpdate() + { + $context = Context::getContext(); + $logged_on_addons = false; + if (isset($context->cookie->username_addons, $context->cookie->password_addons) + && !empty($context->cookie->username_addons) && !empty($context->cookie->password_addons) + ) { + $logged_on_addons = true; + } + $modules = Module::getModulesOnDisk(true, $logged_on_addons, $context->employee->id); + $upgrade_available = 0; + foreach ($modules as $km => $module) { + if ($module->installed && isset($module->version_addons) && $module->version_addons) { // SimpleXMLElement + ++$upgrade_available; + } + } + + return $upgrade_available; + } + + public static function getPercentProductStock() + { + $row = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow( + ' + SELECT SUM(IF(IFNULL(stock.quantity, 0) > 0, 1, 0)) AS with_stock, COUNT(*) AS products + FROM `' . _DB_PREFIX_ . 'product` p + ' . Shop::addSqlAssociation('product', 'p') . ' + LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute` pa ON p.id_product = pa.id_product + ' . Product::sqlStock('p', 'pa') . ' + WHERE product_shop.active = 1' + ); + + return round($row['products'] ? 100 * $row['with_stock'] / $row['products'] : 0, 2) . '%'; + } + + public static function getPercentProductOutOfStock() + { + $row = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow( + ' + SELECT SUM(IF(IFNULL(stock.quantity, 0) <= 0, 1, 0)) AS without_stock, COUNT(*) AS products + FROM `' . _DB_PREFIX_ . 'product` p + ' . Shop::addSqlAssociation('product', 'p') . ' + LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute` pa ON p.id_product = pa.id_product + ' . Product::sqlStock('p', 'pa') . ' + WHERE product_shop.active = 1' + ); + + return round($row['products'] ? 100 * $row['without_stock'] / $row['products'] : 0, 2) . '%'; + } + + public static function getProductAverageGrossMargin() + { + $sql = 'SELECT AVG(1 - (IF(IFNULL(product_attribute_shop.wholesale_price, 0) = 0, product_shop.wholesale_price,product_attribute_shop.wholesale_price) / (IFNULL(product_attribute_shop.price, 0) + product_shop.price))) + FROM `' . _DB_PREFIX_ . 'product` p + ' . Shop::addSqlAssociation('product', 'p') . ' + LEFT JOIN `' . _DB_PREFIX_ . 'product_attribute` pa ON p.id_product = pa.id_product + ' . Shop::addSqlAssociation('product_attribute', 'pa', false) . ' + WHERE product_shop.active = 1'; + $value = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql); + + return round(100 * $value, 2) . '%'; + } + + public static function getDisabledCategories() + { + return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(*) + FROM `' . _DB_PREFIX_ . 'category` c + ' . Shop::addSqlAssociation('category', 'c') . ' + WHERE c.active = 0' + ); + } + + public static function getTotalCategories() + { + return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(*) + FROM `' . _DB_PREFIX_ . 'category` c + ' . Shop::addSqlAssociation('category', 'c') + ); + } + + public static function getDisabledProducts() + { + return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(*) + FROM `' . _DB_PREFIX_ . 'product` p + ' . Shop::addSqlAssociation('product', 'p') . ' + WHERE product_shop.active = 0' + ); + } + + public static function getTotalProducts() + { + return (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(*) + FROM `' . _DB_PREFIX_ . 'product` p + ' . Shop::addSqlAssociation('product', 'p') + ); + } + + public static function getTotalSales($date_from, $date_to, $granularity = false) + { + if ($granularity == 'day') { + $sales = []; + $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( + ' + SELECT LEFT(`invoice_date`, 10) AS date, SUM((total_paid_tax_excl - total_shipping_tax_excl) / o.conversion_rate) AS sales + FROM `' . _DB_PREFIX_ . 'orders` o + LEFT JOIN `' . _DB_PREFIX_ . 'order_state` os ON o.current_state = os.id_order_state + WHERE `invoice_date` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" AND os.logable = 1 + ' . Shop::addSqlRestriction(false, 'o') . ' + GROUP BY LEFT(`invoice_date`, 10)' + ); + foreach ($result as $row) { + $sales[strtotime($row['date'])] = $row['sales']; + } + + return $sales; + } elseif ($granularity == 'month') { + $sales = []; + $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( + ' + SELECT LEFT(`invoice_date`, 7) AS date, SUM((total_paid_tax_excl - total_shipping_tax_excl) / o.conversion_rate) AS sales + FROM `' . _DB_PREFIX_ . 'orders` o + LEFT JOIN `' . _DB_PREFIX_ . 'order_state` os ON o.current_state = os.id_order_state + WHERE `invoice_date` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" AND os.logable = 1 + ' . Shop::addSqlRestriction(false, 'o') . ' + GROUP BY LEFT(`invoice_date`, 7)' + ); + foreach ($result as $row) { + $sales[strtotime($row['date'] . '-01')] = $row['sales']; + } + + return $sales; + } else { + return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT SUM((total_paid_tax_excl - total_shipping_tax_excl) / o.conversion_rate) + FROM `' . _DB_PREFIX_ . 'orders` o + LEFT JOIN `' . _DB_PREFIX_ . 'order_state` os ON o.current_state = os.id_order_state + WHERE `invoice_date` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" AND os.logable = 1 + ' . Shop::addSqlRestriction(false, 'o') + ); + } + } + + public static function get8020SalesCatalog($date_from, $date_to) + { + $distinct_products = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(DISTINCT od.product_id) + FROM `' . _DB_PREFIX_ . 'orders` o + LEFT JOIN `' . _DB_PREFIX_ . 'order_detail` od ON o.id_order = od.id_order + WHERE `invoice_date` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" + ' . Shop::addSqlRestriction(false, 'o') + ); + if (!$distinct_products) { + return '0%'; + } + + return round(100 * $distinct_products / AdminStatsController::getTotalProducts()) . '%'; + } + + public static function getOrders($date_from, $date_to, $granularity = false) + { + if ($granularity == 'day') { + $orders = []; + $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( + ' + SELECT LEFT(`invoice_date`, 10) AS date, COUNT(*) AS orders + FROM `' . _DB_PREFIX_ . 'orders` o + LEFT JOIN `' . _DB_PREFIX_ . 'order_state` os ON o.current_state = os.id_order_state + WHERE `invoice_date` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" AND os.logable = 1 + ' . Shop::addSqlRestriction(false, 'o') . ' + GROUP BY LEFT(`invoice_date`, 10)' + ); + foreach ($result as $row) { + $orders[strtotime($row['date'])] = $row['orders']; + } + + return $orders; + } elseif ($granularity == 'month') { + $orders = []; + $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( + ' + SELECT LEFT(`invoice_date`, 7) AS date, COUNT(*) AS orders + FROM `' . _DB_PREFIX_ . 'orders` o + LEFT JOIN `' . _DB_PREFIX_ . 'order_state` os ON o.current_state = os.id_order_state + WHERE `invoice_date` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" AND os.logable = 1 + ' . Shop::addSqlRestriction(false, 'o') . ' + GROUP BY LEFT(`invoice_date`, 7)' + ); + foreach ($result as $row) { + $orders[strtotime($row['date'] . '-01')] = $row['orders']; + } + + return $orders; + } else { + $orders = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(*) AS orders + FROM `' . _DB_PREFIX_ . 'orders` o + LEFT JOIN `' . _DB_PREFIX_ . 'order_state` os ON o.current_state = os.id_order_state + WHERE `invoice_date` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" AND os.logable = 1 + ' . Shop::addSqlRestriction(false, 'o') + ); + } + + return $orders; + } + + public static function getEmptyCategories() + { + $total = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(*) + FROM `' . _DB_PREFIX_ . 'category` c + ' . Shop::addSqlAssociation('category', 'c') . ' + AND c.`id_category` != ' . (int) Configuration::get('PS_ROOT_CATEGORY') + ); + $used = (int) Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(DISTINCT cp.id_category) + FROM `' . _DB_PREFIX_ . 'category` c + LEFT JOIN `' . _DB_PREFIX_ . 'category_product` cp ON c.id_category = cp.id_category + ' . Shop::addSqlAssociation('category', 'c') . ' + AND c.`id_category` != ' . (int) Configuration::get('PS_ROOT_CATEGORY') + ); + + return (int) ($total - $used); + } + + public static function getCustomerMainGender() + { + $row = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow( + ' + SELECT SUM(IF(g.id_gender IS NOT NULL, 1, 0)) AS total, SUM(IF(type = 0, 1, 0)) AS male, SUM(IF(type = 1, 1, 0)) AS female, SUM(IF(type = 2, 1, 0)) AS neutral + FROM `' . _DB_PREFIX_ . 'customer` c + LEFT JOIN `' . _DB_PREFIX_ . 'gender` g ON c.id_gender = g.id_gender + WHERE c.active = 1 ' . Shop::addSqlRestriction() + ); + + if (!$row['total']) { + return false; + } elseif ($row['male'] > $row['female'] && $row['male'] >= $row['neutral']) { + return ['type' => 'male', 'value' => round(100 * $row['male'] / $row['total'])]; + } elseif ($row['female'] >= $row['male'] && $row['female'] >= $row['neutral']) { + return ['type' => 'female', 'value' => round(100 * $row['female'] / $row['total'])]; + } + + return ['type' => 'neutral', 'value' => round(100 * $row['neutral'] / $row['total'])]; + } + + public static function getBestCategory($date_from, $date_to) + { + return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT ca.`id_category` + FROM `' . _DB_PREFIX_ . 'category` ca + LEFT JOIN `' . _DB_PREFIX_ . 'category_product` capr ON ca.`id_category` = capr.`id_category` + LEFT JOIN ( + SELECT pr.`id_product`, t.`totalPriceSold` + FROM `' . _DB_PREFIX_ . 'product` pr + LEFT JOIN ( + SELECT pr.`id_product`, + IFNULL(SUM(cp.`product_quantity`), 0) AS totalQuantitySold, + IFNULL(SUM(cp.`product_price` * cp.`product_quantity`), 0) / o.conversion_rate AS totalPriceSold + FROM `' . _DB_PREFIX_ . 'product` pr + LEFT OUTER JOIN `' . _DB_PREFIX_ . 'order_detail` cp ON pr.`id_product` = cp.`product_id` + LEFT JOIN `' . _DB_PREFIX_ . 'orders` o ON o.`id_order` = cp.`id_order` + WHERE o.invoice_date BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" + GROUP BY pr.`id_product` + ) t ON t.`id_product` = pr.`id_product` + ) t ON t.`id_product` = capr.`id_product` + WHERE ca.`level_depth` > 1 + GROUP BY ca.`id_category` + ORDER BY SUM(t.`totalPriceSold`) DESC' + ); + } + + public static function getMainCountry($date_from, $date_to) + { + $total_orders = AdminStatsController::getOrders($date_from, $date_to); + if (!$total_orders) { + return false; + } + $row = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow( + ' + SELECT a.id_country, COUNT(*) AS orders + FROM `' . _DB_PREFIX_ . 'orders` o + LEFT JOIN `' . _DB_PREFIX_ . 'address` a ON o.id_address_delivery = a.id_address + WHERE `invoice_date` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" + ' . Shop::addSqlRestriction() + ); + $row['orders'] = round(100 * $row['orders'] / $total_orders, 1); + + return $row; + } + + public static function getAverageCustomerAge() + { + $value = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT AVG(DATEDIFF("' . date('Y-m-d') . ' 00:00:00", birthday)) + FROM `' . _DB_PREFIX_ . 'customer` c + WHERE active = 1 + AND birthday IS NOT NULL AND birthday != "0000-00-00" ' . Shop::addSqlRestriction() + ); + + return round($value / 365); + } + + public static function getPendingMessages() + { + return CustomerThread::getTotalCustomerThreads( + 'status LIKE "%pending%" OR status = "open"' . Shop::addSqlRestriction() + ); + } + + public static function getAverageMessageResponseTime($date_from, $date_to) + { + $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( + ' + SELECT MIN(cm1.date_add) AS question, MIN(cm2.date_add) AS reply + FROM `' . _DB_PREFIX_ . 'customer_message` cm1 + INNER JOIN `' . _DB_PREFIX_ . 'customer_message` cm2 ON (cm1.id_customer_thread = cm2.id_customer_thread AND cm1.date_add < cm2.date_add) + JOIN `' . _DB_PREFIX_ . 'customer_thread` ct ON (cm1.id_customer_thread = ct.id_customer_thread) + WHERE cm1.`date_add` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" + AND cm1.id_employee = 0 AND cm2.id_employee != 0 + ' . Shop::addSqlRestriction() . ' + GROUP BY cm1.id_customer_thread' + ); + $total_questions = $total_replies = $threads = 0; + foreach ($result as $row) { + ++$threads; + $total_questions += strtotime($row['question']); + $total_replies += strtotime($row['reply']); + } + if (!$threads) { + return 0; + } + + return round(($total_replies - $total_questions) / $threads / 3600, 1); + } + + public static function getMessagesPerThread($date_from, $date_to) + { + $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( + ' + SELECT COUNT(*) AS messages + FROM `' . _DB_PREFIX_ . 'customer_thread` ct + LEFT JOIN `' . _DB_PREFIX_ . 'customer_message` cm ON (ct.id_customer_thread = cm.id_customer_thread) + WHERE ct.`date_add` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" + ' . Shop::addSqlRestriction() . ' + AND STATUS = "closed" + GROUP BY ct.id_customer_thread' + ); + $threads = $messages = 0; + foreach ($result as $row) { + ++$threads; + $messages += $row['messages']; + } + if (!$threads) { + return 0; + } + + return round($messages / $threads, 1); + } + + public static function getPurchases($date_from, $date_to, $granularity = false) + { + if ($granularity == 'day') { + $purchases = []; + $result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS( + ' + SELECT + LEFT(`invoice_date`, 10) as date, + SUM(od.`product_quantity` * IF( + od.`purchase_supplier_price` > 0, + od.`purchase_supplier_price` / `conversion_rate`, + od.`original_product_price` * ' . (int) Configuration::get('CONF_AVERAGE_PRODUCT_MARGIN') . ' / 100 + )) as total_purchase_price + FROM `' . _DB_PREFIX_ . 'orders` o + LEFT JOIN `' . _DB_PREFIX_ . 'order_detail` od ON o.id_order = od.id_order + LEFT JOIN `' . _DB_PREFIX_ . 'order_state` os ON o.current_state = os.id_order_state + WHERE `invoice_date` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" AND os.logable = 1 + ' . Shop::addSqlRestriction(false, 'o') . ' + GROUP BY LEFT(`invoice_date`, 10)' + ); + foreach ($result as $row) { + $purchases[strtotime($row['date'])] = $row['total_purchase_price']; + } + + return $purchases; + } else { + return Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT SUM(od.`product_quantity` * IF( + od.`purchase_supplier_price` > 0, + od.`purchase_supplier_price` / `conversion_rate`, + od.`original_product_price` * ' . (int) Configuration::get('CONF_AVERAGE_PRODUCT_MARGIN') . ' / 100 + )) as total_purchase_price + FROM `' . _DB_PREFIX_ . 'orders` o + LEFT JOIN `' . _DB_PREFIX_ . 'order_detail` od ON o.id_order = od.id_order + LEFT JOIN `' . _DB_PREFIX_ . 'order_state` os ON o.current_state = os.id_order_state + WHERE `invoice_date` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" AND os.logable = 1 + ' . Shop::addSqlRestriction(false, 'o') + ); + } + } + + public static function getExpenses($date_from, $date_to, $granularity = false) + { + $expenses = ($granularity == 'day' ? [] : 0); + + $orders = Db::getInstance()->executeS( + ' + SELECT + LEFT(`invoice_date`, 10) AS date, + total_paid_tax_incl / o.conversion_rate AS total_paid_tax_incl, + total_shipping_tax_excl / o.conversion_rate AS total_shipping_tax_excl, + o.module, + a.id_country, + o.id_currency, + c.id_reference AS carrier_reference + FROM `' . _DB_PREFIX_ . 'orders` o + LEFT JOIN `' . _DB_PREFIX_ . 'address` a ON o.id_address_delivery = a.id_address + LEFT JOIN `' . _DB_PREFIX_ . 'carrier` c ON o.id_carrier = c.id_carrier + LEFT JOIN `' . _DB_PREFIX_ . 'order_state` os ON o.current_state = os.id_order_state + WHERE `invoice_date` BETWEEN "' . pSQL($date_from) . ' 00:00:00" AND "' . pSQL($date_to) . ' 23:59:59" AND os.logable = 1 + ' . Shop::addSqlRestriction(false, 'o') + ); + foreach ($orders as $order) { + // Add flat fees for this order + $flat_fees = Configuration::get('CONF_ORDER_FIXED') + ( + $order['id_currency'] == Configuration::get('PS_CURRENCY_DEFAULT') + ? Configuration::get('CONF_' . strtoupper($order['module']) . '_FIXED') + : Configuration::get('CONF_' . strtoupper($order['module']) . '_FIXED_FOREIGN') + ); + + // Add variable fees for this order + $var_fees = $order['total_paid_tax_incl'] * ( + $order['id_currency'] == Configuration::get('PS_CURRENCY_DEFAULT') + ? Configuration::get('CONF_' . strtoupper($order['module']) . '_VAR') + : Configuration::get('CONF_' . strtoupper($order['module']) . '_VAR_FOREIGN') + ) / 100; + + // Add shipping fees for this order + $shipping_fees = $order['total_shipping_tax_excl'] * ( + $order['id_country'] == Configuration::get('PS_COUNTRY_DEFAULT') + ? Configuration::get('CONF_' . strtoupper($order['carrier_reference']) . '_SHIP') + : Configuration::get('CONF_' . strtoupper($order['carrier_reference']) . '_SHIP_OVERSEAS') + ) / 100; + + // Tally up these fees + if ($granularity == 'day') { + if (!isset($expenses[strtotime($order['date'])])) { + $expenses[strtotime($order['date'])] = 0; + } + $expenses[strtotime($order['date'])] += $flat_fees + $var_fees + $shipping_fees; + } else { + $expenses += $flat_fees + $var_fees + $shipping_fees; + } + } + + return $expenses; + } + + public function displayAjaxGetKpi() + { + if (!$this->access('view')) { + return die(json_encode(['error' => 'You do not have the right permission'])); + } + + $currency = new Currency(Configuration::get('PS_CURRENCY_DEFAULT')); + $tooltip = null; + switch (Tools::getValue('kpi')) { + case 'conversion_rate': + $visitors = AdminStatsController::getVisits( + true, + date('Y-m-d', strtotime('-31 day')), + date('Y-m-d', strtotime('-1 day')), + false /*'day'*/ + ); + $orders = AdminStatsController::getOrders( + date('Y-m-d', strtotime('-31 day')), + date('Y-m-d', strtotime('-1 day')), + false /*'day'*/ + ); + + // $data = array(); + // $from = strtotime(date('Y-m-d 00:00:00', strtotime('-31 day'))); + // $to = strtotime(date('Y-m-d 23:59:59', strtotime('-1 day'))); + // for ($date = $from; $date <= $to; $date = strtotime('+1 day', $date)) + // if (isset($visitors[$date]) && $visitors[$date]) + // $data[$date] = round(100 * ((isset($orders[$date]) && $orders[$date]) ? $orders[$date] : 0) / $visitors[$date], 2); + // else + // $data[$date] = 0; + + $visits_sum = $visitors; //array_sum($visitors); + $orders_sum = $orders; //array_sum($orders); + if ($visits_sum) { + $value = round(100 * $orders_sum / $visits_sum, 2); + } elseif ($orders_sum) { + $value = '∞'; + } else { + $value = 0; + } + $value .= '%'; + + // ConfigurationKPI::updateValue('CONVERSION_RATE_CHART', json_encode($data)); + ConfigurationKPI::updateValue('CONVERSION_RATE', $value); + ConfigurationKPI::updateValue( + 'CONVERSION_RATE_EXPIRE', + strtotime(date('Y-m-d 00:00:00', strtotime('+1 day'))) + ); + + break; + + case 'abandoned_cart': + $value = AdminStatsController::getAbandonedCarts( + date('Y-m-d H:i:s', strtotime('-2 day')), + date('Y-m-d H:i:s', strtotime('-1 day')) + ); + ConfigurationKPI::updateValue('ABANDONED_CARTS', $value); + ConfigurationKPI::updateValue('ABANDONED_CARTS_EXPIRE', strtotime('+1 hour')); + + break; + + case 'installed_modules': + $value = AdminStatsController::getInstalledModules(); + ConfigurationKPI::updateValue('INSTALLED_MODULES', $value); + ConfigurationKPI::updateValue('INSTALLED_MODULES_EXPIRE', strtotime('+2 min')); + + break; + + case 'disabled_modules': + $value = AdminStatsController::getDisabledModules(); + ConfigurationKPI::updateValue('DISABLED_MODULES', $value); + ConfigurationKPI::updateValue('DISABLED_MODULES_EXPIRE', strtotime('+2 min')); + + break; + + case 'update_modules': + $value = AdminStatsController::getModulesToUpdate(); + ConfigurationKPI::updateValue('UPDATE_MODULES', $value); + ConfigurationKPI::updateValue('UPDATE_MODULES_EXPIRE', strtotime('+2 min')); + + break; + + case 'percent_product_stock': + $value = AdminStatsController::getPercentProductStock(); + ConfigurationKPI::updateValue('PERCENT_PRODUCT_STOCK', $value); + ConfigurationKPI::updateValue('PERCENT_PRODUCT_STOCK_EXPIRE', strtotime('+4 hour')); + + break; + + case 'percent_product_out_of_stock': + $value = AdminStatsController::getPercentProductOutOfStock(); + $tooltip = $this->trans( + '%value% of your products for sale are out of stock.', + ['%value%' => $value], + 'Admin.Stats.Help' + ); + ConfigurationKPI::updateValue('PERCENT_PRODUCT_OUT_OF_STOCK', $value); + ConfigurationKPI::updateValue('PERCENT_PRODUCT_OUT_OF_STOCK_EXPIRE', strtotime('+4 hour')); + + break; + + case 'product_avg_gross_margin': + $value = AdminStatsController::getProductAverageGrossMargin(); + $tooltip = $this->trans( + 'Gross margin expressed in percentage assesses how cost-effectively you sell your goods. Out of $100, you will retain $%value% to cover profit and expenses.', + ['%value%' => $value], + 'Admin.Stats.Help' + ); + ConfigurationKPI::updateValue('PRODUCT_AVG_GROSS_MARGIN', $value); + ConfigurationKPI::updateValue('PRODUCT_AVG_GROSS_MARGIN_EXPIRE', strtotime('+6 hour')); + + break; + + case 'disabled_categories': + $value = AdminStatsController::getDisabledCategories(); + ConfigurationKPI::updateValue('DISABLED_CATEGORIES', $value); + ConfigurationKPI::updateValue('DISABLED_CATEGORIES_EXPIRE', strtotime('+2 hour')); + + break; + + case 'disabled_products': + $value = round( + 100 * AdminStatsController::getDisabledProducts() / AdminStatsController::getTotalProducts(), + 2 + ) . '%'; + $tooltip = $this->trans( + '%value% of your products are disabled and not visible to your customers', + ['%value%' => $value], + 'Admin.Stats.Help' + ); + ConfigurationKPI::updateValue('DISABLED_PRODUCTS', $value); + ConfigurationKPI::updateValue('DISABLED_PRODUCTS_EXPIRE', strtotime('+2 hour')); + + break; + + case '8020_sales_catalog': + $value = AdminStatsController::get8020SalesCatalog(date('Y-m-d', strtotime('-30 days')), date('Y-m-d')); + $tooltip = $this->trans( + 'Within your catalog, %value% of your products have had sales in the last 30 days', + ['%value%' => $value], + 'Admin.Stats.Help' + ); + $value = $this->trans('%value%% of your Catalog', ['%value%' => $value], 'Admin.Stats.Feature'); + ConfigurationKPI::updateValue('8020_SALES_CATALOG', $value); + ConfigurationKPI::updateValue('8020_SALES_CATALOG_EXPIRE', strtotime('+12 hour')); + + break; + + case 'empty_categories': + $value = AdminStatsController::getEmptyCategories(); + ConfigurationKPI::updateValue('EMPTY_CATEGORIES', $value); + ConfigurationKPI::updateValue('EMPTY_CATEGORIES_EXPIRE', strtotime('+2 hour')); + + break; + + case 'customer_main_gender': + $value = AdminStatsController::getCustomerMainGender(); + + if ($value === false) { + $value = $this->trans('No customers', [], 'Admin.Stats.Feature'); + } elseif ($value['type'] == 'female') { + $value = $this->trans('%percentage%% Female Customers', ['%percentage%' => $value['value']], 'Admin.Stats.Feature'); + } elseif ($value['type'] == 'male') { + $value = $this->trans('%percentage%% Male Customers', ['%percentage%' => $value['value']], 'Admin.Stats.Feature'); + } else { + $value = $this->trans('%percentage%% Neutral Customers', ['%percentage%' => $value['value']], 'Admin.Stats.Feature'); + } + + ConfigurationKPI::updateValue('CUSTOMER_MAIN_GENDER', [$this->context->language->id => $value]); + ConfigurationKPI::updateValue( + 'CUSTOMER_MAIN_GENDER_EXPIRE', + [$this->context->language->id => strtotime('+1 day')] + ); + + break; + + case 'avg_customer_age': + $value = $this->trans('%value% years', ['%value%' => AdminStatsController::getAverageCustomerAge()], 'Admin.Stats.Feature'); + ConfigurationKPI::updateValue('AVG_CUSTOMER_AGE', [$this->context->language->id => $value]); + ConfigurationKPI::updateValue( + 'AVG_CUSTOMER_AGE_EXPIRE', + [$this->context->language->id => strtotime('+1 day')] + ); + + break; + + case 'pending_messages': + $value = (int) AdminStatsController::getPendingMessages(); + ConfigurationKPI::updateValue('PENDING_MESSAGES', $value); + ConfigurationKPI::updateValue('PENDING_MESSAGES_EXPIRE', strtotime('+5 min')); + + break; + + case 'avg_msg_response_time': + $value = $this->trans('%average% hours', ['%average%' => AdminStatsController::getAverageMessageResponseTime( + date('Y-m-d', strtotime('-31 day')), + date('Y-m-d', strtotime('-1 day')) + )], 'Admin.Stats.Feature'); + ConfigurationKPI::updateValue('AVG_MSG_RESPONSE_TIME', $value); + ConfigurationKPI::updateValue('AVG_MSG_RESPONSE_TIME_EXPIRE', strtotime('+4 hour')); + + break; + + case 'messages_per_thread': + $value = round( + AdminStatsController::getMessagesPerThread( + date('Y-m-d', strtotime('-31 day')), + date('Y-m-d', strtotime('-1 day')) + ), + 1 + ); + ConfigurationKPI::updateValue('MESSAGES_PER_THREAD', $value); + ConfigurationKPI::updateValue('MESSAGES_PER_THREAD_EXPIRE', strtotime('+12 hour')); + + break; + + case 'newsletter_registrations': + $moduleManagerBuilder = ModuleManagerBuilder::getInstance(); + $moduleManager = $moduleManagerBuilder->build(); + + $value = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(*) + FROM `' . _DB_PREFIX_ . 'customer` + WHERE newsletter = 1 + ' . Shop::addSqlRestriction(Shop::SHARE_ORDER) + ); + if ($moduleManager->isInstalled('ps_emailsubscription')) { + $value += Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(*) + FROM `' . _DB_PREFIX_ . 'emailsubscription` + WHERE active = 1 + ' . Shop::addSqlRestriction(Shop::SHARE_ORDER) + ); + } + + ConfigurationKPI::updateValue('NEWSLETTER_REGISTRATIONS', $value); + ConfigurationKPI::updateValue('NEWSLETTER_REGISTRATIONS_EXPIRE', strtotime('+6 hour')); + + break; + + case 'enabled_languages': + $value = Language::countActiveLanguages(); + ConfigurationKPI::updateValue('ENABLED_LANGUAGES', $value); + ConfigurationKPI::updateValue('ENABLED_LANGUAGES_EXPIRE', strtotime('+1 min')); + + break; + + case 'frontoffice_translations': + $themes = (new ThemeManagerBuilder($this->context, Db::getInstance())) + ->buildRepository() + ->getList(); + $languages = Language::getLanguages(); + $total = $translated = 0; + foreach ($themes as $theme) { + /* @var Theme $theme */ + foreach ($languages as $language) { + $kpi_key = substr(strtoupper($theme->getName() . '_' . $language['iso_code']), 0, 16); + $total += ConfigurationKPI::get('TRANSLATE_TOTAL_' . $kpi_key); + $translated += ConfigurationKPI::get('TRANSLATE_DONE_' . $kpi_key); + } + } + $value = 0; + if ($translated) { + $value = round(100 * $translated / $total, 1); + } + $value .= '%'; + ConfigurationKPI::updateValue('FRONTOFFICE_TRANSLATIONS', $value); + ConfigurationKPI::updateValue('FRONTOFFICE_TRANSLATIONS_EXPIRE', strtotime('+2 min')); + + break; + + case 'main_country': + if (!($row = AdminStatsController::getMainCountry( + date('Y-m-d', strtotime('-30 day')), + date('Y-m-d') + )) + ) { + $value = $this->trans('No orders', [], 'Admin.Stats.Feature'); + } else { + $country = new Country($row['id_country'], $this->context->language->id); + $value = $this->trans( + '%d%% %s', + ['%d%%' => $row['orders'], '%s' => $country->name], + 'Admin.Stats.Feature' + ); + } + + ConfigurationKPI::updateValue('MAIN_COUNTRY', [$this->context->language->id => $value]); + ConfigurationKPI::updateValue( + 'MAIN_COUNTRY_EXPIRE', + [$this->context->language->id => strtotime('+1 day')] + ); + + break; + + case 'orders_per_customer': + $value = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(*) + FROM `' . _DB_PREFIX_ . 'customer` c + WHERE c.active = 1 + ' . Shop::addSqlRestriction() + ); + if ($value) { + $orders = Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue( + ' + SELECT COUNT(*) + FROM `' . _DB_PREFIX_ . 'orders` o + WHERE o.valid = 1 + ' . Shop::addSqlRestriction() + ); + $value = round($orders / $value, 2); + } + + ConfigurationKPI::updateValue('ORDERS_PER_CUSTOMER', $value); + ConfigurationKPI::updateValue('ORDERS_PER_CUSTOMER_EXPIRE', strtotime('+1 day')); + + break; + + case 'average_order_value': + $row = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow( + ' + SELECT + COUNT(`id_order`) AS orders, + SUM(`total_paid_tax_excl` / `conversion_rate`) AS total_paid_tax_excl + FROM `' . _DB_PREFIX_ . 'orders` + WHERE `invoice_date` BETWEEN "' . pSQL(date('Y-m-d', strtotime('-31 day'))) . ' 00:00:00" AND "' . pSQL( + date('Y-m-d', strtotime('-1 day')) + ) . ' 23:59:59" + ' . Shop::addSqlRestriction() + ); + $value = $this->context->getCurrentLocale()->formatPrice( + $row['orders'] ? $row['total_paid_tax_excl'] / $row['orders'] : 0, + $currency->iso_code + ); + ConfigurationKPI::updateValue('AVG_ORDER_VALUE', $value); + ConfigurationKPI::updateValue( + 'AVG_ORDER_VALUE_EXPIRE', + strtotime(date('Y-m-d 00:00:00', strtotime('+1 day'))) + ); + + break; + + case 'netprofit_visit': + $date_from = date('Y-m-d', strtotime('-31 day')); + $date_to = date('Y-m-d', strtotime('-1 day')); + + $total_visitors = AdminStatsController::getVisits(false, $date_from, $date_to); + $net_profits = AdminStatsController::getTotalSales($date_from, $date_to); + $net_profits -= AdminStatsController::getExpenses($date_from, $date_to); + $net_profits -= AdminStatsController::getPurchases($date_from, $date_to); + + if ($total_visitors) { + $value = $this->context->getCurrentLocale()->formatPrice($net_profits / $total_visitors, $currency->iso_code); + } elseif ($net_profits) { + $value = '∞'; + } else { + $value = $this->context->getCurrentLocale()->formatPrice(0, $currency->iso_code); + } + + ConfigurationKPI::updateValue('NETPROFIT_VISIT', $value); + ConfigurationKPI::updateValue( + 'NETPROFIT_VISIT_EXPIRE', + strtotime(date('Y-m-d 00:00:00', strtotime('+1 day'))) + ); + + break; + + case 'products_per_category': + $products = AdminStatsController::getTotalProducts(); + $categories = AdminStatsController::getTotalCategories(); + $value = round($products / $categories); + ConfigurationKPI::updateValue('PRODUCTS_PER_CATEGORY', $value); + ConfigurationKPI::updateValue('PRODUCTS_PER_CATEGORY_EXPIRE', strtotime('+1 hour')); + + break; + + case 'top_category': + if (!($id_category = AdminStatsController::getBestCategory( + date('Y-m-d', strtotime('-1 month')), + date('Y-m-d') + ))) { + $value = $this->trans('No category', [], 'Admin.Stats.Feature'); + } else { + $category = new Category($id_category, $this->context->language->id); + $value = $category->name; + } + + ConfigurationKPI::updateValue('TOP_CATEGORY', [$this->context->language->id => $value]); + ConfigurationKPI::updateValue( + 'TOP_CATEGORY_EXPIRE', + [$this->context->language->id => strtotime('+1 day')] + ); + + break; + + default: + $value = false; + } + if ($value !== false) { + $array = ['value' => $value, 'tooltip' => $tooltip]; + if (isset($data)) { + $array['data'] = $data; + } + die(json_encode($array)); + } + die(json_encode(['has_errors' => true])); + } + + /** + * Display graphs on the stats page from module data. + */ + public function displayAjaxGraphDraw() + { + if (!$this->access('view')) { + return die(json_encode(['error' => 'You do not have the right permission'])); + } + + $module = Tools::getValue('module'); + $render = Tools::getValue('render'); + $type = Tools::getValue('type'); + $option = Tools::getValue('option'); + $layers = Tools::getValue('layers'); + $width = Tools::getValue('width'); + $height = Tools::getValue('height'); + $id_employee = Tools::getValue('id_employee'); + $id_lang = Tools::getValue('id_lang'); + + $graph = Module::getInstanceByName($module); + if (false === $graph) { + $this->ajaxRender(Tools::displayError()); + + return; + } + + $graph->setEmployee($id_employee); + $graph->setLang($id_lang); + if ($option) { + $graph->setOption($option, $layers); + } + + $graph->create($render, $type, $width, $height, $layers); + $graph->draw(); + } + + /** + * Display grid with module data on the stats page. + */ + public function displayAjaxGraphGrid() + { + if (!$this->access('view')) { + return die(json_encode(['error' => 'You do not have the right permission'])); + } + + $module = Tools::getValue('module'); + $render = Tools::getValue('render'); + $type = Tools::getValue('type'); + $option = Tools::getValue('option'); + $width = (int) (Tools::getValue('width', 600)); + $height = (int) (Tools::getValue('height', 920)); + $start = (int) (Tools::getValue('start', 0)); + $limit = (int) (Tools::getValue('limit', 40)); + $sort = Tools::getValue('sort', 0); // Should be a String. Default value is an Integer because we don't know what can be the name of the column to sort. + $dir = Tools::getValue('dir', 0); // Should be a String : Either ASC or DESC + $id_employee = (int) (Tools::getValue('id_employee')); + $id_lang = (int) (Tools::getValue('id_lang')); + + $grid = Module::getInstanceByName($module); + if (false === $grid) { + $this->ajaxRender(Tools::displayError()); + + return; + } + + $grid->setEmployee($id_employee); + $grid->setLang($id_lang); + if ($option) { + $grid->setOption($option); + } + + $grid->create($render, $type, $width, $height, $start, $limit, $sort, $dir); + $grid->render(); + } +} diff --git a/controllers/admin/AdminStatsTabController.php b/controllers/admin/AdminStatsTabController.php new file mode 100644 index 00000000..aa8de784 --- /dev/null +++ b/controllers/admin/AdminStatsTabController.php @@ -0,0 +1,306 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ +abstract class AdminStatsTabControllerCore extends AdminController +{ + public function init() + { + parent::init(); + $this->bootstrap = true; + $this->action = 'view'; + $this->display = 'view'; + } + + public function initContent() + { + if ($this->ajax) { + return; + } + + $this->toolbar_title = $this->trans('Stats', [], 'Admin.Stats.Feature'); + + if ($this->display == 'view') { + // Some controllers use the view action without an object + if ($this->className) { + $this->loadObject(true); + } + $this->content .= $this->renderView(); + } + + $this->content .= $this->displayMenu(); + $this->content .= $this->displayCalendar(); + $this->content .= $this->displayStats(); + + $this->context->smarty->assign([ + 'content' => $this->content, + ]); + } + + public function initPageHeaderToolbar() + { + parent::initPageHeaderToolbar(); + unset($this->page_header_toolbar_btn['back']); + } + + public function displayCalendar() + { + return AdminStatsTabController::displayCalendarForm([ + 'Calendar' => $this->trans('Calendar', [], 'Admin.Global'), + 'Day' => $this->trans('Day', [], 'Admin.Global'), + 'Month' => $this->trans('Month', [], 'Admin.Global'), + 'Year' => $this->trans('Year', [], 'Admin.Global'), + 'From' => $this->trans('From:', [], 'Admin.Global'), + 'To' => $this->trans('To:', [], 'Admin.Global'), + 'Save' => $this->trans('Save', [], 'Admin.Global'), + ], $this->token); + } + + public static function displayCalendarForm($translations, $token, $action = null, $table = null, $identifier = null, $id = null) + { + $context = Context::getContext(); + + $tpl = $context->controller->createTemplate('calendar.tpl'); + + $context->controller->addJqueryUI('ui.datepicker'); + + if ($identifier === null && Tools::getValue('module')) { + $identifier = 'module'; + $id = Tools::getValue('module'); + } + + $action = Context::getContext()->link->getAdminLink('AdminStats'); + $action .= ($action && $table ? '&' . Tools::safeOutput($action) : ''); + $action .= ($identifier && $id ? '&' . Tools::safeOutput($identifier) . '=' . (int) $id : ''); + $module = Tools::getValue('module'); + $action .= ($module ? '&module=' . Tools::safeOutput($module) : ''); + $action .= (($id_product = Tools::getValue('id_product')) ? '&id_product=' . Tools::safeOutput($id_product) : ''); + $tpl->assign([ + 'current' => self::$currentIndex, + 'token' => $token, + 'action' => $action, + 'table' => $table, + 'identifier' => $identifier, + 'id' => $id, + 'translations' => $translations, + 'datepickerFrom' => Tools::getValue('datepickerFrom', $context->employee->stats_date_from), + 'datepickerTo' => Tools::getValue('datepickerTo', $context->employee->stats_date_to), + ]); + + return $tpl->fetch(); + } + + /* Not used anymore, but still work */ + protected function displayEngines() + { + $tpl = $this->createTemplate('engines.tpl'); + + $autoclean_period = [ + 'never' => $this->trans('Never', [], 'Admin.Global'), + 'week' => $this->trans('Week', [], 'Admin.Global'), + 'month' => $this->trans('Month', [], 'Admin.Global'), + 'year' => $this->trans('Year', [], 'Admin.Global'), + ]; + + $tpl->assign([ + 'current' => self::$currentIndex, + 'token' => $this->token, + 'graph_engine' => Configuration::get('PS_STATS_RENDER'), + 'grid_engine' => Configuration::get('PS_STATS_GRID_RENDER'), + 'auto_clean' => Configuration::get('PS_STATS_OLD_CONNECT_AUTO_CLEAN'), + 'array_graph_engines' => ModuleGraphEngine::getGraphEngines(), + 'array_grid_engines' => ModuleGridEngine::getGridEngines(), + 'array_auto_clean' => $autoclean_period, + ]); + + return $tpl->fetch(); + } + + public function displayMenu() + { + $tpl = $this->createTemplate('menu.tpl'); + + $modules = $this->getModules(); + $module_instance = []; + foreach ($modules as $m => $module) { + if ($module_instance[$module['name']] = Module::getInstanceByName($module['name'])) { + $modules[$m]['displayName'] = $module_instance[$module['name']]->displayName; + } else { + unset( + $module_instance[$module['name']], + $modules[$m] + ); + } + } + + uasort($modules, [$this, 'checkModulesNames']); + + $tpl->assign([ + 'current' => self::$currentIndex, + 'current_module_name' => Tools::getValue('module', 'statsforecast'), + 'token' => $this->token, + 'modules' => $modules, + 'module_instance' => $module_instance, + ]); + + return $tpl->fetch(); + } + + public function checkModulesNames($a, $b) + { + return (bool) ($a['displayName'] > $b['displayName']); + } + + protected function getModules() + { + return array_map( + function ($moduleArray) {return ['name' => $moduleArray['module']]; }, + Hook::getHookModuleExecList('displayAdminStatsModules') + ); + } + + public function displayStats() + { + $tpl = $this->createTemplate('stats.tpl'); + + if ((!($module_name = Tools::getValue('module')) || !Validate::isModuleName($module_name)) && ($module_instance = Module::getInstanceByName('statsforecast')) && $module_instance->active) { + $module_name = 'statsforecast'; + } + + if ($module_name) { + $_GET['module'] = $module_name; + + if (!isset($module_instance)) { + $module_instance = Module::getInstanceByName($module_name); + } + + if ($module_instance && $module_instance->active) { + $hook = Hook::exec('displayAdminStatsModules', null, $module_instance->id); + } + } + + $tpl->assign([ + 'module_name' => $module_name, + 'module_instance' => isset($module_instance) ? $module_instance : null, + 'hook' => isset($hook) ? $hook : null, + ]); + + return $tpl->fetch(); + } + + public function postProcess() + { + $this->context = Context::getContext(); + + $this->processDateRange(); + + if (Tools::getValue('submitSettings')) { + if ($this->access('edit')) { + self::$currentIndex .= '&module=' . Tools::getValue('module'); + Configuration::updateValue('PS_STATS_RENDER', Tools::getValue('PS_STATS_RENDER', Configuration::get('PS_STATS_RENDER'))); + Configuration::updateValue('PS_STATS_GRID_RENDER', Tools::getValue('PS_STATS_GRID_RENDER', Configuration::get('PS_STATS_GRID_RENDER'))); + Configuration::updateValue('PS_STATS_OLD_CONNECT_AUTO_CLEAN', Tools::getValue('PS_STATS_OLD_CONNECT_AUTO_CLEAN', Configuration::get('PS_STATS_OLD_CONNECT_AUTO_CLEAN'))); + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error'); + } + } + } + + public function processDateRange() + { + if (Tools::isSubmit('submitDatePicker')) { + if ((!Validate::isDate($from = Tools::getValue('datepickerFrom')) || !Validate::isDate($to = Tools::getValue('datepickerTo'))) || (strtotime($from) > strtotime($to))) { + $this->errors[] = $this->trans('The specified date is invalid.', [], 'Admin.Stats.Notification'); + } + } + if (Tools::isSubmit('submitDateDay')) { + $from = date('Y-m-d'); + $to = date('Y-m-d'); + } + if (Tools::isSubmit('submitDateDayPrev')) { + $yesterday = time() - 60 * 60 * 24; + $from = date('Y-m-d', $yesterday); + $to = date('Y-m-d', $yesterday); + } + if (Tools::isSubmit('submitDateMonth')) { + $from = date('Y-m-01'); + $to = date('Y-m-t'); + } + if (Tools::isSubmit('submitDateMonthPrev')) { + $m = (date('m') == 1 ? 12 : date('m') - 1); + $y = ($m == 12 ? date('Y') - 1 : date('Y')); + $from = $y . '-' . $m . '-01'; + $to = $y . '-' . $m . date('-t', mktime(12, 0, 0, $m, 15, $y)); + } + if (Tools::isSubmit('submitDateYear')) { + $from = date('Y-01-01'); + $to = date('Y-12-31'); + } + if (Tools::isSubmit('submitDateYearPrev')) { + $from = (date('Y') - 1) . date('-01-01'); + $to = (date('Y') - 1) . date('-12-31'); + } + if (isset($from, $to) && !count($this->errors)) { + $this->context->employee->stats_date_from = $from; + $this->context->employee->stats_date_to = $to; + $this->context->employee->update(); + if (!$this->isXmlHttpRequest()) { + Tools::redirectAdmin($_SERVER['REQUEST_URI']); + } + } + } + + public function ajaxProcessSetDashboardDateRange() + { + $this->processDateRange(); + + if ($this->isXmlHttpRequest()) { + if (is_array($this->errors) && count($this->errors)) { + die(json_encode( + [ + 'has_errors' => true, + 'errors' => [$this->errors], + 'date_from' => $this->context->employee->stats_date_from, + 'date_to' => $this->context->employee->stats_date_to, ] + )); + } else { + die(json_encode( + [ + 'has_errors' => false, + 'date_from' => $this->context->employee->stats_date_from, + 'date_to' => $this->context->employee->stats_date_to, ] + )); + } + } + } + + protected function getDate() + { + $year = isset($this->context->cookie->stats_year) ? $this->context->cookie->stats_year : date('Y'); + $month = isset($this->context->cookie->stats_month) ? sprintf('%02d', $this->context->cookie->stats_month) : '%'; + $day = isset($this->context->cookie->stats_day) ? sprintf('%02d', $this->context->cookie->stats_day) : '%'; + + return $year . '-' . $month . '-' . $day; + } +} diff --git a/controllers/admin/AdminStatusesController.php b/controllers/admin/AdminStatusesController.php new file mode 100644 index 00000000..e3d721d9 --- /dev/null +++ b/controllers/admin/AdminStatusesController.php @@ -0,0 +1,734 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +/** + * @property OrderState $object + */ +class AdminStatusesControllerCore extends AdminController +{ + public function __construct() + { + $this->bootstrap = true; + $this->table = 'order_state'; + $this->className = 'OrderState'; + $this->lang = true; + $this->deleted = true; + $this->colorOnBackground = false; + $this->multishop_context = Shop::CONTEXT_ALL; + $this->imageType = 'gif'; + $this->fieldImageSettings = [ + 'name' => 'icon', + 'dir' => 'os', + ]; + + parent::__construct(); + + $this->bulk_actions = ['delete' => ['text' => $this->trans('Delete selected', [], 'Admin.Actions'), 'confirm' => $this->trans('Delete selected items?', [], 'Admin.Notifications.Warning')]]; + } + + public function init() + { + if (Tools::isSubmit('addorder_return_state')) { + $this->display = 'add'; + } + if (Tools::isSubmit('updateorder_return_state')) { + $this->display = 'edit'; + } + + return parent::init(); + } + + /** + * init all variables to render the order status list. + */ + protected function initOrderStatutsList() + { + $this->table = 'order_state'; + $this->className = 'OrderState'; + $this->_defaultOrderBy = $this->identifier = 'id_order_state'; + $this->list_id = 'order_state'; + $this->deleted = true; + $this->_orderBy = null; + $this->fields_list = [ + 'id_order_state' => [ + 'title' => $this->trans('ID', [], 'Admin.Global'), + 'align' => 'text-center', + 'class' => 'fixed-width-xs', + ], + 'name' => [ + 'title' => $this->trans('Name', [], 'Admin.Global'), + 'width' => 'auto', + 'color' => 'color', + ], + 'logo' => [ + 'title' => $this->trans('Icon', [], 'Admin.Shopparameters.Feature'), + 'align' => 'text-center', + 'image' => 'os', + 'orderby' => false, + 'search' => false, + 'class' => 'fixed-width-xs', + ], + 'send_email' => [ + 'title' => $this->trans('Send email to customer', [], 'Admin.Shopparameters.Feature'), + 'align' => 'text-center', + 'active' => 'sendEmail', + 'type' => 'bool', + 'ajax' => true, + 'orderby' => false, + 'class' => 'fixed-width-sm', + ], + 'delivery' => [ + 'title' => $this->trans('Delivery', [], 'Admin.Global'), + 'align' => 'text-center', + 'active' => 'delivery', + 'type' => 'bool', + 'ajax' => true, + 'orderby' => false, + 'class' => 'fixed-width-sm', + ], + 'invoice' => [ + 'title' => $this->trans('Invoice', [], 'Admin.Global'), + 'align' => 'text-center', + 'active' => 'invoice', + 'type' => 'bool', + 'ajax' => true, + 'orderby' => false, + 'class' => 'fixed-width-sm', + ], + 'template' => [ + 'title' => $this->trans('Email template', [], 'Admin.Shopparameters.Feature'), + ], + ]; + } + + /** + * init all variables to render the order return list. + */ + protected function initOrdersReturnsList() + { + $this->table = 'order_return_state'; + $this->className = 'OrderReturnState'; + $this->_defaultOrderBy = $this->identifier = 'id_order_return_state'; + $this->list_id = 'order_return_state'; + $this->deleted = false; + $this->_orderBy = null; + + $this->fields_list = [ + 'id_order_return_state' => [ + 'title' => $this->trans('ID', [], 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ], + 'name' => [ + 'title' => $this->trans('Name', [], 'Admin.Global'), + 'align' => 'left', + 'width' => 'auto', + 'color' => 'color', + ], + ]; + } + + protected function initOrderReturnsForm() + { + $id_order_return_state = (int) Tools::getValue('id_order_return_state'); + + // Create Object OrderReturnState + $order_return_state = new OrderReturnState($id_order_return_state); + + //init field form variable for order return form + $this->fields_form = []; + + //$this->initToolbar(); + $this->getlanguages(); + $helper = new HelperForm(); + $helper->currentIndex = self::$currentIndex; + $helper->token = $this->token; + $helper->table = 'order_return_state'; + $helper->identifier = 'id_order_return_state'; + $helper->id = $order_return_state->id; + $helper->toolbar_scroll = false; + $helper->languages = $this->_languages; + $helper->default_form_language = $this->default_form_language; + $helper->allow_employee_form_lang = $this->allow_employee_form_lang; + + if ($order_return_state->id) { + $helper->fields_value = [ + 'name' => $this->getFieldValue($order_return_state, 'name'), + 'color' => $this->getFieldValue($order_return_state, 'color'), + ]; + } else { + $helper->fields_value = [ + 'name' => $this->getFieldValue($order_return_state, 'name'), + 'color' => '#ffffff', + ]; + } + + $helper->toolbar_btn = $this->toolbar_btn; + $helper->title = $this->trans('Edit return status', [], 'Admin.Shopparameters.Feature'); + + return $helper; + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_order_state'] = [ + 'href' => self::$currentIndex . '&addorder_state&token=' . $this->token, + 'desc' => $this->trans('Add new order status', [], 'Admin.Shopparameters.Feature'), + 'icon' => 'process-icon-new', + ]; + $this->page_header_toolbar_btn['new_order_return_state'] = [ + 'href' => self::$currentIndex . '&addorder_return_state&token=' . $this->token, + 'desc' => $this->trans('Add new order return status', [], 'Admin.Shopparameters.Feature'), + 'icon' => 'process-icon-new', + ]; + } + + parent::initPageHeaderToolbar(); + } + + /** + * Function used to render the list to display for this controller. + */ + public function renderList() + { + //init and render the first list + $this->addRowAction('edit'); + $this->addRowAction('delete'); + $this->addRowActionSkipList('delete', $this->getUnremovableStatuses()); + $this->bulk_actions = [ + 'delete' => [ + 'text' => $this->trans('Delete selected', [], 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', [], 'Admin.Notifications.Warning'), + 'icon' => 'icon-trash', + ], + ]; + $this->initOrderStatutsList(); + $lists = parent::renderList(); + + //init and render the second list + $this->list_skip_actions = []; + $this->_filter = false; + $this->addRowActionSkipList('delete', [1, 2, 3, 4, 5]); + $this->initOrdersReturnsList(); + $this->checkFilterForOrdersReturnsList(); + + // call postProcess() to take care of actions and filters + $this->postProcess(); + $this->toolbar_title = $this->trans('Return statuses', [], 'Admin.Shopparameters.Feature'); + + parent::initToolbar(); + $lists .= parent::renderList(); + + return $lists; + } + + protected function getUnremovableStatuses() + { + return array_map(function ($row) { + return (int) $row['id_order_state']; + }, Db::getInstance()->executeS('SELECT id_order_state FROM ' . _DB_PREFIX_ . 'order_state WHERE unremovable = 1')); + } + + protected function checkFilterForOrdersReturnsList() + { + // test if a filter is applied for this list + if (Tools::isSubmit('submitFilter' . $this->table) || $this->context->cookie->{'submitFilter' . $this->table} !== false) { + $this->filter = true; + } + + // test if a filter reset request is required for this list + if (isset($_POST['submitReset' . $this->table])) { + $this->action = 'reset_filters'; + } else { + $this->action = ''; + } + } + + public function renderForm() + { + $this->fields_form = [ + 'tinymce' => true, + 'legend' => [ + 'title' => $this->trans('Order status', [], 'Admin.Shopparameters.Feature'), + 'icon' => 'icon-time', + ], + 'input' => [ + [ + 'type' => 'text', + 'label' => $this->trans('Status name', [], 'Admin.Shopparameters.Feature'), + 'name' => 'name', + 'lang' => true, + 'required' => true, + 'hint' => [ + $this->trans('Order status (e.g. \'Pending\').', [], 'Admin.Shopparameters.Help'), + $this->trans('Invalid characters: numbers and', [], 'Admin.Shopparameters.Help') . ' !<>,;?=+()@#"{}_$%:', + ], + ], + [ + 'type' => 'file', + 'label' => $this->trans('Icon', [], 'Admin.Shopparameters.Feature'), + 'name' => 'icon', + 'hint' => $this->trans('Upload an icon from your computer (File type: .gif, suggested size: 16x16).', [], 'Admin.Shopparameters.Help'), + ], + [ + 'type' => 'color', + 'label' => $this->trans('Color', [], 'Admin.Shopparameters.Feature'), + 'name' => 'color', + 'hint' => $this->trans('Status will be highlighted in this color. HTML colors only.', [], 'Admin.Shopparameters.Help') . ' "lightblue", "#CC6600")', + ], + [ + 'type' => 'checkbox', + 'name' => 'logable', + 'values' => [ + 'query' => [ + ['id' => 'on', 'name' => $this->trans('Consider the associated order as validated.', [], 'Admin.Shopparameters.Feature'), 'val' => '1'], + ], + 'id' => 'id', + 'name' => 'name', + ], + ], + [ + 'type' => 'checkbox', + 'name' => 'invoice', + 'values' => [ + 'query' => [ + ['id' => 'on', 'name' => $this->trans('Allow a customer to download and view PDF versions of his/her invoices.', [], 'Admin.Shopparameters.Feature'), 'val' => '1'], + ], + 'id' => 'id', + 'name' => 'name', + ], + ], + [ + 'type' => 'checkbox', + 'name' => 'hidden', + 'values' => [ + 'query' => [ + ['id' => 'on', 'name' => $this->trans('Hide this status in all customer orders.', [], 'Admin.Shopparameters.Feature'), 'val' => '1'], + ], + 'id' => 'id', + 'name' => 'name', + ], + ], + [ + 'type' => 'checkbox', + 'name' => 'send_email', + 'values' => [ + 'query' => [ + ['id' => 'on', 'name' => $this->trans('Send an email to the customer when his/her order status has changed.', [], 'Admin.Shopparameters.Feature'), 'val' => '1'], + ], + 'id' => 'id', + 'name' => 'name', + ], + ], + [ + 'type' => 'checkbox', + 'name' => 'pdf_invoice', + 'values' => [ + 'query' => [ + ['id' => 'on', 'name' => $this->trans('Attach invoice PDF to email.', [], 'Admin.Shopparameters.Feature'), 'val' => '1'], + ], + 'id' => 'id', + 'name' => 'name', + ], + ], + [ + 'type' => 'checkbox', + 'name' => 'pdf_delivery', + 'values' => [ + 'query' => [ + ['id' => 'on', 'name' => $this->trans('Attach delivery slip PDF to email.', [], 'Admin.Shopparameters.Feature'), 'val' => '1'], + ], + 'id' => 'id', + 'name' => 'name', + ], + ], + [ + 'type' => 'checkbox', + 'name' => 'shipped', + 'values' => [ + 'query' => [ + ['id' => 'on', 'name' => $this->trans('Set the order as shipped.', [], 'Admin.Shopparameters.Feature'), 'val' => '1'], + ], + 'id' => 'id', + 'name' => 'name', + ], + ], + [ + 'type' => 'checkbox', + 'name' => 'paid', + 'values' => [ + 'query' => [ + ['id' => 'on', 'name' => $this->trans('Set the order as paid.', [], 'Admin.Shopparameters.Feature'), 'val' => '1'], + ], + 'id' => 'id', + 'name' => 'name', + ], + ], + [ + 'type' => 'checkbox', + 'name' => 'delivery', + 'values' => [ + 'query' => [ + ['id' => 'on', 'name' => $this->trans('Show delivery PDF.', [], 'Admin.Shopparameters.Feature'), 'val' => '1'], + ], + 'id' => 'id', + 'name' => 'name', + ], + ], + [ + 'type' => 'select_template', + 'label' => $this->trans('Template', [], 'Admin.Shopparameters.Feature'), + 'name' => 'template', + 'lang' => true, + 'options' => [ + 'query' => $this->getTemplates(), + 'id' => 'id', + 'name' => 'name', + 'folder' => 'folder', + ], + 'hint' => [ + $this->trans('Only letters, numbers and underscores ("_") are allowed.', [], 'Admin.Shopparameters.Help'), + $this->trans('Email template for both .html and .txt.', [], 'Admin.Shopparameters.Help'), + ], + ], + ], + 'submit' => [ + 'title' => $this->trans('Save', [], 'Admin.Actions'), + ], + ]; + + if (Tools::isSubmit('updateorder_state') || Tools::isSubmit('addorder_state')) { + return $this->renderOrderStatusForm(); + } elseif (Tools::isSubmit('updateorder_return_state') || Tools::isSubmit('addorder_return_state')) { + return $this->renderOrderReturnsForm(); + } else { + return parent::renderForm(); + } + } + + protected function renderOrderStatusForm() + { + if (!($obj = $this->loadObject(true))) { + return; + } + + $this->fields_value = [ + 'logable_on' => $this->getFieldValue($obj, 'logable'), + 'invoice_on' => $this->getFieldValue($obj, 'invoice'), + 'hidden_on' => $this->getFieldValue($obj, 'hidden'), + 'send_email_on' => $this->getFieldValue($obj, 'send_email'), + 'shipped_on' => $this->getFieldValue($obj, 'shipped'), + 'paid_on' => $this->getFieldValue($obj, 'paid'), + 'delivery_on' => $this->getFieldValue($obj, 'delivery'), + 'pdf_delivery_on' => $this->getFieldValue($obj, 'pdf_delivery'), + 'pdf_invoice_on' => $this->getFieldValue($obj, 'pdf_invoice'), + ]; + + if ($this->getFieldValue($obj, 'color') !== false) { + $this->fields_value['color'] = $this->getFieldValue($obj, 'color'); + } else { + $this->fields_value['color'] = '#ffffff'; + } + + return parent::renderForm(); + } + + protected function renderOrderReturnsForm() + { + $helper = $this->initOrderReturnsForm(); + $helper->show_cancel_button = true; + + $back = Tools::safeOutput(Tools::getValue('back', '')); + if (empty($back)) { + $back = self::$currentIndex . '&token=' . $this->token; + } + if (!Validate::isCleanHtml($back)) { + die(Tools::displayError()); + } + + $helper->back_url = $back; + + $this->fields_form[0]['form'] = [ + 'tinymce' => true, + 'legend' => [ + 'title' => $this->trans('Return status', [], 'Admin.Shopparameters.Feature'), + 'icon' => 'icon-time', + ], + 'input' => [ + [ + 'type' => 'text', + 'label' => $this->trans('Status name', [], 'Admin.Shopparameters.Feature'), + 'name' => 'name', + 'lang' => true, + 'required' => true, + 'hint' => [ + $this->trans('Order\'s return status name.', [], 'Admin.Shopparameters.Help'), + $this->trans('Invalid characters: numbers and', [], 'Admin.Shopparameters.Help') . ' !<>,;?=+()@#"�{}_$%:', + ], + ], + [ + 'type' => 'color', + 'label' => $this->trans('Color', [], 'Admin.Shopparameters.Feature'), + 'name' => 'color', + 'hint' => $this->trans('Status will be highlighted in this color. HTML colors only.', [], 'Admin.Shopparameters.Help') . ' "lightblue", "#CC6600")', + ], + ], + 'submit' => [ + 'title' => $this->trans('Save', [], 'Admin.Actions'), + ], + ]; + + return $helper->generateForm($this->fields_form); + } + + protected function getTemplates() + { + $default_path = '../mails/'; + // Mail templates can also be found in the theme folder + $theme_path = '../themes/' . $this->context->shop->theme->getName() . '/mails/'; + + $array = []; + foreach (Language::getLanguages(false) as $language) { + $iso_code = $language['iso_code']; + + // If there is no folder for the given iso_code in /mails or in /themes/[theme_name]/mails, we bypass this language + if (!@filemtime(_PS_ADMIN_DIR_ . '/' . $default_path . $iso_code) && !@filemtime(_PS_ADMIN_DIR_ . '/' . $theme_path . $iso_code)) { + continue; + } + + $theme_templates_dir = _PS_ADMIN_DIR_ . '/' . $theme_path . $iso_code; + $theme_templates = is_dir($theme_templates_dir) ? scandir($theme_templates_dir, SCANDIR_SORT_NONE) : []; + // We merge all available emails in one array + $templates = array_unique(array_merge(scandir(_PS_ADMIN_DIR_ . '/' . $default_path . $iso_code, SCANDIR_SORT_NONE), $theme_templates)); + foreach ($templates as $key => $template) { + if (!strncmp(strrev($template), 'lmth.', 5)) { + $search_result = array_search($template, $theme_templates); + $array[$iso_code][] = [ + 'id' => substr($template, 0, -5), + 'name' => substr($template, 0, -5), + 'folder' => ((!empty($search_result) ? $theme_path : $default_path)), + ]; + } + } + } + + return $array; + } + + public function postProcess() + { + if (Tools::isSubmit($this->table . 'Orderby') || Tools::isSubmit($this->table . 'Orderway')) { + $this->filter = true; + } + + if (Tools::isSubmit('submitAddorder_return_state')) { + if (!$this->access('add')) { + return; + } + + $id_order_return_state = Tools::getValue('id_order_return_state'); + + // Create Object OrderReturnState + $order_return_state = new OrderReturnState((int) $id_order_return_state); + + $order_return_state->color = Tools::getValue('color'); + $order_return_state->name = []; + foreach (Language::getIDs(false) as $id_lang) { + $order_return_state->name[$id_lang] = Tools::getValue('name_' . $id_lang); + } + + // Update object + if (!$order_return_state->save()) { + $this->errors[] = $this->trans('An error has occurred: Can\'t save the current order\'s return status.', [], 'Admin.Orderscustomers.Notification'); + } else { + Tools::redirectAdmin(self::$currentIndex . '&conf=4&token=' . $this->token); + } + } + + if (Tools::isSubmit('submitBulkdeleteorder_return_state')) { + if (!$this->access('delete')) { + return; + } + + $this->className = 'OrderReturnState'; + $this->table = 'order_return_state'; + $this->boxes = Tools::getValue('order_return_stateBox'); + $this->deleted = false; + parent::processBulkDelete(); + } + + if (Tools::isSubmit('deleteorder_return_state')) { + if (!$this->access('delete')) { + return; + } + + $id_order_return_state = Tools::getValue('id_order_return_state'); + + // Create Object OrderReturnState + $order_return_state = new OrderReturnState((int) $id_order_return_state); + + if (!$order_return_state->delete()) { + $this->errors[] = $this->trans('An error has occurred: Can\'t delete the current order\'s return status.', [], 'Admin.Orderscustomers.Notification'); + } else { + Tools::redirectAdmin(self::$currentIndex . '&conf=1&token=' . $this->token); + } + } + + if (Tools::isSubmit('submitAdd' . $this->table)) { + if (!$this->access('add')) { + return; + } + + $this->deleted = false; // Disabling saving historisation + $_POST['invoice'] = (int) Tools::getValue('invoice_on'); + $_POST['logable'] = (int) Tools::getValue('logable_on'); + $_POST['send_email'] = (int) Tools::getValue('send_email_on'); + $_POST['hidden'] = (int) Tools::getValue('hidden_on'); + $_POST['shipped'] = (int) Tools::getValue('shipped_on'); + $_POST['paid'] = (int) Tools::getValue('paid_on'); + $_POST['delivery'] = (int) Tools::getValue('delivery_on'); + $_POST['pdf_delivery'] = (int) Tools::getValue('pdf_delivery_on'); + $_POST['pdf_invoice'] = (int) Tools::getValue('pdf_invoice_on'); + if (!$_POST['send_email']) { + foreach (Language::getIDs(false) as $id_lang) { + $_POST['template_' . $id_lang] = ''; + } + } + + return parent::postProcess(); + } elseif (Tools::isSubmit('delete' . $this->table)) { + if (!$this->access('delete')) { + return; + } + + $order_state = new OrderState(Tools::getValue('id_order_state'), $this->context->language->id); + if (!$order_state->isRemovable()) { + $this->errors[] = $this->trans('For security reasons, you cannot delete default order statuses.', [], 'Admin.Shopparameters.Notification'); + } else { + try { + if (!$order_state->softDelete()) { + throw new PrestaShopException('Error when soft deleting order status'); + } + } catch (PrestaShopException $e) { // see ObjectModel::softDelete too + $this->errors[] = $this->trans('An error occurred during deletion.', [], 'Admin.Notifications.Error'); + + return $order_state; + } + + Tools::redirectAdmin(self::$currentIndex . '&conf=1&token=' . $this->token); + } + } elseif (Tools::isSubmit('submitBulkdelete' . $this->table)) { + if (!$this->access('delete')) { + return; + } + + foreach (Tools::getValue($this->table . 'Box', []) as $selection) { + $order_state = new OrderState((int) $selection, $this->context->language->id); + if (!$order_state->isRemovable()) { + $this->errors[] = $this->trans('For security reasons, you cannot delete default order statuses.', [], 'Admin.Shopparameters.Notification'); + + break; + } + } + + if (!count($this->errors)) { + return parent::postProcess(); + } + } else { + return parent::postProcess(); + } + } + + protected function filterToField($key, $filter) + { + if ($this->table == 'order_state') { + $this->initOrderStatutsList(); + } elseif ($this->table == 'order_return_state') { + $this->initOrdersReturnsList(); + } + + return parent::filterToField($key, $filter); + } + + protected function afterImageUpload() + { + parent::afterImageUpload(); + + if (($id_order_state = (int) Tools::getValue('id_order_state')) && + isset($_FILES) && count($_FILES) && file_exists(_PS_ORDER_STATE_IMG_DIR_ . $id_order_state . '.gif')) { + $current_file = _PS_TMP_IMG_DIR_ . 'order_state_mini_' . $id_order_state . '_' . $this->context->shop->id . '.gif'; + + if (file_exists($current_file)) { + unlink($current_file); + } + } + + return true; + } + + public function ajaxProcessSendEmailOrderState() + { + $id_order_state = (int) Tools::getValue('id_order_state'); + + $sql = 'UPDATE ' . _DB_PREFIX_ . 'order_state SET `send_email`= NOT `send_email` WHERE id_order_state=' . $id_order_state; + $result = Db::getInstance()->execute($sql); + + if ($result) { + echo json_encode(['success' => 1, 'text' => $this->trans('The status has been updated successfully.', [], 'Admin.Notifications.Success')]); + } else { + echo json_encode(['success' => 0, 'text' => $this->trans('An error occurred while updating the status.', [], 'Admin.Notifications.Error')]); + } + } + + public function ajaxProcessDeliveryOrderState() + { + $id_order_state = (int) Tools::getValue('id_order_state'); + + $sql = 'UPDATE ' . _DB_PREFIX_ . 'order_state SET `delivery`= NOT `delivery` WHERE id_order_state=' . $id_order_state; + $result = Db::getInstance()->execute($sql); + + if ($result) { + echo json_encode(['success' => 1, 'text' => $this->trans('The status has been updated successfully.', [], 'Admin.Notifications.Success')]); + } else { + echo json_encode(['success' => 0, 'text' => $this->trans('An error occurred while updating the status.', [], 'Admin.Notifications.Error')]); + } + } + + public function ajaxProcessInvoiceOrderState() + { + $id_order_state = (int) Tools::getValue('id_order_state'); + + $sql = 'UPDATE ' . _DB_PREFIX_ . 'order_state SET `invoice`= NOT `invoice` WHERE id_order_state=' . $id_order_state; + $result = Db::getInstance()->execute($sql); + + if ($result) { + echo json_encode(['success' => 1, 'text' => $this->trans('The status has been updated successfully.', [], 'Admin.Notifications.Success')]); + } else { + echo json_encode(['success' => 0, 'text' => $this->trans('An error occurred while updating the status.', [], 'Admin.Notifications.Error')]); + } + } +} diff --git a/controllers/admin/AdminStoresController.php b/controllers/admin/AdminStoresController.php new file mode 100644 index 00000000..adec7ba6 --- /dev/null +++ b/controllers/admin/AdminStoresController.php @@ -0,0 +1,604 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +/** + * @property Store $object + */ +class AdminStoresControllerCore extends AdminController +{ + public function __construct() + { + $this->bootstrap = true; + $this->table = 'store'; + $this->className = 'Store'; + $this->lang = false; + $this->toolbar_scroll = false; + + parent::__construct(); + + if (!Tools::getValue('realedit')) { + $this->deleted = false; + } + + $this->fieldImageSettings = [ + 'name' => 'image', + 'dir' => 'st', + ]; + + $this->fields_list = [ + 'id_store' => ['title' => $this->trans('ID', [], 'Admin.Global'), 'align' => 'center', 'class' => 'fixed-width-xs'], + 'name' => ['title' => $this->trans('Name', [], 'Admin.Global'), 'filter_key' => 'sl!name'], + 'address1' => ['title' => $this->trans('Address', [], 'Admin.Global'), 'filter_key' => 'sl!address1'], + 'city' => ['title' => $this->trans('City', [], 'Admin.Global')], + 'postcode' => ['title' => $this->trans('Zip/postal code', [], 'Admin.Global')], + 'state' => ['title' => $this->trans('State', [], 'Admin.Global'), 'filter_key' => 'st!name'], + 'country' => ['title' => $this->trans('Country', [], 'Admin.Global'), 'filter_key' => 'cl!name'], + 'phone' => ['title' => $this->trans('Phone', [], 'Admin.Global')], + 'fax' => ['title' => $this->trans('Fax', [], 'Admin.Global')], + 'active' => ['title' => $this->trans('Enabled', [], 'Admin.Global'), 'align' => 'center', 'active' => 'status', 'type' => 'bool', 'orderby' => false], + ]; + + $this->bulk_actions = [ + 'delete' => [ + 'text' => $this->trans('Delete selected', [], 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', [], 'Admin.Notifications.Warning'), + 'icon' => 'icon-trash', + ], + ]; + + $this->_buildOrderedFieldsShop($this->_getDefaultFieldsContent()); + } + + public function renderOptions() + { + // Set toolbar options + $this->display = 'options'; + $this->show_toolbar = true; + $this->toolbar_scroll = true; + $this->initToolbar(); + + return parent::renderOptions(); + } + + public function initToolbar() + { + parent::initToolbar(); + + if ($this->display == 'options') { + unset($this->toolbar_btn['new']); + } elseif ($this->display != 'add' && $this->display != 'edit') { + unset($this->toolbar_btn['save']); + } + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_store'] = [ + 'href' => self::$currentIndex . '&addstore&token=' . $this->token, + 'desc' => $this->trans('Add new store', [], 'Admin.Shopparameters.Feature'), + 'icon' => 'process-icon-new', + ]; + } + + parent::initPageHeaderToolbar(); + } + + public function renderList() + { + // Set toolbar options + $this->display = null; + $this->initToolbar(); + + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + $this->_select = 'cl.`name` country, st.`name` state, sl.*'; + $this->_join = ' + LEFT JOIN `' . _DB_PREFIX_ . 'country_lang` cl + ON (cl.`id_country` = a.`id_country` + AND cl.`id_lang` = ' . (int) $this->context->language->id . ') + LEFT JOIN `' . _DB_PREFIX_ . 'state` st + ON (st.`id_state` = a.`id_state`) + LEFT JOIN `' . _DB_PREFIX_ . 'store_lang` sl + ON (sl.`id_store` = a.`id_store` + AND sl.`id_lang` = ' . (int) $this->context->language->id . ') '; + + return parent::renderList(); + } + + public function renderForm() + { + if (!($obj = $this->loadObject(true))) { + return; + } + + $image = _PS_STORE_IMG_DIR_ . $obj->id . '.jpg'; + $image_url = ImageManager::thumbnail( + $image, + $this->table . '_' . (int) $obj->id . '.' . $this->imageType, + 350, + $this->imageType, + true, + true + ); + $image_size = file_exists($image) ? filesize($image) / 1000 : false; + + $tmp_addr = new Address(); + $res = $tmp_addr->getFieldsRequiredDatabase(); + $required_fields = []; + foreach ($res as $row) { + $required_fields[(int) $row['id_required_field']] = $row['field_name']; + } + + $this->fields_form = [ + 'legend' => [ + 'title' => $this->trans('Stores', [], 'Admin.Shopparameters.Feature'), + 'icon' => 'icon-home', + ], + 'input' => [ + [ + 'type' => 'text', + 'label' => $this->trans('Name', [], 'Admin.Global'), + 'name' => 'name', + 'lang' => true, + 'required' => false, + 'hint' => [ + $this->trans('Store name (e.g. City Center Mall Store).', [], 'Admin.Shopparameters.Feature'), + $this->trans('Allowed characters: letters, spaces and %s', [], 'Admin.Shopparameters.Feature'), + ], + ], + [ + 'type' => 'text', + 'label' => $this->trans('Address', [], 'Admin.Global'), + 'name' => 'address1', + 'lang' => true, + 'required' => true, + ], + [ + 'type' => 'text', + 'label' => $this->trans('Address (2)', [], 'Admin.Global'), + 'name' => 'address2', + 'lang' => true, + ], + [ + 'type' => 'text', + 'label' => $this->trans('Zip/postal code', [], 'Admin.Global'), + 'name' => 'postcode', + 'required' => in_array('postcode', $required_fields), + ], + [ + 'type' => 'text', + 'label' => $this->trans('City', [], 'Admin.Global'), + 'name' => 'city', + 'required' => true, + ], + [ + 'type' => 'select', + 'label' => $this->trans('Country', [], 'Admin.Global'), + 'name' => 'id_country', + 'required' => true, + 'default_value' => (int) $this->context->country->id, + 'options' => [ + 'query' => Country::getCountries($this->context->language->id), + 'id' => 'id_country', + 'name' => 'name', + ], + ], + [ + 'type' => 'select', + 'label' => $this->trans('State', [], 'Admin.Global'), + 'name' => 'id_state', + 'required' => true, + 'options' => [ + 'id' => 'id_state', + 'name' => 'name', + 'query' => null, + ], + ], + [ + 'type' => 'latitude', + 'label' => $this->trans('Latitude / Longitude', [], 'Admin.Shopparameters.Feature'), + 'name' => 'latitude', + 'required' => true, + 'maxlength' => 12, + 'hint' => $this->trans('Store coordinates (e.g. 45.265469/-47.226478).', [], 'Admin.Shopparameters.Feature'), + ], + [ + 'type' => 'text', + 'label' => $this->trans('Phone', [], 'Admin.Global'), + 'name' => 'phone', + ], + [ + 'type' => 'text', + 'label' => $this->trans('Fax', [], 'Admin.Global'), + 'name' => 'fax', + ], + [ + 'type' => 'text', + 'label' => $this->trans('Email address', [], 'Admin.Global'), + 'name' => 'email', + ], + [ + 'type' => 'textarea', + 'label' => $this->trans('Note', [], 'Admin.Global'), + 'name' => 'note', + 'lang' => true, + 'cols' => 42, + 'rows' => 4, + ], + [ + 'type' => 'switch', + 'label' => $this->trans('Active', [], 'Admin.Global'), + 'name' => 'active', + 'required' => false, + 'is_bool' => true, + 'values' => [ + [ + 'id' => 'active_on', + 'value' => 1, + 'label' => $this->trans('Enabled', [], 'Admin.Global'), + ], + [ + 'id' => 'active_off', + 'value' => 0, + 'label' => $this->trans('Disabled', [], 'Admin.Global'), + ], + ], + 'hint' => $this->trans('Whether or not to display this store.', [], 'Admin.Shopparameters.Help'), + ], + [ + 'type' => 'file', + 'label' => $this->trans('Picture', [], 'Admin.Shopparameters.Feature'), + 'name' => 'image', + 'display_image' => true, + 'image' => $image_url ? $image_url : false, + 'size' => $image_size, + 'hint' => $this->trans('Storefront picture.', [], 'Admin.Shopparameters.Help'), + ], + ], + 'hours' => [ + ], + 'submit' => [ + 'title' => $this->trans('Save', [], 'Admin.Actions'), + ], + ]; + + if (Shop::isFeatureActive()) { + $this->fields_form['input'][] = [ + 'type' => 'shop', + 'label' => $this->trans('Shop association', [], 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + ]; + } + + $days = []; + $days[1] = $this->trans('Monday', [], 'Admin.Shopparameters.Feature'); + $days[2] = $this->trans('Tuesday', [], 'Admin.Shopparameters.Feature'); + $days[3] = $this->trans('Wednesday', [], 'Admin.Shopparameters.Feature'); + $days[4] = $this->trans('Thursday', [], 'Admin.Shopparameters.Feature'); + $days[5] = $this->trans('Friday', [], 'Admin.Shopparameters.Feature'); + $days[6] = $this->trans('Saturday', [], 'Admin.Shopparameters.Feature'); + $days[7] = $this->trans('Sunday', [], 'Admin.Shopparameters.Feature'); + + $hours = []; + + $hours_temp = ($this->getFieldValue($obj, 'hours')); + if (is_array($hours_temp) && !empty($hours_temp)) { + $langs = Language::getLanguages(false); + $hours_temp = array_map('json_decode', $hours_temp); + $hours = array_map( + [$this, 'adaptHoursFormat'], + $hours_temp + ); + $hours = (count($langs) > 1) ? $hours : $hours[reset($langs)['id_lang']]; + } + + $this->fields_value = [ + 'latitude' => $this->getFieldValue($obj, 'latitude') ? $this->getFieldValue($obj, 'latitude') : '', + 'longitude' => $this->getFieldValue($obj, 'longitude') ? $this->getFieldValue($obj, 'longitude') : '', + 'days' => $days, + 'hours' => $hours, + ]; + + return parent::renderForm(); + } + + public function postProcess() + { + if (isset($_POST['submitAdd' . $this->table])) { + $langs = Language::getLanguages(false); + /* Cleaning fields */ + foreach ($_POST as $kp => $vp) { + if (!in_array($kp, ['checkBoxShopGroupAsso_store', 'checkBoxShopAsso_store', 'hours'])) { + $_POST[$kp] = trim($vp); + } + if ('hours' === $kp) { + foreach ($vp as $day => $value) { + $_POST['hours'][$day] = is_array($value) ? array_map('trim', $_POST['hours'][$day]) : trim($value); + } + } + } + + /* Rewrite latitude and longitude to 8 digits */ + $_POST['latitude'] = number_format((float) $_POST['latitude'], 8); + $_POST['longitude'] = number_format((float) $_POST['longitude'], 8); + + /* If the selected country does not contain states */ + $id_state = (int) Tools::getValue('id_state'); + $id_country = (int) Tools::getValue('id_country'); + $country = new Country((int) $id_country); + + if ($id_country && $country && !(int) $country->contains_states && $id_state) { + $this->errors[] = $this->trans('You\'ve selected a state for a country that does not contain states.', [], 'Admin.Advparameters.Notification'); + } + + /* If the selected country contains states, then a state have to be selected */ + if ((int) $country->contains_states && !$id_state) { + $this->errors[] = $this->trans('An address located in a country containing states must have a state selected.', [], 'Admin.Shopparameters.Notification'); + } + + $latitude = (float) Tools::getValue('latitude'); + $longitude = (float) Tools::getValue('longitude'); + + if (empty($latitude) || empty($longitude)) { + $this->errors[] = $this->trans('Latitude and longitude are required.', [], 'Admin.Shopparameters.Notification'); + } + + $postcode = Tools::getValue('postcode'); + /* Check zip code format */ + if ($country->zip_code_format && !$country->checkZipCode($postcode)) { + $this->errors[] = $this->trans('Your Zip/postal code is incorrect.', [], 'Admin.Notifications.Error') . '' . $this->trans('It must be entered as follows:', [], 'Admin.Notifications.Error') . ' ' . str_replace('C', $country->iso_code, str_replace('N', '0', str_replace('L', 'A', $country->zip_code_format))); + } elseif (empty($postcode) && $country->need_zip_code) { + $this->errors[] = $this->trans('A Zip/postal code is required.', [], 'Admin.Notifications.Error'); + } elseif ($postcode && !Validate::isPostCode($postcode)) { + $this->errors[] = $this->trans('The Zip/postal code is invalid.', [], 'Admin.Notifications.Error'); + } + /* Store hours */ + foreach ($langs as $lang) { + $hours = []; + for ($i = 1; $i < 8; ++$i) { + if (1 < count($langs)) { + $hours[] = explode(' | ', $_POST['hours'][$i][$lang['id_lang']]); + unset($_POST['hours'][$i][$lang['id_lang']]); + } else { + $hours[] = explode(' | ', $_POST['hours'][$i]); + unset($_POST['hours'][$i]); + } + } + $encodedHours[$lang['id_lang']] = json_encode($hours); + } + $_POST['hours'] = (1 < count($langs)) ? $encodedHours : json_encode($hours); + } + + if (!count($this->errors)) { + parent::postProcess(); + } else { + $this->display = 'add'; + } + } + + protected function postImage($id) + { + $ret = parent::postImage($id); + $generate_hight_dpi_images = (bool) Configuration::get('PS_HIGHT_DPI'); + + if (($id_store = (int) Tools::getValue('id_store')) && isset($_FILES) && count($_FILES) && file_exists(_PS_STORE_IMG_DIR_ . $id_store . '.jpg')) { + $images_types = ImageType::getImagesTypes('stores'); + foreach ($images_types as $image_type) { + ImageManager::resize( + _PS_STORE_IMG_DIR_ . $id_store . '.jpg', + _PS_STORE_IMG_DIR_ . $id_store . '-' . stripslashes($image_type['name']) . '.jpg', + (int) $image_type['width'], + (int) $image_type['height'] + ); + + if ($generate_hight_dpi_images) { + ImageManager::resize( + _PS_STORE_IMG_DIR_ . $id_store . '.jpg', + _PS_STORE_IMG_DIR_ . $id_store . '-' . stripslashes($image_type['name']) . '2x.jpg', + (int) $image_type['width'] * 2, + (int) $image_type['height'] * 2 + ); + } + } + } + + return $ret; + } + + protected function _getDefaultFieldsContent() + { + $this->context = Context::getContext(); + $countryList = []; + $countryList[] = ['id' => '0', 'name' => $this->trans('Choose your country', [], 'Admin.Shopparameters.Feature')]; + foreach (Country::getCountries($this->context->language->id) as $country) { + $countryList[] = ['id' => $country['id_country'], 'name' => $country['name']]; + } + $stateList = []; + $stateList[] = ['id' => '0', 'name' => $this->trans('Choose your state (if applicable)', [], 'Admin.Shopparameters.Feature')]; + foreach (State::getStates($this->context->language->id) as $state) { + $stateList[] = ['id' => $state['id_state'], 'name' => $state['name']]; + } + + $formFields = [ + 'PS_SHOP_NAME' => [ + 'title' => $this->trans('Shop name', [], 'Admin.Shopparameters.Feature'), + 'hint' => $this->trans('Displayed in emails and page titles.', [], 'Admin.Shopparameters.Feature'), + 'validation' => 'isGenericName', + 'required' => true, + 'type' => 'text', + 'no_escape' => true, + ], + 'PS_SHOP_EMAIL' => ['title' => $this->trans('Shop email', [], 'Admin.Shopparameters.Feature'), + 'hint' => $this->trans('Displayed in emails sent to customers.', [], 'Admin.Shopparameters.Help'), + 'validation' => 'isEmail', + 'required' => true, + 'type' => 'text', + ], + 'PS_SHOP_DETAILS' => [ + 'title' => $this->trans('Registration number', [], 'Admin.Shopparameters.Feature'), + 'hint' => $this->trans('Shop registration information (e.g. SIRET or RCS).', [], 'Admin.Shopparameters.Help'), + 'validation' => 'isGenericName', + 'type' => 'textarea', + 'cols' => 30, + 'rows' => 5, + ], + 'PS_SHOP_ADDR1' => [ + 'title' => $this->trans('Shop address line 1', [], 'Admin.Shopparameters.Feature'), + 'validation' => 'isAddress', + 'type' => 'text', + ], + 'PS_SHOP_ADDR2' => [ + 'title' => $this->trans('Shop address line 2', [], 'Admin.Shopparameters.Feature'), + 'validation' => 'isAddress', + 'type' => 'text', + ], + 'PS_SHOP_CODE' => [ + 'title' => $this->trans('Zip/postal code', [], 'Admin.Global'), + 'validation' => 'isGenericName', + 'type' => 'text', + ], + 'PS_SHOP_CITY' => [ + 'title' => $this->trans('City', [], 'Admin.Global'), + 'validation' => 'isGenericName', + 'type' => 'text', + ], + 'PS_SHOP_COUNTRY_ID' => [ + 'title' => $this->trans('Country', [], 'Admin.Global'), + 'validation' => 'isInt', + 'type' => 'select', + 'list' => $countryList, + 'identifier' => 'id', + 'cast' => 'intval', + 'defaultValue' => (int) $this->context->country->id, + ], + 'PS_SHOP_STATE_ID' => [ + 'title' => $this->trans('State', [], 'Admin.Global'), + 'validation' => 'isInt', + 'type' => 'select', + 'list' => $stateList, + 'identifier' => 'id', + 'cast' => 'intval', + ], + 'PS_SHOP_PHONE' => [ + 'title' => $this->trans('Phone', [], 'Admin.Global'), + 'validation' => 'isGenericName', + 'type' => 'text', + ], + 'PS_SHOP_FAX' => [ + 'title' => $this->trans('Fax', [], 'Admin.Global'), + 'validation' => 'isGenericName', + 'type' => 'text', + ], + ]; + + return $formFields; + } + + protected function _buildOrderedFieldsShop($formFields) + { + // You cannot do that, because the fields must be sorted for the country you've selected. + // Simple example: the current country is France, where we don't display the state. You choose "US" as a country in the form. The state is not dsplayed at the right place... + + // $associatedOrderKey = array( + // 'PS_SHOP_NAME' => 'company', + // 'PS_SHOP_ADDR1' => 'address1', + // 'PS_SHOP_ADDR2' => 'address2', + // 'PS_SHOP_CITY' => 'city', + // 'PS_SHOP_STATE_ID' => 'State:name', + // 'PS_SHOP_CODE' => 'postcode', + // 'PS_SHOP_COUNTRY_ID' => 'Country:name', + // 'PS_SHOP_PHONE' => 'phone'); + // $fields = array(); + // $orderedFields = AddressFormat::getOrderedAddressFields(Configuration::get('PS_SHOP_COUNTRY_ID'), false, true); + // foreach ($orderedFields as $lineFields) + // if (($patterns = explode(' ', $lineFields))) + // foreach ($patterns as $pattern) + // if (($key = array_search($pattern, $associatedOrderKey))) + // $fields[$key] = $formFields[$key]; + // foreach ($formFields as $key => $value) + // if (!isset($fields[$key])) + // $fields[$key] = $formFields[$key]; + + $fields = $formFields; + $this->fields_options['contact'] = [ + 'title' => $this->trans('Contact details', [], 'Admin.Shopparameters.Feature'), + 'icon' => 'icon-user', + 'fields' => $fields, + 'submit' => ['title' => $this->trans('Save', [], 'Admin.Actions')], + ]; + } + + public function beforeUpdateOptions() + { + if (isset($_POST['PS_SHOP_STATE_ID']) && $_POST['PS_SHOP_STATE_ID'] != '0') { + $sql = 'SELECT `active` FROM `' . _DB_PREFIX_ . 'state` + WHERE `id_country` = ' . (int) Tools::getValue('PS_SHOP_COUNTRY_ID') . ' + AND `id_state` = ' . (int) Tools::getValue('PS_SHOP_STATE_ID'); + $isStateOk = Db::getInstance()->getValue($sql); + if ($isStateOk != 1) { + $this->errors[] = $this->trans('The specified state is not located in this country.', [], 'Admin.Shopparameters.Notification'); + } + } + } + + public function updateOptionPsShopCountryId($value) + { + if (!$this->errors && $value) { + $country = new Country($value, $this->context->language->id); + if ($country->id) { + Configuration::updateValue('PS_SHOP_COUNTRY_ID', $value); + Configuration::updateValue('PS_SHOP_COUNTRY', pSQL($country->name)); + } + } + } + + public function updateOptionPsShopStateId($value) + { + if (!$this->errors && $value) { + $state = new State($value); + if ($state->id) { + Configuration::updateValue('PS_SHOP_STATE_ID', $value); + Configuration::updateValue('PS_SHOP_STATE', pSQL($state->name)); + } + } + } + + /** + * Adapt the format of hours. + * + * @param array $value + * + * @return array + */ + protected function adaptHoursFormat($value) + { + $separator = array_fill(0, count($value), ' | '); + + return array_map('implode', $value, $separator); + } +} diff --git a/controllers/admin/AdminSuppliersController.php b/controllers/admin/AdminSuppliersController.php new file mode 100644 index 00000000..f195e2b5 --- /dev/null +++ b/controllers/admin/AdminSuppliersController.php @@ -0,0 +1,592 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +/** + * @property Supplier $object + */ +class AdminSuppliersControllerCore extends AdminController +{ + public $bootstrap = true; + + public function __construct() + { + $this->table = 'supplier'; + $this->className = 'Supplier'; + + parent::__construct(); + + $this->addRowAction('view'); + $this->addRowAction('edit'); + $this->addRowAction('delete'); + $this->allow_export = true; + + $this->_defaultOrderBy = 'name'; + $this->_defaultOrderWay = 'ASC'; + + $this->bulk_actions = [ + 'delete' => [ + 'text' => $this->trans('Delete selected', [], 'Admin.Actions'), + 'icon' => 'icon-trash', + 'confirm' => $this->trans('Delete selected items?', [], 'Admin.Notifications.Warning'), + ], + ]; + + $this->_select = 'COUNT(DISTINCT ps.`id_product`) AS products'; + $this->_join = 'LEFT JOIN `' . _DB_PREFIX_ . 'product_supplier` ps ON (a.`id_supplier` = ps.`id_supplier`)'; + $this->_group = 'GROUP BY a.`id_supplier`'; + + $this->fieldImageSettings = ['name' => 'logo', 'dir' => 'su']; + + $this->fields_list = [ + 'id_supplier' => ['title' => $this->trans('ID', [], 'Admin.Global'), 'align' => 'center', 'class' => 'fixed-width-xs'], + 'logo' => ['title' => $this->trans('Logo', [], 'Admin.Global'), 'align' => 'center', 'image' => 'su', 'orderby' => false, 'search' => false], + 'name' => ['title' => $this->trans('Name', [], 'Admin.Global')], + 'products' => ['title' => $this->trans('Number of products', [], 'Admin.Catalog.Feature'), 'align' => 'right', 'filter_type' => 'int', 'tmpTableFilter' => true], + 'active' => ['title' => $this->trans('Enabled', [], 'Admin.Global'), 'align' => 'center', 'active' => 'status', 'type' => 'bool', 'orderby' => false, 'class' => 'fixed-width-xs'], + ]; + } + + public function setMedia($isNewTheme = false) + { + parent::setMedia($isNewTheme); + $this->addJqueryUi('ui.widget'); + $this->addJqueryPlugin('tagify'); + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_supplier'] = [ + 'href' => self::$currentIndex . '&addsupplier&token=' . $this->token, + 'desc' => $this->trans('Add new supplier', [], 'Admin.Catalog.Feature'), + 'icon' => 'process-icon-new', + ]; + } + + parent::initPageHeaderToolbar(); + } + + public function renderForm() + { + // loads current warehouse + if (!($obj = $this->loadObject(true))) { + return; + } + + $image = _PS_SUPP_IMG_DIR_ . $obj->id . '.jpg'; + $image_url = ImageManager::thumbnail( + $image, + $this->table . '_' . (int) $obj->id . '.' . $this->imageType, + 350, + $this->imageType, + true, + true + ); + $image_size = file_exists($image) ? filesize($image) / 1000 : false; + + $tmp_addr = new Address(); + $res = $tmp_addr->getFieldsRequiredDatabase(); + $required_fields = []; + foreach ($res as $row) { + $required_fields[(int) $row['id_required_field']] = $row['field_name']; + } + + $this->fields_form = [ + 'legend' => [ + 'title' => $this->trans('Suppliers', [], 'Admin.Global'), + 'icon' => 'icon-truck', + ], + 'input' => [ + [ + 'type' => 'hidden', + 'name' => 'id_address', + ], + [ + 'type' => 'text', + 'label' => $this->trans('Name', [], 'Admin.Global'), + 'name' => 'name', + 'required' => true, + 'col' => 4, + 'hint' => $this->trans('Invalid characters:', [], 'Admin.Notifications.Info') . ' <>;=#{}', + ], + ( + in_array('company', $required_fields) ? + [ + 'type' => 'text', + 'label' => $this->trans('Company', [], 'Admin.Global'), + 'name' => 'company', + 'display' => in_array('company', $required_fields), + 'required' => in_array('company', $required_fields), + 'maxlength' => 16, + 'col' => 4, + 'hint' => $this->trans('Company name for this supplier', [], 'Admin.Catalog.Help'), + ] + : null + ), + [ + 'type' => 'textarea', + 'label' => $this->trans('Description', [], 'Admin.Global'), + 'name' => 'description', + 'lang' => true, + 'hint' => [ + $this->trans('Invalid characters:', [], 'Admin.Notifications.Info') . ' <>;=#{}', + $this->trans('Will appear in the list of suppliers.', [], 'Admin.Catalog.Help'), + ], + 'autoload_rte' => 'rte', //Enable TinyMCE editor for short description + ], + [ + 'type' => 'text', + 'label' => $this->trans('Phone', [], 'Admin.Global'), + 'name' => 'phone', + 'required' => in_array('phone', $required_fields), + 'maxlength' => 16, + 'col' => 4, + 'hint' => $this->trans('Phone number for this supplier', [], 'Admin.Catalog.Help'), + ], + [ + 'type' => 'text', + 'label' => $this->trans('Mobile phone', [], 'Admin.Global'), + 'name' => 'phone_mobile', + 'required' => in_array('phone_mobile', $required_fields), + 'maxlength' => 16, + 'col' => 4, + 'hint' => $this->trans('Mobile phone number for this supplier.', [], 'Admin.Catalog.Help'), + ], + [ + 'type' => 'text', + 'label' => $this->trans('Address', [], 'Admin.Global'), + 'name' => 'address', + 'maxlength' => 128, + 'col' => 6, + 'required' => true, + ], + [ + 'type' => 'text', + 'label' => $this->trans('Address (2)', [], 'Admin.Global'), + 'name' => 'address2', + 'required' => in_array('address2', $required_fields), + 'col' => 6, + 'maxlength' => 128, + ], + [ + 'type' => 'text', + 'label' => $this->trans('Zip/postal code', [], 'Admin.Global'), + 'name' => 'postcode', + 'required' => in_array('postcode', $required_fields), + 'maxlength' => 12, + 'col' => 2, + ], + [ + 'type' => 'text', + 'label' => $this->trans('City', [], 'Admin.Global'), + 'name' => 'city', + 'maxlength' => 32, + 'col' => 4, + 'required' => true, + ], + [ + 'type' => 'select', + 'label' => $this->trans('Country', [], 'Admin.Global'), + 'name' => 'id_country', + 'required' => true, + 'col' => 4, + 'default_value' => (int) $this->context->country->id, + 'options' => [ + 'query' => Country::getCountries($this->context->language->id, false), + 'id' => 'id_country', + 'name' => 'name', + ], + ], + [ + 'type' => 'select', + 'label' => $this->trans('State', [], 'Admin.Global'), + 'name' => 'id_state', + 'col' => 4, + 'options' => [ + 'id' => 'id_state', + 'query' => [], + 'name' => 'name', + ], + ], + [ + 'type' => 'text', + 'label' => $this->trans('DNI', [], 'Admin.Global'), + 'name' => 'dni', + 'maxlength' => 16, + 'col' => 4, + 'required' => true, // Only required in case of specifics countries + ], + [ + 'type' => 'file', + 'label' => $this->trans('Logo', [], 'Admin.Global'), + 'name' => 'logo', + 'display_image' => true, + 'image' => $image_url ? $image_url : false, + 'size' => $image_size, + 'hint' => $this->trans('Upload a supplier logo from your computer.', [], 'Admin.Catalog.Help'), + ], + [ + 'type' => 'text', + 'label' => $this->trans('Meta title', [], 'Admin.Global'), + 'name' => 'meta_title', + 'lang' => true, + 'col' => 4, + 'hint' => $this->trans('Invalid characters:', [], 'Admin.Notifications.Info') . ' <>;=#{}', + ], + [ + 'type' => 'text', + 'label' => $this->trans('Meta description', [], 'Admin.Global'), + 'name' => 'meta_description', + 'lang' => true, + 'col' => 6, + 'hint' => $this->trans('Invalid characters:', [], 'Admin.Notifications.Info') . ' <>;=#{}', + ], + [ + 'type' => 'tags', + 'label' => $this->trans('Meta keywords', [], 'Admin.Global'), + 'name' => 'meta_keywords', + 'lang' => true, + 'col' => 6, + 'hint' => [ + $this->trans('To add tags, click in the field, write something, and then press the "Enter" key.', [], 'Admin.Shopparameters.Help'), + $this->trans('Invalid characters:', [], 'Admin.Notifications.Info') . ' <>;=#{}', + ], + ], + [ + 'type' => 'switch', + 'label' => $this->trans('Enable', [], 'Admin.Actions'), + 'name' => 'active', + 'required' => false, + 'class' => 't', + 'is_bool' => true, + 'values' => [ + [ + 'id' => 'active_on', + 'value' => 1, + 'label' => $this->trans('Enabled', [], 'Admin.Global'), + ], + [ + 'id' => 'active_off', + 'value' => 0, + 'label' => $this->trans('Disabled', [], 'Admin.Global'), + ], + ], + ], + ], + 'submit' => [ + 'title' => $this->trans('Save', [], 'Admin.Actions'), + ], + ]; + + // loads current address for this supplier - if possible + $address = null; + if (isset($obj->id)) { + $id_address = Address::getAddressIdBySupplierId($obj->id); + + if ($id_address > 0) { + $address = new Address((int) $id_address); + } + } + + // force specific fields values (address) + if ($address != null) { + $this->fields_value = [ + 'id_address' => $address->id, + 'phone' => $address->phone, + 'phone_mobile' => $address->phone_mobile, + 'address' => $address->address1, + 'address2' => $address->address2, + 'postcode' => $address->postcode, + 'city' => $address->city, + 'id_country' => $address->id_country, + 'id_state' => $address->id_state, + 'dni' => $address->dni, + ]; + } else { + $this->fields_value = [ + 'id_address' => 0, + 'id_country' => Configuration::get('PS_COUNTRY_DEFAULT'), + ]; + } + + if (Shop::isFeatureActive()) { + $this->fields_form['input'][] = [ + 'type' => 'shop', + 'label' => $this->trans('Shop association', [], 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + ]; + } + + return parent::renderForm(); + } + + /** + * AdminController::initToolbar() override. + * + * @see AdminController::initToolbar() + */ + public function initToolbar() + { + parent::initToolbar(); + + if (empty($this->display) && $this->can_import) { + $this->toolbar_btn['import'] = [ + 'href' => $this->context->link->getAdminLink('AdminImport', true) . '&import_type=suppliers', + 'desc' => $this->trans('Import', [], 'Admin.Actions'), + ]; + } + } + + public function renderView() + { + $this->toolbar_title = $this->object->name; + $products = $this->object->getProductsLite($this->context->language->id); + $total_product = count($products); + + for ($i = 0; $i < $total_product; ++$i) { + $products[$i] = new Product($products[$i]['id_product'], false, $this->context->language->id); + $products[$i]->loadStockData(); + // Build attributes combinations + $combinations = $products[$i]->getAttributeCombinations($this->context->language->id); + foreach ($combinations as $combination) { + $comb_infos = Supplier::getProductInformationsBySupplier( + $this->object->id, + $products[$i]->id, + $combination['id_product_attribute'] + ); + $comb_array[$combination['id_product_attribute']]['product_supplier_reference'] = $comb_infos['product_supplier_reference']; + $comb_array[$combination['id_product_attribute']]['product_supplier_price_te'] = $this->context->getCurrentLocale()->formatPrice($comb_infos['product_supplier_price_te'], Currency::getIsoCodeById((int) $comb_infos['id_currency'])); + $comb_array[$combination['id_product_attribute']]['reference'] = $combination['reference']; + $comb_array[$combination['id_product_attribute']]['ean13'] = $combination['ean13']; + $comb_array[$combination['id_product_attribute']]['upc'] = $combination['upc']; + $comb_array[$combination['id_product_attribute']]['mpn'] = $combination['mpn']; + $comb_array[$combination['id_product_attribute']]['quantity'] = $combination['quantity']; + $comb_array[$combination['id_product_attribute']]['attributes'][] = [ + $combination['group_name'], + $combination['attribute_name'], + $combination['id_attribute'], + ]; + } + + if (isset($comb_array)) { + foreach ($comb_array as $key => $product_attribute) { + $list = ''; + foreach ($product_attribute['attributes'] as $attribute) { + $list .= $attribute[0] . ' - ' . $attribute[1] . ', '; + } + $comb_array[$key]['attributes'] = rtrim($list, ', '); + } + isset($comb_array) ? $products[$i]->combination = $comb_array : ''; + unset($comb_array); + } else { + $product_infos = Supplier::getProductInformationsBySupplier( + $this->object->id, + $products[$i]->id, + 0 + ); + $products[$i]->product_supplier_reference = $product_infos['product_supplier_reference']; + $currencyId = $product_infos['id_currency'] ?: Currency::getDefaultCurrency()->id; + $products[$i]->product_supplier_price_te = $this->context->getCurrentLocale()->formatPrice($product_infos['product_supplier_price_te'], Currency::getIsoCodeById((int) $currencyId)); + } + } + + $this->tpl_view_vars = [ + 'supplier' => $this->object, + 'products' => $products, + 'stock_management' => Configuration::get('PS_STOCK_MANAGEMENT'), + 'shopContext' => Shop::getContext(), + ]; + + return parent::renderView(); + } + + protected function afterImageUpload() + { + $return = true; + $generate_hight_dpi_images = (bool) Configuration::get('PS_HIGHT_DPI'); + + /* Generate image with differents size */ + if (($id_supplier = (int) Tools::getValue('id_supplier')) && + isset($_FILES) && count($_FILES) && file_exists(_PS_SUPP_IMG_DIR_ . $id_supplier . '.jpg')) { + $images_types = ImageType::getImagesTypes('suppliers'); + foreach ($images_types as $image_type) { + $file = _PS_SUPP_IMG_DIR_ . $id_supplier . '.jpg'; + if (!ImageManager::resize($file, _PS_SUPP_IMG_DIR_ . $id_supplier . '-' . stripslashes($image_type['name']) . '.jpg', (int) $image_type['width'], (int) $image_type['height'])) { + $return = false; + } + + if ($generate_hight_dpi_images) { + if (!ImageManager::resize($file, _PS_SUPP_IMG_DIR_ . $id_supplier . '-' . stripslashes($image_type['name']) . '2x.jpg', (int) $image_type['width'] * 2, (int) $image_type['height'] * 2)) { + $return = false; + } + } + } + + $current_logo_file = _PS_TMP_IMG_DIR_ . 'supplier_mini_' . $id_supplier . '_' . $this->context->shop->id . '.jpg'; + + if (file_exists($current_logo_file)) { + unlink($current_logo_file); + } + } + + return $return; + } + + /** + * AdminController::postProcess() override. + * + * @see AdminController::postProcess() + */ + public function postProcess() + { + // checks access + if (Tools::isSubmit('submitAdd' . $this->table) && !($this->access('add'))) { + $this->errors[] = $this->trans('You do not have permission to add suppliers.', [], 'Admin.Catalog.Notification'); + + return parent::postProcess(); + } + + if (Tools::isSubmit('submitAdd' . $this->table)) { + if (Tools::isSubmit('id_supplier') && !($obj = $this->loadObject(true))) { + return; + } + + // updates/creates address if it does not exist + if (Tools::isSubmit('id_address') && (int) Tools::getValue('id_address') > 0) { + $address = new Address((int) Tools::getValue('id_address')); + } // updates address + else { + $address = new Address(); + } // creates address + + $address->alias = Tools::getValue('name', null); + $address->lastname = 'supplier'; // skip problem with numeric characters in supplier name + $address->firstname = 'supplier'; // skip problem with numeric characters in supplier name + $address->address1 = Tools::getValue('address', null); + $address->address2 = Tools::getValue('address2', null); + $address->postcode = Tools::getValue('postcode', null); + $address->phone = Tools::getValue('phone', null); + $address->phone_mobile = Tools::getValue('phone_mobile', null); + $address->id_country = Tools::getValue('id_country', null); + $address->id_state = Tools::getValue('id_state', null); + $address->city = Tools::getValue('city', null); + $address->dni = Tools::getValue('dni', null); + + $validation = $address->validateController(); + + /* + * Make sure dni is checked without raising an exception. + * This field is mandatory for some countries. + */ + if ($address->validateField('dni', $address->dni) !== true) { + $validation['dni'] = $this->trans( + '%s is invalid.', + [ + 'dni', + ], + 'Admin.Notifications.Error' + ); + } + + // checks address validity + if (count($validation) > 0) { + foreach ($validation as $item) { + $this->errors[] = $item; + } + $this->errors[] = $this->trans('The address is not correct. Please make sure all of the required fields are completed.', [], 'Admin.Catalog.Notification'); + } else { + if (Tools::isSubmit('id_address') && Tools::getValue('id_address') > 0) { + $address->update(); + } else { + $address->save(); + $_POST['id_address'] = $address->id; + } + } + + return parent::postProcess(); + } elseif (Tools::isSubmit('delete' . $this->table)) { + if (!($obj = $this->loadObject(true))) { + return; + } elseif (SupplyOrder::supplierHasPendingOrders($obj->id)) { + $this->errors[] = $this->trans('It is not possible to delete a supplier if there are pending supplier orders.', [], 'Admin.Catalog.Notification'); + } else { + //delete all product_supplier linked to this supplier + Db::getInstance()->execute('DELETE FROM `' . _DB_PREFIX_ . 'product_supplier` WHERE `id_supplier`=' . (int) $obj->id); + + $id_address = Address::getAddressIdBySupplierId($obj->id); + $address = new Address($id_address); + if (Validate::isLoadedObject($address)) { + $address->deleted = 1; + $address->save(); + } + + return parent::postProcess(); + } + } else { + return parent::postProcess(); + } + } + + /** + * @see AdminController::afterAdd() + * + * @param Supplier $object + * + * @return bool + */ + protected function afterAdd($object) + { + $id_address = (int) $_POST['id_address']; + $address = new Address($id_address); + if (Validate::isLoadedObject($address)) { + $address->id_supplier = $object->id; + $address->save(); + } + + return true; + } + + /** + * @see AdminController::afterUpdate() + * + * @param Supplier $object + * + * @return bool + */ + protected function afterUpdate($object) + { + $id_address = (int) $_POST['id_address']; + $address = new Address($id_address); + if (Validate::isLoadedObject($address)) { + if ($address->id_supplier != $object->id) { + $address->id_supplier = $object->id; + $address->save(); + } + } + + return true; + } +} diff --git a/controllers/admin/AdminTabsController.php b/controllers/admin/AdminTabsController.php new file mode 100644 index 00000000..e95a9dc4 --- /dev/null +++ b/controllers/admin/AdminTabsController.php @@ -0,0 +1,384 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +/** + * @property Tab $object + */ +class AdminTabsControllerCore extends AdminController +{ + protected $position_identifier = 'id_tab'; + + public function __construct() + { + $this->bootstrap = true; + $this->multishop_context = Shop::CONTEXT_ALL; + $this->table = 'tab'; + $this->list_id = 'tab'; + $this->className = 'Tab'; + $this->lang = true; + + parent::__construct(); + + $this->fieldImageSettings = [ + 'name' => 'icon', + 'dir' => 't', + ]; + $this->imageType = 'gif'; + $this->bulk_actions = [ + 'delete' => [ + 'text' => $this->trans('Delete selected', [], 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', [], 'Admin.Actions'), + 'icon' => 'icon-trash', + ], + ]; + $this->fields_list = [ + 'id_tab' => [ + 'title' => $this->trans('ID', [], 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ], + 'name' => [ + 'title' => $this->trans('Name', [], 'Admin.Global'), + ], + 'class_name' => [ + 'title' => $this->trans('Class', [], 'Admin.Global'), + ], + 'module' => [ + 'title' => $this->trans('Module', [], 'Admin.Global'), + ], + 'active' => [ + 'title' => $this->trans('Enabled', [], 'Admin.Global'), + 'align' => 'center', + 'active' => 'status', + 'type' => 'bool', + 'orderby' => false, + ], + 'position' => [ + 'title' => $this->trans('Position', [], 'Admin.Global'), + 'filter_key' => 'a!position', + 'position' => 'position', + 'align' => 'center', + 'class' => 'fixed-width-md', + ], + ]; + } + + public function initPageHeaderToolbar() + { + $this->page_header_toolbar_title = $this->trans('Menus', [], 'Admin.Global'); + + if ($this->display == 'details') { + $this->page_header_toolbar_btn['back_to_list'] = [ + 'href' => Context::getContext()->link->getAdminLink('AdminTabs'), + 'desc' => $this->trans('Back to list', [], 'Admin.Actions'), + 'icon' => 'process-icon-back', + ]; + } elseif (empty($this->display)) { + $this->page_header_toolbar_btn['new_menu'] = [ + 'href' => self::$currentIndex . '&addtab&token=' . $this->token, + 'desc' => $this->trans('Add new menu', [], 'Admin.Actions'), + 'icon' => 'process-icon-new', + ]; + } + + parent::initPageHeaderToolbar(); + } + + /** + * AdminController::renderForm() override. + * + * @see AdminController::renderForm() + */ + public function renderForm() + { + $tabs = Tab::getTabs($this->context->language->id, 0); + // If editing, we clean itself + if (Tools::isSubmit('id_tab')) { + foreach ($tabs as $key => $tab) { + if ($tab['id_tab'] == Tools::getValue('id_tab')) { + unset($tabs[$key]); + } + } + } + + // added category "Home" in var $tabs + $tab_zero = [ + 'id_tab' => 0, + 'name' => $this->trans('Home', [], 'Admin.Global'), + ]; + array_unshift($tabs, $tab_zero); + + $this->fields_form = [ + 'legend' => [ + 'title' => $this->trans('Menus', [], 'Admin.Global'), + 'icon' => 'icon-list-ul', + ], + 'input' => [ + [ + 'type' => 'hidden', + 'name' => 'position', + 'required' => false, + ], + [ + 'type' => 'text', + 'label' => $this->trans('Name', [], 'Admin.Global'), + 'name' => 'name', + 'lang' => true, + 'required' => true, + 'hint' => $this->trans('Invalid characters:', [], 'Admin.Notifications.Info') . ' <>;=#{}', + ], + [ + 'type' => 'text', + 'label' => $this->trans('Class', [], 'Admin.Global'), + 'name' => 'class_name', + 'required' => true, + ], + [ + 'type' => 'text', + 'label' => $this->trans('Module', [], 'Admin.Global'), + 'name' => 'module', + ], + [ + 'type' => 'switch', + 'label' => $this->trans('Status', [], 'Admin.Global'), + 'name' => 'active', + 'required' => false, + 'is_bool' => true, + 'values' => [ + [ + 'id' => 'active_on', + 'value' => 1, + 'label' => $this->trans('Enabled', [], 'Admin.Global'), + ], + [ + 'id' => 'active_off', + 'value' => 0, + 'label' => $this->trans('Disabled', [], 'Admin.Global'), + ], + ], + 'hint' => $this->trans('Show or hide menu.', [], 'Admin.Actions'), + ], + ], + 'submit' => [ + 'title' => $this->trans('Save', [], 'Admin.Actions'), + ], + ]; + + $display_parent = true; + if (Validate::isLoadedObject($this->object) && !class_exists($this->object->class_name . 'Controller')) { + $display_parent = false; + } + + if ($display_parent) { + $this->fields_form['input'][] = [ + 'type' => 'select', + 'label' => $this->trans('Parent', [], 'Admin.Global'), + 'name' => 'id_parent', + 'options' => [ + 'query' => $tabs, + 'id' => 'id_tab', + 'name' => 'name', + ], + ]; + } + + return parent::renderForm(); + } + + /** + * AdminController::renderList() override. + * + * @see AdminController::renderList() + */ + public function renderList() + { + $this->addRowAction('edit'); + $this->addRowAction('details'); + $this->addRowAction('delete'); + + $this->_where = 'AND a.`id_parent` = 0'; + $this->_orderBy = 'position'; + + return parent::renderList(); + } + + public function initProcess() + { + if (Tools::getIsset('details' . $this->table)) { + $this->list_id = 'details'; + + if (isset($_POST['submitReset' . $this->list_id])) { + $this->processResetFilters(); + } + } else { + $this->list_id = 'tab'; + } + + return parent::initProcess(); + } + + public function renderDetails() + { + if (($id = Tools::getValue('id_tab'))) { + $this->lang = false; + $this->list_id = 'details'; + $this->addRowAction('edit'); + $this->addRowAction('delete'); + $this->toolbar_btn = []; + + /** @var Tab $tab */ + $tab = $this->loadObject($id); + $this->toolbar_title = $tab->name[$this->context->employee->id_lang]; + + $this->_select = 'b.*'; + $this->_join = 'LEFT JOIN `' . _DB_PREFIX_ . 'tab_lang` b ON (b.`id_tab` = a.`id_tab` AND b.`id_lang` = ' . + (int) $this->context->language->id . ')'; + $this->_where = 'AND a.`id_parent` = ' . (int) $id; + $this->_orderBy = 'position'; + $this->_use_found_rows = false; + + self::$currentIndex = self::$currentIndex . '&details' . $this->table; + $this->processFilter(); + + return parent::renderList(); + } + } + + public function postProcess() + { + /* PrestaShop demo mode */ + if (_PS_MODE_DEMO_) { + $this->errors[] = $this->trans('This functionality has been disabled.', [], 'Admin.Notifications.Error'); + + return; + } + /* PrestaShop demo mode*/ + + if (($id_tab = (int) Tools::getValue('id_tab')) && ($direction = Tools::getValue('move')) && Validate::isLoadedObject($tab = new Tab($id_tab))) { + if ($tab->move($direction)) { + Tools::redirectAdmin(self::$currentIndex . '&token=' . $this->token); + } + } elseif (Tools::getValue('position') && !Tools::isSubmit('submitAdd' . $this->table)) { + if ($this->access('edit') !== '1') { + $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error'); + } elseif (!Validate::isLoadedObject($object = new Tab((int) Tools::getValue($this->identifier)))) { + $this->errors[] = $this->trans('An error occurred while updating the status for an object.', [], 'Admin.Notifications.Error') . + ' ' . $this->table . ' ' . $this->trans('(cannot load object)', [], 'Admin.Notifications.Error'); + } + if (!$object->updatePosition((int) Tools::getValue('way'), (int) Tools::getValue('position'))) { + $this->errors[] = $this->trans('Failed to update the position.', [], 'Admin.Notifications.Error'); + } else { + Tools::redirectAdmin(self::$currentIndex . '&conf=5&token=' . Tools::getAdminTokenLite('AdminTabs')); + } + } elseif (Tools::isSubmit('submitAdd' . $this->table) && Tools::getValue('id_tab') === Tools::getValue('id_parent')) { + $this->errors[] = $this->trans('You can\'t put this menu inside itself. ', [], 'Admin.Advparameters.Notification'); + } elseif (Tools::isSubmit('submitAdd' . $this->table) && $id_parent = (int) Tools::getValue('id_parent')) { + $this->redirect_after = self::$currentIndex . '&id_' . $this->table . '=' . $id_parent . '&details' . $this->table . '&conf=4&token=' . $this->token; + } elseif (isset($_GET['details' . $this->table]) && is_array($this->bulk_actions)) { + $submit_bulk_actions = array_merge([ + 'enableSelection' => [ + 'text' => $this->trans('Enable selection', [], 'Admin.Actions'), + 'icon' => 'icon-power-off text-success', + ], + 'disableSelection' => [ + 'text' => $this->trans('Disable selection', [], 'Admin.Actions'), + 'icon' => 'icon-power-off text-danger', + ], + ], $this->bulk_actions); + foreach ($submit_bulk_actions as $bulk_action => $params) { + if (Tools::isSubmit('submitBulk' . $bulk_action . $this->table) || Tools::isSubmit('submitBulk' . $bulk_action)) { + if ($this->access('edit')) { + $this->action = 'bulk' . $bulk_action; + $this->boxes = Tools::getValue($this->list_id . 'Box'); + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error'); + } + + break; + } elseif (Tools::isSubmit('submitBulk')) { + if ($this->access('edit')) { + $this->action = 'bulk' . Tools::getValue('select_submitBulk'); + $this->boxes = Tools::getValue($this->list_id . 'Box'); + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error'); + } + + break; + } + } + } else { + // Temporary add the position depend of the selection of the parent category + if (!Tools::isSubmit('id_tab')) { // @todo Review + $_POST['position'] = Tab::getNbTabs(Tools::getValue('id_parent')); + } + } + + if (!count($this->errors)) { + parent::postProcess(); + } + } + + protected function afterImageUpload() + { + /** @var Tab $obj */ + if (!($obj = $this->loadObject(true))) { + return; + } + @rename(_PS_IMG_DIR_ . 't/' . $obj->id . '.gif', _PS_IMG_DIR_ . 't/' . $obj->class_name . '.gif'); + } + + public function ajaxProcessUpdatePositions() + { + $way = (int) (Tools::getValue('way')); + $id_tab = (int) (Tools::getValue('id')); + $positions = Tools::getValue('tab'); + + // when changing positions in a tab sub-list, the first array value is empty and needs to be removed + if (!$positions[0]) { + unset($positions[0]); + // reset indexation from 0 + $positions = array_merge($positions); + } + + foreach ($positions as $position => $value) { + $pos = explode('_', $value); + + if (isset($pos[2]) && (int) $pos[2] === $id_tab) { + if ($tab = new Tab((int) $pos[2])) { + if (isset($position) && $tab->updatePosition($way, $position)) { + echo 'ok position ' . (int) $position . ' for tab ' . (int) $pos[1] . '\r\n'; + } else { + echo '{"hasError" : true, "errors" : "Can not update tab ' . (int) $id_tab . ' to position ' . (int) $position . ' "}'; + } + } else { + echo '{"hasError" : true, "errors" : "This tab (' . (int) $id_tab . ') can t be loaded"}'; + } + + break; + } + } + } +} diff --git a/controllers/admin/AdminTagsController.php b/controllers/admin/AdminTagsController.php new file mode 100644 index 00000000..11633090 --- /dev/null +++ b/controllers/admin/AdminTagsController.php @@ -0,0 +1,168 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +/** + * @property Tag $object + */ +class AdminTagsControllerCore extends AdminController +{ + public $bootstrap = true; + + public function __construct() + { + $this->table = 'tag'; + $this->className = 'Tag'; + + parent::__construct(); + + $this->fields_list = [ + 'id_tag' => [ + 'title' => $this->trans('ID', [], 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ], + 'lang' => [ + 'title' => $this->trans('Language', [], 'Admin.Global'), + 'filter_key' => 'l!name', + ], + 'name' => [ + 'title' => $this->trans('Name', [], 'Admin.Global'), + 'filter_key' => 'a!name', + ], + 'products' => [ + 'title' => $this->trans('Products', [], 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + 'havingFilter' => true, + ], + ]; + + $this->bulk_actions = [ + 'delete' => [ + 'text' => $this->trans('Delete selected', [], 'Admin.Actions'), + 'icon' => 'icon-trash', + 'confirm' => $this->trans('Delete selected items?', [], 'Admin.Notifications.Warning'), + ], + ]; + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_tag'] = [ + 'href' => self::$currentIndex . '&addtag&token=' . $this->token, + 'desc' => $this->trans('Add new tag', [], 'Admin.Shopparameters.Feature'), + 'icon' => 'process-icon-new', + ]; + } + + parent::initPageHeaderToolbar(); + } + + public function renderList() + { + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + $this->_select = 'l.name as lang, COUNT(pt.id_product) as products'; + $this->_join = ' + LEFT JOIN `' . _DB_PREFIX_ . 'product_tag` pt + ON (a.`id_tag` = pt.`id_tag`) + LEFT JOIN `' . _DB_PREFIX_ . 'lang` l + ON (l.`id_lang` = a.`id_lang`)'; + $this->_group = 'GROUP BY a.name, a.id_lang'; + + return parent::renderList(); + } + + public function postProcess() + { + if ($this->access('edit') && Tools::getValue('submitAdd' . $this->table)) { + if (($id = (int) Tools::getValue($this->identifier)) && ($obj = new $this->className($id)) && Validate::isLoadedObject($obj)) { + /** @var Tag $obj */ + $previous_products = $obj->getProducts(); + $removed_products = []; + + foreach ($previous_products as $product) { + if (!in_array($product['id_product'], $_POST['products'])) { + $removed_products[] = $product['id_product']; + } + } + + if (Configuration::get('PS_SEARCH_INDEXATION')) { + Search::removeProductsSearchIndex($removed_products); + } + + $obj->setProducts($_POST['products']); + } + } + + return parent::postProcess(); + } + + public function renderForm() + { + /** @var Tag $obj */ + if (!($obj = $this->loadObject(true))) { + return; + } + + $this->fields_form = [ + 'legend' => [ + 'title' => $this->trans('Tag', [], 'Admin.Shopparameters.Feature'), + 'icon' => 'icon-tag', + ], + 'input' => [ + [ + 'type' => 'text', + 'label' => $this->trans('Name', [], 'Admin.Global'), + 'name' => 'name', + 'required' => true, + ], + [ + 'type' => 'select', + 'label' => $this->trans('Language', [], 'Admin.Global'), + 'name' => 'id_lang', + 'required' => true, + 'options' => [ + 'query' => Language::getLanguages(false), + 'id' => 'id_lang', + 'name' => 'name', + ], + ], + ], + 'selects' => [ + 'products' => $obj->getProducts(true), + 'products_unselected' => $obj->getProducts(false), + ], + 'submit' => [ + 'title' => $this->trans('Save', [], 'Admin.Actions'), + ], + ]; + + return parent::renderForm(); + } +} diff --git a/controllers/admin/AdminTaxRulesGroupController.php b/controllers/admin/AdminTaxRulesGroupController.php new file mode 100644 index 00000000..4cee0c65 --- /dev/null +++ b/controllers/admin/AdminTaxRulesGroupController.php @@ -0,0 +1,578 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +/** + * @property TaxRulesGroup $object + */ +class AdminTaxRulesGroupControllerCore extends AdminController +{ + public $tax_rule; + public $selected_countries = []; + public $selected_states = []; + public $errors_tax_rule; + + public function __construct() + { + $this->bootstrap = true; + $this->table = 'tax_rules_group'; + $this->className = 'TaxRulesGroup'; + $this->lang = false; + + parent::__construct(); + + $this->fields_list = [ + 'id_tax_rules_group' => [ + 'title' => $this->trans('ID', [], 'Admin.Global'), + 'align' => 'center', + 'class' => 'fixed-width-xs', + ], + 'name' => [ + 'title' => $this->trans('Name', [], 'Admin.Global'), + ], + 'active' => [ + 'title' => $this->trans('Enabled', [], 'Admin.Global'), + 'active' => 'status', + 'type' => 'bool', + 'orderby' => false, + 'align' => 'center', + 'class' => 'fixed-width-sm', + ], + ]; + + $this->bulk_actions = [ + 'delete' => [ + 'text' => $this->trans('Delete selected', [], 'Admin.Actions'), + 'confirm' => $this->trans('Delete selected items?', [], 'Admin.Notifications.Warning'), + 'icon' => 'icon-trash', + ], + ]; + + $this->_where .= ' AND a.deleted = 0'; + } + + public function initPageHeaderToolbar() + { + if (empty($this->display)) { + $this->page_header_toolbar_btn['new_tax_rules_group'] = [ + 'href' => self::$currentIndex . '&addtax_rules_group&token=' . $this->token, + 'desc' => $this->trans('Add new tax rules group', [], 'Admin.International.Feature'), + 'icon' => 'process-icon-new', + ]; + } + if ($this->display === 'edit') { + $this->page_header_toolbar_btn['new'] = [ + 'href' => '#', + 'desc' => $this->trans('Add a new tax rule', [], 'Admin.International.Feature'), + ]; + } + + parent::initPageHeaderToolbar(); + } + + public function renderList() + { + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + return parent::renderList(); + } + + public function initRulesList($id_group) + { + $this->table = 'tax_rule'; + $this->list_id = 'tax_rule'; + $this->identifier = 'id_tax_rule'; + $this->className = 'TaxRule'; + $this->lang = false; + $this->list_simple_header = false; + $this->toolbar_btn = null; + $this->list_no_link = true; + + $this->bulk_actions = [ + 'delete' => ['text' => $this->trans('Delete selected', [], 'Admin.Actions'), 'confirm' => $this->trans('Delete selected items?', [], 'Admin.Notifications.Warning'), 'icon' => 'icon-trash'], + ]; + + $this->fields_list = [ + 'country_name' => [ + 'title' => $this->trans('Country', [], 'Admin.Global'), + ], + 'state_name' => [ + 'title' => $this->trans('State', [], 'Admin.Global'), + ], + 'zipcode' => [ + 'title' => $this->trans('Zip/Postal code', [], 'Admin.Global'), + 'class' => 'fixed-width-md', + ], + 'behavior' => [ + 'title' => $this->trans('Behavior', [], 'Admin.International.Feature'), + ], + 'rate' => [ + 'title' => $this->trans('Tax', [], 'Admin.Global'), + 'class' => 'fixed-width-sm', + ], + 'description' => [ + 'title' => $this->trans('Description', [], 'Admin.Global'), + ], + ]; + + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + $this->_select = ' + c.`name` AS country_name, + s.`name` AS state_name, + CONCAT_WS(" - ", a.`zipcode_from`, a.`zipcode_to`) AS zipcode, + t.rate'; + + $this->_join = ' + LEFT JOIN `' . _DB_PREFIX_ . 'country_lang` c + ON (a.`id_country` = c.`id_country` AND id_lang = ' . (int) $this->context->language->id . ') + LEFT JOIN `' . _DB_PREFIX_ . 'state` s + ON (a.`id_state` = s.`id_state`) + LEFT JOIN `' . _DB_PREFIX_ . 'tax` t + ON (a.`id_tax` = t.`id_tax`)'; + $this->_where = 'AND `id_tax_rules_group` = ' . (int) $id_group; + $this->_use_found_rows = false; + + $this->show_toolbar = false; + $this->tpl_list_vars = ['id_tax_rules_group' => (int) $id_group]; + + $this->_filter = false; + + return parent::renderList(); + } + + public function renderForm() + { + $this->fields_form = [ + 'legend' => [ + 'title' => $this->trans('Tax Rules', [], 'Admin.International.Feature'), + 'icon' => 'icon-money', + ], + 'input' => [ + [ + 'type' => 'text', + 'label' => $this->trans('Name', [], 'Admin.Global'), + 'name' => 'name', + 'required' => true, + 'hint' => $this->trans('Invalid characters:', [], 'Admin.Notifications.Info') . ' <>;=#{}', + ], + [ + 'type' => 'switch', + 'label' => $this->trans('Enable', [], 'Admin.Actions'), + 'name' => 'active', + 'required' => false, + 'is_bool' => true, + 'values' => [ + [ + 'id' => 'active_on', + 'value' => 1, + 'label' => $this->trans('Enabled', [], 'Admin.Global'), + ], + [ + 'id' => 'active_off', + 'value' => 0, + 'label' => $this->trans('Disabled', [], 'Admin.Global'), + ], + ], + ], + ], + 'submit' => [ + 'title' => $this->trans('Save and stay', [], 'Admin.Actions'), + 'stay' => true, + ], + ]; + + if (Shop::isFeatureActive()) { + $this->fields_form['input'][] = [ + 'type' => 'shop', + 'label' => $this->trans('Shop association', [], 'Admin.Global'), + 'name' => 'checkBoxShopAsso', + ]; + } + + if (!($obj = $this->loadObject(true))) { + return; + } + if (!isset($obj->id)) { + $this->no_back = false; + $content = parent::renderForm(); + } else { + $this->no_back = true; + $this->page_header_toolbar_btn['new'] = [ + 'href' => '#', + 'desc' => $this->trans('Add a new tax rule', [], 'Admin.International.Feature'), + ]; + $content = parent::renderForm(); + $this->tpl_folder = 'tax_rules/'; + $content .= $this->initRuleForm(); + + // We change the variable $ tpl_folder to avoid the overhead calling the file in list_action_edit.tpl in intList (); + + $content .= $this->initRulesList((int) $obj->id); + } + + return $content; + } + + public function initRuleForm() + { + $this->fields_form[0]['form'] = [ + 'legend' => [ + 'title' => $this->trans('New tax rule', [], 'Admin.International.Feature'), + 'icon' => 'icon-money', + ], + 'input' => [ + [ + 'type' => 'select', + 'label' => $this->trans('Country', [], 'Admin.Global'), + 'name' => 'country', + 'id' => 'country', + 'options' => [ + 'query' => Country::getCountries($this->context->language->id), + 'id' => 'id_country', + 'name' => 'name', + 'default' => [ + 'value' => 0, + 'label' => $this->trans('All', [], 'Admin.Global'), + ], + ], + ], + [ + 'type' => 'select', + 'label' => $this->trans('State', [], 'Admin.Global'), + 'name' => 'states[]', + 'id' => 'states', + 'multiple' => true, + 'options' => [ + 'query' => [], + 'id' => 'id_state', + 'name' => 'name', + 'default' => [ + 'value' => 0, + 'label' => $this->trans('All', [], 'Admin.Global'), + ], + ], + ], + [ + 'type' => 'hidden', + 'name' => 'action', + ], + [ + 'type' => 'text', + 'label' => $this->trans('Zip/postal code range', [], 'Admin.International.Feature'), + 'name' => 'zipcode', + 'required' => false, + 'hint' => $this->trans('You can define a range of Zip/postal codes (e.g., 75000-75015) or simply use one Zip/postal code.', [], 'Admin.International.Help'), + ], + [ + 'type' => 'select', + 'label' => $this->trans('Behavior', [], 'Admin.International.Feature'), + 'name' => 'behavior', + 'required' => false, + 'options' => [ + 'query' => [ + [ + 'id' => 0, + 'name' => $this->trans('This tax only', [], 'Admin.International.Feature'), + ], + [ + 'id' => 1, + 'name' => $this->trans('Combine', [], 'Admin.International.Feature'), + ], + [ + 'id' => 2, + 'name' => $this->trans('One after another', [], 'Admin.International.Feature'), + ], + ], + 'id' => 'id', + 'name' => 'name', + ], + 'hint' => [ + $this->trans('You must define the behavior if an address matches multiple rules:', [], 'Admin.International.Help') . '', + $this->trans('- This tax only: Will apply only this tax', [], 'Admin.International.Help') . '', + $this->trans('- Combine: Combine taxes (e.g.: 10% + 5% = 15%)', [], 'Admin.International.Help') . '', + $this->trans('- One after another: Apply taxes one after another (e.g.: 100 + 10% => 110 + 5% = 115.5)', [], 'Admin.International.Help'), + ], + ], + [ + 'type' => 'select', + 'label' => $this->trans('Tax', [], 'Admin.Global'), + 'name' => 'id_tax', + 'required' => false, + 'options' => [ + 'query' => Tax::getTaxes((int) $this->context->language->id), + 'id' => 'id_tax', + 'name' => 'name', + 'default' => [ + 'value' => 0, + 'label' => $this->trans('No Tax', [], 'Admin.International.Help'), + ], + ], + 'hint' => $this->trans('(Total tax: 9%)', [], 'Admin.International.Help'), + ], + [ + 'type' => 'text', + 'label' => $this->trans('Description', [], 'Admin.Global'), + 'name' => 'description', + ], + ], + 'submit' => [ + 'title' => $this->trans('Save and stay', [], 'Admin.Actions'), + 'stay' => true, + ], + ]; + + if (!($obj = $this->loadObject(true))) { + return; + } + + $this->fields_value = [ + 'action' => 'create_rule', + 'id_tax_rules_group' => $obj->id, + 'id_tax_rule' => '', + ]; + + $this->getlanguages(); + $helper = new HelperForm(); + $helper->override_folder = $this->tpl_folder; + $helper->currentIndex = self::$currentIndex; + $helper->token = $this->token; + $helper->table = 'tax_rule'; + $helper->identifier = 'id_tax_rule'; + $helper->id = $obj->id; + $helper->toolbar_scroll = true; + $helper->show_toolbar = true; + $helper->languages = $this->_languages; + $helper->default_form_language = $this->default_form_language; + $helper->allow_employee_form_lang = $this->allow_employee_form_lang; + $helper->fields_value = $this->getFieldsValue($this->object); + $helper->toolbar_btn['save_new_rule'] = [ + 'href' => self::$currentIndex . '&id_tax_rules_group=' . $obj->id . '&action=create_rule&token=' . $this->token, + 'desc' => 'Save tax rule', + 'class' => 'process-icon-save', + ]; + $helper->submit_action = 'create_rule'; + + return $helper->generateForm($this->fields_form); + } + + public function initProcess() + { + if (Tools::isSubmit('deletetax_rule')) { + if ($this->access('delete')) { + $this->action = 'delete_tax_rule'; + } else { + $this->errors[] = $this->trans('You do not have permission to delete this.', [], 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('submitBulkdeletetax_rule')) { + if ($this->access('delete')) { + $this->action = 'bulk_delete_tax_rules'; + } else { + $this->errors[] = $this->trans('You do not have permission to delete this.', [], 'Admin.Notifications.Error'); + } + } elseif (Tools::getValue('action') == 'create_rule') { + if ($this->access('add')) { + $this->action = 'create_rule'; + } else { + $this->errors[] = $this->trans('You do not have permission to add this.', [], 'Admin.Notifications.Error'); + } + } else { + parent::initProcess(); + } + } + + protected function processCreateRule() + { + $zip_code = Tools::getValue('zipcode'); + $zip_code = ('' === $zip_code) ? 0 : $zip_code; + $id_rule = (int) Tools::getValue('id_tax_rule'); + $id_tax = (int) Tools::getValue('id_tax'); + $id_tax_rules_group = (int) Tools::getValue('id_tax_rules_group'); + $behavior = (int) Tools::getValue('behavior'); + $description = pSQL(Tools::getValue('description')); + + if ((int) ($id_country = Tools::getValue('country')) == 0) { + $countries = Country::getCountries($this->context->language->id); + $this->selected_countries = []; + foreach ($countries as $country) { + $this->selected_countries[] = (int) $country['id_country']; + } + } else { + $this->selected_countries = [$id_country]; + } + $this->selected_states = Tools::getValue('states'); + + if (empty($this->selected_states) || count($this->selected_states) == 0) { + $this->selected_states = [0]; + } + $tax_rules_group = new TaxRulesGroup((int) $id_tax_rules_group); + foreach ($this->selected_countries as $id_country) { + $first = true; + foreach ($this->selected_states as $id_state) { + if ($tax_rules_group->hasUniqueTaxRuleForCountry($id_country, $id_state, $id_rule)) { + $this->errors[] = $this->trans('A tax rule already exists for this country/state with tax only behavior.', [], 'Admin.International.Notification'); + + continue; + } + $tr = new TaxRule(); + + // update or creation? + if (isset($id_rule) && $first) { + $tr->id = $id_rule; + $first = false; + } + + $tr->id_tax = $id_tax; + $tax_rules_group = new TaxRulesGroup((int) $id_tax_rules_group); + $tr->id_tax_rules_group = (int) $tax_rules_group->id; + $tr->id_country = (int) $id_country; + $tr->id_state = (int) $id_state; + list($tr->zipcode_from, $tr->zipcode_to) = $tr->breakDownZipCode($zip_code); + + // Construct Object Country + $country = new Country((int) $id_country, (int) $this->context->language->id); + + if ($zip_code && $country->need_zip_code) { + if ($country->zip_code_format) { + foreach ([$tr->zipcode_from, $tr->zipcode_to] as $zip_code) { + if ($zip_code) { + if (!$country->checkZipCode($zip_code)) { + $this->errors[] = $this->trans( + 'The Zip/postal code is invalid. It must be typed as follows: %format% for %country%.', + [ + '%format%' => str_replace('C', $country->iso_code, str_replace('N', '0', str_replace('L', 'A', $country->zip_code_format))), + '%country%' => $country->name, + ], + 'Admin.International.Notification' + ); + } + } + } + } + } + + $tr->behavior = (int) $behavior; + $tr->description = $description; + $this->tax_rule = $tr; + $_POST['id_state'] = $tr->id_state; + + $this->errors = array_merge($this->errors, $this->validateTaxRule($tr)); + + if (count($this->errors) == 0) { + $tax_rules_group = $this->updateTaxRulesGroup($tax_rules_group); + $tr->id = (int) $tax_rules_group->getIdTaxRuleGroupFromHistorizedId((int) $tr->id); + $tr->id_tax_rules_group = (int) $tax_rules_group->id; + + if (!$tr->save()) { + $this->errors[] = $this->trans('An error has occurred: Cannot save the current tax rule.', [], 'Admin.International.Notification'); + } + } + } + } + + if (count($this->errors) == 0) { + Tools::redirectAdmin( + self::$currentIndex . '&' . $this->identifier . '=' . (int) $tax_rules_group->id . '&conf=4&update' . $this->table . '&token=' . $this->token + ); + } else { + $this->display = 'edit'; + } + } + + protected function processBulkDeleteTaxRules() + { + $this->deleteTaxRule(Tools::getValue('tax_ruleBox')); + } + + protected function processDeleteTaxRule() + { + $this->deleteTaxRule([Tools::getValue('id_tax_rule')]); + } + + protected function deleteTaxRule(array $id_tax_rule_list) + { + $result = true; + + foreach ($id_tax_rule_list as $id_tax_rule) { + $tax_rule = new TaxRule((int) $id_tax_rule); + if (Validate::isLoadedObject($tax_rule)) { + $tax_rules_group = new TaxRulesGroup((int) $tax_rule->id_tax_rules_group); + $tax_rules_group = $this->updateTaxRulesGroup($tax_rules_group); + $tax_rule = new TaxRule($tax_rules_group->getIdTaxRuleGroupFromHistorizedId((int) $id_tax_rule)); + if (Validate::isLoadedObject($tax_rule)) { + $result &= $tax_rule->delete(); + } + } + } + + Tools::redirectAdmin( + self::$currentIndex . '&' . $this->identifier . '=' . (int) $tax_rules_group->id . '&conf=4&update' . $this->table . '&token=' . $this->token + ); + } + + /** + * Check if the tax rule could be added in the database. + * + * @param TaxRule $tr + * + * @return array + */ + protected function validateTaxRule(TaxRule $tr) + { + // @TODO: check if the rule already exists + return $tr->validateController(); + } + + protected function displayAjaxUpdateTaxRule() + { + if ($this->access('view')) { + $id_tax_rule = Tools::getValue('id_tax_rule'); + $tax_rules = new TaxRule((int) $id_tax_rule); + $output = []; + foreach ($tax_rules as $key => $result) { + $output[$key] = $result; + } + die(json_encode($output)); + } + } + + /** + * @param TaxRulesGroup $object + * + * @return TaxRulesGroup + */ + protected function updateTaxRulesGroup($object) + { + static $tax_rules_group = null; + if ($tax_rules_group === null) { + $object->update(); + $tax_rules_group = $object; + } + + return $tax_rules_group; + } +} diff --git a/controllers/admin/AdminTrackingController.php b/controllers/admin/AdminTrackingController.php new file mode 100644 index 00000000..85b1cadd --- /dev/null +++ b/controllers/admin/AdminTrackingController.php @@ -0,0 +1,482 @@ + + * @copyright 2007-2019 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + * International Registered Trademark & Property of PrestaShop SA + */ +use PrestaShop\PrestaShop\Adapter\SymfonyContainer; + +/** + * @property Product|Category $object + */ +class AdminTrackingControllerCore extends AdminController +{ + public $bootstrap = true; + + /** @var HelperList */ + protected $_helper_list; + + public function postprocess() + { + if (Tools::getValue('id_product') && Tools::isSubmit('statusproduct')) { + $this->table = 'product'; + $this->identifier = 'id_product'; + $this->action = 'status'; + $this->className = 'Product'; + } elseif (Tools::getValue('id_category') && Tools::isSubmit('statuscategory')) { + $this->table = 'category'; + $this->identifier = 'id_category'; + $this->action = 'status'; + $this->className = 'Category'; + } + + $this->list_no_link = true; + + parent::postprocess(); + } + + public function initContent() + { + if ($id_category = Tools::getValue('id_category') && Tools::getIsset('viewcategory')) { + Tools::redirectAdmin($this->context->link->getAdminLink('AdminProducts') . '&id_category=' . (int) $id_category . '&viewcategory'); + } + + $this->_helper_list = new HelperList(); + + if (!Configuration::get('PS_STOCK_MANAGEMENT')) { + $this->warnings[] = $this->trans('List of products without available quantities for sale are not displayed because stock management is disabled.', array(), 'Admin.Catalog.Notification'); + } + + $methods = get_class_methods($this); + $tpl_vars['arrayList'] = array(); + foreach ($methods as $method_name) { + if (preg_match('#getCustomList(.+)#', $method_name, $matches)) { + $this->clearListOptions(); + $this->content .= call_user_func(array($this, $matches[0])); + } + } + $this->context->smarty->assign(array( + 'content' => $this->content, + )); + } + + public function getCustomListCategoriesEmpty() + { + $this->table = 'category'; + $this->list_id = 'empty_categories'; + $this->lang = true; + $this->className = 'Category'; + $this->identifier = 'id_category'; + $this->_orderBy = 'id_category'; + $this->_orderWay = 'DESC'; + $this->_list_index = 'index.php?controller=AdminCategories'; + $this->_list_token = Tools::getAdminTokenLite('AdminCategories'); + + $this->addRowAction('edit'); + $this->addRowAction('view'); + $this->addRowActionSkipList('edit', array((int) Configuration::get('PS_ROOT_CATEGORY'))); + + $this->fields_list = (array( + 'id_category' => array('title' => $this->trans('ID', array(), 'Admin.Global'), 'class' => 'fixed-width-xs', 'align' => 'center'), + 'name' => array('title' => $this->trans('Name', array(), 'Admin.Global'), 'filter_key' => 'b!name'), + 'description' => array('title' => $this->trans('Description', array(), 'Admin.Global'), 'callback' => 'getDescriptionClean'), + 'active' => array('title' => $this->trans('Status', array(), 'Admin.Global'), 'type' => 'bool', 'active' => 'status', 'align' => 'center', 'class' => 'fixed-width-xs'), + )); + $this->clearFilters(); + + $this->_join = Shop::addSqlAssociation('category', 'a'); + $this->_filter = ' AND NOT EXISTS ( + SELECT 1 + FROM `' . _DB_PREFIX_ . 'category_product` cp + WHERE a.`id_category` = cp.id_category + ) + AND a.`id_category` != ' . (int) Configuration::get('PS_ROOT_CATEGORY'); + $this->toolbar_title = $this->trans('List of empty categories:', array(), 'Admin.Catalog.Feature'); + + return $this->renderList(); + } + + public function getCustomListProductsAttributesNoStock() + { + if (!Configuration::get('PS_STOCK_MANAGEMENT')) { + return; + } + + $this->table = 'product'; + $this->list_id = 'no_stock_products_attributes'; + $this->lang = true; + $this->identifier = 'id_product'; + $this->_orderBy = 'id_product'; + $this->_orderWay = 'DESC'; + $this->className = 'Product'; + $this->_list_index = 'index.php?controller=AdminProducts'; + $this->_list_token = Tools::getAdminTokenLite('AdminProducts'); + $this->show_toolbar = false; + + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + $this->fields_list = array( + 'id_product' => array('title' => $this->trans('ID', array(), 'Admin.Global'), 'class' => 'fixed-width-xs', 'align' => 'center'), + 'reference' => array('title' => $this->trans('Reference', array(), 'Admin.Global')), + 'name' => array('title' => $this->trans('Name', array(), 'Admin.Global'), 'filter_key' => 'b!name'), + 'active' => array('title' => $this->trans('Status', array(), 'Admin.Global'), 'type' => 'bool', 'active' => 'status', 'align' => 'center', 'class' => 'fixed-width-xs', 'filter_key' => 'a!active'), + ); + + $this->clearFilters(); + + $this->_join = Shop::addSqlAssociation('product', 'a'); + $this->_filter = 'AND EXISTS ( + SELECT 1 + FROM `' . _DB_PREFIX_ . 'product` p + ' . Product::sqlStock('p') . ' + WHERE a.id_product = p.id_product AND EXISTS ( + SELECT 1 + FROM `' . _DB_PREFIX_ . 'product_attribute` WHERE `' . _DB_PREFIX_ . 'product_attribute`.id_product = p.id_product + ) + AND IFNULL(stock.quantity, 0) <= 0 + )'; + $this->toolbar_title = $this->trans('List of products with combinations but without available quantities for sale:', array(), 'Admin.Catalog.Feature'); + + return $this->renderList(); + } + + public function getCustomListProductsNoStock() + { + if (!Configuration::get('PS_STOCK_MANAGEMENT')) { + return; + } + + $this->table = 'product'; + $this->list_id = 'no_stock_products'; + $this->className = 'Product'; + $this->lang = true; + $this->identifier = 'id_product'; + $this->_orderBy = 'id_product'; + $this->_orderWay = 'DESC'; + $this->show_toolbar = false; + $this->_list_index = 'index.php?controller=AdminProducts'; + $this->_list_token = Tools::getAdminTokenLite('AdminProducts'); + + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + $this->fields_list = array( + 'id_product' => array('title' => $this->trans('ID', array(), 'Admin.Global'), 'class' => 'fixed-width-xs', 'align' => 'center'), + 'reference' => array('title' => $this->trans('Reference', array(), 'Admin.Global')), + 'name' => array('title' => $this->trans('Name', array(), 'Admin.Global')), + 'active' => array('title' => $this->trans('Status', array(), 'Admin.Global'), 'type' => 'bool', 'active' => 'status', 'align' => 'center', 'class' => 'fixed-width-xs', 'filter_key' => 'a!active'), + ); + $this->clearFilters(); + + $this->_join = Shop::addSqlAssociation('product', 'a'); + $this->_filter = 'AND EXISTS ( + SELECT 1 + FROM `' . _DB_PREFIX_ . 'product` p + ' . Product::sqlStock('p') . ' + WHERE a.id_product = p.id_product AND NOT EXISTS ( + SELECT 1 + FROM `' . _DB_PREFIX_ . 'product_attribute` pa WHERE pa.id_product = p.id_product + ) + AND IFNULL(stock.quantity, 0) <= 0 + )'; + + $this->toolbar_title = $this->trans('List of products without combinations and without available quantities for sale:', array(), 'Admin.Catalog.Feature'); + + return $this->renderList(); + } + + public function getCustomListProductsDisabled() + { + $this->table = 'product'; + $this->list_id = 'disabled_products'; + $this->className = 'Product'; + $this->lang = true; + $this->identifier = 'id_product'; + $this->_orderBy = 'id_product'; + $this->_orderWay = 'DESC'; + $this->_filter = 'AND product_shop.`active` = 0'; + $this->show_toolbar = false; + $this->_list_index = 'index.php?controller=AdminProducts'; + $this->_list_token = Tools::getAdminTokenLite('AdminProducts'); + + $this->addRowAction('edit'); + $this->addRowAction('delete'); + + $this->fields_list = array( + 'id_product' => array('title' => $this->trans('ID', array(), 'Admin.Global'), 'class' => 'fixed-width-xs', 'align' => 'center'), + 'reference' => array('title' => $this->trans('Reference', array(), 'Admin.Global')), + 'name' => array('title' => $this->trans('Name', array(), 'Admin.Global'), 'filter_key' => 'b!name'), + ); + + $this->clearFilters(); + + $this->_join = Shop::addSqlAssociation('product', 'a'); + $this->toolbar_title = $this->trans('List of disabled products', array(), 'Admin.Catalog.Feature'); + + return $this->renderList(); + } + + public function getCustomListProductsWithoutPhoto() + { + $this->table = 'product'; + $this->list_id = 'products_without_photo'; + $this->lang = true; + $this->identifier = 'id_product'; + $this->_orderBy = 'id_product'; + $this->_orderWay = 'DESC'; + $this->className = 'Product'; + $this->_list_index = 'index.php?controller=AdminProducts'; + $this->_list_token = Tools::getAdminTokenLite('AdminProducts'); + $this->show_toolbar = false; + $this->addRowAction('edit'); + $this->addRowAction('delete'); + $this->fields_list = array( + 'id_product' => array('title' => $this->trans('ID', array(), 'Admin.Global'), 'class' => 'fixed-width-xs', 'align' => 'center'), + 'reference' => array('title' => $this->trans('Reference', array(), 'Admin.Global')), + 'name' => array('title' => $this->trans('Name', array(), 'Admin.Global'), 'filter_key' => 'b!name'), + 'active' => array('title' => $this->trans('Status', array(), 'Admin.Global'), 'type' => 'bool', 'active' => 'status', 'align' => 'center', 'class' => 'fixed-width-xs'), + ); + $this->clearFilters(); + $this->_join = Shop::addSqlAssociation('product', 'a'); + $this->_filter = 'AND NOT EXISTS ( + SELECT 1 + FROM `' . _DB_PREFIX_ . 'image` img + WHERE a.id_product = img.id_product + )'; + $this->toolbar_title = $this->trans('List of products without images', array(), 'Admin.Catalog.Feature'); + + return $this->renderList(true); + } + + public function getCustomListProductsWithoutDescription() + { + $this->table = 'product'; + $this->list_id = 'products_without_description'; + $this->lang = true; + $this->identifier = 'id_product'; + $this->_orderBy = 'id_product'; + $this->_orderWay = 'DESC'; + $this->className = 'Product'; + $this->_list_index = 'index.php?controller=AdminProducts'; + $this->_list_token = Tools::getAdminTokenLite('AdminProducts'); + $this->show_toolbar = false; + $this->addRowAction('edit'); + $this->addRowAction('delete'); + $this->fields_list = array( + 'id_product' => array('title' => $this->trans('ID', array(), 'Admin.Global'), 'class' => 'fixed-width-xs', 'align' => 'center'), + 'reference' => array('title' => $this->trans('Reference', array(), 'Admin.Global')), + 'name' => array('title' => $this->trans('Name', array(), 'Admin.Global'), 'filter_key' => 'b!name'), + 'active' => array('title' => $this->trans('Status', array(), 'Admin.Global'), 'type' => 'bool', 'active' => 'status', 'align' => 'center', 'class' => 'fixed-width-xs'), + ); + $this->clearFilters(); + $defaultLanguage = new Language(Configuration::get('PS_LANG_DEFAULT')); + $this->_join = Shop::addSqlAssociation('product', 'a'); + $this->_filter = 'AND EXISTS ( + SELECT 1 + FROM `' . _DB_PREFIX_ . 'product_lang` pl + WHERE + a.id_product = pl.id_product AND + pl.id_lang = ' . (int) $defaultLanguage->id . ' AND + pl.id_shop = ' . (int) $this->context->shop->id . ' AND + description = "" AND description_short = "" + )'; + $this->toolbar_title = $this->trans('List of products without description', array(), 'Admin.Catalog.Feature'); + + return $this->renderList(true); + } + + public function getCustomListProductsWithoutPrice() + { + $this->table = 'product'; + $this->list_id = 'products_without_price'; + $this->lang = true; + $this->identifier = 'id_product'; + $this->_orderBy = 'id_product'; + $this->_orderWay = 'DESC'; + $this->className = 'Product'; + $this->_list_index = 'index.php?controller=AdminProducts'; + $this->_list_token = Tools::getAdminTokenLite('AdminProducts'); + $this->show_toolbar = false; + $this->addRowAction('edit'); + $this->addRowAction('delete'); + $this->fields_list = array( + 'id_product' => array('title' => $this->trans('ID', array(), 'Admin.Global'), 'class' => 'fixed-width-xs', 'align' => 'center'), + 'reference' => array('title' => $this->trans('Reference', array(), 'Admin.Global')), + 'name' => array('title' => $this->trans('Name', array(), 'Admin.Global'), 'filter_key' => 'b!name'), + 'active' => array('title' => $this->trans('Status', array(), 'Admin.Global'), 'type' => 'bool', 'active' => 'status', 'align' => 'center', 'class' => 'fixed-width-xs', 'ajax' => true), + ); + $this->clearFilters(); + $this->_join = Shop::addSqlAssociation('product', 'a'); + $this->_filter = ' AND a.price = "0.000000" AND a.wholesale_price = "0.000000" AND NOT EXISTS ( + SELECT 1 + FROM `' . _DB_PREFIX_ . 'specific_price` sp + WHERE a.id_product = sp.id_product + )'; + $this->toolbar_title = $this->trans('List of products without price', array(), 'Admin.Catalog.Feature'); + + return $this->renderList(); + } + + public function renderList($withPagination = false) + { + $paginationLimit = 20; + $this->processFilter(); + + if (!($this->fields_list && is_array($this->fields_list))) { + return false; + } + $this->getList($this->context->language->id, null, null, 0, $withPagination ? $paginationLimit : null); + + $helper = new HelperList(); + + // Empty list is ok + if (!is_array($this->_list)) { + $this->displayWarning($this->trans('Bad SQL query', array(), 'Admin.Notifications.Error') . '' . htmlspecialchars($this->_list_error)); + + return false; + } + + $this->setHelperDisplay($helper); + if ($withPagination) { + $helper->_default_pagination = $paginationLimit; + $helper->_pagination = $this->_pagination; + } + $helper->tpl_vars = $this->tpl_list_vars; + $helper->tpl_delete_link_vars = $this->tpl_delete_link_vars; + + // For compatibility reasons, we have to check standard actions in class attributes + foreach ($this->actions_available as $action) { + if (!in_array($action, $this->actions) && isset($this->$action) && $this->$action) { + $this->actions[] = $action; + } + } + $helper->is_cms = $this->is_cms; + $list = $helper->generateList($this->_list, $this->fields_list); + + return $list; + } + + public function displayEnableLink($token, $id, $value, $active, $id_category = null, $id_product = null) + { + $this->_helper_list->currentIndex = $this->_list_index; + $this->_helper_list->identifier = $this->identifier; + $this->_helper_list->table = $this->table; + + // Since Categories controller is migrated to Symfony + // it makes use of new endpoint instead of relaying on legacy controller + if ($this->list_id === 'empty_categories') { + $url = SymfonyContainer::getInstance()->get('router')->generate('admin_categories_toggle_status', [ + 'categoryId' => $id, + ]); + $this->context->smarty->assign('migrated_url_enable', $url); + + $html = $this->_helper_list->displayEnableLink( + $this->_list_token, + $id, + $value, + $active, + $id_category, + $id_product, + true + ); + + $this->context->smarty->clearAssign('migrated_url_enable'); + + return $html; + } + + return $this->_helper_list->displayEnableLink($this->_list_token, $id, $value, $active, $id_category, $id_product); + } + + public function displayDeleteLink($token, $id, $name = null) + { + $this->_helper_list->currentIndex = $this->_list_index; + $this->_helper_list->identifier = $this->identifier; + $this->_helper_list->table = $this->table; + + return $this->_helper_list->displayDeleteLink($this->_list_token, $id, $name); + } + + public function displayEditLink($token, $id, $name = null) + { + $this->_helper_list->currentIndex = $this->_list_index; + $this->_helper_list->identifier = $this->identifier; + $this->_helper_list->table = $this->table; + + return $this->_helper_list->displayEditLink($this->_list_token, $id, $name); + } + + protected function clearFilters() + { + if (Tools::isSubmit('submitResetempty_categories')) { + $this->processResetFilters('empty_categories'); + } + + if (Tools::isSubmit('submitResetno_stock_products_attributes')) { + $this->processResetFilters('no_stock_products_attributes'); + } + + if (Tools::isSubmit('submitResetno_stock_products')) { + $this->processResetFilters('no_stock_products'); + } + + if (Tools::isSubmit('submitResetdisabled_products')) { + $this->processResetFilters('disabled_products'); + } + + if (Tools::isSubmit('submitResetproducts_without_photo')) { + $this->processResetFilters('products_without_photo'); + } + if (Tools::isSubmit('submitResetproducts_without_description')) { + $this->processResetFilters('products_without_description'); + } + if (Tools::isSubmit('submitResetproducts_without_price')) { + $this->processResetFilters('products_without_price'); + } + } + + public function clearListOptions() + { + $this->table = ''; + $this->actions = array(); + $this->list_skip_actions = array(); + $this->lang = false; + $this->identifier = ''; + $this->_orderBy = ''; + $this->_orderWay = ''; + $this->_filter = ''; + $this->_group = ''; + $this->_where = ''; + $this->list_title = $this->trans('Product disabled', array(), 'Admin.Catalog.Feature'); + } + + public function getList($id_lang, $order_by = null, $order_way = null, $start = 0, $limit = null, $id_lang_shop = false) + { + parent::getList($id_lang, $order_by, $order_way, $start, $limit, Context::getContext()->shop->id); + } + + public static function getDescriptionClean($description) + { + return Tools::getDescriptionClean($description); + } +} diff --git a/controllers/admin/AdminTranslationsController.php b/controllers/admin/AdminTranslationsController.php new file mode 100644 index 00000000..cf52fcc1 --- /dev/null +++ b/controllers/admin/AdminTranslationsController.php @@ -0,0 +1,3320 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ +use PrestaShop\PrestaShop\Core\Addon\Theme\Theme; +use PrestaShop\PrestaShop\Core\Addon\Theme\ThemeManagerBuilder; +use PrestaShop\PrestaShop\Core\Foundation\Filesystem\FileSystem; + +class AdminTranslationsControllerCore extends AdminController +{ + /** Name of theme by default */ + const DEFAULT_THEME_NAME = _PS_DEFAULT_THEME_NAME_; + const TEXTAREA_SIZED = 70; + + /** @var string : Link which list all pack of language */ + protected $link_lang_pack = 'http://i18n.prestashop.com/translations/%ps_version%/available_languages.json'; + + /** @var int : number of sentence which can be translated */ + protected $total_expression = 0; + + /** @var int : number of sentence which aren't translated */ + protected $missing_translations = 0; + + /** @var array : List of ISO code for all languages */ + protected $all_iso_lang = []; + + /** @var array */ + protected $modules_translations = []; + + /** @var array : List of folder which must be ignored */ + protected static $ignore_folder = ['.', '..', '.svn', '.git', '.htaccess', 'index.php']; + + /** @var array : List of content type accepted for translation mail file */ + protected static $content_type_accepted = ['txt', 'tpl', 'html']; + + /** @var array : List of theme by translation type : FRONT, BACK, ERRORS... */ + protected $translations_informations = []; + + /** @var array : List of all languages */ + protected $languages; + + /** @var array : List of all themes */ + protected $themes; + + /** @var string : Directory of selected theme */ + protected $theme_selected; + + /** @var string : Name of translations type */ + protected $type_selected; + + /** @var Language object : Language for the selected language */ + protected $lang_selected; + + /** @var bool : Is true if number of var exceed the suhosin request or post limit */ + protected $post_limit_exceed = false; + + public function __construct() + { + $this->bootstrap = true; + $this->multishop_context = Shop::CONTEXT_ALL; + $this->table = 'translations'; + + parent::__construct(); + + $this->link_lang_pack = str_replace('%ps_version%', _PS_VERSION_, $this->link_lang_pack); + + $this->themes = (new ThemeManagerBuilder($this->context, Db::getInstance())) + ->buildRepository() + ->getList(); + } + + /* + * Set the type which is selected + */ + public function setTypeSelected($type_selected) + { + $this->type_selected = $type_selected; + } + + /** + * AdminController::initContent() override. + * + * @see AdminController::initContent() + */ + public function initContent() + { + if (null !== $this->type_selected) { + $method_name = 'initForm' . $this->type_selected; + if (method_exists($this, $method_name)) { + $this->content = $this->initForm($method_name); + } else { + $this->errors[] = $this->trans('"%type%" does not exist.', ['%type%' => $this->type_selected], 'Admin.Notifications.Error'); + $this->content = $this->initMain(); + } + } else { + $this->content = $this->initMain(); + } + + $this->context->smarty->assign([ + 'content' => $this->content, + ]); + } + + /** + * This function create vars by default and call the good method for generate form. + * + * @param $method_name + * + * @return mixed Call the method $this->method_name() + */ + public function initForm($method_name) + { + // Create a title for each translation page + $title = $this->trans( + '%1$s (Language: %2$s, Theme: %3$s)', + [ + '%1$s' => (empty($this->translations_informations[$this->type_selected]['name']) ? false : $this->translations_informations[$this->type_selected]['name']), + '%2$s' => $this->lang_selected->name, + '%3$s' => $this->theme_selected ? $this->theme_selected : $this->trans('None', [], 'Admin.Global'), + ], + 'Admin.International.Feature' + ); + + // Set vars for all forms + $this->tpl_view_vars = [ + 'lang' => $this->lang_selected->iso_code, + 'title' => $title, + 'type' => $this->type_selected, + 'theme' => $this->theme_selected, + 'post_limit_exceeded' => $this->post_limit_exceed, + 'url_submit' => self::$currentIndex . '&submitTranslations' . ucfirst($this->type_selected) . '=1&token=' . $this->token, + 'url_submit_installed_module' => self::$currentIndex . '&submitSelect' . ucfirst($this->type_selected) . '=1&token=' . $this->token, + 'toggle_button' => $this->displayToggleButton(), + 'textarea_sized' => self::TEXTAREA_SIZED, + ]; + + // Call method initForm for a type + return $this->{$method_name}(); + } + + /** + * AdminController::initToolbar() override. + * + * @see AdminController::initToolbar() + */ + public function initToolbar() + { + $this->toolbar_btn['save-and-stay'] = [ + 'short' => 'SaveAndStay', + 'href' => '#', + 'desc' => $this->trans('Save and stay', [], 'Admin.Actions'), + ]; + $this->toolbar_btn['save'] = [ + 'href' => '#', + 'desc' => $this->trans('Update translations', [], 'Admin.International.Feature'), + ]; + $this->toolbar_btn['cancel'] = [ + 'href' => self::$currentIndex . '&token=' . $this->token, + 'desc' => $this->trans('Cancel', [], 'Admin.Actions'), + ]; + } + + /** + * Generate the Main page. + */ + public function initMain() + { + if ( + !in_array( + $this->authorizationLevel(), + [ + AdminController::LEVEL_VIEW, + AdminController::LEVEL_EDIT, + AdminController::LEVEL_ADD, + AdminController::LEVEL_DELETE, + ] + ) + ) { + Tools::redirectAdmin(Context::getContext()->link->getAdminLink('AdminDashboard')); + } + + // Block add/update a language + $packsToInstall = []; + $packsToUpdate = []; + $token = Tools::getAdminToken('AdminLanguages' . (int) Tab::getIdFromClassName('AdminLanguages') . (int) $this->context->employee->id); + $arrayStreamContext = @stream_context_create(['http' => ['method' => 'GET', 'timeout' => 8]]); + + if ($langPacks = Tools::file_get_contents($this->link_lang_pack, false, $arrayStreamContext)) { + if ($langPacks != '' && $langPacks = json_decode($langPacks, true)) { + foreach ($langPacks as $locale => $langName) { + $langDetails = Language::getJsonLanguageDetails($locale); + if (!Language::isInstalledByLocale($locale)) { + $packsToInstall[$locale] = $langDetails['name']; + } else { + $packsToUpdate[$locale] = $langDetails['name']; + } + } + } + } + + $modules = []; + foreach ($this->getListModules(true) as $module) { + $modules[$module->name] = [ + 'name' => $module->name, + 'displayName' => $module->displayName, + 'urlToTranslate' => !$module->isUsingNewTranslationSystem() ? $this->context->link->getAdminLink( + 'AdminTranslations', + true, + [], + [ + 'type' => 'modules', + 'module' => $module->name, + ] + ) : '', + ]; + } + + $this->tpl_view_vars = [ + 'theme_default' => self::DEFAULT_THEME_NAME, + 'theme_lang_dir' => _THEME_LANG_DIR_, + 'token' => $this->token, + 'languages' => $this->languages, + 'translations_type' => $this->translations_informations, + 'packs_to_install' => $packsToInstall, + 'packs_to_update' => $packsToUpdate, + 'url_submit' => self::$currentIndex . '&token=' . $this->token, + 'themes' => $this->themes, + 'modules' => $modules, + 'current_theme_name' => $this->context->shop->theme_name, + 'url_create_language' => 'index.php?controller=AdminLanguages&addlang&token=' . $token, + 'level' => $this->authorizationLevel(), + ]; + + $this->toolbar_scroll = false; + + $this->content .= $this->renderKpis(); + $this->content .= parent::renderView(); + + return $this->content; + } + + /** + * This method merge each arrays of modules translation in the array of modules translations. + */ + protected function getModuleTranslations() + { + global $_MODULE; + $name_var = (empty($this->translations_informations[$this->type_selected]['var']) ? false : $this->translations_informations[$this->type_selected]['var']); + + if (!isset($_MODULE) && !isset($GLOBALS[$name_var])) { + $GLOBALS[$name_var] = []; + } elseif (isset($_MODULE)) { + if (is_array($GLOBALS[$name_var]) && is_array($_MODULE)) { + $GLOBALS[$name_var] = array_merge($GLOBALS[$name_var], $_MODULE); + } else { + $GLOBALS[$name_var] = $_MODULE; + } + } + } + + /** + * This method is only used by AdminTranslations::submitCopyLang(). + * + * It try to create folder in new theme. + * + * When a translation file is copied for a module, its translation key is wrong. + * We have to change the translation key and rewrite the file. + * + * @param string $dest file name + * + * @return bool + */ + protected function checkDirAndCreate($dest) + { + $bool = true; + + // To get only folder path + $path = dirname($dest); + + // If folder wasn't already added + // Do not use Tools::file_exists_cache because it changes over time! + if (!file_exists($path)) { + if (!mkdir($path, FileSystem::DEFAULT_MODE_FOLDER, true)) { + $bool &= false; + $this->errors[] = $this->trans('Cannot create the folder "%folder%". Please check your directory writing permissions.', ['%folder%' => $path], 'Admin.International.Notification'); + } + } + + return $bool; + } + + /** + * Read the Post var and write the translation file. + * This method overwrites the old translation file. + * + * @param bool $override_file Set true if this file is a override + * + * @throws PrestaShopException + */ + protected function writeTranslationFile($override_file = false) + { + $type = Tools::toCamelCase($this->type_selected, true); + + if (isset($this->translations_informations[$this->type_selected])) { + $translation_informations = $this->translations_informations[$this->type_selected]; + } else { + return; + } + + if ($override_file) { + $file_path = $translation_informations['override']['dir'] . $translation_informations['override']['file']; + } else { + $file_path = $translation_informations['dir'] . $translation_informations['file']; + } + + if ($file_path && !file_exists($file_path)) { + if (!file_exists(dirname($file_path)) && !mkdir(dirname($file_path), FileSystem::DEFAULT_MODE_FOLDER, true)) { + throw new PrestaShopException($this->trans('Directory "%folder%" cannot be created', ['%folder%' => dirname($file_path)], 'Admin.Notifications.Error')); + } elseif (!touch($file_path)) { + throw new PrestaShopException($this->trans('File "%file%" cannot be created', ['%file%' => $file_path], 'Admin.Notifications.Error')); + } + } + + $thm_name = str_replace('.', '', Tools::getValue('theme')); + $kpi_key = substr(strtoupper($thm_name . '_' . Tools::getValue('lang')), 0, 16); + + if ($fd = fopen($file_path, 'wb')) { + // Get value of button save and stay + $save_and_stay = Tools::isSubmit('submitTranslations' . $type . 'AndStay'); + + // Unset all POST which are not translations + unset( + $_POST['submitTranslations' . $type], + $_POST['submitTranslations' . $type . 'AndStay'], + $_POST['lang'], + $_POST['token'], + $_POST['theme'], + $_POST['type'] + ); + + // Get all POST which aren't empty + $to_insert = []; + foreach ($_POST as $key => $value) { + if (!empty($value)) { + $to_insert[$key] = $value; + } + } + + ConfigurationKPI::updateValue('FRONTOFFICE_TRANSLATIONS_EXPIRE', time()); + ConfigurationKPI::updateValue('TRANSLATE_TOTAL_' . $kpi_key, count($_POST)); + ConfigurationKPI::updateValue('TRANSLATE_DONE_' . $kpi_key, count($to_insert)); + + // translations array is ordered by key (easy merge) + ksort($to_insert); + $tab = $translation_informations['var']; + fwrite($fd, " $value) { + fwrite($fd, '$' . $tab . '[\'' . pSQL($key, true) . '\'] = \'' . pSQL($value, true) . '\';' . "\n"); + } + fwrite($fd, "\n?>"); + fclose($fd); + + // Redirect + if ($save_and_stay) { + $this->redirect(true); + } else { + $this->redirect(); + } + } else { + throw new PrestaShopException($this->trans('Cannot write this file: "%folder%"', ['%folder%' => $file_path], 'Admin.Notifications.Error')); + } + } + + public function submitCopyLang() + { + if (!($from_lang = Tools::getValue('fromLang')) || !($to_lang = Tools::getValue('toLang'))) { + $this->errors[] = $this->trans('You must select two languages in order to copy data from one to another.', [], 'Admin.International.Notification'); + } elseif (!($from_theme = Tools::getValue('fromTheme')) || !($to_theme = Tools::getValue('toTheme'))) { + $this->errors[] = $this->trans('You must select two themes in order to copy data from one to another.', [], 'Admin.International.Notification'); + } elseif (!Language::copyLanguageData(Language::getIdByIso($from_lang), Language::getIdByIso($to_lang))) { + $this->errors[] = $this->trans('An error occurred while copying data.', [], 'Admin.International.Notification'); + } elseif ($from_lang == $to_lang && $from_theme == $to_theme) { + $this->errors[] = $this->trans('There is nothing to copy (same language and theme).', [], 'Admin.International.Notification'); + } else { + $theme_exists = ['from_theme' => false, 'to_theme' => false]; + foreach ($this->themes as $theme) { + if ($theme->getName() == $from_theme) { + $theme_exists['from_theme'] = true; + } + if ($theme->getName() == $to_theme) { + $theme_exists['to_theme'] = true; + } + } + if ($theme_exists['from_theme'] == false || $theme_exists['to_theme'] == false) { + $this->errors[] = $this->trans('Theme(s) not found', [], 'Admin.International.Notification'); + } + } + if (count($this->errors)) { + return; + } + + $bool = true; + $items = Language::getFilesList($from_lang, $from_theme, $to_lang, $to_theme, false, false, true); + foreach ($items as $source => $dest) { + if (!$this->checkDirAndCreate($dest)) { + $this->errors[] = $this->trans('Impossible to create the directory "%folder%".', ['%folder%' => $dest], 'Admin.International.Notification'); + } elseif (!copy($source, $dest)) { + $this->errors[] = $this->trans('Impossible to copy "%source%" to "%dest%".', ['%source%' => $source, '%dest%' => $dest], 'Admin.International.Notification'); + } elseif (strpos($dest, 'modules') && basename($source) === $from_lang . '.php' && $bool !== false) { + if (!$this->changeModulesKeyTranslation($dest, $from_theme, $to_theme)) { + $this->errors[] = $this->trans('Impossible to translate "%dest%".', ['%dest%' => $dest], 'Admin.International.Notification'); + } + } + } + if (!count($this->errors)) { + $this->redirect(false, 14); + } + $this->errors[] = $this->trans('A part of the data has been copied but some of the language files could not be found.', [], 'Admin.International.Notification'); + } + + /** + * Change the key translation to according it to theme name. + * + * @param string $path + * @param string $theme_from + * @param string $theme_to + * + * @return bool + */ + public function changeModulesKeyTranslation($path, $theme_from, $theme_to) + { + $content = file_get_contents($path); + $arr_replace = []; + $bool_flag = true; + if (preg_match_all('#\$_MODULE\[\'([^\']+)\'\]#Ui', $content, $matches)) { + foreach ($matches[1] as $value) { + $arr_replace[$value] = str_replace($theme_from, $theme_to, $value); + } + $content = str_replace(array_keys($arr_replace), array_values($arr_replace), $content); + $bool_flag = (file_put_contents($path, $content) === false) ? false : true; + } + + return $bool_flag; + } + + public function exportTabs() + { + // Get name tabs by iso code + $tabs = Tab::getTabs($this->lang_selected->id); + + // Get name of the default tabs + $tabs_default_lang = Tab::getTabs(1); + + $tabs_default = []; + foreach ($tabs_default_lang as $tab) { + $tabs_default[$tab['class_name']] = pSQL($tab['name']); + } + + // Create content + $content = " tabs are by default in Spanish + * 2) create a new language, say, Klingon => tabs are populated using the default, Spanish, tabs + * 3) export the Klingon language pack + * + * => Since you have not yet translated the tabs into Klingon, + * without the condition below, you would get tabs exported, but in Spanish. + * This would lead to a Klingon pack actually containing Spanish. + * + * This has caused many issues in the past, so, as a precaution, tabs from + * the default language are not exported. + * + */ + if ($tabs_default[$tab['class_name']] != pSQL($tab['name'])) { + $content .= "\n\$_TABS['" . $tab['class_name'] . "'] = '" . pSQL($tab['name']) . "';"; + } + } + } + $content .= "\n\nreturn \$_TABS;"; + + $dir = _PS_TRANSLATIONS_DIR_ . $this->lang_selected->iso_code . DIRECTORY_SEPARATOR; + $path = $dir . 'tabs.php'; + + // Check if tabs.php exists for the selected Iso Code + if (!Tools::file_exists_cache($dir)) { + if (!mkdir($dir, FileSystem::DEFAULT_MODE_FOLDER, true)) { + throw new PrestaShopException('The file ' . $dir . ' cannot be created.'); + } + } + if (!file_put_contents($path, $content)) { + throw new PrestaShopException('File "' . $path . '" does not exist and cannot be created in ' . $dir); + } + if (!is_writable($path)) { + $this->displayWarning($this->trans('This file must be writable: %file%', ['%file%' => $path], 'Admin.Notifications.Error')); + } + } + + public function submitExportLang() + { + if ($this->lang_selected->iso_code && $this->theme_selected) { + $this->exportTabs(); + $items = array_flip(Language::getFilesList($this->lang_selected->iso_code, $this->theme_selected, false, false, false, false, true)); + $file_name = _PS_TRANSLATIONS_DIR_ . '/export/' . $this->lang_selected->iso_code . '.gzip'; + $gz = new Archive_Tar($file_name, true); + if ($gz->createModify($items, null, _PS_ROOT_DIR_)) { + ob_start(); + header('Pragma: public'); + header('Expires: 0'); + header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); + header('Cache-Control: public'); + header('Content-Description: File Transfer'); + header('Content-type: application/octet-stream'); + header('Content-Disposition: attachment; filename="' . $this->lang_selected->iso_code . '.gzip' . '"'); + header('Content-Transfer-Encoding: binary'); + ob_end_flush(); + readfile($file_name); + @unlink($file_name); + exit; + } + $this->errors[] = $this->trans('An error occurred while creating archive.', [], 'Admin.International.Notification'); + } + $this->errors[] = $this->trans('Please select a language and a theme.', [], 'Admin.International.Notification'); + } + + public static function checkAndAddMailsFiles($iso_code, $files_list) + { + if (Language::getIdByIso('en')) { + $default_language = 'en'; + } else { + $default_language = Language::getIsoById((int) Configuration::get('PS_LANG_DEFAULT')); + } + + if (!$default_language || !Validate::isLanguageIsoCode($default_language)) { + return false; + } + + // 1 - Scan mails files + $mails = []; + if (Tools::file_exists_cache(_PS_MAIL_DIR_ . $default_language . '/')) { + $mails = scandir(_PS_MAIL_DIR_ . $default_language . '/', SCANDIR_SORT_NONE); + } + + $mails_new_lang = []; + + // Get all email files + foreach ($files_list as $file) { + if (preg_match('#^mails\/([a-z0-9]+)\/#Ui', $file['filename'], $matches)) { + $slash_pos = strrpos($file['filename'], '/'); + $mails_new_lang[] = substr($file['filename'], -(strlen($file['filename']) - $slash_pos - 1)); + } + } + + // Get the difference + $arr_mails_needed = array_diff($mails, $mails_new_lang); + + // Add mails files + foreach ($arr_mails_needed as $mail_to_add) { + if (!in_array($mail_to_add, self::$ignore_folder)) { + @copy(_PS_MAIL_DIR_ . $default_language . '/' . $mail_to_add, _PS_MAIL_DIR_ . $iso_code . '/' . $mail_to_add); + } + } + + // 2 - Scan modules files + $modules = scandir(_PS_MODULE_DIR_, SCANDIR_SORT_NONE); + + $module_mail_en = []; + $module_mail_iso_code = []; + + foreach ($modules as $module) { + if (!in_array($module, self::$ignore_folder) && Tools::file_exists_cache(_PS_MODULE_DIR_ . $module . '/mails/' . $default_language . '/')) { + $arr_files = scandir(_PS_MODULE_DIR_ . $module . '/mails/' . $default_language . '/', SCANDIR_SORT_NONE); + + foreach ($arr_files as $file) { + if (!in_array($file, self::$ignore_folder)) { + if (Tools::file_exists_cache(_PS_MODULE_DIR_ . $module . '/mails/' . $default_language . '/' . $file)) { + $module_mail_en[] = _PS_MODULE_DIR_ . $module . '/mails/ISO_CODE/' . $file; + } + + if (Tools::file_exists_cache(_PS_MODULE_DIR_ . $module . '/mails/' . $iso_code . '/' . $file)) { + $module_mail_iso_code[] = _PS_MODULE_DIR_ . $module . '/mails/ISO_CODE/' . $file; + } + } + } + } + } + + // Get the difference in this modules + $arr_modules_mails_needed = array_diff($module_mail_en, $module_mail_iso_code); + + // Add mails files for this modules + foreach ($arr_modules_mails_needed as $file) { + $file_en = str_replace('ISO_CODE', $default_language, $file); + $file_iso_code = str_replace('ISO_CODE', $iso_code, $file); + $dir_iso_code = substr($file_iso_code, 0, -(strlen($file_iso_code) - strrpos($file_iso_code, '/') - 1)); + + if (!file_exists($dir_iso_code)) { + mkdir($dir_iso_code); + file_put_contents($dir_iso_code . '/index.php', Tools::getDefaultIndexContent()); + } + + if (Tools::file_exists_cache($file_en)) { + copy($file_en, $file_iso_code); + } + } + } + + /** + * Move theme translations in selected themes. + * + * @param array $files + * @param array $themes_selected + */ + public function checkAndAddThemesFiles($files, $themes_selected) + { + foreach ($files as $file) { + // Check if file is a file theme + if (preg_match('#^themes\/([a-z0-9]+)\/lang\/#Ui', $file['filename'], $matches)) { + $slash_pos = strrpos($file['filename'], '/'); + $name_file = substr($file['filename'], -(strlen($file['filename']) - $slash_pos - 1)); + $name_default_theme = $matches[1]; + $deleted_old_theme = false; + + // Get the old file theme + if (file_exists(_PS_THEME_DIR_ . 'lang/' . $name_file)) { + $theme_file_old = _PS_THEME_DIR_ . 'lang/' . $name_file; + } else { + $deleted_old_theme = true; + $theme_file_old = str_replace(self::DEFAULT_THEME_NAME, $name_default_theme, _PS_THEME_DIR_ . 'lang/' . $name_file); + } + + // Move the old file theme in the new folder + foreach ($themes_selected as $theme_name) { + if (file_exists($theme_file_old)) { + copy($theme_file_old, str_replace($name_default_theme, $theme_name, $theme_file_old)); + } + } + + if ($deleted_old_theme) { + @unlink($theme_file_old); + } + } + } + } + + /** + * Add new translations tabs by code ISO. + * + * @param array $iso_code + * @param array $files + * + * @return array + */ + public static function addNewTabs($iso_code, $files) + { + $errors = []; + + foreach ($files as $file) { + // Check if file is a file theme + if (preg_match('#^translations\/' . $iso_code . '\/tabs.php#Ui', $file['filename'], $matches) && Validate::isLanguageIsoCode($iso_code)) { + // Include array width new translations tabs + $_TABS = []; + clearstatcache(); + if (file_exists(_PS_ROOT_DIR_ . DIRECTORY_SEPARATOR . $file['filename'])) { + include_once _PS_ROOT_DIR_ . DIRECTORY_SEPARATOR . $file['filename']; + } + + if (is_array($_TABS) && count($_TABS)) { + foreach ($_TABS as $class_name => $translations) { + // Get instance of this tab by class name + $tab = Tab::getInstanceFromClassName($class_name); + //Check if class name exists + if (isset($tab->class_name) && !empty($tab->class_name)) { + $id_lang = Language::getIdByIso($iso_code, true); + $tab->name[(int) $id_lang] = $translations; + + // Do not crash at intall + if (!isset($tab->name[Configuration::get('PS_LANG_DEFAULT')])) { + $tab->name[(int) Configuration::get('PS_LANG_DEFAULT')] = $translations; + } + + if (!Validate::isGenericName($tab->name[(int) $id_lang])) { + $errors[] = Context::getContext()->getTranslator()->trans('Tab "%s" is not valid', [$tab->name[(int) $id_lang]], 'Admin.International.Notification'); + } else { + $tab->update(); + } + } + } + } + } + } + + return $errors; + } + + public static function checkTranslationFile($content) + { + $lines = array_map('trim', explode("\n", $content)); + $global = false; + foreach ($lines as $line) { + // PHP tags + if (in_array($line, ['', ''])) { + continue; + } + + // Global variable declaration + if (!$global && preg_match('/^global\s+\$([a-z0-9-_]+)\s*;$/i', $line, $matches)) { + $global = $matches[1]; + + continue; + } + // Global variable initialization + if ($global != false && preg_match('/^\$' . preg_quote($global, '/') . '\s*=\s*array\(\s*\)\s*;$/i', $line)) { + continue; + } + + // Global variable initialization without declaration + if (!$global && preg_match('/^\$([a-z0-9-_]+)\s*=\s*array\(\s*\)\s*;$/i', $line, $matches)) { + $global = $matches[1]; + + continue; + } + + // Assignation + if (preg_match('/^\$' . preg_quote($global, '/') . '\[\'' . _PS_TRANS_PATTERN_ . '\'\]\s*=\s*\'' . _PS_TRANS_PATTERN_ . '\'\s*;$/i', $line)) { + continue; + } + + // Sometimes the global variable is returned... + if (preg_match('/^return\s+\$' . preg_quote($global, '/') . '\s*;$/i', $line, $matches)) { + continue; + } + + return false; + } + + return true; + } + + public function submitImportLang() + { + if (!isset($_FILES['file']['tmp_name']) || !$_FILES['file']['tmp_name']) { + $this->errors[] = $this->trans('No file has been selected.', [], 'Admin.Notifications.Error'); + } else { + $gz = new Archive_Tar($_FILES['file']['tmp_name'], true); + $filename = $_FILES['file']['name']; + $iso_code = str_replace(['.tar.gz', '.gzip'], '', $filename); + + if (Validate::isLangIsoCode($iso_code)) { + $themes_selected = Tools::getValue('theme', [self::DEFAULT_THEME_NAME]); + $files_list = AdminTranslationsController::filterTranslationFiles($gz->listContent()); + $files_paths = AdminTranslationsController::filesListToPaths($files_list); + + $uniqid = uniqid(); + $sandbox = _PS_CACHE_DIR_ . 'sandbox' . DIRECTORY_SEPARATOR . $uniqid . DIRECTORY_SEPARATOR; + if ($gz->extractList($files_paths, $sandbox)) { + foreach ($files_list as $file2check) { + //don't validate index.php, will be overwrite when extract in translation directory + if (pathinfo($file2check['filename'], PATHINFO_BASENAME) == 'index.php') { + continue; + } + + if (preg_match('@^[0-9a-z-_/\\\\]+\.php$@i', $file2check['filename'])) { + if (!@filemtime($sandbox . $file2check['filename']) || !AdminTranslationsController::checkTranslationFile(file_get_contents($sandbox . $file2check['filename']))) { + $this->errors[] = $this->trans('Validation failed for: %file%', ['%file%' => $file2check['filename']], 'Admin.International.Notification'); + } + } elseif (!preg_match('@mails[0-9a-z-_/\\\\]+\.(html|tpl|txt)$@i', $file2check['filename'])) { + $this->errors[] = $this->trans('Unidentified file found: %file%', ['%file%' => $file2check['filename']], 'Admin.International.Notification'); + } + } + Tools::deleteDirectory($sandbox, true); + } + + $i = 0; + $tmp_array = []; + foreach ($files_paths as $files_path) { + $path = dirname($files_path); + if (is_dir(_PS_TRANSLATIONS_DIR_ . '../' . $path) && !is_writable(_PS_TRANSLATIONS_DIR_ . '../' . $path) && !in_array($path, $tmp_array)) { + $this->errors[] = (!$i++ ? $this->trans('The archive cannot be extracted.', [], 'Admin.International.Notification') . ' ' : '') . $this->trans('The server does not have permissions for writing.', [], 'Admin.Notifications.Error') . ' ' . $this->trans('Please check rights for %file%', ['%file%' => $path], 'Admin.Notifications.Error'); + $tmp_array[] = $path; + } + } + + if (count($this->errors)) { + return false; + } + + if ($error = $gz->extractList($files_paths, _PS_TRANSLATIONS_DIR_ . '../')) { + if (is_object($error) && !empty($error->message)) { + $this->errors[] = $this->trans('The archive cannot be extracted.', [], 'Admin.International.Notification') . ' ' . $error->message; + } else { + foreach ($files_list as $file2check) { + if (pathinfo($file2check['filename'], PATHINFO_BASENAME) == 'index.php' && file_put_contents(_PS_TRANSLATIONS_DIR_ . '../' . $file2check['filename'], Tools::getDefaultIndexContent())) { + continue; + } + } + + // Clear smarty modules cache + Tools::clearCache(); + + if (Validate::isLanguageFileName($filename)) { + if (!Language::checkAndAddLanguage($iso_code)) { + $conf = 20; + } else { + // Reset cache + Language::loadLanguages(); + + AdminTranslationsController::checkAndAddMailsFiles($iso_code, $files_list); + $this->checkAndAddThemesFiles($files_list, $themes_selected); + $tab_errors = AdminTranslationsController::addNewTabs($iso_code, $files_list); + + if (count($tab_errors)) { + $this->errors += $tab_errors; + + return false; + } + } + } + + /* + * @see AdminController::$_conf + */ + $this->redirect(false, (isset($conf) ? $conf : '15')); + } + } + $this->errors[] = $this->trans('The archive cannot be extracted.', [], 'Admin.International.Notification'); + } else { + $this->errors[] = $this->trans('ISO CODE invalid "%iso_code%" for the following file: "%file%"', ['%iso_code%' => $iso_code, '%file%' => $filename], 'Admin.International.Notification'); + } + } + } + + /** + * Filter the translation files contained in a .gzip pack + * and return only the ones that we want. + * + * Right now the function only needs to check that + * the modules for which we want to add translations + * are present on the shop (installed or not). + * + * @param array $list Is the output of Archive_Tar::listContent() + * + * @return array + */ + public static function filterTranslationFiles($list) + { + $kept = []; + foreach ($list as $file) { + if ('index.php' == basename($file['filename'])) { + continue; + } + if (preg_match('#^modules/([^/]+)/#', $file['filename'], $m)) { + if (is_dir(_PS_MODULE_DIR_ . $m[1])) { + $kept[] = $file; + } + } else { + $kept[] = $file; + } + } + + return $kept; + } + + /** + * Turn the list returned by + * AdminTranslationsController::filterTranslationFiles() + * into a list of paths that can be passed to + * Archive_Tar::extractList(). + * + * @param array $list + * + * @return array + */ + public static function filesListToPaths($list) + { + $paths = []; + foreach ($list as $item) { + $paths[] = $item['filename']; + } + + return $paths; + } + + public function submitAddLang() + { + $languageDetails = Language::getJsonLanguageDetails(Tools::getValue('params_import_language')); + $isoCode = $languageDetails['iso_code']; + + if (Validate::isLangIsoCode($isoCode)) { + if ($success = Language::downloadAndInstallLanguagePack($isoCode, _PS_VERSION_, null, true)) { + Language::loadLanguages(); + Tools::clearAllCache(); + + /* + * @see AdminController::$_conf + */ + $this->redirect(false, '15'); + } elseif (is_array($success) && count($success) > 0) { + foreach ($success as $error) { + $this->errors[] = $error; + } + } + } + } + + /** + * This method check each file (tpl or php file), get its sentences to translate, + * compare with posted values and write in iso code translation file. + * + * @param string $file_name + * @param array $files + * @param string $theme_name + * @param string $module_name + * @param string|bool $dir + * + * @throws PrestaShopException + */ + protected function findAndWriteTranslationsIntoFile($file_name, $files, $theme_name, $module_name, $dir = false) + { + // These static vars allow to use file to write just one time. + static $cache_file = []; + static $str_write = ''; + static $array_check_duplicate = []; + + // Set file_name in static var, this allow to open and wright the file just one time + if (!isset($cache_file[$theme_name . '-' . $file_name])) { + $str_write = ''; + $cache_file[$theme_name . '-' . $file_name] = true; + if (!Tools::file_exists_cache(dirname($file_name))) { + mkdir(dirname($file_name), FileSystem::DEFAULT_MODE_FOLDER, true); + } + if (!Tools::file_exists_cache($file_name)) { + file_put_contents($file_name, ''); + } + if (!is_writable($file_name)) { + throw new PrestaShopException($this->trans('Cannot write to the theme\'s language file (%s). Please check writing permissions.', [$file_name], 'Admin.International.Notification')); + } + + // this string is initialized one time for a file + $str_write .= "userParseFile($content, $this->type_selected, $type_file, $module_name); + + // Write each translation on its module file + $template_name = substr(basename($file), 0, -4); + + foreach ($matches as $key) { + if ($theme_name) { + $post_key = md5(strtolower($module_name) . '_' . strtolower($theme_name) . '_' . strtolower($template_name) . '_' . md5($key)); + $pattern = '\'<{' . strtolower($module_name) . '}' . strtolower($theme_name) . '>' . strtolower($template_name) . '_' . md5($key) . '\''; + } else { + $post_key = md5(strtolower($module_name) . '_' . strtolower($template_name) . '_' . md5($key)); + $pattern = '\'<{' . strtolower($module_name) . '}prestashop>' . strtolower($template_name) . '_' . md5($key) . '\''; + } + + if (array_key_exists($post_key, $_POST) && !in_array($pattern, $array_check_duplicate)) { + if ($_POST[$post_key] == '') { + continue; + } + $array_check_duplicate[] = $pattern; + $str_write .= '$_MODULE[' . $pattern . '] = \'' . pSQL(str_replace(["\r\n", "\r", "\n"], ' ', $_POST[$post_key])) . '\';' . "\n"; + ++$this->total_expression; + } + } + } + } + + if (isset($cache_file[$theme_name . '-' . $file_name]) && $str_write != " $file) { + if ($file[0] === '.' || in_array(substr($file, 0, strrpos($file, '.')), $this->all_iso_lang)) { + unset($files[$key]); + } elseif ($type_clear === 'file' && !in_array(substr($file, strrpos($file, '.')), $arr_good_ext)) { + unset($files[$key]); + } elseif ($type_clear === 'directory' && (!is_dir($path . $file) || in_array($file, $arr_exclude))) { + unset($files[$key]); + } + } + + return $files; + } + + /** + * This method get translation for each files of a module, + * compare with global $_MODULES array and fill AdminTranslations::modules_translations array + * With key as English sentences and values as their iso code translations. + * + * @param array $files + * @param string $theme_name + * @param string $module_name + * @param string|bool $dir + */ + protected function findAndFillTranslations($files, $theme_name, $module_name, $dir = false) + { + $name_var = (empty($this->translations_informations[$this->type_selected]['var']) ? false : $this->translations_informations[$this->type_selected]['var']); + + // added for compatibility + $GLOBALS[$name_var] = array_change_key_case($GLOBALS[$name_var]); + + // Thank to this var similar keys are not duplicate + // in AndminTranslation::modules_translations array + // see below + $array_check_duplicate = []; + foreach ($files as $file) { + if ((preg_match('/^(.*).tpl$/', $file) || preg_match('/^(.*).php$/', $file)) && Tools::file_exists_cache($file_path = $dir . $file)) { + // Get content for this file + $content = file_get_contents($file_path); + + // Module files can now be ignored by adding this string in a file + if (strpos($content, 'IGNORE_THIS_FILE_FOR_TRANSLATION') !== false) { + continue; + } + + // Get file type + $type_file = substr($file, -4) == '.tpl' ? 'tpl' : 'php'; + + // Parse this content + $matches = $this->userParseFile($content, $this->type_selected, $type_file, $module_name); + + // Write each translation on its module file + $template_name = substr(basename($file), 0, -4); + + foreach ($matches as $key) { + $md5_key = md5($key); + $module_key = '<{' . Tools::strtolower($module_name) . '}' . strtolower($theme_name) . '>' . Tools::strtolower($template_name) . '_' . $md5_key; + $default_key = '<{' . Tools::strtolower($module_name) . '}prestashop>' . Tools::strtolower($template_name) . '_' . $md5_key; + // to avoid duplicate entry + if (!in_array($module_key, $array_check_duplicate)) { + $array_check_duplicate[] = $module_key; + if (!isset($this->modules_translations[$theme_name][$module_name][$template_name][$key]['trad'])) { + ++$this->total_expression; + } + if ($theme_name && array_key_exists($module_key, $GLOBALS[$name_var])) { + $this->modules_translations[$theme_name][$module_name][$template_name][$key]['trad'] = html_entity_decode($GLOBALS[$name_var][$module_key], ENT_COMPAT, 'UTF-8'); + } elseif (array_key_exists($default_key, $GLOBALS[$name_var])) { + $this->modules_translations[$theme_name][$module_name][$template_name][$key]['trad'] = html_entity_decode($GLOBALS[$name_var][$default_key], ENT_COMPAT, 'UTF-8'); + } else { + $this->modules_translations[$theme_name][$module_name][$template_name][$key]['trad'] = ''; + ++$this->missing_translations; + } + $this->modules_translations[$theme_name][$module_name][$template_name][$key]['use_sprintf'] = $this->checkIfKeyUseSprintf($key); + } + } + } + } + } + + /** + * Get list of files which must be parsed by directory and by type of translations. + * + * @return array : list of files by directory + */ + public function getFileToParseByTypeTranslation() + { + $directories = []; + + switch ($this->type_selected) { + case 'front': + $directories['php'] = [ + _PS_FRONT_CONTROLLER_DIR_ => scandir(_PS_FRONT_CONTROLLER_DIR_, SCANDIR_SORT_NONE), + _PS_OVERRIDE_DIR_ . 'controllers/front/' => scandir(_PS_OVERRIDE_DIR_ . 'controllers/front/', SCANDIR_SORT_NONE), + _PS_CLASS_DIR_ . 'controller/' => ['FrontController.php'], + ]; + + $directories['tpl'] = [_PS_ALL_THEMES_DIR_ => scandir(_PS_ALL_THEMES_DIR_, SCANDIR_SORT_NONE)]; + self::$ignore_folder[] = 'modules'; + $directories['tpl'] = array_merge($directories['tpl'], $this->listFiles(_PS_THEME_SELECTED_DIR_)); + if (isset($directories['tpl'][_PS_THEME_SELECTED_DIR_ . 'pdf/'])) { + unset($directories['tpl'][_PS_THEME_SELECTED_DIR_ . 'pdf/']); + } + + if (Tools::file_exists_cache(_PS_THEME_OVERRIDE_DIR_)) { + $directories['tpl'] = array_merge($directories['tpl'], $this->listFiles(_PS_THEME_OVERRIDE_DIR_)); + } + + break; + + case 'back': + $directories = [ + 'php' => [ + _PS_ADMIN_CONTROLLER_DIR_ => scandir(_PS_ADMIN_CONTROLLER_DIR_, SCANDIR_SORT_NONE), + _PS_OVERRIDE_DIR_ . 'controllers/admin/' => scandir(_PS_OVERRIDE_DIR_ . 'controllers/admin/', SCANDIR_SORT_NONE), + _PS_CLASS_DIR_ . 'helper/' => scandir(_PS_CLASS_DIR_ . 'helper/', SCANDIR_SORT_NONE), + _PS_CLASS_DIR_ . 'controller/' => ['AdminController.php'], + _PS_CLASS_DIR_ => ['PaymentModule.php'], + ], + 'php-sf2' => [ + _PS_ROOT_DIR_ . '/src/' => Tools::scandir(_PS_ROOT_DIR_ . '/src/', 'php', '', true), + ], + 'tpl-sf2' => Tools::scandir(_PS_ROOT_DIR_ . '/src/PrestaShopBundle/Resources/views/', 'twig', '', true), + 'tpl' => $this->listFiles(_PS_ADMIN_DIR_ . DIRECTORY_SEPARATOR . 'themes/'), + 'specific' => [ + _PS_ADMIN_DIR_ . DIRECTORY_SEPARATOR => [], + ], + ]; + + // For translate the template which are overridden + if (file_exists(_PS_OVERRIDE_DIR_ . 'controllers' . DIRECTORY_SEPARATOR . 'admin' . DIRECTORY_SEPARATOR . 'templates')) { + $directories['tpl'] = array_merge($directories['tpl'], $this->listFiles(_PS_OVERRIDE_DIR_ . 'controllers' . DIRECTORY_SEPARATOR . 'admin' . DIRECTORY_SEPARATOR . 'templates')); + } + + break; + + case 'errors': + $directories['php'] = [ + _PS_ROOT_DIR_ => scandir(_PS_ROOT_DIR_, SCANDIR_SORT_NONE), + _PS_ADMIN_DIR_ . DIRECTORY_SEPARATOR => scandir(_PS_ADMIN_DIR_ . DIRECTORY_SEPARATOR, SCANDIR_SORT_NONE), + _PS_FRONT_CONTROLLER_DIR_ => scandir(_PS_FRONT_CONTROLLER_DIR_, SCANDIR_SORT_NONE), + _PS_ADMIN_CONTROLLER_DIR_ => scandir(_PS_ADMIN_CONTROLLER_DIR_, SCANDIR_SORT_NONE), + _PS_OVERRIDE_DIR_ . 'controllers/front/' => scandir(_PS_OVERRIDE_DIR_ . 'controllers/front/', SCANDIR_SORT_NONE), + _PS_OVERRIDE_DIR_ . 'controllers/admin/' => scandir(_PS_OVERRIDE_DIR_ . 'controllers/admin/', SCANDIR_SORT_NONE), + ]; + + // Get all files for folders classes/ and override/classes/ recursively + $directories['php'] = array_merge($directories['php'], $this->listFiles(_PS_CLASS_DIR_, [], 'php')); + $directories['php'] = array_merge($directories['php'], $this->listFiles(_PS_OVERRIDE_DIR_ . 'classes/', [], 'php')); + + break; + + case 'fields': + $directories['php'] = $this->listFiles(_PS_CLASS_DIR_, [], 'php'); + + break; + + case 'pdf': + $tpl_theme = Tools::file_exists_cache(_PS_THEME_SELECTED_DIR_ . 'pdf/') ? scandir(_PS_THEME_SELECTED_DIR_ . 'pdf/', SCANDIR_SORT_NONE) : []; + $directories = [ + 'php' => [ + _PS_CLASS_DIR_ . 'pdf/' => scandir(_PS_CLASS_DIR_ . 'pdf/', SCANDIR_SORT_NONE), + _PS_OVERRIDE_DIR_ . 'classes/pdf/' => scandir(_PS_OVERRIDE_DIR_ . 'classes/pdf/', SCANDIR_SORT_NONE), + ], + 'tpl' => [ + _PS_PDF_DIR_ => scandir(_PS_PDF_DIR_, SCANDIR_SORT_NONE), + _PS_THEME_SELECTED_DIR_ . 'pdf/' => $tpl_theme, + ], + ]; + $directories['tpl'] = array_merge($directories['tpl'], $this->getModulesHasPDF()); + $directories['php'] = array_merge($directories['php'], $this->getModulesHasPDF(true)); + + break; + + case 'mails': + $directories['php'] = [ + _PS_FRONT_CONTROLLER_DIR_ => scandir(_PS_FRONT_CONTROLLER_DIR_, SCANDIR_SORT_NONE), + _PS_ADMIN_CONTROLLER_DIR_ => scandir(_PS_ADMIN_CONTROLLER_DIR_, SCANDIR_SORT_NONE), + _PS_OVERRIDE_DIR_ . 'controllers/front/' => scandir(_PS_OVERRIDE_DIR_ . 'controllers/front/', SCANDIR_SORT_NONE), + _PS_OVERRIDE_DIR_ . 'controllers/admin/' => scandir(_PS_OVERRIDE_DIR_ . 'controllers/admin/', SCANDIR_SORT_NONE), + _PS_ADMIN_DIR_ . DIRECTORY_SEPARATOR => scandir(_PS_ADMIN_DIR_ . DIRECTORY_SEPARATOR, SCANDIR_SORT_NONE), + ]; + + // Get all files for folders classes/ and override/classes/ recursively + $directories['php'] = array_merge($directories['php'], $this->listFiles(_PS_CLASS_DIR_, [], 'php')); + $directories['php'] = array_merge($directories['php'], $this->listFiles(_PS_OVERRIDE_DIR_ . 'classes/', [], 'php')); + $directories['php'] = array_merge($directories['php'], $this->getModulesHasMails()); + + break; + } + + return $directories; + } + + /** + * This method parse a file by type of translation and type file. + * + * @param $content + * @param $type_translation : front, back, errors, modules... + * @param string|bool $type_file : (tpl|php) + * @param string $module_name : name of the module + * + * @return array + */ + protected function userParseFile($content, $type_translation, $type_file = false, $module_name = '') + { + switch ($type_translation) { + case 'front': + // Parsing file in Front office + if ($type_file == 'php') { + $regex = '/this->l\((\')' . _PS_TRANS_PATTERN_ . '\'[\)|\,]/U'; + } else { + $regex = '/\{l\s*s=([\'\"])' . _PS_TRANS_PATTERN_ . '\1(\s*sprintf=.*)?(\s*js=1)?\s*\}/U'; + } + + break; + + case 'back': + // Parsing file in Back office + if ($type_file == 'php') { + $regex = '/this->l\((\')' . _PS_TRANS_PATTERN_ . '\'[\)|\,]/U'; + } elseif ($type_file == 'specific') { + $regex = '/Translate::getAdminTranslation\((\')' . _PS_TRANS_PATTERN_ . '\'(?:,.*)*\)/U'; + } else { + $regex = '/\{l\s*s\s*=([\'\"])' . _PS_TRANS_PATTERN_ . '\1(\s*sprintf=.*)?(\s*js=1)?(\s*slashes=1)?.*\}/U'; + } + + break; + + case 'errors': + // Parsing file for all errors syntax + $regex = '/Tools::displayError\((\')' . _PS_TRANS_PATTERN_ . '\'(,\s*(.+))?\)/U'; + + break; + + case 'modules': + // Parsing modules file + if ($type_file == 'php') { + $regex = '/->l\(\s*(\')' . _PS_TRANS_PATTERN_ . '\'(\s*,\s*?\'(.+)\')?(\s*,\s*?(.+))?\s*\)/Ums'; + } else { + // In tpl file look for something that should contain mod='module_name' according to the documentation + $regex = '/\{l\s*s=([\'\"])' . _PS_TRANS_PATTERN_ . '\1.*\s+mod=\'' . $module_name . '\'.*\}/U'; + } + + break; + + case 'pdf': + // Parsing PDF file + if ($type_file == 'php') { + $regex = [ + '/HTMLTemplate.*::l\((\')' . _PS_TRANS_PATTERN_ . '\'[\)|\,]/U', + '/->l\((\')' . _PS_TRANS_PATTERN_ . '\'(, ?\'(.+)\')?(, ?(.+))?\)/U', + ]; + } else { + $regex = '/\{l\s*s=([\'\"])' . _PS_TRANS_PATTERN_ . '\1(\s*sprintf=.*)?(\s*js=1)?(\s*pdf=\'true\')?\s*\}/U'; + } + + break; + } + + if (!is_array($regex)) { + $regex = [$regex]; + } + + $strings = []; + foreach ($regex as $regex_row) { + $matches = []; + $n = preg_match_all($regex_row, $content, $matches); + for ($i = 0; $i < $n; ++$i) { + $quote = $matches[1][$i]; + $string = $matches[2][$i]; + + if ($quote === '"') { + // Escape single quotes because the core will do it when looking for the translation of this string + $string = str_replace('\'', '\\\'', $string); + // Unescape double quotes + $string = preg_replace('/\\\\+"/', '"', $string); + } + + $strings[] = $string; + } + } + + return array_unique($strings); + } + + /** + * Get all translations informations for all type of translations. + * + * array( + * 'type' => array( + * 'name' => string : title for the translation type, + * 'var' => string : name of var for the translation file, + * 'dir' => string : dir of translation file + * 'file' => string : file name of translation file + * ) + * ) + */ + public function getTranslationsInformations() + { + $this->translations_informations = [ + 'back' => [ + 'name' => $this->trans('Back office translations', [], 'Admin.International.Feature'), + 'var' => '_LANGADM', + 'dir' => _PS_TRANSLATIONS_DIR_ . $this->lang_selected->iso_code . '/', + 'file' => 'admin.php', + 'sf_controller' => true, + 'choice_theme' => false, + ], + 'themes' => [ + 'name' => $this->trans('Themes translations', [], 'Admin.International.Feature'), + 'var' => '_THEMES', + 'dir' => '', + 'file' => '', + 'sf_controller' => true, + 'choice_theme' => true, + ], + 'modules' => [ + 'name' => $this->trans('Installed modules translations', [], 'Admin.International.Feature'), + 'var' => '_MODULES', + 'dir' => _PS_ROOT_DIR_ . '/modules/', + 'file' => '', + 'sf_controller' => true, + 'choice_theme' => false, + ], + 'mails' => [ + 'name' => $this->trans('Email translations', [], 'Admin.International.Feature'), + 'var' => '_LANGMAIL', + 'dir' => _PS_MAIL_DIR_ . $this->lang_selected->iso_code . '/', + 'file' => 'lang.php', + 'sf_controller' => false, + 'choice_theme' => false, + ], + 'others' => [ + 'name' => $this->trans('Other translations', [], 'Admin.International.Feature'), + 'var' => '_OTHERS', + 'dir' => '', + 'file' => '', + 'sf_controller' => true, + 'choice_theme' => false, + ], + ]; + + if (defined('_PS_THEME_SELECTED_DIR_')) { + $this->translations_informations['modules']['override'] = ['dir' => _PS_THEME_SELECTED_DIR_ . 'modules/', 'file' => '']; + $this->translations_informations['mails']['override'] = ['dir' => _PS_THEME_SELECTED_DIR_ . 'mails/' . $this->lang_selected->iso_code . '/', 'file' => 'lang.php']; + } + } + + /** + * Get all informations on : languages, theme and the translation type. + */ + public function getInformations() + { + // Get all Languages + $this->languages = Language::getLanguages(false); + + // Get all iso_code of languages + foreach ($this->languages as $language) { + $this->all_iso_lang[] = $language['iso_code']; + } + + // Get folder name of theme + if (($theme = Tools::getValue('selected-theme')) && !is_array($theme)) { + $theme_exists = $this->theme_exists($theme); + if (!$theme_exists) { + throw new PrestaShopException($this->trans('Invalid theme "%theme%"', ['%theme%' => Tools::safeOutput($theme)], 'Admin.International.Notification')); + } + $this->theme_selected = Tools::safeOutput($theme); + } + + // Set the path of selected theme + if ($this->theme_selected) { + define('_PS_THEME_SELECTED_DIR_', _PS_ROOT_DIR_ . '/themes/' . $this->theme_selected . '/'); + } else { + define('_PS_THEME_SELECTED_DIR_', ''); + } + + // Get type of translation + if (($type = Tools::getValue('type')) && !is_array($type)) { + $this->type_selected = strtolower(Tools::safeOutput($type)); + } + + // Get selected language + if (Tools::getValue('lang') || Tools::getValue('iso_code')) { + $iso_code = Tools::getValue('lang') ? Tools::getValue('lang') : Tools::getValue('iso_code'); + + if (!Validate::isLangIsoCode($iso_code) || !in_array($iso_code, $this->all_iso_lang)) { + throw new PrestaShopException($this->trans('Invalid iso code "%iso_code%"', ['%iso_code%' => Tools::safeOutput($iso_code)], 'Admin.International.Notification')); + } + + $this->lang_selected = new Language((int) Language::getIdByIso($iso_code)); + } else { + $this->lang_selected = new Language((int) Language::getIdByIso('en')); + } + + // Get all information for translations + $this->getTranslationsInformations(); + } + + public function renderKpis() + { + $time = time(); + $kpis = []; + + /* The data generation is located in AdminStatsControllerCore */ + + $helper = new HelperKpi(); + $helper->id = 'box-languages'; + $helper->icon = 'icon-microphone'; + $helper->color = 'color1'; + $helper->href = $this->context->link->getAdminLink('AdminLanguages'); + $helper->title = $this->trans('Enabled Languages', [], 'Admin.International.Feature'); + if (ConfigurationKPI::get('ENABLED_LANGUAGES') !== false) { + $helper->value = ConfigurationKPI::get('ENABLED_LANGUAGES'); + } + $helper->source = $this->context->link->getAdminLink('AdminStats') . '&ajax=1&action=getKpi&kpi=enabled_languages'; + $helper->refresh = (bool) (ConfigurationKPI::get('ENABLED_LANGUAGES_EXPIRE') < $time); + $kpis[] = $helper->generate(); + + $helper = new HelperKpi(); + $helper->id = 'box-country'; + $helper->icon = 'icon-home'; + $helper->color = 'color2'; + $helper->title = $this->trans('Main Country', [], 'Admin.International.Feature'); + $helper->subtitle = $this->trans('30 Days', [], 'Admin.Global'); + if (ConfigurationKPI::get('MAIN_COUNTRY', $this->context->language->id) !== false) { + $helper->value = ConfigurationKPI::get('MAIN_COUNTRY', $this->context->language->id); + } + $helper->source = $this->context->link->getAdminLink('AdminStats') . '&ajax=1&action=getKpi&kpi=main_country'; + $helper->refresh = (bool) (ConfigurationKPI::get('MAIN_COUNTRY_EXPIRE', $this->context->language->id) < $time); + $kpis[] = $helper->generate(); + + $helper = new HelperKpi(); + $helper->id = 'box-translations'; + $helper->icon = 'icon-list'; + $helper->color = 'color3'; + $helper->title = $this->trans('Front office Translations', [], 'Admin.International.Feature'); + if (ConfigurationKPI::get('FRONTOFFICE_TRANSLATIONS') !== false) { + $helper->value = ConfigurationKPI::get('FRONTOFFICE_TRANSLATIONS'); + } + $helper->source = $this->context->link->getAdminLink('AdminStats') . '&ajax=1&action=getKpi&kpi=frontoffice_translations'; + $helper->refresh = (bool) (ConfigurationKPI::get('FRONTOFFICE_TRANSLATIONS_EXPIRE') < $time); + $kpis[] = $helper->generate(); + + $helper = new HelperKpiRow(); + $helper->kpis = $kpis; + + return $helper->generate(); + } + + /** + * AdminController::postProcess() override. + * + * @see AdminController::postProcess() + */ + public function postProcess() + { + $this->getInformations(); + + /* PrestaShop demo mode */ + if (_PS_MODE_DEMO_) { + $this->errors[] = $this->trans('This functionality has been disabled.', [], 'Admin.Notifications.Error'); + + return; + } + /* PrestaShop demo mode */ + + try { + if (Tools::isSubmit('submitCopyLang')) { + if ($this->access('add')) { + $this->submitCopyLang(); + } else { + $this->errors[] = $this->trans('You do not have permission to add this.', [], 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('submitExport')) { + if ($this->access('add')) { + $this->submitExportLang(); + } else { + $this->errors[] = $this->trans('You do not have permission to add this.', [], 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('submitImport')) { + if ($this->access('add')) { + $this->submitImportLang(); + } else { + $this->errors[] = $this->trans('You do not have permission to add this.', [], 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('submitAddLanguage')) { + if ($this->access('add')) { + $this->submitAddLang(); + } else { + $this->errors[] = $this->trans('You do not have permission to add this.', [], 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('submitTranslationsPdf')) { + if ($this->access('edit')) { + // Only the PrestaShop team should write the translations into the _PS_TRANSLATIONS_DIR_ + if (!$this->theme_selected) { + $this->writeTranslationFile(); + } else { + $this->writeTranslationFile(true); + } + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('submitTranslationsBack') || Tools::isSubmit('submitTranslationsErrors') || Tools::isSubmit('submitTranslationsFields') || Tools::isSubmit('submitTranslationsFront')) { + if ($this->access('edit')) { + $this->writeTranslationFile(); + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('submitTranslationsMails') || Tools::isSubmit('submitTranslationsMailsAndStay')) { + if ($this->access('edit')) { + $this->submitTranslationsMails(); + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('submitTranslationsModules')) { + if ($this->access('edit')) { + // Get list of modules + if ($modules = $this->getListModules()) { + // Get files of all modules + $arr_files = $this->getAllModuleFiles($modules, null, $this->lang_selected->iso_code, true); + + // Find and write all translation modules files + foreach ($arr_files as $value) { + $this->findAndWriteTranslationsIntoFile($value['file_name'], $value['files'], $value['theme'], $value['module'], $value['dir']); + } + + // Clear modules cache + Tools::clearAllCache(); + + // Redirect + if (Tools::getIsset('submitTranslationsModulesAndStay')) { + $this->redirect(true); + } else { + $this->redirect(); + } + } + } else { + $this->errors[] = $this->trans('You do not have permission to edit this.', [], 'Admin.Notifications.Error'); + } + } elseif (Tools::isSubmit('submitSelectModules')) { + $this->redirect(false, false, true); + } + } catch (PrestaShopException $e) { + $this->errors[] = $e->getMessage(); + } + } + + /** + * This method redirect in the translation main page or in the translation page. + * + * @param bool $save_and_stay : true if the user has clicked on the button "save and stay" + * @param bool $conf : id of confirmation message + * @param bool $modify_translation : true if the user has clicked on the button "Modify translation" + */ + protected function redirect($save_and_stay = false, $conf = false, $modify_translation = false) + { + $conf = !$conf ? 4 : $conf; + $url_base = self::$currentIndex . '&token=' . $this->token . '&conf=' . $conf; + if ($modify_translation) { + Tools::redirectAdmin(self::$currentIndex . '&token=' . $this->token . '&lang=' . Tools::getValue('langue') . '&type=' . $this->type_selected . '&module=' . Tools::getValue('module') . '&theme=' . $this->theme_selected); + } elseif ($save_and_stay) { + Tools::redirectAdmin($url_base . '&lang=' . $this->lang_selected->iso_code . '&type=' . $this->type_selected . '&module=' . Tools::getValue('module') . '&theme=' . $this->theme_selected); + } else { + Tools::redirectAdmin($url_base . '&action=settings'); + } + } + + protected function getMailPattern() + { + Tools::displayAsDeprecated('Email pattern is no longer used, emails are always saved like they are.'); + // Let the indentation like it. + return ' + +
' . stripcslashes($subject_mail) . '
+ ' . $this->trans('There was a problem getting the mail files.', [], 'Admin.International.Notification') . ' + ' . $this->trans('English language files must exist in %folder% folder', [ + '%folder%' => '' . preg_replace('@/[a-z]{2}(/?)$@', '/en$1', $mails['directory']) . '', + ], 'Admin.International.Notification') . ' +