count() === 0) { return; } if (isset($products->_pompoVipPricesFetched)) { return; } $products->fetchPricelists(); $this->fetchDefaultPriceForDiscount($products); $currencyContext = Contexts::get(CurrencyContext::class); // pokud existuje cenik s DMOC, tak ho nactu if ($dmocPriceListId = ($this->configuration->getDMOCPriceLists()[$currencyContext->getActiveId()] ?? false)) { foreach ($products as $product) { $product->dmocPrice = $product->pricelists[$dmocPriceListId]['productPrice'] ?? null; $product->dmocDiscount = \DecimalConstants::zero(); if ($product->dmocPrice) { $product->dmocPrice = PriceCalculator::convert($product->dmocPrice, $product->getProductPrice()->getCurrency()); $productPriceWithVat = $product->getProductPrice()->getPriceWithVat(); $dmocPriceWithVat = $product->dmocPrice->getPriceWithVat(); if ($dmocPriceWithVat->isPositive()) { $product->dmocDiscount = \DecimalConstants::one()->sub($productPriceWithVat->div($dmocPriceWithVat))->mul(\DecimalConstants::hundred()); } else { $product->dmocDiscount = \DecimalConstants::zero(); } } } } // pokud existuje cenik pro prihlaseneho uzivatele, tak nactu VIP cenu if ($vipPriceListId = ($this->configuration->getUsersPriceLists()[$currencyContext->getActiveId()] ?? false)) { $userIsActive = Contexts::get(UserContext::class)->isActive(); foreach ($products as $product) { $product->userVipPrice = $product->pricelists[$vipPriceListId]['productPrice'] ?? null; $product->vipOriginalPrice = null; // pokud jsem prihlasenej a existuje VIP cena, tak chci nacist "Pompo setri" cenu if ($userIsActive && $product->userVipPrice) { $userVipPrice = $product->userVipPrice; // puvodni cena produktu (neprihlaseny uzivatel) // pokud mam puvodni cenu brat z ceniku if ($originalPriceListId = ($this->configuration->getPriceLists()[$currencyContext->getActiveId()] ?? false)) { $originalPrice = $product->pricelists[$originalPriceListId]['productPrice'] ?? $product->priceOriginal->getObject(); $defaultPriceForDiscount = $originalPrice->getPriceWithoutDiscount(); if (!empty($product->pricelists[$originalPriceListId]['price_for_discount'])) { $defaultPriceForDiscount = new ProductPrice( toDecimal($product->pricelists[$originalPriceListId]['price_for_discount']), Contexts::get(CurrencyContext::class)->getOrDefault($product->pricelists[$originalPriceListId]['currency']), $product->vat ); } } else { $originalPrice = $product->priceOriginal->getObject(); $defaultPriceForDiscount = $product->defaultPriceForDiscount ?? $originalPrice->getPriceWithoutDiscount(); } // prevod cen na stejnou menu PriceCalculator::makeCompatible($userVipPrice, $originalPrice, false); // pokud je VIP cena i original cena stejna $vipSavingsEnabled = true; if ($userVipPrice->getPriceWithVat()->equals($originalPrice->getPriceWithVat())) { $originalPrice = $defaultPriceForDiscount; $vipSavingsEnabled = false; } // prevod cen na stejnou menu PriceCalculator::makeCompatible($userVipPrice, $originalPrice, false); $product->priceForDiscount = $originalPrice; $product->originalPrice = $originalPrice; $product->vipOriginalPrice = $originalPrice; // spocitam rozdil - tim zjistim kolik mi setri Pompo klub $diff = $originalPrice->getPriceWithVat()->sub($userVipPrice->getPriceWithVat()); $product->vipSavingsPrice = null; // pokud je rozdil vetsi jak 0, tak ho ulozim at ho muzu zobrazit if ($vipSavingsEnabled && $diff->isPositive()) { $product->vipSavingsPrice = new Price($diff, $userVipPrice->getCurrency(), 0); } } } } $products->_pompoVipPricesFetched = true; } public function fetchDefaultPriceForDiscount(ProductCollection $products): void { if ($products->count() === 0) { return; } $qb = sqlQueryBuilder() ->select('id, COALESCE(price_for_discount, price) as price_for_discount, vat') ->from('products') ->where(Operator::inIntArray($products->getProductIds(), 'id')); $defaultCurrency = Contexts::get(CurrencyContext::class)->getDefault(); foreach ($qb->execute() as $item) { if (isset($products[$item['id']]) && !empty($item['price_for_discount'])) { $price_for_discount = toDecimal($item['price_for_discount']); $products[$item['id']]->defaultPriceForDiscount = new ProductPrice($price_for_discount, $defaultCurrency, getVat($item['vat'])); } } } /** * Multifetch pro nafetchovani informaci okolo skladu / prodejen, ktere se pouzivaji v katalogu, v kosiku atd... * * Nafetchuje skladovosti rozdelene podle skladu - jde o centralni sklad, sklad aktualne vybrane prodejny a soucet kusu na ostatnich prodejnach. */ public function fetchStoreInfo(ProductCollection $products): void { if (!findModule(\Modules::SELLERS) || $products->count() === 0) { return; } static $storeIdBySeller = []; $sellerContext = Contexts::get(SellerContext::class); // nacist ID aktivniho skladu podle aktivni prodejny if (!($storeIdBySeller[$sellerContext->getActiveId()] ?? false)) { $storeId = sqlQueryBuilder() ->select('id_store') ->from('sellers') ->where(Operator::equals(['id' => $sellerContext->getActiveId()])) ->execute()->fetchOne(); if ($storeId) { $storeIdBySeller[$sellerContext->getActiveId()] = (int) $storeId; } } $activeStoreId = $storeIdBySeller[$sellerContext->getActiveId()] ?? null; // nemusim to fetchovat znova, protoze uz to je fetchnuty if ($products->_pompoStoresFetched[$activeStoreId] ?? false) { return; } // multifetchu informace o skaldech $products->fetchStoresInStore(); $products->fetchProductOfSuppliersInStore(); $sellersByStoreId = $this->getSellersByStoreId(); // k produktum doplnim potrebne informace foreach ($products as $product) { // skladovost na centralnim skladu $product->inStoreMain = (int) ($product->storesInStore[$this->configuration->getMainStoreId()]['in_store'] ?? 0); // skladovost u dodavatele $product->inStoreSupplier = (int) ($product->in_store_suppliers ?? 0); // skladovost na moji prodejne $product->inStoreSeller = (int) ($product->storesInStore[$activeStoreId]['in_store'] ?? 0); // skladovost na ostatnich prodejnach $inStoreSellerOther = 0; foreach ($product->storesInStore ?? [] as $storeId => $item) { // v $sellersByStoreId mam prodejce indexovany podle ID skladu // zaroven tam jsou odfiltrovany prodejny podle aktivniho jazyku - pro cs jen CZ prodejny a pro sk jen SK prodejny // takze na CZ shopu nebudeme zobrazovat skladovost SK prodejen, protoze SK prodejna stejne na CZ shopu nejde vybrat if (!($sellersByStoreId[$storeId] ?? false)) { continue; } $inStoreSellerOther += max(0, $item['in_store']); } $product->inStoreSellerOther = (int) $inStoreSellerOther; } if (!isset($products->_pompoStoresFetched)) { $products->_pompoStoresFetched = []; } $products->_pompoStoresFetched[$activeStoreId] = true; } /** * Vrací, zda je daný produkt skladem u marketplace dodavatele. */ public function isInStoreMarketplace(\Product $product, float $quantity): bool { $marketplaceSuppliers = $this->configuration->getMarketplaceSuppliers(); $suppliers = $product->getSuppliers(); foreach ($marketplaceSuppliers as $marketplaceSupplierId => $_) { if (($suppliers[0][$marketplaceSupplierId] ?? 0) > $quantity) { return true; } } return false; } /** * Vrátí datum dopravy navýšený a potřebný počet dnů. * * @deprecated `External\PompoBundle\Overrides\Order\DeliveryInfo::calculateCollectionDate` is used instead */ public function getDeliveryDateIncremented(\DateTime $deliveryDate, array $items, ?\Delivery $delivery = null): \DateTime { return $deliveryDate; } public function getDeliveryExpeditionDate(\Delivery $delivery): \DateTime { // Kontroluju kvuli tomu, zda se stihne odeslat jeste dnes nebo ne $expeditionDate = new \DateTime(); $timeHours = '13:00:00'; if (!empty($delivery->time_hours)) { $timeHours = $delivery->time_hours; } try { $hourParts = explode(':', $timeHours); $maxHoursToday = (new \DateTime()) ->setTime((int) ($hourParts[0] ?? 0), (int) ($hourParts[1] ?? 0), (int) ($hourParts[2] ?? 0)); // Pokud se nestihne odeslat jeste dnes, tak navysim datum jeste o jeden den if ((new \DateTime()) > $maxHoursToday) { $expeditionDate = DateUtil::calcWorkingDays(1, $expeditionDate); } } catch (\Throwable $e) { } return $expeditionDate; } /** * Vrací date increment - počet dní o kolik se má navýšit datum doručení. * * Date increment se urcuje podle dodavatele, zavrene expedice z centraly nebo podle specialne nastaveneho incrementu pro dany den. */ public function getDateIncrement(array $items, ?array $orderedQuantities = [], ?\DateTime $expeditionDate = null): int { $increment = 0; if (empty($items)) { return $increment; } $filterItems = []; foreach ($items as $key => $_) { $parts = explode('/', (string) $key); $filterItems[$parts[0]] = $filterItems[$parts[0]] ?? null; if ($parts[1] ?? null) { $filterItems[$parts[0]][] = $parts[1]; } } // nactu si produkty $products = $this->getProducts($filterItems); // fetchnu si k produktum dodatecne info $this->fetchProductsSupplierDeliveryDateIncrement($products); $this->fetchStoreInfo($products); // projdu produkty foreach ($products as $key => $product) { // pokud je to nejhracka, tak nemame prodejny a zajima nas inStore field na produktu, ale pokud // jsme na pompu, tak nas zajima inStoreMain, kde je skladovost na hlavnim skladu bez prodejen $inStoreMain = $this->configuration->isNejhracka() ? $product->inStore : ($product->inStoreMain ?? 0); $tmpIncrement = 0; // pocet kusu skladem, ktere objednavam $requiredQuantity = $items[$key] ?? 0; // pocet kusu, ktere jsou objednane v uz vytvorene objednavce $orderedQuantity = $orderedQuantities[$key] ?? 0; // pocet kusu skladem na hlavnim skladu $inStore = $inStoreMain + min($orderedQuantity, $inStoreMain); // pocet kusu skladem u dodavatele $inStoreSuppliers = $product->in_store_suppliers; // navysit increment podle skladovosti - pokud neni skladem na hlavnim skladu, ale je skladem u dodavatele, tak dorucuji pozdeji if ($inStore < $requiredQuantity && $inStoreSuppliers > 0) { $tmpIncrement = (int) ($product->supplierDeliveryDateIncrement ?? 0); if ($supplierIncrement = $this->getSupplierOrderIncrement($product->_pompoSupplierId ?? self::SUPPLIER_ID)) { $tmpIncrement += $supplierIncrement; } } $increment = max($increment, $tmpIncrement); } $expeditionDate ??= new \DateTime(); // pokud je napr. expedice z centraly v dany den uzavrena, tak musim doruceni navysit if ($mainStoreIncrement = $this->getMainStoreDeliveryDateIncrement($expeditionDate)) { $increment += $mainStoreIncrement; } // pokud je v dany den nastaven nejaky dodatecny increment (Nastaveni e-shopu, Pompo a "Navýšení data doručení" if ($dayIncrement = $this->getDeliveryIncrementByDay($expeditionDate)) { $increment += $dayIncrement; } return $increment; } /** * Vrací datumy, kdy je hlavní sklad uzavřen a tímpádem je uzavčena i expedice z něj. */ public function getMainStoreClosedDates(): array { $dbcfg = \Settings::getDefault(); $from = $dbcfg->datago['mainStoreClosed']['from'] ?? null; $to = $dbcfg->datago['mainStoreClosed']['to'] ?? null; if (!empty($from) && !empty($to)) { try { $dateFrom = (new \DateTime($from))->setTime(0, 0); $dateTo = (new \DateTime($to))->setTime(23, 59, 59); return [$dateFrom, $dateTo]; } catch (\Throwable $e) { } } return [null, null]; } /** * Vrátí počet dní, o které je potřeba navýšit datum doručení pokud je expedice z centrály uzavřena. * Např. kvůli inventuře, svátku, nebo Vánocům. */ public function getMainStoreDeliveryDateIncrement(\DateTime $expeditionDate): int { $increment = 0; [$from, $to] = $this->getMainStoreClosedDates(); if (!empty($from) && !empty($to)) { try { // Pokud je centralni sklad z nejakyho duvodu uzavren if ($expeditionDate >= $from && $expeditionDate <= $to) { // rozdil mezi datumama $diff = (new \DateTime())->diff($to); // Ensure the increment is at least 1 day $increment = max(1, abs($diff->days)); } } catch (\Throwable $e) { } } return $increment; } public function getDeliveryIncrementByDay(\DateTime $expeditionDate): int { $dbcfg = \Settings::getDefault(); if ($increment = $dbcfg->pompoSettings['dayDeliveryIncrement'][$expeditionDate->format('N')] ?? 0) { return (int) $increment; } return 0; } /** * Odecte od incrementu nepracovni dny, aby kdyz se zavola DateUtil::calcWorkingDays nebylo datum o ty nepracovni dny posunuty. * * TODO: Zpusobilo to problemy s datumem doruceni pred svatkama (4.7.2023). Upravovalo se to pred Vanocema, takze tam to taky delalo nejakej problem. Nasimulovat, projit a vyresit! */ private function getDateIncrementWithoutWorkdays(int $increment): int { if ($increment <= 0) { return $increment; } foreach (range(1, $increment) as $i) { $date = (new \DateTime())->add(new \DateInterval('P'.$i.'D')); if (!DateUtil::isWorkday($date)) { $increment--; } } return $increment; } /** * Vraci pocet dnu do dalsiho objednavaciho dne od dodavatele. */ private function getSupplierOrderIncrement(int $supplierId = self::SUPPLIER_ID): int { static $supplierIncrement = []; if (!($supplierIncrement[$supplierId] ?? false)) { $supplier = sqlQueryBuilder() ->select('*') ->from('suppliers') ->where(Operator::equals(['id' => $supplierId])) ->execute()->fetchAssociative(); $supplier['data'] = json_decode($supplier['data'] ?? '', true) ?? []; $today = new \DateTime(); $orderHours = array_filter($supplier['data']['order_hours'] ?? []); $increment = null; // zkontrolovat, zda se bude dneska jeste objednavat od dodavatele if ($todayOrderHour = $orderHours[$today->format('N')] ?? null) { [$hour, $minute] = explode(':', $todayOrderHour); $todayOrderDate = (new \DateTime())->setTime((int) $hour, (int) $minute); // stiham objednat jeste dneska if ($today < $todayOrderDate) { $increment = 0; } } if ($increment === null) { // najit nejblizsi den, kdy se delaji objednavky dodavatelovi $increment = $this->recursivelyGetClosestDeliveryDateIncrement($supplier['data']['order_hours'] ?? []); } $supplierIncrement[$supplierId] = $increment; } return $supplierIncrement[$supplierId]; } /** * Rekurzivne prochazi objednaci casy nastavene u dodavatele, aby se nasel nejblizsi den, kdy se od dodavatele bude objednavat. */ private function recursivelyGetClosestDeliveryDateIncrement(array $orderHours, int $increment = 0, bool $deep = false): int { $today = new \DateTime(); $found = false; foreach ($orderHours as $day => $hour) { if (!$deep && $day <= $today->format('N')) { continue; } // if is weekday and hour is empty, skip // weekday should not be added to increment, because we are adding only working days if (in_array($day, [6, 7]) && empty($hour)) { continue; } $increment++; if (!empty($hour)) { $found = true; break; } } if (!$found) { if ($deep) { return 0; } $increment = $this->recursivelyGetClosestDeliveryDateIncrement($orderHours, $increment, true); } return $increment; } /** * Nafetchuje k produktum increment podle zaznamu v products of suppliers - kazdy produkt muze byt totiz delivery increment jeste povyseny. */ public function fetchProductsSupplierDeliveryDateIncrement(ProductCollection $products): ProductCollection { if ($products->count() === 0) { return $products; } if (isset($products->_pompoSupplierDeliveryDateIncrementFetched)) { return $products; } $qb = sqlQueryBuilder() ->select('pos.id_supplier, pos.id_product, pos.id_variation, MIN(COALESCE(pos.note, COALESCE(IF(JSON_VALUE(data, \'$.delivery_time\')=\'\', 4, JSON_VALUE(data, \'$.delivery_time\')), 4))) as note') ->from('products_of_suppliers', 'pos') ->join('pos', 'suppliers', 's', 's.id = pos.id_supplier') ->andWhere('pos.in_store > 0') ->groupBy('pos.id_product, pos.id_variation'); $specs = []; foreach ($products as $key => $product) { $id = explode('/', (string) $key); $productId = (int) $id[0]; $variationId = null; if (!empty($id[1])) { $variationId = (int) $id[1]; } $specs[] = Operator::equalsNullable(['id_product' => $productId, 'id_variation' => $variationId]); } $qb->andWhere(Operator::orX($specs)); foreach ($qb->execute() as $item) { $key = $item['id_product']; if ($item['id_variation']) { $key .= '/'.$item['id_variation']; } if ($products[$key] ?? false) { $products[$key]->_pompoSupplierId = $item['id_supplier'] ?? null; $products[$key]->supplierDeliveryDateIncrement = !empty($item['note']) ? (int) $item['note'] : null; } } $products->_pompoSupplierDeliveryDateIncrementFetched = true; return $products; } /** * Nastaví produktům stitek podle podmínek. * * Bezva cena - DMOC sleva >= 16 * Cenový hit - DMOC sleva >= 36 */ public function generateProductDiscountLabels(): void { // Bezva cena $labelIdBC = $this->labelUtil->getLabelIdByCode('BC'); // Cenový hit $labelIdCH = $this->labelUtil->getLabelIdByCode('CH'); if (!$labelIdBC || !$labelIdCH) { return; } $productList = clone $this->productList; $productList->fetchProductOfSuppliersInStore(); $productList->applyDefaultFilterParams(); $productList->addResultModifiers(fn (ProductCollection $products) => $this->fetchAdditionalPrices($products)); $batchSize = 500; $updateData = [ $labelIdBC => [], $labelIdCH => [], ]; // projdu produkty abych nasel ty, kterym potrebuju nastavit kampan $iterationLimit = $productList->getProductsCount() / $batchSize; for ($i = 0; $i < $iterationLimit; $i++) { $productList->limit($batchSize, $i * $batchSize); foreach ($productList->getProducts() as $product) { if (!isset($product->dmocDiscount)) { continue; } if (!$product->dmocDiscount->isPositive()) { continue; } $discount = $product->dmocDiscount->asFloat(); // Cenovy hit if ($discount >= 36) { $updateData[$labelIdCH][] = $product->id; // Bezva cena } elseif ($discount >= 16) { $updateData[$labelIdBC][] = $product->id; } } } foreach ($updateData as $labelId => $data) { // smazu vygenerovane stitky od produktu sqlQueryBuilder() ->delete('product_labels_relation') ->where(Operator::equals(['id_label' => $labelId])) ->execute(); $baseInsertQb = sqlQueryBuilder() ->insert('product_labels_relation') ->onDuplicateKeyUpdate(['id_label', 'id_product']); // nastavim stitek k produktum foreach (array_chunk($data, 500) as $products) { $qb = clone $baseInsertQb; foreach ($products as $productId) { $qb->multiDirectValues(['id_label' => $labelId, 'id_product' => $productId, 'generated' => 1]); } $qb->execute(); } } } public function updateProductsVIPPriceList(): void { $selectQb = sqlQueryBuilder() ->select($this->configuration->getVIPPriceListId(), 'p.id', 'ROUND(p.price_buy * 1.05, 4) as vip_price') ->from('products', 'p') ->where('price_buy IS NOT NULL AND price_buy > 0'); sqlQuery("INSERT INTO pricelists_products (id_pricelist, id_product, price) {$selectQb->getSQL()} ON DUPLICATE KEY UPDATE price = VALUES(price);"); } /** * Nastaví produktům flagy podle podmínek. * * Bezva cena - DMOC sleva >= 16 * Cenový hit - DMOC sleva >= 36 * * @depracated Delete me when labels are fully used instead of campaigns */ public function generateProductDiscountFlags(): void { $campaigns = getCampaigns(); if (!isset($campaigns['BC']) || !isset($campaigns['CH'])) { return; } $productList = clone $this->productList; $productList->fetchProductOfSuppliersInStore(); $productList->applyDefaultFilterParams(); $productList->addResultModifiers(fn (ProductCollection $products) => $this->fetchAdditionalPrices($products)); $batchSize = 500; $updateData = [ 'BC' => [], 'CH' => [], ]; // projdu produkty abych nasel ty, kterym potrebuju nastavit kampan $iterationLimit = $productList->getProductsCount() / $batchSize; for ($i = 0; $i < $iterationLimit; $i++) { $productList->limit($batchSize, $i * $batchSize); foreach ($productList->getProducts() as $product) { if (!isset($product->dmocDiscount)) { continue; } if (!$product->dmocDiscount->isPositive()) { continue; } $discount = $product->dmocDiscount->asFloat(); // Cenovy hit if ($discount >= 36) { $updateData['CH'][] = $product->id; // Bezva cena } elseif ($discount >= 16) { $updateData['BC'][] = $product->id; } } } foreach ($updateData as $campaign => $data) { // smazu kampan od produktu sqlQueryBuilder() ->update('products') ->set('campaign', 'REMOVE_FROM_SET(:campaign, campaign)') ->where(Operator::findInSet([$campaign], 'campaign')) ->setParameter('campaign', $campaign) ->execute(); // nastavim kampan k produktum foreach (array_chunk($data, 500) as $products) { sqlQueryBuilder() ->update('products') ->set('campaign', 'ADD_TO_SET(:campaign, campaign)') ->where(Operator::inIntArray(array_values($products), 'id')) ->setParameter('campaign', $campaign) ->execute(); } } } /** * Vrací prodejce indexované podle ID skladu. */ private function getSellersByStoreId(): array { return Mapping::mapKeys( Contexts::get(SellerContext::class)->getSupported(), function ($k, $v) { return [$v['id_store'], $v]; } ); } /** * Vrací `ProductCollection` podle zadaných ID produktů. */ private function getProducts(array $products): ProductCollection { static $productsCache = []; $cacheKey = md5(serialize($products)); if (!($productsCache[$cacheKey] ?? false)) { $productList = clone $this->productList; $productList->setVariationsAsResult(true); $collection = $productList->andSpec(Product::productsAndVariationsIds($products)) ->getProducts(); $collection->fetchProductOfSuppliersInStore(); $productsCache[$cacheKey] = $collection; } return $productsCache[$cacheKey]; } }