* @copyright 2007-2015 PrestaShop SA
* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
if (!defined('_PS_VERSION_')) {
exit;
}
class statsproduct extends ModuleGraph
{
private $html = '';
private $query = '';
private $option = 0;
private $id_product = 0;
public function __construct()
{
$this->name = 'statsproduct';
$this->tab = 'analytics_stats';
$this->version = '2.0.3';
$this->author = 'PrestaShop';
$this->need_instance = 0;
parent::__construct();
$this->displayName = $this->trans('Product details', array(), 'Modules.Statsproduct.Admin');
$this->description = $this->trans('Adds detailed statistics for each product to the Stats dashboard.', array(), 'Modules.Statsproduct.Admin');
$this->ps_versions_compliancy = array('min' => '1.7.1.0', 'max' => _PS_VERSION_);
}
public function install()
{
return (parent::install() && $this->registerHook('AdminStatsModules'));
}
public function getTotalBought($id_product)
{
$date_between = ModuleGraph::getDateBetween();
$sql = 'SELECT SUM(od.`product_quantity`) AS total
FROM `'._DB_PREFIX_.'order_detail` od
LEFT JOIN `'._DB_PREFIX_.'orders` o ON o.`id_order` = od.`id_order`
WHERE od.`product_id` = '.(int)$id_product.'
'.Shop::addSqlRestriction(Shop::SHARE_ORDER, 'o').'
AND o.valid = 1
AND o.`date_add` BETWEEN '.$date_between;
return (int)Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql);
}
public function getTotalSales($id_product)
{
$date_between = ModuleGraph::getDateBetween();
$sql = 'SELECT SUM(od.`total_price_tax_excl`) AS total
FROM `'._DB_PREFIX_.'order_detail` od
LEFT JOIN `'._DB_PREFIX_.'orders` o ON o.`id_order` = od.`id_order`
WHERE od.`product_id` = '.(int)$id_product.'
'.Shop::addSqlRestriction(Shop::SHARE_ORDER, 'o').'
AND o.valid = 1
AND o.`date_add` BETWEEN '.$date_between;
return (float)Db::getInstance(_PS_USE_SQL_SLAVE_)->getValue($sql);
}
public function getTotalViewed($id_product)
{
$date_between = ModuleGraph::getDateBetween();
$sql = 'SELECT SUM(pv.`counter`) AS total
FROM `'._DB_PREFIX_.'page_viewed` pv
LEFT JOIN `'._DB_PREFIX_.'date_range` dr ON pv.`id_date_range` = dr.`id_date_range`
LEFT JOIN `'._DB_PREFIX_.'page` p ON pv.`id_page` = p.`id_page`
LEFT JOIN `'._DB_PREFIX_.'page_type` pt ON pt.`id_page_type` = p.`id_page_type`
WHERE pt.`name` = \'product\'
'.Shop::addSqlRestriction(false, 'pv').'
AND p.`id_object` = '.(int)$id_product.'
AND dr.`time_start` BETWEEN '.$date_between.'
AND dr.`time_end` BETWEEN '.$date_between;
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->getRow($sql);
return isset($result['total']) ? $result['total'] : 0;
}
private function getProducts($id_lang)
{
$sql = 'SELECT p.`id_product`, p.reference, pl.`name`, IFNULL(stock.quantity, 0) as quantity
FROM `'._DB_PREFIX_.'product` p
'.Product::sqlStock('p', 0).'
LEFT JOIN `'._DB_PREFIX_.'product_lang` pl ON p.`id_product` = pl.`id_product`'.Shop::addSqlRestrictionOnLang('pl').'
'.Shop::addSqlAssociation('product', 'p').'
'.(Tools::getValue('id_category') ? 'LEFT JOIN `'._DB_PREFIX_.'category_product` cp ON p.`id_product` = cp.`id_product`' : '').'
WHERE pl.`id_lang` = '.(int)$id_lang.'
'.(Tools::getValue('id_category') ? 'AND cp.id_category = '.(int)Tools::getValue('id_category') : '');
if (version_compare(_PS_VERSION_, '1.7.0.0', '>=')) {
$sql .= ' AND p.state = ' . Product::STATE_SAVED;
}
$sql .= ' ORDER BY pl.`name`';
return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
}
private function getSales($id_product)
{
$sql = 'SELECT o.date_add, o.id_order, o.id_customer, od.product_quantity, (od.product_price * od.product_quantity) as total, od.tax_name, od.product_name
FROM `'._DB_PREFIX_.'orders` o
LEFT JOIN `'._DB_PREFIX_.'order_detail` od ON o.id_order = od.id_order
WHERE o.date_add BETWEEN '.$this->getDate().'
'.Shop::addSqlRestriction(Shop::SHARE_ORDER, 'o').'
AND o.valid = 1
AND od.product_id = '.(int)$id_product;
return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
}
private function getCrossSales($id_product, $id_lang)
{
$sql = 'SELECT pl.name as pname, pl.id_product, SUM(od.product_quantity) as pqty, AVG(od.product_price) as pprice
FROM `'._DB_PREFIX_.'orders` o
LEFT JOIN `'._DB_PREFIX_.'order_detail` od ON o.id_order = od.id_order
LEFT JOIN `'._DB_PREFIX_.'product_lang` pl ON (pl.id_product = od.product_id AND pl.id_lang = '.(int)$id_lang.Shop::addSqlRestrictionOnLang('pl').')
WHERE o.id_customer IN (
SELECT o.id_customer
FROM `'._DB_PREFIX_.'orders` o
LEFT JOIN `'._DB_PREFIX_.'order_detail` od ON o.id_order = od.id_order
WHERE o.date_add BETWEEN '.$this->getDate().'
AND o.valid = 1
AND od.product_id = '.(int)$id_product.'
)
'.Shop::addSqlRestriction(Shop::SHARE_ORDER, 'o').'
AND o.date_add BETWEEN '.$this->getDate().'
AND o.valid = 1
AND od.product_id != '.(int)$id_product.'
GROUP BY od.product_id
ORDER BY pqty DESC';
return Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($sql);
}
public function hookAdminStatsModules()
{
$id_category = (int)Tools::getValue('id_category');
$currency = Context::getContext()->currency;
if (Tools::getValue('export')) {
if (!Tools::getValue('exportType')) {
$this->csvExport(array(
'layers' => 2,
'type' => 'line',
'option' => '42'
));
}
}
$this->html = '
'.$this->displayName.'
'.$this->trans('Guide', array(), 'Admin.Global').'
'.$this->trans('Number of purchases compared to number of views', array(), 'Modules.Statsproduct.Admin').'
'.$this->trans('After choosing a category and selecting a product, informational graphs will appear.', array(), 'Modules.Statsproduct.Admin').'
- '.$this->trans('If you notice that a product is often purchased but viewed infrequently, you should display it more prominently in your Front Office.', array(), 'Modules.Statsproduct.Admin').'
- '.$this->trans('On the other hand, if a product has many views but is not often purchased, we advise you to check or modify this product\'s information, description and photography again, see if you can find something better.', array(), 'Modules.Statsproduct.Admin').'
';
if ($id_product = (int)Tools::getValue('id_product')) {
if (Tools::getValue('export')) {
if (Tools::getValue('exportType') == 1) {
$this->csvExport(array(
'layers' => 2,
'type' => 'line',
'option' => '1-'.$id_product
));
} elseif (Tools::getValue('exportType') == 2) {
$this->csvExport(array(
'type' => 'pie',
'option' => '3-'.$id_product
));
}
}
$product = new Product($id_product, false, $this->context->language->id);
$total_bought = $this->getTotalBought($product->id);
$total_sales = $this->getTotalSales($product->id);
$total_viewed = $this->getTotalViewed($product->id);
$this->html .= ''.$product->name.' - '.$this->trans('Details', array(), 'Modules.Statsproduct.Admin').'
'.$this->engine(array(
'layers' => 2,
'type' => 'line',
'option' => '1-'.$id_product
)).'
- '.$this->trans('Total bought', array(), 'Modules.Statsproduct.Admin').' '.$total_bought.'
- '.$this->trans('Sales (tax excluded)', array(), 'Modules.Statsproduct.Admin').' '.Tools::displayprice($total_sales, $currency).'
- '.$this->trans('Total Viewed', array(), 'Modules.Statsproduct.Admin').' '.$total_viewed.'
- '.$this->trans('Conversion rate', array(), 'Modules.Statsproduct.Admin').' '.number_format($total_viewed ? $total_bought / $total_viewed : 0, 2).'
'.$this->trans('CSV Export', array(), 'Modules.Statsproduct.Admin').'
';
if ($has_attribute = $product->hasAttributes() && $total_bought) {
$this->html .= '
'.$this->trans('Attribute sales distribution', array(), 'Modules.Statsproduct.Admin').'
'.$this->engine(array('type' => 'pie', 'option' => '3-'.$id_product)).'
'.$this->trans('CSV Export', array(), 'Modules.Statsproduct.Admin').'';
}
if ($total_bought) {
$sales = $this->getSales($id_product);
$this->html .= '
'.$this->trans('Sales', array(), 'Admin.Global').'
|
'.$this->trans('Date', array(), 'Admin.Global').'
|
'.$this->trans('Order', array(), 'Admin.Global').'
|
'.$this->trans('Customer', array(), 'Admin.Global').'
|
'.($has_attribute ? ''.$this->trans('Attribute', array(), 'Admin.Global').' | ' : '').'
'.$this->trans('Quantity', array(), 'Admin.Global').'
|
'.$this->trans('Price', array(), 'Admin.Global').'
|
';
$token_order = Tools::getAdminToken('AdminOrders'.(int)Tab::getIdFromClassName('AdminOrders').(int)$this->context->employee->id);
$token_customer = Tools::getAdminToken('AdminCustomers'.(int)Tab::getIdFromClassName('AdminCustomers').(int)$this->context->employee->id);
foreach ($sales as $sale) {
$this->html .= '
| '.Tools::displayDate($sale['date_add'], null, false).' |
'.(int)$sale['id_order'].' |
'.(int)$sale['id_customer'].' |
'.($has_attribute ? ''.$sale['product_name'].' | ' : '').'
'.(int)$sale['product_quantity'].' |
'.Tools::displayprice($sale['total'], $currency).' |
';
}
$this->html .= '
';
$cross_selling = $this->getCrossSales($id_product, $this->context->language->id);
if (count($cross_selling)) {
$this->html .= '
'.$this->trans('Cross selling', array(), 'Modules.Statsproduct.Admin').'
|
'.$this->trans('Product name', array(), 'Admin.Shopparameters.Feature').'
|
'.$this->trans('Quantity sold', array(), 'Admin.Global').'
|
'.$this->trans('Average price', array(), 'Admin.Global').'
|
';
$token_products = Tools::getAdminToken('AdminProducts'.(int)Tab::getIdFromClassName('AdminProducts').(int)$this->context->employee->id);
foreach ($cross_selling as $selling) {
$this->html .= '
| '.$selling['pname'].' |
'.(int)$selling['pqty'].' |
'.Tools::displayprice($selling['pprice'], $currency).' |
';
}
$this->html .= '
';
}
}
} else {
$categories = Category::getCategories((int)$this->context->language->id, true, false);
$this->html .= '
'.$this->trans('Products available', array(), 'Modules.Statsproduct.Admin').'
|
'.$this->trans('Reference', array(), 'Admin.Global').'
|
'.$this->trans('Name', array(), 'Admin.Global').'
|
'.$this->trans('Available quantity for sale', array(), 'Admin.Global').'
|
';
foreach ($this->getProducts($this->context->language->id) as $product) {
$this->html .= '
| '.$product['reference'].' |
'.$product['name'].'
|
'.$product['quantity'].' |
';
}
$this->html .= '
'.$this->trans('CSV Export', array(), 'Modules.Statsproduct.Admin').'
';
}
return $this->html;
}
public function setOption($option, $layers = 1)
{
$options = explode('-', $option);
if (count($options) === 2) {
list($this->option, $this->id_product) = $options;
} else {
$this->option = $option;
}
$date_between = $this->getDate();
switch ($this->option) {
case 1:
$this->_titles['main'][0] = $this->trans('Popularity', array(), 'Modules.Statsproduct.Admin');
$this->_titles['main'][1] = $this->trans('Sales', array(), 'Admin.Global');
$this->_titles['main'][2] = $this->trans('Visits (x100)', array(), 'Modules.Statsproduct.Admin');
$this->query[0] = 'SELECT o.`date_add`, SUM(od.`product_quantity`) AS total
FROM `'._DB_PREFIX_.'order_detail` od
LEFT JOIN `'._DB_PREFIX_.'orders` o ON o.`id_order` = od.`id_order`
WHERE od.`product_id` = '.(int)$this->id_product.'
'.Shop::addSqlRestriction(Shop::SHARE_ORDER, 'o').'
AND o.valid = 1
AND o.`date_add` BETWEEN '.$date_between.'
GROUP BY o.`date_add`';
$this->query[1] = 'SELECT dr.`time_start` AS date_add, (SUM(pv.`counter`) / 100) AS total
FROM `'._DB_PREFIX_.'page_viewed` pv
LEFT JOIN `'._DB_PREFIX_.'date_range` dr ON pv.`id_date_range` = dr.`id_date_range`
LEFT JOIN `'._DB_PREFIX_.'page` p ON pv.`id_page` = p.`id_page`
LEFT JOIN `'._DB_PREFIX_.'page_type` pt ON pt.`id_page_type` = p.`id_page_type`
WHERE pt.`name` = \'product\'
'.Shop::addSqlRestriction(false, 'pv').'
AND p.`id_object` = '.(int)$this->id_product.'
AND dr.`time_start` BETWEEN '.$date_between.'
AND dr.`time_end` BETWEEN '.$date_between.'
GROUP BY dr.`time_start`';
break;
case 3:
$this->query = 'SELECT product_attribute_id, SUM(od.`product_quantity`) AS total
FROM `'._DB_PREFIX_.'orders` o
LEFT JOIN `'._DB_PREFIX_.'order_detail` od ON o.`id_order` = od.`id_order`
WHERE od.`product_id` = '.(int)$this->id_product.'
'.Shop::addSqlRestriction(Shop::SHARE_ORDER, 'o').'
AND o.valid = 1
AND o.`date_add` BETWEEN '.$date_between.'
GROUP BY od.`product_attribute_id`';
$this->_titles['main'] = $this->trans('Attributes', array(), 'Admin.Global');
break;
case 42:
$this->_titles['main'][1] = $this->trans('Reference', array(), 'Admin.Global');
$this->_titles['main'][2] = $this->trans('Name', array(), 'Admin.Global');
$this->_titles['main'][3] = $this->trans('Stock', array(), 'Modules.Statsproduct.Admin');
break;
}
}
protected function getData($layers)
{
if ($this->option == 42) {
$products = $this->getProducts($this->context->language->id);
foreach ($products as $product) {
$this->_values[0][] = $product['reference'];
$this->_values[1][] = $product['name'];
$this->_values[2][] = $product['quantity'];
$this->_legend[] = $product['id_product'];
}
} elseif ($this->option != 3) {
$this->setDateGraph($layers, true);
} else {
$product = new Product($this->id_product, false, (int)$this->getLang());
$comb_array = array();
$assoc_names = array();
$combinations = $product->getAttributeCombinations((int)$this->getLang());
foreach ($combinations as $combination) {
$comb_array[$combination['id_product_attribute']][] = array(
'group' => $combination['group_name'],
'attr' => $combination['attribute_name']
);
}
foreach ($comb_array as $id_product_attribute => $product_attribute) {
$list = '';
foreach ($product_attribute as $attribute) {
$list .= trim($attribute['group']).' - '.trim($attribute['attr']).', ';
}
$list = rtrim($list, ', ');
$assoc_names[$id_product_attribute] = $list;
}
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($this->query);
foreach ($result as $row) {
$this->_values[] = $row['total'];
$this->_legend[] = @$assoc_names[$row['product_attribute_id']];
}
}
}
protected function setAllTimeValues($layers)
{
for ($i = 0; $i < $layers; $i++) {
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($this->query[$i]);
foreach ($result as $row) {
$this->_values[$i][(int)substr($row['date_add'], 0, 4)] += $row['total'];
}
}
}
protected function setYearValues($layers)
{
for ($i = 0; $i < $layers; $i++) {
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($this->query[$i]);
foreach ($result as $row) {
$this->_values[$i][(int)substr($row['date_add'], 5, 2)] += $row['total'];
}
}
}
protected function setMonthValues($layers)
{
for ($i = 0; $i < $layers; $i++) {
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($this->query[$i]);
foreach ($result as $row) {
$this->_values[$i][(int)substr($row['date_add'], 8, 2)] += $row['total'];
}
}
}
protected function setDayValues($layers)
{
for ($i = 0; $i < $layers; $i++) {
$result = Db::getInstance(_PS_USE_SQL_SLAVE_)->executeS($this->query[$i]);
foreach ($result as $row) {
$this->_values[$i][(int)substr($row['date_add'], 11, 2)] += $row['total'];
}
}
}
}