first commit
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Created by PhpStorm.
|
||||
* User: ondra
|
||||
* Date: 22.11.17
|
||||
* Time: 13:14.
|
||||
*/
|
||||
|
||||
namespace KupShop\QuantityDiscountBundle\Admin\Tabs;
|
||||
|
||||
use KupShop\AdminBundle\Admin\WindowTab;
|
||||
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
|
||||
use KupShop\QuantityDiscountBundle\Util\QuantityDiscountUtil;
|
||||
use Query\Operator;
|
||||
|
||||
class quantityDiscountWindowTab extends WindowTab
|
||||
{
|
||||
protected $title = 'flapQuantityDiscount';
|
||||
|
||||
protected $template = 'quantityDiscount.tpl';
|
||||
|
||||
public static function getTypes()
|
||||
{
|
||||
return [
|
||||
'products' => 1,
|
||||
];
|
||||
}
|
||||
|
||||
public function getVars($smarty_tpl_vars)
|
||||
{
|
||||
$ID = $this->getID();
|
||||
|
||||
$quantityDiscountUtil = ServiceContainer::getService(QuantityDiscountUtil::class);
|
||||
|
||||
$groups = $quantityDiscountUtil->getGroups();
|
||||
|
||||
$discounts = [];
|
||||
foreach ($groups as $groupId => $group) {
|
||||
$discounts[$groupId] = [];
|
||||
}
|
||||
|
||||
$qb = sqlQueryBuilder()
|
||||
->select('*')
|
||||
->from('products_quantity_discounts')
|
||||
->where(Operator::equals(['id_product' => $ID]))
|
||||
->orderBy('pieces, id_variation', 'ASC');
|
||||
|
||||
foreach ($qb->execute() as $discount) {
|
||||
$discounts[$discount['id_group'] ?? 0][$discount['id']] = $discount;
|
||||
}
|
||||
|
||||
$data['groups'] = $groups;
|
||||
$data['discounts'] = $discounts;
|
||||
$data['discounts_types'] = $quantityDiscountUtil->getDiscountTypes();
|
||||
|
||||
$data['variations'] = sqlFetchAll(
|
||||
sqlQueryBuilder()
|
||||
->select('id, title')
|
||||
->from('products_variations')
|
||||
->where(Operator::equals(['id_product' => $ID]))
|
||||
->execute(),
|
||||
['id' => 'title']
|
||||
);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function handleUpdate()
|
||||
{
|
||||
parent::handleUpdate();
|
||||
|
||||
$ID = $this->getID();
|
||||
|
||||
$data = $this->getData();
|
||||
|
||||
foreach ($data as $group => $discounts) {
|
||||
[$_, $groupId] = explode('-', $group);
|
||||
foreach ($discounts as $key => $discount) {
|
||||
if ($key == 0 || ($key < 0 && ($discount['delete'] ?? false) == 'on')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// delete
|
||||
if (($discount['delete'] ?? false) == 'on' && $key > 0) {
|
||||
sqlQueryBuilder()
|
||||
->delete('products_quantity_discounts')
|
||||
->where(Operator::equals(['id' => $key]))
|
||||
->execute();
|
||||
continue;
|
||||
}
|
||||
|
||||
$variationId = empty($discount['variation']) ? null : $discount['variation'];
|
||||
|
||||
$discountValue = toDecimal($this->preparePrice($discount['discount']));
|
||||
if ($discount['discount_type'] !== QuantityDiscountUtil::DISCOUNT_TYPE_PERC && $discount['with_vat']) {
|
||||
$discountValue = $discountValue->div(toDecimal(1)->add(toDecimal($discount['vat'])->div(toDecimal(100))));
|
||||
}
|
||||
|
||||
// update
|
||||
if ($key > 0) {
|
||||
sqlQueryBuilder()
|
||||
->update('products_quantity_discounts')
|
||||
->directValues(
|
||||
[
|
||||
'id_variation' => $variationId,
|
||||
'pieces' => $discount['pieces'],
|
||||
'discount' => $discountValue,
|
||||
'discount_type' => $discount['discount_type'],
|
||||
]
|
||||
)
|
||||
->where(Operator::equals(['id' => $key]))
|
||||
->execute();
|
||||
} else {
|
||||
sqlQueryBuilder()
|
||||
->insert('products_quantity_discounts')
|
||||
->directValues(
|
||||
[
|
||||
'id_group' => $groupId == 0 ? null : $groupId,
|
||||
'id_product' => $ID,
|
||||
'id_variation' => $variationId,
|
||||
'pieces' => $discount['pieces'],
|
||||
'discount' => $discountValue,
|
||||
'discount_type' => $discount['discount_type'],
|
||||
]
|
||||
)
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
$txt_str['quantityDiscountWindowTab'] = [
|
||||
'flapQuantityDiscount' => 'Množstevní sleva',
|
||||
];
|
||||
@@ -0,0 +1,193 @@
|
||||
<div id="flapQuantityDiscount" class="tab-pane fade in boxFlex">
|
||||
<h1 class="h4 main-panel-title">Množstevní sleva</h1>
|
||||
|
||||
{get_contexts currency=1 assign='contexts'}
|
||||
|
||||
<div id="{$tab.prefix}">
|
||||
|
||||
<div class="panel-group panel-group-lists ui-sortable">
|
||||
|
||||
{* TODO: upravit GUI, aby se pridavala jedna velka skupina, kde se vybere Skupina + Varianta a pod tu se potom budou pridavat jednotlive slevy *}
|
||||
{foreach $tab.data.discounts as $groupId => $discounts}
|
||||
<div style="margin-bottom:20px">
|
||||
<div class="panel">
|
||||
<div class="panel-heading">
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
<p class="input-height">Skupina <strong>{$tab.data.groups[$groupId]} (ID: {$groupId})</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel-group panel-group-lists panel">
|
||||
<div class="panel-body border" style="border:1px solid rgba(0, 0, 0, 0.1);border-top:none;border-radius:3px;">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h1 class="h6">Množstevní slevy</h1 class="h5">
|
||||
</div>
|
||||
</div>
|
||||
<!-- Discount rows -->
|
||||
<div id="discountsRowForm_{$groupId}">
|
||||
{foreach [[]] + $discounts as $discountId => $discount}
|
||||
<div id="discountRow_{$groupId}" {if $discountId} data-form-item{else}data-form-new style="display:none"{/if}>
|
||||
<div class="row">
|
||||
<div class="form-group">
|
||||
<div class="col-md-1 control-label"><label>Od</label></div>
|
||||
<div class="col-md-2">
|
||||
<div class="input-group">
|
||||
<input class="form-control input-sm" name="{$tab.prefix}[group-{$groupId}][{$discountId}][pieces]"
|
||||
value="{$discount.pieces}" type="text">
|
||||
<span class="input-group-addon">ks</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-1 control-label"><label>Sleva</label></div>
|
||||
<div class="col-md-4">
|
||||
<div class="input-group discountDropdown " id="discountDropdown_{$groupId}_{$discountId}">
|
||||
<span class="input-group-addon" style="padding: 0px; border: 0px; min-width: 75px;">
|
||||
<select name="{$tab.prefix}[group-{$groupId}][{$discountId}][discount_type]"
|
||||
class="selecter" data-discount_type>
|
||||
{foreach $tab.data.discounts_types as $key=>$value}
|
||||
<option value="{$key}" {if $discount.discount_type==$key}selected{/if}>{$value}</option>
|
||||
{/foreach}
|
||||
</select>
|
||||
</span>
|
||||
<input type="text" class="form-control input-sm"
|
||||
name="{$tab.prefix}[group-{$groupId}][{$discountId}][discount]" data-price value="{$discount.discount|format_editable_number}"
|
||||
maxlength="20"
|
||||
onKeyPress="checkInputData('float');"/>
|
||||
<input type="hidden" name="{$tab.prefix}[group-{$groupId}][{$discountId}][with_vat]"
|
||||
data-price-vat value="">
|
||||
<input type="hidden" name="{$tab.prefix}[group-{$groupId}][{$discountId}][vat]" data-vat
|
||||
value="{getVat($body.data.vat)}">
|
||||
<div class="input-group-btn" data-dropdown="" style="{if $discount.discount_type=='perc'}display:none{/if}" data-discount_type_price>
|
||||
<a class="btn btn-primary dropdown-toggle btn-sm" data-toggle="dropdown">
|
||||
<span data-dropdown-state=""></span>
|
||||
<span class="caret"></span>
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a data-value="1">{'withTax'|translate:'choice'}</a></li>
|
||||
<li><a data-value="0">{'withoutTax'|translate:'choice'}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
priceVatDropdown($('#discountDropdown_{$groupId}_{$discountId}'), 0);
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{if !empty($tab.data.variations)}
|
||||
<div class="col-md-1 control-label"><label>Varianta</label></div>
|
||||
<div class="col-md-2">
|
||||
<select class="selecter" name="{$tab.prefix}[group-{$groupId}][{$discountId}][variation]">
|
||||
<option value="">-- Všechny varianty --</option>
|
||||
{foreach $tab.data.variations as $variationId => $variationTitle}
|
||||
<option value="{$variationId}" {if $discount.id_variation == $variationId}selected{/if}>{$variationTitle}</option>
|
||||
{/foreach}
|
||||
</select>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="col-md-1">
|
||||
<a class="btn-sm btn btn-danger" data-form-delete>
|
||||
<input class="hidden" type="checkbox" name="{$tab.prefix}[group-{$groupId}][{$discountId}][delete]"/>
|
||||
<span class="glyphicon glyphicon-remove"></span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/foreach}
|
||||
<div class="row bottom-space">
|
||||
<div class="col-md-3">
|
||||
<a href="#" data-form-add="">
|
||||
<span class="glyphicon glyphicon-plus"></span> Přidat slevu
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="application/javascript">
|
||||
initForm({
|
||||
selector: '#discountsRowForm_{$groupId}',
|
||||
beforeAdd: function (original) {
|
||||
var $item = original();
|
||||
|
||||
$item.find('.discountDropdown').each(function () {
|
||||
priceVatDropdown($(this),{($dbcfg.prod_prefer_price_vat == 'Y') ? 1 : 0})
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/foreach}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="application/javascript">
|
||||
{block initForm}
|
||||
initForm({
|
||||
selector:'#{$tab.prefix}'
|
||||
});
|
||||
{/block}
|
||||
|
||||
function copyDiscountRow(groupId, event) {
|
||||
if (groupId == 0) {
|
||||
groupId = getNewGroupId(event);
|
||||
}
|
||||
|
||||
var item = $("#discountRowNew");
|
||||
unstyleFormInputs(item);
|
||||
var newItem = item.clone();
|
||||
|
||||
newItem.find('[data-pieces]').attr('name', '{$tab.prefix}[' + groupId + '][pieces][]');
|
||||
newItem.find('[data-discount]').attr('name', '{$tab.prefix}[' + groupId + '][discount][]');
|
||||
newItem.find('[data-discount_type]').attr('name', '{$tab.prefix}[' + groupId + '][discount_type][]');
|
||||
newItem.find('[data-variation]').attr('name', '{$tab.prefix}[' + groupId + '][variation][]');
|
||||
|
||||
item.after(newItem);
|
||||
|
||||
newItem.slideDown();
|
||||
styleFormInputs(newItem);
|
||||
|
||||
if (groupId > 0) {
|
||||
newItem.insertAfter("#discountRow_" + regionId);
|
||||
} else {
|
||||
var parentRow = event.target;
|
||||
parentRow = parentRow.closest("div.row");
|
||||
newItem.insertBefore(parentRow);
|
||||
newItem.attr('id', 'discountRow');
|
||||
}
|
||||
}
|
||||
|
||||
function getNewGroupId(event) {
|
||||
var id = 0;
|
||||
var item = event.target;
|
||||
item = item.closest("div.panel-body");
|
||||
if (item.getElementsByTagName("input").length > 0) {
|
||||
var input_name = item.getElementsByTagName("input")[0].name;
|
||||
input_name = input_name.replace('{$tab.prefix}[', '');
|
||||
input_name = input_name.substr(0, input_name.indexOf(']'));
|
||||
id = parseInt(input_name);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
$('#quantityDiscountWindowTab').on('change update', 'select[data-discount_type]', function (e) {
|
||||
$(this).parents('.discountDropdown').find('[data-discount_type_price]').toggle(!($(this).val() === 'perc'));
|
||||
});
|
||||
$('select[data-discount_type]').each(function () {
|
||||
if (!($(this).val() === 'perc')) {
|
||||
$(this).parents('.discountDropdown').each(function () {
|
||||
priceVatDropdown($(this),{($dbcfg.prod_prefer_price_vat == 'Y') ? 1 : 0})
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\QuantityDiscountBundle\Context;
|
||||
|
||||
class QuantityDiscountContext
|
||||
{
|
||||
/** @var bool|int */
|
||||
protected $active = true;
|
||||
|
||||
/**
|
||||
* @return bool|int
|
||||
*/
|
||||
public function getActive()
|
||||
{
|
||||
return $this->active;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool|int|null
|
||||
*/
|
||||
public function setActive($active)
|
||||
{
|
||||
$this->active = $active;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Created by PhpStorm.
|
||||
* User: ondra
|
||||
* Date: 28.11.17
|
||||
* Time: 15:45.
|
||||
*/
|
||||
|
||||
namespace KupShop\QuantityDiscountBundle\EventListener;
|
||||
|
||||
use KupShop\I18nBundle\Util\PriceConverter;
|
||||
use KupShop\KupShopBundle\Context\CurrencyContext;
|
||||
use KupShop\KupShopBundle\Util\Contexts;
|
||||
use KupShop\OrderingBundle\Event\OrderItemEvent;
|
||||
use KupShop\QuantityDiscountBundle\Util\QuantityDiscountPrice;
|
||||
use KupShop\QuantityDiscountBundle\Util\QuantityDiscountUtil;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class OrderItemListener implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private QuantityDiscountPrice $quantityDiscountPrice,
|
||||
private ?PriceConverter $priceConverter,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
OrderItemEvent::CALCULATE_PRICE => [
|
||||
['setPrice', 200],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function setPrice(OrderItemEvent $event)
|
||||
{
|
||||
$value = $event->getPrice();
|
||||
|
||||
if ($discount = $this->quantityDiscountPrice->getDiscount($event->getProduct()->id, toDecimal($event->getPieces()), $event->getVariationId())) {
|
||||
if ($discount['discount_type'] == QuantityDiscountUtil::DISCOUNT_TYPE_PERC) {
|
||||
$value = $value->addDiscount($discount['discount']);
|
||||
} else {
|
||||
// $value is always in default currency when CALCULATE_PRICE event is triggered, so convert discount value to default currency
|
||||
$discountValue = $this->priceConverter?->convert(
|
||||
$discount['discount_type'],
|
||||
Contexts::get(CurrencyContext::class)->getDefault(),
|
||||
$discount['discount']
|
||||
) ?: toDecimal($discount['discount']);
|
||||
|
||||
$value = $value->sub($discountValue);
|
||||
}
|
||||
}
|
||||
|
||||
$event->setPrice($value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Created by PhpStorm.
|
||||
* User: ondra
|
||||
* Date: 22.11.17
|
||||
* Time: 13:10.
|
||||
*/
|
||||
|
||||
namespace KupShop\QuantityDiscountBundle;
|
||||
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
|
||||
class QuantityDiscountBundle extends Bundle
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KupShop\QuantityDiscountBundle\Query;
|
||||
|
||||
use Query\Operator;
|
||||
|
||||
class QuantityDiscount
|
||||
{
|
||||
public static function byGroup(bool|int $groupId, ?string $alias = null): callable
|
||||
{
|
||||
$field = 'id_group';
|
||||
|
||||
if ($alias) {
|
||||
$field = $alias.'.'.$field;
|
||||
}
|
||||
|
||||
if (!is_bool($groupId)) {
|
||||
return Operator::equals([$field => $groupId]);
|
||||
}
|
||||
|
||||
return Operator::equalsNullable([$field => null]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
twig_component:
|
||||
defaults:
|
||||
KupShop\QuantityDiscountBundle\Twig\Components\: '@QuantityDiscount/components/'
|
||||
|
||||
services:
|
||||
_defaults:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
KupShop\QuantityDiscountBundle\:
|
||||
resource: ../../{Twig}
|
||||
@@ -0,0 +1,15 @@
|
||||
services:
|
||||
KupShop\QuantityDiscountBundle\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: ../../{Context,Util}
|
||||
|
||||
KupShop\QuantityDiscountBundle\Admin\Tabs\:
|
||||
resource: ../../Admin/Tabs/*.php
|
||||
autoconfigure: true
|
||||
|
||||
KupShop\QuantityDiscountBundle\EventListener\OrderItemListener:
|
||||
class: KupShop\QuantityDiscountBundle\EventListener\OrderItemListener
|
||||
autowire: true
|
||||
tags:
|
||||
- { name: kernel.event_subscriber }
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
|
||||
use KupShop\KupShopBundle\Util\Price\PriceCalculator;
|
||||
use KupShop\KupShopBundle\Wrapper\PriceWrapper;
|
||||
use KupShop\QuantityDiscountBundle\Context\QuantityDiscountContext;
|
||||
use KupShop\QuantityDiscountBundle\Query\QuantityDiscount;
|
||||
use KupShop\QuantityDiscountBundle\Util\QuantityDiscountPrice;
|
||||
use Query\Operator;
|
||||
|
||||
function smarty_function_get_quantity_discounts($params, &$smarty)
|
||||
{
|
||||
if (!isset($params['id_product'])) {
|
||||
throw new InvalidArgumentException("Parameter 'id_product' is missing!");
|
||||
}
|
||||
|
||||
$newData = [];
|
||||
|
||||
$quantityDiscountContext = ServiceContainer::getService(QuantityDiscountContext::class);
|
||||
if ($active = $quantityDiscountContext->getActive()) {
|
||||
$qb = sqlQueryBuilder()
|
||||
->select('pieces, discount, discount_type')
|
||||
->from('products_quantity_discounts')
|
||||
->where(\Query\Operator::equals(['id_product' => $params['id_product']]))
|
||||
->orderBy('pieces', 'ASC');
|
||||
|
||||
$orX = ['id_variation IS NULL'];
|
||||
if ($params['id_variation'] ?? null) {
|
||||
$orX[] = Operator::equals(['id_variation' => $params['id_variation']]);
|
||||
}
|
||||
|
||||
$qb->andWhere(Operator::orX($orX));
|
||||
$qb->andWhere(QuantityDiscount::byGroup($active));
|
||||
|
||||
$data = $qb->execute();
|
||||
|
||||
$quantityDiscountPrice = ServiceContainer::getService(QuantityDiscountPrice::class);
|
||||
|
||||
$product = new Product($params['id_product']);
|
||||
$product->createFromDB();
|
||||
|
||||
$price = $priceBase = $quantityDiscountPrice->getPrice($params['id_product'], 1, $params['id_variation'] ?? null);
|
||||
|
||||
$newData[1] = [
|
||||
'pieces_from' => 1,
|
||||
'pieces_to' => null,
|
||||
'discount' => toDecimal(0),
|
||||
'discount_value' => toDecimal(0),
|
||||
'discount_type' => null,
|
||||
'price' => PriceWrapper::wrap($price),
|
||||
];
|
||||
$lastItem = &$newData[1];
|
||||
|
||||
foreach ($data as $discount) {
|
||||
$price = $quantityDiscountPrice->getPrice($params['id_product'], $discount['pieces'], $params['id_variation'] ?? null);
|
||||
|
||||
$newData[$discount['pieces']] = [
|
||||
'pieces_from' => $discount['pieces'],
|
||||
'pieces_to' => null,
|
||||
'discount' => toDecimal($discount['discount']),
|
||||
'discount_value' => PriceWrapper::wrap(PriceCalculator::sub($priceBase, $price)),
|
||||
'discount_type' => $discount['discount_type'],
|
||||
'price' => PriceWrapper::wrap($price),
|
||||
];
|
||||
|
||||
$lastItem['pieces_to'] = $discount['pieces'] - 1;
|
||||
$lastItem = &$newData[$discount['pieces']];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($params['assign'])) {
|
||||
$smarty->assign($params['assign'], $newData);
|
||||
} else {
|
||||
return $newData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{get_quantity_discounts id_product={$body.product.id} assign='quantityDiscounts'}
|
||||
|
||||
{if $quantityDiscounts}
|
||||
<div data-variations="variations" class="variations-wrapper p-y-2 m-t-2">
|
||||
|
||||
<h4>Slevy při odběru více kusů</h4>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="product-variations display-table">
|
||||
{$p=0}
|
||||
|
||||
{$quantityDiscountsCount = count($quantityDiscounts)}
|
||||
{foreach $quantityDiscounts as $quantityDiscount}
|
||||
{$p=$p+1}
|
||||
|
||||
{$discount = $quantityDiscount.discount}
|
||||
<label class="display-row custom-control custom-checkbox product-variations-item"
|
||||
data-variation-price="{*{$variation.price|format_price}*}"
|
||||
data-variation-weight="{*{$variation.weight|format_float:0} kg*}"
|
||||
data-variation-price-per-unit="{*{($variation.price.value_with_vat->asFloat()/$variation.weight)|format_float:2} Kč / kg*}"
|
||||
data-variation-delivery-time="{*{$variation.delivery_time}*}"
|
||||
data-variation-delivery-class="{if $variation.in_store > 0}icons_in_store_checked inStore{/if}">
|
||||
|
||||
<span class="var-title display-cell">
|
||||
<b>{$quantityDiscount.pieces_from}{if $quantityDiscount.pieces_to} - {$quantityDiscount.pieces_to} ks{else} a více{/if}</b> {$body.product.title}
|
||||
</span>
|
||||
|
||||
|
||||
<span class="display-cell var-discount hidden-md-down">
|
||||
{if $discount->isPositive()}
|
||||
<span class="discount">
|
||||
-{$discount->asFloat()|round}%
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
|
||||
<span class="display-cell var-price-pet-unit">
|
||||
{*{($variation.price.value_with_vat->asFloat()/$variation.weight)|format_float:2} Kč / kg*}
|
||||
</span>
|
||||
|
||||
<strong class="display-cell var-price" data-variation-price>
|
||||
{$quantityDiscount.price.value_with_vat|format_price} / ks
|
||||
</strong>
|
||||
|
||||
</label>
|
||||
{/foreach}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Created by PhpStorm.
|
||||
* User: ondra
|
||||
* Date: 22.11.17
|
||||
* Time: 13:49.
|
||||
*/
|
||||
|
||||
namespace KupShop\QuantityDiscountBundle\Resources\upgrade;
|
||||
|
||||
use KupShop\KupShopBundle\Context\CurrencyContext;
|
||||
use KupShop\KupShopBundle\Util\Contexts;
|
||||
use Query\Operator;
|
||||
|
||||
class QuantityDiscountUpgrade extends \UpgradeNew
|
||||
{
|
||||
public function check_quantityDiscounts()
|
||||
{
|
||||
return $this->checkTableExists('products_quantity_discounts');
|
||||
}
|
||||
|
||||
/** Create table products_quantity_discounts */
|
||||
public function upgrade_quantityDiscounts()
|
||||
{
|
||||
sqlQuery('CREATE TABLE IF NOT EXISTS products_quantity_discounts (
|
||||
id INT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
id_product INT(11) NOT NULL,
|
||||
pieces INT(11) NOT NULL,
|
||||
discount NUMERIC(12, 8) NOT NULL,
|
||||
CONSTRAINT FK_id_product FOREIGN KEY (id_product) REFERENCES products (id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;');
|
||||
|
||||
$this->upgradeOK();
|
||||
}
|
||||
|
||||
public function check_quantityDiscountGroupId(): bool
|
||||
{
|
||||
return $this->checkColumnExists('products_quantity_discounts', 'id_group');
|
||||
}
|
||||
|
||||
/** products_quantity_discounts: add column 'id_group' */
|
||||
public function upgrade_quantityDiscountGroupId(): void
|
||||
{
|
||||
sqlQuery('ALTER TABLE products_quantity_discounts ADD COLUMN id_group INT(11) DEFAULT NULL AFTER id');
|
||||
sqlQuery('ALTER TABLE products_quantity_discounts ADD INDEX id_group_index (`id_group`)');
|
||||
|
||||
$this->upgradeOK();
|
||||
}
|
||||
|
||||
public function check_IdProductOnUpdateCascade(): bool
|
||||
{
|
||||
return $this->checkConstraintRule('products_quantity_discounts', 'FK_id_product', null, 'CASCADE');
|
||||
}
|
||||
|
||||
/** products_quantity_discounts: id_product - add missing on update cascade */
|
||||
public function upgrade_IdProductOnUpdateCascade(): void
|
||||
{
|
||||
sqlQuery('alter table products_quantity_discounts drop foreign key FK_id_product;');
|
||||
|
||||
sqlQuery('alter table products_quantity_discounts
|
||||
add constraint FK_id_product
|
||||
foreign key (id_product) references products (id)
|
||||
on update cascade on delete cascade;');
|
||||
|
||||
$this->upgradeOK();
|
||||
}
|
||||
|
||||
public function check_quantityDiscountVariationId(): bool
|
||||
{
|
||||
return $this->checkColumnExists('products_quantity_discounts', 'id_variation');
|
||||
}
|
||||
|
||||
/** products_quantity_discounts: add column 'id_variation' */
|
||||
public function upgrade_quantityDiscountVariationId(): void
|
||||
{
|
||||
sqlQuery('ALTER TABLE products_quantity_discounts ADD COLUMN id_variation INT(11) DEFAULT NULL AFTER id_product');
|
||||
sqlQuery('ALTER TABLE products_quantity_discounts ADD CONSTRAINT quantity_discounts_id_variation FOREIGN KEY (id_variation) REFERENCES products_variations (id) ON DELETE CASCADE ON UPDATE CASCADE');
|
||||
|
||||
$this->upgradeOK();
|
||||
}
|
||||
|
||||
public function check_quantityDiscountDiscountType(): bool
|
||||
{
|
||||
return $this->checkColumnExists('products_quantity_discounts', 'discount_type');
|
||||
}
|
||||
|
||||
/** products_quantity_discounts: add column 'discount_type' */
|
||||
public function upgrade_quantityDiscountDiscountType(): void
|
||||
{
|
||||
sqlQuery("ALTER TABLE products_quantity_discounts ADD COLUMN discount_type VARCHAR(10) NOT NULL DEFAULT 'perc'");
|
||||
|
||||
$this->upgradeOK();
|
||||
}
|
||||
|
||||
public function check_QuantityDiscountsDiscountTypeChange(): bool
|
||||
{
|
||||
return sqlQueryBuilder()
|
||||
->select('discount_type')
|
||||
->from('products_quantity_discounts')
|
||||
->where(Operator::equals(['discount_type' => 'amount']))
|
||||
->execute()->rowCount() > 0;
|
||||
}
|
||||
|
||||
/** Quantity discounts: use currency as discount_type instead of `amount` keyword */
|
||||
public function upgrade_QuantityDiscountsDiscountTypeChange(): void
|
||||
{
|
||||
sqlQueryBuilder()
|
||||
->update('products_quantity_discounts')
|
||||
->directValues(['discount_type' => Contexts::get(CurrencyContext::class)->getDefaultId()])
|
||||
->where(Operator::equals(['discount_type' => 'amount']))
|
||||
->execute();
|
||||
|
||||
$this->upgradeOK();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<div {{ attributes }}>
|
||||
<div class="discount">
|
||||
{% if this.discount.discount >= 1 %}
|
||||
{% if this.discount.discount_type == 'perc' %}
|
||||
<span>{{ this.discount.discount|round }}%</span>
|
||||
{% else %}
|
||||
<span>-{{ discount.discount_value|formatPrice(withVat: true, ceil: false, decimal: 'dynamic') }}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="pieces">
|
||||
{{ this.discount.pieces_from }}
|
||||
{%- if this.discount.pieces_to %}
|
||||
{%- if this.discount.pieces_to != this.discount.pieces_from -%}
|
||||
-{{ this.discount.pieces_to }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
a více
|
||||
{% endif %}
|
||||
{{ this.discount.unit }}
|
||||
</div>
|
||||
<div class="price">
|
||||
<strong>{{ this.discount.price|formatPrice }}</strong> / {{ this.discount.unit }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,66 @@
|
||||
@use "sass:map";
|
||||
@use "@twig/scss/_global" as global;
|
||||
@use "@twig/scss/variables/_variables-components" as components;
|
||||
|
||||
$params: (
|
||||
"border": global.$border-width solid global.$border-color,
|
||||
"padding": 7px 15px,
|
||||
"margin-bottom": -1px,
|
||||
"font-size": global.$font-size-smaller,
|
||||
"font-weight": global.$font-weight-bold,
|
||||
"text-transform": none,
|
||||
"letter-spacing": 0,
|
||||
"background": global.$discount-color,
|
||||
"color": global.$white,
|
||||
"border-radius": global.$border-radius-base,
|
||||
);
|
||||
|
||||
@if global-variable-exists(c-quantitydiscounts-discount, components) {
|
||||
$keys: map.keys(components.$c-quantitydiscounts-discount);
|
||||
@each $name in $keys {
|
||||
@if not map.get($params, $name) {
|
||||
@warn "Neexistující proměnná '#{$name}' v komponentě '$#{component("QuantityDiscounts:Discount", "class")}'.";
|
||||
}
|
||||
}
|
||||
|
||||
$params: map.merge($params, components.$c-quantitydiscounts-discount);
|
||||
}
|
||||
|
||||
.#{component("QuantityDiscounts:Discount", "class")} {
|
||||
align-items: center;
|
||||
border: map.get($params, "border");
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: map.get($params, "margin-bottom");
|
||||
padding: map.get($params, "padding");
|
||||
position: relative;
|
||||
|
||||
.discount {
|
||||
width: 20%;
|
||||
span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
line-height: 1;
|
||||
font-size: map.get($params, "font-size");
|
||||
font-weight: map.get($params, "font-weight");
|
||||
text-transform: map.get($params, "text-transform");
|
||||
letter-spacing: map.get($params, "letter-spacing");
|
||||
padding: map.get($params, "padding");
|
||||
border-radius: map.get($params, "border-radius");
|
||||
background: map.get($params, "background");
|
||||
color: map.get($params, "color");
|
||||
}
|
||||
}
|
||||
|
||||
.pieces {
|
||||
font-weight: map.get($params, "font-weight");
|
||||
margin-right: 10px;
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.price {
|
||||
margin-left: 10px;
|
||||
text-align: right;
|
||||
width: 30%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{% if this.active and this.shouldShowDiscounts %}
|
||||
<div {{ attributes }}>
|
||||
{% if this.discounts %}
|
||||
<p class="title-default">{{ this.title|default('Nakupte více kusů a ušetřete') }}</p>
|
||||
<div>
|
||||
{% for discount in this.discounts %}
|
||||
<twig:QuantityDiscounts:Discount discount="{{ discount }}"/>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,30 @@
|
||||
@use "sass:map";
|
||||
@use "@twig/scss/_global" as global;
|
||||
@use "@twig/scss/variables/_variables-components" as components;
|
||||
|
||||
$params: (
|
||||
"margin-bottom": global.$spacer,
|
||||
);
|
||||
|
||||
@if global-variable-exists(c-quantitydiscounts-discounts, components) {
|
||||
$keys: map.keys(components.$c-quantitydiscounts-discounts);
|
||||
@each $name in $keys {
|
||||
@if not map.get($params, $name) {
|
||||
@warn "Neexistující proměnná '#{$name}' v komponentě '$#{component("QuantityDiscounts:Discounts", "class")}'.";
|
||||
}
|
||||
}
|
||||
|
||||
$params: map.merge($params, components.$c-quantitydiscounts-discounts);
|
||||
}
|
||||
|
||||
.#{component("QuantityDiscounts:Discounts", "class")} {
|
||||
width: 100%;
|
||||
container-type: inline-size;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
margin-bottom: map.get($params, "margin-bottom");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"products_quantity_discounts": [
|
||||
{
|
||||
"id": 1,
|
||||
"id_group": null,
|
||||
"id_product": 3,
|
||||
"id_variation": null,
|
||||
"pieces": 2,
|
||||
"discount": 5.0000,
|
||||
"discount_type": "perc"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"id_group": null,
|
||||
"id_product": 3,
|
||||
"id_variation": null,
|
||||
"pieces": 5,
|
||||
"discount": 10.0000,
|
||||
"discount_type": "perc"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"id_group": null,
|
||||
"id_product": 3,
|
||||
"id_variation": null,
|
||||
"pieces": 10,
|
||||
"discount": 20.0000,
|
||||
"discount_type": "perc"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"id_group": null,
|
||||
"id_product": 6,
|
||||
"id_variation": null,
|
||||
"pieces": 3,
|
||||
"discount": 20.0000,
|
||||
"discount_type": "perc"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"id_group": 1,
|
||||
"id_product": 3,
|
||||
"id_variation": null,
|
||||
"pieces": 2,
|
||||
"discount": 10.0000,
|
||||
"discount_type": "perc"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"id_group": null,
|
||||
"id_product": 6,
|
||||
"id_variation": 19,
|
||||
"pieces": 3,
|
||||
"discount": 25.0000,
|
||||
"discount_type": "perc"
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"id_group": 1,
|
||||
"id_product": 6,
|
||||
"id_variation": 19,
|
||||
"pieces": 5,
|
||||
"discount": 5.0000,
|
||||
"discount_type": "CZK"
|
||||
},
|
||||
{
|
||||
"id": 8,
|
||||
"id_group": null,
|
||||
"id_product": 6,
|
||||
"id_variation": 19,
|
||||
"pieces": 10,
|
||||
"discount": 50.0000,
|
||||
"discount_type": "CZK"
|
||||
},
|
||||
{
|
||||
"id": 9,
|
||||
"id_group": 1,
|
||||
"id_product": 3,
|
||||
"id_variation": null,
|
||||
"pieces": 20,
|
||||
"discount": 100.0000,
|
||||
"discount_type": "CZK"
|
||||
},
|
||||
{
|
||||
"id": 10,
|
||||
"id_group": null,
|
||||
"id_product": 3,
|
||||
"id_variation": null,
|
||||
"pieces": 25,
|
||||
"discount": 100.0000,
|
||||
"discount_type": "CZK"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"id_group": null,
|
||||
"id_product": 3,
|
||||
"id_variation": null,
|
||||
"pieces": 50,
|
||||
"discount": 6.0000,
|
||||
"discount_type": "EUR"
|
||||
}
|
||||
],
|
||||
"price_levels": [
|
||||
{
|
||||
"id": 1,
|
||||
"name": "B2B",
|
||||
"descr": "",
|
||||
"discount": 0,
|
||||
"discount_discount": null,
|
||||
"unit": "perc",
|
||||
"combine": "N"
|
||||
}
|
||||
],
|
||||
"price_levels_products": [
|
||||
{
|
||||
"id_price_level": 1,
|
||||
"id_product": 3,
|
||||
"discount": 50,
|
||||
"unit": "perc"
|
||||
}
|
||||
],
|
||||
"users_dealer_price_level": [
|
||||
{
|
||||
"id_user": 1,
|
||||
"id_price_level": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Created by PhpStorm.
|
||||
* User: ondra
|
||||
* Date: 23.11.17
|
||||
* Time: 8:08.
|
||||
*/
|
||||
|
||||
namespace KupShop\QuantityDiscountBundle\Tests;
|
||||
|
||||
use KupShop\DevelopmentBundle\Util\Tests\CartTestTrait;
|
||||
use KupShop\QuantityDiscountBundle\Context\QuantityDiscountContext;
|
||||
use KupShop\QuantityDiscountBundle\Util\QuantityDiscountPrice;
|
||||
use KupShop\QuantityDiscountBundle\Util\QuantityDiscountUtil;
|
||||
|
||||
class QuantityDiscountTest extends \DatabaseTestCase
|
||||
{
|
||||
use CartTestTrait;
|
||||
|
||||
private QuantityDiscountUtil $quantityDiscountUtil;
|
||||
private QuantityDiscountPrice $quantityDiscountPrice;
|
||||
private QuantityDiscountContext $quantityDiscountContext;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->quantityDiscountUtil = $this->get(QuantityDiscountUtil::class);
|
||||
$this->quantityDiscountPrice = $this->get(QuantityDiscountPrice::class);
|
||||
$this->quantityDiscountContext = $this->get(QuantityDiscountContext::class);
|
||||
}
|
||||
|
||||
/** @dataProvider data_testAddQuantityDiscount */
|
||||
public function testAddQuantityDiscount(int $productId, int $pieces, \Decimal $value, string $type, string $expectedType): void
|
||||
{
|
||||
$this->quantityDiscountUtil->addQuantityDiscount($productId, null, $pieces, $value, type: $type);
|
||||
|
||||
$discount = $this->quantityDiscountPrice->getDiscount($productId, toDecimal($pieces));
|
||||
|
||||
$this->assertEquals($value, $discount['discount']);
|
||||
$this->assertEquals($expectedType, $discount['discount_type']);
|
||||
}
|
||||
|
||||
public function data_testAddQuantityDiscount(): iterable
|
||||
{
|
||||
yield 'Add perc quantity discount' => [1, 10, toDecimal(10), QuantityDiscountUtil::DISCOUNT_TYPE_PERC, QuantityDiscountUtil::DISCOUNT_TYPE_PERC];
|
||||
yield 'Add EUR quantity discount' => [1, 10, toDecimal(10), 'EUR', 'EUR'];
|
||||
yield 'Add deprecated amount quantity discount (inserted with default currency)' => [1, 10, toDecimal(10), QuantityDiscountUtil::DISCOUNT_TYPE_AMOUNT, 'CZK'];
|
||||
}
|
||||
|
||||
public function testGetDiscountWithGroupId(): void
|
||||
{
|
||||
$this->quantityDiscountContext->setActive(1);
|
||||
|
||||
$discount = $this->quantityDiscountPrice->getDiscount(3, toDecimal(3));
|
||||
$this->assertEquals(10, $discount['discount']);
|
||||
$this->assertEquals(QuantityDiscountUtil::DISCOUNT_TYPE_PERC, $discount['discount_type']);
|
||||
|
||||
$discount = $this->quantityDiscountPrice->getDiscount(3, toDecimal(20));
|
||||
$this->assertEquals(100, $discount['discount']);
|
||||
$this->assertEquals('CZK', $discount['discount_type']);
|
||||
}
|
||||
|
||||
public function testGetVariationPrice()
|
||||
{
|
||||
$this->assertEquals('2400', $this->quantityDiscountPrice->getPrice(6, toDecimal(3), 17)->getPriceWithVat()->asFloat());
|
||||
$this->assertEquals('2250', $this->quantityDiscountPrice->getPrice(6, toDecimal(3), 19)->getPriceWithVat()->asFloat());
|
||||
$this->assertEquals('2940', $this->quantityDiscountPrice->getPrice(6, toDecimal(10), 19)->getPriceWithVat()->asFloat());
|
||||
}
|
||||
|
||||
public function testGetPrice()
|
||||
{
|
||||
$this->assertEquals('800', $this->quantityDiscountPrice->getPrice(3, toDecimal(1))->getPriceWithVat()->asFloat());
|
||||
$this->assertEquals('760', $this->quantityDiscountPrice->getPrice(3, toDecimal(3))->getPriceWithVat()->asFloat());
|
||||
$this->assertEquals('720', $this->quantityDiscountPrice->getPrice(3, toDecimal(5))->getPriceWithVat()->asFloat());
|
||||
$this->assertEquals('720', $this->quantityDiscountPrice->getPrice(3, toDecimal(8))->getPriceWithVat()->asFloat());
|
||||
$this->assertEquals('640', $this->quantityDiscountPrice->getPrice(3, toDecimal(10))->getPriceWithVat()->asFloat());
|
||||
$this->assertEquals('640', $this->quantityDiscountPrice->getPrice(3, toDecimal(20))->getPriceWithVat()->asFloat());
|
||||
$this->assertEquals('679', $this->quantityDiscountPrice->getPrice(3, toDecimal(25))->getPriceWithVat()->asFloat());
|
||||
$this->assertEquals('611', $this->quantityDiscountPrice->getPrice(3, toDecimal(50))->getPriceWithVat()->asFloat(), 'Discount in EUR - 6 EUR (without vat) * 26 = 156 CZK discount, 188.76 with vat');
|
||||
|
||||
$this->quantityDiscountContext->setActive(1);
|
||||
$this->assertEquals('679', $this->quantityDiscountPrice->getPrice(3, toDecimal(20))->getPriceWithVat()->asFloat());
|
||||
}
|
||||
|
||||
public function testGetDiscount()
|
||||
{
|
||||
$this->assertEquals(null, $this->quantityDiscountPrice->getDiscount(3, toDecimal(1)));
|
||||
$this->assertEquals(5, $this->quantityDiscountPrice->getDiscount(3, toDecimal(4))['discount']);
|
||||
$this->assertEquals(10, $this->quantityDiscountPrice->getDiscount(3, toDecimal(5))['discount']);
|
||||
$this->assertEquals(10, $this->quantityDiscountPrice->getDiscount(3, toDecimal(9))['discount']);
|
||||
$this->assertEquals(20, $this->quantityDiscountPrice->getDiscount(3, toDecimal(10))['discount']);
|
||||
$this->assertEquals(20, $this->quantityDiscountPrice->getDiscount(3, toDecimal(20))['discount']);
|
||||
$this->assertEquals(QuantityDiscountUtil::DISCOUNT_TYPE_PERC, $this->quantityDiscountPrice->getDiscount(3, toDecimal(20))['discount_type']);
|
||||
|
||||
$this->assertEquals(100, $this->quantityDiscountPrice->getDiscount(3, toDecimal(25))['discount']);
|
||||
$this->assertEquals('CZK', $this->quantityDiscountPrice->getDiscount(3, toDecimal(25))['discount_type']);
|
||||
|
||||
$this->assertEquals(6, $this->quantityDiscountPrice->getDiscount(3, toDecimal(50))['discount']);
|
||||
$this->assertEquals('EUR', $this->quantityDiscountPrice->getDiscount(3, toDecimal(50))['discount_type']);
|
||||
}
|
||||
|
||||
/*
|
||||
public function testGetPriceForDealer()
|
||||
{
|
||||
$user = \User::createFromId(1);
|
||||
$user->activateUser();
|
||||
// user has B2B price level
|
||||
// quantity discount should not apply
|
||||
$this->assertEquals('0', $this->quantityDiscountPrice->getDiscountPercent(3, 1)->asFloat());
|
||||
$this->assertEquals('0', $this->quantityDiscountPrice->getDiscountPercent(3, 4)->asFloat());
|
||||
$this->assertEquals('0', $this->quantityDiscountPrice->getDiscountPercent(3, 5)->asFloat());
|
||||
$this->assertEquals('0', $this->quantityDiscountPrice->getDiscountPercent(3, 9)->asFloat());
|
||||
$this->assertEquals('0', $this->quantityDiscountPrice->getDiscountPercent(3, 10)->asFloat());
|
||||
$this->assertEquals('0', $this->quantityDiscountPrice->getDiscountPercent(3, 20)->asFloat());
|
||||
$this->assertEquals('800', $this->quantityDiscountPrice->getPrice(3, 1)->getPriceWithVat()->asFloat());
|
||||
$this->assertEquals('800', $this->quantityDiscountPrice->getPrice(3, 3)->getPriceWithVat()->asFloat());
|
||||
$this->assertEquals('800', $this->quantityDiscountPrice->getPrice(3, 5)->getPriceWithVat()->asFloat());
|
||||
$this->assertEquals('800', $this->quantityDiscountPrice->getPrice(3, 8)->getPriceWithVat()->asFloat());
|
||||
$this->assertEquals('800', $this->quantityDiscountPrice->getPrice(3, 10)->getPriceWithVat()->asFloat());
|
||||
$this->assertEquals('800', $this->quantityDiscountPrice->getPrice(3, 20)->getPriceWithVat()->asFloat());
|
||||
}
|
||||
*/
|
||||
|
||||
public function testCartPriceForDealer()
|
||||
{
|
||||
$user = \User::createFromId(1);
|
||||
$user->activateUser();
|
||||
$cart = $this->addToCart(3, null, 10);
|
||||
// 10 pcs => 20% discount => 6400
|
||||
// B2B price level => 50% discount => 3200
|
||||
$this->assertEquals('3200', $cart->totalPricePay);
|
||||
}
|
||||
|
||||
/** @dataProvider data_cartPrice */
|
||||
public function testCartPrice(int $productId, ?int $variationId, int $pieces, float $expectedTotalPrice): void
|
||||
{
|
||||
$cart = $this->addToCart($productId, $variationId, $pieces);
|
||||
|
||||
$this->assertEquals($expectedTotalPrice, $cart->totalPricePay->asFloat());
|
||||
}
|
||||
|
||||
public function data_cartPrice(): array
|
||||
{
|
||||
return [
|
||||
// aplikuje se sleva 10%
|
||||
[3, null, 10, 6400],
|
||||
// aplikuje se sleva 5%
|
||||
[3, null, 6, 4320],
|
||||
// neaplikuje se sleva
|
||||
[6, 19, 2, 6000],
|
||||
// aplikuje se sleva 25%
|
||||
[6, 19, 3, 6750],
|
||||
[6, 19, 4, 9000],
|
||||
// aplikuje se sleva 20%
|
||||
[6, 17, 4, 9600],
|
||||
// aplikuje se sleva 6 EUR - cena produktu 611 Kc * 50
|
||||
[3, null, 50, 30550],
|
||||
];
|
||||
}
|
||||
|
||||
private function addToCart($id_product, $id_variation = null, $pieces = 1): \Cart
|
||||
{
|
||||
$this->createCart();
|
||||
$cart = $this->cart;
|
||||
|
||||
$item = [
|
||||
'id_product' => $id_product,
|
||||
'id_variation' => $id_variation,
|
||||
'pieces' => $pieces,
|
||||
];
|
||||
$cart->addItem($item);
|
||||
$cart->save();
|
||||
$cart->load();
|
||||
$cart->createFromDB();
|
||||
|
||||
return $cart;
|
||||
}
|
||||
|
||||
public function getDataSet()
|
||||
{
|
||||
return $this->getJsonDataSetFromFile();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\QuantityDiscountBundle\Twig\Components\QuantityDiscounts;
|
||||
|
||||
use KupShop\ComponentsBundle\Attributes\Component;
|
||||
use KupShop\ComponentsBundle\Attributes\Version;
|
||||
use KupShop\ComponentsBundle\Twig\BaseComponent;
|
||||
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
|
||||
|
||||
#[AsTwigComponent(template: '@QuantityDiscount/components/QuantityDiscounts/Discount/Discount.1.html.twig')]
|
||||
#[Component(1, [
|
||||
new Version(1, newJs: null),
|
||||
])]
|
||||
class Discount extends BaseComponent
|
||||
{
|
||||
public array $discount;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\QuantityDiscountBundle\Twig\Components\QuantityDiscounts;
|
||||
|
||||
use KupShop\ComponentsBundle\Attributes\Component;
|
||||
use KupShop\ComponentsBundle\Attributes\Version;
|
||||
use KupShop\ComponentsBundle\Twig\BaseComponent;
|
||||
use KupShop\ComponentsBundle\Twig\DataProvider\ProductDataTrait;
|
||||
use KupShop\KupShopBundle\Util\Contexts;
|
||||
use KupShop\KupShopBundle\Util\Price\PriceCalculator;
|
||||
use KupShop\QuantityDiscountBundle\Context\QuantityDiscountContext;
|
||||
use KupShop\QuantityDiscountBundle\Query\QuantityDiscount;
|
||||
use KupShop\QuantityDiscountBundle\Util\QuantityDiscountPrice;
|
||||
use Query\Operator;
|
||||
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
|
||||
|
||||
#[AsTwigComponent(template: '@QuantityDiscount/components/QuantityDiscounts/Discounts.1.html.twig')]
|
||||
#[Component(1, [
|
||||
new Version(1, newJs: null),
|
||||
])]
|
||||
class Discounts extends BaseComponent
|
||||
{
|
||||
use ProductDataTrait;
|
||||
|
||||
public ?string $title = null;
|
||||
|
||||
protected array $discounts;
|
||||
|
||||
public function __construct(private readonly QuantityDiscountPrice $quantityDiscountPrice)
|
||||
{
|
||||
}
|
||||
|
||||
public function getActive(): bool|int
|
||||
{
|
||||
$quantityDiscountContext = Contexts::get(QuantityDiscountContext::class);
|
||||
|
||||
return $quantityDiscountContext->getActive();
|
||||
}
|
||||
|
||||
public function getDiscounts(): array
|
||||
{
|
||||
if (!isset($this->discounts)) {
|
||||
$this->discounts = [];
|
||||
|
||||
$qb = sqlQueryBuilder()
|
||||
->select('pieces, discount, discount_type')
|
||||
->from('products_quantity_discounts')
|
||||
->where(Operator::equals(['id_product' => $this->id_product]))
|
||||
->orderBy('pieces', 'ASC');
|
||||
|
||||
$orX = ['id_variation IS NULL'];
|
||||
if ($this->id_variation) {
|
||||
$orX[] = Operator::equals(['id_variation' => $this->id_variation]);
|
||||
}
|
||||
|
||||
$qb->andWhere(Operator::orX($orX));
|
||||
$qb->andWhere(QuantityDiscount::byGroup($this->getActive()));
|
||||
|
||||
$data = $qb->execute();
|
||||
if ($data->rowCount() == 0) {
|
||||
return $this->discounts;
|
||||
}
|
||||
|
||||
$priceBase = $this->quantityDiscountPrice->getPrice($this->id_product, 1, $this->id_variation);
|
||||
$unit = $this->product->unit['short_name'] ?? 'ks';
|
||||
|
||||
$this->discounts[1] = [
|
||||
'pieces_from' => 1,
|
||||
'pieces_to' => null,
|
||||
'unit' => $unit,
|
||||
'discount' => 0,
|
||||
'discount_value' => 0,
|
||||
'discount_type' => null,
|
||||
'price' => $priceBase,
|
||||
];
|
||||
$lastItem = &$this->discounts[1];
|
||||
|
||||
foreach ($data as $discount) {
|
||||
$price = $this->quantityDiscountPrice->getPrice($this->id_product, $discount['pieces'], $this->id_variation);
|
||||
|
||||
$this->discounts[$discount['pieces']] = [
|
||||
'pieces_from' => $discount['pieces'],
|
||||
'pieces_to' => null,
|
||||
'unit' => $unit,
|
||||
'discount' => $discount['discount'],
|
||||
'discount_value' => PriceCalculator::sub($priceBase, $price),
|
||||
'discount_type' => $discount['discount_type'],
|
||||
'price' => $price,
|
||||
];
|
||||
|
||||
$lastItem['pieces_to'] = $discount['pieces'] - 1;
|
||||
$lastItem = &$this->discounts[$discount['pieces']];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->discounts;
|
||||
}
|
||||
|
||||
public function getShouldShowDiscounts(): bool
|
||||
{
|
||||
$shouldShow = false;
|
||||
|
||||
foreach ($this->getDiscounts() as $discount) {
|
||||
if ((int) $discount['discount'] > 0) {
|
||||
$shouldShow = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $shouldShow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Created by PhpStorm.
|
||||
* User: ondra
|
||||
* Date: 23.11.17
|
||||
* Time: 8:01.
|
||||
*/
|
||||
|
||||
namespace KupShop\QuantityDiscountBundle\Util;
|
||||
|
||||
use KupShop\I18nBundle\Util\PriceConverter;
|
||||
use KupShop\KupShopBundle\Context\CurrencyContext;
|
||||
use KupShop\KupShopBundle\Util\Contexts;
|
||||
use KupShop\KupShopBundle\Util\Price\Price;
|
||||
use KupShop\KupShopBundle\Util\Price\ProductPrice;
|
||||
use KupShop\QuantityDiscountBundle\Context\QuantityDiscountContext;
|
||||
use KupShop\QuantityDiscountBundle\Query\QuantityDiscount;
|
||||
use Query\Operator;
|
||||
use Query\QueryBuilder;
|
||||
|
||||
class QuantityDiscountPrice
|
||||
{
|
||||
public function __construct(
|
||||
private QuantityDiscountContext $quantityDiscountContext,
|
||||
protected ?PriceConverter $priceConverter = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getPrice($idProduct, $pieces, $idVariation = null): Price
|
||||
{
|
||||
if ($productPrice = $this->getProductPrice($idProduct, $idVariation)) {
|
||||
return $this->applyDiscount($productPrice, $idProduct, $pieces, $idVariation);
|
||||
}
|
||||
|
||||
return new Price(toDecimal(0), Contexts::get(CurrencyContext::class)->getDefault(), getVat());
|
||||
}
|
||||
|
||||
public function applyDiscount(ProductPrice $productPrice, $idProduct, $pieces, $idVariation): Price
|
||||
{
|
||||
$discountedValue = $this->calculateQuantityDiscountForProductPrice(
|
||||
$productPrice,
|
||||
$this->getDiscount($idProduct, toDecimal($pieces), $idVariation)
|
||||
);
|
||||
|
||||
return new Price($discountedValue, $productPrice->getCurrency(), $productPrice->getVat());
|
||||
}
|
||||
|
||||
public function calculateQuantityDiscountPriceForProductPrice(ProductPrice $productPrice, array $discount): Price
|
||||
{
|
||||
return new Price(
|
||||
$this->calculateQuantityDiscountForProductPrice($productPrice, $discount),
|
||||
$productPrice->getCurrency(),
|
||||
$productPrice->getVat()
|
||||
);
|
||||
}
|
||||
|
||||
private function calculateQuantityDiscountForProductPrice(ProductPrice $productPrice, ?array $discount): \Decimal
|
||||
{
|
||||
$value = $productPrice->getPriceWithVat(false)
|
||||
->removeVat($productPrice->getVat());
|
||||
|
||||
if (!$discount) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($discount['discount_type'] == QuantityDiscountUtil::DISCOUNT_TYPE_PERC) {
|
||||
$value = $value->addDiscount($discount['discount']);
|
||||
} else {
|
||||
$discountValue = $this->priceConverter?->convert($discount['discount_type'], $productPrice->getCurrency(), $discount['discount']) ?: toDecimal($discount['discount']);
|
||||
$value = \Decimal::max($value->sub($discountValue), \DecimalConstants::zero());
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function getProductPrice($idProduct, $idVariation = null): ?ProductPrice
|
||||
{
|
||||
if ($product = $this->getProduct($idProduct, $idVariation)) {
|
||||
return $product->getProductPrice();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getDiscount(
|
||||
int $idProduct,
|
||||
\Decimal $pieces,
|
||||
?int $variationId = null,
|
||||
): ?array {
|
||||
$active = $this->quantityDiscountContext->getActive();
|
||||
if (!$active) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$specs = [
|
||||
Operator::equals(['id_product' => $idProduct]),
|
||||
QuantityDiscount::byGroup($active),
|
||||
];
|
||||
|
||||
if ($variationId) {
|
||||
if ($quantityDiscount = $this->getDiscountQueryBuilder($specs,
|
||||
$pieces)->andWhere(Operator::equals(['id_variation' => $variationId]))->execute()->fetchAssociative()) {
|
||||
return $quantityDiscount;
|
||||
}
|
||||
}
|
||||
|
||||
$quantityDiscount = $this->getDiscountQueryBuilder($specs,
|
||||
$pieces, )->andWhere('id_variation IS NULL')->execute()->fetchAssociative();
|
||||
|
||||
return $quantityDiscount ?: null;
|
||||
}
|
||||
|
||||
private function getDiscountQueryBuilder($specs, $pieces): QueryBuilder
|
||||
{
|
||||
return sqlQueryBuilder()
|
||||
->select('discount, discount_type')
|
||||
->from('products_quantity_discounts')
|
||||
->where('pieces <= :pieces')
|
||||
->andWhere(Operator::andX($specs))
|
||||
->orderBy('pieces', 'DESC')
|
||||
->setParameter('pieces', $pieces)
|
||||
->setMaxResults(1);
|
||||
}
|
||||
|
||||
private function getProduct($idProduct, $idVariation = null): ?\Product
|
||||
{
|
||||
if ($idVariation !== null) {
|
||||
$product = new \Variation($idProduct, $idVariation);
|
||||
} else {
|
||||
$product = new \Product($idProduct);
|
||||
}
|
||||
|
||||
if (!$product->createFromDB()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $product;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\QuantityDiscountBundle\Util;
|
||||
|
||||
use KupShop\KupShopBundle\Context\CurrencyContext;
|
||||
use KupShop\KupShopBundle\Util\Contexts;
|
||||
use Query\Operator;
|
||||
|
||||
class QuantityDiscountUtil
|
||||
{
|
||||
public const DISCOUNT_TYPE_PERC = 'perc';
|
||||
/** @deprecated Use currency id (CZK, EUR...) instead of `amount` keyword */
|
||||
public const DISCOUNT_TYPE_AMOUNT = 'amount';
|
||||
|
||||
public function getDiscountTypes(): array
|
||||
{
|
||||
$types = [self::DISCOUNT_TYPE_PERC => '%'];
|
||||
|
||||
$currencyContext = Contexts::get(CurrencyContext::class);
|
||||
|
||||
foreach ($currencyContext->getAll() as $currency) {
|
||||
$types[$currency->getId()] = $currency->getId();
|
||||
}
|
||||
|
||||
return $types;
|
||||
}
|
||||
|
||||
public function addQuantityDiscount(int $productId, ?int $variationId, int $pieces, \Decimal $discount, ?int $groupId = null, ?string $type = self::DISCOUNT_TYPE_PERC): bool
|
||||
{
|
||||
// backward compatibility: remove me when TYPE_AMOUNT is not used
|
||||
if ($type === self::DISCOUNT_TYPE_AMOUNT) {
|
||||
$type = Contexts::get(CurrencyContext::class)->getDefaultId();
|
||||
}
|
||||
|
||||
$id = sqlQueryBuilder()
|
||||
->select('id')
|
||||
->from('products_quantity_discounts')
|
||||
->where(
|
||||
Operator::equalsNullable(
|
||||
[
|
||||
'id_group' => $groupId,
|
||||
'id_product' => $productId,
|
||||
'id_variation' => $variationId,
|
||||
'pieces' => $pieces,
|
||||
'discount_type' => $type,
|
||||
]
|
||||
)
|
||||
)->execute()->fetchOne();
|
||||
|
||||
if ($id) {
|
||||
sqlQueryBuilder()
|
||||
->update('products_quantity_discounts')
|
||||
->directValues(
|
||||
[
|
||||
'discount' => $discount,
|
||||
]
|
||||
)
|
||||
->where(Operator::equals(['id' => $id]))
|
||||
->execute();
|
||||
} else {
|
||||
sqlQueryBuilder()
|
||||
->insert('products_quantity_discounts')
|
||||
->directValues(
|
||||
[
|
||||
'id_group' => $groupId,
|
||||
'id_product' => $productId,
|
||||
'id_variation' => $variationId,
|
||||
'pieces' => $pieces,
|
||||
'discount' => $discount,
|
||||
'discount_type' => $type,
|
||||
]
|
||||
)->execute();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function deleteQuantityDiscount(int $productId, ?int $variationId, int $pieces, ?int $groupId = null): void
|
||||
{
|
||||
sqlQueryBuilder()
|
||||
->delete('products_quantity_discounts')
|
||||
->where(
|
||||
Operator::equalsNullable(
|
||||
[
|
||||
'id_group' => $groupId,
|
||||
'id_product' => $productId,
|
||||
'id_variation' => $variationId,
|
||||
'pieces' => $pieces,
|
||||
]
|
||||
)
|
||||
)->execute();
|
||||
}
|
||||
|
||||
public function getGroups(): array
|
||||
{
|
||||
// default group
|
||||
$groups = [
|
||||
0 => $this->getDefaultGroupName(),
|
||||
];
|
||||
|
||||
// additional groups
|
||||
if ($additionalGroups = findModule(\Modules::QUANTITY_DISCOUNT, 'groups', [])) {
|
||||
$groups = array_merge($groups, $additionalGroups);
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
public function getDefaultGroupName(): string
|
||||
{
|
||||
return 'E-Shop';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user