1856 lines
61 KiB
PHP
1856 lines
61 KiB
PHP
<?php
|
|
|
|
use KupShop\BonusProgramBundle\Utils\BonusComputer;
|
|
use KupShop\CatalogBundle\ProductList\FilterParams;
|
|
use KupShop\CatalogBundle\ProductList\MultiFetch;
|
|
use KupShop\CatalogBundle\ProductList\ProductCollection;
|
|
use KupShop\CatalogBundle\Query\RelatedProducts;
|
|
use KupShop\CatalogBundle\Util\ProductAvailabilityUtil;
|
|
use KupShop\I18nBundle\Translations\PhotosTranslation;
|
|
use KupShop\I18nBundle\Translations\ProductsUnitsTranslation;
|
|
use KupShop\I18nBundle\Util\PriceConverter;
|
|
use KupShop\KupShopBundle\Config;
|
|
use KupShop\KupShopBundle\Context\CurrencyContext;
|
|
use KupShop\KupShopBundle\Context\PosContext;
|
|
use KupShop\KupShopBundle\Context\PriceLevelContext;
|
|
use KupShop\KupShopBundle\Context\VatContext;
|
|
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
|
|
use KupShop\KupShopBundle\Util\Contexts;
|
|
use KupShop\KupShopBundle\Util\Price\Price;
|
|
use KupShop\KupShopBundle\Util\Price\PriceLevelPrice;
|
|
use KupShop\KupShopBundle\Util\Price\PriceUtil;
|
|
use KupShop\KupShopBundle\Util\Price\ProductPrice;
|
|
use KupShop\KupShopBundle\Util\Suppliers\SuppliersUtil;
|
|
use KupShop\KupShopBundle\Wrapper\PriceWrapper;
|
|
use KupShop\OrderingBundle\Event\OrderItemEvent;
|
|
use KupShop\PricelistBundle\Context\PricelistContext;
|
|
use KupShop\ProductsChargesBundle\Util\ChargesUtil;
|
|
use KupShop\RestrictionsBundle\Utils\Restrictions;
|
|
use Query\Operator as Op;
|
|
use Query\Translation;
|
|
|
|
/**
|
|
* Class representing one product.
|
|
*
|
|
* @author Joe
|
|
*/
|
|
#[AllowDynamicProperties]
|
|
class ProductBase implements ArrayAccess
|
|
{
|
|
use DatabaseCommunication;
|
|
|
|
/**
|
|
* Product ID.
|
|
*
|
|
* @var int
|
|
*/
|
|
public $id = -1;
|
|
|
|
/**
|
|
* Product texts.
|
|
*
|
|
* @var string
|
|
*/
|
|
public $title = '';
|
|
public $descr = '';
|
|
public $longDescr = '';
|
|
public $parameters = '';
|
|
|
|
public $id_block;
|
|
|
|
/**
|
|
* SEO texts.
|
|
*
|
|
* @var string
|
|
*/
|
|
public $meta_title = '';
|
|
public $meta_description = '';
|
|
public $meta_keywords = '';
|
|
|
|
/**
|
|
* Images.
|
|
*
|
|
* @var array
|
|
*/
|
|
public $image = [];
|
|
public $photos = [];
|
|
public $photoId = -1;
|
|
public $photoDescr = '';
|
|
public $photoDateUpdate;
|
|
|
|
/**
|
|
* Prices.
|
|
*
|
|
* @var int
|
|
*/
|
|
// cena nejlevnejsi varianty, nebo cena produktu, pokud nema varianty
|
|
public $price = 0; // ve vysledku je to string, napr. "100 Kč"
|
|
// priceRaw = p.price - cena produktu
|
|
public $priceRaw = 0; // Decimal
|
|
public $priceNoVat = 0; // string, napr. "83 Kč", skoro se nepouziva, muzeme smazat?
|
|
public $priceCommon; // ProductPrice
|
|
// cena nejdrazsi varianty, nebo cena produktu, pokud nema varianty
|
|
public $priceMax = 0; // array
|
|
public $pricelistPrice; // nepouziva se?
|
|
|
|
public $pricelistId;
|
|
|
|
/**
|
|
* @var Decimal
|
|
*/
|
|
public $discount;
|
|
public $pricelistDiscount;
|
|
public $vat = 0;
|
|
public $vat_id = 0;
|
|
|
|
/**
|
|
* Nove promenne s cenou.
|
|
*
|
|
* @var Decimal[]
|
|
*/
|
|
public $price_array = [];
|
|
public $priceCommon_array = [];
|
|
|
|
/**
|
|
* @var ?array
|
|
*/
|
|
public $producer;
|
|
/**
|
|
* Basic product informations.
|
|
*
|
|
* @var unknown_type
|
|
*/
|
|
public $producerTitle;
|
|
public $producerImage;
|
|
public $idProducer;
|
|
public $code = '';
|
|
public $guarantee = 0;
|
|
public $campaign = '';
|
|
public $campaign_codes = [];
|
|
public $charges;
|
|
public $visible;
|
|
|
|
/**
|
|
* Stock and delivery time informations.
|
|
*
|
|
* @var int
|
|
*/
|
|
public $inStore = 0;
|
|
public $deliveryTime = 0;
|
|
public $deliveryTimeText = '';
|
|
public $deliveryTimeRaw = 0;
|
|
protected $canBuy;
|
|
protected $canWatch;
|
|
|
|
/**
|
|
* Additional informations.
|
|
*
|
|
* @var unknown_type
|
|
*/
|
|
public $links = [];
|
|
public $attachments = [];
|
|
/**
|
|
* @var ParameterConfiguration[]
|
|
*/
|
|
public $configurations;
|
|
public $param;
|
|
public $related = [];
|
|
public $commentsOpen = false;
|
|
public $comments = [];
|
|
public $variations;
|
|
public $alsoBuyed = [];
|
|
public $templates;
|
|
|
|
public $sets;
|
|
public $gifts;
|
|
public $labels;
|
|
public $multiSets;
|
|
public $collections;
|
|
public $products_related;
|
|
public $matched_id_variation;
|
|
|
|
public ?\KupShop\ComponentsBundle\Entity\Thumbnail $thumbnail;
|
|
|
|
public $data;
|
|
public $sections;
|
|
|
|
public $width;
|
|
public $height;
|
|
public $depth;
|
|
|
|
/** @var \KupShop\PricelistBundle\Util\Price\PriceListPrice */
|
|
protected $productPrice;
|
|
|
|
public $ean;
|
|
public $weight;
|
|
public $in_store_suppliers;
|
|
public ?DateTimeImmutable $forecasted_delivery_date = null;
|
|
public $note;
|
|
public $priceOriginal; // PriceWrapper
|
|
public $price_buy; // array|ProductPrice
|
|
public $priceForDiscount; // ProductPrice
|
|
public $reviews;
|
|
public ?array $rating = null;
|
|
public $variationId;
|
|
public $bonus_points;
|
|
public $unit;
|
|
/**
|
|
* @var string
|
|
*/
|
|
public $note_;
|
|
public $id_cn;
|
|
|
|
public $productCode;
|
|
public array $convertors = [];
|
|
|
|
/** @var string{'Y', 'N'} */
|
|
public string $showInSearch = 'Y';
|
|
|
|
public ?int $position = null;
|
|
public ?int $pieces_sold = null;
|
|
|
|
/**
|
|
* Constructor.
|
|
*/
|
|
public function __construct($id = -1)
|
|
{
|
|
$this->id = $id;
|
|
|
|
$this->discount = DecimalConstants::zero();
|
|
}
|
|
|
|
/**
|
|
* Fetches data from database, product ID specified in $pid.
|
|
*
|
|
* @param int $pid
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function createFromDB($pid = null)
|
|
{
|
|
if ($pid) {
|
|
$this->id = $pid;
|
|
}
|
|
|
|
$SQL = $this->createQueryBuilder()
|
|
->andWhere(Op::equals(['p.id' => $this->id]))
|
|
->execute();
|
|
|
|
if (sqlNumRows($SQL) == 1) {
|
|
$prod = sqlFetchAssoc($SQL);
|
|
|
|
$this->title = $prod['title'];
|
|
|
|
$this->createFromArray($prod);
|
|
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetches data from given array.
|
|
*
|
|
* @param array $data
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function createFromArray($data)
|
|
{
|
|
$this->id = $data['id'];
|
|
$this->id_block = $data['id_block'] ?? null;
|
|
$this->title = $data['title'];
|
|
$this->discount = toDecimal($data['discount']);
|
|
$this->vat = getVat($data['vat']);
|
|
$this->vat_id = $data['vat'];
|
|
$this->original_vat = $data['original_vat'] ?? $data['vat'];
|
|
|
|
$this->deliveryTime = $data['delivery_time'];
|
|
$this->code = $data['code'];
|
|
|
|
$this->guarantee = $data['guarantee'] ?? null;
|
|
$this->idProducer = $data['producer'] ?? null;
|
|
$this->longDescr = trim($data['long_descr'] ?? '');
|
|
$this->descr = trim($data['short_descr'] ?? '');
|
|
$this->parameters = trim($data['parameters'] ?? '');
|
|
$this->campaign = productCampaign($data['campaign'] ?? '', $this->campaign_codes);
|
|
$this->inStore = $data['in_store'] ?? null;
|
|
$this->visible = $data['figure'] ?? null;
|
|
$this->ean = $data['ean'] ?? null;
|
|
$this->weight = $data['weight'] ?? null;
|
|
|
|
$this->meta_title = $data['meta_title'] ?? '';
|
|
$this->meta_description = $data['meta_description'] ?? '';
|
|
$this->meta_keywords = $data['meta_keywords'] ?? '';
|
|
$this->matched_id_variation = getVal('matched_id_variation', $data, '');
|
|
|
|
$this->data = getVal('data', $data);
|
|
|
|
$this->width = $data['width'] ?? null;
|
|
$this->height = $data['height'] ?? null;
|
|
$this->depth = $data['depth'] ?? null;
|
|
$this->showInSearch = $data['show_in_search'] ?? 'Y';
|
|
$this->productCode = $data['productCode'] ?? '';
|
|
$this->pieces_sold = $data['pieces_sold'] ?? null;
|
|
$this->position = $data['position'] ?? null;
|
|
|
|
if (findModule(Modules::PRICELISTS)) {
|
|
if (array_key_exists('pricelist_discount', $data)) {
|
|
$this->pricelistDiscount = toDecimal($data['pricelist_discount']);
|
|
$pricelistContext = ServiceContainer::getService(\KupShop\PricelistBundle\Context\PricelistContext::class);
|
|
$this->pricelistId = $pricelistContext->getActiveId();
|
|
|
|
$extend = $pricelistContext->getActive()?->getUseProductDiscount();
|
|
if (($extend && $this->pricelistId && !$this->pricelistDiscount->isZero()) || (!$extend && $this->pricelistId)) {
|
|
$this->discount = $this->pricelistDiscount;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (array_key_exists('id_photo', $data)) {
|
|
$this->photoId = $data['id_photo'];
|
|
$this->photoDescr = $data['descr_photo'];
|
|
$this->photoDateUpdate = $data['id_photo_update'];
|
|
}
|
|
|
|
if (!empty($data['in_store_suppliers'])) {
|
|
$this->in_store_suppliers = $data['in_store_suppliers'];
|
|
}
|
|
|
|
if (findModule('products', 'note')) {
|
|
$this->note_ = $data['note_'] ?? '';
|
|
}
|
|
|
|
if (isset($data['price'])) {
|
|
$this->fetchPrices($data);
|
|
}
|
|
|
|
if (findModule(Modules::BONUS_PROGRAM) && isset($data['price'])) {
|
|
$this->bonus_points = (is_null($data['bonus_points']) ? null : toDecimal($data['bonus_points']));
|
|
}
|
|
|
|
if (findModule(Modules::PRODUCTS, Modules::SUB_UNITS)) {
|
|
$this->fetchUnit($data);
|
|
}
|
|
|
|
$this->id_cn = getVal('id_cn', $data);
|
|
$this->date_added = $data['date_added'] ?? '';
|
|
|
|
if (isset($data['in_store_min'])) {
|
|
$this->in_store_min = $data['in_store_min'];
|
|
}
|
|
|
|
if (isset($data['date_stock_in']) && $data['date_stock_in'] !== '0000-00-00 00:00:00') {
|
|
try {
|
|
$this->date_stock_in = new DateTime($data['date_stock_in']);
|
|
} catch (Exception $e) {
|
|
}
|
|
}
|
|
|
|
if (!empty($data['variationsIds'])) {
|
|
$this->variationsIds = array_filter(explode(',', $data['variationsIds']));
|
|
}
|
|
|
|
if (array_key_exists('has_variations', $data)) {
|
|
$this->has_variations = (bool) $data['has_variations'];
|
|
}
|
|
|
|
if (findModule(Modules::PRODUCTS_SERIAL_NUMBERS)) {
|
|
$this->serial_number_require = $data['serial_number_require'] ?? 'N';
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public function fetchProducer()
|
|
{
|
|
if ($this->producer) {
|
|
return $this->producer;
|
|
}
|
|
|
|
if (findModule(Modules::PRODUCERS)) {
|
|
$multiFetch = ServiceContainer::getService(MultiFetch::class);
|
|
$multiFetch->fetchProducers(new ProductCollection([$this->id => $this]));
|
|
}
|
|
|
|
return $this->producer;
|
|
}
|
|
|
|
public function fetchPrices($data)
|
|
{
|
|
$currencyContext = Contexts::get(CurrencyContext::class);
|
|
$priceConverter = ServiceContainer::getService(PriceConverter::class);
|
|
|
|
$this->productPrice = new ProductPrice(toDecimal($data['price']), $currencyContext->getDefault(), getVat($data['vat']), $data['discount']);
|
|
$this->productPrice->setSource($this);
|
|
|
|
$priceForDiscountCurrency = $currencyContext->getDefault();
|
|
|
|
if (findModule(Modules::PRICELISTS) && isset($data['pricelist_price'])) {
|
|
/** @var \KupShop\PricelistBundle\Entity\Pricelist $activePricelist */
|
|
$activePricelist = Contexts::get(PricelistContext::class)->getActive();
|
|
|
|
$extend = $activePricelist?->getUseProductDiscount();
|
|
if (($extend && $this->pricelistId && isset($data['pricelist_discount'])) || (!$extend && $this->pricelistId)) {
|
|
$data['discount'] = $data['pricelist_discount'];
|
|
}
|
|
|
|
if (!$data['pricelist_currency']) {
|
|
$data['pricelist_currency'] = $currencyContext->getDefault();
|
|
} else {
|
|
$data['pricelist_currency'] = $currencyContext->getALL()[$data['pricelist_currency']];
|
|
}
|
|
|
|
// správně by se mělo brát z ceníků - nyní se v případě různosti měn porovnavali například EUR s CZK
|
|
$data['priceMax'] = $data['pricelist_price_max'] ?? $data['pricelist_price'];
|
|
|
|
$originalPrice = $this->productPrice;
|
|
$this->productPrice = new \KupShop\PricelistBundle\Util\Price\PriceListPrice(toDecimal($data['pricelist_price']), $data['pricelist_currency'], getVat($data['vat']), $data['discount']);
|
|
$this->productPrice->setOriginalPrice($originalPrice);
|
|
$this->productPrice->setSource($this);
|
|
|
|
$coefficient = $activePricelist?->getCoefficient();
|
|
if ($coefficient && in_array($data['price_source'], ['p', 'pv'])) {
|
|
/* v pripade koeficientu na ceniku je aplikovan i na originalPrice */
|
|
$this->productPrice->applyCoefficient(toDecimal($coefficient));
|
|
if (!empty($data['price_for_discount'])) {
|
|
$data['price_for_discount'] *= $coefficient;
|
|
}
|
|
if (!empty($data['priceMax'])) {
|
|
$data['priceMax'] *= $coefficient;
|
|
}
|
|
}
|
|
|
|
$this->priceOriginal = PriceWrapper::wrap($this->productPrice->getOriginalPrice());
|
|
$this->priceRaw = $priceConverter->convert($this->productPrice->getCurrency(), $currencyContext->getDefault(), $this->productPrice->getValue());
|
|
|
|
// prerazim CPS od produktu CPS z ceniku, pokud ji mam dostupnou
|
|
if (!empty($data['pricelist_price_history']) && array_key_exists('pricelist_price_for_discount', $data)) {
|
|
$data['price_for_discount'] = $data['pricelist_price_for_discount'];
|
|
$priceForDiscountCurrency = $data['pricelist_currency'];
|
|
}
|
|
} else {
|
|
$priceOriginal = new ProductPrice(toDecimal($data['priceRaw']), $currencyContext->getDefault(), getVat($data['vat']));
|
|
$priceOriginal->setSource($this);
|
|
$this->priceOriginal = PriceWrapper::wrap($priceOriginal);
|
|
$this->priceRaw = $priceConverter->convert($this->productPrice->getCurrency(), $currencyContext->getDefault(), $data['priceRaw']);
|
|
}
|
|
|
|
$originalProductPrice = $this->productPrice;
|
|
|
|
$pricelevelContext = Contexts::get(PriceLevelContext::class);
|
|
if ($pricelevel = $pricelevelContext->getActive()) {
|
|
$this->productPrice = new PriceLevelPrice($this->productPrice);
|
|
$IDcat = isset($data['priceLevelSections']) ? explode(',', $data['priceLevelSections']) : null;
|
|
$pricelevelDiscount = $pricelevel->getDiscount($this->id, $IDcat, $this->idProducer, $this);
|
|
$this->productPrice->setPricelevelDiscount($pricelevelDiscount);
|
|
$this->productPrice->setSource($this);
|
|
}
|
|
|
|
if (!findModule(\Modules::PRODUCTS, \Modules::SUB_MODERN_PRICES)) {
|
|
// Legacy prices - preformatted, old "array" prices, ...
|
|
|
|
$IDcategory = $this['priceLevelSections'] ?? null;
|
|
$IDproducer = $this->idProducer ?? -1;
|
|
|
|
$this->price_array = formatCustomerPrice($priceConverter->convert($originalProductPrice->getCurrency(), $currencyContext->getDefault(), $originalProductPrice->getValue()), $originalProductPrice->getDiscount(), $originalProductPrice->getVat()->asFloat(), $this->id, $IDcategory, $IDproducer, null, $currencyContext->getDefault());
|
|
|
|
if (isset($data['priceMax'])) {
|
|
$this->priceMax = formatCustomerPrice($data['priceMax'], $originalProductPrice->getDiscount(), $originalProductPrice->getVat()->asFloat(), $this->id, $IDcategory, $IDproducer);
|
|
}
|
|
|
|
$this->priceCommon_array = formatPrice(applyCurrency($data['price_common']), 0);
|
|
|
|
$this->price = printPrice($this->price_array, ['printdealerdiscount' => true, 'currency' => $currencyContext->getActive()->getId()]);
|
|
|
|
$this->priceNoVat = printPrice($this->price_array, ['withVat' => false]);
|
|
|
|
if (array_key_exists('price_buy', $data)) {
|
|
$this->price_buy = formatPrice(applyCurrency($data['price_buy']), $originalProductPrice->getVat()->asFloat());
|
|
}
|
|
} else {
|
|
// TODO: Zabránit vzniku, šetřit čas
|
|
unset($this->priceOriginal);
|
|
|
|
if (isset($data['priceMax'])) {
|
|
$this->priceMax = new ProductPrice(toDecimal($data['priceMax']), $originalProductPrice->getCurrency(), $originalProductPrice->getVat(), $originalProductPrice->getDiscount());
|
|
if ($this->productPrice instanceof PriceLevelPrice) {
|
|
$this->priceMax = new PriceLevelPrice($this->priceMax);
|
|
$this->priceMax->setPricelevelDiscount($this->productPrice->getPricelevelDiscount());
|
|
}
|
|
$this->priceMax->setSource($this);
|
|
}
|
|
|
|
if (array_key_exists('price_buy', $data)) {
|
|
$this->price_buy = new ProductPrice(toDecimal($data['price_buy']), $currencyContext->getDefault(), getVat($data['vat']));
|
|
$this->price_buy->setSource($this);
|
|
}
|
|
|
|
// TODO: Jediný co zbyde "divný" je priceRaw. Zkusit najít použití a případně zabít. Pak už zbyde jen productPrice, jupí! :-)
|
|
}
|
|
|
|
if ($data['price_common'] > 0) {
|
|
$price_common = toDecimal($data['price_common'])->removeVat($this->productPrice->getVat());
|
|
$this->priceCommon = new ProductPrice($price_common, $currencyContext->getDefault(), $this->productPrice->getVat());
|
|
$this->priceCommon->setSource($this);
|
|
}
|
|
|
|
if (array_key_exists('price_for_discount', $data)) {
|
|
$price_for_discount = toDecimal($data['price_for_discount']);
|
|
$this->priceForDiscount = new ProductPrice($price_for_discount, $priceForDiscountCurrency, $this->productPrice->getVat());
|
|
$this->priceForDiscount->setSource($this);
|
|
}
|
|
|
|
$this->recalculatePricesVatFromPriceWithVat($this);
|
|
}
|
|
|
|
protected function recalculatePricesVatFromPriceWithVat(Product $product): void
|
|
{
|
|
if (!PriceUtil::isProductPricesVatFromTop()) {
|
|
return;
|
|
}
|
|
|
|
$fields = ['priceOriginal', 'productPrice', 'priceForDiscount'];
|
|
|
|
$originalVatId = $product->original_vat ?? $product->vat_id;
|
|
|
|
$originalVat = (float) getVat($originalVatId);
|
|
$vatContext = Contexts::get(VatContext::class);
|
|
if ($vatContext->getActive() == $vatContext::NO_VAT) {
|
|
$originalVat = $vatContext->getVat($originalVatId)['vat'];
|
|
}
|
|
|
|
foreach ($fields as $field) {
|
|
if (!isset($product->{$field})) {
|
|
continue;
|
|
}
|
|
|
|
// PriceLevelPrice se uvnitr pocita specialne :/
|
|
if ($product->{$field} instanceof PriceLevelPrice) {
|
|
$product->{$field}->setOriginalPrice(
|
|
PriceUtil::recalculatePriceVatFromPriceWithVat(
|
|
$product->{$field}->getOriginalPrice(),
|
|
$originalVat,
|
|
$product->vat_id
|
|
)
|
|
);
|
|
}
|
|
|
|
// prepocitam cenu tak, aby DPH bylo pocitano ze shora
|
|
$product->{$field} = PriceUtil::recalculatePriceVatFromPriceWithVat($product->{$field}, $originalVat, $product->vat_id);
|
|
}
|
|
|
|
$currencyContext = Contexts::get(CurrencyContext::class);
|
|
$priceConverter = ServiceContainer::getService(PriceConverter::class);
|
|
|
|
// do price_array dam product price, aby to tam bylo taky prepocitany
|
|
$this->price_array = PriceWrapper::wrap($product->getProductPrice());
|
|
// prepocitam price raw podle aktualni productPrice - kvuli kosiku
|
|
$priceRaw = $this->getProductPrice()->getValue();
|
|
// pokud se jedna o cenu pocitanou pres price level, tak si do price raw vytahnu cenu z puvodni ceny
|
|
// protoze v $value na PriceLevelPrice je ulozena cena, na kterou je aplikovana uz ta cenova hladina
|
|
// a takovou cenu nechceme, protoze to neni priceRaw
|
|
if ($this->getProductPrice() instanceof PriceLevelPrice) {
|
|
$priceRaw = $this->getProductPrice()->getOriginalPrice()->getValue();
|
|
}
|
|
|
|
$this->priceRaw = $priceConverter->convert($this->getProductPrice()->getCurrency(), $currencyContext->getDefault(), $priceRaw);
|
|
}
|
|
|
|
public function fetchCharges($data = false, bool $onlyVisible = true)
|
|
{
|
|
if (!findModule(Modules::PRODUCTS_CHARGES)) {
|
|
return [];
|
|
}
|
|
|
|
$util = ServiceContainer::getService(ChargesUtil::class);
|
|
|
|
if ($this->charges === null) {
|
|
$multiFetch = ServiceContainer::getService(MultiFetch::class);
|
|
$multiFetch->fetchProductsCharges(new ProductCollection([$this->id => $this]), $onlyVisible);
|
|
|
|
$this->charges = $util->applyPrices($this->charges, $this);
|
|
}
|
|
|
|
return $util->applyData($this->charges, $data, $this);
|
|
}
|
|
|
|
/**
|
|
* @param $products Product[] array of products
|
|
* @param $parameters int[] array of parameter IDs
|
|
*
|
|
* @deprecated Use KupShop\CatalogBundle\ProductList\MultiFetch service instead
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function fetchParametersMulti($products, $parameters = null)
|
|
{
|
|
$multiFetch = ServiceContainer::getService(\KupShop\CatalogBundle\ProductList\MultiFetch::class);
|
|
|
|
if (!$products instanceof \KupShop\CatalogBundle\ProductList\ProductCollection) {
|
|
// Add idd to keys
|
|
$products = array_combine(array_column($products, 'id'), $products);
|
|
$products = new \KupShop\CatalogBundle\ProductList\ProductCollection($products);
|
|
}
|
|
|
|
return $multiFetch->fetchParameters($products, $parameters);
|
|
}
|
|
|
|
public function fetchParameters($parameters = null)
|
|
{
|
|
if (is_array($this->param)) {
|
|
return $this->param;
|
|
}
|
|
|
|
if (findModule('products_parameters')) {
|
|
self::fetchParametersMulti([$this], $parameters);
|
|
} else {
|
|
$this->param = [];
|
|
}
|
|
|
|
return $this->param;
|
|
}
|
|
|
|
/**
|
|
* @param $products Product[]|ProductCollection
|
|
*/
|
|
public static function fetchSetsMulti($products, $showOutOfStock = null): void
|
|
{
|
|
if (isset($products[0]->sets)) {
|
|
return;
|
|
}
|
|
|
|
if (is_array($products)) {
|
|
$products = new ProductCollection($products); // legacy support for $products as array
|
|
}
|
|
|
|
$multiFetch = ServiceContainer::getService(\KupShop\CatalogBundle\ProductList\MultiFetch::class);
|
|
$multiFetch->fetchSets($products, $showOutOfStock);
|
|
}
|
|
|
|
/**
|
|
* @return array
|
|
*/
|
|
public function fetchGifts($showOutOfStock = null)
|
|
{
|
|
if (is_array($this->gifts)) {
|
|
return $this->gifts;
|
|
}
|
|
|
|
$this->gifts = [];
|
|
if (\findModule(\Modules::PRODUCT_GIFTS)) {
|
|
$multiFetch = ServiceContainer::getService(\KupShop\CatalogBundle\ProductList\MultiFetch::class);
|
|
$multiFetch->fetchGifts(new \KupShop\CatalogBundle\ProductList\ProductCollection([$this->id => $this]), $showOutOfStock);
|
|
}
|
|
|
|
return $this->gifts;
|
|
}
|
|
|
|
public function fetchLabels($visibility = null)
|
|
{
|
|
if ($this->labels) {
|
|
return $this->labels;
|
|
}
|
|
|
|
if (\findModule(\Modules::LABELS, Modules::SUB_PRODUCT_LABELS)) {
|
|
$multiFetch = ServiceContainer::getService(MultiFetch::class);
|
|
$multiFetch->fetchProductLabels(new ProductCollection([$this->id => $this]), $visibility);
|
|
} else {
|
|
$this->labels = [];
|
|
}
|
|
|
|
return $this->labels;
|
|
}
|
|
|
|
/**
|
|
* @return array(Product)
|
|
*/
|
|
public function fetchSets($showOutOfStock = null)
|
|
{
|
|
if (is_array($this->sets)) {
|
|
return $this->sets;
|
|
}
|
|
|
|
if (findModule(\Modules::PRODUCT_SETS)) {
|
|
self::fetchSetsMulti([$this->id => $this], $showOutOfStock);
|
|
} else {
|
|
$this->sets = [];
|
|
}
|
|
|
|
return $this->sets;
|
|
}
|
|
|
|
/**
|
|
* @return \KupShop\CatalogBundle\Entity\Section[]
|
|
*/
|
|
public function fetchSections()
|
|
{
|
|
if (is_array($this->sections)) {
|
|
return $this->sections;
|
|
}
|
|
|
|
$multiFetch = ServiceContainer::getService(\KupShop\CatalogBundle\ProductList\MultiFetch::class);
|
|
|
|
$multiFetch->fetchSections(new \KupShop\CatalogBundle\ProductList\ProductCollection([$this->id => $this]));
|
|
|
|
return $this->sections;
|
|
}
|
|
|
|
/**
|
|
* @param bool $fetchAll
|
|
*
|
|
* @return array(Product)
|
|
*/
|
|
public function fetchCollections($fetchAll = true)
|
|
{
|
|
if (is_array($this->collections)) {
|
|
return $this->collections;
|
|
}
|
|
|
|
$dbcfg = Settings::getDefault();
|
|
|
|
$this->collections = [];
|
|
|
|
if (findModule('products_collections')) {
|
|
$qb = sqlQueryBuilder()
|
|
->select('p.id, p.figure, p.title, p.code, MIN(COALESCE(pv.price, p.price)) as price, MAX(COALESCE(pv.price, p.price)) as priceMax,
|
|
p.price_common, p.vat, p.discount, p.producer, p.delivery_time, p.campaign, p.figure, p.in_store, p.price as priceRaw');
|
|
|
|
if (findModule(\Modules::BONUS_PROGRAM)) {
|
|
$qb->addSelect('COALESCE(pv.bonus_points, p.bonus_points) as bonus_points');
|
|
}
|
|
|
|
if (findModule(Modules::PRODUCTS, Modules::SUB_UNITS)) {
|
|
$qb->addSelect('p.unit, pu.short_name, pu.long_name as unit_long_name')
|
|
->leftJoin('p', 'products_units', 'pu', 'p.unit=pu.id');
|
|
if (findModule(Modules::PRODUCTS, Modules::SUB_UNITS_FLOAT)) {
|
|
$qb->addSelect('pu.pieces_precision');
|
|
}
|
|
}
|
|
|
|
if (findModule(Modules::RESTRICTIONS)) {
|
|
$restrictions = ServiceContainer::getService(Restrictions::class);
|
|
$qb->andWhere($restrictions->getRestrictionSpec());
|
|
}
|
|
|
|
$qbOthers = clone $qb;
|
|
|
|
$qb->addSelect('ps.id_product = :id_product as own')
|
|
->from('products_collections', 'ps')
|
|
->leftJoin('ps', 'products', 'p', 'p.id = IF(ps.id_product = :id_product, ps.id_product_related, ps.id_product)')
|
|
->leftJoin('p', 'products_variations', 'pv', 'p.id=pv.id_product')
|
|
->andWhere('(ps.id_product=:id_product OR ps.id_product_related=:id_product)')
|
|
->groupBy('p.id')
|
|
->setParameter('id_product', $this->id);
|
|
|
|
$own = [];
|
|
$others = [];
|
|
foreach ($qb->execute() as $row) {
|
|
$product = new Product();
|
|
$product->createFromArray($row);
|
|
|
|
if ($row['own']) {
|
|
if ($dbcfg->prod_show_not_in_store == 'N' && !isAdministration() && $row['in_store'] <= 0) {
|
|
continue;
|
|
} else {
|
|
$own[$product->id] = $product;
|
|
}
|
|
} else {
|
|
$others[$product->id] = $product;
|
|
}
|
|
}
|
|
|
|
if ($others && $fetchAll) {
|
|
$qbOthers->addSelect('ps.id_product');
|
|
|
|
if ($dbcfg->prod_show_not_in_store == 'N' && !isAdministration()) {
|
|
$qbOthers->andWhere('p.in_store > 0');
|
|
}
|
|
|
|
$qbOthers->from('products_collections', 'ps')
|
|
->leftJoin('ps', 'products', 'p', 'p.id = ps.id_product_related')
|
|
->leftJoin('p', 'products_variations', 'pv', 'p.id=pv.id_product')
|
|
->andWhere(Op::andX(Op::inIntArray(array_keys($others), 'ps.id_product'),
|
|
Op::not(Op::equals(['p.id' => $this->id]))))
|
|
->groupBy('p.id')
|
|
->orderBy('ps.id_product');
|
|
|
|
foreach ($others as &$other_prod) {
|
|
if ($dbcfg->prod_show_not_in_store == 'N' && !isAdministration() && $other_prod->inStore <= 0) {
|
|
$other_prod->visible = 'N';
|
|
}
|
|
}
|
|
|
|
foreach ($qbOthers->execute() as $other) {
|
|
$product = new Product();
|
|
$product->createFromArray($other);
|
|
|
|
$others[$other['id_product']]->collections[] = $product;
|
|
}
|
|
}
|
|
|
|
$this->collections = ['own' => $own, 'others' => $others];
|
|
}
|
|
|
|
return $this->collections;
|
|
}
|
|
|
|
public function fetchProductsRelated()
|
|
{
|
|
if (is_array($this->products_related)) {
|
|
return $this->products_related;
|
|
}
|
|
|
|
$this->products_related = [];
|
|
|
|
if (findModule('products_related')) {
|
|
$SQL = sqlQueryBuilder()
|
|
->select('p.title')->fromProducts()
|
|
->addSelect(RelatedProducts::relatedProductsSpec([$this->id]));
|
|
|
|
if (findModule(Modules::DYNAMIC_RELATED_PRODUCTS)) {
|
|
$SQL->andWhere(Op::isNull('pr.id_products_related_dynamic'));
|
|
}
|
|
|
|
foreach ($SQL->execute() as $row) {
|
|
$this->products_related[] = $row;
|
|
}
|
|
}
|
|
|
|
return $this->products_related;
|
|
}
|
|
|
|
public function fetchRating(): ?array
|
|
{
|
|
if ($this->rating !== null) {
|
|
return $this->rating;
|
|
}
|
|
|
|
$this->rating = [];
|
|
|
|
if (findModule(Modules::REVIEWS)) {
|
|
$multiFetch = ServiceContainer::getService(MultiFetch::class);
|
|
$multiFetch->fetchRating(new ProductCollection([$this->id => $this]));
|
|
}
|
|
|
|
return $this->rating;
|
|
}
|
|
|
|
// backward compatibility
|
|
public function fetchReviews()
|
|
{
|
|
return $this->reviews ??= $this->fetchRating();
|
|
}
|
|
|
|
public function isVisible()
|
|
{
|
|
return $this->visible != 'N';
|
|
}
|
|
|
|
public function isOld()
|
|
{
|
|
return $this->visible == 'O';
|
|
}
|
|
|
|
public function canBuy(): bool
|
|
{
|
|
if (isset($this->canBuy)) {
|
|
return $this->canBuy;
|
|
}
|
|
|
|
if (empty($this->deliveryTimeText)) {
|
|
$this->prepareDeliveryText();
|
|
}
|
|
|
|
return ProductAvailabilityUtil::canBuy($this->deliveryTime, $this->isOld(), $this->canWatch, $this->canBuy);
|
|
}
|
|
|
|
public function canWatch(): ?bool
|
|
{
|
|
if (!findModule(Modules::WATCHDOG)) {
|
|
return null;
|
|
}
|
|
|
|
if (!isset($this->canWatch)) {
|
|
$this->canBuy();
|
|
}
|
|
|
|
return $this->canWatch ?? false;
|
|
}
|
|
|
|
public function hasVariations()
|
|
{
|
|
return $this->has_variations ??= returnSQLResult('SELECT COUNT(*) FROM '.getTableName('products_variations')." pv WHERE pv.id_product={$this->id}") > 0;
|
|
}
|
|
|
|
public function findVariation($id)
|
|
{
|
|
if (!empty($this->variations['variations'][$id])) {
|
|
return $this->variations['variations'][$id];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public function fetchVariations($in_store_only = false)
|
|
{
|
|
if (!is_null($this->variations)) {
|
|
return $this->variations;
|
|
}
|
|
|
|
$this->variations = [];
|
|
|
|
if (findModule('products_variations')) {
|
|
// nacist varianty k produktu
|
|
$this->variations['variations'] = Variations::getProductVariations(intval($this->id), $in_store_only);
|
|
|
|
// docist jmenovky, pokud jsou varianty
|
|
if (count($this->variations['variations']) > 0) {
|
|
$this->variations['labels'] = Variations::getProductLabels(intval($this->id));
|
|
}
|
|
}
|
|
|
|
return $this->variations;
|
|
}
|
|
|
|
/**
|
|
* @param $products Product[]
|
|
*/
|
|
public static function fetchVariationsMulti($products, $labels)
|
|
{
|
|
$multiFetch = ServiceContainer::getService(\KupShop\CatalogBundle\ProductList\MultiFetch::class);
|
|
|
|
if (!$products instanceof \KupShop\CatalogBundle\ProductList\ProductCollection) {
|
|
$products = new \KupShop\CatalogBundle\ProductList\ProductCollection($products);
|
|
}
|
|
|
|
$multiFetch->fetchVariations($products, $labels);
|
|
}
|
|
|
|
public function fetchLinks()
|
|
{
|
|
$SQL = sqlQuery('SELECT *
|
|
FROM '.getTableName('links')."
|
|
WHERE id_product='".$this->id."' ");
|
|
|
|
foreach ($SQL as $key => $link) {
|
|
if (empty($link['title'])) {
|
|
$link['title'] = '['.($key + 1).']';
|
|
}
|
|
|
|
$this->links[] = $link;
|
|
}
|
|
|
|
sqlFreeResult($SQL);
|
|
}
|
|
|
|
public function fetchAttachments()
|
|
{
|
|
$SQL = sqlQueryBuilder()->select('*')
|
|
->from('attachments')
|
|
->where(Op::equals(['id_product' => $this->id]))
|
|
->orderBy('position', 'ASC')
|
|
->execute();
|
|
|
|
foreach ($SQL as $attachment) {
|
|
$link = $attachment['link'];
|
|
$attachment['remote'] = preg_match('@^([a-z]{2,4}://)@i', $link);
|
|
|
|
if (!$attachment['remote'] && isset($link[0]) && $link[0] == '/') {
|
|
$attachment['size'] = @filesize('.'.urldecode($link));
|
|
}
|
|
|
|
$this->attachments[] = $attachment;
|
|
}
|
|
|
|
sqlFreeResult($SQL);
|
|
}
|
|
|
|
public function fetchImages($mainSize, $additionalSize = null)
|
|
{
|
|
if ($mainSize) {
|
|
if ($this->photoId != -1) {
|
|
$this->image = getImage($this->photoId, null, null, $mainSize, $this->photoDescr, strtotime($this->photoDateUpdate ?? ''));
|
|
} else {
|
|
$this->image = leadImage($this->id, $mainSize);
|
|
}
|
|
}
|
|
|
|
if ($additionalSize) {
|
|
$qb = sqlQueryBuilder()
|
|
->select('ph.id, ph.descr, ph.source, ph.image_2, ppr.show_in_lead, ph.date_update')
|
|
->from('photos_products_relation', 'ppr')
|
|
->leftJoin('ppr', 'photos', 'ph', 'ppr.id_photo = ph.id')
|
|
->where(Translation::coalesceTranslatedFields(PhotosTranslation::class))
|
|
->andWhere('ppr.id_product=:id_product AND ppr.active="Y"')
|
|
->groupBy('ph.id')
|
|
->setParameter('id_product', $this->id)
|
|
->orderBy('position', 'ASC');
|
|
|
|
if (findModule(Modules::PRODUCTS_VARIATIONS_PHOTOS)) {
|
|
$qb->having('MAX(ppr.show_in_lead) != \'Y\'');
|
|
} else {
|
|
$qb->andWhere('ppr.show_in_lead != "Y"');
|
|
}
|
|
|
|
if (findModule(Modules::VIDEOS)) {
|
|
$qb->addSelect('id_cdn id_video')
|
|
->leftJoin('ph', 'videos', 'v', 'v.id_photo = ph.id');
|
|
}
|
|
|
|
$SQL = $qb->execute();
|
|
|
|
while (($image = sqlFetchAssoc($SQL)) !== false) {
|
|
// skip main image, that is attached to an variation
|
|
if ($image['id'] == ($this->image['id'] ?? null)) {
|
|
continue;
|
|
}
|
|
|
|
if (empty($image['image_2'])) {
|
|
continue;
|
|
}
|
|
|
|
$this->photos[] = array_merge(
|
|
getImage($image['id'], $image['image_2'], $image['source'], $additionalSize, $image['descr'], strtotime($image['date_update'])),
|
|
['show_in_lead' => $image['show_in_lead'], 'id_video' => $image['id_video'] ?? null]
|
|
);
|
|
}
|
|
|
|
sqlFreeResult($SQL);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sanitize and fill-in texts.
|
|
*/
|
|
public function prepareTexts()
|
|
{
|
|
// Sanitize text
|
|
$this->title = htmlspecialchars($this->title);
|
|
}
|
|
|
|
public function prepareDeliveryText()
|
|
{
|
|
$cfg = Config::get();
|
|
|
|
$inStore = $this->inStore;
|
|
|
|
$this->deliveryTimeRaw = $this->deliveryTime;
|
|
$this->deliveryTimeText = getProductDeliveryText($inStore, $this->deliveryTime);
|
|
|
|
if ($inStore <= 0) {
|
|
$inStoreSuppliers = $this->getInStoreSuppliers();
|
|
if (findModule(\Modules::PRODUCTS_SUPPLIERS, \Modules::SUB_ALLOW_NEGATIVE_IN_STORE)) {
|
|
$inStoreSuppliers += $this->inStore;
|
|
}
|
|
|
|
if ($inStoreSuppliers > 0) {
|
|
if (findModule(\Modules::PRODUCTS_SUPPLIERS) && findModule(\Modules::PRODUCTS_SUPPLIERS, \Modules::SUB_DELIVERY_TIME)) {
|
|
$this->deliveryTime = $cfg['Modules']['products_suppliers']['delivery_time'];
|
|
$languageContext = \KupShop\KupShopBundle\Util\Contexts::get(\KupShop\KupShopBundle\Context\LanguageContext::class);
|
|
|
|
if (!$languageContext->translationActive()) {
|
|
$this->deliveryTimeText = sprintf($cfg['Products']['DeliveryTime'][$cfg['Modules']['products_suppliers']['delivery_time']], strval($inStoreSuppliers));
|
|
} else {
|
|
$this->deliveryTimeText = sprintf(translate_shop($this->deliveryTime, 'deliveryTime'), strval($inStoreSuppliers));
|
|
}
|
|
}
|
|
}
|
|
$this->inStore = $inStore;
|
|
$this->in_store_suppliers = $inStoreSuppliers;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param Decimal $pieces
|
|
*
|
|
* @return Decimal
|
|
*/
|
|
public function getPrice($variationId, $data, $pieces)
|
|
{
|
|
$price = toDecimal($this->priceRaw);
|
|
|
|
if (!is_null($variationId)) {
|
|
$price = Variations::getCustomPrice($variationId, $price);
|
|
}
|
|
|
|
$dispatcher = ServiceContainer::getService('event_dispatcher');
|
|
$event = $dispatcher->dispatch(new OrderItemEvent($this, $variationId, $price, $pieces, $data), OrderItemEvent::CALCULATE_PRICE);
|
|
$price = $event->getPrice();
|
|
|
|
if (findModule(\Modules::PRODUCTS_PARAMETERS, \Modules::SUB_CONFIGURATIONS) && !empty($data['configurations'])) {
|
|
$this->fetchParameters();
|
|
|
|
$config = $data['configurations'];
|
|
foreach ($this->configurations as $id_parameter => $parameter) {
|
|
$values = $parameter->fetchValues(false);
|
|
if (!empty($config[$id_parameter]) && array_key_exists($config[$id_parameter], $values)) {
|
|
$configurationPriceWithVat = $values[$config[$id_parameter]]->configuration_price;
|
|
$price = $price->add($configurationPriceWithVat->removeVat($this->vat)->removeDiscount($this->discount));
|
|
}
|
|
}
|
|
}
|
|
|
|
return $price;
|
|
}
|
|
|
|
public function printNote($data)
|
|
{
|
|
$ret = '';
|
|
|
|
if (findModule(\Modules::PRODUCTS_PARAMETERS, \Modules::SUB_CONFIGURATIONS) && !empty($data['configurations'])) {
|
|
$this->fetchParameters();
|
|
|
|
$parts = [];
|
|
$config = $data['configurations'];
|
|
foreach ($this->configurations as $id_parameter => $parameter) {
|
|
$values = $parameter->fetchValues();
|
|
if (!empty($config[$id_parameter]) && array_key_exists($config[$id_parameter], $values)) {
|
|
$value = $values[$config[$id_parameter]];
|
|
$parts[] = "{$parameter->name}: {$value->value}";
|
|
} elseif ($parameter['value_type'] == 'char' && !empty($config[$id_parameter])) {
|
|
$parts[] = "{$parameter->name}: {$config[$id_parameter]}";
|
|
}
|
|
}
|
|
|
|
if ($parts) {
|
|
$ret .= join(', ', $parts);
|
|
}
|
|
}
|
|
|
|
return $ret;
|
|
}
|
|
|
|
public function parseNote($data): array
|
|
{
|
|
if (!$data) {
|
|
return [];
|
|
}
|
|
|
|
$data = json_decode($data, true);
|
|
|
|
return is_array($data) ? $data : [];
|
|
}
|
|
|
|
public function dumpNote($data)
|
|
{
|
|
return json_encode($data);
|
|
}
|
|
|
|
public function storeIn($variationId, $count)
|
|
{
|
|
$posContext = Contexts::get(PosContext::class);
|
|
if (!$posContext->getActiveId() && findModule('orders', \Modules::SUB_STORE_IN_DISABLE) && $count >= 0) {
|
|
return;
|
|
}
|
|
|
|
if (!empty($variationId)) {
|
|
$table = getTableName('products_variations');
|
|
$id = $variationId;
|
|
} else {
|
|
$table = getTableName('products');
|
|
$id = $this->id;
|
|
}
|
|
|
|
$query = "SELECT in_store FROM {$table}
|
|
WHERE id={$id} FOR UPDATE";
|
|
if (($inStoreOrigin = returnSQLResult($query)) !== false) {
|
|
$inStoreNew = $inStoreOrigin + $count;
|
|
|
|
if ($count < 0 && $inStoreNew < 0) {
|
|
$reserved = max($inStoreOrigin, 0);
|
|
} else {
|
|
$reserved = -$count;
|
|
}
|
|
|
|
sqlQuery("UPDATE {$table}
|
|
SET in_store={$inStoreNew}
|
|
WHERE id={$id}");
|
|
} else {
|
|
return logError(__FILE__, __LINE__, "Variation id {$variationId} does not exists for product id {$this->id}!");
|
|
}
|
|
|
|
// React to store depletion
|
|
if ($inStoreNew == 0 && $inStoreOrigin > 0) {
|
|
$dbcfg = Settings::getDefault();
|
|
|
|
if (!empty($variationId)) {
|
|
if ($dbcfg->prod_do_after_order == 'delete') {
|
|
sqlQuery('DELETE FROM '.getTableName('products_variations')."
|
|
WHERE id={$variationId}");
|
|
}
|
|
if ($dbcfg->prod_do_after_order == 'status') {
|
|
sqlQuery('UPDATE '.getTableName('products_variations')."
|
|
SET delivery_time='-2'
|
|
WHERE id={$variationId}");
|
|
}
|
|
if ($dbcfg->prod_do_after_order == 'hide') {
|
|
$this->storeInHideAction($variationId);
|
|
}
|
|
} else {
|
|
if ($dbcfg->prod_do_after_order == 'delete') {
|
|
$this->delete();
|
|
}
|
|
if ($dbcfg->prod_do_after_order == 'status') {
|
|
sqlQuery('UPDATE '.getTableName('products')."
|
|
SET delivery_time='-2'
|
|
WHERE id={$this->id}");
|
|
}
|
|
if ($dbcfg->prod_do_after_order == 'hide') {
|
|
sqlQuery('UPDATE '.getTableName('products')."
|
|
SET figure='O'
|
|
WHERE id={$this->id}");
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->updateInStore();
|
|
|
|
return $reserved;
|
|
}
|
|
|
|
public function storeInHideAction($variationId)
|
|
{
|
|
sqlQuery('UPDATE '.getTableName('products_variations')."
|
|
SET figure='N'
|
|
WHERE id={$variationId}");
|
|
|
|
$this->updateInStore();
|
|
|
|
// hide also whole project if whole product is not available
|
|
if ($this->inStore <= 0) {
|
|
sqlQuery('UPDATE '.getTableName('products')."
|
|
SET figure='O'
|
|
WHERE id={$this->id}");
|
|
}
|
|
}
|
|
|
|
public function sell($variationId, $count)
|
|
{
|
|
$reserved = null;
|
|
|
|
sqlGetConnection()->transactional(function () use ($variationId, $count, &$reserved) {
|
|
// Update pieces_sold in all cases
|
|
sqlQuery('UPDATE products
|
|
SET pieces_sold=pieces_sold+:count
|
|
WHERE id=:id',
|
|
['id' => $this->id, 'count' => $count]
|
|
);
|
|
|
|
// Check if store management is enabled
|
|
if (Settings::getDefault()->prod_subtract_from_store != 'Y') {
|
|
return;
|
|
}
|
|
|
|
// Subtract items from store
|
|
$reserved = $this->storeIn($variationId, -$count);
|
|
});
|
|
|
|
return $reserved;
|
|
}
|
|
|
|
public function updateInStore()
|
|
{
|
|
sqlQuery("UPDATE products p
|
|
SET p.in_store=(
|
|
SELECT COALESCE(SUM(GREATEST(pv.in_store, 0)), p.in_store)
|
|
FROM products_variations pv
|
|
WHERE pv.id_product=p.id
|
|
)
|
|
WHERE p.id={$this->id}");
|
|
|
|
$this->inStore = returnSQLResult("SELECT p.in_store
|
|
FROM products p
|
|
WHERE p.id={$this->id}");
|
|
}
|
|
|
|
public function updateDeliveryTime()
|
|
{
|
|
global $cfg;
|
|
|
|
$delivery_times = sqlQueryBuilder()->select('delivery_time')->from('products_variations')
|
|
->where(
|
|
Op::equals([
|
|
'id_product' => $this->id,
|
|
'figure' => 'Y',
|
|
]))
|
|
->execute()->fetchAll();
|
|
|
|
if (empty($delivery_times)) {
|
|
return true;
|
|
}
|
|
|
|
$minDelay = null;
|
|
$bestID = null;
|
|
foreach ($delivery_times as $variation) {
|
|
$index = $variation['delivery_time'];
|
|
switch ($index) {
|
|
case -2:
|
|
$delay = 99;
|
|
break;
|
|
case -1:
|
|
$delay = 2.5;
|
|
break;
|
|
default:
|
|
$delay = abs($index);
|
|
break;
|
|
}
|
|
$delay = $cfg['Products']['DeliveryTimeDays'][$variation['delivery_time']] ?? $delay;
|
|
if ($minDelay === null || $delay < $minDelay) {
|
|
$minDelay = $delay;
|
|
$bestID = $index;
|
|
}
|
|
}
|
|
|
|
$this->deliveryTime = $bestID;
|
|
|
|
return sqlAffectedRows(sqlQuery('UPDATE '.getTableName('products')." p
|
|
SET p.delivery_time='{$this->deliveryTime}'
|
|
WHERE p.id={$this->id}"));
|
|
}
|
|
|
|
// Delete product or variation based on $variation_id. If $variation_id is null, delete product. If it is last variation, delete also product.
|
|
public function deleteVariation($variation_id = null)
|
|
{
|
|
$ret = 0;
|
|
$variationsCount = returnSqlResult("SELECT COUNT(*)
|
|
FROM products_variations pv
|
|
WHERE pv.id_product={$this->id}");
|
|
|
|
if ($variationsCount > 0 && $variation_id == null) {
|
|
return $ret;
|
|
}
|
|
|
|
if ($variation_id) {
|
|
sqlQuery("UPDATE products_variations SET figure='N' WHERE id={$variation_id}");
|
|
$ret++;
|
|
|
|
$emptyProduct = returnSqlResult("SELECT COUNT(*) > 0
|
|
FROM products_variations pv
|
|
WHERE pv.id_product={$this->id} AND pv.figure = 'Y' ");
|
|
|
|
if ($emptyProduct) {
|
|
return $ret;
|
|
}
|
|
}
|
|
|
|
sqlQuery("UPDATE products SET figure='O' WHERE id={$this->id}");
|
|
|
|
$ret++;
|
|
|
|
return $ret;
|
|
}
|
|
|
|
public function delete()
|
|
{
|
|
$img = new Photos('product');
|
|
|
|
$SQL = sqlQuery('SELECT ph.id
|
|
FROM '.getTableName('photos').' AS ph
|
|
LEFT JOIN '.getTableName('photos-products')." AS php ON ph.id=php.id_photo
|
|
WHERE php.id_product={$this->id} ");
|
|
|
|
foreach ($SQL as $row) {
|
|
$IDph = $row['id'];
|
|
$samePhotoCount = returnSQLResult('SELECT COUNT(*) FROM '.getTableName('photos-products')." php WHERE php.id_photo={$IDph}");
|
|
if ($samePhotoCount <= 1) {
|
|
$img->erasePhoto($IDph);
|
|
}
|
|
}
|
|
|
|
// smazat sekce
|
|
sqlQuery('DELETE FROM '.getTableName('products-sections')." WHERE id_product={$this->id}", '@');
|
|
// smazat odkazy
|
|
sqlQuery('DELETE FROM '.getTableName('links')." WHERE id_product={$this->id}", '@');
|
|
// smazat prilohy
|
|
sqlQuery('DELETE FROM '.getTableName('attachments')." WHERE id_product={$this->id}", '@');
|
|
// smazat souvisejici zbozi
|
|
sqlQuery('DELETE FROM '.getTableName('products-related')." WHERE id_top_product={$this->id}", '@');
|
|
// smazat zbozi
|
|
sqlQuery('DELETE FROM '.getTableName('products')." WHERE id={$this->id}", '@');
|
|
}
|
|
|
|
public function checkCodeUnique()
|
|
{
|
|
if (returnSQLResult('SELECT COUNT(*) FROM '.getTableName('products')." p WHERE p.code LIKE '{$this->code}' AND p.id!='{$this->id}'") == 0) {
|
|
return true;
|
|
}
|
|
|
|
// Strip "(1)"
|
|
$code = preg_replace('/ \([0-9]+\)$/', '', $this->code);
|
|
|
|
$index = 1;
|
|
while (returnSQLResult('SELECT COUNT(*) FROM '.getTableName('products')." p WHERE p.code LIKE '{$code} ({$index})' AND p.id!='{$this->id}'") > 0) {
|
|
$index++;
|
|
}
|
|
|
|
$this->code = "{$code} ({$index})";
|
|
|
|
return false;
|
|
}
|
|
|
|
public function getInStoreOffset($variation_id = null)
|
|
{
|
|
$query = 'SELECT SUM(oi.pieces)
|
|
FROM '.getTableName('order_items').' oi
|
|
LEFT JOIN '.getTableName('orders').' o ON oi.id_order=o.id,
|
|
'.getTableName('orders')." o2
|
|
WHERE o2.id={$_GET['IDo']} AND o.date_created >= o2.date_created AND oi.id_product={$this->id}
|
|
AND o.status_storno=0 AND o.status IN (".join(',', getStatuses('active')).')';
|
|
|
|
if (!empty($variation_id)) {
|
|
$query .= "AND oi.id_variation={$variation_id}";
|
|
}
|
|
|
|
return intval(returnSQLResult($query));
|
|
}
|
|
|
|
public function getInStoreSuppliers($id_variation = null)
|
|
{
|
|
if ((!findModule(\Modules::PRODUCTS_SUPPLIERS) && !findModule(\Modules::SUPPLIERS)) || !SuppliersUtil::isSuppliersStoreEnabled()) {
|
|
return 0;
|
|
}
|
|
|
|
if (isset($this->in_store_suppliers) && ($id_variation == $this->variationId)) {
|
|
return $this->in_store_suppliers;
|
|
}
|
|
|
|
$old = $this->in_store_suppliers ?? null;
|
|
$this->in_store_suppliers = null;
|
|
|
|
$multiFetch = ServiceContainer::getService(MultiFetch::class);
|
|
$productListId = $this->id;
|
|
if ($id_variation) {
|
|
$productListId .= '/'.$id_variation;
|
|
}
|
|
$productCollection = new ProductCollection([$productListId => $this]);
|
|
if ($id_variation) {
|
|
$productCollection->setEntityType(FilterParams::ENTITY_VARIATION);
|
|
}
|
|
$multiFetch->fetchProductOfSuppliersInStore($productCollection);
|
|
|
|
$result = $this->in_store_suppliers;
|
|
|
|
if ($id_variation != $this->variationId) {
|
|
$this->in_store_suppliers = $old;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
public function getSuppliers($id_variation = null)
|
|
{
|
|
if (!findModule(\Modules::PRODUCTS_SUPPLIERS) && !findModule(\Modules::SUPPLIERS)) {
|
|
return [];
|
|
}
|
|
|
|
$query = 'SELECT pos.id_variation, pos.in_store, pos.id_supplier
|
|
FROM '.getTableName('products_of_suppliers').' pos
|
|
WHERE pos.id_product=:id';
|
|
|
|
if (!empty($id_variation)) {
|
|
$query .= ' AND pos.id_variation=:id_variation';
|
|
}
|
|
|
|
$SQL = sqlQuery($query, ['id' => $this->id, 'id_variation' => $id_variation]);
|
|
|
|
$suppliers = [];
|
|
foreach ($SQL as $row) {
|
|
$variation = $row['id_variation'] ?: '0';
|
|
$suppliers[$variation][$row['id_supplier']] = $row['in_store'];
|
|
}
|
|
|
|
return $suppliers;
|
|
}
|
|
|
|
public static function isValid($productId, $variationId)
|
|
{
|
|
$where = selectQueryCreate(['p.id' => $productId, 'pv.id' => $variationId], true);
|
|
$count = returnSQLResult('SELECT COUNT(*)
|
|
FROM '.getTableName('products').' p
|
|
LEFT JOIN '.getTableName('products_variations')." pv ON pv.id_product = p.id
|
|
WHERE {$where}");
|
|
|
|
return $count == 1;
|
|
}
|
|
|
|
public static function automaticUpdateFlagForNewProducts($id_product = false)
|
|
{
|
|
$cfg = Config::get();
|
|
$dbcfg = Settings::getDefault();
|
|
|
|
if (!empty($dbcfg['prod_flag_automatic_new']) && !empty($cfg['Products']['Flags']['N'])) {
|
|
$field = empty($dbcfg['prod_flag_automatic_new_field']) ? 'date_added' : $dbcfg['prod_flag_automatic_new_field'];
|
|
|
|
$where = ($id_product) ? ' id=:id AND' : '';
|
|
$where .= " NOT FIND_IN_SET('MN', campaign) AND";
|
|
|
|
sqlQuery("UPDATE products SET campaign = ADD_TO_SET('N', campaign) WHERE {$where} NOT FIND_IN_SET('N', campaign) AND DATE_ADD({$field}, INTERVAL :days DAY) > CURDATE()", ['id' => $id_product, 'days' => $dbcfg['prod_flag_automatic_new']]);
|
|
|
|
sqlQuery("UPDATE products SET campaign = REMOVE_FROM_SET('N', campaign) WHERE {$where} FIND_IN_SET('N', campaign) AND DATE_ADD({$field}, INTERVAL :days DAY) < CURDATE() ", ['id' => $id_product, 'days' => $dbcfg['prod_flag_automatic_new']]);
|
|
}
|
|
}
|
|
|
|
public static function getName($id)
|
|
{
|
|
return returnSQLResult('SELECT title FROM products WHERE id=:id', ['id' => $id]);
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function getTableFields()
|
|
{
|
|
$dbCfg = Settings::getDefault();
|
|
|
|
$fields = "p.id, pv.id as variation_id, p.title, p.code, p.ean, p.short_descr, p.long_descr, p.parameters,
|
|
MIN(COALESCE(pv.price, p.price)) as price, MAX(COALESCE(pv.price, p.price)) as priceMax, p.price as priceRaw, p.price_common, p.discount,
|
|
p.producer, p.guarantee, p.delivery_time, DATE_FORMAT(p.updated, '{$dbCfg->date_format}') AS d_update, (pv.id IS NOT NULL) as has_variations,
|
|
p.campaign, p.figure, p.data, p.width, p.height, p.depth, p.code as productCode, COALESCE(pv.date_added, p.date_added) as date_added, GROUP_CONCAT(pv.id) as variationsIds, p.show_in_search";
|
|
|
|
if (findModule(\Modules::BONUS_PROGRAM)) {
|
|
$fields .= ', p.bonus_points';
|
|
}
|
|
|
|
if (findModule(Modules::PRODUCTS, Modules::SUB_DESCR_PLUS)) {
|
|
$fields .= ',p.id_block';
|
|
}
|
|
|
|
if (findModule('products', 'note')) {
|
|
$fields .= ', COALESCE(pv.note, p.note) note_';
|
|
}
|
|
|
|
if (findModule(Modules::STOCK_IN)) {
|
|
$fields .= ', p.date_stock_in';
|
|
}
|
|
|
|
if (findModule(Modules::PRODUCTS_SERIAL_NUMBERS)) {
|
|
$fields .= ', p.serial_number_require';
|
|
}
|
|
|
|
return $fields;
|
|
}
|
|
|
|
/* dalsi field_ methody nepridavat do class Product, ale do ProductWrapper */
|
|
|
|
public function field_productPrice()
|
|
{
|
|
return PriceWrapper::wrap($this->productPrice);
|
|
}
|
|
|
|
public function field_price_array()
|
|
{
|
|
if (empty($this->price_array)) { // SUB_MODERN_PRICES
|
|
return PriceWrapper::wrap($this->productPrice);
|
|
}
|
|
|
|
return $this->price_array;
|
|
}
|
|
|
|
public function field_price_buy()
|
|
{
|
|
if ($this->price_buy instanceof ProductPrice) {
|
|
return PriceWrapper::wrap($this->price_buy);
|
|
}
|
|
|
|
return $this->price_buy;
|
|
}
|
|
|
|
public function field_priceCommon()
|
|
{
|
|
if ($this->priceCommon instanceof ProductPrice) {
|
|
return PriceWrapper::wrap($this->priceCommon);
|
|
}
|
|
|
|
return $this->priceCommon;
|
|
}
|
|
|
|
public function field_priceMax()
|
|
{
|
|
if ($this->priceMax instanceof ProductPrice) {
|
|
return PriceWrapper::wrap($this->priceMax);
|
|
}
|
|
|
|
return $this->priceMax;
|
|
}
|
|
|
|
public function field_originalPrice()
|
|
{
|
|
return PriceWrapper::wrap($this->productPrice->getOriginalPrice());
|
|
}
|
|
|
|
public function field_bonus_points()
|
|
{
|
|
return $this->getBonusPoints($this->variationId);
|
|
}
|
|
|
|
public function field_producerTitle()
|
|
{
|
|
return $this->producer['name'] ?? null;
|
|
}
|
|
|
|
public function field_producerImage()
|
|
{
|
|
return $this->producer['photo'] ?? null;
|
|
}
|
|
|
|
public function field_weight()
|
|
{
|
|
if (!empty($this->weight)) {
|
|
return $this->weight;
|
|
}
|
|
|
|
if (findModule(\Modules::PRODUCTS, \Modules::SUB_WEIGHT)) {
|
|
$this->weight = 0.;
|
|
foreach ($this->fetchSets() as $set_product) {
|
|
$this->weight += ($set_product->weight ?? 0) * $set_product->set_pieces;
|
|
}
|
|
}
|
|
|
|
return $this->weight;
|
|
}
|
|
|
|
/* dalsi field_ methody nepridavat do class Product, ale do ProductWrapper */
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function offsetMethod($offset)
|
|
{
|
|
return 'field_'.$offset;
|
|
}
|
|
|
|
/**
|
|
* Implements ArrayAccess interface.
|
|
*/
|
|
public function offsetSet($offset, $value): void
|
|
{
|
|
$this->{$offset} = $value;
|
|
}
|
|
|
|
public function offsetExists($offset): bool
|
|
{
|
|
return isset($this->{$offset});
|
|
}
|
|
|
|
public function offsetUnset($offset): void
|
|
{
|
|
unset($this->{$offset});
|
|
}
|
|
|
|
public function offsetGet($offset): mixed
|
|
{
|
|
$method = $this->offsetMethod($offset);
|
|
if (method_exists($this, $method)) {
|
|
$res = call_user_func([$this, $method]);
|
|
|
|
return $res;
|
|
}
|
|
|
|
return isset($this->{$offset}) ? $this->{$offset} : null;
|
|
}
|
|
|
|
/**
|
|
* @return \Doctrine\DBAL\Query\QueryBuilder
|
|
*/
|
|
protected function createQueryBuilder()
|
|
{
|
|
$qb = sqlQueryBuilder()
|
|
->select($this->getTableFields())
|
|
->addSelect(\Query\Product::withVat())
|
|
->fromProducts()
|
|
->joinVariationsOnProducts()
|
|
->groupBy('p.id')
|
|
->setMaxResults(1);
|
|
|
|
$inStoreField = \Query\Product::getInStoreField(false, $qb);
|
|
|
|
if (findModule('products', 'showMax')) {
|
|
$qb->addSelect('LEAST('.$inStoreField.', COALESCE(p.in_store_show_max, '.findModule('products', 'showMax').')) in_store');
|
|
} else {
|
|
$qb->addSelect($inStoreField.' in_store');
|
|
}
|
|
|
|
if (findModule('seo')) {
|
|
$qb->addSelect('p.meta_title, p.meta_description, p.meta_keywords');
|
|
}
|
|
|
|
if (findModule(\Modules::PRODUCTS, \Modules::SUB_WEIGHT)) {
|
|
$qb->addSelect('p.weight');
|
|
}
|
|
|
|
if (findModule(\Modules::PRODUCTS, \Modules::SUB_PRICE_BUY)) {
|
|
$qb->addSelect('p.price_buy');
|
|
}
|
|
|
|
if (findModule(\Modules::PRICELISTS)) {
|
|
$pricelistContext = ServiceContainer::getService(\KupShop\PricelistBundle\Context\PricelistContext::class);
|
|
|
|
if ($pricelistContext->getActiveId()) {
|
|
$qb->andWhere(\KupShop\PricelistBundle\Query\Product::applyPricelist($pricelistContext->getActiveId(), $this->variationId ?? null));
|
|
}
|
|
}
|
|
|
|
$qb->andWhere(
|
|
Query\Translation::coalesceTranslatedFields(
|
|
\KupShop\I18nBundle\Translations\ProductsTranslation::class
|
|
)
|
|
);
|
|
|
|
if (findModule(Modules::PRODUCTS, Modules::SUB_UNITS)) {
|
|
$qb->leftJoin('p', 'products_units', 'pu', 'p.unit=pu.id')
|
|
->addSelect('p.unit, pu.short_name, pu.short_name_admin, pu.long_name as unit_long_name')
|
|
->andWhere(Translation::coalesceTranslatedFields(ProductsUnitsTranslation::class));
|
|
}
|
|
|
|
if (findModule(Modules::PRODUCTS, Modules::SUB_UNITS_FLOAT)) {
|
|
$qb->addSelect('pu.pieces_precision');
|
|
}
|
|
|
|
if (findModule(Modules::OSS_VATS)) {
|
|
$qb->addSelect('p.id_cn');
|
|
}
|
|
|
|
if (findModule(\Modules::PRICE_HISTORY)) {
|
|
$qb->addSelect('p.price_for_discount');
|
|
}
|
|
|
|
if (findModule(\Modules::COMPONENTS)) {
|
|
$qb->addSelect(\Query\Product::withProductPhotoId());
|
|
}
|
|
|
|
return $qb;
|
|
}
|
|
|
|
/**
|
|
* Bacha! Může vracet i null - například ve fulltextu, kde se volá createFromArray, ale není selectnutý field price.
|
|
*
|
|
* @return ProductPrice|null
|
|
*/
|
|
public function getProductPrice()
|
|
{
|
|
return $this->productPrice;
|
|
}
|
|
|
|
public function setProductPrice($productPrice): void
|
|
{
|
|
$this->productPrice = $productPrice;
|
|
}
|
|
|
|
public function fetchBonusPoints(): void
|
|
{
|
|
if (!findModule(Modules::BONUS_PROGRAM)) {
|
|
return;
|
|
}
|
|
|
|
$multiFetch = ServiceContainer::getService(MultiFetch::class);
|
|
$multiFetch->fetchBonusPoints(new ProductCollection([$this->id => $this]));
|
|
}
|
|
|
|
protected function calculateBonusPoints($id_variation = null): Decimal
|
|
{
|
|
$bonusComputer = ServiceContainer::getService(BonusComputer::class);
|
|
|
|
return $bonusComputer->getProductsBonusPoints($this, $id_variation);
|
|
}
|
|
|
|
public function getBonusPoints($id_variation = null): ?Decimal
|
|
{
|
|
if (!findModule(Modules::BONUS_PROGRAM)) {
|
|
return null;
|
|
}
|
|
|
|
$id_variation = $id_variation ?: $this->matched_id_variation ?: null;
|
|
|
|
if (isset($this->bonus_points_cache[$id_variation])) {
|
|
return $this->bonus_points_cache[$id_variation];
|
|
}
|
|
|
|
if ($id_variation) {
|
|
$variation = $this->findVariation($id_variation);
|
|
if (isset($variation['bonus_points'])) { // rucni nastaveni bodu primo u varianty
|
|
return $this->bonus_points_cache[$id_variation] = toDecimal($variation['bonus_points']);
|
|
}
|
|
}
|
|
|
|
if ($this->bonus_points) { // rucni nastaveni bodu primo u produktu
|
|
return $this->bonus_points_cache[$id_variation] = $this->bonus_points;
|
|
}
|
|
|
|
return $this->bonus_points_cache[$id_variation] = $this->calculateBonusPoints($id_variation);
|
|
}
|
|
|
|
private function fetchUnit($data)
|
|
{
|
|
if (empty($data['unit'])) {
|
|
$this->unit = [];
|
|
|
|
return;
|
|
}
|
|
|
|
$this->unit = [
|
|
'id' => $data['unit'],
|
|
'short_name' => $data['short_name'],
|
|
'short_name_admin' => $data['short_name_admin'] ?? $data['short_name'],
|
|
'long_name' => $data['unit_long_name'],
|
|
];
|
|
|
|
if (findModule(Modules::PRODUCTS, Modules::SUB_UNITS_FLOAT)) {
|
|
switch ($data['pieces_precision']) {
|
|
case 0.0001: $this->unit['pieces_precision'] = 4;
|
|
break;
|
|
case 0.001: $this->unit['pieces_precision'] = 3;
|
|
break;
|
|
case 0.01: $this->unit['pieces_precision'] = 2;
|
|
break;
|
|
case 0.1: $this->unit['pieces_precision'] = 1;
|
|
break;
|
|
case 0:
|
|
default: $this->unit['pieces_precision'] = 0;
|
|
}
|
|
|
|
$this->unit['step'] = $data['pieces_precision'];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return array|mixed
|
|
*/
|
|
public function getData()
|
|
{
|
|
if (!$this->data) {
|
|
return [];
|
|
}
|
|
|
|
return json_decode($this->data, true);
|
|
}
|
|
|
|
/**
|
|
* @return bool
|
|
*/
|
|
public function isVirtual()
|
|
{
|
|
$generate_coupon = ($this->getData()['generate_coupon'] ?? 'N');
|
|
if ($generate_coupon == 'Y') {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @return string
|
|
*/
|
|
public function getCN()
|
|
{
|
|
return $this->id_cn ?? Settings::getDefault()['oss_vats']['default'] ?? '';
|
|
}
|
|
}
|
|
|
|
if (empty($subclass)) {
|
|
class Product extends ProductBase
|
|
{
|
|
}
|
|
}
|