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;
}
}