Inital commit
This commit is contained in:
26
local/modules/BestSellers/BestSellers.php
Normal file
26
local/modules/BestSellers/BestSellers.php
Normal 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;
|
||||
}
|
||||
28
local/modules/BestSellers/Config/config.xml
Normal file
28
local/modules/BestSellers/Config/config.xml
Normal 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>
|
||||
31
local/modules/BestSellers/Config/module.xml
Normal file
31
local/modules/BestSellers/Config/module.xml
Normal 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>
|
||||
6
local/modules/BestSellers/Config/routing.xml
Normal file
6
local/modules/BestSellers/Config/routing.xml
Normal 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>
|
||||
116
local/modules/BestSellers/EventListeners/BestSellersEvent.php
Normal file
116
local/modules/BestSellers/EventListeners/BestSellersEvent.php
Normal 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;
|
||||
}
|
||||
}
|
||||
129
local/modules/BestSellers/EventListeners/EventManager.php
Normal file
129
local/modules/BestSellers/EventListeners/EventManager.php
Normal 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.
|
||||
}
|
||||
}
|
||||
}
|
||||
39
local/modules/BestSellers/Hook/HookManager.php
Normal file
39
local/modules/BestSellers/Hook/HookManager.php
Normal 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)
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
18
local/modules/BestSellers/I18n/backOffice/default/en_US.php
Normal file
18
local/modules/BestSellers/I18n/backOffice/default/en_US.php
Normal 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',
|
||||
);
|
||||
18
local/modules/BestSellers/I18n/backOffice/default/fr_FR.php
Normal file
18
local/modules/BestSellers/I18n/backOffice/default/fr_FR.php
Normal 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.',
|
||||
);
|
||||
5
local/modules/BestSellers/I18n/en_US.php
Normal file
5
local/modules/BestSellers/I18n/en_US.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return array(
|
||||
'Best sellers' => 'Best sellers',
|
||||
);
|
||||
5
local/modules/BestSellers/I18n/fr_FR.php
Normal file
5
local/modules/BestSellers/I18n/fr_FR.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return array(
|
||||
'Best sellers' => 'Meilleures ventes',
|
||||
);
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return array(
|
||||
'Best sellers' => 'Our best sellers',
|
||||
);
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
return array(
|
||||
'Best sellers' => 'Nos meilleures ventes',
|
||||
);
|
||||
146
local/modules/BestSellers/Loop/BestSellerLoop.php
Normal file
146
local/modules/BestSellers/Loop/BestSellerLoop.php
Normal 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'))
|
||||
;
|
||||
}
|
||||
}
|
||||
164
local/modules/BestSellers/Readme.md
Normal file
164
local/modules/BestSellers/Readme.md
Normal 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>
|
||||
11
local/modules/BestSellers/composer.json
Normal file
11
local/modules/BestSellers/composer.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
Reference in New Issue
Block a user