Files
kupshop/bundles/KupShop/POSBundle/Util/PosOrderUtil.php
2025-08-02 16:30:27 +02:00

404 lines
17 KiB
PHP

<?php
namespace KupShop\POSBundle\Util;
use KupShop\ContentBundle\View\Exception\ValidationException;
use KupShop\GraphQLBundle\ApiAdmin\Types\Order\OrderItem;
use KupShop\GraphQLBundle\ApiPos\Types\Order\PosOrder;
use KupShop\GraphQLBundle\ApiPos\Types\Order\PosOrderFinish;
use KupShop\GraphQLBundle\ApiPos\Types\Order\PosOrderResponse;
use KupShop\GraphQLBundle\ApiPos\Types\Purchase\PosPurchase;
use KupShop\GraphQLBundle\ApiPos\Types\User\PosUser;
use KupShop\GraphQLBundle\ApiPos\Types\Warehouse\PosOrderWarehouse;
use KupShop\GraphQLBundle\Exception\GraphQLNotFoundException;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Context\LanguageContext;
use KupShop\KupShopBundle\Context\PosContext;
use KupShop\KupShopBundle\Context\UserContext;
use KupShop\KupShopBundle\Exception\InvalidArgumentException;
use KupShop\KupShopBundle\Util\Database\QueryHint;
use KupShop\OrderDiscountBundle\Util\DiscountManager;
use KupShop\OrderingBundle\OrderList\OrderList;
use KupShop\OrderingBundle\Util\Order\OrderInfo;
use KupShop\OrderingBundle\Util\Order\OrderUtil;
use KupShop\OrderingBundle\Util\Purchase\PurchaseUtil;
use KupShop\POSBundle\Event\PosOrderEvent;
use KupShop\WarehouseBundle\Util\StoreItemWorker;
use Query\Operator;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class PosOrderUtil
{
public function __construct(
// Contexts
protected PosContext $posContext,
protected CurrencyContext $currencyContext,
protected UserContext $userContext,
protected LanguageContext $languageContext,
// Utils
protected PosUtil $posUtil,
protected OrderList $orderList,
protected PurchaseUtil $purchaseUtil,
protected DiscountManager $discountManager,
protected OrderUtil $orderUtil,
protected EventDispatcherInterface $eventDispatcher,
protected PosPurchaseStateUtil $posPurchaseStateUtil,
protected PosPaymentUtil $posPaymentUtil,
protected ?PosWarehouseUtil $posWarehouseUtil = null,
protected ?StoreItemWorker $storeItemWorker = null,
) {
}
public function getOrder(?int $id, ?string $code): PosOrder
{
$posEntity = $this->posContext->getActive();
try {
if ($id) {
$spec = Operator::equals(['o.id' => $id]);
} elseif ($code) {
$spec = Operator::equals(['o.order_no' => $code]);
} else {
throw new GraphQLNotFoundException('Objednávka nebyla nalezena.');
}
$orders = $this->orderList->andSpec($spec)
->fetchItems()
->getOrders();
if (!($order = $orders->current())) {
throw new InvalidArgumentException(sprintf('Order with CODE "%s" was not found!', $code));
}
$order->productList->fetchPhotos('product_gallery', ['Y', 'N']);
$order->productList->fetchStoresInStore(findModule(\Modules::PRODUCTS_VARIATIONS));
} catch (\InvalidArgumentException $e) {
throw new GraphQLNotFoundException('Objednávka nebyla nalezena.');
}
return new PosOrder(
order: $order,
posEntity: $posEntity,
warehouse: $this->getWarehouseOrderData($order),
payments: $this->posPaymentUtil->getOrderPayments($order->id, $order),
collection: $orders
);
}
private function getWarehouseOrderData(\Order $order): ?PosOrderWarehouse
{
if (!findModule(\Modules::WAREHOUSE)) {
return null;
}
return QueryHint::withRouteToMaster(function () use ($order) {
$warehouseOrder = $this->storeItemWorker->getWarehouseOrder($order->id);
return new PosOrderWarehouse(
warehouseOrder: $warehouseOrder ?: null,
errors: ($warehouseOrder['id'] ?? false) ? $this->storeItemWorker->checkOrderMatchesBoxContent($warehouseOrder['id']) : []
);
});
}
public function recalculateAndUpdateOrder(
array $orderItems,
array $couponItems,
array $newProducts,
array $newCoupons,
?int $appliedBonusPoints,
?PosUser $user,
?int $idOrder,
bool $savePurchase,
string $methodPayment,
string $paidPrice,
bool $purchase,
): PosPurchase {
$posEntity = $this->posContext->getActive();
$loadedCoupons = [];
$loadedProducts = [];
$posOrderResponse = null;
$saved = false;
// Loads products by scanned bar codes
$this->mergeScannedProductsToOrderItemsApi($newProducts, $orderItems, $loadedProducts);
// Create purchase state from API
$purchaseState = $this->posPurchaseStateUtil->createStateFromApi(
$orderItems,
$couponItems,
$user,
$methodPayment,
$paidPrice,
$appliedBonusPoints,
$idOrder === null
);
// Save state to order
if ($savePurchase) {
// Add POS create data
$data = $purchaseState->getCustomData();
$data['type'] = 'POS';
$data['IDPos'] = $posEntity->getId();
// Paid by invoice (skip default delivery select)
$delivery_type = null;
if (($data['methodPayment'] ?? false) && $data['methodPayment'] != 'UNDEFINED') {
$delivery_type = $this->posUtil->getPosDeliveryType($data['IDPos'], $data['methodPayment']);
}
// Sets first not null delivery
if (empty($delivery_type)) {
if ($posEntity->getCashDeliveryType()) {
$delivery_type = $this->posUtil->getPosDeliveryType($data['IDPos'], 'CASH');
} elseif ($posEntity->getCardDeliveryType()) {
$delivery_type = $this->posUtil->getPosDeliveryType($data['IDPos'], 'CARD');
} elseif ($posEntity->getInvoiceDeliveryType()) {
$delivery_type = $this->posUtil->getPosDeliveryType($data['IDPos'], 'INVOICE');
} elseif ($posEntity->getCustomDeliveryType()) {
$delivery_type = $this->posUtil->getPosDeliveryType($data['IDPos'], 'CUSTOM');
}
}
if (is_null($delivery_type)) {
throw new ValidationException('Není nastaven žádný způsob doručení');
}
$purchaseState->setDeliveryTypeId($delivery_type['id']);
$data['order']['id_delivery'] = $delivery_type['id'];
$data['order']['delivery_type'] = $delivery_type['name'];
$order = null;
// Pokud má objednávka již nastavený způsob doručení, tak editace to už nezmění (aktulizuje to dopravu a může to změnit cenu)
if ($idOrder) {
$order = new \Order();
$order->createFromDB($idOrder);
if ($order->getDeliveryId()) {
$purchaseState->setDeliveryTypeId($order->getDeliveryId());
$data['order']['id_delivery'] = $order->getDeliveryId();
$data['order']['delivery_type'] = $order->delivery_type;
}
} else {
// Zdroj nastavuju pouze na nové objednávce
$data['order']['source'] = OrderInfo::ORDER_SOURCE_POS;
}
$data['order']['pos'] = $posEntity->getId();
$data['order']['flags'] = 'POS';
$purchaseState->setCustomData($data);
// Final stock check before creating an order
$event = new PosOrderEvent($purchaseState, $posEntity->getId(), $order, $idOrder == null);
$this->eventDispatcher->dispatch($event, PosOrderEvent::PURCHASE_STATE_CHECK);
// Creates order from PurchaseState
$order = $this->purchaseUtil->createOrderFromPurchaseState($purchaseState, $idOrder);
// Final stock check before creating an order
$event->setOrder($order);
$this->eventDispatcher->dispatch($event, PosOrderEvent::PURCHASE_STATE_ORDER_CREATED);
// Creates payment with status CREATED (save purchase comes only on new order)
$uuidPayment = $this->posPaymentUtil->createOrderPayment($order, $order->getPurchaseState(), $purchase);
// Loading data results from order
$items = $this->posPurchaseStateUtil->getPurchaseItemsFromOrder($posEntity, $order);
$priceWithoutVat = $order->getTotalPrice()->getPriceWithoutVat();
$priceWithVat = $order->getTotalPrice()->getPriceWithVat();
$posOrderResponse = new PosOrderResponse(
idOrder: $order->id,
noOrder: $order->order_no,
isPaid: $order->isPaid(),
remainingPayment: $order->getRemainingPayment(),
status: $order->status,
uuidPayment: $uuidPayment,
posOrderWarehouse: $this->getWarehouseOrderData($order),
posOrderPayments: $this->posPaymentUtil->getOrderPayments($order->id, $order)
);
$saved = true;
} else {
// Loading data results from purchase state
$items = $this->posPurchaseStateUtil->getPurchaseItemsFromPurchaseState($posEntity, $purchaseState);
$priceWithoutVat = $purchaseState->getTotalPrice()->getPriceWithoutVat();
$priceWithVat = $purchaseState->getTotalPrice()->getPriceWithVat();
}
return new PosPurchase(
items: $items,
discounts: $this->posPurchaseStateUtil->getDiscountItemsFromPurchaseState($purchaseState),
usedDiscounts: $purchaseState->getUsedDiscounts(),
charges: $this->posPurchaseStateUtil->getChargesFromPurchaseState($purchaseState),
priceWithoutVat: $priceWithoutVat,
priceWithVat: $priceWithVat,
loadedProducts: $loadedProducts,
loadedCoupons: $loadedCoupons,
posOrderResponse: $posOrderResponse,
saved: $saved,
);
}
private function mergeScannedProductsToOrderItemsApi(array $newProducts, array &$orderItems, array &$loadedProducts): void
{
if ($newProducts) {
foreach ($newProducts as $code) {
if ((int) $code) {
$ean = (int) $code;
}
$sqlProduct = sqlQueryBuilder()
->select('p.id as product_id, pv.id as variation_id, p.title as product_title, pv.title as variant_title, v.vat, COALESCE(pv.price, p.price) as price')
->from('products', 'p')
->leftJoin('p', 'products_variations', 'pv', 'pv.id_product=p.id')
->leftJoin('p', 'vats', 'v', 'p.vat=v.id');
if (findModule(\Modules::SUPPLIERS)) {
$sqlProduct->addSelect('COALESCE(pos.ean, pv.ean, p.ean) as ean')
->leftJoin('p',
'products_of_suppliers',
'pos',
'pos.id_product=p.id AND (pos.id_variation = pv.id OR pv.id IS NULL)')
->andWhere('p.code LIKE :code');
if ($ean ?? false) {
$sqlProduct->orWhere('p.ean = :ean OR pv.ean = :ean OR pos.ean = :ean');
}
} else {
$sqlProduct->addSelect('COALESCE(pv.ean, p.ean) as ean')
->andWhere('p.code LIKE :code');
if ($ean ?? false) {
$sqlProduct->orWhere('p.ean = :ean OR pv.ean = :ean');
}
}
$sqlProduct->setParameter('code', "%{$code}%");
if ($ean ?? false) {
$sqlProduct->setParameter('ean', $ean);
}
if (findModule(\Modules::PRODUCTS_VARIATIONS, \Modules::SUB_CODE)) {
$sqlProduct->orWhere('pv.code LIKE :code')
->addSelect('COALESCE(pv.code, p.code) as code');
} else {
$sqlProduct->addSelect('p.code');
}
$sqlProduct = $sqlProduct->groupBy('p.id')->execute()->fetch();
if (!empty($sqlProduct)) {
$found = null;
$found_key = null;
foreach ($orderItems as $key => $oItem) {
if ($oItem->item->getItem()['idProduct'] == $sqlProduct['product_id'] && $oItem->item->getItem()['idVariation'] == $sqlProduct['variation_id']) {
$found_key = $key;
$found = true;
}
}
$orderItems[] = new OrderItem(
new \KupShop\OrderingBundle\Entity\Order\OrderItem(
[
'idProduct' => $sqlProduct['product_id'],
'idVariation' => $sqlProduct['variation_id'],
'title' => ($sqlProduct['variation_id']) ? "{$sqlProduct['product_title']} - {$sqlProduct['variant_title']}" : "{$sqlProduct['product_title']}",
'pieces' => ($found) ? $orderItems[$found_key]->item->getItem()['pieces'] + 1 : 1,
'vat' => $sqlProduct['vat'],
'priceWithoutVat' => $sqlProduct['price'],
'discount' => null,
]
)
);
unset($orderItems[$found_key]);
$loadedProducts[] = $code;
}
}
}
}
public function finishOrder($orderID): PosOrderFinish
{
$requiredStatus = PosUtil::getHandledOrderStatus();
$order = new \Order();
$order->createFromDB($orderID);
$order->fetchItems();
// Nelze vyřídit objednávku 2x, špatně by de odebíraly zásoby z pozic
if ($order->status === $requiredStatus) {
throw new ValidationException('Nelze znovu vyřídit již dokončenou objednávku');
}
sqlGetConnection()->transactional(function () use (&$order, $requiredStatus) {
// Přepne status objednávky na status vyřízení (podle nastavení)
$order->changeStatus(
$requiredStatus,
'Vyřízeno v pokladně '.date(\Settings::getDateFormat().' '.\Settings::getTimeFormat(), time()),
false,
);
if (findModule(\Modules::WAREHOUSE)) {
$posEntity = $this->posContext->getActive();
$warehouseOrder = $this->storeItemWorker->getWarehouseOrder($order->id);
// Pokud má objednávka datum vychystání, tak v pokladně nejde editovat -> je vychystána
// Kvůli dokončení se nemůžou znovu přesunout zásoby a proto se přesuny mezi boxy přeskakují
if (!isset($warehouseOrder['date_close'])) {
// Box v případě že není předvychystaný box, tak se použije virtuální
$idBox = $warehouseOrder['id_position'] ?? $posEntity->getVirtualBox();
// Najde se rozdíl položek mezi boxem a objednávkou
$warehouseDiff = $this->storeItemWorker->checkOrderMatchesBoxContent(
id_warehouse_order: $warehouseOrder['id'] ?? null,
id_order: $order->id,
id_box: $idBox
);
// Pokud není záznam objednávky ve warehouse_order tak ho vytvořím (nákup)
if (is_null($warehouseOrder['id'] ?? null)) {
$this->posWarehouseUtil->createWarehouseOrderRecord($order);
}
// Vyrovnání boxu, tak aby obsah odpovídal položkám objednávky
$this->posWarehouseUtil->updateBoxToMatchOrder(
order: $order,
warehouseDiffs: $warehouseDiff,
idBox: $idBox
);
// Vysype box ven
$this->storeItemWorker->cleanCompletedBox($idBox);
}
}
});
return new PosOrderFinish(
idOrder: $order->id,
status: $order->status
);
}
public function stornoOrder($orderID): PosOrderFinish
{
$order = new \Order();
$order->createFromDB($orderID);
if ($order->order_no ?? false) {
if (!$order->isActive()) {
throw new ValidationException('Objednávka není aktivní');
}
if ($order->isClosed()) {
throw new ValidationException('Objednávka je již uzavřena');
}
if (findModule(\Modules::ORDER_PAYMENT) && abs($order->getPayments()) > 1) {
throw new ValidationException('Objednávku nelze stronovat, protože k ní existuje platba, která doposud nebyla vrácena! Nejprve vraťte platby');
}
$order->storno();
} else {
throw new ValidationException('Objednávka nebyla nalezena');
}
return new PosOrderFinish(
idOrder: $order->id,
status: $order->status_storno,
);
}
}