first commit

This commit is contained in:
2025-08-02 16:30:27 +02:00
commit 23646bfcee
14851 changed files with 1750626 additions and 0 deletions

800
admin/pos.pos.php Normal file
View File

@@ -0,0 +1,800 @@
<?php
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
use KupShop\OrderingBundle\Event\OrderEvent;
use KupShop\OrderingBundle\Event\OrderItemEvent;
use KupShop\WarehouseBundle\Util\StoreItemWorker;
use Query\Operator;
$main_class = 'PosPos';
class PosPos extends Window
{
use DatabaseCommunication;
protected $template = 'window/pos.pos.tpl';
/** @var PosCart pos cart object */
protected $cart;
/** @var Order order object */
protected $order;
protected $id_order;
/** @var User user object */
protected $user;
/** @var ArrayAccess error */
protected $result_info = [
'type' => null,
'msg' => null,
];
public function get_vars()
{
$vars = parent::get_vars();
$this->loadUser();
$this->id_order = getVal('id_order');
if (empty($this->id_order) && (isset($this->order) || is_null($this->order)) && !is_a($this->order, 'Order')) {
$this->cart = null;
$this->createCart();
$vars['cart'] = $this->cart;
} else {
$this->getCompleteOrder();
$vars['cart'] = $this->order;
}
$vars['display'] = getVal('display');
$vars['user'] = $this->user;
$vars['discount'] = (!empty($ctrl['dealerPricelevel']['discount'])) ? $ctrl['dealerPricelevel']['discount'] : 0;
$vars['coupon'] = (!empty($_SESSION['coupon'])) ? $_SESSION['coupon'] : '';
$vars['vats'] = PosCart::getVats();
$vars['result_info'] = $this->getResultInfo();
global $adminName;
if (!empty($adminName)) {
$vars['admin_name'] = $adminName;
}
if (findModule(Modules::WAREHOUSE) && !empty($vars['cart']->products)) {
$StoreItemWorker = ServiceContainer::getService(StoreItemWorker::class);
foreach ($vars['cart']->products as &$product) {
$positions = $StoreItemWorker->getProductPositions(
Operator::equalsNullable(['p.id' => $product->id, 'pv.id' => $product->id_variation])
);
if (is_array($product)) {
$product['positions'] = $positions;
} else {
$product->positions = $positions;
}
}
}
return $vars;
}
public function handle()
{
$this->loadUser();
try {
parent::handle();
} catch (POSInternalException $e) {
$this->posHandleException($e);
$msg = $e->getMessage();
$this->addErrorInfo(self::RESULT_TYPE_ERROR, $msg);
header('pos-error: '.urlencode($msg));
} catch (Exception $e) {
// Doctrine\DBAL\SQLParserUtilsException
$this->posHandleException($e);
$this->result_info = 'Chyba! Vývojáři byli informováni. Zkuste to za okamžik znovu.';
}
}
protected function posHandleException(Exception $e)
{
$sentry = getRaven();
$sentry->captureMessage('POS error', [], ['extra' => [
'request' => $_REQUEST,
'error_message' => $e->getMessage(),
'error_except' => $e,
]]);
}
public function handleCheckCart()
{
}
public function handleAddProduct()
{
$id_product = getVal('id_product');
$id_variation = (getVal('id_variation') != '') ? getVal('id_variation') : null;
$code = getVal('ean');
$ean = intval($code);
$find_by = '';
if (!$id_product && $ean) {
$query = sqlQueryBuilder()
->select('p.title as label, p.id as id_product, pv.id as id_variation')
->from('products', 'p')
->leftJoin('p', 'products_variations', 'pv', 'pv.id_product=p.id');
if (findModule(Modules::SUPPLIERS)) {
$query->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}%' OR p.ean LIKE '{$ean}' OR pv.ean LIKE '{$ean}' OR pos.ean LIKE '{$ean}'");
} else {
$query->addSelect('COALESCE(pv.ean, p.ean) as ean')
->andWhere("p.code LIKE '%{$code}%' OR p.ean LIKE '{$ean}' OR pv.ean LIKE '{$ean}'");
}
if (!empty($cfg['Modules']['products_variations']['variationCode'])) {
$query->orWhere("pv.code LIKE '%{$code}%'")
->addSelect('COALESCE(pv.code, p.code) as code');
} else {
$query->addSelect('p.code');
}
$SQL = $query->groupBy('p.id')
->execute()
->fetchAll();
if (sizeof($SQL) == 0) {
$this->addErrorInfo(self::RESULT_TYPE_ERROR, "Tento EAN/KOD ({$ean}) není přiřazen k žádnému produktu");
return;
} elseif (sizeof($SQL) > 1) {
$this->addErrorInfo(self::RESULT_TYPE_ERROR, "Existuje více produktů s tímto EAN/KOD ({$ean}), vložte ručně");
return;
} else {
$row = $SQL[0];
if ($ean == $row['code']) {
$find_by = ' podle kódu '.$row['code'];
} elseif ($ean == $row['ean']) {
$find_by = ' podle eanu '.$row['code'];
}
$id_product = $row['id_product'];
if (!empty($row['id_variation'])) {
$id_variation = $row['id_variation'];
}
}
}
$this->id_order = getVal('id_order');
if ($id_product) {
if (!$this->id_order) {
$this->createCart();
$item = ['id_product' => $id_product, 'id_variation' => $id_variation, 'pieces' => 1];
$res = $this->cart->addItem($item);
} else {
$this->order = new Order($this->id_order);
$this->order->createFromDB($this->id_order);
$this->order->fetchItems();
$existing_item = array_filter($this->order->items, function ($i) use ($id_product, $id_variation) { return ($i['id_product'] == $id_product) && ($i['id_variation'] == $id_variation); });
if (!empty($existing_item)) {
$existing_item = reset($existing_item);
$res = $this->order->updateItem($existing_item['id'], $existing_item['pieces'] + 1);
} else {
$res = $this->order->insertItem($id_product, $id_variation, 1);
$product = new Product($id_product);
$dispatcher = ServiceContainer::getService('event_dispatcher');
$event = $dispatcher->dispatch(
new OrderItemEvent($product, $id_variation, null, 1, null, $this->order),
OrderItemEvent::ITEM_UPDATED
);
}
$this->order->recalculate();
$this->editOrderEvent();
}
$this->addErrorInfo($res, $res == 1 ? 'Přidáno'.$find_by : $res);
}
}
public function handleAddNonProduct()
{
$userKey = Cart::getCartID();
$price = getVal('price');
$params = [
'id' => getVal('item_id'),
'pieces' => getVal('pieces'),
'discount' => getVal('product_discount'),
'title' => getVal('title'),
'piece_price' => $this->preparePrice($price),
];
$this->createCart();
$this->id_order = getVal('id_order');
if (empty($this->id_order)) {
$price = getVal('piece_price', $params, '');
$price = $this->preparePrice($price);
$price_with_vat = roundPrice($price);
$vat = PosCart::getVatByID(getVal('vat', null, 0));
$price = $price_with_vat->removeVat($vat)->asFloat();
$res = $this->cart->addNonItem([
'date' => date('Y-m-d H:i:s'),
'id_product' => 0,
'id_variation' => null,
'pieces' => 1,
'user_key' => $userKey,
'note' => json_encode([
'title' => getVal('title'),
'piece_price' => $price,
'vat' => $vat,
'discount' => 0,
]),
]
);
$this->addErrorInfo($res, 'Přidáno.');
} else {
$this->order = new Order($this->id_order);
$this->order->createFromDB($this->id_order);
$vat = PosCart::getVatByID(getVal('vat', null, 0));
$price = toDecimal($params['piece_price'])->removeVat($vat);
$this->order->insertNonItem($this->id_order, $price, $price, 1, $vat, $params['title']);
$this->order->recalculate();
$this->editOrderEvent();
$this->addErrorInfo(self::RESULT_TYPE_OK, 'Přidáno.');
}
}
public function handleChangeItem()
{
$price = getVal('price');
$params = [
'id' => getVal('item_id'),
'pieces' => getVal('pieces'),
'discount' => getVal('product_discount'),
'title' => getVal('title'),
'piece_price' => $this->preparePrice($price),
];
$this->createCart();
$this->id_order = getVal('id_order');
if (empty($this->id_order)) {
$item = sqlFetchAssoc($this->selectSQL('cart', ['id' => $params['id']], ['id', 'pieces', 'note']));
$item = json_decode($item['note'], true);
$price_with_vat = roundPrice($item['piece_price']);
$row['piece_price'] = $price_with_vat->removeVat($item['vat'])->asFloat();
$params['note'] = json_encode([
'piece_price' => $item['piece_price'],
'vat' => $item['vat'],
'title' => $item['title'],
'discount' => $params['discount'],
]);
$res = $this->cart->updateNonItem($params);
$this->addErrorInfo($res, $res ? 'Změněno.' : $res);
} else {
$this->order = new Order($this->id_order);
$this->order->createFromDB($this->id_order);
$this->order->updateItem($params['id'], $params['pieces']);
$order_item = sqlQueryBuilder()->select('*')->from('order_items')->where('id=:item_id')->setParameter('item_id', $params['id'])->execute()->fetch();
$old_discount = getVal('discount', $order_item, 0);
$price = toDecimal($order_item['piece_price'])->removeDiscount($old_discount);
$price_with_vat = calcPrice($price, $order_item['tax'], $params['discount']);
$price_with_vat_rounded = roundPrice($price_with_vat);
$price_without_vat = $price_with_vat_rounded->removeVat($order_item['tax']);
$res = $this->updateSQL('order_items',
[
'piece_price' => $price_without_vat,
'total_price' => $price_without_vat->mul(toDecimal($order_item['pieces'])),
'discount' => $params['discount'],
], ['id' => $params['id']]);
$this->order->recalculate();
$this->editOrderEvent();
$this->addErrorInfo($res, 'Změněno.');
}
}
public function handleDeleteItem()
{
$item_id = getVal('item_id');
$this->id_order = getVal('id_order');
if (empty($this->id_order)) {
$this->createCart();
$res = $this->cart->deleteItem($item_id);
} else {
$this->order = new Order($this->id_order);
$this->order->createFromDB($this->id_order);
$res = $this->order->deleteItem($item_id);
$this->editOrderEvent();
}
$this->addErrorInfo($res, 'Položka odebrána.');
}
public function handleCreateOrder($submitCart = false)
{
$this->createCart();
global $dbcfg;
$this->cachedbcfg();
$dbcfg->order_send_received_mail = 'N';
$dbcfg->order_send_status_mail = 'N';
$dbcfg->order_availability = Settings::ORDER_AVAILABILITY_ALL;
$this->order = $this->cart->submitOrder();
if (is_a($this->order, 'Order')) {
$this->order->addFlag('POS');
if (!empty($this->user->id)) {
$this->order->assignUser($this->user->id);
$this->order->id_user = $this->user->id;
$eventDispatcher = ServiceContainer::getService('event_dispatcher');
$event = new OrderEvent($this->order);
$eventDispatcher->dispatch($event, OrderEvent::ORDER_COMPLETE);
}
$this->cachedbcfg(true);
$this->addErrorInfo(self::RESULT_TYPE_OK, 'Objednávka vytvořena.');
return true;
} else {
$this->addErrorInfo(self::RESULT_TYPE_ERROR, $this->getErrorMsg($this->order));
}
$this->addErrorInfo(self::RESULT_TYPE_ERROR, 'Nedefinovaná chyba vytváření objednávky');
}
protected function cachedbcfg($return = false)
{
global $dbcfg;
static $dbcfg_backup = null;
if ($dbcfg_backup) {
$dbcfg = $dbcfg_backup;
} else {
$dbcfg_backup = $dbcfg;
}
}
/**
* @throws Throwable
*/
public function handlePay()
{
global $adminID;
$this->id_order = getVal('id_order');
if (!$this->id_order) {
sqlGetConnection()->setNestTransactionsWithSavepoints(true);
sqlGetConnection()->transactional(function () {
$create_order = $this->handleCreateOrder(true);
if ($create_order !== true) {
$this->addErrorInfo(self::RESULT_TYPE_ERROR, 'Vytvoření nákupu selhalo. Zkuste to za okamžik znovu.');
return;
}
$this->id_order = $this->order->id;
});
}
$result = $this->checkBuyingProcess();
if (!$result) {
return;
}
if (!empty($this->id_order)) {
$this->order = new Order($this->id_order);
$this->order->createFromDB($this->id_order);
$payment_method = getVal('method');
$descr = 'Nákup č. '.$this->order->order_no;
$value = $this->order->getRemainingPayment();
if ($payment_method == Payment::METHOD_INVOICE || empty($value)) {
$payed = true;
$this->addErrorInfo(self::RESULT_TYPE_OK, 'Vše proběhlo v pořádku.');
} else {
$payed = $this->order->insertPayment($value, $descr, date('Y-m-d H:i'), true, $payment_method);
sqlQuery('UPDATE IGNORE order_payments SET admin=:admin WHERE id_order=:id_order ORDER BY id DESC LIMIT 1', ['id_order' => $this->order->id, 'admin' => $adminID]);
$this->addErrorInfo(self::RESULT_TYPE_OK, 'Zaplaceno.');
}
if ($payed) {
$this->changeOrderStatus();
try {
$this->updateOrderDeliveryType($payment_method, $this->order);
} catch (Exception $e) {
$this->posHandleException($e);
$this->addErrorInfo(self::RESULT_TYPE_ERROR, 'Chyba převodu objednávky do správného způsobu doručení podle platby.');
}
} else {
$this->addErrorInfo(self::RESULT_TYPE_ERROR, 'Nákup byl již zaplacen.');
}
} else {
$this->addErrorInfo(self::RESULT_TYPE_ERROR, 'Chyba! Platba nemá vazbu na objednávku.');
}
}
public function updateOrderDeliveryType($payment_method, Order $order)
{
$delivery_type = PosCart::findDeliveryTypeByPayMethod($payment_method);
$this->updateSQL('orders', [
'delivery_type' => $delivery_type['name'],
'id_delivery' => $delivery_type['id'],
], ['id' => $order->id]);
}
public function handleOrderLoad()
{
$this->id_order = getVal('id_order');
if ($this->id_order) {
$this->order = new Order($this->id_order);
$this->getCompleteOrder();
$this->addErrorInfo(self::RESULT_TYPE_OK, 'Načteno.');
return;
} else {
$this->clearSessions();
}
$this->addErrorInfo(self::RESULT_TYPE_OK, 'Odebráno.');
}
public function handleUserLoad()
{
$this->id_order = getVal('id_order');
$user_id = getVal('user_id');
if ($this->id_order) {
$this->order = new Order($this->id_order);
$this->order->assignUser($user_id > 0 ? $user_id : null);
if ($user_id == -1) {
$this->updateSQL('orders', ['invoice_name' => 'Kamenný obchod'], ['id' => $this->id_order]);
}
$this->getCompleteOrder();
$this->addErrorInfo(self::RESULT_TYPE_OK, 'Načteno.');
return;
} else {
$this->clearSessions();
}
$this->addErrorInfo(self::RESULT_TYPE_OK, 'Odebráno.');
}
public function handleAddCoupon()
{
$this->id_order = getVal('id_order');
if (empty($this->id_order)) {
$coupon = getVal('coupon_number');
// Pro odsrtaňování kuponu mu stačí dát ID -1
if (!empty($coupon)) {
if ($coupon == '-1') {
unset($_SESSION['coupon']);
} else {
$_SESSION['coupon'] = $coupon;
}
}
// kontrola existujícího kupónu je zajištěna autocompletem
// po přidání do session se při vytváření objektu košíku sama třída košíku postará o přidání kupónu
$this->createCart();
}
// Kupón nelze přidávat k již vytvořené objednávce, lze pouze do košíku
}
protected function clearSessions()
{
unset($_SESSION['priceLevel']);
unset($_SESSION['coupon']);
unset($_SESSION['pos_user_id']);
}
public function handleClearOrder()
{
$this->clearSessions();
$cart = new PosCart();
$cart->setPurchaseUtil(ServiceContainer::getService(\KupShop\OrderingBundle\Util\Purchase\PurchaseUtil::class));
$cart->clear();
}
public function handleExtendSession()
{
echo 'ok';
}
public function handleStorno()
{
$this->id_order = getVal('id_order');
if (!empty($this->id_order)) {
$this->order = new Order($this->id_order);
$this->getCompleteOrder();
}
}
public function handleMoveMoney()
{
$value = getVal('value');
$descr = getVal('descr');
global $adminID;
if (!empty($value)) {
if ($value > 0) {
$method = 4;
} else {
$method = 5;
}
$fields = ['price' => $value,
'method' => $method,
'note' => $descr,
'date' => date('Y-m-d H:i:s'),
'admin' => !empty($adminID) ? $adminID : null,
];
$ret = $this->insertSQL('order_payments', $fields);
if ($ret) {
$this->addErrorInfo(self::RESULT_TYPE_OK, ($value > 0) ? 'Peníze vloženy' : 'Peníze vybrány');
} else {
$this->addErrorInfo(self::RESULT_TYPE_ERROR, 'Chyba operace. Zkuste to za okamžik znovu.');
}
}
}
protected function createCart()
{
global $cfg;
if (empty($this->cart)) {
$this->cart = new PosCart();
}
$this->cart->setPurchaseUtil(ServiceContainer::getService(\KupShop\OrderingBundle\Util\Purchase\PurchaseUtil::class));
$this->cart->createFromDB();
if (!empty($_SESSION['coupon'])) {
$this->cart->addCoupon($_SESSION['coupon']);
}
if (empty($cfg['Modules']['pos']['id_transport'])) {
throw new RuntimeException('Missing pos id_transport in config');
} else {
$this->cart->setTransport($cfg['Modules']['pos']['id_transport']);
}
if (!empty($ctrl['dealerPricelevel']['discount'])) {
$this->cart->discounts = $ctrl['dealerPricelevel']['discount'];
}
}
protected function getCompleteOrder()
{
if (!$this->order) {
$this->order = new Order($this->id_order);
$this->order->createFromDB($this->id_order);
}
$this->order->fetchItems();
$this->order->createFromDB($this->order->id);
if ($this->order->id_user) {
$this->user = User::createFromId($this->order->id_user);
$this->user->activateUser();
$userContext = \KupShop\KupShopBundle\Util\Contexts::get(\KupShop\KupShopBundle\Context\UserContext::class);
$userContext->activate($this->user);
}
$this->order['totalPricePayNoVat'] = toDecimal(0);
$this->order['totalPriceWithVat'] = toDecimal($this->order->total_price);
$this->order['totalPricePay'] = toDecimal($this->order->getRemainingPayment());
$this->order->products = [];
// Z důvodu sjednocení - košík používá "products", kdežto objednávka "items" , to samé s price, title, ..
foreach ($this->order->items as &$item) {
$item['title'] = $item['descr'];
$this->order['totalPricePayNoVat'] = $this->order['totalPricePayNoVat']->add($item['value_without_vat_no_rounding']->mul(toDecimal($item['pieces'])));
// mozna neni potreba - pripadne odstranit
$item['price']['price_with_vat'] = $item['value_with_vat'];
$item['price']['price_without_vat'] = $item['value_without_vat'];
$item['price']['value_with_vat'] = $item['value_with_vat'];
$item['price']['value_without_vat'] = $item['value_without_vat'];
$item['totalPrice']['value_with_vat'] = $item['total_price']['value_with_vat'];
$item['totalPrice']['value_without_vat'] = $item['total_price']['value_without_vat'];
$item['inStore'] = $item['in_store'];
$this->order->products[$item['id']] = $item;
$this->order->products[$item['id']]['id'] = $item['id_product'];
}
unset($this->order->items);
}
protected function getDefaultCtrl()
{
return [
'id_level' => 0,
'discount' => 0,
'discount_discount' => 0,
'add' => false,
'title' => 'Pokladna',
'unit' => 'perc',
'sections' => [],
'products' => [],
'producers' => [],
];
}
protected function loadUser()
{
global $ctrl;
$userKey = Cart::getCartID();
// Kvůli classCart, která načítá uživatele z ctrl['id']
$ctrl['id'] = null;
$user_id = getVal('user_id', null, getVal('pos_user_id', $_SESSION, null));
if (!empty($user_id)) {
$_SESSION['pos_user_id'] = $user_id;
}
$ctrl['dealerPricelevel'] = null;
if ($user_id == -1) {
$this->user = null;
}
if ($user_id > 0) {
$this->user = User::createFromId($user_id);
if ($this->user) {
$this->user->activateUser();
}
$userContext = \KupShop\KupShopBundle\Util\Contexts::get(\KupShop\KupShopBundle\Context\UserContext::class);
$userContext->activate($this->user);
}
}
public const RESULT_TYPE_OK = 1;
public const RESULT_TYPE_ERROR = 0;
/**
* @return ArrayAccess
*/
protected function getResultInfo()
{
return $this->result_info;
}
protected function addErrorInfo($type, $msg)
{
if (!empty($type)) {
$this->result_info['type'] = self::RESULT_TYPE_OK;
$this->result_info['msg'] = $msg;
} else {
$this->result_info['type'] = self::RESULT_TYPE_ERROR;
$this->result_info['msg'] = 'Chyba: '.(($type) ? $type : $msg);
}
}
protected function editOrderEvent()
{
$this->order->createFromDB($this->id_order);
$this->order->fetchItems();
$eventDispatcher = ServiceContainer::getService('event_dispatcher');
$event = new OrderEvent($this->order);
$eventDispatcher->dispatch($event, OrderEvent::ORDER_EDITED);
}
protected function getErrorMsg($error)
{
switch ($error) {
// Discount
case 2:
return translate_shop('error', 'order')['failed'];
break;
// User errors
case 5:
return translate_shop('error', 'order')['missing_transport'];
break;
case 6:
return translate_shop('error', 'user')['missing_name_invoice'];
break;
case 7:
return translate_shop('error', 'user')['missing_street_invoice'];
break;
case 8:
return translate_shop('error', 'user')['missing_city_invoice'];
break;
case 9:
return translate_shop('error', 'user')['missing_zip_invoice'];
break;
case 10:
return translate_shop('error', 'user')['missing_email_invoice'];
break;
case 11:
return translate_shop('error', 'user')['missing_phone_invoice'];
break;
case 12:
return translate_shop('error', 'order')['missing_products'];
break;
case 13:
return translate_shop('error', 'user')['false_zip'];
break;
case 14:
return translate_shop('error', 'user')['false_phone'];
break;
case 15:
return replacePlaceholders(
translate_shop('error', 'user')['login_exists'],
['URL' => path('kupshop_user_login_login')]
);
break;
case 16:
return translate_shop('error', 'user')['false_length_passw'];
break;
case 17:
return translate_shop('error', 'order')['false_country'];
break;
case 18:
return translate_shop('error', 'order')['disabled_country'];
break;
case 19:
return translate_shop('error', 'order')['invalid_variation'];
break;
}
return 'Unknown error';
}
protected function checkBuyingProcess()
{
return true;
}
protected function changeOrderStatus()
{
$statusHandled = findModule('pos', 'status_handled', getStatuses('handled')[0]);
$this->order->changeStatus($statusHandled, '', false);
}
}