select('pc.id_product AS id') ->from('products_collections', 'pc') ->where('pc.id_product_related IN (:ids) OR pc.id_product IN (:ids)'); $related_products = sqlQueryBuilder()->select('pc.id_product_related AS id') ->from('products_collections', 'pc') ->leftJoin('pc', 'products_collections', 'pc_main', 'pc_main.id_product = pc.id_product') ->where('pc_main.id_product IN (:ids) OR pc_main.id_product_related IN (:ids)'); $qb->setParameter('ids', $ids, Connection::PARAM_INT_ARRAY); return $main_products->getSQL().' UNION '.$related_products->getSQL(); } public static function collectionsProductsSubQuery($productIds) { // tabulka [id, main_product, related_product] // produkt neni v kolekci -> [id, null, null] // produkt je hlavni v kolekci -> [id, id, related_product_1], [id, id, related_product_2], ... // produkt je v kolekci -> [id, main_product, related_product_1], [id, main_product, related_product_2], ... $products = sqlQueryBuilder()->select('id')->fromProducts()->where(Operator::inIntArray($productIds, 'id')); return sqlQueryBuilder() ->select('DISTINCT p.id, COALESCE(pc_main.id_product, pc_related.id_product) AS main_product, COALESCE(pc_main.id_product_related, pc_related_2.id_product_related) AS related_product') ->from("({$products->getSQL()})", 'p')->addParameters($products->getParameters(), $products->getParameterTypes()) ->leftJoin('p', 'products_collections', 'pc_main', 'pc_main.id_product = p.id') ->leftJoin('p', 'products_collections', 'pc_related', 'pc_related.id_product_related = p.id') ->leftJoin('p', 'products_collections', 'pc_related_2', 'pc_related_2.id_product = pc_related.id_product'); } public static function isVisible() { return Filter::byFigure('Y'); } public static function inSection($sectionId) { return self::inSections([$sectionId]); } public static function inSections(array $sectionIds, $alias = 'ps') { return function (QueryBuilder $qb) use ($sectionIds) { $sectionIds = array_merge($sectionIds, [-1000]); // nonsense id, so there are always at least two ids - hack for better queryplan $existsSubquery = sqlQueryBuilder()->select('id_product') ->from('products_in_sections') ->where('id_product = p.id') ->andWhere(Operator::inIntArray($sectionIds, 'id_section')); return Operator::exists( $existsSubquery ->addParameters($existsSubquery->getParameters(), $existsSubquery->getParameterTypes()) ); }; } public static function inSectionsRecursive(array $sectionIds) { return self::inSections( call_user_func_array('array_merge', array_map('getDescendantCategories', $sectionIds)) ); } public static function inSectionsByLevel(array $ancestorAndLevel, array $excludeOptions = []) { if (isset($excludeOptions['ancestorID']) && isset($excludeOptions['level'])) { unset($ancestorAndLevel[$excludeOptions['ancestorID']][$excludeOptions['level']]); } $sections = []; foreach ($ancestorAndLevel as $ancestorID => $data) { foreach ($data as $level => $rawSectionIDs) { $sections[] = self::inSections( call_user_func_array('array_merge', array_map('getDescendantCategories', $rawSectionIDs)), 'ps_'.$ancestorID.'_'.$level ); } } return Operator::andX($sections); } public static function inStore($useVariations = false) { return function (QueryBuilder $qb) use ($useVariations) { if ($useVariations) { $qb->joinVariationsOnProducts(); $inStore = $qb->expr()->gt(static::getInStoreField(true, $qb), 0); } else { $inStore = $qb->expr()->gt(static::getInStoreField(false, $qb), 0); } return $inStore; }; } public static function inStoreVariations() { return function (QueryBuilder $qb) { $qb->joinVariationsOnProducts(); // $qb->andWhere('pv.id IS NOT NULL AND pv.in_store > 0'); $inStore = $qb->expr()->gt('COALESCE(pv.in_store, 1)', 0); return $inStore; }; } public static function inStoreSupplier($useVariations = false) { if (!findModule(\Modules::PRODUCTS_SUPPLIERS)) { return null; } return function (QueryBuilder $qb) use ($useVariations) { $suppliers = sqlQueryBuilder()->select('1') ->from('products_of_suppliers', 'pf_pos') ->andWhere('p.id = pf_pos.id_product') ->andWhere('pf_pos.in_store > 0'); if ($useVariations) { $qb->joinVariationsOnProducts(); $suppliers->andWhere('(pv.id IS NULL and pf_pos.id_variation IS NULL) OR (pv.id=pf_pos.id_variation)'); } return Operator::exists($suppliers); }; } public static function inStoreSeller(): callable { if (!findModule(\Modules::SELLERS) || !($storeId = Contexts::get(SellerContext::class)->getActive()['id_store'] ?? null)) { return self::inStore(); } return Filter::byStores([$storeId]); } public static function withOneProductPhotoId($variationsAsResult = false, $imageKind = null, bool $fallbackToProductPhoto = true) { return function (QueryBuilder $qb) use ($variationsAsResult, $imageKind, $fallbackToProductPhoto) { $photoQb = sqlQueryBuilder() ->from('products', 'p2') ->leftJoin('p2', 'products_variations', 'pv2', 'pv2.id_product = p2.id') ->where('p2.id = p.id AND ((pv.id IS NULL) OR pv.id = pv2.id)') ->addSelect(self::withProductPhotoId($variationsAsResult, $imageKind, $fallbackToProductPhoto, 'p2', 'pv2', false)) ->setMaxResults(1); $qb->addQueryBuilderParameters($photoQb); return '('.$photoQb->getSQL().') as id_photo'; }; } public static function withProductPhotoId($variationsAsResult = false, $imageKind = null, bool $fallbackToProductPhoto = true, $aP = 'p', $aPV = 'pv', $selectAdditional = true) { return function (QueryBuilder $qb) use ($selectAdditional, $variationsAsResult, $imageKind, $fallbackToProductPhoto, $aP, $aPV) { $addSelects = []; if ($selectAdditional) { $addSelects = [['date_update', 'id_photo_update']]; } $joinCondition = "{$aP}.id=ppr.id_product"; if (findModule(\Modules::PRODUCTS_VARIATIONS_PHOTOS)) { if (is_null($imageKind)) { $imageKind = $variationsAsResult ? 'N' : 'Y'; } if ($variationsAsResult) { // use variation photo if present // fallback to product's main photo in case product has no variation $joinCondition .= " AND (ppr.id_variation = {$aPV}.id OR ({$aPV}.id IS NULL AND ppr.show_in_lead='Y'))"; } else { $joinCondition .= " AND ppr.show_in_lead='{$imageKind}'"; } if ($fallbackToProductPhoto) { // fallback to product photo when variation exists, but has no photo $qb->leftJoin($aP, 'photos_products_relation', 'ppr2', "{$aP}.id=ppr2.id_product AND ppr2.show_in_lead='Y'"); $qb->leftJoin('ppr2', 'photos', 'ph2', 'ph2.id = ppr2.id_photo'); $aliasesToSelect = ['ph', 'ph2']; } else { $aliasesToSelect = ['ph']; } } else { $joinCondition .= " AND ppr.show_in_lead='Y'"; $aliasesToSelect = ['ph']; } $qb->leftJoin($aP, 'photos_products_relation', 'ppr', $joinCondition); // + ppr.id_variation $qb->leftJoin('ppr', 'photos', 'ph', 'ph.id = ppr.id_photo'); $qb->setForceIndexForJoin('ph', 'PRIMARY'); $createSelect = function ($field, $alias = null) use ($aliasesToSelect) { return 'COALESCE('.implode( ',', array_map( function ($val) use ($field) { return $val.".{$field}"; }, $aliasesToSelect ) ).') as '.($alias ?? $field); }; $selects = $createSelect('id', 'id_photo'); foreach ($addSelects as $aS) { $selects .= ', '.$createSelect($aS[0], $aS[1] ?? null); } if ($selectAdditional) { $qb->andWhere(Translation::coalesceTranslatedFields(PhotosTranslation::class, ['descr' => 'descr_photo'])); } return $selects; }; } public static function withVatAndDiscount(QueryBuilder $qb, $priceField = 'p.price') { $qb->joinVariationsOnProducts(); $discountField = 'p.discount'; if (findModule(\Modules::PRICELISTS)) { $pricelistContext = ServiceContainer::getService(PricelistContext::class); if ($pricelistContext->getActiveId()) { $qb->andWhere(\KupShop\PricelistBundle\Query\Product::applyPricelist($pricelistContext->getActiveId())) ->leftJoin('prl', 'currencies', 'c', 'c.id=prl.currency'); $priceField = 'COALESCE(prlv.price*c.rate, pv.price, prlp.price*c.rate, p.price)'; $discountField = 'COALESCE(IF(prlv.price IS NULL, NULL, prlp.discount), IF(pv.price IS NULL, NULL, p.discount), IF(prlp.price IS NULL, NULL, prlp.discount), IF(p.price IS NULL, NULL, p.discount))'; } } return "{$priceField} * (1+v.vat/100) * (1-{$discountField}/100)"; } public static function productsAndVariationsIds(array $items): ?callable { $specs = []; foreach ($items as $productId => $variationId) { if (is_array($variationId)) { foreach ($variationId as $varId) { $specs[] = Operator::equalsNullable(['p.id' => $productId, 'pv.id' => $varId]); } } elseif (is_null($variationId)) { $specs[] = Operator::equalsNullable(['p.id' => $productId]); } else { $specs[] = Operator::equalsNullable(['p.id' => $productId, 'pv.id' => $variationId]); } } if (!$specs) { return Operator::andX('1 = 0'); } return Operator::orX($specs); } public static function productsIds($products) { return function (QueryBuilder $qb) use ($products) { $qb->setParameter('products', $products, Connection::PARAM_INT_ARRAY) ->andWhere(Operator::inIntArray($products, 'p.id')) ->addOrderBy('FIELD(p.id, :products)'); }; } public static function getProductInStock(QueryBuilder $qb) { $query = static::getInStoreField(true, $qb); if (findModule(\Modules::PRODUCTS_SUPPLIERS) && in_array(\Settings::getDefault()->order_availability, [\Settings::ORDER_AVAILABILITY_IN_STORE_OR_SUPPLIER_STORE, \Settings::ORDER_AVAILABILITY_ALL])) { // in stock = in_store (pokud je > 0) + skladem u dodavatele (pokud je > 0) // aby nedochazelo k dvojitemu odecitani skladovosti (sklad + pos) if (!findModule(\Modules::PRODUCTS_SUPPLIERS, \Modules::SUB_ALLOW_NEGATIVE_IN_STORE)) { $query = "GREATEST(0, {$query})"; } $query .= ' + (SELECT GREATEST(0, COALESCE(SUM(pos.in_store), 0)) FROM products_of_suppliers pos WHERE p.id = pos.id_product AND ((pv.id IS NULL and pos.id_variation IS NULL) OR (pv.id=pos.id_variation)))'; } if (findModule('products', 'showMax')) { $cfg = Config::get(); $query = 'LEAST('.$query.', COALESCE(pv.in_store_show_max, p.in_store_show_max, '.$cfg['Modules']['products']['showMax'].'))'; } // aby se na FE nedostala zaporna skladovost, minimum je 0 if (findModule(\Modules::PRODUCTS_SUPPLIERS, \Modules::SUB_ALLOW_NEGATIVE_IN_STORE)) { $query = "GREATEST(0, {$query})"; } return $query; } public static function getInStoreField($useVariations = true, $qb = null) { $field = 'in_store'; if ($callable = findModule('products', 'in_store_field')) { if (is_callable($callable)) { $field = call_user_func($callable, $useVariations, $qb); } } if ($callable = findModule('products', 'in_store_spec')) { if (is_callable($callable)) { return call_user_func($callable, $useVariations, $qb); } } return static::applyProductReservationsOnInStoreField( $useVariations ? "COALESCE(pv.{$field}, p.{$field})" : 'p.'.$field, $useVariations ); } public static function getStoresQuantityField(QueryBuilder $qb): string { if ($callable = findModule('products', 'stores_quantity_spec')) { if (is_callable($callable)) { return call_user_func($callable, $qb); } } return 'SUM(si.quantity)'; } public static function getDiscountField(&$spec): string { $spec = null; $discountField = 'p.discount'; if (findModule(\Modules::PRICE_HISTORY)) { $price = 'p.price'; $price_for_discount = 'p.price_for_discount'; $price_common = 'p.price_common'; if (findModule(\Modules::PRODUCTS_VARIATIONS)) { $spec = function (QueryBuilder $qb) { $qb->joinVariationsOnProducts(); }; $price = 'COALESCE(pv.price, p.price)'; $price_for_discount = 'COALESCE(pv.price_for_discount, p.price_for_discount)'; $price_common = 'COALESCE(pv.price_common, p.price_common)'; } if (findModule(\Modules::PRODUCTS, \Modules::SUB_PRICE_COMMON)) { // pokud je price_for_discount deaktivovana, vypocitat slevu z price_common $price_for_discount = "IF({$price_for_discount} = -1, {$price_common}, {$price_for_discount})"; } $discountField = "IF({$price_for_discount} > 0, (100 - {$price}*(100-p.discount)/{$price_for_discount}), p.discount)"; $display_discount = \Settings::getDefault()['prod_display_discount'] ?? null; if ($display_discount == 'Y') { $discountField = "IF(p.discount > 0, {$discountField}, 0)"; } $display_discount = \Settings::getDefault()['prod_display_discount_price_common'] ?? null; if ($display_discount == 'Y') { $discountField = "IF({$price_common} > 0, {$discountField}, 0)"; } } return $discountField; } public static function withVat() { $vatContext = Contexts::get(VatContext::class); if ($vatContext->isOssActive()) { return function (QueryBuilder $qb) { $defaultVat = Contexts::get(VatContext::class)->getDefault(); $qb->joinVatsOnProducts() ->addSelect('COALESCE(v.id, :default_vat) vat, p.vat AS original_vat') ->setParameter('default_vat', $defaultVat['id']); }; } else { return function (QueryBuilder $qb) { $qb->addSelect('p.vat'); }; } } public static function withVats($countries) { return function (QueryBuilder $qb) use ($countries) { $defaultCn = \Settings::getDefault()['oss_vats']['default'] ?? null; foreach ($countries as $country) { $qbVat = sqlQueryBuilder()->select('vv.vat')->from('vats', 'vv') ->leftJoin('vv', 'vats_cns', 'vatcns', 'vv.id = vatcns.id_vat') ->andWhere('id_cn = COALESCE(p.id_cn, :defaultCn) AND id_country = :country_'.$country) ->setParameter('defaultCn', $defaultCn) ->setParameter('country_'.$country, $country); $qb->addSubselect($qbVat, 'vat_'.$country); } }; } public static function allowBuyNotInStore() { // vraci spec s delivery_time, ktere nemaji povolen nakup produktu a variant co nejsou skladem (in_store = 0) $deliveryTimeCfg = Config::get()['Products']['DeliveryTimeConfig'] ?? []; if (!empty($deliveryTimeCfg)) { $notAllowed = array_filter($deliveryTimeCfg, function ($key) { if (!array_key_exists('allowBuy', $key)) { return false; } return !$key['allowBuy']; }); return Operator::orX( Operator::inIntArray(array_keys($notAllowed), 'p.delivery_time'), Operator::inIntArray(array_keys($notAllowed), 'pv.delivery_time')); } return Operator::andX('0=0'); } protected static function applyProductReservationsOnInStoreField(string $inStoreField, bool $useVariations): string { if (!findModule(\Modules::PRODUCT_RESERVATIONS)) { return $inStoreField; } $subquery = sqlQueryBuilder() ->select('COALESCE(SUM(pr.quantity), 0)') ->from('product_reservations', 'pr') ->andWhere($useVariations ? 'pv.id = pr.id_variation' : 'p.id = pr.id_product'); if ($ignoredTypes = Contexts::get(UserContext::class)->getIgnoredReservationTypes()) { $subquery->andWhere( Operator::not('pr.type IN ('.implode(',', array_map(fn ($x) => '"'.$x.'"', $ignoredTypes)).')') ); } return "({$inStoreField} - ({$subquery->getSQL()}))"; } public static function generatedCoupons() { return Operator::equals([JsonOperator::value('p.data', 'generate_coupon') => 'Y']); } }