productUtil = $productUtil; $this->configuration = $configuration; $this->orderUtil = $orderUtil; $this->orderItemInfo = $orderItemInfo; $this->pompoUtil = $pompoUtil; $this->drsApi = $drsApi; $this->dataGoApi = $dataGoApi; $this->sellerUtil = $sellerUtil; } /** * Rozdělí objednávku typu ORDER_TRANSPORT_RESERVATION na dvě objednávky. Jedna objednávka bude čistě s dostupnýma produktama * na prodejně a druhá objednávka bude obsahovat produkty, které jsou dostupné pouzen a centrálním skladu a je potřeba je * tedy převézt na prodejnu. */ public function orderSplitTransportReservation(\Order $order): void { // rozdelujeme jen typ ORDER_TRANSPORT_RESERVATION if ($this->getOrderType($order) !== OrderType::ORDER_TRANSPORT_RESERVATION) { return; } // cely to delam v transakci sqlGetConnection()->transactional(function () use ($order) { // force-loadnu si data, protoze eventy tam mohli neco pridat a bez force-loadu bych to tam nemel $orderData = $order->getDataAll(true); $sellerId = empty($orderData['sellerId']) ? null : (int) $orderData['sellerId']; $items = $this->getPurchaseStateProducts($order->getPurchaseState(), $sellerId, false); $itemsGrouped = $this->getPurchaseStateProductsGrouped($order->getPurchaseState()); // prvne se podivam, zda je co rozdelovat, abych nevytvoril prazdnou rezervacni objednavku $reservationItems = false; foreach ($items as $item) { if (($item->getProduct()->inStoreSeller ?? 0) > 0) { $reservationItems = true; break; } } if (!$reservationItems) { return; } // zduplikuju objednavku - tohle bude objednavka s cistou rezervaci $reservationOrder = $this->orderUtil->copyOrder($order->id); $reservationOrder->status_previous = -1; $reservationOrder->status = -1; $reservationOrder->setData('orderType', OrderType::ORDER_RESERVATION); $reservationOrder->setData('orderPartId', 2); $deliveryData = array_merge($orderData['delivery_data'] ?? [], ['seller_id' => $sellerId]); // TODO: mozna bude potreba pridat vetsi logiku $deliveryData['deliveryDate'] = (new \DateTime())->format('Y-m-d'); // insertnu vazbu objednavky na prodejnu k rezervacni objednavce if ($sellerId) { sqlQueryBuilder()->insert('order_sellers') ->directValues([ 'id_order' => $reservationOrder->id, 'id_seller' => $sellerId, ]) ->onDuplicateKeyUpdate(['id_order', 'id_seller']) ->execute(); } $reservationOrder->setData('delivery_data', $deliveryData); // Odeberu flag "Rezervace s prepravou" po zkopirovani z puvodni objednavky $this->orderUtil->removeFlag($reservationOrder, 'RT'); // pridam flag "Rezervace" $this->orderUtil->addFlag($reservationOrder, 'RZ'); // pridam flag "Vznikla rozdelenim" $this->orderUtil->addFlag($reservationOrder, 'VR'); foreach ($items as $item) { $key = $this->getPurchaseItemKey($item); $itemGrouped = $itemsGrouped[$key] ?? []; $itemGrouped->inStoreSeller = ($itemGrouped->inStoreSeller ?? ($item->getProduct()->inStoreSeller ?? 0)); // pokud to neni skladem na prodejne, tak to zustava v objednavce if ($itemGrouped->inStoreSeller <= 0) { continue; } // pocet kusu, ktere muzu zarezervovat $reservationPieces = min($itemGrouped->inStoreSeller, $item->getPieces()); $newPieces = $item->getPieces() - $reservationPieces; // zduplikuju item do rezervace s novym poctem kusu $this->copyOrderItem($reservationOrder, (int) $item->getId(), (int) $reservationPieces); $itemGrouped->inStoreSeller -= $reservationPieces; // u aktualni objednavky odeberu pocet kusu, ktere sli do rezervace $order->updateItem($item->getId(), $newPieces); // smazu item z objednavky, pokud slo vsechno do rezervace if ($newPieces <= 0) { sqlQueryBuilder() ->delete('order_items') ->where(Operator::equals(['id' => $item->getId()])) ->execute(); } } // nastavim na objednavku ID objednavky s rezervaci - je to hlavne kvuli FE, abychom pak mohli na dekovacce zobrazit odkazy na obe objednavky $order->setData('reservationOrderId', $reservationOrder->id); $order->setData('orderPartId', 1); // resetuju purchase state objednavky $order->setPurchaseState(null); // recalculate na objednavky $reservationOrder->recalculate(); $order->recalculate(); // resetnu itemy na objednavce, aby se znovu loadnuly, kdyz by si o ne nekdo rekl a bylo to tam spravne $order->items = []; // Odeslat e-mail k rezervacni objednavce $reservationOrder->changeStatus(0); // Zaloguju k objednavce, ze z ni byla vytvorena rezervacni objednavka $order->logHistory( sprintf('Z této objednávka byla vytvořena rezervační objednávka číslo %s', $reservationOrder->id, $reservationOrder->order_no) ); // Zaloguju k rezervacni objednavce info o tom, ze vznikla rozdelenim $reservationOrder->logHistory( sprintf('Rezervační objednávka, která vznikla rozdělením z objednávky číslo %s', $order->id, $order->order_no) ); }); } /** * Vrátí typ objednávky - jeden z typů definovaných v classe OrderType. */ public function getOrderType(\Order $order): string { // Objednavka uz na sobe orderType ma ulozeny if ($orderType = $order->getData('orderType')) { return $orderType; } // objednávka je přes přepravce if (!$this->isOrderInPerson($order)) { return OrderType::ORDER_TRANSPORT; } // kouknu, jestli ma objednavka manipulacni poplatek // nebo je to osobni odber s centralnim skladem if ($this->isTransportChargeInOrder($order) || $this->isOrderInPersonWithMainStore($order)) { return OrderType::ORDER_TRANSPORT_RESERVATION; } return OrderType::ORDER_RESERVATION; } /** * Lze pouzit pouze pokud vim, ze je doprava osobni odber. Protoze s PurchaseStatu nepoznam o jakou dopravu se jedna. * * Vrati to teda ORDER_TRANSPORT_RESERVATION nebo ORDER_RESERVATION */ public function getPurchaseStateOrderType(PurchaseState $purchaseState, int $sellerId): string { $seller = $this->sellerUtil->getSeller($sellerId); $notInSellerStore = false; foreach ($this->getPurchaseStateProducts($purchaseState, $sellerId) as $item) { // pokud neni v dostatecnem skladu na prodejne, tak uz to nemuze byt cista rezervace // nebo pokud se jedna o prodejnu, ktera ma nastaveny sklad na hlavni sklad - v takovem pripade to taky nesmi byt cista rezervace if (($item->getProduct()->inStoreSeller ?? 0) < $item->getPieces() || $seller['id_store'] === $this->configuration->getMainStoreId()) { $notInSellerStore = true; break; } } if ($notInSellerStore) { return OrderType::ORDER_TRANSPORT_RESERVATION; } return OrderType::ORDER_RESERVATION; } /** * Vrací informaci o tom, zda pro PurchaseState existuje prodejna, na kterou je možné provést závoz. */ public function hasProductWithInPersonTransferDelivery(PurchaseState $purchaseState): bool { if (!$this->configuration->isPompo()) { return false; } $transferDeliverySupported = false; $sellerContext = Contexts::get(SellerContext::class); $sellers = $sellerContext->getSupported(); $this->fetchSellersCartInfo( $purchaseState, $sellers ); foreach ($sellers as $seller) { if ($seller['availability'] === ProductAvailability::SELLER_IN_STORE_TRANSFER) { $transferDeliverySupported = true; break; } } return $transferDeliverySupported; } /** * Fetchne skladovou dostupnost k jednotlivym produktum v PurchaseStatu. Například na druhém kroku dopravy, kde * zobrazujeme produkty s dostupností. */ public function fetchPurchaseStateProductsAvailability(PurchaseState $purchaseState, ?int $sellerId = null, ?string $pickType = null): void { $productsInfo = $this->getPurchaseStateProducts($purchaseState, $sellerId); $giftsInfo = $this->getPurchaseStateGifts($purchaseState, $sellerId); $getInfoItem = function (ProductPurchaseItem $item) use ($productsInfo, $giftsInfo) { $key = $this->getPurchaseItemKey($item); if (!($infoItem = $productsInfo[$key] ?? null)) { if (!($infoItem = $giftsInfo[$key] ?? null)) { return null; } } return $infoItem; }; // zmerguju si produkty se slevama, abych prosel vsechno naraz $items = array_merge($purchaseState->getProducts(), $purchaseState->getDiscounts()); // doplnim jeste darky od produktu z additional items foreach ($purchaseState->getProducts() as $item) { foreach ($item->getAdditionalItems() as $additionalItem) { $items[] = $additionalItem; } } foreach ($items as $item) { // pokud to neni ProductPurchaseItem, tak me to nezajima if (!($item instanceof ProductPurchaseItem)) { continue; } if (!($infoItem = $getInfoItem($item))) { continue; } $product = $infoItem->getProduct(); $pieces = (float) $infoItem->getPieces(); $totalMainStore = ($product->inStoreMain ?? 0) + ($product->inStoreSupplier ?? 0); $totalSellerStore = ($product->inStoreMain ?? 0) + ($product->inStoreSupplier ?? 0) + ($product->inStoreSeller ?? 0); $productAvailability = ProductAvailability::NOT_IN_STORE; // Pokud je na hlavnim skladu dostatek mnozstvi if (($product->inStoreMain ?? 0) >= $pieces) { $productAvailability = ProductAvailability::IN_STORE; } elseif (($product->inStoreSupplier ?? 0) >= ($pieces - ($product->inStoreMain ?? 0))) { $productAvailability = ProductAvailability::IN_STORE_SUPPLIER; } if ($sellerId) { if ($pickType === 'complete') { $productAvailability = $productAvailability > ProductAvailability::NOT_IN_STORE ? ProductAvailability::SELLER_IN_STORE_PARTIALLY : ProductAvailability::NOT_IN_STORE; } if ($pickType !== 'complete') { $productAvailability = $this->getSellerAvailability( $product->inStoreSeller ?? 0, ($product->inStoreMain ?? 0) + ($product->inStoreSupplier ?? 0), $pieces ); } } // pridam notu, abych v kosiku byl schopny zobrazit primo u produktu stav skladu (napr. na kroku s dopravou) $item->addNote('availability', $productAvailability); if ($sellerId && $totalSellerStore < $pieces) { // pridam notu, abych v kosiku byl schopny zobrazit primo u produktu stav skladu (napr. na kroku s dopravou) $item->addNote('availability', ProductAvailability::NOT_IN_STORE); } if (!$sellerId && $totalMainStore < $pieces) { // pridam notu, abych v kosiku byl schopny zobrazit primo u produktu stav skladu (napr. na kroku s dopravou) $item->addNote('availability', ProductAvailability::NOT_IN_STORE); } } } /** * Kontrola dostupnosti pro každou dopravu. Kontrola se provádí pro každou dopravu, abych dokázal správně zadisablovat * dopravy, přes které nejde objednat. * * Kontroluje dostupnost produktu na prodejnach vs centralni sklad pro osobni odbery a pro dopravce. * * 1) Na prodejnu mohu objednat bud z centralniho skladu (zavoz na prodejnu) nebo muzu rezervovat zbozi na prodejne, ale nemohu * objednat zbozi z jine prodejny. * 2) Pokud objednavam pres dopravce, tak musi byt produkt skladem na centralnim sklade nebo u dodavatele, ale nemohu * objednat produkt z prodejny. */ public function validatePurchaseStateByDelivery(\Delivery $delivery, PurchaseState $purchaseState): void { $selectedDeliveryType = $purchaseState->getDeliveryType(); // pokud se jedna o osobni odber if ($delivery instanceof \OdberNaProdejne) { if ($delivery->getPointId()) { $sellerId = (int) $delivery->getPointId(); $seller = $this->sellerUtil->getSeller($sellerId); // zkontroluju, ze se na daneho sellera da objednavat - muzou prodejnu uzvarit napr. kvuli inventure if (!$seller || ($seller['data']['orders_disabled'] ?? 'N') === 'Y') { throw new DeliveryException( translate('seller_sellerOrdersDisabled', 'pompo'), translate('seller_sellerOrdersDisabled', 'pompo'), DeliveryException::ERROR_SOFT ); } // zvaliduju produkty v PurchaseState foreach ($this->getPurchaseStateProducts($purchaseState, $sellerId) as $item) { $product = $item->getProduct(); $pieces = (float) $item->getPieces(); // objednavam na prodejnu a nemam to skladem na centrala nebo na vybrane prodejne - je to nejspis skladem pouze na jine prodejne if ($seller['id_store'] != $this->configuration->getMainStoreId()) { $totalSellerStore = ($product->inStoreMain ?? 0) + ($product->inStoreSupplier ?? 0) + max($product->inStoreSeller ?? 0, 0); } else { $totalSellerStore = ($product->inStoreMain ?? 0) + ($product->inStoreSupplier ?? 0); } // musim mit vybranou dopravu "Osobni odber" a prodejnu, abych mohl zacit spoustet validaci if ($selectedDeliveryType && $sellerId) { // kontrola restrikci, pokud mam dostatecny sklad, ale neni dostatek kusu na prodejne a bude se muset zavazet z centralniho skladu // a teprve v takovou chvili muzu spustit kontrolu restrikci if ($totalSellerStore >= $pieces && ($product->inStoreSeller ?? 0) < $pieces) { try { // povolit a zkontrolovat restrikce $delivery->restrictionsEnabled = true; $delivery->checkRestrictions($purchaseState); $delivery->restrictionsEnabled = false; } catch (DeliveryException $e) { $delivery->restrictionsEnabled = false; // re-throw as soft error throw new DeliveryException( $e->getMessage(), $e->getShortMessage(), DeliveryException::ERROR_SOFT ); } } // kontrola dostupnosti if ($totalSellerStore < $pieces) { // vyhodim chybu, ale doprava "Osobni odber" neni zadisablovana, protoze si muzu chtit zmenit na jinou prodejnu throw new DeliveryException( // Produkt "%s" není na vybrané prodejně dostupný. sprintf( translate('seller_productNotInStore', 'pompo'), trim($item->getName()), $totalSellerStore ) ); } } } } return; } // validace PurchaseStatu pro ostatni dopravce static $products; if (!$products) { // pro dopravce mi staci nacist data k PurchaseStatu jen jednou, protoze tam by nemel byt zadny rozdil $products = $this->getPurchaseStateProducts($purchaseState); } // zvaliduju produkty v PurchaseState foreach ($products as $item) { $product = $item->getProduct(); $pieces = (float) $item->getPieces(); // kontroluju jednotlive dopravce dopravce, takze skladovost je hlavni sklad + sklad dodavatele $totalMainStore = ($product->inStoreMain ?? 0) + ($product->inStoreSupplier ?? 0); if ($totalMainStore < $pieces) { // pokud nemam dostatecne mnozstvi skladem, tak vyhazuju chybu, ktera disabluje i dopravy, aby nesla vybrat $deliveryException = new DeliveryException( // Produkt "%s" není možné objednat přes dopravce, protože je skladem pouze na vybraných prodejnách. sprintf( translate('productNotInMainStore', 'pompo'), trim($item->getName()), $totalMainStore ), // %s - na centralnim skladu je pouze %s ks sprintf( translate('productNotInMainStoreShort', 'pompo'), trim($item->getName()), $totalMainStore ), DeliveryException::ERROR_DELIVERY_DISABLED ); // pokud je dana doprava vybrana, tak vyhodim chybu aby se zobrazila i cervena hlaska nad kosikem if ($selectedDeliveryType && $selectedDeliveryType->id_delivery == $delivery->id) { throw $deliveryException; } // pokud doprava vybrana neni, tak jen nastavim chybu k doprave, aby se doprava disablovala a zobrazila u ni hlaska $delivery->exception = $deliveryException; } } } /** * Nafetchuje skladovou dostupnost k prodejcum - pouziva se to v kosiku, pri vybirani prodejny pro vyzvednuti. * * Zaroven to ke kazdemu prodejci prida deliveryDateIncrement, kterej se v kosiku pricte k predpokladanemu datu dorucenu * * $availability - viz. \External\PompoBundle\Util\Ordering\ProductAvailability */ public function fetchSellersCartInfo(PurchaseState $purchaseState, array &$sellers): void { // potrebuju si nafetchovat pripadny increment u dodavatele (kdyby jediny misto, kde je skladem byl dodavatel) $fetchProductSupplierIncrements = function (ProductCollection $products) { $this->productUtil->fetchProductsSupplierDeliveryDateIncrement($products); }; $giftProducts = $this->getPurchaseStateGifts($purchaseState, null); $productQuantityMainStore = []; $productQuantityByStore = []; // ulozim si skladova data do pole, aby se mi s tim lepe pracovalo foreach ($this->getPurchaseStateProducts($purchaseState, null, true, $fetchProductSupplierIncrements) as $item) { $product = $item->getProduct(); // Skladovost hlavni sklad + dodavatel $productQuantityMainStore[$item->getId()] = ($product->inStoreMain ?? 0) + ($product->inStoreSupplier ?? 0); // Skladovost dodavatele $productQuantitySupplier[$item->getId()] = $product->inStoreSupplier ?? 0; // Skladovost na jednotlivych prodejnach foreach ($product->storesInStore ?? [] as $storeId => $store) { $productQuantityByStore[$storeId][$item->getId()] = $store['in_store']; } } // projdu vsechny prodejny, abych k nim nacetl dostupnost foreach ($sellers as &$seller) { $availability = null; $deliveryDateIncrement = 0; $completePickupAvailable = true; // nactu si dostupnost darku v kosiku - zajima me jestli je na prodejne dostupny, nebo musi jit z centralniho skladu $giftsSellerAvailability = null; foreach ($giftProducts as $giftProduct) { if (($giftProduct->getProduct()->storesInStore[$seller['id_store']]['in_store'] ?? 0) > 0) { $giftsSellerAvailability = $giftsSellerAvailability === null ? 1 : min(1, $giftsSellerAvailability); } else { $giftsSellerAvailability = $giftsSellerAvailability === null ? 0 : min(0, $giftsSellerAvailability); } } // projdu produkty v kosiku foreach ($this->getPurchaseStateProductsGrouped($purchaseState) as $item) { $mainStoreQuantity = (float) ($productQuantityMainStore[$item->getId()] ?? 0); $sellerStoreQuantity = (float) ($productQuantityByStore[$seller['id_store']][$item->getId()] ?? 0); $productAvailability = $this->getSellerAvailability( $sellerStoreQuantity, $mainStoreQuantity, (float) $item->getPieces() ); // pokud to ma byt zavoz na prodejnu, tak musim udelat kontrolu restrikci if ($productAvailability === ProductAvailability::SELLER_IN_STORE_TRANSFER) { // kontrola restrikci, pokud mam dostatecny sklad, ale neni dostatek kusu na prodejne a bude se muset zavazet z centralniho skladu if (($mainStoreQuantity + $sellerStoreQuantity) > $item->getPieces() && $sellerStoreQuantity < $item->getPieces()) { // najdu si dopravu "OdberNaProdejne" $inPersonDeliveries = array_filter(\Delivery::getAll(), fn ($x) => $x instanceof \OdberNaProdejne); if (!empty($inPersonDeliveries) && ($inPersonDelivery = reset($inPersonDeliveries))) { try { $inPersonDelivery->restrictionsEnabled = true; $inPersonDelivery->checkRestrictions($purchaseState); $inPersonDelivery->restrictionsEnabled = false; } catch (DeliveryException $e) { // pokud restrikce nedovoluji zavoz na prodejnu, tak prodejnu oznacim jako, ze to tam neni dostupne $productAvailability = ProductAvailability::NOT_IN_STORE; $inPersonDelivery->restrictionsEnabled = false; } } } } // Ukladam si, zda je mozne zavest na prodejnu jako kompletni zasilku - v pripade, ze na prodejne neni vse skladem // tak muzu vybirat mezi "vyzvednout po castech" nebo "vyzvednout kompletni", ale k tomu, aby to slo vyzvednout komplet // je potreba, aby vsechno bylo skladem na centralnim skladu, odkud se ta kompletni zasilka na prodejnu zaveze if (($productQuantityMainStore[$item->getId()] ?? 0) < $item->getPieces()) { $completePickupAvailable = false; } // Pokud je produkt pouze na centralnim skladu, ale neco predtim uz bylo i na prodejne, tak chci zobrazovat "castecne skladem" if ($availability >= ProductAvailability::SELLER_IN_STORE_PARTIALLY && $productAvailability === ProductAvailability::SELLER_IN_STORE_TRANSFER) { $productAvailability = ProductAvailability::SELLER_IN_STORE_PARTIALLY; } // Pokud je produkt na prodejne, ale neco predtim bylo pouze na centralnim skladu, tak chci zobrazovat "castecne skladem" if ($availability === ProductAvailability::SELLER_IN_STORE_TRANSFER && $productAvailability >= ProductAvailability::SELLER_IN_STORE_PARTIALLY) { $availability = ProductAvailability::SELLER_IN_STORE_PARTIALLY; } $availability = $availability === null ? $productAvailability : min($availability, $productAvailability); // jakmile nema vsechny kusy produktu na prodejne, a nemam ani dostatek na centralnim skladu, tak zacinam resit deliveryDateIncrement ze skladu dodavatele $inStoreMainWithoutSupplier = ($productQuantityMainStore[$item->getId()] ?? 0) - ($productQuantitySupplier[$item->getId()] ?? 0); if ($availability < ProductAvailability::IN_STORE && $inStoreMainWithoutSupplier < $item->getPieces() && ($productQuantitySupplier[$item->getId()] ?? 0) > 0) { $deliveryDateIncrement = max($deliveryDateIncrement, $item->getProduct()->supplierDeliveryDateIncrement ?? 0); } } // pokud mam v kosiku darky a nektery z darku neni dostupny, tak se osobni odber musi tvarit jako zavoz if ($giftsSellerAvailability === 0) { $availability = ProductAvailability::SELLER_IN_STORE_TRANSFER; } // ulozim dostupnost na prodejce // viz. viz. \External\PompoBundle\Util\Ordering\ProductAvailability $seller['availability'] = $availability; // ulozim si, zda je mozne vyzvednout jako kompletni zasilku v pripade, ze je zbozi na prodejne castecne skladem $seller['completePickupAvailable'] = $completePickupAvailable; // jeste si potrebuju spocitat date increment pro prodejnu a pak navysit o pripadny deliveryDateIncrement $seller['deliveryDate'] = DateUtil::calcWorkingDays($deliveryDateIncrement, $this->getSellerDeliveryDate($seller, $availability)); // pokud je zbozi castecne skladem, tak v kosiku davame moznost rozdelit objednavku, aby neco slo vyzvednout ihned a neco az pozdeji if ($availability === ProductAvailability::SELLER_IN_STORE_PARTIALLY) { $seller['deliveryDatePartially'] = $this->getSellerDeliveryDate($seller, ProductAvailability::IN_STORE); } } } /** * Vrátí dostupnost na konkrétní prodejně. * * 0 - nedostupne (neni skladem nikde) * 1 - na prodejnu zavezeme (je skladem na centralnim sklade nebo u dodavatele, takze na prodejnu muzeme zavezt) * 2 - castecne skladem (cast je skladem na prodejne, ale cast jen na centralnim sklade nebu u dodavatele) * 3 - skladem (vsechno je skladem na prodejne) */ public function getSellerAvailability(float $sellerInStore, float $mainInStore, float $pieces): int { $productAvailability = ProductAvailability::NOT_IN_STORE; // Produkt je v dostatecnem mnozstvi skladem na prodejne if ($sellerInStore >= $pieces) { $productAvailability = ProductAvailability::IN_STORE; // Produkt je na prodejne jen v castecnem mnozstvi } elseif ($sellerInStore > 0) { $productAvailability = ProductAvailability::SELLER_IN_STORE_PARTIALLY; } // Produkt je dostupny na centralnim skladu if ($mainInStore > 0) { $productAvailability = max($productAvailability, ProductAvailability::SELLER_IN_STORE_TRANSFER); } return $productAvailability; } /** * Nacte ke vsem produktum v PurchaseStatu informace okolo skladu. * * @return ProductPurchaseItem[] */ public function getPurchaseStateProducts(PurchaseState $purchaseState, ?int $sellerId = null, bool $grouped = true, ?callable $customResultModifier = null): array { return $this->activateSeller( $sellerId, function () use ($purchaseState, $grouped, $customResultModifier) { $products = $purchaseState->createProductCollection(); // k PurchaseStatu si nactu informace o skladu $this->productUtil->fetchStoreInfo($products); if ($customResultModifier) { $customResultModifier($products); } return $grouped ? $this->getPurchaseStateProductsGrouped($purchaseState) : $purchaseState->getProducts(); } ); } /** * Vrací, zda je objednávka na osobní odběr (vyzvednutí na prodejně) nebo ne. */ public function isOrderInPerson(\Order $order): bool { if ($deliveryType = $order->getDeliveryType()) { if ($delivery = $deliveryType->getDelivery()) { if ($delivery instanceof \OdberNaProdejne || $delivery->isInPerson()) { return true; } } } return false; } /** * Vrací, zda je objednávka osobní odběr (vyzvednutí na prodejně) a prodejna má zároveň nastavený * centrální sklad jako svůj sklad. * * V tu chvíli se objednávka musí tvářit jako objednávka se závozem na prodejnu, jen tam nejsou poplatky atd.. * z pohledu FE se to tedy tváří jako klasická rezervace, ale z pohledu backendu se to musí tvářít jako rezervace se závozem. */ public function isOrderInPersonWithMainStore(\Order $order): bool { if ($this->isOrderInPerson($order)) { if ($sellerId = ($order->getDeliveryType()->getDelivery()->getPointId() ?: $order->getData('sellerId'))) { if ($seller = $this->sellerUtil->getSeller((int) $sellerId)) { if ($seller['id_store'] == $this->configuration->getMainStoreId()) { return true; } } } } return false; } /** * Vrací true nebo false na základě toho, jestli je v objednávce manipulační poplatek (položka s příplatkem). */ public function isTransportChargeInOrder(\Order $order): bool { foreach ($order->fetchItems() as $item) { if ($this->orderItemInfo->getItemType($item) === OrderItemInfo::TYPE_CHARGE) { if (in_array($item['note']['id_charge'] ?? null, self::CHARGES_HANDLING_FEE)) { return true; } } } return false; } public function getOrderSeller(\Order $order): ?array { if (!$this->sellerUtil) { return null; } if ($sellerId = ($order->getData('delivery_data')['seller_id'] ?? false)) { if ($seller = $this->sellerUtil->getSeller((int) $sellerId)) { return $seller; } } return null; } /** * Vrátí dárky, které jsou vybrány v košíku. */ private function getPurchaseStateGifts(PurchaseState $purchaseState, ?int $sellerId): array { $gifts = array_filter($purchaseState->getDiscounts(), fn ($x) => $x instanceof ProductPurchaseItem); foreach ($purchaseState->getProducts() as $item) { foreach ($item->getAdditionalItems() as $additionalItem) { if ($additionalItem instanceof ProductPurchaseItem) { $gifts[] = $additionalItem; } } } if (empty($gifts)) { return []; } $giftProducts = Mapping::mapKeys($gifts, fn ($k, $v) => [$v->getProduct()->id, $v->getProduct()]); $this->activateSeller($sellerId, fn () => $this->productUtil->fetchStoreInfo(new ProductCollection($giftProducts))); return Mapping::mapKeys($gifts, fn ($k, $v) => [$v->getProduct()->id, $v]); } /** * Vrátí produkty v PurchaseStatu zgroupované podle ID produkty a ID varianty. * * Je to hlavně kvůli košíku a kontrolám skladovosti / kontrole typu objednávky, protože můžu mít v košíku dva řádky * stejného produktu kvůli poznámce, kde si lidé říkájí o speifickou barvu, variantu... */ private function getPurchaseStateProductsGrouped(PurchaseState $purchaseState): array { $productsGrouped = []; foreach ($purchaseState->getProducts() as $item) { $key = $this->getPurchaseItemKey($item); // pokud uz produkt jednou mam, tak akorat aktualizuju pocet kusu if ($productsGrouped[$key] ?? false) { $itemCopy = new ProductPurchaseItem( $productsGrouped[$key]->getIdProduct(), $productsGrouped[$key]->getIdVariation(), $productsGrouped[$key]->getPieces() + $item->getPieces(), $productsGrouped[$key]->getPrice(), $productsGrouped[$key]->getNote(), $productsGrouped[$key]->getIdDiscount(), ); $itemCopy->setId( $productsGrouped[$key]->getId() ); if ($productsGrouped[$key]->getProduct()) { $itemCopy->setProduct($productsGrouped[$key]->getProduct()); } $productsGrouped[$key] = $itemCopy; continue; } $productsGrouped[$key] = $item; } return $productsGrouped; } /** * Zkopíruje položku objednávky do jiné objednávky. */ private function copyOrderItem(\Order $newOrder, int $oldItemId, ?int $newPieces = null): void { $item = sqlQueryBuilder() ->select('*') ->from('order_items') ->where(Operator::equals(['id' => $oldItemId])) ->execute()->fetchAssociative(); if (!$item) { return; } unset($item['id']); $item['id_order'] = $newOrder->id; if ($newPieces) { $item['pieces'] = $newPieces; $item['pieces_reserved'] = $newPieces; $item['total_price'] = toDecimal($item['piece_price'])->mul(toDecimal($newPieces)); } sqlQueryBuilder() ->insert('order_items') ->directValues($item) ->execute(); } /** * Vrací datum doručeni pro konkrétního prodejce - používá se v košíku. */ public function getSellerDeliveryDate(array $seller, int $availability): \DateTime { static $deliveryDate = null; static $deliveryDays = 0; $shipmentDate = null; // Pokud je vsechno skladem na prodejne, tak si to muzu vyzvednout uz dnes. // nebo pokud je to zavoz na prodejnu, ale prodejna ma nastaveny sklad jako Centralni sklad, tak je datum zavozu // v podstate ten samy den, protoze se pravdepodobne jedna o "Prodejnu", ktera se nachazi na hlavnim skladu if ($availability === ProductAvailability::IN_STORE) { // najdu nejblizsi datum, kdy si to tam budu moct vyzvednout $shipmentDate = $this->sellerUtil->getClosestOpenDate($seller, 3); } // loadnu si delivery date a delivery days dopravy OdberNaProdejne if ($deliveryDate === null) { $delivery = array_filter(\Delivery::getAll(), function ($d) { return $d instanceof \OdberNaProdejne; }); $delivery = reset($delivery); if ($delivery) { $deliveryDate = $delivery->getDeliveryDate() ?: new \DateTime(); $deliveryDays = $delivery->time_days ? (int) $delivery->time_days : 0; } else { $deliveryDate = new \DateTime(); } } if (!$shipmentDate) { // najdu nejblizsi datum zavozu $shipmentDate = $this->getSellerClosestShipmentDate($seller, $deliveryDate); } if (!empty($seller['data']['inventory']['from']) && !empty($seller['data']['inventory']['to'])) { try { $inventoryFrom = new \DateTime($seller['data']['inventory']['from']); $inventoryTo = (new \DateTime($seller['data']['inventory']['to']))->setTime(23, 59, 59); // pokud jsem se datumem trefil zrovna do inventury, tak musim posunout datum az za inventuru if ($shipmentDate >= $inventoryFrom && $shipmentDate <= $inventoryTo) { $shipmentDate = $this->getSellerClosestShipmentDate($seller, DateUtil::calcWorkingDays($deliveryDays, $inventoryTo->add(new \DateInterval('P1D')))); } } catch (\Throwable $e) { } } // Pokud budu na prodejnu zavazet a zaroven bude centralni sklad uzavren, tak musim posunout zavoz az na jiny termin if ($availability !== ProductAvailability::IN_STORE) { [$closedFrom, $closedTo] = $this->productUtil->getMainStoreClosedDates(); if (!empty($closedFrom) && !empty($closedTo)) { // Pokud je centrala zrovna uzavrena, tak zavoz posouvam if ($shipmentDate >= $closedFrom && $shipmentDate <= $closedTo) { $shipmentDate = $this->getSellerClosestShipmentDate($seller, DateUtil::calcWorkingDays($deliveryDays, $closedTo->add(new \DateInterval('P1D')))); } } } return $shipmentDate; } /** * Vrací nejbližší datum závozu z centrálního skladu na prodejnu. * * @param \DateTime $date - datum od kterého se má začít nejbližší datum pro závoz hledat */ private function getSellerClosestShipmentDate(array $seller, \DateTime $date): \DateTime { // ulozim si cislo dne v tydnu $dayIndex = $date->format('N'); $shipmentDate = clone $date; // pokud se zavazi ve stejny ten jako je shipmentDate, tak uz nemusim hledat jiny termin zavozu if ($seller['data']['shipment'][$dayIndex] ?? 0) { return $shipmentDate; } // najit nejblizsi den zavozu $closestDayIndex = null; foreach (range($dayIndex, 7) as $index) { // v dany den se nezavazi, takze pokracuju dal if (!($seller['data']['shipment'][$index] ?? 0)) { continue; } // pokud se zavasi, tak si ulozim cislo dnu v tydnu, kdy to bude if ($index >= $dayIndex) { $closestDayIndex = $index; break; } } // pokud jsem nenasel zadny termin zavozu, tak muzu byt napriklad u konce tydne a zavazi se jen zacatkem tydne // takze najdu prvni den zavozu v tydnu if ($closestDayIndex === null) { $shipmentDays = array_keys(array_filter($seller['data']['shipment'] ?? [], function ($x) { return !empty($x); })); $closestDayIndex = reset($shipmentDays); } if ($closestDayIndex) { // shipmentDate nastavim na datum, kdy bude dalsi zavoz (napr. next Monday, coz mi upravi datum tak, aby tam bylo pristi pondeli) $shipmentDate = $shipmentDate->modify('next '.$this->getDaysOfWeek()[$closestDayIndex]); } return $shipmentDate; } /** * Zvaliduje skladovost produktu vuci DataGo. */ public function checkPurchaseStateStockDataGo(PurchaseState $purchaseState): void { $items = []; // pripravim si pole polozek foreach ($purchaseState->getProducts() as $item) { if (empty($item->getProduct()->code)) { continue; } $items[] = [ 'number' => $item->getProduct()->code, 'quantity' => $item->getPieces(), ]; } if (empty($items)) { return; } $data = [ 'currency' => Contexts::get(CurrencyContext::class)->getActiveId(), 'items' => [ 'item' => $items, ], ]; [$user, $pass] = $this->pompoUtil->getDataGoDefaultCredentials(); $this->dataGoApi->setCredentials($user, $pass); // zkontroluju, ze je DataGo API dostupne, a pokud neni, tak neprovadim kontrolu if (!$this->dataGoApi->isApiAvailable()) { return; } try { // odesilam request na DataGo $result = $this->dataGoApi->checkItems($data); } catch (\Throwable $e) { if (isDevelopment()) { throw $e; } return; } if (ArrayUtil::isDictionary($result['items']['item'] ?? [])) { $result['items']['item'] = [$result['items']['item']]; } // process result $stockInfo = []; foreach ($result['items']['item'] ?? [] as $item) { $stockInfo[$item['number']] = $item['quantity']['total'] ?? 0; } // overuji dostupnost podle vysledku z DataGo foreach ($purchaseState->getProducts() as $item) { if (($stockInfo[$item->getProduct()->code] ?? false) === false) { continue; } $availableQuantity = $stockInfo[$item->getProduct()->code]; // neni na centranil skladu a ani u dodavatele if ($availableQuantity < $item->getPieces()) { // muze to ale byt produkt z marketplacu, takze potrebuju jeste zkontrolovat, zda neni skladem // u dodavatele kterej je pres marketplace if ($this->productUtil->isInStoreMarketplace($item->getProduct(), (float) $item->getPieces())) { continue; } throw new PompoCartException( // Produkt "%s" není v požadovaném množství skladem. sprintf(translate('productNotInStore', 'pompo'), $item->getName()) ); } } } /** * Zvaliduje skladovost vuci DRSu a DataGo. * * Vyplnene $sellerId znamena, ze objednavam na prodejnu. V opacnem pripade objednavam pres prepravce. * * 1. Pokud prijde $sellerId, tak nejdriv koukam na sklad dane prodejny a zbytky, ktere nejsou skladem potom overim vuci centrale (DataGo) * 2. Pokud neprijde $sellerId, tak validuju pouze vuci centrale (DataGo), protoze z prodejny se neda posilat dopravcem. */ public function checkPurchaseStateStock(PurchaseState $purchaseState, ?int $sellerId = null): void { if ($sellerId) { $branchId = sqlQueryBuilder() ->select('JSON_VALUE(s.data, "$.branchId")') ->from('stores', 's') ->join('s', 'sellers', 'sell', 's.id = sell.id_store') ->where(Operator::equals(['sell.id' => $sellerId])) ->execute()->fetchOne(); if (!$branchId) { return; } // kodu produktu v kosiku $productCodes = array_filter(array_map(function ($x) { return $x->getProduct() ? ($x->getProduct()->code ?? null) : null; }, $purchaseState->getProducts())); $drsStockStatus = []; // nactu si skladovost na prodejne foreach ($this->drsApi->getStock($productCodes, (string) $branchId) as $item) { $drsStockStatus[mb_strtolower($item['VART'])] = (int) ($item['Quantity'] ?? 0); } // kontroluju skladovost na prodejne $dataGoProducts = []; foreach ($purchaseState->getProducts() as $item) { if (!$item->getProduct()) { continue; } if (!($code = $item->getProduct()->code ?? null)) { continue; } $codeLower = mb_strtolower($code); // produkt neni skladem na prodejne if (($drsStockStatus[$codeLower] ?? false) === false) { // produkt budu jeste validovat vuci centrale $dataGoProducts[] = [ 'code' => $code, 'purchaseItem' => $item, 'pieces' => $item->getPieces(), ]; continue; } // produkt je skladem na prodejne, ale neni v dostatecnem mnozstvi if ($drsStockStatus[$codeLower] < $item->getPieces()) { // zbytek produktu zvaliduju jeste vuci centrale $dataGoProducts[] = [ 'code' => $code, 'purchaseItem' => $item, 'pieces' => ($item->getPieces() - $drsStockStatus[$codeLower]), ]; } } // zbytek zvaliduju vuci DataGo, protoze kdyz neco nemam na prodejne, tak to muzu mit na centrale a na // prodejnu se to muze prevezt if (!empty($dataGoProducts)) { $purchaseItems = []; foreach ($dataGoProducts as $item) { $originalPurchaseItem = $item['purchaseItem']; $purchaseItem = new ProductPurchaseItem( $originalPurchaseItem->getIdProduct(), $originalPurchaseItem->getIdVariation(), $item['pieces'], $originalPurchaseItem->getPrice(), $originalPurchaseItem->getNote(), $originalPurchaseItem->getIdDiscount() ); if ($originalPurchaseItem->getProduct()) { $purchaseItem->setProduct($originalPurchaseItem->getProduct()); } $purchaseItems[] = $purchaseItem; } // spustim validaci zbylych produktu vuci centrale $this->checkPurchaseStateStockDataGo( new PurchaseState($purchaseItems) ); } return; } // zvaliduju cely PurchaseState vuci centrale $this->checkPurchaseStateStockDataGo($purchaseState); } /** * Vrací ID PurchaseItemu složené z ID produktu a ID varianty. */ private function getPurchaseItemKey(ProductPurchaseItem $item): string { $key = $item->getIdProduct(); if ($item->getIdVariation()) { $key .= '/'.$item->getIdVariation(); } return (string) $key; } /** * Pomocná funkce, která vrací dny v týdnu. */ private function getDaysOfWeek(): array { return [ 1 => 'Monday', 2 => 'Tuesday', 3 => 'Wednesday', 4 => 'Thursday', 5 => 'Friday', 6 => 'Saturday', 7 => 'Sunday', ]; } /** * Pomocná funkce, která slouží k aktivaci konkrétní prodejny. */ private function activateSeller(?int $sellerId, callable $callback) { if (!$sellerId) { return $callback(); } $sellerContext = Contexts::get(SellerContext::class); $originalSellerId = $sellerContext->getActiveId(); $sellerContext->activate((string) $sellerId); $result = $callback(); $sellerContext->activate($originalSellerId); return $result; } }