first commit

This commit is contained in:
2025-08-02 16:30:27 +02:00
commit 23646bfcee
14851 changed files with 1750626 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
<?php
namespace KupShop\ProductsChargesBundle\Actions\Frontend;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Context\VatContext;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\KupShopBundle\Util\Price\Price;
use KupShop\OrderDiscountBundle\Actions\Frontend\AbstractHandler;
use KupShop\OrderDiscountBundle\Actions\Frontend\HandlerWithPurchaseStateInterface;
use KupShop\OrderDiscountBundle\Actions\Frontend\HandlerWithPurchaseStateTrait;
use KupShop\ProductsChargesBundle\Actions\OrdersChargesAction;
use KupShop\ProductsChargesBundle\Translations\ProductsChargesTranslation;
use Query\Operator;
use Query\Translation;
class OrdersChargesHandler extends AbstractHandler implements HandlerWithPurchaseStateInterface
{
use HandlerWithPurchaseStateTrait;
protected $template = 'order_discount/actions/orders_charge.tpl';
public $templateSize = 'full';
public function setActionData(array $actionData): AbstractHandler
{
$this->actionData = $actionData;
if ($id_charge = $actionData['id_charge'] ?? null) {
$charge = sqlQueryBuilder()->select('ch.*')
->from('charges', 'ch')
->where(Operator::equals(['ch.id' => $id_charge]))
->andWhere(Translation::coalesceTranslatedFields(ProductsChargesTranslation::class))
->execute()->fetch();
if ($charge) {
$currency = Contexts::get(CurrencyContext::class)->getOrDefault($charge['currency'] ?? null);
if ($charge['product'] = $this->getChargeProduct($charge)) {
// uplatnit sazbu DPH podle priplatkoveho produktu
$vat = getVat($charge['product']->vat_id);
} elseif (is_null($charge['vat'])) {
// uplatnit sazbu DPH podle produktů v objednávce
$vat = $this->purchaseState->getDeliveryVat();
} else {
$vat = getVat($charge['vat']);
if ($vat != 0) {
// v případě OSS se použije výchozí DPH dané země
$vat = Contexts::get(VatContext::class)->isCountryOssActive() ? getVat() : $vat;
}
}
$price = $charge['price'];
if ($charge['percentage'] !== null && $this->purchaseState) {
$price = OrdersChargesAction::calculatePercentCharges(
$this->purchaseState->getProductsTotalPrice(),
$charge['percentage']
);
$currency = $this->purchaseState->getProductsTotalPrice()->getCurrency();
}
$charge['price'] = new Price(toDecimal($price), $currency, $vat);
$this->actionData['charge'] = $charge;
}
}
return $this;
}
public function getVars(): array
{
$vars = parent::getVars();
$vars['disabled'] = false;
$charge = &$vars['data']['charge'];
$product = $this->getChargeProduct($charge);
$vars['data']['product'] = $product;
if ($product) {
$custom_data = json_decode($charge['data'] ?? '', true);
$order_not_in_store = ($custom_data['order_not_in_store'] ?? false);
if (!$order_not_in_store && ($product->inStore <= 0)) {
$charge['figure'] = 'N';
}
}
return $vars;
}
public function getChargeProduct(array $charge): ?\Product
{
if ($product = $charge['product'] ?? null) {
return $product;
}
if ($id_product = $charge['id_product'] ?? null) {
if ($id_variation = $charge['id_variation'] ?? null) {
$product = new \Variation($id_product, $id_variation);
} else {
$product = new \Product($id_product);
}
if (!$product->createFromDB()) {
$product = null;
}
}
return $product;
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace KupShop\ProductsChargesBundle\Actions;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Context\VatContext;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\KupShopBundle\Util\Price\Price;
use KupShop\KupShopBundle\Util\Price\PriceCalculator;
use KupShop\KupShopBundle\Util\Price\TotalPrice;
use KupShop\OrderDiscountBundle\Actions\AbstractAction;
use KupShop\OrderDiscountBundle\Actions\Frontend\HandlerInterface;
use KupShop\OrderDiscountBundle\Entity\OrderDiscount;
use KupShop\OrderingBundle\Entity\Purchase\ChargePurchaseItem;
use KupShop\OrderingBundle\Entity\Purchase\ProductPurchaseItem;
use KupShop\OrderingBundle\Entity\Purchase\PurchaseState;
use KupShop\OrderingBundle\Util\Order\OrderItemInfo;
use KupShop\ProductsChargesBundle\Actions\Frontend\OrdersChargesHandler;
use Query\Operator;
class OrdersChargesAction extends AbstractAction
{
protected static $type = 'orders_charge';
protected static $position = 130;
protected $adminTemplate = 'actions/orders_charges.tpl';
protected $translationSection = 'productsCharges';
private $ordersChargesHandler;
public function __construct(OrdersChargesHandler $ordersChargesHandler)
{
$this->ordersChargesHandler = $ordersChargesHandler;
}
public static function calculatePercentCharges(TotalPrice $totalPrice, $percentage)
{
return $totalPrice
->getPriceWithoutVat()
->div(toDecimal('100.0'))
->mul(toDecimal($percentage));
}
public function applyResult(PurchaseState &$purchaseState, OrderDiscount $orderDiscount, array $data)
{
$charge = sqlQueryBuilder()->select('*')->from('charges')
->where(Operator::equals(['id' => $data['id_charge']]))
->execute()->fetch();
if ($charge) {
$required = $charge['required'] ?? null;
$defaultChecked = $charge['checked'] ?? null;
$checked = ($data['handled']['checked'] ?? $required == 'Y' ?: $defaultChecked);
if ($checked == 'Y') {
$name = $orderDiscount->getDisplayName();
$note = ['item_type' => OrderItemInfo::TYPE_CHARGE, 'id_charge' => $data['id_charge']];
$product = false;
if ($id_product = $charge['id_product'] ?? null) {
if ($id_variation = $charge['id_variation'] ?? null) {
$product = new \Variation($id_product, $id_variation);
} else {
$product = new \Product($id_product);
}
if (!$product->createFromDB()) {
$product = false;
}
}
if ($product) {
// uplatnit sazbu DPH podle priplatkoveho produktu
$vat = getVat($product->vat_id);
} elseif (is_null($charge['vat'])) {
// uplatnit sazbu DPH podle produktů v objednávce
$vat = $purchaseState->getDeliveryVat();
} else {
$vat = getVat($charge['vat']);
if ($vat != 0) {
// v případě OSS se použije výchozí DPH dané země
$vat = Contexts::get(VatContext::class)->isCountryOssActive() ? getVat() : $vat;
}
}
$discountPrice = toDecimal($charge['price']);
$currency = Contexts::get(CurrencyContext::class)->getOrDefault($charge['currency'] ?? null);
if ($charge['percentage'] ?? false) {
$discountPrice = self::calculatePercentCharges($purchaseState->getTotalPrice(), $charge['percentage']);
$currency = $purchaseState->getTotalPrice()->getCurrency();
}
$discountPrice = new Price($discountPrice, $currency, $vat);
$discountPrice = PriceCalculator::convert($discountPrice, $purchaseState->getTotalPrice()->getCurrency());
$discountPriceValue = $discountPrice->getPriceWithoutVat(); // kvuli spravnemu zaokrouhleni
$discountPrice = new Price($discountPriceValue, $discountPrice->getCurrency(), $vat);
if ($product) {
$purchaseItem = new ProductPurchaseItem($id_product, $id_variation ?? null, 1,
$discountPrice, $note, $orderDiscount->getId());
$purchaseItem->id_charge = $data['id_charge'];
$purchaseItem->setName($name);
} else {
$purchaseItem = new ChargePurchaseItem($name, $discountPrice, $data['id_charge'], $orderDiscount->getId(), $note);
}
$purchaseState->addCharge($purchaseItem);
$purchaseState->addUsedDiscount($orderDiscount->getId());
if ($message = $data['messages']['success'] ?? '') {
$this->messages['success'] = $message;
}
} else {
if ($message = $data['messages']['warning'] ?? '') {
$this->messages['warning'] = $message;
}
}
}
}
public function getFrontendHandler(): ?HandlerInterface
{
return $this->ordersChargesHandler;
}
protected function getVars($vars)
{
$vars['charges'] = sqlFetchAll(
sqlQueryBuilder()
->select('id, IF(admin_title != "", admin_title, title) as title')->from('charges')
->andWhere("type = 'order'")
->execute(),
['id' => 'title']
);
return $vars;
}
}

View File

@@ -0,0 +1,42 @@
<?php
$txt_str['productsCharges'] = [
'module' => 'Příplatky',
'navigation' => 'Příplatky',
'toolbar_list' => 'Seznam příplatků',
'toolbar_add' => 'Přidat příplatek',
'titleAdd' => 'Nový příplatek',
'titleEdit' => 'Úprava příplatku',
'flap1' => 'Příplatek',
'confirmDelete' => 'Opravdu chcete smazat tento příplatek?',
'products_charges' => 'Příplatky ke zboží',
'name' => 'Název',
'display_name' => 'Název v e-shopu',
'display_name_tooltip' => 'Název v e-shopu uvidí uživatelé. Pokud jej nevyplníte, automaticky se do e-shopu propíše text z Názvu.',
'percentage_tooltip' => 'Vyplníte-li hodnotu v procentech, prioritizuje se před fixní cenou.',
'type' => 'Typ příplatku',
'type_product' => 'K produktu',
'type_order' => 'K objednávce',
'descr' => 'Popis',
'price' => 'Cena',
'vat' => 'Daň',
'product' => 'Příplatkový produkt',
'charge_required' => 'Povinný příplatek',
'charge_required_help' => 'Příplatek je vždy zaškrtnutý a zákazník ho nemůže odstranit.',
'charge_included' => 'Již zahrnut v ceně',
'charge_included_help' => 'Aktivováním příplatku se nezvýší cena za produkt(y).',
'charge_checked' => 'Ve výchozím stavu zaškrtnutý',
'charge_checked_help' => 'Příplatek je ve výchozím stavu zaškrtnutý, aktivní, ale zákazník má možnost zaškrtnutí zrušit.',
'charge_onetime' => 'Jednorázový příplatek',
'charge_onetime_help' => 'Příplatek bude u každé položky započítán jen jednou, nehledě na to, kolik kusů dané položky má uživatel v košíku.',
'orders_charge' => 'Příplatek k objednávce',
'errorNotAllValid' => 'Zadejte alespoň název, cenu a DPH',
'active' => 'Aktivní',
'activityEdited' => 'Upraven příplatek: %s',
'activityAdded' => 'Přidán příplatek: %s',
'activityDeleted' => 'Smazán příplatek: %s',
'orders_chargeDescription' => 'Příplatek k objednávce',
];

View File

@@ -0,0 +1,35 @@
<?php
$txt_str['productsCharges'] = [
'module' => 'Charges',
'navigation' => 'Charges',
'toolbar_list' => 'Show charges',
'toolbar_add' => 'Add charge',
'titleAdd' => 'Add new charge',
'titleEdit' => 'Edit charge',
'flap1' => 'Charge',
'confirmDelete' => 'Do you really want to delete this charge?',
'products_charges' => 'Product charges',
'title' => 'Title',
'descr' => 'Description',
'price' => 'Price',
'vat' => 'VAT',
'charge_required' => 'Compulsory',
'charge_required_help' => 'Charge is always active, customers cannot uncheck it.',
'charge_included' => 'Included',
'charge_included_help' => 'By activating this charge, the price of product(s) remains the same.',
'charge_checked' => 'Checked by default',
'charge_checked_help' => 'Charge is checked by default, but customer can uncheck it at will.',
'charge_onetime' => 'One-time charge',
'charge_onetime_help' => 'The charge will only be applied once for each item, regardless of how many items the user has in their cart.',
'orders_charge' => 'Orders charge',
'errorNotAllValid' => 'These fields are required: ',
'active' => 'Active',
'activityEdited' => 'Charge edited: %s',
'activityAdded' => 'Charge added: %s',
'activityDeleted' => 'Charge deleted: %s',
'orders_chargeDescription' => 'Orders charge',
];

View File

@@ -0,0 +1,76 @@
<?php
use KupShop\AdminBundle\AdminList\BaseList;
class ProductsChargesList extends BaseList
{
protected $showMassEdit = true;
protected $tableName = 'charges';
protected ?string $tableAlias = 'ch';
protected $tableDef = [
'id' => 'id',
'fields' => [
'name' => ['field' => 'ch.admin_title', 'size' => 1, 'translate' => true, 'spec' => 'ch.admin_title', 'fieldType' => BaseList::TYPE_STRING],
'display_name' => ['field' => 'ch.title', 'size' => 2, 'translate' => true, 'spec' => 'ch.title', 'fieldType' => BaseList::TYPE_STRING],
'type' => ['field' => 'ch.type', 'size' => 1, 'translate' => true, 'render' => 'renderType', 'fieldType' => ProductsChargesList::TYPE_LIST],
'descr' => ['field' => 'ch.descr', 'visible' => 'N', 'size' => 3, 'translate' => true, 'spec' => 'ch.descr', 'fieldType' => BaseList::TYPE_STRING],
'price' => ['field' => 'ch.price', 'render' => 'renderPriceVat', 'size' => 0.7, 'translate' => true, 'spec' => 'ch.price', 'fieldType' => BaseList::TYPE_PRICE],
'vat' => ['field' => 'ch.vat', 'render' => 'renderVat', 'size' => 0.7, 'translate' => true, 'spec' => 'ch.vat', 'fieldType' => ProductsChargesList::TYPE_LIST],
'charge_required' => ['field' => 'ch.required', 'render' => 'renderBoolean', 'size' => 0.7, 'translate' => true, 'spec' => 'ch.required', 'fieldType' => BaseList::TYPE_BOOL],
'charge_included' => ['field' => 'ch.included', 'render' => 'renderBoolean', 'size' => 0.7, 'translate' => true, 'spec' => 'ch.included', 'fieldType' => BaseList::TYPE_BOOL, 'visible' => 'N'],
'charge_checked' => ['field' => 'ch.checked', 'render' => 'renderBoolean', 'size' => 0.7, 'translate' => true, 'spec' => 'ch.checked', 'fieldType' => BaseList::TYPE_BOOL, 'visible' => 'N'],
],
];
protected function getVats()
{
return sqlQueryBuilder()
->select('id', 'vat')
->from('vats')
->execute()
->fetchAll();
}
public function customizeTableDef($tableDef)
{
$tableDef = parent::customizeTableDef($tableDef);
$tableDef['fields']['type']['fieldOptions'] = [
'product' => translate('type_product'),
'order' => translate('type_order'),
];
$vats = $this->getVats();
foreach ($vats as $vat) {
$tableDef['fields']['vat']['fieldOptions'][(int) $vat['id']] = $vat['vat'].'%';
}
return $tableDef;
}
public function renderVat($values, $column)
{
$value = $this->getListRowValue($values, $column['field']);
if ($value) {
return getVat($value).'%';
}
return translate('byProducts', 'vats');
}
public function renderType($values, $column)
{
$type = $values['type'] ?? '';
return translate("type_{$type}", 'productsCharges', true);
}
public function getQuery()
{
$qb = sqlQueryBuilder()->select('*')->from('charges', 'ch');
return $qb;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace KupShop\ProductsChargesBundle\Admin\lists;
use KupShop\I18nBundle\Admin\lists\TranslateList;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\ProductsChargesBundle\Translations\ProductsChargesTranslation;
class TranslateProductsChargesList extends TranslateList
{
protected $template = 'list/translateProductsCharges.tpl';
protected $listType = 'translateProductsCharges';
public function __construct()
{
parent::__construct();
$this->translation = ServiceContainer::getService(ProductsChargesTranslation::class);
}
}
$main_class = TranslateProductsChargesList::class;

View File

@@ -0,0 +1,78 @@
<?php
namespace KupShop\ProductsChargesBundle\Admin;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\ProductsChargesBundle\Translations\ProductsChargesTranslation;
class ProductsCharges extends \Window
{
protected $tableName = 'charges';
protected $nameField = 'admin_title';
protected $required = [
'admin_title' => true,
];
protected $defaults = [
'figure' => 'Y',
];
public function get_vars()
{
$vars = parent::get_vars();
$pageVars = getVal('body', $vars);
$vatValue = 0;
if (!empty($pageVars['data']['vat'])) {
$vatValue = getVat($pageVars['data']['vat']);
}
$pageVars['data']['vatValue'] = $vatValue;
$pageVars['data']['price'] = toDecimal($pageVars['data']['price'] ?? 0);
$pageVars['data']['currencies'] = Contexts::get(CurrencyContext::class)->getSupported();
$pageVars['data']['translation_figure'] = $this->getTranslationUtil()?->getTranslationsFigure(ProductsChargesTranslation::class, $this->getID());
$this->unserializeCustomData($pageVars['data']);
$pageVars['vats'] = \KupShop\KupShopBundle\Context\VatContext::getAdminVats(true);
$vars['body'] = $pageVars;
return $vars;
}
public function getData()
{
$data = parent::getData();
if (getVal('Submit')) {
$this->serializeCustomData($data);
if ($data['priceWithVat'] && $data['price'] > 0) {
$data['price'] = toDecimal($data['price'])->removeVat(getVat($data['vat']));
}
if (trim($data['product']) === '') {
$data['id_product'] = '';
$data['id_variation'] = '';
}
}
return $data;
}
public function handleUpdate()
{
$data = $this->getData();
$result = parent::handleUpdate();
if ($result) {
// Save translations figure
$this->getTranslationUtil()?->updateTranslationsFigure(ProductsChargesTranslation::class, $this->getID(), $data['translation_figure'] ?? []);
}
return $result;
}
}
return ProductsCharges::class;

View File

@@ -0,0 +1,19 @@
{extends "actions/action.tpl"}
{block "action"}
<div class="form-group">
<div class="col-md-2 control-label"><label>{'orders_charge'|translate:'productsCharges'}</label></div>
<div class="col-md-8">
{print_select name="{$name}[data][id_charge]" var=$charges selected=$data.id_charge}
</div>
<div class="col-md-1">
{if $data.id_charge}
<a href="javascript:nw('productsCharges', {$data.id_charge})" title="{'titleEdit'|translate:'productsCharges'}">
<span class="badge" title="{'titleEdit'|translate:'productsCharges'}">
<i class="glyphicon glyphicon-cog"></i>
</span>
</a>
{/if}
</div>
</div>
{/block}

View File

@@ -0,0 +1,5 @@
{extends file="[I18nBundle]/list/translate.tpl"}
{block translationObjectTitle}
{$object.id} | <a title="{'titleEdit'|translate:'productsCharges'}" href="javascript:nw('productsCharges', '{$object.id}')">{$object.title}</a>
{/block}

View File

@@ -0,0 +1,195 @@
{extends file="[shared]/window.tpl"}
{block tabs}
{windowTab id='flap1'}
{/block}
{block tabsContent}
<div id="flap1" class="tab-pane fade active in boxStatic">
<div class="form-group">
<div class="col-md-2 control-label"><label>{'name'|translate}</label></div>
<div class="col-md-4">
<input type="text" class="form-control input-sm" name="data[admin_title]" size="30" maxlength="100" value="{$body.data.admin_title}"/>
</div>
<div class="col-md-2 control-label">
<label>{'display_name'|translate}</label>
<a class="help-tip" data-toggle="tooltip" title="{'display_name_tooltip'|translate}"><i class="glyphicon glyphicon-question-sign"></i></a>
</div>
<div class="col-md-4">
<input type="text" class="form-control input-sm" name="data[title]" size="30" maxlength="100" value="{$body.data.title}"/>
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label"><label>{'descr'|translate}</label>
{insert_llm_button type='product_charges' target="data[descr]"}
</div>
<div class="col-md-10">
<textarea class="form-control input-sm" name="data[descr]">{$body.data.descr}</textarea>
{insert_wysiwyg target="data[descr]"}
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label"><label>{'active'|translate}</label></div>
<div class="col-md-10">
{print_toggle name="figure" value=$body.data.figure}
{include 'utils/translations.figure.tpl' figureData=$body.data.translation_figure parentFigure=$body.data.figure}
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label"><label>{'type'|translate}</label></div>
<div class="col-md-10">
<div class="radio pull-left">
<input type="radio" name="data[type]" class="check" value="product" {$body.data.type|checked:'product'} id="t1">
<label for="t1"> {'type_product'|translate}</label>
<input type="radio" name="data[type]" class="check" value="order" {$body.data.type|checked:'order'} id="t2">
<label for="t2"> {'type_order'|translate}</label>
</div>
</div>
</div>
<hr>
<div class="form-group">
<div class="col-md-2 control-label"><label>{'price'|translate}</label></div>
<div class="col-md-3">
<div class="input-group" data-price-dropdown>
<div class="input-group-btn">
<select class="selecter" name="data[currency]">
{foreach $body.data.currencies as $currencyID => $currency}
<option{if $currencyID == $body.data.currency} selected{/if}>{$currencyID}</option>
{/foreach}
</select>
</div>
{* Pouzivam cenu bez DPH primo z DB a cenu s DPH vypocita JS - priceVatDropdown, takze nechci provadet format_editable_price *}
<input data-price="" type="text" class="form-control input-sm" name="data[price]" id="price"
value="{$body.data.price}" />
<div class="input-group-btn" data-dropdown>
<a class="btn btn-primary dropdown-toggle btn-sm" data-toggle="dropdown">
<span data-dropdown-state>
{'withTax'|translate:'choice'}
</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>
<input type="hidden" name="data[priceWithVat]" data-price-vat="{$body.data.vatValue}" value="0"/>
<script type="text/javascript">
priceVatDropdown($('[data-price-dropdown]'), {if is_null($body.data.vat)}0{else}1{/if});
</script>
</div>
</div>
<div class="col-md-1 control-label">
<label>{'or'|translate:'choice'}</label>
<a class="help-tip" data-toggle="tooltip" title="{'percentage_tooltip'|translate}"><i class="glyphicon glyphicon-question-sign"></i></a>
</div>
<div class="col-md-2">
<div class="input-group">
<input type="text" class="form-control input-sm" name="data[percentage]" id="percentage" maxlength="20"
value="{$body.data.percentage}"/>
<span class="input-group-addon">%</span>
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label"><label>{'vat'|translate}</label></div>
<div class="col-md-3">
<select name="data[vat]" class="selecter" id="priceVat">
<option value="" data-vat="0" {if is_null($body.data.vat)}selected{/if}>{'byProducts'|translate:'vats'}</option>
{foreach $body.vats as $key => $vat}
<option value="{$vat.id}" data-vat="{$vat.vat}" {if $body.data.vat == $vat.id}selected{/if}>
{$vat.descr}
</option>
{/foreach}
</select>
</div>
</div>
<div class="form-group product_autocomplete">
<div class="col-md-2 control-label"><label>{'product'|translate}</label></div>
<div class="col-md-6">
<input type="text" data-autocomplete-search="product" autocomplete="off" name="data[product]"
class="form-control input-sm autocomplete-control" placeholder="Vyhledat produkt"
data-preload="product_variation" value="{if $body.data.id_product}{$body.data.id_product}{if $body.data.id_variation}-{$body.data.id_variation}{/if}{/if}">
<input type="hidden" name="data[id_product]" value="{$body.data.id_product}">
<input type="hidden" name="data[id_variation]" value="{$body.data.id_variation}">
<script type="application/javascript">
$('[data-autocomplete-search="product"]').adminVariationAutoComplete({
allowSelectProduct: false,
select: function (e, item) {
let { product, variation } = item;
if (variation && typeof variation !== 'string' && variation.id) {
variation = variation.id
}
$('[name="data[id_product]"]').val(product.id);
$('[name="data[id_variation]"]').val(variation);
return true;
}
});
</script>
</div>
<div class="col-md-4 toggle-wrapper">
<div class="checkbox">
{$checked = ''}
{if $body.data.data.order_not_in_store}
{$checked = 'checked'}
{/if}
<input type="checkbox" class="check" value="1" id="order_not_in_store"
name="data[data][order_not_in_store]" {$checked}>
<label class="small" for="order_not_in_store">Povolit objednat zboží, které není skladem</label>
</div>
</div>
</div>
<div class="row form-group">
<div class="col-md-2"></div>
<div class="col-md-10 d-flex align-center">
{print_toggle value=$body.data.required|default:'Y' name="required"}
<label>{'charge_required'|translate}
<a class="help-tip" data-toggle="tooltip" title="{'charge_required_help'|translate}"><i
class="bi bi-question-circle"></i></a>
</label>
</div>
</div>
<div class="row form-group">
<div class="col-md-2"></div>
<div class="col-md-10 d-flex align-center">
{print_toggle value=$body.data.checked|default:'N' name="checked"}
<label>{'charge_checked'|translate}
<a class="help-tip" data-toggle="tooltip" title="{'charge_checked_help'|translate}"><i
class="bi bi-question-circle"></i></a>
</label>
</div>
</div>
<div class="row form-group">
<div class="col-md-2"></div>
<div class="col-md-10 d-flex align-center">
{print_toggle value=$body.data.included|default:'N' name="included"}
<label>{'charge_included'|translate}
<a class="help-tip" data-toggle="tooltip" title="{'charge_included_help'|translate}"><i
class="bi bi-question-circle"></i></a>
</label>
</div>
</div>
<div class="row form-group">
<div class="col-md-2"></div>
<div class="col-md-10 d-flex align-center">
{print_toggle value=$body.data.data.onetime|default:'N' nameRaw="data[data][onetime]"}
<label>{'charge_onetime'|translate}
<a class="help-tip" data-toggle="tooltip" title="{'charge_onetime_help'|translate}"><i
class="bi bi-question-circle"></i></a>
</label>
</div>
</div>
</div>
{block "custom-data"}{/block}
{/block}

View File

@@ -0,0 +1,41 @@
<?php
namespace KupShop\ProductsChargesBundle\AdminRegister;
use KupShop\AdminBundle\AdminRegister\AdminRegister;
use KupShop\AdminBundle\AdminRegister\IAdminRegisterDynamic;
use KupShop\AdminBundle\AdminRegister\IAdminRegisterStatic;
class ProductsChargesAdminRegister extends AdminRegister implements IAdminRegisterDynamic, IAdminRegisterStatic
{
public function getDynamicMenu(): array
{
$menu = [
static::createMenuItem('productsMenu',
[
'name' => 'productsCharges',
'title' => translate('navigation', 'productsCharges'),
'left' => 's=menu.php&type=productsCharges',
'right' => 's=list.php&type=productsCharges',
]),
];
if (findModule(\Modules::TRANSLATIONS)) {
$menu[] = self::createMenuItem('translate',
[
'name' => 'translate.translateProductsCharges',
'title' => translate('navigation', 'productsCharges'),
'right' => 's=list.php&type=translateProductsCharges',
]);
}
return $menu;
}
public function getDynamicPermissions(): array
{
return [
static::createPermissions('productsCharges', [\Modules::PRODUCTS_CHARGES], ['CHARGES']),
];
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace KupShop\ProductsChargesBundle\EventListener;
use KupShop\KupShopBundle\Util\Price\Price;
use KupShop\OrderingBundle\Event\OrderItemEvent;
use KupShop\OrderingBundle\Util\Order\OrderItemInfo;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class OrderItemListener implements EventSubscriberInterface
{
use \DatabaseCommunication;
public static function getSubscribedEvents()
{
return [
OrderItemEvent::ITEM_CREATED => [
['itemCharge', 200],
],
];
}
public function itemCharge(OrderItemEvent $event)
{
$product = $event->getProduct();
$data = $event->getData();
$items_table = $data['items_table'];
$row = $data['row'];
$note = $product->parseNote($row['note'] ?? false);
// ----------------------------------------------------------------
// kdyz je u zbozi příplatek, ktery nebyl jeste zapocitan v cene
foreach ($product->fetchCharges($note['charges'] ?? []) as $charge) {
if ($charge['included'] == 'N' && $charge['active']) {
/** @var Price $price */
$price = $charge['price'];
$PiecePrice = $price->getPriceWithoutVat();
if (($charge['data']['onetime'] ?? 'N') === 'Y') {
$chargePieces = \DecimalConstants::one();
} else {
$chargePieces = toDecimal($row['pieces']);
}
$TotalPrice = $PiecePrice->mul($chargePieces);
$itemData = [
'charge' => [
'id_product_charge' => $charge['id'],
'id_charge' => $charge['id_charge'],
'id_product' => $charge['id_product'],
'id_item_parent' => $row['id'],
],
'item_type' => OrderItemInfo::TYPE_CHARGE,
];
$productTitle = !empty($product->title) ? $product->title : $product->descr;
$chargeTitle = !empty($charge['title']) ? $charge['title'] : $charge['admin_title'];
if (!empty($charge['product']['code'])) {
$code = str_replace('%CODE', trim($charge['product']['code']), translate('code', 'order'));
$chargeTitle = "{$chargeTitle} {$code}";
}
$productTitle = "{$chargeTitle}: {$productTitle}";
$charge_product = $charge['product'] ?? null;
$id_product = ($charge_product ? $charge_product->id : null);
$id_variation = ($charge_product ? $charge_product->variationId : null);
// zapsat polozku do DB
$this->insertSQL(
$items_table,
[
'id_order' => $row['id_order'],
'id_product' => $id_product,
'id_variation' => $id_variation,
'pieces' => $chargePieces,
'pieces_reserved' => $chargePieces,
'piece_price' => $PiecePrice,
'total_price' => $TotalPrice,
'descr' => $productTitle,
'tax' => $this->getChargeItemVat($product, $charge),
'note' => json_encode($itemData),
]
);
if ($charge_product) {
$charge_product->sell($id_variation, $row['pieces']);
}
// total price of whole order
$orderTotalPrice = $event->getPrice();
$orderTotalPrice = $orderTotalPrice->add($TotalPrice->addVat($this->getChargeItemVat($product, $charge)));
// update total price of order
$event->setPrice($orderTotalPrice);
}
}
}
protected function getChargeItemVat(\Product $product, array $charge): float
{
return $charge['price']->getVat()->asFloat();
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace KupShop\ProductsChargesBundle\EventListener;
use KupShop\KupShopBundle\Util\Price\Price;
use KupShop\OrderingBundle\Entity\Purchase\ChargePurchaseItem;
use KupShop\OrderingBundle\Entity\Purchase\ProductPurchaseItem;
use KupShop\OrderingBundle\Event\PurchaseStateCreatedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class PurchaseStateListener implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
PurchaseStateCreatedEvent::class => [
['addProductCharges', 200],
],
];
}
public function addProductCharges(PurchaseStateCreatedEvent $event): void
{
foreach ($event->getPurchaseState()->getProducts() as $purchaseItem) {
$this->addProductChargesToPurchaseItem(
$purchaseItem,
$purchaseItem->getProduct()->fetchCharges(
$purchaseItem->getNote()['charges'] ?? []
)
);
}
}
private function addProductChargesToPurchaseItem(ProductPurchaseItem $purchaseItem, array $charges): void
{
foreach ($charges as $charge) {
if (!($charge['included'] == 'N' && $charge['active'])) {
continue;
}
/** @var Price $price */
$price = $charge['price'];
if (($charge['data']['onetime'] ?? 'N') === 'Y') {
$chargePieces = \DecimalConstants::one();
} else {
$chargePieces = toDecimal($purchaseItem->getPieces());
}
$totalPrice = new Price($price->getPriceWithoutVat()->mul($chargePieces), $price->getCurrency(), $price->getVat());
$purchaseItem->addAdditionalItem(
new ChargePurchaseItem($charge['title'], $totalPrice, $charge['id'])
);
}
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace KupShop\ProductsChargesBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class ProductsChargesBundle extends Bundle
{
}

View File

@@ -0,0 +1,7 @@
services:
_defaults:
autoconfigure: true
autowire: true
KupShop\ProductsChargesBundle\:
resource: ../../{Actions,AdminRegister,EventListener,Translations,Util}

View File

@@ -0,0 +1,236 @@
<?php
namespace KupShop\ProductsChargesBundle\Resources\upgrade;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Util\Contexts;
class ProductsChargesUpgrade extends \UpgradeNew
{
public function check_ChargesTable()
{
return $this->checkTableExists('charges');
}
/** Create product charges table */
public function upgrade_ChargesTable()
{
sqlQuery('CREATE TABLE charges (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(100) NOT NULL,
price DECIMAL(15,4) NOT NULL,
vat INT(11) UNSIGNED,
FOREIGN KEY (vat) REFERENCES vats(id) ON DELETE SET NULL ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
');
sqlQuery('DROP TABLE IF EXISTS products_charges;');
sqlQuery('CREATE TABLE products_charges (
id INT AUTO_INCREMENT PRIMARY KEY,
id_charge INT NOT NULL,
id_product INT NOT NULL,
included ENUM("Y", "N") DEFAULT "N" NOT NULL,
required ENUM("Y", "N") DEFAULT "Y" NOT NULL,
FOREIGN KEY (id_charge) REFERENCES charges(id) ON DELETE CASCADE ON UPDATE CASCADE,
FOREIGN KEY (id_product) REFERENCES products(id) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
');
$this->upgradeOK();
}
public function check_chargesRequired()
{
return $this->checkColumnExists('charges', 'required');
}
/** Add columns to `charges` table */
public function upgrade_chargesRequired()
{
sqlQuery('ALTER TABLE charges
ADD COLUMN descr TEXT DEFAULT NULL,
ADD COLUMN included ENUM("Y", "N") DEFAULT "N" NOT NULL,
ADD COLUMN required ENUM("Y", "N") DEFAULT "Y" NOT NULL,
ADD COLUMN checked ENUM("Y", "N") DEFAULT "N" NOT NULL,
ADD COLUMN orders_charge ENUM("Y", "N") DEFAULT "N" NOT NULL;
ALTER TABLE products_charges DROP COLUMN included, DROP COLUMN required;');
$this->upgradeOK();
}
public function check_chargesTranslations()
{
return findModule(\Modules::TRANSLATIONS) && $this->checkTableExists('charges_translations');
}
/** Create charges_translations table */
public function upgrade_chargesTranslations()
{
sqlQuery('CREATE TABLE IF NOT EXISTS charges_translations
(
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
id_charge INT NOT NULL,
id_language VARCHAR(2) NOT NULL,
id_admin INT DEFAULT NULL,
created TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated DATETIME DEFAULT NULL,
title VARCHAR(100) DEFAULT NULL,
descr TEXT DEFAULT NULL,
FOREIGN KEY (id_charge)
REFERENCES charges (id)
ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE UNIQUE INDEX charges_translations_id_charge_id_language_uindex
ON charges_translations (id_charge, id_language);
');
$this->upgradeOK();
}
public function check_RemoveCharge()
{
return !$this->checkColumnExists('products', 'charge_id');
}
/** Remove product.charge_id, product.charge_included */
public function upgrade_RemoveCharge()
{
sqlQuery('alter table products
drop column charge_id,
drop column charge_included');
$this->upgradeOK();
}
public function check_chargesPercentage()
{
return $this->checkColumnExists('charges', 'percentage');
}
/** Add percentage to `charges` table */
public function upgrade_chargesPercentage()
{
sqlQuery('ALTER TABLE charges
ADD COLUMN percentage SMALLINT NULL,
MODIFY price decimal(15,4) NULL;
');
$this->upgradeOK();
}
public function check_ChargeCurrencyColumn(): bool
{
return findModule(\Modules::CURRENCIES) && $this->checkColumnExists('charges', 'currency');
}
public function upgrade_ChargeCurrencyColumn(): void
{
$defaultCurrency = Contexts::get(CurrencyContext::class)->getDefaultId();
sqlQuery('ALTER TABLE charges ADD COLUMN currency VARCHAR(3) DEFAULT ? NOT NULL AFTER id ', [$defaultCurrency]);
sqlQuery('
ALTER TABLE charges ADD CONSTRAINT fk_charges_currencies FOREIGN KEY (currency)
REFERENCES currencies(id)
ON DELETE CASCADE ON UPDATE CASCADE;
');
$this->upgradeOK();
}
public function check_ChargeAdminTitleColumn(): bool
{
return $this->checkColumnExists('charges', 'admin_title');
}
public function upgrade_ChargeAdminTitleColumn(): void
{
sqlQuery('ALTER TABLE charges ADD COLUMN admin_title VARCHAR(100) NOT NULL AFTER title ');
$this->upgradeOK();
}
public function check_ChargesDataColumn()
{
return $this->checkColumnExists('charges', 'data');
}
/** Add 'data' column into table 'charges' */
public function upgrade_ChargesDataColumn()
{
sqlQuery('ALTER TABLE charges ADD COLUMN data MEDIUMTEXT DEFAULT NULL');
$this->upgradeOK();
}
public function check_RemoveOrdersCharge()
{
return !$this->checkColumnExists('charges', 'orders_charge');
}
/** Remove charges.orders_charge, add charges.type column */
public function upgrade_RemoveOrdersCharge()
{
sqlQuery("ALTER TABLE charges ADD COLUMN type ENUM('order', 'product') DEFAULT 'product'");
sqlQuery("UPDATE charges SET type = 'order' WHERE orders_charge = 'Y'");
sqlQuery('ALTER TABLE charges DROP COLUMN orders_charge');
$this->upgradeOK();
}
public function check_ChargesProductColumn()
{
return $this->checkColumnExists('charges', 'id_product');
}
/** Add id_product column into table 'charges' */
public function upgrade_ChargesProductColumn()
{
sqlQuery('ALTER TABLE charges ADD COLUMN id_product int DEFAULT NULL');
sqlQuery('ALTER TABLE charges ADD CONSTRAINT charges_products_id_fk
FOREIGN KEY (id_product) REFERENCES products (id)
ON UPDATE CASCADE ON DELETE SET NULL');
$this->upgradeOK();
}
public function check_ChargesProductVariationColumn()
{
return findModule(\Modules::PRODUCTS_VARIATIONS) && $this->checkColumnExists('charges', 'id_variation');
}
/** Add id_variation column into table 'charges' */
public function upgrade_ChargesProductVariationColumn()
{
sqlQuery('ALTER TABLE charges ADD COLUMN id_variation int DEFAULT NULL');
sqlQuery('ALTER TABLE charges ADD CONSTRAINT charges_products_variations_id_fk
FOREIGN KEY (id_variation) REFERENCES products_variations (id)
ON UPDATE CASCADE ON DELETE SET NULL');
$this->upgradeOK();
}
public function check_ChargesFigureColumn(): bool
{
return $this->checkColumnExists('charges', 'figure');
}
/** charges: add `figure` column */
public function upgrade_ChargesFigureColumn(): void
{
sqlQuery('ALTER TABLE charges ADD COLUMN figure ENUM("Y", "N") DEFAULT "Y" NOT NULL AFTER admin_title');
$this->upgradeOK();
}
public function check_ChargesTranslationsFigureColumn(): bool
{
return findModule(\Modules::TRANSLATIONS) && $this->checkColumnExists('charges_translations', 'figure');
}
/** charges_translations: add `figure` column */
public function upgrade_ChargesTranslationsFigureColumn(): void
{
sqlQuery('ALTER TABLE charges_translations ADD COLUMN figure ENUM("Y", "N") DEFAULT NULL AFTER title');
$this->upgradeOK();
}
}

View File

@@ -0,0 +1,175 @@
{
"charges": [
{
"id": 1,
"title": "Volitelny za 100",
"figure": "Y",
"price": "82.6446",
"vat": 1,
"descr": "pokus",
"included": "N",
"required": "N",
"checked": "Y",
"type": "product"
},
{
"id": 2,
"title": "povinny",
"figure": "Y",
"price": "100.0000",
"vat": 1,
"descr": "nejaky ten popis",
"included": "N",
"required": "Y",
"checked": "N",
"type": "product"
},
{
"id": 3,
"title": "V ceně",
"figure": "Y",
"price": "60.0000",
"vat": 1,
"descr": null,
"included": "Y",
"required": "Y",
"checked": "N",
"type": "product"
},
{
"id": 4,
"title": "Objednavkovy priplatek 100 Kc",
"figure": "Y",
"price": "82.6446",
"vat": 1,
"descr": "pokus",
"included": "N",
"required": "Y",
"checked": "N",
"type": "order"
},
{
"id": 6,
"title": "Ekologicke balení",
"figure": "Y",
"price": "82.6446",
"vat": 1,
"descr": "pokus",
"included": "N",
"required": "N",
"checked": "N",
"type": "order"
},
{
"id": 7,
"title": "Skryty priplatek",
"figure": "N",
"price": "82.6446",
"vat": 1,
"descr": "pokus",
"included": "N",
"required": "Y",
"checked": "N",
"type": "product"
},
{
"id": 8,
"title": "Produktovy priplatek za 200",
"figure": "N",
"price": "165.2893",
"vat": 1,
"descr": "pokus",
"included": "N",
"required": "N",
"checked": "Y",
"type": "product"
}
],
"products_charges": [
{
"id": 1,
"id_charge": 1,
"id_product": 1
},
{
"id": 2,
"id_charge": 3,
"id_product": 3
},
{
"id": 3,
"id_charge": 2,
"id_product": 2
},
{
"id": 4,
"id_charge": 7,
"id_product": 8
},
{
"id": 5,
"id_charge": 8,
"id_product": 3
}
],
"order_discounts": [
{
"id": 56,
"date_created": "2022-03-28 09:36:01",
"name": "Dobrovolný příplatek",
"display_name": "",
"uses_count": 4,
"active": "Y",
"data": null,
"position": 48
},
{
"id": 55,
"date_created": "2022-03-28 09:36:01",
"name": "Pojištění dopravy zásilky",
"display_name": "",
"uses_count": 4,
"active": "Y",
"data": null,
"position": 48
},
{
"id": 54,
"date_created": "2022-03-28 09:32:50",
"name": "Balné",
"display_name": "",
"uses_count": 3,
"active": "Y",
"data": null,
"position": 47
}
],
"order_discounts_triggers": [
{
"id": 112,
"id_order_discount": 54,
"type": "price_range",
"data": "{\"min\":\"\",\"max\":\"500\",\"withVat\":\"1\"}"
}
],
"order_discounts_actions": [
{
"id": 64,
"id_order_discount": 55,
"type": "orders_charge",
"data": "{\"id_charge\":\"4\"}"
},
{
"id": 63,
"id_order_discount": 54,
"type": "orders_charge",
"data": "{\"id_charge\":\"5\"}"
},
{
"id": 65,
"id_order_discount": 56,
"type": "orders_charge",
"data": "{\"id_charge\":\"6\"}"
}
]
}

View File

@@ -0,0 +1,173 @@
<?php
namespace KupShop\ProductsChargesBundle\Tests;
use KupShop\DevelopmentBundle\Util\Tests\CartTestTrait;
use KupShop\OrderingBundle\Entity\Purchase\ProductPurchaseItem;
use KupShop\OrderingBundle\Util\Order\OrderItemInfo;
use Query\Operator;
class ChargesTest extends \DatabaseTestCase
{
use CartTestTrait;
/**
* BUG: Doprava nebyla v objednavce zdarma pokud ji aktivoval produktovy priplatek.
*
* Pokud mam v kosiku produkt a pridam produktovy priplatek, diky kteremu ziskam narok na dopravu zdarma, tak se doprava zdarma musi
* projevit i do objednavky.
*/
public function testProductChargeWithFreeDelivery(): void
{
$this->setActiveCharges([8]);
sqlQueryBuilder()->update('order_discounts')->directValues(['active' => 'N'])->execute();
$this->prepareCart();
$this->insertProduct(3, pieces: 2);
$this->setDeliveryType(4);
$this->recalculateCart();
$products = $this->cart->getPurchaseState()->getProducts();
$this->assertCount(1, $products, 'V košíku je jeden produkt');
/** @var ProductPurchaseItem $product */
$product = reset($products);
$this->assertCount(1, $product->getAdditionalItems(), 'Produkt ma příplatek');
// tohle failovalo, protoze pri vytvoreni objednavky se produktovy priplatek nezapocital a doprava nebyla v objednavce zdarma
$this->checkOrderPriceIsSameAsCart();
}
/** @dataProvider data_testProductFetchCharges */
public function testProductFetchCharges(int $productId, int $expectedCount): void
{
$product = new \Product();
$product->createFromDB($productId);
$this->assertCount($expectedCount, $product->fetchCharges());
}
public function data_testProductFetchCharges(): iterable
{
yield 'Priplatek u produktu' => [1, 1];
yield 'Skryty priplatek u produktu nesmi byt nacten' => [8, 0];
}
public function testHiddenProductChargeIsNotUsed(): void
{
$this->prepareCart();
$this->insertProduct(8);
$this->recalculateCart();
$this->assertEquals(5090, $this->cart->totalPricePay->asFloat(), 'Cena produktu je 5090 Kc a cena priplatku u nej je 100 Kc, ale priplatek neni aktivni, takze nesmi byt pricteny');
$this->checkOrderPriceIsSameAsCart();
}
public function testDefaultOnCharge()
{
$this->prepareCart();
$this->insertProduct(1, 16);
$this->recalculateCart();
// Cena je 800 za produkt + 100 za příplatek + povinny Objednavkovy priplatek 100 Kc
$this->assertEquals(1000, $this->cart->totalPricePay->asFloat());
$this->checkOrderPriceIsSameAsCart();
}
public function testDefaultOnTurnedOffCharge()
{
$this->prepareCart();
$itemId = $this->insertProduct(1, 16);
$this->cart->updateItem($itemId, ['note' => ['charges' => ['1' => '']]]);
$this->recalculateCart();
// Cena je 800 za produkt + 0 za vyonutý příplatek + povinny Objednavkovy priplatek 100 Kc
$this->assertEquals(900, $this->cart->totalPricePay->asFloat());
$this->checkOrderPriceIsSameAsCart();
}
public function testChargeWithFreeDelivery()
{
// dorpava zdarma od 6150 -> celková cena objednávky je 5999 produkt + 100 povinný příplatek + 100 volitelný příplatek
sqlQueryBuilder()->update('delivery_type')->set('price_dont_countin_from', 6150)->where(Operator::equals(['id' => 1]))->execute();
$this->prepareCart();
$this->cart->setActionHandlersData([65 => ['checked' => 'Y']]); // nepovinný příplatek - 100
// Doprava a platba - 100Kč (zdarma od 6150Kč)
$this->cart->setDeliveryAndPayment(3, 3);
// Cena produktu 5999 Kč + 100Kč příplatek
$this->insertProduct(9);
$this->checkOrderPriceIsSameAsCart();
}
public function testRequiredCharge()
{
$this->prepareCart();
$this->insertProduct(2, 9);
$this->recalculateCart();
// Cena je 2250 za produkt + 121 za příplatek + povinny Objednavkovy priplatek 100 Kc
$this->assertEquals(2471, $this->cart->totalPricePay->asFloat());
$this->checkOrderPriceIsSameAsCart();
}
public function testOrderCharge()
{
$orderItemInfo = $this->get(OrderItemInfo::class);
$this->prepareCart();
$this->insertProduct(8);
$this->cart->charges[] = 4;
$this->recalculateCart();
// Cena je 4990 za produkt + 100 za příplatek
$this->assertEquals(5090, $this->cart->totalPricePay->asFloat());
$this->checkOrderPriceIsSameAsCart();
$items = $this->order->fetchItems();
$this->assertCount(2, $items, '1 item + 1 order charge should be in items');
$orderChargeItem = null;
foreach ($items as $item) {
if ($orderItemInfo->getItemType($item) === OrderItemInfo::TYPE_CHARGE) {
$orderChargeItem = $item;
}
}
$this->assertNotEmpty($orderChargeItem, 'Check that order charge item exists');
}
protected function getDataSet()
{
return $this->getJsonDataSetFromFile();
}
private function setActiveCharges(array $ids): void
{
sqlQueryBuilder()
->update('charges')
->directValues(['figure' => 'N'])
->execute();
sqlQueryBuilder()
->update('charges')
->directValues(['figure' => 'Y'])
->where(Operator::inIntArray($ids, 'id'))
->execute();
}
}

View File

@@ -0,0 +1,43 @@
{
"charges": [
{
"id": 999,
"title": "Procentuální 1000%",
"price": 0.0,
"vat": 1,
"percentage": 1000,
"descr": "pokus",
"included": "Y",
"required": "Y",
"checked": "Y",
"type": "order"
}
],
"products_charges": [
{
"id": 1,
"id_charge": 999,
"id_product": 8
}
],
"order_discounts": [
{
"id": 10,
"date_created": "2022-03-28 09:32:50",
"name": "Foo Bar 1000%",
"display_name": "",
"uses_count": 3,
"active": "Y",
"data": null,
"position": 20
}
],
"order_discounts_actions": [
{
"id": 10,
"id_order_discount": 10,
"type": "orders_charge",
"data": "{\"id_charge\":\"999\"}"
}
]
}

View File

@@ -0,0 +1,26 @@
<?php
namespace KupShop\ProductsChargesBundle\Tests;
use KupShop\DevelopmentBundle\Util\Tests\CartTestTrait;
class OrdersChargesTest extends \DatabaseTestCase
{
use CartTestTrait;
public function testOrderWithPercentCharge(): void
{
$this->createCart();
$this->assertEquals(0, $this->cart->totalPricePay->asFloat());
$this->insertProduct(8);
$this->recalculateCart();
$this->assertEquals(4990 * 11, $this->cart->getPurchaseState()->getTotalPrice()->getPriceWithVat()->asFloat());
$this->assertEquals(4990 * 10, $this->cart->getPurchaseState()->getChargesTotalPrice()->getPriceWithVat()->asFloat());
$this->assertEquals(4990, $this->cart->getPurchaseState()->getProductsTotalPrice()->getPriceWithVat()->asFloat());
}
protected function getDataSet()
{
return $this->getJsonDataSetFromFile();
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace KupShop\ProductsChargesBundle\Translations;
use KupShop\I18nBundle\Translations\BaseTranslation;
class ProductsChargesTranslation extends BaseTranslation
{
protected $columns = [
'title' => ['alias' => 'Název', 'maxlength' => 100],
'descr' => ['alias' => 'Popis', 'richtext' => true],
'figure' => ['alias' => 'Aktivní', 'select' => [null => 'Nenastavovat', 'Y' => 'Ano', 'N' => 'Ne']],
];
protected $tableName = 'charges';
protected $tableAlias = 'ch';
protected $nameColumn = 'title';
}

View File

@@ -0,0 +1,102 @@
<?php
namespace KupShop\ProductsChargesBundle\Util;
use KupShop\I18nBundle\Entity\Currency;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Context\VatContext;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\KupShopBundle\Util\Price\Price;
use KupShop\KupShopBundle\Util\Price\PriceCalculator;
class ChargesUtil
{
/** @var VatContext */
protected $vatContext;
/** @required */
public function setVatContext(VatContext $vatContext)
{
$this->vatContext = $vatContext;
}
/**
* @param null $data
* @param \Variation $product
*
* @return array
*
* @throws \Exception
*/
public function applyData(array $charges, $data = null, $product = null)
{
if ($data === false) {
return $charges;
}
foreach ($charges as &$charge) {
$charge['active'] = $this->isChargeActive($charge ?: [], $data);
}
return $charges;
}
public function applyPrices(array $charges, ?\Product $product = null): array
{
$activeCurrency = Contexts::get(CurrencyContext::class)->getActive();
foreach ($charges as &$charge) {
if ($product && ($charge['percentage'] ?? false)) {
$price = $product->getPrice($product->variationId, [], toDecimal(1));
$charge['price'] = $this->applyPercentageCharge($price, $charge['percentage'], $charge['price']->getCurrency(), $product['vat']);
}
$vat = null;
if ($charge['product']) {
// uplatnit sazbu DPH podle priplatkoveho produktu
$vat = $charge['product']->getProductPrice()->getVat();
} elseif (is_null($charge['vat'])) { // null = "podle produktů"
// uplatnit sazbu DPH podle produktu
$vat = $product?->getProductPrice()?->getVat();
}
if (!empty($vat) && ($charge['price']->getVat() != $vat)) {
$charge['price'] = new Price($charge['price']->getValue(), $charge['price']->getCurrency(), $vat);
}
$charge['price'] = PriceCalculator::convert($charge['price'], $activeCurrency);
}
return $charges;
}
/**
* @param $price \Decimal
* @param $percentage int
* @param $currency Currency
*
* @return Price
*
* @throws \Exception
*/
public function applyPercentageCharge($price, $percentage, $currency, $vat)
{
return new Price($price->div(toDecimal(100))->mul(toDecimal($percentage)), $currency, $vat);
}
protected function isChargeActive(array $charge, ?array $data = null): bool
{
$id = $charge['id'];
$active = $charge['checked'] == 'Y';
if (isset($data[$id])) {
$active = ($data[$id] == 'Y' ? true : ($data[$id] == 'N' ? false : (bool) $data[$id]));
}
if ($charge['required'] == 'Y' || $charge['included'] == 'Y') {
$active = true;
}
return $active;
}
}