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,20 @@
<?php
namespace KupShop\PricelistBundle\Admin\lists;
class PricelistList extends \KupShop\AdminBundle\AdminList\BaseList
{
protected $tableDef = [
'id' => 'id',
'fields' => [
'ID' => ['field' => 'id', 'size' => 0.1],
'Název' => ['field' => 'name'],
'Měna' => ['field' => 'currency'],
],
];
protected $tableName = 'pricelists';
protected ?string $tableAlias = 'pl';
}
return PricelistList::class;

View File

@@ -0,0 +1,130 @@
<?php
use Query\Operator;
class Pricelist extends Window
{
protected $nameField = 'name';
protected $tableName = 'pricelists';
/** @var \Doctrine\ORM\EntityManager */
private $em;
public function __construct()
{
$this->em = \KupShop\KupShopBundle\Util\Compat\ServiceContainer::getService('doctrine.orm.entity_manager');
}
public function get_vars()
{
$vars = parent::get_vars();
$pageVars = getVal('body', $vars);
$pageVars['currencies'] = $this->getCurrencies();
$this->unserializeCustomData($pageVars['data']);
$vars['body'] = $pageVars;
return $vars;
}
public function getData()
{
$data = parent::getData();
if (!empty($data)) {
if (findModule(Modules::PRICE_HISTORY)) {
$data['price_history'] = ($data['price_history'] === 'Y' || $data['price_history'] == 1) ? 1 : 0;
}
$data['data'] = array_merge($this->getCustomData(), $data['data'] ?? []);
$this->serializeCustomData($data);
}
return $data;
}
public function handleUpdate()
{
$currentPriceHistory = $this->getObject()['price_history'] ?? null;
$result = parent::handleUpdate();
if (findModule(Modules::PRICE_HISTORY) && $this->getID()) {
$priceHistory = $this->getData()['price_history'] ?? false;
// pokud se zmenila hodnota v poli price_history
if ($currentPriceHistory != $priceHistory) {
// historie cen byla zapnuta
if ($priceHistory) {
// Insertnu prvotni historii
sqlQuery('
INSERT INTO price_history (id_pricelist, id_product, id_variation, price, last, date_change, up)
SELECT
pp.id_pricelist,
pp.id_product,
pp.id_variation,
pp.price,
1,
DATE_SUB(NOW(), INTERVAL 1 DAY),
1
FROM pricelists_products pp
JOIN products p ON p.id = pp.id_product
WHERE (pp.price > 0) AND p.figure = "Y" AND pp.id_pricelist = :priceListId;
',
['priceListId' => $this->getID()]);
// Nastavit CPS
sqlQueryBuilder()
->update('pricelists_products', 'pp')
->join('pp', 'products', 'p', 'p.id = pp.id_product')
->leftJoin('pp', 'products_variations', 'pv', 'pv.id = pp.id_variation')
->set('pp.price_for_discount', 'COALESCE(pp.price, pv.price, p.price)')
->where(Operator::equals(['pp.id_pricelist' => $this->getID()]))
->execute();
} else { // historie cen u ceniku byla vypnuta
// Smazani CPS
sqlQueryBuilder()
->update('pricelists_products')
->directValues(['price_for_discount' => null])
->where(Operator::equals(['id_pricelist' => $this->getID()]))
->execute();
// Smazani historie
sqlQueryBuilder()
->delete('price_history')
->where(Operator::equals(['id_pricelist' => $this->getID()]))
->execute();
}
}
}
return $result;
}
private function getCurrencies()
{
$repository = $this->em->getRepository(\KupShop\I18nBundle\Entity\Currency::class);
return $repository->findAll();
}
public function handleGeneratePricelistToken(): void
{
$token = bin2hex(random_bytes(16));
sqlQueryBuilder()
->update('pricelists')
->directValues(['token' => $token])
->where(Operator::equals(['id' => $this->getID()]))
->execute();
$this->returnOK();
}
}
$countries = new Pricelist();
$countries->run();

View File

@@ -0,0 +1,301 @@
<?php
class ProductsPricelists extends Window
{
public $priceListWorker;
public $vat;
protected $template = 'window/products.pricelists.tpl';
private $productId;
/** @var \Doctrine\ORM\EntityManager */
private $em;
public function __construct()
{
$this->em = \KupShop\KupShopBundle\Util\Compat\ServiceContainer::getService('doctrine.orm.entity_manager');
$this->priceListWorker = \KupShop\KupShopBundle\Util\Compat\ServiceContainer::getService(\KupShop\PricelistBundle\Util\PriceListWorker::class);
}
public function get_vars()
{
$dbcfg = Settings::getDefault();
$vars = parent::get_vars();
$pageVars = getVal('body', $vars, []);
$pageVars['ID'] = $this->getProductId();
$pageVars['defaultPricelist'] = $this->getDefaultPricelist();
$pageVars['defaultCurrency'] = $this->getDefaultCurrency();
$pageVars['pricelists'] = $this->getPricelists();
$pageVars['data'] = $this->getPricelistData();
$pageVars['variations'] = $this->getVariations();
$pageVars['vat'] = $this->getVat();
$pageVars['data']['showPricesWithVat'] = $dbcfg['prod_prefer_price_vat'] == 'Y' || $dbcfg['prod_prefer_price_vat'] == 'F';
$vars['body'] = $pageVars;
return $vars;
}
public function handle()
{
parent::handle();
$data = parent::getData();
unset($data['ID'], $data['showPricesWithVat']);
if (!empty($data)) {
$this->handleUpdatePricelists();
}
}
public function handleUpdatePricelists()
{
$data = parent::getData();
$showVat = $data['showPricesWithVat'];
unset($data['ID']);
unset($data['showPricesWithVat']);
foreach ($data as $pricelistId => $options) {
$options['showVat'] = $showVat;
if ($pricelistId > 0) {
$this->priceListWorker->updatePricelists($pricelistId, $options, $this->getProductId(), $this->getVat());
// variations update
if (isset($options['vars']) && $options['vars'] == 'true') {
foreach ($options['priceVar'] as $varId => $price) {
$discount = $options['discountVar'][$varId] ?? '';
$varOptions = [
'price' => $this->preparePrice($price),
'discount' => $this->preparePrice($discount),
'showVat' => $showVat,
];
$this->priceListWorker->updatePricelists($pricelistId, $varOptions, $this->getProductId(), $this->getVat(), $varId);
}
}
// updating default pricelist
} elseif ($pricelistId == 0) {
$this->updateDefaultPricelist($options);
// variations update in default pricelist
if (isset($options['vars']) && $options['vars'] == 'true') {
foreach ($options['priceVar'] as $varId => $price) {
$this->updateDefaultVariations($varId, $this->preparePrice($price), $options['showVat']);
}
}
}
}
$this->returnOK();
}
public function handleUpdate()
{
if (getVal('submitUpdatePricelists')) {
$data = parent::getData();
$showVat = $data['showPricesWithVat'];
unset($data['ID']);
unset($data['showPricesWithVat']);
foreach ($data as $pricelistId => $options) {
$options['showVat'] = $showVat;
if ($pricelistId > 0) {
$this->priceListWorker->updatePricelists($pricelistId, $options, $this->getProductId(), $this->getVat());
// variations update
if (isset($options['vars']) && $options['vars'] == 'true') {
foreach ($options['priceVar'] as $varId => $price) {
$this->priceListWorker->updatePricelists($pricelistId, ['price' => $this->preparePrice($price), 'showVat' => $showVat], $this->getProductId(), $this->getVat(), $varId);
}
}
// updating default pricelist
} elseif ($pricelistId == 0) {
$this->updateDefaultPricelist($options);
// variations update in default pricelist
if (isset($options['vars']) && $options['vars'] == 'true') {
foreach ($options['priceVar'] as $varId => $price) {
$this->updateDefaultVariations($varId, $this->preparePrice($price), $options['showVat']);
}
}
}
}
}
return true;
}
public function updateDefaultPricelist($data)
{
// vat remove before saving to database
if ($data['showVat'] == 'Y') {
$data['price'] = $this->priceListWorker->removeVatFromPrice($this->preparePrice($data['price']), $this->getVat());
}
$price = ['price' => $data['price'], 'discount' => $data['discount']];
$where = ['id' => $this->getProductId()];
$this->updateSQL('products', $price, $where);
}
public function updateDefaultVariations($varId, $price, $showVat)
{
$price = Decimal::create($price);
// vat remove before saving to database
if ($showVat == 'Y') {
$price = $this->priceListWorker->removeVatFromPrice($price, $this->getVat());
}
if ($price->isZero()) {
$price = null;
}
$this->updateSQL('products_variations', ['price' => $price], ['id' => $varId]);
}
/**
* @param \KupShop\PricelistBundle\Entity\Pricelist $pricelist
* @param Decimal $price
*
* @return Decimal
*/
public function recalcPriceFromCZK($pricelist, $price)
{
if ($pricelist) {
if ($price) {
return $price->div($pricelist->getCurrency()->getRate());
}
} else {
if ($price) {
$currency = $this->getDefaultCurrency();
$rate = Decimal::create($currency['rate']);
return $price->div($rate);
}
}
return $price;
}
/**
* @param \KupShop\I18nBundle\Entity\Currency $currency
* @param string $price
*
* @return Decimal
*/
public function recalcPriceToCZK($currency, $price)
{
$price = Decimal::create($price);
return $price->mul($currency->getRate());
}
public function getDefaultCurrency()
{
$dbcfg = Settings::getDefault();
return sqlQueryBuilder()
->select('id as code, symbol, rate')
->from('currencies')
->where('id = ?')->setParameter(0, $dbcfg->currency)
->setMaxResults(1)
->execute()->fetch();
}
public function getDefaultPricelist()
{
return sqlQueryBuilder()->select('price', 'discount')
->from('products')
->where('id = ?')->setParameter(0, $this->getProductId())
->execute()->fetch();
}
/* Variations data */
public function getPricelistData()
{
$return = [];
$repository = $this->em->getRepository(\KupShop\PricelistBundle\Entity\PricelistProduct::class);
$data = $repository->findBy(['productId' => $this->getProductId()]);
/** @var \KupShop\PricelistBundle\Entity\PricelistProduct $value */
foreach ($data as $value) {
// pricelists
if ($value->getVariationId() == null) {
$return[$value->getPricelist()->getId()]['pricelist'] = [
'pricelistId' => $value->getPricelist()->getId(),
'variationId' => $value->getVariationId(),
'price' => $value->getPrice(),
'discount' => $value->getDiscount(),
];
} else { // variations
if ($value->getPricelist()) {
$pricelistId = $value->getPricelist()->getId();
} else {
$pricelistId = 0;
}
$return[$pricelistId][$value->getVariationId()] = [
'pricelistId' => $pricelistId,
'variationId' => $value->getVariationId(),
'price' => $value->getPrice(),
'discount' => $value->getDiscount(),
];
}
}
return $return;
}
public function getVariations()
{
if (findModule('products_variations')) {
$qb = sqlQueryBuilder()
->select('id, title, price')
->from('products_variations')
->where('id_product = ?')->setParameter(0, $this->getProductId())
->execute();
if ($qb->rowCount() != 0) {
$data = [];
foreach ($qb as $var) {
$data[$var['id']]['id'] = $var['id'];
$data[$var['id']]['title'] = $var['title'];
$data[$var['id']]['price'] = $var['price'];
}
return $data;
}
}
return false;
}
public function getVat()
{
if ($this->vat == null) {
$this->vat = sqlQueryBuilder()
->select('v.vat')
->from('vats', 'v')
->leftJoin('v', 'products', 'p', 'v.id=p.vat')
->where('p.id = ?')->setParameter(0, $this->getProductId())
->execute()->fetchColumn();
}
return $this->vat;
}
public function getPricelists()
{
$repository = $this->em->getRepository(\KupShop\PricelistBundle\Entity\Pricelist::class);
return $repository->findAll();
}
public function getProductId()
{
if ($this->productId == null) {
$this->productId = getVal('ID');
}
return $this->productId;
}
protected function getObject()
{
return null;
}
}
(new ProductsPricelists())->run();

View File

@@ -0,0 +1,114 @@
{extends file="[shared]/window.tpl"}
{block tabs}
{if $body.acn == 'add'}
{$tabTitle = {'add'|translate}}
{else}
{$tabTitle = {'edit'|translate}}
{/if}
{windowTab id="edit" label=$tabTitle}
{/block}
{block tabsContent}
<div id="edit" class="tab-pane fade active in boxStatic box">
{if $body.acn == 'add'}
<h1 class="h4 main-panel-title">{'add'|translate}</h1>
{else}
<h1 class="h4 main-panel-title">{'edit'|translate}</h1>
{/if}
<div class="form-group boxFlex box-row">
<div class="col-md-2 control-label">
<label>{'pricelistName'|translate}</label>
</div>
<div class="col-md-9 box">
<input name="data[name]" class="form-control input-sm" value="{$body.data.name}">
</div>
</div>
<div class="form-group boxFlex box-row">
<div class="col-md-2 control-label">
<label>{'currency'|translate}</label>
</div>
<div class="col-md-9 box">
<select name="data[currency]" class="selecter">
{foreach $body.currencies as $currency}
<option value="{$currency->getId()}"
{if $currency->getId() == $body.data.currency}selected{/if}>{$currency->getId()} {$currency->getName()}</option>
{/foreach}
</select>
</div>
</div>
{ifmodule PRICELISTS__ACTIVATION_TOKEN}
<div class="form-group boxFlex box-row">
<div class="col-md-2 control-label">
<label>{'token'|translate}</label>
<a class="help-tip" data-toggle="tooltip" title="{'tokenToolTip'|translate}"><i class="bi bi-question-circle"></i></a>
</div>
<div class="col-md-6">
<div class="input-group">
<input type="text" class="form-control input-sm" name="data[token]" readonly value="{$body.data.token}">
<div class="input-group-btn">
{$title = 'generateToken'|translate}
{if !empty($body.data.token)}
{$title = 'regenerateToken'|translate}
{/if}
<button name="acn" value="generatePricelistToken" title="{$title}"
class="btn btn-sm btn-success {if !empty($body.data.token)}confirm{/if}"
{if $body.acn == 'add'}disabled{/if}>
<span class="glyphicon glyphicon-refresh"></span> {$title}
</button>
</div>
</div>
</div>
</div>
{/ifmodule}
{ifmodule PRICE_HISTORY}
{if isSuperuser()}
<div class="form-group">
<div class="col-md-2 control-label ">
<label>
{'priceHistory'|translate}
<span class="glyphicon glyphicon-flash"></span>
</label>
</div>
<div class="col-md-1">
{print_toggle name='price_history'}
</div>
</div>
{else}
<input type="hidden" name="data[price_history]" value="{$body.data.price_history}">
{/if}
{/ifmodule}
<div class="form-group">
<div class="col-md-2 control-label ">
<label>
{'useProductDiscount'|translate}
<span class="glyphico"></span>
</label>
</div>
<div class="col-md-1">
{print_toggle name='use_product_discount' numeric=1}
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label ">
<label>
{'coefficient'|translate}
<span class="glyphico"></span>
</label>
<a data-original-title="{'coefficientToolTip'|translate}" class="help-tip" data-toggle="tooltip">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="col-md-1">
<input type="number" name="data[coefficient]" class="form-control input-sm" value="{$body.data.coefficient}" maxlength="3"
min="0.01" max="10" step="0.01">
</div>
</div>
</div>
{/block}

View File

@@ -0,0 +1,650 @@
{extends file="[shared]/windowFrame.tpl"}
{block body_class}class="panel_frame"{/block}
{block content}
<form id="pricelistsForm" name="editform" method="post" action="launch.php?s=products.pricelists.php&amp;ID={$body.ID}" class="form-horizontal">
<div id="pricelists" class="col-xs-12">
<div class="row bottom-space">
<div class="col-md-7" data-form-mass-open>
{if $body.variations}
<a class="btn btn-sm">
<i class="glyphicon glyphicon-plus-sign"></i>
</a>
{/if}
</div>
<div class="col-md-4 control-label">
<label>{'showPricesDPH'|translate:'products'}</label>
</div>
<div class="col-md-1" style="">
<div class="input-group">
{if !$body.data.showPricesWithVat}{$vatValue = 'N'}{/if}
{print_toggle name="showPricesWithVat" value=$vatValue}
</div>
</div>
</div>
<div class="panel-group panel-group-lists">
<div class="panel panel-heading" style="padding-bottom:4px">
<div class="row">
<div class="col-md-4 text-center">
</div>
<div class="col-md-3">
<small><strong>{'price'|translate:'products'}</strong></small>
</div>
<div class="col-md-2">
<small><strong>{'discountPricelists'|translate:'products'}</strong></small>
</div>
<div class="col-md-2">
<small><strong>{'finalPrice'|translate:'products'}</strong></small>
</div>
</div>
</div>
{* Default pricelist *}
<div class="panel-group panel-group-lists panel" data-form-item="" data-price-currency="{$body.defaultCurrency.code}" data-price-currency-rate="{$body.defaultCurrency.rate}"
id="defaultPricelist">
<div class="panel-heading collapsed row-green" data-toggle="collapse" data-target="#collapse_defaultPricelist">
<div class="row" data-price="row">
<div class="col-md-3">
<p class="input-height"><strong>Základní ceník</strong></p>
</div>
<div class="col-md-1 control-label">
<label>Cena</label>
</div>
<div class="col-md-2">
<div class="input-group">
<input class="form-control input-sm novat" data-price="price" name="data[0][price]" id="defaultPricelistPrice" maxlength="20"
value="{$body.defaultPricelist.price}" type="text">
<input class="form-control input-sm novat" data-price="priceHidden" maxlength="20" value="{$body.defaultPricelist.price}" type="hidden">
<span class="input-group-addon">{$body.defaultCurrency.symbol}</span>
</div>
</div>
<div class="col-md-1 control-label">
<label>Sleva</label>
</div>
<div class="col-md-2">
<div class="input-group">
<input class="form-control input-sm" data-price="discount" name="data[0][discount]" maxlength="20" value="{$body.defaultPricelist.discount}" type="text">
<span class="input-group-addon">%</span>
</div>
</div>
<div class="col-md-2">
<div class="input-group">
<input type="text" data-price="finalPrice" name="data[0][finalPrice]" class="form-control input-sm novat">
<span class="input-group-addon">{$body.defaultCurrency.symbol}</span>
</div>
</div>
{if $body.variations}
<div class="col-md-1">
<a class="btn-sm btn btn-gray" data-target="#collapse_defaultPricelist" data-toggle="collapse">
{'variations'|translate:'products'}
</a>
</div>
{/if}
</div>
</div>
{* Default pricelist Variations *}
{if $body.variations}
<div id="collapse_defaultPricelist" class="panel-collapse collapse">
<div class="panel-body">
{foreach $body.variations as $var}
<div class="row form-group form-group-flex no-margin" data-price="rowVar">
<div class="col-md-3">
<p class="input-height">{$var.title}</p>
</div>
<div class="col-md-1 control-label">
<label>Cena</label>
</div>
<div class="col-md-2">
<div class="input-group">
<input class="form-control input-sm novat" data-price="priceVar" name="data[0][priceVar][{$var.id}]" maxlength="20"
value="{if $var.price != 0}{$var.price}{/if}" type="text">
<input class="form-control input-sm novat" data-price="priceVarHidden" data-var-id="{$var.id}" maxlength="20"
value="{if $var.price != 0}{$var.price}{/if}" type="hidden">
<span class="input-group-addon">{$body.defaultCurrency.symbol}</span>
</div>
</div>
<div class="col-md-3"></div>
<div class="col-md-2">
<div class="input-group">
<input class="form-control input-sm novat" data-price="finalPriceVar" name="data[0][finalPriceVar]" disabled maxlength="20" value=""
type="text">
<span class="input-group-addon">{$body.defaultCurrency.symbol}</span>
</div>
</div>
<input type="hidden" name="data[0][vars]" value="true">
</div>
{if !$var@last}<hr style="margin-top: 5px; margin-bottom: 5px;" class="row">{/if}
{/foreach}
</div>
</div>
{/if}
</div>
{* Pricelists saved in database *}
{foreach $body.pricelists as $pricelist}
<div class="panel no-margin" data-form-item="{$pricelist@index}" data-price-currency="{$pricelist->getCurrency()->getId()}"
data-price-currency-rate="{$pricelist->getCurrency()->getRate()}">
<div class="panel-heading collapsed row-green" data-toggle="collapse" data-target="#collapse_{$pricelist@index}">
<div class="row" data-price="row">
<div class="col-md-3">
<p class="input-height"><strong>{$pricelist->getName()}</strong></p>
</div>
<div class="col-md-1 control-label">
<label>Cena</label>
</div>
<div class="col-md-2">
<div class="input-group">
<input class="form-control input-sm novat" data-price="price" name="data[{$pricelist->getId()}][price]" id="price" maxlength="20"
value="{$body.data.{$pricelist->getId()}.pricelist.price}" type="text">
<input class="form-control input-sm novat" data-price="priceHidden" id="price" maxlength="20"
value="{$body.data.{$pricelist->getId()}.pricelist.price}" type="hidden">
<input class="form-control input-sm novat" data-price="coefficient" id="coefficient" maxlength="20"
value="{$pricelist->getCoefficient()}" type="hidden">
<span class="input-group-addon">{$pricelist->getCurrency()->getSymbol()}</span>
</div>
</div>
<div class="col-md-1 control-label">
<label>Sleva</label>
</div>
<div class="col-md-2">
<div class="input-group">
<input class="form-control input-sm" data-price="discount" name="data[{$pricelist->getId()}][discount]" maxlength="20"
value="{if $body.data.{$pricelist->getId()}.pricelist.discount}{$body.data.{$pricelist->getId()}.pricelist.discount->printFloatValue(-8)}{/if}" type="text">
<span class="input-group-addon">%</span>
</div>
</div>
<div class="col-md-2">
<div class="input-group">
<input type="text" data-price="finalPrice" name="data[{$pricelist->getId()}][finalPrice]" class="form-control input-sm novat">
<span class="input-group-addon">{$pricelist->getCurrency()->getSymbol()}</span>
</div>
</div>
{if $body.variations}
<div class="col-md-1">
<a class="btn-sm btn btn-gray" data-target="#collapse_{$pricelist@index}" data-toggle="collapse">
{'variations'|translate:'products'}
</a>
</div>
{/if}
</div>
</div>
{if $body.variations}
<div id="collapse_{$pricelist@index}" class="panel-collapse collapse">
<div class="panel-body">
{foreach $body.variations as $var}
<div class="row form-group form-group-flex no-margin" data-price="rowVar">
<div class="col-md-3">
<p class="input-height">{$var.title}</p>
</div>
<div class="col-md-1 control-label">
<label>Cena</label>
</div>
<div class="col-md-2">
<div class="input-group">
{if !$body.data.{$pricelist->getId()}.{$var.id}.price}
{$price = $var.price}
{if $price == 0}
{$price = ''}
{/if}
{else}
{$price = {$body.data.{$pricelist->getId()}.{$var.id}.price}}
{/if}
<input class="form-control input-sm novat" data-price="priceVar" data-var-id="{$var.id}"
name="data[{$pricelist->getId()}][priceVar][{$var.id}]" maxlength="20" value="{$body.data.{$pricelist->getId()}.{$var.id}.price}"
type="text">
<input class="form-control input-sm novat" data-price="priceVarHidden" data-var-id="{$var.id}" maxlength="20" value="{$price}" type="hidden">
<span class="input-group-addon">{$pricelist->getCurrency()->getSymbol()}</span>
</div>
</div>
<div class="col-md-1 control-label">
<label>Sleva</label>
</div>
<div class="col-md-2">
<div class="input-group">
<input class="form-control input-sm" data-price="discountVar" name="data[{$pricelist->getId()}][discountVar][{$var.id}]" maxlength="20"
value="{$body.data.{$pricelist->getId()}.{$var.id}.discount}" type="text">
<span class="input-group-addon">%</span>
</div>
</div>
<div class="col-md-2">
<div class="input-group">
<input class="form-control input-sm novat" data-price="finalPriceVar" name="data[{$pricelist->getId()}][finalPriceVar]" disabled
maxlength="20" value="" type="text">
<span class="input-group-addon">{$pricelist->getCurrency()->getSymbol()}</span>
</div>
</div>
<input type="hidden" name="data[{$pricelist->getId()}][vars]" value="true">
</div>
{if !$var@last}<hr style="margin-top: 5px; margin-bottom: 5px;" class="row">{/if}
{/foreach}
</div>
</div>
{/if}
</div>
{/foreach}
</div>
<div class="row bottom-space">
<div class="col-md-4 pull-right">
<button class="btn btn-primary btn-block" type="submit" name="acn" value="updatePricelists">{'savePricelists'|translate:'products'}</button>
</div>
</div>
</div>
</form>
<script type="text/javascript">
var Price = {
recalcVats: function (vat, type) {
$('[data-price=price], [data-price=priceHidden]').each(function () {
var formGroup = Price.recalcVat($(this), vat, type);
if (formGroup) {
Price.setFinalPrice(formGroup.find('[data-price=finalPrice]'));
}
});
$('[data-price=priceVar]').each(function () {
var formGroup = Price.recalcVat($(this), vat, type);
if (formGroup) {
Price.setFinalPriceVar(formGroup.find('[data-price=finalPriceVar]'));
}
});
$('[data-price=finalPriceVar], [data-price=priceVarHidden]').each(function () {
if (!$(this).attr('data-vat')) {
Price.recalcVat($(this), vat, type);
}
});
$('[data-price=finalPrice]').each(function () {
Price.recalcVat($(this), vat, type);
});
},
recalcVat: function (element, vat, type) {
var priceVat;
var $price = element,
$value;
if ($price.val()) {
$value = new BigNumber($price.val());
switch (type) {
// calculate price with vat
case 1:
var tmpPriceWithVat = $value.times((100 + vat) / 100).round(4).toNumber();
priceVat = Math.round((tmpPriceWithVat + Number.EPSILON) * 100) / 100;
break;
// price without vat
case 0:
priceVat = $value.times(100).dividedBy(100 + vat).round(4);//$price.val() * 100 / (100 + vat)));
break;
}
$price.val(priceVat);
return element.parents('.form-group');
}
},
recalcFinalPrice: function (price, discount) {
var discount_price;
if (discount == '' || discount == undefined) {
discount = 0;
}
if (price == undefined) {
price = 0;
}
discount = new BigNumber(discount);
discount_price = new BigNumber(100).minus(discount).dividedBy(new BigNumber(100)).times(price).round(4); //formatPrice(price * ((100 - discount) / 100));
return discount_price;
},
recalcDiscount: function (price, finalPrice) {
price = new BigNumber(price);
finalPrice = new BigNumber(finalPrice);
var discount;
if (finalPrice.greaterThan(new BigNumber(0))) {
discount = new BigNumber(100).minus(finalPrice.dividedBy(price).times(100)).round(4);//formatPrice(100 - (discount_price / price) * 100));
}
return discount;
},
setPrice: function (element, price) {
this.getHiddenPrice(element).val(price);
this.getPrice(element).val(price);
},
setHiddenPrice: function (element, price) {
this.getHiddenPrice(element).val(price);
},
setHiddenPriceVar: function (element, price) {
this.getHiddenPriceVar(element).val(price);
},
setPriceVar: function (element, price) {
this.getPriceVar(element).val(price);
},
setDiscount: function (element) {
var hiddenPrice;
if (this.getHiddenPrice(element).val() == '') {
hiddenPrice = 0;
} else {
hiddenPrice = this.getHiddenPrice(element).val();
}
var discount = this.recalcDiscount(
hiddenPrice,
this.getFinalPrice(element).val()
);
this.getDiscount(element).val(discount);
},
setFinalPrice: function (element) {
var price;
if (this.getPrice(element).val() != '') {
price = this.getPrice(element).val();
} else {
price = this.getHiddenPrice(element).val();
}
var finalPrice = this.recalcFinalPrice(
price,
this.getDiscount(element).val()
);
if (this.getDiscount(element).val() === '') {
this.getFinalPrice(element).val(price);
} else {
this.getFinalPrice(element).val(finalPrice);
}
},
setFinalPriceVar: function (element) {
var finalPrice;
if (this.getPriceVar(element).val() !== '') {
finalPrice = this.recalcFinalPrice(
this.getPriceVar(element).val(),
this.getDiscountVar(element).val()
);
this.getFinalPriceVar(element).val(finalPrice);
this.getFinalPriceVar(element).attr('data-vat', 1);
} else {
var row = this.getRow(element);
var priceVar = this.getHiddenPriceVar(element).val();
if (priceVar != '') {
finalPrice = this.recalcFinalPrice(
priceVar,
this.getDiscount(row).val()
);
} else {
finalPrice = this.recalcFinalPrice(
this.getHiddenPrice(row).val(),
this.getDiscount(row).val()
);
}
this.getFinalPriceVar(element).val(finalPrice);
}
},
checkEmptyPricelistPrice: function (element) {
var pricelistPrice = this.getPrice(element).val();
var pricelist = element.parents('.panel');
var currency = pricelist.data('price-currency'),
rate = pricelist.data('price-currency-rate');
let coefficient = this.getRow(element).find('[data-price=coefficient]').val();
if (pricelistPrice === '') {
pricelistPrice = this.getPrice(this.getDefaultRow()).val();
if (coefficient) {
pricelistPrice = pricelistPrice * coefficient
}
var price = this.recalcCurrency(currency, pricelistPrice, rate);
//this.setPrice(element, price);
this.setHiddenPrice(element, price);
}
},
checkEmptyVariations: function (element) {
var idVar = element.data('var-id'),
priceVar = element.val();
var finalPrice, currency, rate, pricelist;
var discount;
let coefficient = this.getRow(element).find('[data-price=coefficient]').val();
// not default pricelist
if (idVar) {
var defaultVarPrice = $('input[name="data[0][priceVar][' + idVar + ']"]').val();
// have variation an default variation price in default pricelist
if (defaultVarPrice && priceVar == '') {
//apply pricelist coefficient
if (coefficient) {
defaultVarPrice = defaultVarPrice * coefficient
}
// currency data
pricelist = element.parents('.panel');
currency = pricelist.data('price-currency');
rate = pricelist.data('price-currency-rate');
discount = this.getDiscount(element).val();
finalPrice = this.recalcFinalPrice(defaultVarPrice, discount);
finalPrice = this.recalcCurrency(currency, finalPrice, rate);
this.getHiddenPriceVar(element).val(this.recalcCurrency(currency, defaultVarPrice, rate));
this.getFinalPriceVar(element).val(finalPrice);
}
} else { // default pricelist
if (!element.val()) {
var defaultPrice = this.getPrice(this.getDefaultRow()).val();
//apply pricelist coefficient
if (coefficient) {
defaultPrice = defaultPrice * coefficient
}
discount = this.getDiscount(this.getDefaultRow()).val();
finalPrice = this.recalcFinalPrice(defaultPrice, discount);
this.getFinalPriceVar(element).val(finalPrice);
}
}
},
checkFinalPriceVar: function (element) {
var price = this.getPriceVar(element);
var varId = price.data('var-id');
if (price.val() == '' && varId) {
var defaultVarPrice = $('input[name="data[0][priceVar][' + varId + ']"]').val();
if (defaultVarPrice == '') {
var pricelistPrice = this.getHiddenPrice(element).val();
this.setHiddenPriceVar(element, pricelistPrice);
}
}
},
recalcCurrency: function (currency, price, currencyRate) {
var defaultCurrency = this.getDefaultRow().data('price-currency');
if (currency != defaultCurrency) {
price = new BigNumber(price);
price = price.dividedBy(currencyRate);
}
return price;
},
getVarRow: function (element) {
return $(element).closest('[data-price=rowVar]');
},
getRow: function (element) {
return $(element).closest('.panel').find('[data-price=row]');
},
getDefaultRow: function () {
return $('#defaultPricelist');
},
getPrice: function (element) {
return this.getRow(element).find('[data-price=price]');
},
getHiddenPrice: function (element) {
return this.getRow(element).find('[data-price=priceHidden]');
},
getHiddenPriceVar: function (element) {
return this.getVarRow(element).find('[data-price=priceVarHidden]');
},
getPriceVar: function (element) {
return this.getVarRow(element).find('[data-price=priceVar]');
},
getDiscount: function (element) {
return this.getRow(element).find('[data-price=discount]');
},
getDiscountVar: function (element) {
var discountVar = this.getVarRow(element).find('[data-price=discountVar]');
if (!discountVar.val()) {
discountVar = this.getDiscount(element);
}
return discountVar;
},
getFinalPrice: function (element) {
return this.getRow(element).find('[data-price=finalPrice]');
},
getFinalPriceVar: function (element) {
return this.getVarRow(element).find('[data-price=finalPriceVar]');
}
};
// keyups
$('[data-price="discount"]').keyup(function (e) {
if (e.originalEvent !== undefined) {
Price.setFinalPrice($(this));
}
{if $body.variations}
// update variations final prices
$('[data-price="priceVar"]').each(function () {
Price.setFinalPriceVar($(this));
});
{/if}
});
$('[data-price="discountVar"]').keyup(function (e) {
if (e.originalEvent !== undefined) {
Price.setFinalPriceVar($(this));
}
});
$('[data-price="price"]').keyup(function () {
Price.setDiscount($(this));
if ($(this).val() != '') {
Price.setHiddenPrice($(this), $(this).val());
}
$('[data-price="price"]').each(function () {
Price.checkEmptyPricelistPrice($(this));
$('[data-price="discount"]').trigger('keyup');
});
$('[data-price="discount"]').each(function () {
Price.setFinalPrice($(this));
});
});
$('[data-price="finalPrice"]').keyup(function () {
Price.setDiscount($(this));
$('[data-price="discount"]').trigger('keyup');
});
{if $body.variations}
$('[data-price="priceVar"]').keyup(function () {
// When is variation price deleted check finalPrices
if (!$(this).val()) {
$('[data-price="finalPriceVar"]').each(function () {
Price.checkFinalPriceVar($(this));
$('[data-price="discount"]').trigger('keyup');
});
}
Price.setHiddenPriceVar($(this), $(this).val());
Price.setFinalPriceVar($(this));
$('[data-price="priceVar"]').each(function () {
Price.checkEmptyVariations($(this));
});
});
{/if}
// onpage load
$(document).ready(function () {
$('[data-price="price"]').each(function () {
Price.checkEmptyPricelistPrice($(this));
});
{if $body.variations}
$('[data-price="priceVar"]').each(function () {
Price.setFinalPriceVar($(this));
Price.checkEmptyVariations($(this));
});
{/if}
$('[data-price="discount"]').each(function () {
Price.setFinalPrice($(this));
});
if ($('input[name="data[showPricesWithVat]"]').is(':checked')) {
Price.recalcVats({$body.vat}, 1);
}
});
// vat switch
$('input[name="data[showPricesWithVat]"]').change(function () {
if ($(this).is(':checked')) {
Price.recalcVats({$body.vat}, 1);
} else {
Price.recalcVats({$body.vat}, 0);
}
});
{block initAddForm}
initForm({
selector: '#pricelists',
beforeAdd: function (original) {
var $item = original();
}
});
{/block}
</script>
{/block}

View File

@@ -0,0 +1,148 @@
<?php
namespace KupShop\PricelistBundle\Context;
use Doctrine\ORM\EntityManagerInterface;
use KupShop\KupShopBundle\Context\ContextInterface;
use KupShop\KupShopBundle\Context\EmptiableContext;
use KupShop\KupShopBundle\Context\UserContext;
use KupShop\PricelistBundle\Entity\Pricelist;
use Symfony\Contracts\Service\Attribute\Required;
class PricelistContext implements ContextInterface, EmptiableContext
{
public const PERSISTENT_KEY = 'persistentPricelist';
protected EntityManagerInterface $em;
private UserContext $userContext;
protected ?string $activeId = null;
private bool $loaded = false;
/** @var array<string, Pricelist> */
protected array $activeCache = [];
public function __construct()
{
}
public function activate(?string $id)
{
if (null === $id) {
trigger_error('Passing $id=null into ContextInterface::activate is deprecated! Use ::clearCache() instead.', \E_USER_DEPRECATED);
}
if ($id) {
$this->activeId = $id;
$this->loaded = true;
} else {
$this->clearCache();
}
}
public function getActive(): ?Pricelist
{
if (null !== ($id = $this->getActiveId())) {
if (array_key_exists($id, $this->activeCache)) {
return $this->activeCache[$id];
} else {
return $this->activeCache[$id] = $this->loadPricelistById($id);
}
}
return null;
}
protected function loadActive()
{
if ($user = $this->userContext->getActive()) {
if (!empty($user->id_pricelist)) {
return $user->id_pricelist;
}
foreach ($user->getGroups() as $group) {
if (!empty($group['id_pricelist'])) {
return $group['id_pricelist'];
}
}
}
return null;
}
public function getActiveId(): ?string
{
return $this->isEmpty() ? null : $this->activeId ??= $this->loadActive();
}
protected function loadPricelistById($id)
{
return $this->em->getRepository(Pricelist::class)->find($id);
}
public function getDefault(): ?Pricelist
{
if (!$this->getDefaultId()) {
return null;
}
return $this->em->getRepository(Pricelist::class)->find($this->getDefaultId());
}
public function getDefaultId(): ?string
{
return null;
}
protected function loadPricelistByCurrency($currency)
{
return $this->em->getRepository(Pricelist::class)->findOneBy(['currency' => $currency]);
}
/**
* @return Pricelist[]
*/
public function getSupported(): array
{
return $this->em->getRepository(Pricelist::class)->findAll();
}
public function clearCache(): void
{
$this->loaded = false;
$this->activeId = null;
}
public function forceEmpty(): void
{
$this->loaded = true;
$this->activeId = null;
}
public function isEmpty(): bool
{
return $this->loaded && $this->activeId === null;
}
public function isValid(string $id): bool
{
return true;
}
public function getMultipleActiveIds(int $priceListId): ?array
{
return null;
}
#[Required]
public function _setUserContext(UserContext $userContext): void
{
$this->userContext = $userContext;
}
#[Required]
public function _setEm(EntityManagerInterface $em): void
{
$this->em = $em;
}
}

View File

@@ -0,0 +1,193 @@
<?php
namespace KupShop\PricelistBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Class Pricelist.
*
* @ORM\Entity
*
* @ORM\Table(name="pricelists", options={"collate":"utf8"})
*/
class Pricelist
{
/**
* @ORM\Id
*
* @ORM\Column(type="integer")
*
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(type="string", length=60)
*/
private $name;
/**
* @ORM\Column(type="boolean", name="use_product_discount")
*/
private $useProductDiscount;
/**
* @ORM\Column(type="float", name="coefficient")
*/
private $coefficient;
/**
* @ORM\ManyToOne(targetEntity="KupShop\I18nBundle\Entity\Currency")
*
* @ORM\JoinColumn(name="currency")
*/
private $currency;
/**
* @ORM\OneToMany(targetEntity="KupShop\PricelistBundle\Entity\PricelistProduct", mappedBy="pricelist", cascade={"remove", "persist"})
*/
private $product;
/**
* Constructor.
*/
public function __construct()
{
$this->product = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Get id.
*
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* Set name.
*
* @param string $name
*
* @return Pricelist
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name.
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Set currency.
*
* @return Pricelist
*/
public function setCurrency(?\KupShop\I18nBundle\Entity\Currency $currency = null)
{
$this->currency = $currency;
return $this;
}
/**
* Get currency.
*
* @return \KupShop\I18nBundle\Entity\Currency
*/
public function getCurrency()
{
return $this->currency;
}
/**
* Add product.
*
* @return Pricelist
*/
public function addProduct(PricelistProduct $product)
{
$this->product[] = $product;
return $this;
}
/**
* Remove product.
*/
public function removeProduct(PricelistProduct $product)
{
$this->product->removeElement($product);
}
/**
* Get product.
*
* @return \Doctrine\Common\Collections\Collection
*/
public function getProduct()
{
return $this->product;
}
/**
* Get extend product discount field.
*
* @return bool
*/
public function getUseProductDiscount()
{
return $this->useProductDiscount;
}
/**
* Set use product discount field.
*
* @param bool $useProductDiscount
*
* @return Pricelist
*/
public function setUseProductDiscount($useProductDiscount)
{
$this->useProductDiscount = $useProductDiscount;
return $this;
}
/**
* Get coefficient field.
*
* @return float
*/
public function getCoefficient()
{
return $this->coefficient;
}
/**
* Set use product discount field.
*
* @param float $coefficient
*
* @return Pricelist
*/
public function setCoefficient($coefficient)
{
$this->coefficient = $coefficient;
return $this;
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace KupShop\PricelistBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Class Pricelist.
*
* @ORM\Entity
* @ORM\Table(name="pricelists", options={"collate":"utf8"})
*/
class Pricelist
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\Column(type="string", length=60)
*/
private $name;
/**
* @ORM\ManyToOne(targetEntity="KupShop\I18nBundle\Entity\Currency")
* @ORM\JoinColumn(name="currency")
*/
private $currency;
/**
* Get id.
*
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* Set name.
*
* @param string $name
*
* @return Pricelist
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name.
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Set currency.
*
* @param \KupShop\I18nBundle\Entity\Currency $currency
*
* @return Pricelist
*/
public function setCurrency(\KupShop\I18nBundle\Entity\Currency $currency = null)
{
$this->currency = $currency;
return $this;
}
/**
* Get currency.
*
* @return \KupShop\I18nBundle\Entity\Currency
*/
public function getCurrency()
{
return $this->currency;
}
}

View File

@@ -0,0 +1,186 @@
<?php
namespace KupShop\PricelistBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\UniqueConstraint;
/**
* Class ProductPricelist.
*
* @ORM\Entity
*
* @ORM\Table(name="pricelists_products",
* uniqueConstraints={@UniqueConstraint(name="pricelist_product",
* columns={"id_pricelist","id_product","id_variation"})})
*/
class PricelistProduct
{
/**
* @ORM\Id
*
* @ORM\Column(type="integer")
*
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity="KupShop\PricelistBundle\Entity\Pricelist", inversedBy="product")
*
* @ORM\JoinColumn(name="id_pricelist", referencedColumnName="id", onDelete="CASCADE", nullable=false)
*/
private $pricelist;
/**
* @ORM\ManyToOne()
*
* @ORM\Column(name="id_product", type="integer")
*/
private $productId;
/**
* @ORM\Column(name="id_variation", type="integer", nullable=true)
*
* @ORM\ManyToOne(cascade={"persist", "remove"})
*/
private $variationId;
/**
* @ORM\Column(type="decimal", precision=15, scale=4, nullable=true)
*/
private $price;
/**
* @ORM\Column(type="decimal", scale=8, precision=12, nullable=true)
*/
private $discount;
/**
* Get id.
*
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* Set productId.
*
* @param int $productId
*
* @return PricelistProduct
*/
public function setProductId($productId)
{
$this->productId = $productId;
return $this;
}
/**
* Get productId.
*
* @return int
*/
public function getProductId()
{
return $this->productId;
}
/**
* Set variationId.
*
* @param int $variationId
*
* @return PricelistProduct
*/
public function setVariationId($variationId)
{
$this->variationId = $variationId;
return $this;
}
/**
* Get variationId.
*
* @return int
*/
public function getVariationId()
{
return $this->variationId;
}
/**
* Set price.
*
* @param string $price
*
* @return PricelistProduct
*/
public function setPrice($price)
{
$this->price = $price;
return $this;
}
/**
* Get price.
*
* @return string
*/
public function getPrice()
{
return $this->price;
}
/**
* Set discount.
*
* @param string $discount
*
* @return PricelistProduct
*/
public function setDiscount($discount)
{
$this->discount = $discount;
return $this;
}
/**
* Get discount.
*
* @return string
*/
public function getDiscount()
{
return $this->discount;
}
/**
* Set pricelistId.
*
* @return PricelistProduct
*/
public function setPricelist(?Pricelist $pricelist = null)
{
$this->pricelist = $pricelist;
return $this;
}
/**
* Get pricelistId.
*
* @return \KupShop\PricelistBundle\Entity\Pricelist
*/
public function getPricelist()
{
return $this->pricelist;
}
}

View File

@@ -0,0 +1,156 @@
<?php
namespace KupShop\PricelistBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Class ProductPricelist.
*
* @ORM\Entity
* @ORM\Table(name="pricelists_products")
*/
class PricelistProduct
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity="KupShop\PricelistBundle\Entity\Pricelist")
* @ORM\JoinColumn(name="id_pricelist")
*/
private $pricelistId;
/**
* @ORM\ManyToOne()
* @ORM\Column(name="id_product", type="integer")
*/
private $productId;
/**
* @ORM\Column(name="id_variation", type="integer", nullable=true)
* @ORM\ManyToOne()
*/
private $variationId;
/**
* @ORM\Column(type="decimal", precision=15, scale=4, nullable=true)
*/
private $price;
/**
* @ORM\Column(type="integer", nullable=true)
*/
private $discount;
/**
* Get id.
*
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* Set price.
*
* @param string $price
*
* @return PricelistProduct
*/
public function setPrice($price)
{
$this->price = $price;
return $this;
}
/**
* Get price.
*
* @return string
*/
public function getPrice()
{
return $this->price;
}
/**
* Set discount.
*
* @param int $discount
*
* @return PricelistProduct
*/
public function setDiscount($discount)
{
$this->discount = $discount;
return $this;
}
/**
* Get discount.
*
* @return int
*/
public function getDiscount()
{
return $this->discount;
}
/**
* Set pricelistId.
*
* @param \KupShop\PricelistBundle\Entity\Pricelist $pricelistId
*
* @return PricelistProduct
*/
public function setPricelistId(\KupShop\PricelistBundle\Entity\Pricelist $pricelistId = null)
{
$this->pricelistId = $pricelistId;
return $this;
}
/**
* Get pricelistId.
*
* @return \KupShop\PricelistBundle\Entity\Pricelist
*/
public function getPricelistId()
{
return $this->pricelistId;
}
/**
* Set variationId.
*
* @param int $variationId
*
* @return PricelistProduct
*/
public function setVariationId($variationId)
{
$this->variationId = $variationId;
return $this;
}
/**
* Get variationId.
*
* @return int
*/
public function getVariationId()
{
return $this->variationId;
}
}

View File

@@ -0,0 +1,159 @@
<?php
namespace KupShop\PricelistBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Class ProductPricelist
* @package KupShop\PricelistBundle\Entity
*
* @ORM\Entity
* @ORM\Table(name="pricelists_products")
*/
class ProductPricelist
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity="KupShop\PricelistBundle\Entity\Pricelist")
* @ORM\JoinColumn(name="id_pricelist")
*/
private $pricelistId;
/**
* @ORM\ManyToOne(targetEntity="KupShop\CatalogBundle\Entity\Product")
* @ORM\JoinColumn(name="id_product")
*/
private $productId;
/**
* @ORM\Column(name="id_variation", type="integer")
*/
private $variationId;
/**
* @ORM\Column(type="decimal", precision=15, scale=4)
*/
private $price;
/**
* @ORM\Column(type="integer")
*/
private $discount;
/**
* Get id
*
* @return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set price
*
* @param string $price
*
* @return ProductPricelist
*/
public function setPrice($price)
{
$this->price = $price;
return $this;
}
/**
* Get price
*
* @return string
*/
public function getPrice()
{
return $this->price;
}
/**
* Set discount
*
* @param integer $discount
*
* @return ProductPricelist
*/
public function setDiscount($discount)
{
$this->discount = $discount;
return $this;
}
/**
* Get discount
*
* @return integer
*/
public function getDiscount()
{
return $this->discount;
}
/**
* Set pricelistId
*
* @param \KupShop\PricelistBundle\Entity\Pricelist $pricelistId
*
* @return ProductPricelist
*/
public function setPricelistId(\KupShop\PricelistBundle\Entity\Pricelist $pricelistId = null)
{
$this->pricelistId = $pricelistId;
return $this;
}
/**
* Get pricelistId
*
* @return \KupShop\PricelistBundle\Entity\Pricelist
*/
public function getPricelistId()
{
return $this->pricelistId;
}
/**
* Set productId
*
* @param \KupShop\CatalogBundle\Entity\Product $productId
*
* @return ProductPricelist
*/
public function setProductId(\KupShop\CatalogBundle\Entity\Product $productId = null)
{
$this->productId = $productId;
return $this;
}
/**
* Get productId
*
* @return \KupShop\CatalogBundle\Entity\Product
*/
public function getProductId()
{
return $this->productId;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace KupShop\PricelistBundle\EventListener;
use KupShop\KupShopBundle\Event\CreateMenuEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class CreateMenuListener implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
CreateMenuEvent::COMPLETING_TREE => [
['addItem', 200],
],
];
}
/**
* @var CreateMenuEvent
*/
public function addItem(CreateMenuEvent $event)
{
$event->addItem('productsMenu',
[
'name' => 'pricelist',
'left' => 's=menu.php&type=pricelist', 'right' => 's=list.php&type=pricelist',
]
);
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace KupShop\PricelistBundle\EventListener;
use KupShop\KupShopBundle\Exception\RedirectException;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\KupShopBundle\Util\RequestUtil;
use KupShop\PricelistBundle\Context\PricelistContext;
use Query\Operator;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ControllerEvent;
class RequestListener implements EventSubscriberInterface
{
/** @required */
public RequestUtil $requestUtil;
public static function getSubscribedEvents()
{
$listeners = [];
if (findModule(\Modules::PRICELISTS, \Modules::SUB_ACTIVATION_TOKEN)) {
$listeners[] = ['handleTokenPricelist', 200];
}
$listeners[] = ['handlePersistentPricelist', 200];
return [
ControllerEvent::class => $listeners,
];
}
public function handleTokenPricelist(ControllerEvent $event)
{
$pricelistContext = Contexts::get(PricelistContext::class);
if ($pricelistContext->getActiveId()) {
return;
}
$request = $event->getRequest();
if ($token = $request->get('wpj_activate_pricelist', $request->get('wpjplid'))) {
$pricelist = sqlQueryBuilder()->select('id')->from('pricelists')
->where(Operator::equals(['token' => $token]))
->execute()->fetchOne();
if ($pricelist) {
$session = $request->getSession();
$session->set(PricelistContext::PERSISTENT_KEY, $pricelist);
}
$this->requestUtil->modifyQueryParameters($request, function ($queryParams) {
unset($queryParams['wpj_activate_pricelist'], $queryParams['wpjplid']);
return $queryParams;
});
throw new RedirectException($request->getUri());
}
}
public function handlePersistentPricelist(ControllerEvent $event)
{
if ($pricelist = $event->getRequest()->getSession()->get(PricelistContext::PERSISTENT_KEY)) {
Contexts::get(PricelistContext::class)->activate($pricelist);
}
}
}

View File

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

View File

@@ -0,0 +1,113 @@
<?php
namespace KupShop\PricelistBundle\Query;
use Doctrine\DBAL\Connection;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\PricelistBundle\Context\PricelistContext;
use Query\Operator;
use Query\QueryBuilder;
class Product
{
public static function getPriceListDiscountSpec($pricelistId, $condition)
{
$priceListIds = static::getActivePriceLists($pricelistId);
return function (QueryBuilder $qb) use ($priceListIds, $condition) {
$qb->leftJoin('p', 'pricelists_products', 'prlp', 'prlp.id_product = p.id AND prlp.id_variation IS null AND prlp.id_pricelist IN (:id_pricelist)')
->setParameter('id_pricelist', $priceListIds, Connection::PARAM_INT_ARRAY);
return $condition;
};
}
public static function applyPricelist($pricelist, $variation = null, $selectMinPrice = false)
{
$pricelistSelect = $selectMinPrice ?
'MIN(COALESCE(prlv.price, pv.price, prlp.price, p.price)) as pricelist_price, '
.'MAX(COALESCE(prlv.price, pv.price, prlp.price, p.price)) as pricelist_price_max'
: 'COALESCE(prlv.price, pv.price, prlp.price, p.price) as pricelist_price';
if (findModule(\Modules::PRICE_HISTORY)) {
$priceForDiscountSelect = $selectMinPrice ?
'MIN(COALESCE(prlv.price_for_discount, prlp.price_for_discount)) as pricelist_price_for_discount,
MAX(COALESCE(prlv.price_for_discount, prlp.price_for_discount)) as pricelist_price_for_discount_max'
: 'COALESCE(prlv.price_for_discount, prlp.price_for_discount) as pricelist_price_for_discount';
$pricelistSelect .= ', COALESCE(prl_v.price_history, prl.price_history) as pricelist_price_history, '.$priceForDiscountSelect;
}
return function (QueryBuilder $qb) use ($variation, $pricelistSelect, $pricelist) {
$qb->addSelect(
$pricelistSelect,
'COALESCE(prlv.discount, prlp.discount) as pricelist_discount',
'COALESCE(prlv.id_pricelist, prlp.id_pricelist) as from_pricelist',
'COALESCE(IF(prlv.price IS NULL, NULL, "prlv"),
IF(prlp.price IS NULL, NULL, "prlp"),
IF(pv.price IS NULL, NULL, "pv"),
IF(p.price IS NULL, NULL, "p")) as price_source',
'COALESCE(IF(prlv.price IS NULL, NULL, prl_v.currency),
IF(pv.price IS NULL, NULL, ""),
IF(prlp.price IS NULL, NULL, prl.currency),
IF(p.price IS NULL, NULL, "")) as pricelist_currency'
)
->joinPriceListsProducts($pricelist);
if ($variation) {
$qb->andWhere(Operator::equals(['pv.id' => $variation]));
}
};
}
public static function applyPricelistOnVariation($pricelist)
{
$priceListSelect = 'COALESCE(prlv.price, pv.price, prlp.price, p.price) as pricelist_price, COALESCE(prlv.discount, prlp.discount) as pricelist_discount';
if (findModule(\Modules::PRICE_HISTORY)) {
$priceListSelect .= ', COALESCE(prl_v.price_history, prl.price_history) as pricelist_price_history, COALESCE(prlv.price_for_discount, prlp.price_for_discount) as pricelist_price_for_discount';
}
$priceListIds = static::getActivePriceLists((int) $pricelist);
return function (QueryBuilder $qb) use ($priceListSelect, $priceListIds) {
$qb->addSelect($priceListSelect,
'COALESCE(prlv.id_pricelist, prlp.id_pricelist) as from_pricelist',
'COALESCE(IF(prlv.price IS NULL, NULL, "prlv"),
IF(prlp.price IS NULL, NULL, "prlp"),
IF(pv.price IS NULL, NULL, "pv"),
IF(p.price IS NULL, NULL, "p")) as price_source',
'COALESCE(IF(prlv.price IS NULL, NULL, prl_v.currency),
IF(pv.price IS NULL, NULL, ""),
IF(prlp.price IS NULL, NULL, prl.currency),
IF(p.price IS NULL, NULL, "")) as pricelist_currency',
'COALESCE(prlv.discount, prlp.discount) as pricelistDiscount')
->leftJoin('pv', 'pricelists_products', 'prlp', 'prlp.id_product = pv.id_product AND prlp.id_variation IS null AND prlp.id_pricelist IN (:id_pricelist)')
->leftJoin('pv', 'pricelists_products', 'prlv', 'prlv.id_product = pv.id_product AND prlv.id_variation = pv.id AND prlv.id_pricelist IN (:id_pricelist)')
->leftJoin('prlp', 'pricelists', 'prl', 'prlp.id_pricelist = prl.id')
->leftJoin('prlv', 'pricelists', 'prl_v', 'prlv.id_pricelist = prl_v.id')
->setParameter('id_pricelist', $priceListIds, Connection::PARAM_INT_ARRAY);
if (count($priceListIds) <= 1) {
$qb->setForceIndexForJoin('prlp', 'pricelist_product')
->setForceIndexForJoin('prlv', 'pricelist_product');
}
if (!$qb->isAliasPresent('p')) {
$qb->leftJoin('pv', 'products', 'p', 'p.id = pv.id_product');
}
};
}
public static function getActivePriceLists(int $priceListId): array
{
$priceListContext = Contexts::get(PricelistContext::class);
if ($priceListIds = $priceListContext->getMultipleActiveIds($priceListId)) {
return $priceListIds;
}
return [$priceListId];
}
}

View File

@@ -0,0 +1,8 @@
services:
_defaults:
autowire: true
autoconfigure: true
KupShop\PricelistBundle\:
resource: ../../{Context,EventListener,Util}
exclude: ../../Util/Price/PriceListPrice.php

View File

@@ -0,0 +1,198 @@
<?php
class PricelistUpgrade extends UpgradeNew
{
protected $priority = 1100;
public function check_tableExists()
{
return $this->checkTableExists('pricelists');
}
public function check_tablePricelistsProductsExists()
{
return $this->checkTableExists('pricelists_products');
}
public function check_uniqueIdVariation()
{
return $this->checkIndexOnColumnExists('pricelists_products', 'id_variation');
}
/** Pricelists_products set (id_pricelist, id_product, id_variation) unique */
public function upgrade_uniqueIdVariation()
{
sqlQuery('ALTER TABLE pricelists_products ADD CONSTRAINT pricelist_product UNIQUE (id_pricelist, id_product, id_variation);');
$this->upgradeOK();
}
/** Creating pricelists table */
public function upgrade_tableExists()
{
sqlQuery('CREATE TABLE pricelists (
id INT AUTO_INCREMENT NOT NULL,
currency VARCHAR(3) DEFAULT NULL,
name VARCHAR(60) NOT NULL,
INDEX IDX_28511D046956883F (currency),
PRIMARY KEY (id)
) ENGINE = InnoDB');
sqlQuery('ALTER TABLE pricelists ADD CONSTRAINT FK_28511D046956883F FOREIGN KEY (currency) REFERENCES currencies (id)');
$this->upgradeOK();
}
/** Creating pricelists_products table */
public function upgrade_tablePricelistsProductsExists()
{
sqlQuery('CREATE TABLE pricelists_products (
id INT AUTO_INCREMENT NOT NULL,
id_pricelist INT NOT NULL,
id_product INT NOT NULL,
id_variation INT DEFAULT NULL,
price NUMERIC(15, 4) DEFAULT NULL COMMENT \'(DC2Type:decimal)\',
discount NUMERIC(12, 8) DEFAULT NULL COMMENT \'(DC2Type:decimal)\',
INDEX IDX_49750F94DCFA564A (id_pricelist),
CONSTRAINT FK_49750F94DCFA564A FOREIGN KEY (id_pricelist) REFERENCES pricelists (id) ON DELETE CASCADE,
PRIMARY KEY(id)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB;');
$this->upgradeOK();
}
public function check_emptyPricelistsProducts()
{
return sqlQueryBuilder()->select('count(id)')
->from('pricelists_products')
->where('price IS NULL AND discount IS NULL')
->execute()->fetchColumn() > 0;
}
/** Delete empty pricelists_products */
public function upgrade_emptyPricelistsProducts()
{
sqlQueryBuilder()->delete('pricelists_products')
->where('price IS NULL AND discount IS NULL')
->execute();
$this->upgradeOK();
}
public function check_integrityUnique()
{
return $this->checkConstraintExists('pricelists_products', 'integrity_unique');
}
/** Add virtual column to check uniqueness even for NULL variations */
public function upgrade_integrityUnique()
{
try {
sqlQuery('ALTER TABLE pricelists_products ADD integrity_unique varchar(50) as (CONCAT_WS("-", `id_pricelist`, `id_product`, `id_variation`)) VIRTUAL');
} catch (Exception $e) {
// Ignore exception if column already exists - caused by failing DELETE statement
}
sqlQuery('DELETE pp FROM pricelists_products pp WHERE pp.id NOT IN (SELECT t.* FROM (SELECT MAX(pp2.id) FROM pricelists_products pp2 GROUP BY pp2.integrity_unique) t)');
sqlQuery('ALTER TABLE pricelists_products ADD UNIQUE integrity_unique(integrity_unique)');
$this->upgradeOK();
}
public function check_ProductForeignKey(): bool
{
return $this->checkConstraintWithColumnExists('pricelists_products', 'FK_pricelists_products_id_product', 'id_product');
}
/** pricelist_products: add foreign key on id_product and id_variation columns */
public function upgrade_ProductForeignKey(): void
{
// Smazat zaznamy na neexistujici produkty
sqlQuery('DELETE FROM pricelists_products WHERE id_product NOT IN (SELECT id FROM products)');
sqlQuery('DELETE FROM pricelists_products WHERE id_variation IS NOT NULL AND id_variation NOT IN (SELECT id FROM products_variations)');
// Vytvorim foreign keys
sqlQuery('ALTER TABLE pricelists_products ADD CONSTRAINT `FK_pricelists_products_id_product`
FOREIGN KEY (id_product) REFERENCES products (id) ON DELETE CASCADE ON UPDATE CASCADE');
sqlQuery('ALTER TABLE pricelists_products ADD CONSTRAINT `FK_pricelists_products_id_variation`
FOREIGN KEY (id_variation) REFERENCES products_variations (id) ON DELETE CASCADE ON UPDATE CASCADE');
$this->upgradeOK();
}
public function check_PricelistsToken()
{
return findModule(Modules::PRICELISTS, Modules::SUB_ACTIVATION_TOKEN) && $this->checkColumnExists('pricelists', 'token');
}
/** Add `token` column to `pricelists` */
public function upgrade_PricelistsToken()
{
sqlQuery('ALTER TABLE pricelists ADD COLUMN token varchar(255)');
$this->upgradeOK();
}
public function check_PriceListPriceHistory(): bool
{
return findModule(Modules::PRICE_HISTORY) && $this->checkColumnExists('pricelists', 'price_history');
}
/** Add pricelists.price_history column */
public function upgrade_PriceListPriceHistory(): void
{
sqlQuery('ALTER TABLE pricelists ADD COLUMN price_history TINYINT DEFAULT 0');
sqlQuery('ALTER TABLE pricelists_products ADD COLUMN price_for_discount DECIMAL(15, 4) DEFAULT NULL AFTER price');
$this->upgradeOK();
}
public function check_extendProductDiscount(): bool
{
return $this->checkColumnExists('pricelists', 'use_product_discount');
}
/** Add pricelists.use_product_discount column */
public function upgrade_extendProductDiscount(): void
{
sqlQuery('ALTER TABLE pricelists ADD COLUMN use_product_discount TINYINT DEFAULT 0');
$this->upgradeOK();
}
public function check_coefficient(): bool
{
return $this->checkColumnExists('pricelists', 'coefficient');
}
/** Add pricelists.coefficient column */
public function upgrade_coefficient(): void
{
sqlQuery('ALTER TABLE pricelists ADD COLUMN coefficient DECIMAL(4,2) DEFAULT NULL');
$this->upgradeOK();
}
public function check_generated(): bool
{
return $this->checkColumnExists('pricelists_products', 'generated');
}
/** Add pricelists_products.generated column */
public function upgrade_generated(): void
{
sqlQuery('ALTER TABLE pricelists_products ADD COLUMN generated TINYINT DEFAULT 0');
$this->upgradeOK();
}
public function check_PriceListDataColumn(): bool
{
return $this->checkColumnExists('pricelists', 'data');
}
/** add `pricelists.data` column */
public function upgrade_PriceListDataColumn(): void
{
sqlQuery('ALTER TABLE pricelists ADD COLUMN data MEDIUMTEXT DEFAULT NULL');
$this->upgradeOK();
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace KupShop\PricelistBundle\Resources\upgrade;
class UserUpgrade extends \UpgradeNew
{
protected $priority = 1110;
public function check_PriceListId(): bool
{
return $this->checkColumnExists('users', 'id_pricelist');
}
/** users: add id_pricelist column */
public function upgrade_PriceListId(): void
{
sqlQuery('ALTER TABLE users ADD COLUMN id_pricelist INT(11)');
sqlQuery('ALTER TABLE users ADD FOREIGN KEY (id_pricelist) REFERENCES pricelists(id) ON UPDATE CASCADE ON DELETE SET NULL');
$this->upgradeOK();
}
public function check_UserGroupsPriceListId(): bool
{
return $this->checkColumnExists('users_groups', 'id_pricelist');
}
/** Add `users_groups.id_pricelist` */
public function upgrade_UserGroupsPriceListId(): void
{
sqlQuery('ALTER TABLE users_groups ADD COLUMN id_pricelist INT DEFAULT NULL;');
sqlQuery('ALTER TABLE users_groups ADD CONSTRAINT users_groups_id_pricelists FOREIGN KEY (id_pricelist) REFERENCES pricelists(id) ON UPDATE CASCADE ON DELETE CASCADE;');
$this->upgradeOK();
}
public function check_UserGroupsPriceListConstraint(): bool
{
return $this->checkConstraintRule('users_groups', 'users_groups_id_pricelists', 'SET NULL', 'CASCADE');
}
public function upgrade_UserGroupsPriceListConstraint(): void
{
try {
sqlQuery('ALTER TABLE users_groups DROP CONSTRAINT users_groups_id_pricelists;');
} catch (\Exception $e) {
}
sqlQuery('ALTER TABLE users_groups ADD CONSTRAINT users_groups_id_pricelists FOREIGN KEY (id_pricelist) REFERENCES pricelists(id) ON UPDATE CASCADE ON DELETE SET NULL;');
$this->upgradeOK();
}
}

View File

@@ -0,0 +1,26 @@
{
"pricelists" : [
{
"id" : 4,
"currency" : "EUR",
"name" : "TEST_EUR",
"price_history" : 1,
"use_product_discount" : 1,
"coefficient" : null,
"data" : null
}
],
"pricelists_products" : [
{
"id" : 14,
"id_pricelist" : 4,
"id_product" : 11,
"id_variation" : null,
"price" : 499.9000,
"price_for_discount" : 499.9000,
"discount" : 15.00000000,
"integrity_unique" : "4-11",
"generated" : 0
}
]
}

View File

@@ -0,0 +1,72 @@
<?php
namespace KupShop\PricelistBundle\Tests;
use KupShop\PricelistBundle\Util\PriceListWorker;
use Query\Operator;
class PriceListWorkerTest extends \DatabaseTestCase
{
private PriceListWorker $priceListWorker;
protected function setUp(): void
{
parent::setUp();
$this->priceListWorker = $this->get(PriceListWorker::class);
}
/**
* @dataProvider data_testWithoutDiscountUpdatePricelist
*/
public function testWithoutDiscountUpdatePricelist(bool $withDiscountUpdate, \Decimal $discount, array $result)
{
$pricelistId = 4;
$productId = 11;
$variationId = null;
$price = toDecimal(449);
$this->priceListWorker->updatePriceList($pricelistId, $productId, $variationId, $price, $discount, $withDiscountUpdate);
$pricelistProduct = $this->getPricelistProduct($pricelistId, $productId, $variationId)[0];
$this->assertEquals($result, $pricelistProduct);
}
public function data_testWithoutDiscountUpdatePricelist()
{
// nemelo by updatovat slevu - zustane 15%
yield 'WithDiscountUpdateFalse' => [false, toDecimal(0), [
'id_pricelist' => 4,
'id_product' => 11,
'id_variation' => null,
'price' => '449.0000',
'price_for_discount' => '499.9000',
'discount' => '15.00000000',
]];
// melo by updatovat slevu - prepise se na null
yield 'WithDiscountUpdateTrue' => [true, toDecimal(0), [
'id_pricelist' => 4,
'id_product' => 11,
'id_variation' => null,
'price' => '449.0000',
'price_for_discount' => '499.9000',
'discount' => null,
]];
}
protected function getPricelistProduct(int $priceListId, int $productId, ?int $variationId)
{
return sqlQueryBuilder()
->select('id_pricelist, id_product, id_variation, price, price_for_discount, discount')
->from('pricelists_products')
->where(Operator::equalsNullable(['id_pricelist' => $priceListId, 'id_product' => $productId, 'id_variation' => $variationId]))
->execute()
->fetchAllAssociative();
}
public function getDataSet()
{
return $this->getJsonDataSetFromFile();
}
}

View File

@@ -0,0 +1,303 @@
{
"currencies": [
{
"id": "CZK",
"name": "Česká koruna",
"symbol": "Kč",
"price_round": 100,
"price_round_order": 0,
"price_round_direction": "math",
"price_precision": "0",
"price_decimal_mark": ",",
"rate": 1.00000000,
"sync": "N",
"corrections": 0.00,
"sync_date_update": null
}
],
"products": [
{
"id": 44802,
"title": "16GB USB ADATA C008 bílo/modrá (potisk)",
"code": "RQZA000101",
"ean": 4718050609642,
"short_descr": "",
"long_descr": "<br>* Elegantní a praktický USB flash drive. <br>* Vhodné pro potisk <br>* Kapacita - 16GB <br>* Barva/design - Modro-bílá <br>* Rozměry - 59,95x19,83x8,85 mm <br>* Hmotnost - 10g <br>* Rozhraní - USB 2.0 a kompatibilní i s USB 1.1 <br>* Příslušenství - poutko na pověšení <br>* Systémové požadavky - Windows 98/98SE/ME/2000/XP/Vista™/7 nebo vyšší, Mac OS 9.0 nebo vyšší, Linux Kernel 2.4 nebo vyšší <br>* Zdarma aplikace: <br>* UFDtoGO - nástroj pro obsluhu Vaseho USB Flash Drive <br>* Norton Internet Security (60 -dní trial) - anitvirus Více informací na stránkách výrobce: <br>* http://www.adata.com.tw/index_en.html",
"parameters": "",
"price": 77.6860,
"price_for_discount": 172.7273,
"price_common": 0.0000,
"vat": 1,
"discount": 2.00000000,
"producer": null,
"guarantee": 0,
"in_store": 2,
"pieces_sold": 0,
"delivery_time": 0,
"campaign": "",
"updated": "2023-08-04 01:21:01",
"date_added": "2023-06-30 15:29:18",
"figure": "Y",
"show_raw_price": "N",
"position": null,
"meta_title": null,
"meta_description": null,
"meta_keywords": null,
"show_in_feed": "Y",
"max_cpc": 0,
"in_store_min": null,
"note": "AC008-16G-RWE",
"weight": null,
"data": "{\"in_store_30\":\"2\"}",
"bonus_points": null,
"date_stock_in": null,
"price_buy": 68.0000,
"show_in_search": "Y",
"width": null,
"height": null,
"depth": null
},
{
"id": 1728,
"title": "70Mai Dash Cam 1S",
"code": "1BI3000101",
"ean": 6971669780463,
"short_descr": "Kamera do auta, 1080p při 30 fps, úhel záběru 130 stupňů, kvalitní snímač Sony IMX323",
"long_descr": "<h3>Mi Dash Cam 70 Mai 1S</h3>\r\n\r\n<p>Hledáte kameru do auta, která nebude stát mnoho peněz a přesto dokáže nabídnout vysokou kvalitu pořízeného záznamu? Právě jste ji našli - Mi Dash Cam 70 Mai 1S nabízí kromě uvedených vlastností ještě celou řadu zajímavých parametrů.</p>\r\n\r\n<p style=\"text-align:center\"><img alt=\"Kamera do auta Xiaomi Mi Dash Cam 70 Mai 1S\" src=\"/data/photos/old/product-texts/xiaomi-mi-dash-cam-70-mai-1s-3-jpg.jpg\" style=\"height:606px; width:800px\" title=\"Kamera do auta Xiaomi Mi Dash Cam 70 Mai 1S\" /></p>\r\n\r\n<p>Kvalitu záznamu garantuje osvědčený snímač Sony IMX323 s deklarovanou světelností f/2,2, díky které dokáže pořizovat hezký obraz i za zhoršených světelných podmínek. Zaznamenané video má vysoké rozlišení 1080p, tedy 1920 × 1080 obrazových bodů, což je zárukou zachycení dostatečných detailů.</p>\r\n\r\n<h3>Co umí chytrá kamera do auta?</h3>\r\n\r\n<p>Kameru nainstalujete za čelní sklo a její objektiv bude snímat dění před vozidlem. Díky širokému úhlu záběru 130 stupňů natáčí situaci v dostatečné perspektivě. Záznam videa je ukládán na paměťovou kartu - ta není součástí balení a doporučujeme používat karty microSD alespoň třídy Class 10.</p>\r\n\r\n<p style=\"text-align:center\"><img alt=\"Kamera do auta Xiaomi Mi Dash Cam 70 Mai 1S\" src=\"/data/photos/old/product-texts/xiaomi-mi-dash-cam-70-mai-1s-1-jpg.jpg\" style=\"height:465px; width:960px\" title=\"Kamera do auta Xiaomi Mi Dash Cam 70 Mai 1S\" /></p>\r\n\r\n<p>Kamera Mi Dash Cam 70 Mai 1S podporuje karty s kapacitou 16, 32, nebo 64 GB, což postačuje pro uložení mnoha hodin záznamu v nejvyšší kvalitě. V případě, že dojde k zaplnění karty, začne kamera automaticky přepisovat nejstarší záznamy.</p>\r\n\r\n<p style=\"text-align:center\"><img alt=\"Kamera do auta Xiaomi Mi Dash Cam 70 Mai 1S\" src=\"/data/photos/old/product-texts/xiaomi-mi-dash-cam-70-mai-1s-5-jpg.jpg\" style=\"height:537px; width:960px\" title=\"Kamera do auta Xiaomi Mi Dash Cam 70 Mai 1S\" /></p>\r\n\r\n<p>Zařízení je vybaveno snímačem zrychlení - v případě, že kamera zaznamená prudké brzdění nebo náraz, automaticky uloží předcházející záznam pro případné snazší dokazování vzniku nehody. Ke kameře se lze připojit přes aplikaci v mobilním telefonu a spravovat, stahovat a sdílet pořízená videa.</p>\r\n\r\n<p style=\"text-align:center\"><img alt=\"Kamera do auta Xiaomi Mi Dash Cam 70 Mai 1S\" src=\"/data/photos/old/product-texts/xiaomi-mi-dash-cam-70-mai-1s-4-jpg.jpg\" style=\"height:693px; width:960px\" title=\"Kamera do auta Xiaomi Mi Dash Cam 70 Mai 1S\" /></p>\r\n\r\n<h3>Vlastnosti Mi Dash Cam 70 Mai 1S</h3>\r\n\r\n<ul>\r\n\t<li>Název: Mi Dash Cam 70 Mai 1S</li>\r\n\t<li>Typ zařízení: kamera do auta/„černá skříňka“</li>\r\n\t<li>Snímač: Sony IMX323</li>\r\n\t<li>Rozlišení videa: 1080p - 1920 × 1080 obrazových bodů</li>\r\n\t<li>Úhel záběru: 130 stupňů</li>\r\n\t<li>Procesor: MSC8328P</li>\r\n\t<li>Úložiště: paměťová karta microSD (není součástí balení)</li>\r\n\t<li>Komunikace s telefonem: ano, přes aplikaci, Wi-Fi</li>\r\n</ul>\r\n\r\n<p style=\"text-align:center\"><img alt=\"Kamera do auta Xiaomi Mi Dash Cam 70 Mai 1S\" src=\"/data/photos/old/product-texts/xiaomi-mi-dash-cam-70-mai-1s-2-jpg.jpg\" style=\"height:716px; width:1000px\" title=\"Kamera do auta Xiaomi Mi Dash Cam 70 Mai 1S\" /></p>\r\n\r\n<h3>Obsah balení</h3>\r\n\r\n<ul>\r\n\t<li>1× kamera Mi Dash Cam 70 Mai 1S</li>\r\n\t<li>1× napájecí kabel dlouhý cca 3,5 metru</li>\r\n\t<li>1× uživatelská příručka</li>\r\n</ul>",
"parameters": "<h3>Vlastnosti Mi Dash Cam 70 Mai 1S</h3>\r\n\r\n<ul>\r\n\t<li>Název: Mi Dash Cam 70 Mai 1S</li>\r\n\t<li>Typ zařízení: kamera do auta/„černá skříňka“</li>\r\n\t<li>Snímač: Sony IMX323</li>\r\n\t<li>Rozlišení videa: 1080p - 1920 × 1080 obrazových bodů</li>\r\n\t<li>Úhel záběru: 130 stupňů</li>\r\n\t<li>Procesor: MSC8328P</li>\r\n\t<li>Úložiště: paměťová karta microSD (není součástí balení)</li>\r\n\t<li>Komunikace s telefonem: ano, přes aplikaci, Wi-Fi</li>\r\n</ul>\r\n\r\n<h3>Obsah balení</h3>\r\n\r\n<ul>\r\n\t<li>1× kamera Mi Dash Cam 70 Mai 1S</li>\r\n\t<li>1× napájecí kabel dlouhý cca 3,5 metru</li>\r\n\t<li>1× uživatelská příručka</li>\r\n</ul>",
"price": 794.2149,
"price_for_discount": 769.4215,
"price_common": 0.0000,
"vat": 1,
"discount": 20.00000000,
"producer": null,
"guarantee": 0,
"in_store": 8,
"pieces_sold": 1071,
"delivery_time": 0,
"campaign": "POP",
"updated": "2023-08-04 03:06:20",
"date_added": "2022-07-07 00:00:00",
"figure": "Y",
"show_raw_price": "N",
"position": null,
"meta_title": "Mi Dash Cam 70 Mai 1S - skvělá kamera bez kompromisů",
"meta_description": "Mi Dash Cam 70 Mai 1S",
"meta_keywords": "yi 4k action camera,akční kamera yi,xiaomi akční kamera,kit,voděodolné pouzdro",
"show_in_feed": "Y",
"max_cpc": 0,
"in_store_min": null,
"note": "",
"weight": 0.000000,
"data": "{\"abra\":{\"1BI3000101\":{\"in_store\":{\"1\":\"0\",\"7\":\"0\",\"3\":\"0\",\"2\":\"0\",\"6\":\"0\",\"10\":\"1\"},\"on_the_way_count\":\"0\",\"on_the_way_date\":\"26.07.2021\"}},\"generate_coupon\":\"N\",\"generate_coupon_discount\":\"4\",\"in_store_01\":\"8.000000\",\"in_store_20\":\"0\",\"in_store_30\":\"0\"}",
"bonus_points": null,
"date_stock_in": null,
"price_buy": 659.0000,
"show_in_search": "Y",
"width": null,
"height": null,
"depth": null
},
{
"id": 306,
"title": "Originální TPU obal pro Xiaomi Mi Max",
"code": "master_TMMH000101",
"ean": null,
"short_descr": "",
"long_descr": "<p><strong>Originální TPU obal pro Xiaomi Mi Max</strong></p>\r\n\r\n<p>Máte strach, že váš telefon utrží nepříjemné rány a škrábance při denním nošení? Co jej ochránit speciálním obalem z tvrdého materiálu, který udrží váš telefon v bezpečí? Svou odolností zamezí mechanickému poškození, je velmi příjemný na dotek a získáte navíc pevnější úchyt telefonu. Povedená konstrukce ani neruší design přístroje, obal nebrání pohodlnému ovládání a zároveň nezhoršuje funkce telefonu. Aplikace obalu je snadná, zároveň ale pevně drží a samovolně se z přístroje rozhodně neuvolní.</p>\r\n\r\n<p><img alt=\"Xiaomi Mi Max TPU\" src=\"/data/photos/old/product-texts/xiaomi-20mi-20max-20tpu-20-20pink-jpg.jpg\" style=\"height:363px; margin-left:auto; margin-right:auto; width:500px\" title=\"Xiaomi Mi Max TPU\" />  </p>\r\n\r\n<p><strong>Detail</strong></p>\r\n\r\n<ul>\r\n\t<li>ochrání tělo telefonu před mechanickým poškozením</li>\r\n\t<li>ideální ochrana pro telefon Xiaomi Mi Max</li>\r\n</ul>\r\n\r\n<p> </p>\r\n\r\n<p><strong>Obsah balení</strong></p>\r\n\r\n<ul>\r\n\t<li>TPU obal pro Xiaomi Redmi Mi Max</li>\r\n</ul>",
"parameters": "<p><strong>Detail</strong></p>\r\n\r\n<ul>\r\n\t<li>ochrání tělo telefonu před mechanickým poškozením</li>\r\n\t<li>ideální ochrana pro telefon Xiaomi Mi Max</li>\r\n</ul>\r\n\r\n<p> </p>\r\n\r\n<p><strong>Obsah balení</strong></p>\r\n\r\n<ul>\r\n\t<li>TPU obal pro Xiaomi Redmi Mi Max</li>\r\n</ul>",
"price": 81.8182,
"price_for_discount": 81.8182,
"price_common": 0.0000,
"vat": 1,
"discount": 33.00000000,
"producer": null,
"guarantee": 0,
"in_store": 6,
"pieces_sold": 65,
"delivery_time": 0,
"campaign": "",
"updated": "2023-08-07 15:17:58",
"date_added": "2022-07-07 00:00:00",
"figure": "Y",
"show_raw_price": "N",
"position": null,
"meta_title": null,
"meta_description": null,
"meta_keywords": "",
"show_in_feed": "Y",
"max_cpc": 0,
"in_store_min": null,
"note": null,
"weight": 0.000000,
"data": "{\"abra\":{\"5F60000101\":{\"in_store\":{\"5\":\"0\",\"1\":\"3\",\"6\":\"0\"}},\"1F60000101\":{\"in_store\":{\"5\":\"0\",\"7\":\"0\",\"1\":\"1\",\"6\":\"0\",\"3\":\"0\"}},\"4F60000101\":{\"in_store\":{\"5\":\"0\",\"1\":\"0\",\"6\":\"0\"}},\"3F60000101\":{\"in_store\":{\"5\":\"0\",\"1\":\"4\",\"6\":\"0\"}},\"2F60000101\":{\"in_store\":{\"5\":\"0\",\"1\":\"2\",\"6\":\"0\",\"3\":\"0\"}}},\"generate_coupon\":\"N\",\"generate_coupon_discount\":\"4\"}",
"bonus_points": null,
"date_stock_in": null,
"price_buy": null,
"show_in_search": "Y",
"width": null,
"height": null,
"depth": null
}
],
"products_variations": [
{
"id": 1455,
"id_product": 306,
"code": "2F60000101",
"ean": 8595633307982,
"title": "Barva: Černá",
"in_store": 0,
"delivery_time": 0,
"price": 81.8182,
"price_for_discount": 81.8182,
"figure": "Y",
"updated": "2023-05-11 11:26:10",
"note": null,
"weight": null,
"bonus_points": null,
"price_buy": 0.0000,
"width": null,
"height": null,
"depth": null,
"data": "{\"in_store_01\":\"0.000000\"}",
"price_common": 0.0000
},
{
"id": 1456,
"id_product": 306,
"code": "1F60000101",
"ean": 8595633307999,
"title": "Barva: Průhledná",
"in_store": 1,
"delivery_time": 0,
"price": 189.2562,
"price_for_discount": 81.8182,
"figure": "Y",
"updated": "2023-05-26 15:42:07",
"note": null,
"weight": null,
"bonus_points": null,
"price_buy": 100.0000,
"width": null,
"height": null,
"depth": null,
"data": "{\"in_store_01\":\"1.000000\"}",
"price_common": 0.0000
},
{
"id": 1457,
"id_product": 306,
"code": "4F60000101",
"ean": 8595633308019,
"title": "Barva: Růžová",
"in_store": 0,
"delivery_time": 0,
"price": 247.1074,
"price_for_discount": 247.1074,
"figure": "Y",
"updated": "2023-05-11 11:26:10",
"note": null,
"weight": null,
"bonus_points": null,
"price_buy": 0.0000,
"width": null,
"height": null,
"depth": null,
"data": "{\"in_store_01\":\"0.000000\"}",
"price_common": 0.0000
},
{
"id": 1458,
"id_product": 306,
"code": "3F60000101",
"ean": 8595633308002,
"title": "Barva: Modrá",
"in_store": 2,
"delivery_time": 0,
"price": 247.1074,
"price_for_discount": 81.8182,
"figure": "Y",
"updated": "2023-07-27 03:02:05",
"note": null,
"weight": null,
"bonus_points": null,
"price_buy": 151.0000,
"width": null,
"height": null,
"depth": null,
"data": "{\"in_store_01\":\"2.000000\"}",
"price_common": 0.0000
},
{
"id": 1459,
"id_product": 306,
"code": "5F60000101",
"ean": 8595633308026,
"title": "Barva: Žlutá",
"in_store": 3,
"delivery_time": 0,
"price": 222.3140,
"price_for_discount": 81.8182,
"figure": "Y",
"updated": "2023-05-26 15:42:07",
"note": null,
"weight": null,
"bonus_points": null,
"price_buy": 134.0000,
"width": null,
"height": null,
"depth": null,
"data": "{\"in_store_01\":\"3.000000\"}",
"price_common": 0.0000
}
],
"pricelists" : [
{
"id": 1,
"currency": "CZK",
"name": "CZK ceník",
"price_history": 0,
"use_product_discount": 1
},
{
"id": 2,
"currency": "CZK",
"name": "CZK ceník - bez přeráření",
"price_history": 0,
"use_product_discount": 0
}
],
"pricelists_products" : [
{
"id": 1,
"id_pricelist": 1,
"id_product": 44802,
"id_variation": null,
"price": 41.5,
"price_for_discount": null,
"discount": 5.00000000,
"integrity_unique": "1-44802"
},
{
"id": 2,
"id_pricelist": 2,
"id_product": 44802,
"id_variation": null,
"price": 41.5,
"price_for_discount": null,
"discount": 5.00000000,
"integrity_unique": "1-44802"
},
{
"id": 3,
"id_pricelist": 1,
"id_product": 306,
"id_variation": 1456,
"price": null,
"price_for_discount": null,
"discount": 80.00000000,
"integrity_unique": "1-306-1456"
}
]
}

View File

@@ -0,0 +1,145 @@
<?php
namespace KupShop\PricelistBundle\Tests;
use KupShop\DevelopmentBundle\Util\Tests\CartTestTrait;
use KupShop\OrderingBundle\Cart;
use KupShop\PricelistBundle\Context\PricelistContext;
class PricelistDiscountTest extends \DatabaseTestCase
{
use CartTestTrait;
/** @var PricelistContext */
private $pricelistContext;
protected function setUp(): void
{
parent::setUp();
$this->pricelistContext = $this->get(PricelistContext::class);
}
public function getDataSet()
{
return $this->getJsonDataSetFromFile();
}
public function testProductWithPricelistDiscountAndEnabledExtension()
{
$this->pricelistContext->activate(1);
$this->cart = $this->get(Cart::class);
$this->createCart();
$this->insertProduct(44802);
$this->cart->createFromDB();
// Kontrola nastavené slevy
$product = reset($this->cart->products)['product'];
$this->assertEquals(5.0, $product->discount->asFloat());
// Cena je v eurech načtená z ceníků
$this->assertEquals(50, $product->getProductPrice()->getPriceWithVatWithoutDiscount()->asFloat());
$this->assertEquals(48, $product->getProductPrice()->getPriceWithVat()->asFloat());
$this->assertEquals(48, $this->cart->totalPricePay->asFloat());
}
public function testProductWithPricelistDiscountAndDisabledExtension()
{
$this->pricelistContext->activate(2);
$this->cart = $this->get(Cart::class);
$this->createCart();
$this->insertProduct(44802);
$this->cart->createFromDB();
// Kontrola nastavené slevy
$product = reset($this->cart->products)['product'];
$this->assertEquals(5.0, $product->discount->asFloat());
// Cena je v eurech načtená z ceníků
$this->assertEquals(50, $product->getProductPrice()->getPriceWithVatWithoutDiscount()->asFloat());
$this->assertEquals(48, $product->getProductPrice()->getPriceWithVat()->asFloat());
$this->assertEquals(48, $this->cart->totalPricePay->asFloat());
}
public function testProductWithouPricelistDiscountAndDisabledExtension()
{
$this->pricelistContext->activate(2);
$this->cart = $this->get(Cart::class);
$this->createCart();
$this->insertProduct(1728);
$this->cart->createFromDB();
$product = reset($this->cart->products)['product'];
$this->assertEquals(0.0, $product->discount->asFloat());
$this->assertEquals(961, $product->getProductPrice()->getPriceWithVatWithoutDiscount()->asFloat());
$this->assertEquals(961, $product->getProductPrice()->getPriceWithVat()->asFloat());
$this->assertEquals(961, $this->cart->totalPricePay->asFloat());
}
public function testProductWithouPricelistDiscountAndEnabledExtension()
{
$this->pricelistContext->activate(1);
$this->cart = $this->get(Cart::class);
$this->createCart();
$this->insertProduct(1728);
$this->cart->createFromDB();
$product = reset($this->cart->products)['product'];
$this->assertEquals(20.0, $product->discount->asFloat());
$this->assertEquals(961, $product->getProductPrice()->getPriceWithVatWithoutDiscount()->asFloat());
$this->assertEquals(769, $product->getProductPrice()->getPriceWithVat()->asFloat());
$this->assertEquals(769, $this->cart->totalPricePay->asFloat());
}
public function testVariantWithouPricelistDiscountAndEnabledExtension()
{
$this->pricelistContext->activate(1);
$this->cart = $this->get(Cart::class);
$this->createCart();
$this->insertProduct(306, 1455);
$this->cart->createFromDB();
$product = reset($this->cart->products)['product'];
$this->assertEquals(33.0, $product->discount->asFloat());
$this->assertEquals(99, $product->getProductPrice()->getPriceWithVatWithoutDiscount()->asFloat());
$this->assertEquals(66, $product->getProductPrice()->getPriceWithVat()->asFloat());
$this->assertEquals(66, $this->cart->totalPricePay->asFloat());
}
public function testVariantWithPricelistDiscountAndEnabledExtension()
{
$this->pricelistContext->activate(1);
$this->cart = $this->get(Cart::class);
$this->createCart();
$this->insertProduct(306, 1456);
$this->cart->createFromDB();
$product = reset($this->cart->products)['product'];
$this->assertEquals(80.0, $product->discount->asFloat());
$this->assertEquals(229, $product->getProductPrice()->getPriceWithVatWithoutDiscount()->asFloat());
$this->assertEquals(46, $product->getProductPrice()->getPriceWithVat()->asFloat());
$this->assertEquals(46, $this->cart->totalPricePay->asFloat());
}
}

View File

@@ -0,0 +1,126 @@
{
"pricelists" : [
{
"id":1,
"currency":"CZK",
"name":"PricelistCZK",
"coefficient":null
},
{
"id":2,
"currency":"EUR",
"name":"PricelistEUR",
"coefficient":null
}
],
"pricelists_products" : [
{
"id":1,
"id_pricelist":1,
"id_product":1,
"id_variation":null,
"price":1000,
"discount":null,
"generated":0
},
{
"id":2,
"id_pricelist":2,
"id_product":1,
"id_variation":null,
"price":38.5,
"discount":null,
"generated":0
},
{
"id":3,
"id_pricelist":1,
"id_product":4,
"id_variation":1,
"price":260,
"discount":null,
"generated":0
},
{
"id":8,
"id_pricelist":1,
"id_product":4,
"id_variation":2,
"price":260,
"discount":5,
"generated":0
},
{
"id":4,
"id_pricelist":2,
"id_product":4,
"id_variation":null,
"price":10,
"discount":10,
"generated":0
},
{
"id":9,
"id_pricelist":2,
"id_product":4,
"id_variation":2,
"price":10,
"discount":20,
"generated":0
},
{
"id":5,
"id_pricelist":1,
"id_product":3,
"id_variation":null,
"price":260,
"discount":10,
"generated":0
},
{
"id":6,
"id_pricelist":2,
"id_product":3,
"id_variation":null,
"price":10,
"discount":10,
"generated":0
},
{
"id":7,
"id_pricelist":2,
"id_product":6,
"id_variation":null,
"price":null,
"discount":null,
"generated":0
},
{
"id":10,
"id_pricelist":2,
"id_product":11,
"id_variation":22,
"price":10,
"discount":null,
"generated":0
},
{
"id":11,
"id_pricelist":2,
"id_product":10,
"id_variation":null,
"price":20,
"discount":null,
"generated":0
},
{
"id":12,
"id_pricelist":2,
"id_product":9,
"id_variation":null,
"price":null,
"discount":25,
"generated":0
}
]
}

View File

@@ -0,0 +1,308 @@
<?php
namespace KupShop\PricelistBundle\Tests;
use KupShop\DevelopmentBundle\Util\Tests\CartTestTrait;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\OrderingBundle\Util\CartFactory;
use KupShop\PricelistBundle\Context\PricelistContext;
use KupShop\PricelistBundle\Entity\Pricelist;
class PricelistTest extends \DatabaseTestCase
{
use CartTestTrait;
protected function setUp(): void
{
$this->createCart();
parent::setUp();
}
/*
* HACK
* Prazdny test - prvni test vzdycky spadl s chybou:
* No database selected
* nevime proc, ale tohle to fixnulo
*/
public function testFixSetUp()
{
$this->assertTrue(true);
}
public function testPricelistEurPrice()
{
$pricelistContext = ServiceContainer::getService(PricelistContext::class);
$currencyContext = ServiceContainer::getService(CurrencyContext::class);
$pricelistContext->activate(2);
$currencyContext->activate('EUR');
$productList = new \ProductList();
$products = $productList->getProducts();
$this->assertEquals('46,60 €', $products[1]->price);
$this->assertEquals('115,40 €', $products[6]->price);
}
public function testPricelistCzkPrice()
{
$pricelistContext = ServiceContainer::getService(PricelistContext::class);
$pricelistContext->activate(1);
$productList = new \ProductList();
$product = $productList->getProducts()[1];
$this->assertEquals('1 210 Kč', $product->price);
}
public function testPricelistCzkVariationPrice()
{
$pricelistContext = ServiceContainer::getService(PricelistContext::class);
$pricelistContext->activate(1);
$productList = new \ProductList();
$productList->fetchVariations(null);
$product = $productList->getProducts()[4];
$this->assertEquals('315 Kč', $product->price);
$variation = $product->variations[1];
$this->assertEquals('315 Kč', printPrice($variation['price']));
$variation = $product->variations[2];
$this->assertEquals('299 Kč', printPrice($variation['price'])); // 315 - 5% (variation pricelist discount)
}
public function testPricelistEurVariationPrice()
{
$pricelistContext = ServiceContainer::getService(PricelistContext::class);
$currencyContext = ServiceContainer::getService(CurrencyContext::class);
$pricelistContext->activate(2);
$currencyContext->activate('EUR');
$productList = new \ProductList();
$productList->fetchVariations(null);
$product = $productList->getProducts()[4];
// Zakomentováno, varianty nemají na sobě cenu, a tak se nedokáže správně dofetchovat na produkt správná pricelist cena se slevou.
// Vzhledem ke komplexnosti problému a jeho malému use-casu jsme se to rozhodli neřešit.
// $this->assertEquals('9,70 €', $product->price); // 12,10 - 20% (variation pricelist discount from cheapest variation)
$variation = $product->variations[1];
$this->assertEquals('10,90 €', printPrice($variation['price'])); // 12,10 - 10% (product pricelist discount)
$variation = $product->variations[2];
$this->assertEquals('9,70 €', printPrice($variation['price'])); // 12,10 - 20% (variation pricelist discount)
}
public function testCartCZK()
{
$pricelistContext = ServiceContainer::getService(PricelistContext::class);
$pricelistContext->activate(1);
$this->loginUser(1);
// insert product id 3, 2 pieces
$this->insertProduct(3, null, 2);
$this->cart->createFromDB();
$this->assertEquals(\Decimal::create('566.0000', 4), $this->cart->totalPriceWithVat);
$this->checkOrderPriceIsSameAsCart();
}
public function testCartVariationCZK()
{
$pricelistContext = ServiceContainer::getService(PricelistContext::class);
$pricelistContext->activate(1);
$this->loginUser(1);
// insert product id 4, id variation 1
$this->insertProduct(4, 1); // price = 315 (no discount)
// insert product id 4, id variation 2
$this->insertProduct(4, 2); // price = 299 = 315 - 5% (variation pricelist discount)
$this->cart->createFromDB();
$this->assertEquals(\Decimal::create('614.0000', 4), $this->cart->totalPriceWithVat); // 315 + 299
$this->checkOrderPriceIsSameAsCart();
}
public function testCartEUR()
{
$pricelistContext = ServiceContainer::getService(PricelistContext::class);
$currencyContext = ServiceContainer::getService(CurrencyContext::class);
$pricelistContext->activate(2);
$this->loginUser(1);
$currencyContext->activate('EUR');
// insert product id 3, 2 pieces
$this->insertProduct(3, null, 2);
$this->cart->createFromDB();
$this->assertEquals(\Decimal::create('21.8000', 4), $this->cart->totalPriceWithVat);
$this->checkOrderPriceIsSameAsCart();
}
public function testCartVariationEUR()
{
$pricelistContext = ServiceContainer::getService(PricelistContext::class);
$currencyContext = ServiceContainer::getService(CurrencyContext::class);
$pricelistContext->activate(2);
$this->loginUser(1);
$currencyContext->activate('EUR');
// insert product id 4, id variation 1
$this->insertProduct(4, 1); // price = 10,90 € = 12,10 - 10% (product pricelist discount)
// insert product id 4, id variation 2
$this->insertProduct(4, 2); // price = 9,70 € = 12,10 - 20% (variation pricelist discount)
$this->cart->createFromDB();
$this->assertEquals(\Decimal::create('20.6000', 4), $this->cart->totalPriceWithVat); // 10,90 + 9,70
$this->checkOrderPriceIsSameAsCart();
}
public function testProductListFetchVariations()
{
$productList = new \ProductList(false);
$productList->fetchVariations([11]);
$product = $productList->getProducts()[1];
$this->assertEquals(20, $product->discount->asFloat());
// activate pricelist
$pricelistContext = ServiceContainer::getService(PricelistContext::class);
$pricelistContext->activate(1);
$productList = new \ProductList(false);
$productList->fetchVariations(null);
$product = $productList->getProducts()[1];
$this->assertEquals(0, $product->discount->asFloat());
$product = $productList->getProducts()[4];
$this->assertEquals(0, $product->discount->asFloat());
$variation = $product->variations[1];
$this->assertNull($variation['pricelistDiscount']);
$variation = $product->variations[2];
$this->assertEquals(5, $variation['pricelistDiscount']);
$productList = new \ProductList(true); // variationsAsResult
$products = $productList->getProducts();
$product = $products['4/1'];
$this->assertEquals(315, $product->getProductPrice()->getPriceWithVat()->asFloat()); // pricelist price, no discount
$this->assertEquals(0, $product->discount->asFloat());
$product = $products['4/2'];
$this->assertEquals(299, $product->getProductPrice()->getPriceWithVat()->asFloat()); // 315 - 5% (variation pricelist discount)
$this->assertEquals(5, $product->discount->asFloat());
}
public function testApplyPriceListCoefficient()
{
$pricelistContext = ServiceContainer::getService(PricelistContext::class);
$currencyContext = ServiceContainer::getService(CurrencyContext::class);
$pricelistContext->activate(2);
$currencyContext->activate('EUR');
sqlQueryBuilder()->update('pricelists_products')->directValues(['discount' => 25])->andWhere('id_product=6')->execute();
$this->insertProduct(8); // produkt se zakladni cenou 191.9€
$this->insertProduct(7, 6); // varianta se zakladni cenou 48.2€
$this->insertProduct(10); // produkt s pevou cenikovou cenou 23€
$this->insertProduct(11, 22); // varianta s pevou cenikovou cenou 12.1€*/
$this->insertProduct(6, 17); // varianta se slevou 25% ze zakl. ceny 115.4€
$this->insertProduct(9); // produkt se slevou 25% ze zakl. ceny 241.9€
$this->cart->createFromDB();
$expected = 191.9 + 48.2 + 23 + 12.1 + 115.4 * 0.75 + 241.9 * 0.75;
self::assertEquals($expected, $this->cart->totalPriceWithVat->asFloat(), 'Porovnani cen pred nastavenim koeficientu ceniku', 0.1);
/** @var Pricelist $activePricelist */
$activePricelist = Contexts::get(PricelistContext::class)->getActive();
$activePricelist?->setCoefficient(2);
$this->cart = $this->get(CartFactory::class)->create();
$this->cart->createFromDB();
$expected = 191.9 * 2 + 48.2 * 2 + 23 + 12.1 + (115.4 * 2) * 0.75 + (241.9 * 2) * 0.75;
self::assertEquals($expected, $this->cart->totalPriceWithVat->asFloat(), 'Porovnani cen po nastaveni koeficientu ceniku', 0.1);
}
public function testApplyPriceListCoefficientProduct()
{
$pricelistContext = ServiceContainer::getService(PricelistContext::class);
$currencyContext = ServiceContainer::getService(CurrencyContext::class);
$priceConverter = ServiceContainer::getService(\KupShop\I18nBundle\Util\PriceConverter::class);
$pricelistContext->activate(2);
$currencyContext = ServiceContainer::getService(CurrencyContext::class);
$priceConverter = ServiceContainer::getService(\KupShop\I18nBundle\Util\PriceConverter::class);
$pricelistContext->activate(2);
$currencyContext->activate('EUR');
$product = new \Product(); // produkt se zakladni cenou 191.9€
$product->createFromDB('8');
self::assertEquals('191,90 €', $product->price, 'Porovnani cen pred nastavenim koeficientu ceniku');
self::assertEquals('191.92', round($priceConverter->convert($product->getProductPrice()->getCurrency(), $currencyContext->getActive(), $product->getProductPrice()->getPriceWithVat())->asFloat(), 2), 'Porovnani cen pred nastavenim koeficientu ceniku');
self::assertEquals('191.92', round($priceConverter->convert($product->getProductPrice()->getCurrency(), $currencyContext->getActive(), $product->priceOriginal->getValue()->addVat(21))->asFloat(), 2), 'Porovnani cen pred nastavenim koeficientu ceniku - price original');
self::assertEquals('191.92', round($priceConverter->convert($product->getProductPrice()->getCurrency(), $currencyContext->getActive(), $product->priceForDiscount->getValue()->addVat(21))->asFloat(), 2), 'Porovnani cen pred nastavenim koeficientu ceniku - price for discount');
/** @var Pricelist $activePricelist */
$activePricelist = Contexts::get(PricelistContext::class)->getActive();
$activePricelist?->setCoefficient(2);
$product->createFromDB('8');
self::assertEquals('383,85 €', $product->price, 'Porovnani cen po nastavenim koeficientu ceniku');
self::assertEquals('383.85', round($priceConverter->convert($product->getProductPrice()->getCurrency(), $currencyContext->getActive(), $product->getProductPrice()->getPriceWithVat())->asFloat(), 2), 'Porovnani cen po nastavenim koeficientu ceniku');
self::assertEquals('383.85', round($priceConverter->convert($product->getProductPrice()->getCurrency(), $currencyContext->getActive(), $product->priceOriginal->getValue()->addVat(21))->asFloat(), 2), 'Porovnani cen po nastavenim koeficientu ceniku - price original');
self::assertEquals('383.85', round($priceConverter->convert($product->getProductPrice()->getCurrency(), $currencyContext->getActive(), $product->priceForDiscount->getValue()->addVat(21))->asFloat(), 2), 'Porovnani cen po nastavenim koeficientu ceniku - price for discount');
}
public function getDataSet()
{
return $this->getJsonDataSetFromFile();
}
private function insertProduct($id_product, $id_variation = null, $pieces = 1)
{
$item = [
'id_product' => $id_product,
'id_variation' => $id_variation,
'pieces' => $pieces,
];
$this->assertGreaterThan(0, $this->cart->addItem($item));
}
private function loginUser($id_user)
{
$user = \User::createFromId($id_user);
$user->activateUser();
}
private function checkOrderPriceIsSameAsCart()
{
// je to fuj, ale je to kvuli spravnym cenam... aby se zapocitala i cena DeliveryType
$this->cart->max_step = 1;
$this->cart->createFromDB();
$order = $this->cart->submitOrder();
$this->assertInstanceOf(\Order::class, $order);
$this->assertEquals($order->total_price->asFloat(), $this->cart->totalPricePay->asFloat(), '', 0.01);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace KupShop\PricelistBundle\Util\Price;
use KupShop\KupShopBundle\Util\Price\ProductPrice;
class PriceListPrice extends ProductPrice
{
/** @var ProductPrice */
protected $originalPrice;
public function setOriginalPrice(ProductPrice $originalPrice)
{
$this->originalPrice = $originalPrice;
}
/**
* @return ProductPrice
*/
public function getOriginalPrice()
{
return $this->originalPrice;
}
/**
* @return \Decimal
*/
public function getDiscount()
{
return $this->discount;
}
/**
* @return ProductPrice
*/
public function applyCoefficient(\Decimal $coefficient)
{
$this->setValue($this->value->mul($coefficient));
$this->originalPrice->setValue($this->originalPrice->value->mul($coefficient));
return $this;
}
}

View File

@@ -0,0 +1,264 @@
<?php
namespace KupShop\PricelistBundle\Util;
use KupShop\KupShopBundle\Util\Functional\Mapping;
use Query\Operator;
class PriceListWorker
{
use \DatabaseCommunication;
private array $priceListsCache = [];
private array $priceListsByNameCache = [];
public function getPriceList(int $id): ?array
{
$this->loadPriceListsCache();
if ($this->priceListsCache[$id] ?? false) {
return $this->priceListsCache[$id];
}
return null;
}
public function getPriceLists(): array
{
$this->loadPriceListsCache();
return $this->priceListsCache;
}
public function findPriceList(string $name, string $currency): int
{
$priceListName = mb_strtolower($name.'-'.$currency);
if (!$this->priceListsByNameCache) {
$this->priceListsByNameCache = Mapping::mapKeys(
sqlQueryBuilder()
->select('id, name, currency')
->from('pricelists')
->execute()->fetchAll(),
function ($k, $v) {
return [mb_strtolower($v['name'].'-'.$v['currency']), (int) $v['id']];
}
);
}
if (!($this->priceListsByNameCache[$priceListName] ?? false)) {
$this->priceListsByNameCache[$priceListName] = sqlGetConnection()->transactional(
function () use ($name, $currency) {
sqlQueryBuilder()
->insert('pricelists')
->directValues(
[
'name' => $name,
'currency' => $currency,
]
)->execute();
return (int) sqlInsertId();
}
);
}
return $this->priceListsByNameCache[$priceListName];
}
public function updatePriceList(int $priceListId, int $productId, ?int $variationId, \Decimal $price, ?\Decimal $discount = null, bool $withDiscountUpdate = true): void
{
$options = [
'price' => $price->asFloat(),
'showVat' => 'N',
];
if ($withDiscountUpdate) {
$options['discount'] = $discount?->asFloat();
}
$this->updatePricelists(
$priceListId,
$options,
$productId,
0,
$variationId,
$withDiscountUpdate
);
}
/**
* Updates discount only (price is not changed).
*/
public function updatePriceListDiscount(int $priceListId, int $productId, ?int $variationId, ?\Decimal $discount): void
{
sqlQueryBuilder()
->update('pricelists_products')
->directValues(['discount' => $discount])
->where(Operator::equals(['id' => $this->getPriceListItemId($priceListId, $productId, $variationId)]))
->execute();
}
/** @deprecated use updatePriceList */
public function updatePricelists(int $pricelistId, array $options, int $productId, ?float $vat, ?int $variationId = null, bool $withDiscountUpdate = true)
{
if (empty($options['price'])) {
$options['price'] = null;
}
if (empty($options['discount'])) {
$options['discount'] = null;
}
$data = ['id_pricelist' => $pricelistId, 'id_product' => $productId, 'id_variation' => $variationId];
$pricelistProduct = sqlQueryBuilder()
->select('*')
->from('pricelists_products')
->where(Operator::equalsNullable($data))
->execute()->fetchAssociative();
if (is_null($options['price']) && is_null($options['discount'])) {
if (!empty($pricelistProduct)) {
if (!$withDiscountUpdate) {
$affectedRows = sqlQueryBuilder()
->delete('pricelists_products')
->where(Operator::equals(['id' => $pricelistProduct['id']]))
->andWhere(Operator::isNull('discount'))
->execute();
if (!$affectedRows) {
sqlQueryBuilder()
->update('pricelists_products')
->directValues(['price' => null])
->andWhere(Operator::equals(['id' => $pricelistProduct['id']]))
->andWhere(Operator::not(Operator::isNull('discount')))
->execute();
}
} else {
sqlQueryBuilder()->delete('pricelists_products')
->where(Operator::equals(['id' => $pricelistProduct['id']]))
->execute();
}
}
return;
}
if (empty($pricelistProduct)) {
$pricelistProduct = $data;
}
$price = $this->preparePrice($options['price']);
// vat remove before saving to database
if ($options['showVat'] == 'Y') {
$price = $this->removeVatFromPrice($price, $vat);
}
$discount = null;
if (!$withDiscountUpdate) {
$discount = $this->getPriceListDiscount($pricelistId, $productId, $variationId);
}
if ($discount || $withDiscountUpdate) {
$pricelistProduct['discount'] = $options['discount'] ?? $discount;
}
$pricelistProduct['price'] = $price;
$pricelistProduct['generated'] = !isset($options['generated']) ? $pricelistProduct['generated'] ?? 0 : $options['generated'];
sqlQueryBuilder()
->insert('pricelists_products')
->directValues($pricelistProduct)
->onDuplicateKeyUpdate(['price', 'discount', 'generated'])
->execute();
}
/**
* @return \Decimal
*/
public function removeVatFromPrice($price, $vat)
{
if ($price == null) {
return null;
}
$price = \Decimal::create($price, 4);
return $price->mul(\Decimal::create(100))->div(\Decimal::create(100 + $vat));
}
public function getPriceListItemId(int $priceListId, int $productId, ?int $variationId = null): int
{
$id = sqlQueryBuilder()
->select('id')
->from('pricelists_products')
->where(
Operator::equalsNullable(
[
'id_pricelist' => $priceListId,
'id_product' => $productId,
'id_variation' => $variationId,
])
)->execute()->fetchOne();
if (!$id) {
$id = sqlGetConnection()->transactional(function () use ($priceListId, $productId, $variationId) {
sqlQueryBuilder()
->insert('pricelists_products')
->directValues(
[
'id_pricelist' => $priceListId,
'id_product' => $productId,
'id_variation' => $variationId,
]
)->execute();
return (int) sqlInsertId();
});
}
return $id;
}
public function getPriceListItem(int $priceListId, int $productId, ?int $variationId = null): ?array
{
$item = sqlQueryBuilder()
->select('*')
->from('pricelists_products')
->where(
Operator::equalsNullable(
[
'id_pricelist' => $priceListId,
'id_product' => $productId,
'id_variation' => $variationId,
])
)->execute()->fetchAssociative();
return $item ?: null;
}
protected function loadPriceListsCache(): void
{
if (!$this->priceListsCache) {
$this->priceListsCache = Mapping::mapKeys(
sqlQueryBuilder()
->select('id, name, currency')
->from('pricelists')
->execute()->fetchAllAssociative(),
function ($k, $v) {
return [(int) $v['id'], $v];
}
);
}
}
protected function getPriceListDiscount(int $priceListId, int $productId, ?int $variationId)
{
return sqlQueryBuilder()
->select('discount')
->from('pricelists_products')
->where(Operator::equalsNullable(['id_pricelist' => $priceListId, 'id_product' => $productId, 'id_variation' => $variationId]))
->execute()
->fetchOne();
}
}