Inital commit

This commit is contained in:
2020-11-19 15:36:28 +01:00
parent 71f32f83d3
commit 66ce4ee218
18077 changed files with 2166122 additions and 35184 deletions

View File

@@ -0,0 +1,26 @@
<?php
/*************************************************************************************/
/* This file is part of the Thelia package. */
/* */
/* Copyright (c) OpenStudio */
/* email : dev@thelia.net */
/* web : http://www.thelia.net */
/* */
/* For the full copyright and license information, please view the LICENSE.txt */
/* file that was distributed with this source code. */
/*************************************************************************************/
namespace BestSellers;
use Thelia\Module\BaseModule;
class BestSellers extends BaseModule
{
/** @var string */
const DOMAIN_NAME = 'bestsellers';
const GET_BEST_SELLING_PRODUCTS = "bestsellers.event.get_best_selling_products";
/* Data cache lifetime in minutes */
const CACHE_LIFETIME_IN_MINUTES = 1440;
}

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8" ?>
<config xmlns="http://thelia.net/schema/dic/config"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://thelia.net/schema/dic/config http://thelia.net/schema/dic/config/thelia-1.0.xsd">
<loops>
<loop name="best_selling_products" class="BestSellers\Loop\BestSellerLoop" />
</loops>
<services>
<service id="best_sellers.event_listener" class="BestSellers\EventListeners\EventManager">
<argument id="thelia.cache" type="service"/>
<tag name="kernel.event_subscriber"/>
</service>
</services>
<hooks>
<hook id="best_sellers.hook.back" class="BestSellers\Hook\HookManager">
<tag name="hook.event_listener" event="main.top-menu-tools" type="back" method="onMainTopMenuTools" />
<tag name="hook.event_listener" event="product.modification.form-right.bottom" type="back" templates="render:product-edit.html" />
</hook>
<hook id="best_sellers.hook.front">
<tag name="hook.event_listener" event="home.body" templates="render:home-body.html" />
</hook>
</hooks>
</config>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="http://thelia.net/schema/dic/module"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://thelia.net/schema/dic/module http://thelia.net/schema/dic/module/module-2_2.xsd">
<fullnamespace>BestSellers\BestSellers</fullnamespace>
<descriptive locale="en_US">
<title>Display your best sellers on your home page, and get information about products sales</title>
</descriptive>
<descriptive locale="fr_FR">
<title>Afficher vos produits les plus vendus sur votre page d'acceuil, et obtenez des détails sur les ventes de vos produits</title>
</descriptive>
<languages>
<language>en_US</language>
<language>fr_FR</language>
</languages>
<version>1.1.1</version>
<authors>
<author>
<name>Franck Allimant</name>
<company>CQFDev</company>
<email>thelia@cqfdev.fr</email>
<website>www.cqfdev.fr</website>
</author>
</authors>
<type>classic</type>
<thelia>2.3.4</thelia>
<stability>prod</stability>
<mandatory>0</mandatory>
<hidden>0</hidden>
</module>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" ?>
<routes xmlns="http://symfony.com/schema/routing"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
</routes>

View File

@@ -0,0 +1,116 @@
<?php
/*************************************************************************************/
/* Copyright (c) Franck Allimant, CQFDev */
/* email : thelia@cqfdev.fr */
/* web : http://www.cqfdev.fr */
/* */
/* For the full copyright and license information, please view the LICENSE */
/* file that was distributed with this source code. */
/*************************************************************************************/
/**
* Created by Franck Allimant, CQFDev <franck@cqfdev.fr>
* Date: 21/05/2018 16:34
*/
namespace BestSellers\EventListeners;
use Thelia\Core\Event\ActionEvent;
class BestSellersEvent extends ActionEvent
{
/** @var \DateTime */
protected $startDate;
/** @var \DateTime */
protected $endDate;
/** @var array */
protected $bestSellingProductsData = [];
protected $totalSales = 0;
/**
* BestSellersEvent constructor.
* @param $startDate
* @param $endDate
*/
public function __construct(\DateTime $startDate = null, \DateTime $endDate = null)
{
$this->startDate = null === $startDate ? new \DateTime("1970-01-01") : $startDate;
$this->endDate = null === $endDate ? new \DateTime() : $endDate;
}
/**
* @return \DateTime
*/
public function getStartDate()
{
return $this->startDate;
}
/**
* @param \DateTime $startDate
* @return $this
*/
public function setStartDate($startDate)
{
$this->startDate = $startDate;
return $this;
}
/**
* @return \DateTime
*/
public function getEndDate()
{
return $this->endDate;
}
/**
* @param \DateTime $endDate
* @return $this
*/
public function setEndDate($endDate)
{
$this->endDate = $endDate;
return $this;
}
/**
* @return array
*/
public function getBestSellingProductsData()
{
return ! empty($this->bestSellingProductsData) ? $this->bestSellingProductsData : [];
}
/**
* @param array $bestSellingProductsData
* @return $this
*/
public function setBestSellingProductsData($bestSellingProductsData)
{
$this->bestSellingProductsData = $bestSellingProductsData;
return $this;
}
/**
* @return int
*/
public function getTotalSales()
{
return $this->totalSales;
}
/**
* @param float $totalSales
* @return $this
*/
public function setTotalSales($totalSales)
{
$this->totalSales = floatval($totalSales);
return $this;
}
}

View File

@@ -0,0 +1,129 @@
<?php
/*************************************************************************************/
/* Copyright (c) Franck Allimant, CQFDev */
/* email : thelia@cqfdev.fr */
/* web : http://www.cqfdev.fr */
/* */
/* For the full copyright and license information, please view the LICENSE */
/* file that was distributed with this source code. */
/*************************************************************************************/
namespace BestSellers\EventListeners;
use BestSellers\BestSellers;
use Propel\Runtime\Connection\PdoConnection;
use Propel\Runtime\Propel;
use Psr\Cache\InvalidArgumentException;
use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Thelia\Action\BaseAction;
use Thelia\Model\Map\OrderProductTableMap;
use Thelia\Model\Map\OrderTableMap;
use Thelia\Model\Map\ProductTableMap;
use Thelia\Model\OrderStatusQuery;
class EventManager extends BaseAction implements EventSubscriberInterface
{
/** @var AdapterInterface $cacheAdapter */
protected $cacheAdapter;
/**
* DigressivePriceListener constructor.
* @param AdapterInterface $cacheAdapter
*/
public function __construct(AdapterInterface $cacheAdapter)
{
$this->cacheAdapter = $cacheAdapter;
}
public static function getSubscribedEvents()
{
return [
BestSellers::GET_BEST_SELLING_PRODUCTS => [ "calculateBestSellers", 128 ]
];
}
public function calculateBestSellers(BestSellersEvent $event)
{
$cacheKey = sprintf(
"best_sellers_%s_%s",
$event->getStartDate()->format('Y-m-d'),
$event->getEndDate()->format('Y-m-d')
);
try {
$cacheItem = $this->cacheAdapter->getItem($cacheKey);
if (! $cacheItem->isHit()) {
/** @var PdoConnection $con */
$con = Propel::getConnection();
$query = "
SELECT
" . ProductTableMap::ID . " as product_id,
SUM(" . OrderProductTableMap::QUANTITY . ") as total_quantity,
SUM(".OrderProductTableMap::QUANTITY." * IF(" . OrderProductTableMap::WAS_IN_PROMO . "," . OrderProductTableMap::PROMO_PRICE . ", ".OrderProductTableMap::PRICE.")) as total_sales
FROM
" . OrderProductTableMap::TABLE_NAME . "
LEFT JOIN
" . OrderTableMap::TABLE_NAME . " on " . OrderTableMap::ID . " = " . OrderProductTableMap::ORDER_ID . "
LEFT JOIN
" . ProductTableMap::TABLE_NAME . " on " . ProductTableMap::REF . " = " . OrderProductTableMap::PRODUCT_REF . "
WHERE
" . OrderTableMap::CREATED_AT . " >= ?
AND
" . OrderTableMap::CREATED_AT . " <= ?
AND
" . OrderTableMap::STATUS_ID . " not in (?, ?)
GROUP BY
" . ProductTableMap::ID . "
ORDER BY
total_quantity desc
";
$query = preg_replace("/order([^_])/", "`order`$1", $query);
$stmt = $con->prepare($query);
$res = $stmt->execute([
$event->getStartDate()->format("Y-m-d H:i:s"),
$event->getEndDate()->format("Y-m-d H:i:s"),
OrderStatusQuery::getNotPaidStatus()->getId(),
OrderStatusQuery::getCancelledStatus()->getId()
]);
$data = [];
$totalSales = 0;
while ($res && $result = $stmt->fetch(\PDO::FETCH_ASSOC)) {
$data[] = $result;
$totalSales += $result['total_sales'];
}
$struct = [
'data' => $data,
'total_sales' => $totalSales
];
$cacheItem
->set(json_encode($struct))
->expiresAfter(60 * BestSellers::CACHE_LIFETIME_IN_MINUTES)
;
$this->cacheAdapter->save($cacheItem);
}
$struct = json_decode($cacheItem->get(), true);
$event
->setBestSellingProductsData($struct['data'])
->setTotalSales($struct['total_sales'])
;
} catch (InvalidArgumentException $e) {
// Nothing to do with this, return an empty result.
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
/*************************************************************************************/
/* */
/* Copyright (c) Franck Allimant, CQFDev */
/* email : thelia@cqfdev.fr */
/* web : http://www.cqfdev.fr */
/* */
/* For the full copyright and license information, please view the LICENSE */
/* file that was distributed with this source code. */
/* */
/*************************************************************************************/
/**
* @author Franck Allimant <franck@cqfdev.fr>
*
* Creation date: 23/03/2015 12:09
*/
namespace BestSellers\Hook;
use BestSellers\BestSellers;
use Thelia\Core\Event\Hook\HookRenderBlockEvent;
use Thelia\Core\Hook\BaseHook;
use Thelia\Tools\URL;
class HookManager extends BaseHook
{
public function onMainTopMenuTools(HookRenderBlockEvent $event)
{
$event->add(
[
'id' => 'bestsellers_menu_tags',
'class' => '',
'url' => URL::getInstance()->absoluteUrl('/admin/best-sellers'),
'title' => $this->trans("Best sellers", [], BestSellers::DOMAIN_NAME)
]
);
}
}

View File

@@ -0,0 +1,18 @@
<?php
return array(
' to : ' => ' to : ',
'% of sales: <strong>%count%</strong>' => '% of sales: <strong>%count%</strong>',
'Best sellers' => 'Best sellers',
'Product reference' => 'Product reference',
'Product title' => 'Product title',
'Sale ratio' => '% of sales',
'Sales total' => 'Sales total',
'Sold amount for this product: <strong>%count</strong>' => 'Sold amount for this product: <strong>%count</strong>',
'Statistics' => 'Statistics',
'Total amount' => 'Total amount w/o tax',
'Total sales for this product: <strong>%count</strong>' => 'Total sales for this product: <strong>%count</strong>',
'View' => 'View',
'View from : ' => 'View from : ',
'You have not sold any products yet' => 'You have not sold any products yet',
);

View File

@@ -0,0 +1,18 @@
<?php
return array(
' to : ' => ' à : ',
'% of sales: <strong>%count%</strong>' => '% du CA : <strong>%count%</strong>',
'Best sellers' => 'Meilleures ventes',
'Product reference' => 'Référence produit',
'Product title' => 'Titre du produit',
'Sale ratio' => '% du CA',
'Sales total' => 'Total des ventes',
'Sold amount for this product: <strong>%count</strong>' => 'Montant total HT vendu : <strong>%count</strong>',
'Statistics' => 'Statistiques',
'Total amount' => 'Montant total HT',
'Total sales for this product: <strong>%count</strong>' => 'Total des ventes de ce produit : <strong>%count</strong>',
'View' => 'Afficher',
'View from : ' => 'Afficher de : ',
'You have not sold any products yet' => 'Aucun produit vendu pour le moment.',
);

View File

@@ -0,0 +1,5 @@
<?php
return array(
'Best sellers' => 'Best sellers',
);

View File

@@ -0,0 +1,5 @@
<?php
return array(
'Best sellers' => 'Meilleures ventes',
);

View File

@@ -0,0 +1,5 @@
<?php
return array(
'Best sellers' => 'Our best sellers',
);

View File

@@ -0,0 +1,5 @@
<?php
return array(
'Best sellers' => 'Nos meilleures ventes',
);

View File

@@ -0,0 +1,146 @@
<?php
/*************************************************************************************/
/* Copyright (c) Franck Allimant, CQFDev */
/* email : thelia@cqfdev.fr */
/* web : http://www.cqfdev.fr */
/* */
/* For the full copyright and license information, please view the LICENSE */
/* file that was distributed with this source code. */
/*************************************************************************************/
namespace BestSellers\Loop;
use BestSellers\BestSellers;
use BestSellers\EventListeners\BestSellersEvent;
use Propel\Runtime\ActiveQuery\Criteria;
use Thelia\Core\Template\Element\LoopResultRow;
use Thelia\Core\Template\Loop\Argument\Argument;
use Thelia\Core\Template\Loop\Product;
use Thelia\Model\Map\ProductTableMap;
use Thelia\Type\EnumListType;
use Thelia\Type\TypeCollection;
/**
* Class BestSellerLoop
* @package BestSellers\Loop
* @method string getStartDate()
* @method string getEndDate()
*/
class BestSellerLoop extends Product
{
protected function getArgDefinitions()
{
$args = parent::getArgDefinitions();
return $args->addArguments([
Argument::createAnyTypeArgument('start_date', null, false),
Argument::createAnyTypeArgument('end_date', null, false),
new Argument(
'order',
new TypeCollection(
new EnumListType(
[
'id', 'id_reverse',
'alpha', 'alpha_reverse',
'min_price', 'max_price',
'manual', 'manual_reverse',
'created', 'created_reverse',
'updated', 'updated_reverse',
'ref', 'ref_reverse',
'visible', 'visible_reverse',
'position', 'position_reverse',
'promo',
'new',
'random',
'given_id',
'sold_count', 'sold_count_reverse',
'sold_amount', 'sold_amount_reverse',
'sale_ratio', 'sale_ratio_reverse'
]
)
),
'alpha'
),
]);
}
public function buildModelCriteria()
{
$query = parent::buildModelCriteria();
$startDate = $this->getStartDate() ? new \DateTime($this->getStartDate()) : null;
$endDate = $this->getEndDate() ? new \DateTime($this->getEndDate()) : null;
$event = new BestSellersEvent($startDate, $endDate);
$this->dispatcher->dispatch(BestSellers::GET_BEST_SELLING_PRODUCTS, $event);
$caseClause = $caseSalesClause = '';
$productData = $event->getBestSellingProductsData();
array_walk($productData, function($item) use (&$caseClause, &$caseSalesClause) {
$caseClause .= sprintf("WHEN %d THEN %f ", $item['product_id'], $item['total_quantity']);
$caseSalesClause .= sprintf("WHEN %d THEN %f ", $item['product_id'], $item['total_sales']);
});
if (! empty($caseClause)) {
$query
->withColumn('CASE ' . ProductTableMap::ID . ' ' . $caseClause . ' ELSE 0 END', 'sold_quantity')
->withColumn('CASE ' . ProductTableMap::ID . ' ' . $caseSalesClause . ' ELSE 0 END', 'sold_amount')
;
} else {
$query
->withColumn('(0)', 'sold_quantity')
->withColumn('(0)', 'sold_amount')
;
}
if ($event->getTotalSales() !== 0) {
$query->withColumn("(select 100 * sold_amount / " . $event->getTotalSales() . ")", 'sale_ratio');
} else {
$query->withColumn('(0)', 'sale_ratio');
}
$orders = $this->getOrder();
foreach ($orders as $order) {
switch ($order) {
case "sold_count":
$query->orderBy('sold_quantity', Criteria::ASC);
break;
case "sold_count_reverse":
$query->orderBy('sold_quantity', Criteria::DESC);
break;
case "sold_amount":
$query->orderBy('sold_amount', Criteria::ASC);
break;
case "sold_amount_reverse":
$query->orderBy('sold_amount', Criteria::DESC);
break;
case "sale_ratio":
$query->orderBy('sale_ratio', Criteria::ASC);
break;
case "sale_ratio_reverse":
$query->orderBy('sale_ratio', Criteria::DESC);
break;
}
}
return $query;
}
/**
* @param LoopResultRow $loopResultRow
* @param \Thelia\Model\Product $item
* @throws \Propel\Runtime\Exception\PropelException
*/
protected function addOutputFields(LoopResultRow $loopResultRow, $item)
{
$loopResultRow
->set("SOLD_QUANTITY", $item->getVirtualColumn('sold_quantity'))
->set("SOLD_AMOUNT", $item->getVirtualColumn('sold_amount'))
->set("SALE_RATIO", $item->getVirtualColumn('sale_ratio'))
;
}
}

View File

@@ -0,0 +1,164 @@
# Best Sellers
# en_US
This modules provides a loop which return the best (or the worst) sales.
## Installation
Manually, or with composer :
```
composer require cqfdev/best-sellers-module:~1.0
```
## Usage
This module shows the 4 best sales of your shop on the front page via the `home.body` hook.
You can also add where you want in your template (front or back-office), a loop `best_selling_products` to show your best or your worst sales.
In the back-office, you can see your best sales in the "Tools" menu.
Finally, the total number of sales of a product appears on the product sheet.
## Hook
This module shows the 4 best sales of your shop on the front page via the `home.body` hook.
## Loop
The module provide the loop `best_selling_product`, which extend the loop `product`. All the arguments of the `product` loop are therefore available.
`best_selling_products` loop
### Input parameters
All the arguments of the loop `product` are available.
The loop offers two new values for the parameter `order` of the loop `product``
- sold_count_reverse : sort by number of sales in decreasing order
- sold_count : sort by number of sales in increasing order
|Argument |Description |
|--- |--- |
|**start-date** | The period start date to be consider. By default, january 1st 1970. |
|**end-date** | The period end date to be consider. By default, today's date. |
### Output variables
All the variables of the loop `product`are available.
|Variable |Description |
|--- |--- |
|$SOLD_QUANTITY | The quantity of sold product on the considered period |
|$SOLD_AMOUNT | The total amount untaxed of sales on the considered period |
|$SALE_RATIO | The percentage of sales on the considered period |
### Example
To get your 10 best sales of all time:
<ul>
{loop type="best_selling_products" name="best-sellers" limit=10 order='sold_count_reverse'}
<li>{$REF} : {$TITLE} : {$SOLD_QUANTITY}</li>
{/loop}²²
</ul>
To get your 5 best sales of the month :
<ul>
{loop type="best_selling_products" name="best-sellers-this-month" order='sold_count_reverse' start_date={$smarty.now|date_format:'%Y-%m-01'} limit=5}
<li>{$REF} : {$TITLE} : {$SOLD_QUANTITY}</li>
{/loop}
</ul>
To get your 10 worst sales of all time :
<ul>
{loop type="best_selling_products" name="best-sellers" limit=10 order='sold_count'}
<li>{$REF} : {$TITLE} : {$SOLD_QUANTITY}</li>
{/loop}
</ul>
# fr_FR
Ce module vous fournit une boucle qui retourne vos meilleures (ou vos pires) ventes.
## Installation
Manuellement, ou avec composer :
```
composer require cqfdev/best-sellers-module:~1.0
```
## Usage
Ce module affiche les 4 meilleures ventes de votre boutique sur la page d'accueil, via le hook 'home.body'
Vous pouvez aussi ajouter où vous voulez dans votre template front office ou back-office une boucle `best_selling_products` pour afficher vos meilleures ou pires ventes.
Dans le back-office, vous pouvez voir vos meilleures ventes dans le menu "Outil".
Enfin, le nombre de ventes total d'un produit apparaît sur la fiche produit.
## Hook
Le module affiche les 4 meilleures ventes de votre boutique sur la page d'accueil, via le hook `home.body`
## Loop
Le module vous propose la boucle `best_selling_products`, qui étend la boucle `product`. Tous les arguments de la boucle `product` sont donc disponibles.
`best_selling_products` loop
### Paramètres en entrée
Tous les arguments de la boucle `product` sont disponibles.
La boucle propose deux valeurs supplémentaires pour le paramètre `order` de la boucle `product`:
- sold_count_reverse : trier par nombre de ventes décroissantes
- sold_count : trier par nombre de ventes croissantes
|Argument |Description |
|--- |--- |
|**start-date** | la date de début de période à prendre en compte. Par défaut, le 1er janvier 1970. |
|**end-date** | la date de fin de période à prendre en compte. Par défaut, la date du jour. |
### Variables en sortie
Toutes les variables de la boucle `product` sont disponibles.
|Variable |Description |
|--- |--- |
|$SOLD_QUANTITY | La quantité de produit vendue sur la période considérée |
|$SOLD_AMOUNT | Le montant total HT des ventes sur la période considérée |
|$SALE_RATIO | Le pourcentage du CA sur la période considérée |
### Exemple
Pour obtenir vos 10 meilleures ventes de tous les temps :
<ul>
{loop type="best_selling_products" name="best-sellers" limit=10 order='sold_count_reverse'}
<li>{$REF} : {$TITLE} : {$SOLD_QUANTITY}</li>
{/loop}
</ul>
Pour obtenir les 5 meilleures ventes du mois :
<ul>
{loop type="best_selling_products" name="best-sellers-this-month" order='sold_count_reverse' start_date={$smarty.now|date_format:'%Y-%m-01'} limit=5}
<li>{$REF} : {$TITLE} : {$SOLD_QUANTITY}</li>
{/loop}
</ul>
Pour obtenir vos 10 pires ventes de tous les temps :
<ul>
{loop type="best_selling_products" name="best-sellers" limit=10 order='sold_count'}
<li>{$REF} : {$TITLE} : {$SOLD_QUANTITY}</li>
{/loop}
</ul>

View File

@@ -0,0 +1,11 @@
{
"name": "cqfdev/best-sellers-module",
"license": "LGPL-3.0-or-later",
"type": "thelia-module",
"require": {
"thelia/installer": "~1.1"
},
"extra": {
"installer-name": "BestSellers"
}
}

View File

@@ -0,0 +1,149 @@
{extends file="admin-layout.tpl"}
{block name="no-return-functions"}
{$admin_current_location = 'tools'}
{$page = $smarty.get.page|default:1}
{$order = $smarty.get.order|default:'sold_count_reverse'}
{$maxYear = $smarty.now|date_format:'%Y'}
{$minYear = $maxYear}
{loop type="order" name="first-order" customer="*" order="create-date" limit=1}
{$minYear = {format_date date=$CREATE_DATE format="Y"}}
{/loop}
{$startDate = $smarty.get.startDate|default:"$minYear-01-01"}
{$endDate = $smarty.get.endDate|default:$smarty.now|date_format:'%Y-%m-%d'}
{/block}
{block name="page-title"}{intl l='Best sellers' d='bestsellers.bo.default'}{/block}
{block name="check-resource"}admin.bestsellers{/block}
{block name="check-access"}view{/block}
{block name="main-content"}
<div id="wrapper" class="container">
<div class="row">
<div class="col-md-12">
<div class="general-block-decorator">
<form class="form-inline" action="{url path="/admin/best-sellers"}" method="GET" style="margin-bottom: 20px;">
<input type="hidden" name="order" value="{$order}">
<div class="form-group">
<label for="start-date">{intl l='View from : ' d='bestsellers.bo.default'}</label>
<input id="start-date" class="form-control datecombo" data-format="YYYY-MM-DD" data-template="DD / MM / YYYY" name="startDate" value="{$startDate}" type="text">
</div>
<div class="form-group">
<label for="end-date">{intl l=' to : ' d='bestsellers.bo.default'}</label>
<input id="end-date" class="form-control datecombo" data-format="YYYY-MM-DD" data-template="DD / MM / YYYY" name="endDate" value="{$endDate}" type="text">
</div>
<button type="submit" class="btn btn-primary">{intl l='View' d='bestsellers.bo.default'}</button>
</form>
{ifloop rel="sales-count"}
<table class="table table-bordered table-condensed table-hover">
<thead>
<tr>
<th>
{admin_sortable_header
current_order=$order
order='ref'
reverse_order='ref_reverse'
path="/admin/best-sellers?startDate=$startDate&endDate=$endDate"
label={intl l="Product reference" d='bestsellers.bo.default'}
}
</th>
<th>
{admin_sortable_header
current_order=$order
order='alpha'
reverse_order='alpha_reverse'
path="/admin/best-sellers?startDate=$startDate&endDate=$endDate"
label={intl l="Product title" d='bestsellers.bo.default'}
}
</th>
<th class="text-right">
{admin_sortable_header
current_order=$order
order='sold_count'
reverse_order='sold_count_reverse'
path="/admin/best-sellers?startDate=$startDate&endDate=$endDate"
label={intl l="Sales total" d='bestsellers.bo.default'}
}
</th>
<th class="text-right">
{admin_sortable_header
current_order=$order
order='sold_amount'
reverse_order='sold_amount_reverse'
path="/admin/best-sellers?startDate=$startDate&endDate=$endDate"
label={intl l="Total amount" d='bestsellers.bo.default'}
}
</th>
<th class="text-right">
{admin_sortable_header
current_order=$order
order='sale_ratio'
reverse_order='sale_ratio_reverse'
path="/admin/best-sellers?startDate=$startDate&endDate=$endDate"
label={intl l="Sale ratio" d='bestsellers.bo.default'}
}
</th>
</tr>
</thead>
<tbody>
{loop type="best_selling_products" name="sales-count" backend_context=true visible='*' order=$order limit=20 page=$page start_date=$startDate end_date=$endDate return_url=false}
<tr>
<td><a href="{url path="/admin/products/update" product_id=$ID}">{$REF}</a></td>
<td><a href="{url path="/admin/products/update" product_id=$ID}">{$TITLE}</a></td>
<td class="text-right">{$SOLD_QUANTITY|round}</td>
<td class="text-right">{format_money number=$SOLD_AMOUNT}</td>
<td class="text-right">{$SALE_RATIO|string_format:"%.2f"}%</td>
</tr>
{/loop}
<tfoot>
<tr>
<td colspan="100">
{include
file = "includes/pagination.html"
loop_ref = "sales-count"
max_page_count = 10
page_url = {url path="/admin/best-sellers" order=$order startDate=$startDate endDate=$endDate}
}
</td>
</tr>
</tfoot>
</table>
{/ifloop}
{elseloop rel="sales-count"}
<div class="alert alert-info">
{intl l="You have not sold any products yet" d='bestsellers.bo.default'}
</div>
{/elseloop}
</div>
</div>
</div>
</div>
{/block}
{block name="javascript-initialization"}
<script src="{javascript file="/assets/js/moment-with-locales.min.js"}"></script>
<script src="{javascript file="/assets/js/bootstrap-editable/bootstrap-editable.js"}"></script>
<script>
$(function(){
$('.datecombo').combodate({
minYear: {$minYear},
maxYear: {$maxYear},
firstItem: 'none'
});
$('select', '.combodate').addClass('form-control');
})
</script>
{/block}

View File

@@ -0,0 +1,14 @@
{loop type="best_selling_products" name="sales-count" backend_context=true visible='*' id=$product_id return_url=false}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{intl d='bestsellers.bo.default' l="Statistics"}</h3>
</div>
<div class="panel-body">
<ul>
<li>{intl d='bestsellers.bo.default' l="Total sales for this product: <strong>%count</strong>" count=$SOLD_QUANTITY|round}</li>
<li>{intl d='bestsellers.bo.default' l="Sold amount for this product: <strong>%count</strong>" count={format_money number=$SOLD_AMOUNT}}</li>
<li>{intl d='bestsellers.bo.default' l="% of sales: <strong>%count%</strong>" count=$SALE_RATIO|string_format:"%.2f"}</li>
</ul>
</div>
</div>
{/loop}

View File

@@ -0,0 +1,15 @@
{ifloop rel="best-selling"}
<section id="best-selling-products" class="grid">
<div class="products-heading">
<h2>{intl l="Best sellers" d="bestsellers.fo.default"}</h2>
</div>
<div class="products-content">
<ul class="products-grid list-unstyled row">
{loop name="best-selling" type="best_selling_products" limit="4" order="sold_count_reverse"}
{include file="includes/single-product.html" colClass="col-md-3 col-sm-4" product_id=$ID hasBtn=false hasDescription=false width="218" height="146"}
{/loop}
</ul>
</div>
</section>
{/ifloop}