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

View File

@@ -0,0 +1,10 @@
<?php
namespace External\FlexiBeeBundle\AbraFlexiTypes;
use AbraFlexi\RW;
class CenikovaSkupina extends RW
{
public ?string $evidence = 'cenikova-skupina';
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace External\FlexiBeeBundle\AbraFlexiTypes;
use AbraFlexi\RW;
/**
* Custom typ, protoze v knihovne chybi.
*/
class FakturaVydanaPolozka extends RW
{
public ?string $evidence = 'faktura-vydana-polozka';
}

View File

@@ -0,0 +1,10 @@
<?php
namespace External\FlexiBeeBundle\AbraFlexiTypes;
use AbraFlexi\RW;
class Odberatel extends RW
{
public ?string $evidence = 'odberatel';
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace External\FlexiBeeBundle\AbraFlexiTypes;
use AbraFlexi\RW;
/**
* Custom typ, protoze v knihovne chybi.
*/
class Sklad extends RW
{
public ?string $evidence = 'sklad';
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace External\FlexiBeeBundle\AbraFlexiTypes;
use AbraFlexi\RW;
/**
* Custom typ, protoze v knihovne chybi.
*/
class Stredisko extends RW
{
public ?string $evidence = 'stredisko';
}

View File

@@ -0,0 +1,53 @@
<?php
namespace External\FlexiBeeBundle\Admin\Actions;
use External\FlexiBeeBundle\Synchronizers\OrderSynchronizer;
use External\FlexiBeeBundle\Util\FlexiBeeUtil;
use KupShop\AdminBundle\Admin\Actions\AbstractAction;
use KupShop\AdminBundle\Admin\Actions\ActionResult;
use KupShop\AdminBundle\Admin\Actions\IAction;
use Symfony\Contracts\Service\Attribute\Required;
class OrderSendToFlexiAction extends AbstractAction implements IAction
{
#[Required]
public OrderSynchronizer $orderSynchronizer;
#[Required]
public FlexiBeeUtil $flexiUtil;
public function getTypes(): array
{
return ['orders'];
}
public function getName(): string
{
return '[Flexi] Odeslat do Flexi';
}
public function isVisible()
{
return !empty($this->getId());
}
public function showInMassEdit()
{
return true;
}
public function execute(&$data, array $config, string $type): ActionResult
{
$orderId = (int) $this->getId();
if ($this->orderSynchronizer->syncSingleItemToFlexi([
'id' => $orderId,
'id_flexi' => $this->flexiUtil->getMapping('order', $orderId),
])) {
return new ActionResult(true);
}
return new ActionResult(false, 'Odeslání do Flexi se nezdařilo.');
}
}

View File

@@ -0,0 +1,241 @@
<?php
declare(strict_types=1);
namespace External\FlexiBeeBundle\Admin\Tabs;
use External\FlexiBeeBundle\Exception\FlexiBeeException;
use External\FlexiBeeBundle\Synchronizers\StoreSynchronizer;
use External\FlexiBeeBundle\Util\FlexiBeeApi;
use External\FlexiBeeBundle\Util\FlexiBeeConfiguration;
use External\FlexiBeeBundle\Util\FlexiBeeLocator;
use External\FlexiBeeBundle\Util\FlexiBeeUtil;
use KupShop\AdminBundle\Admin\WindowTab;
use KupShop\KupShopBundle\Email\OrderMessageEmail;
class FlexiBeeSettingsTab extends WindowTab
{
protected $title = 'flapFlexiBeeSettings';
protected $template = 'window/settings.flexibee.tpl';
private FlexiBeeApi $api;
private FlexiBeeUtil $util;
private FlexiBeeLocator $locator;
private FlexiBeeConfiguration $configuration;
private OrderMessageEmail $orderMessageEmail;
public function __construct(
FlexiBeeApi $api,
FlexiBeeUtil $util,
FlexiBeeLocator $locator,
FlexiBeeConfiguration $configuration,
OrderMessageEmail $orderMessageEmail,
) {
$this->api = $api;
$this->util = $util;
$this->locator = $locator;
$this->configuration = $configuration;
$this->orderMessageEmail = $orderMessageEmail;
}
public static function getTypes()
{
return [
'settings' => 0,
];
}
public function getVars($smarty_tpl_vars)
{
$dbcfg = \Settings::getDefault();
return [
'flexibee' => $dbcfg->loadValue('flexibee') ?: [],
'flexibeeData' => [
'statuses' => $this->getFlexiBeeOrderStatuses(),
'stores' => $this->getFlexiBeeStores(),
'centrals' => $this->getFlexiBeeCentrals(),
'synchronizers' => $this->getSynchronizers(),
'pricelists' => $this->getFlexiBeePriceLists(),
],
'orderMessages' => $this->orderMessageEmail->getOrderMessages(),
'deliveries' => \DeliveryType::getDeliveries(true),
'payments' => \DeliveryType::getPayments(true),
'changesApiButton' => $this->isConfigValid() && !$this->isChangesApiAvailable(),
];
}
public function handleUpdate()
{
$this->configuration->refresh();
}
/**
* Otestuje připojení k FlexiBee.
*/
public function handleCheckConnection(): void
{
// kontroluju, zda je vyplněná konfigurace
if (!$this->isConfigValid()) {
$this->addHTMLError('<div class="alert alert-danger">Nejsou vyplněny všechny potřebné údaje!</div>');
return;
}
// otestuju komunikaci s API
if (!$this->api->isConnectionValid()) {
$this->addHTMLError('<div class="alert alert-danger">Připojení k FlexiBee se nepodařilo! Překontrolujte zadané údaje.</div>');
return;
}
$this->addHTMLError('<div class="alert alert-success">Připojení k FlexiBee proběhlo úspěšné.</div>');
// zkontroluju, že je zapnuté ChangesAPI, které v synchronizaci používáme
if (!$this->api->isChangesAPIEnabled()) {
$this->addHTMLError('<div class="alert alert-danger"><strong>ChangesAPI</strong>, které je pro synchronizaci vyžadováno, není ve FlexiBee aktivní!</div>');
}
}
public function handleChangesAPIEnable(): void
{
// tlacitko se zobrazi jen kdyz neni changesAPI aktivni
$this->api->changesAPIEnable();
}
/**
* Spustí synchronizaci skladů.
*/
public function handleSynchronizeStores(): void
{
try {
$this->util->synchronize([StoreSynchronizer::getType()]);
} catch (FlexiBeeException $e) {
$this->addHTMLError('Sklady se nepodařilo sesynchronizovat!');
return;
}
$this->addHTMLError('Sklady úspěšně sesynchronizovány.');
}
public function getLabel()
{
return 'FlexiBee';
}
/**
* Vrací seznam typů synchronizací.
*/
protected function getSynchronizers(): array
{
$result = [];
foreach ($this->locator->getTypes() as $type => $_) {
// Sklad tady nezobrazuju, protoze ten synchronizuju zvlast podle nastaveni skladu
if ($type === StoreSynchronizer::getType()) {
continue;
}
$result[$type] = translate('synchronizer_'.$type, 'flexibee');
}
return $result;
}
/**
* FlexiBee stavy. Mají je ve FlexiBee natvrdo, tak by to mělo stačit takhle vydefinovaný.
*/
protected function getFlexiBeeOrderStatuses(): array
{
return [
'stavDoklObch.pripraveno',
'stavDoklObch.schvaleno',
'stavDoklObch.castecneNaCeste',
'stavDoklObch.naCeste',
'stavDoklObch.castVydano',
'stavDoklObch.vydano',
'stavDoklObch.castHotovo',
'stavDoklObch.hotovo',
];
}
/**
* Vrací střediska ve FlexiBee.
*/
protected function getFlexiBeeCentrals(): array
{
if ($centrals = getCache('flexiBeeCentrals')) {
return $centrals;
}
$centrals = $this->api->isConnectionValid() ? $this->api->getCentrals() : [];
$result = [];
foreach ($centrals as $central) {
$result[$central['id']] = $central['nazev'];
}
setCache('flexiBeeCentrals', $result, 3600);
return $result;
}
/**
* Vrací sklady ve FlexiBee.
*/
protected function getFlexiBeeStores(): array
{
if ($stores = getCache('flexiBeeStores')) {
return $stores;
}
$stores = $this->api->isConnectionValid() ? $this->api->getStores(true) : [];
$result = [];
foreach ($stores as $store) {
$result[$store['id']] = $store['nazev'];
}
setCache('flexiBeeStores', $result, 3600);
return $result;
}
protected function getFlexiBeePriceLists(): array
{
$pricelists = $this->api->isConnectionValid() ? $this->api->getPriceLists() : [];
$result[0] = 'Všechny ceníky';
foreach ($pricelists as $pricelist) {
$result[$pricelist['id']] = $pricelist['nazev'];
}
return $result;
}
public function isConfigValid(): bool
{
try {
$this->configuration->getAPIConfig();
} catch (FlexiBeeException $e) {
return false;
}
return true;
}
protected function isChangesApiAvailable(): bool
{
if (getCache('flexiBeeActiveAPI')) {
return true;
}
if ($result = $this->api->isConnectionValid() && $this->api->isChangesAPIEnabled()) {
setCache('flexiBeeActiveAPI', $result, 3600);
return $result;
}
return false;
}
}

View File

@@ -0,0 +1,11 @@
<?php
$txt_str['flexibee'] = [
'synchronizer_order' => 'Objednávky',
'synchronizer_price' => 'Ceny',
'synchronizer_product' => 'Produkty',
'synchronizer_store' => 'Sklady',
'synchronizer_supply' => 'Skladová zásoba',
'synchronizer_user' => 'Uživatelé',
'synchronizer_pricelist' => 'Ceníky',
];

View File

@@ -0,0 +1 @@
{extends "[AdminBundle]actions/baseAction.tpl"}

View File

@@ -0,0 +1,286 @@
<div id="flapFlexiBeeSettings" class="tab-pane fade in boxFlex">
<div class="row bottom-space">
<div class="col-md-12">
<h1 class="h4 main-panel-title">Nastavení API</h1>
</div>
</div>
<input type="hidden" name="disable_autoload[flexibee]" value="1">
<div class="form-group">
<div class="col-md-2 control-label">
<label>Adresa FlexiBee</label>
</div>
<div class="col-md-5">
<input type="text" class="form-control input-sm" name="data[flexibee][api][url]"
value="{$body.data.flexibee.api.url}">
</div>
<div class="col-md-3 col-md-offset-1">
<button type="submit" class="btn btn-success btn-block" name="acn" value="checkConnection">Ověřit připojení</button>
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">
<label>Firma</label>
</div>
<div class="col-md-5">
<input type="text" class="form-control input-sm" name="data[flexibee][api][company]"
value="{$body.data.flexibee.api.company}">
</div>
{if $tab.data.changesApiButton}
<div class="col-md-3 col-md-offset-1">
<button type="submit" class="btn btn-success btn-block" name="acn" value="changesAPIEnable">Zapnout ChangesAPI</button>
</div>
{/if}
</div>
<div class="form-group">
<div class="col-md-2 control-label two-lines">
<label>Uživatelské jméno</label>
</div>
<div class="col-md-2">
<div class="input-group">
<input type="text" class="form-control input-sm" name="data[flexibee][api][user]"
value="{$body.data.flexibee.api.user}">
</div>
</div>
<div class="col-md-1 control-label">
<label>Heslo</label>
</div>
<div class="col-md-2">
<div class="input-group">
<input type="password" class="form-control input-sm" name="data[flexibee][api][password]" autocomplete="new-password"
value="{$body.data.flexibee.api.password}">
</div>
</div>
</div>
<div class="row bottom-space">
<div class="col-md-12">
<h1 class="h4 main-panel-title">Nastavení synchronizace</h1>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading" data-toggle="collapse" data-target="#flexibee_sync">
<p style="margin-left: 10px;"><strong>Obecné</strong></p>
</div>
<div id="flexibee_sync" class="panel-collapse collapse in">
<div class="panel-body">
<div class="form-group">
<div class="col-md-2 control-label">
<label>Povolené synchronizace</label>
</div>
<div class="col-md-5">
<select class="selecter" multiple name="data[flexibee][config][enabled_types][]">
{foreach $tab.data.flexibeeData.synchronizers as $type => $name}
<option value="{$type}"
{if $body.data.flexibee.config.enabled_types && in_array($type, $body.data.flexibee.config.enabled_types)}selected{/if}>{$name}</option>
{/foreach}
</select>
</div>
</div>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading" data-toggle="collapse" data-target="#flexibee_sync_stores">
<p style="margin-left: 10px;"><strong>Sklady</strong></p>
</div>
<div id="flexibee_sync_stores" class="panel-collapse collapse in">
<div class="panel-body">
<div class="form-group">
<div class="col-md-2 control-label">
<label>Synchronizované sklady</label>
</div>
<div class="col-md-5">
<select class="selecter" multiple name="data[flexibee][config][stores][]">
{foreach $tab.data.flexibeeData.stores as $storeId => $store}
<option value="{$storeId}"
{if $body.data.flexibee.config.stores && in_array($storeId, $body.data.flexibee.config.stores)}selected{/if}>{$store}</option>
{/foreach}
</select>
</div>
<div class="col-md-3 col-md-offset-1">
<button type="submit" class="btn btn-success btn-block" name="acn" value="synchronizeStores">
Sesynchronizovat sklady
</button>
</div>
</div>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading" data-toggle="collapse" data-target="#flexibee_sync_orders">
<p style="margin-left: 10px;"><strong>Objednávky</strong></p>
</div>
<div id="flexibee_sync_orders" class="panel-collapse collapse in">
<div class="panel-body">
<div class="form-group">
<div class="col-md-2 control-label">
<label>Středisko pro zápis</label>
</div>
<div class="col-md-5">
<select class="selecter" name="data[flexibee][config][order][central]">
<option value="">-- nevybráno --</option>
{foreach $tab.data.flexibeeData.centrals as $centralId => $name}
<option value="{$centralId}"
{if $body.data.flexibee.config.order.central == $centralId}selected{/if}>{$name}</option>
{/foreach}
</select>
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">
<label>Výchozí uživatel</label>
</div>
<div class="col-md-5">
<input class="input-sm form-control" name="data[flexibee][config][order][id_user]"
value="{$body.data.flexibee.config.order.id_user}" placeholder="ID uživatele ve FlexiBee">
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">
<label>Chybový stav</label>
<a class="help-tip" data-toggle="tooltip"
title="" data-original-title="Objednávky, u kterých dojde k 10 neuspěšným pokusům o synchronizaci,
budou přepnuty do tohoto stavu">
<i class="bi bi-question-circle"></i>
</a>
</div>
<div class="col-md-5">
<select class="selecter" name="data[flexibee][config][order][error_status]">
<option value="">-- neměnit stav --</option>
{foreach getOrderStatuses() as $key => $orderStatus}
<option value="{$key}"
{if $body.data.flexibee.config.order.error_status == $key}selected{/if}>{$orderStatus.name}</option>
{/foreach}
</select>
</div>
</div>
<div class="form-group">
<div class="col-md-2 control-label">
<label>
Obousměrná synchronizace
<a class="help-tip" data-toggle="tooltip"
title="" data-original-title="Zapnutím oboustranné synchronizace se začnou synchronizovat změny objednávek směrem
z FlexiBee do e-shopu. Tzn. že pokud například přidáte k objednávce ve FlexiBee novou položku, tak se položka
přenese i do objednávky na e-shopu. ">
<i class="bi bi-question-circle"></i>
</a>
</label>
</div>
<div class="col-md-5">
{print_toggle nameRaw='data[flexibee][config][order][duplex_sync]' value= $body.data.flexibee.config.order.duplex_sync}
</div>
</div>
<div class="row">
<div class="col-md-12" style="border-bottom: 1px solid #e6e9ed; margin: 5px 0 10px; padding-bottom: 6px;">
<strong>Změny stavů</strong>
</div>
</div>
{foreach $tab.data.flexibeeData.statuses as $status}
<div class="form-group">
<div class="col-md-2 control-label">
<label>{$status}</label>
</div>
<div class="col-md-2">
<select class="selecter" name="data[flexibee][config][order][statuses][{$status}][status]">
<option value="">-- neměnit stav --</option>
{foreach getOrderStatuses() as $key => $orderStatus}
<option value="{$key}" {if $body.data.flexibee.config.order.statuses[$status].status !== '' && $body.data.flexibee.config.order.statuses[$status].status !== null && $body.data.flexibee.config.order.statuses[$status].status == $key}selected{/if}>{$orderStatus.name}</option>
{/foreach}
</select>
</div>
<div class="col-md-3">
<select class="selecter" name="data[flexibee][config][order][statuses][{$status}][order_message]">
<option value="">-- neodesílat zprávu uživateli --</option>
{foreach $tab.data.orderMessages as $orderMessage}
<option value="{$orderMessage.id}" {if $body.data.flexibee.config.order.statuses[$status].order_message == $orderMessage.id}selected{/if}>
{$orderMessage.name}
</option>
{/foreach}
</select>
</div>
</div>
{/foreach}
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading" data-toggle="collapse" data-target="#flexibee_sync_delivery_payment">
<p style="margin-left: 10px;"><strong>Dopravy a platby</strong></p>
</div>
<div id="flexibee_sync_delivery_payment" class="panel-collapse collapse in">
<div class="panel-body">
<div class="row">
<div class="col-md-6">
<div class="row">
<div class="col-md-12" style="border-bottom: 1px solid #e6e9ed; margin: 5px 0 10px; padding-bottom: 6px;">
<strong>Dopravy</strong>
</div>
</div>
{foreach $tab.data.deliveries as $delivery}
<div class="form-group">
<div class="col-md-4 control-label">
<label>{$delivery.name}</label>
</div>
<div class="col-md-6">
<input class="input-sm form-control" name="data[flexibee][config][delivery][{$delivery.id}]"
value="{$body.data.flexibee.config.delivery[$delivery.id]}" placeholder="Kód ve FlexiBee">
</div>
</div>
{/foreach}
</div>
<div class="col-md-6">
<div class="row">
<div class="col-md-12" style="border-bottom: 1px solid #e6e9ed; margin: 5px 0 10px; padding-bottom: 6px;">
<strong>Platby</strong>
</div>
</div>
{foreach $tab.data.payments as $payment}
<div class="form-group">
<div class="col-md-4 control-label">
<label>{$payment.name}</label>
</div>
<div class="col-md-6">
<input class="input-sm form-control" name="data[flexibee][config][payment][{$payment.id}]"
value="{$body.data.flexibee.config.payment[$payment.id]}" placeholder="Kód ve FlexiBee">
</div>
</div>
{/foreach}
</div>
</div>
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading" data-toggle="collapse" data-target="#flexibee_sync_pricelists">
<p style="margin-left: 10px;"><strong>Ceníky</strong></p>
</div>
<div id="flexibee_sync_pricelists" class="panel-collapse collapse in">
<div class="panel-body">
<div class="form-group">
<div class="col-md-2 control-label">
<label>Synchronizované ceníky</label>
</div>
<div class="col-md-5">
<select class="selecter" multiple name="data[flexibee][config][pricelists][]">
{foreach $tab.data.flexibeeData.pricelists as $pricelistId => $pricelist}
<option value="{$pricelistId}"
{if $body.data.flexibee.config.pricelists && in_array($pricelistId, $body.data.flexibee.config.pricelists)}selected{/if}>{$pricelist}</option>
{/foreach}
</select>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace External\FlexiBeeBundle\Controller;
use External\FlexiBeeBundle\Exception\FlexiBeeException;
use External\FlexiBeeBundle\Synchronizers\OrderSynchronizer;
use External\FlexiBeeBundle\Util\FlexiBeeLocator;
use KupShop\AdminBundle\AdminRequiredControllerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;
class FlexiBeeController extends AbstractController implements AdminRequiredControllerInterface
{
/**
* @Route("/_flexi/dump_order/{id_order}/")
* @Route("/_flexi/dump_order/{id_order}/{type}/", requirements={"type"="(create|realize)"})
*/
public function dumpOrder(OrderSynchronizer $orderSynchronizer, int $id_order, ?string $type = null): JsonResponse
{
try {
$order = \Order::get($id_order);
} catch (\Throwable $e) {
throw new NotFoundHttpException('Order not found');
}
switch ($type) {
case 'realize':
$data = [
'realize' => $orderSynchronizer->getOrderRealizeData($order),
'invoice' => $orderSynchronizer->getOrderInvoiceData($order),
];
break;
default:
$data = $orderSynchronizer->getOrderData($order);
}
return new JsonResponse(
$data
);
}
/**
* @Route("/_flexi/{type}/")
*/
public function syncType(FlexiBeeLocator $locator, string $type): JsonResponse
{
try {
$synchronizer = $locator->getServiceByType($type);
$synchronizer->sync();
} catch (FlexiBeeException $e) {
return new JsonResponse(['success' => false, 'message' => $e->getMessage()]);
}
return new JsonResponse(['success' => true, 'message' => sprintf('Synchronization of "%s" done', $type)]);
}
/**
* @Route("/_flexi/{type}/{id}/")
*/
public function syncSingleItem(FlexiBeeLocator $locator, string $type, int $id): JsonResponse
{
try {
$synchronizer = $locator->getServiceByType($type);
$synchronizer->syncSingleItem($id);
} catch (FlexiBeeException $e) {
return new JsonResponse(['success' => false, 'message' => $e->getMessage()]);
}
return new JsonResponse(['success' => true, 'message' => 'Single item synchronization completed']);
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace External\FlexiBeeBundle\EventSubscriber;
use External\FlexiBeeBundle\Util\FlexiBeeUtil;
use KupShop\OrderingBundle\Event\OrderEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class OrderSubscriber implements EventSubscriberInterface
{
private FlexiBeeUtil $flexiBeeUtil;
public function __construct(FlexiBeeUtil $flexiBeeUtil)
{
$this->flexiBeeUtil = $flexiBeeUtil;
}
public static function getSubscribedEvents(): array
{
return [
OrderEvent::ORDER_PAID => [
['updateFlexiBeeOrder', 200],
],
OrderEvent::ORDER_EDITED => [
['updateFlexiBeeOrder', 200],
],
OrderEvent::ORDER_STORNO => [
['updateFlexiBeeOrder', 200],
],
];
}
public function updateFlexiBeeOrder(OrderEvent $event): void
{
$order = $event->getOrder();
// synchronizace na zaklade toho znovu odesle objednavku do FlexiBee - aktualizuje ji
$this->flexiBeeUtil->setFlexiOrderData(
(int) $order->id,
'orderUpdated',
1
);
}
}

View File

@@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace External\FlexiBeeBundle\Exception;
class FlexiBeeException extends \Exception
{
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace External\FlexiBeeBundle;
use External\FlexiBeeBundle\Synchronizers\SynchronizerInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class FlexiBeeBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
$container->registerForAutoconfiguration(SynchronizerInterface::class)
->addTag('flexibee.synchronizer')
->setAutowired(true);
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace External\FlexiBeeBundle\Inspections;
use Composer\InstalledVersions;
use KupShop\SystemInspectionBundle\Inspections\Compile\CompileInspectionInterface;
use KupShop\SystemInspectionBundle\Inspections\Inspection;
use KupShop\SystemInspectionBundle\InspectionWriters\MessageTypes\ComposerMissingMessage;
use KupShop\SystemInspectionBundle\InspectionWriters\MessageTypes\SimpleMessage;
class FlexiBeeInspection extends Inspection implements CompileInspectionInterface
{
public function runInspection(): ?array
{
$errors = [];
// check for stores module
if (!findModule(\Modules::STORES)) {
$errors[] = new SimpleMessage('Stores module is required when FlexiBeeBundle is enabled!');
}
// check for synchronization module
if (!findModule(\Modules::SYNCHRONIZATION)) {
$errors[] = new SimpleMessage('Synchronization module is required when FlexiBeeBundle is enabled!');
}
// check for composer package
try {
InstalledVersions::getVersion('spojenet/flexibee');
} catch (\Exception $e) {
$errors[] = new ComposerMissingMessage(
'Missing required composer dependency "spojenet/flexibee"! Run `composer require spojenet/flexibee` to fix it.'
);
}
return $errors;
}
}

View File

@@ -0,0 +1,3 @@
FlexiBeeBundle:
resource: "@FlexiBeeBundle/Controller/"
type: annotation

View File

@@ -0,0 +1,10 @@
services:
_defaults:
autoconfigure: true
autowire: true
External\FlexiBeeBundle\:
resource: ../../{Admin/Tabs,Admin/Actions,Controller,EventSubscriber,Inspections,SynchronizationRegister,Synchronizers,Util}
External\FlexiBeeBundle\Util\FlexiBeeLocator:
arguments: [!tagged_locator { tag: 'flexibee.synchronizer', index_by: 'key', default_index_method: 'getType' }]

View File

@@ -0,0 +1,22 @@
<?php
namespace External\FlexiBeeBundle\Resources\script;
use External\FlexiBeeBundle\Synchronizers\BaseSynchronizer;
use External\FlexiBeeBundle\Util\FlexiBeeUtil;
use KupShop\AdminBundle\Util\Script\Script;
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
class RunSynchronizationScript extends Script
{
protected static $name = '[FlexiBee]RunSynchronization';
protected static $defaultParameters = ['types' => ['product'], 'mode' => BaseSynchronizer::MODE_FULL];
protected function run(array $arguments)
{
$util = ServiceContainer::getService(FlexiBeeUtil::class);
$util->synchronize($arguments['types'], $arguments['mode']);
}
}
return RunSynchronizationScript::class;

View File

@@ -0,0 +1,96 @@
<?php
namespace External\FlexiBeeBundle\Resources\upgrade;
class FlexiBeeUpgrade extends \UpgradeNew
{
protected $priority = 100;
public function check_FlexiProducts(): bool
{
return $this->checkTableExists('flexi_products');
}
/** Add table flexi_products */
public function upgrade_FlexiProducts(): void
{
sqlQuery('CREATE TABLE flexi_products (
id_flexi INT(11) NOT NULL PRIMARY KEY ,
id_product INT(11) NOT NULL,
id_variation INT(11) DEFAULT NULL,
CONSTRAINT FK_flexi_products_id_product FOREIGN KEY (id_product) REFERENCES products(id) ON DELETE CASCADE,
CONSTRAINT FK_flexi_products_id_variation FOREIGN KEY (id_variation) REFERENCES products_variations(id) ON DELETE CASCADE
)');
$this->upgradeOK();
}
public function check_FlexiOrders(): bool
{
return $this->checkTableExists('flexi_orders');
}
/** Add table flexi_orders */
public function upgrade_FlexiOrders(): void
{
sqlQuery('CREATE TABLE flexi_orders (
id_flexi INT(11) NOT NULL PRIMARY KEY ,
id_order INT(10) UNSIGNED NOT NULL,
data MEDIUMTEXT DEFAULT NULL,
CONSTRAINT FK_flexi_orders_id_order FOREIGN KEY (id_order) REFERENCES orders(id) ON DELETE CASCADE
)');
$this->upgradeOK();
}
public function check_FlexiUsers(): bool
{
return $this->checkTableExists('flexi_users');
}
/** Add table flexi_users */
public function upgrade_FlexiUsers(): void
{
sqlQuery('CREATE TABLE flexi_users (
id_flexi INT(11) NOT NULL PRIMARY KEY ,
id_user INT(11) UNSIGNED NOT NULL,
CONSTRAINT FK_flexi_users_id_user FOREIGN KEY (id_user) REFERENCES users(id) ON DELETE CASCADE
)');
$this->upgradeOK();
}
public function check_FlexiStores(): bool
{
return $this->checkTableExists('flexi_stores');
}
/** Add table flexi_stores */
public function upgrade_FlexiStores(): void
{
sqlQuery('CREATE TABLE flexi_stores (
id_flexi INT(11) NOT NULL PRIMARY KEY ,
id_store INT(11) NOT NULL,
CONSTRAINT FK_flexi_stores_id_store FOREIGN KEY (id_store) REFERENCES stores(id) ON DELETE CASCADE
)');
$this->upgradeOK();
}
public function check_FlexiPricelists(): bool
{
return $this->checkTableExists('flexi_pricelists');
}
/** Add table flexi_pricelists */
public function upgrade_FlexiPricelists(): void
{
sqlQuery('CREATE TABLE flexi_pricelists (
id_flexi INT(11) NOT NULL PRIMARY KEY ,
id_pricelist INT(11) NOT NULL,
CONSTRAINT FK_flexi_pricelists_id_pricelist FOREIGN KEY (id_pricelist) REFERENCES pricelists(id) ON DELETE CASCADE ON UPDATE CASCADE
)');
$this->upgradeOK();
}
}

View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace External\FlexiBeeBundle\SynchronizationRegister;
use External\FlexiBeeBundle\Synchronizers\OrderSynchronizer;
use External\FlexiBeeBundle\Synchronizers\PriceListSynchronizer;
use External\FlexiBeeBundle\Synchronizers\PriceSynchronizer;
use External\FlexiBeeBundle\Synchronizers\ProductSynchronizer;
use External\FlexiBeeBundle\Synchronizers\StoreSynchronizer;
use External\FlexiBeeBundle\Synchronizers\SupplySynchronizer;
use External\FlexiBeeBundle\Synchronizers\UserSynchronizer;
use External\FlexiBeeBundle\Util\FlexiBeeConfiguration;
use External\FlexiBeeBundle\Util\FlexiBeeUtil;
use KupShop\SynchronizationBundle\Synchronization\Job;
use KupShop\SynchronizationBundle\Synchronization\SynchronizationRegisterInterface;
use Symfony\Component\Console\Output\OutputInterface;
class FlexiBeeSynchronizationRegister implements SynchronizationRegisterInterface
{
private FlexiBeeConfiguration $configuration;
private FlexiBeeUtil $util;
public function __construct(FlexiBeeConfiguration $configuration, FlexiBeeUtil $util)
{
$this->configuration = $configuration;
$this->util = $util;
}
public function getJobs(): iterable
{
// hlavni synchronizacni thread
yield Job::create('FlexiBee::main', fn (OutputInterface $output) => $this->synchronize($output))
->everyMinute(5);
// zalozeni skladu - nepotrebuju to spoustet furt, tak to oddelim od toho hlavniho threadu
yield Job::create('FlexiBee::stores', fn () => $this->util->synchronize([StoreSynchronizer::getType()]))
->hourly();
}
private function synchronize(OutputInterface $output): void
{
// Tady mam serazene synchronizace tak, jak by meli jit po sobe
$synchronizers = [
UserSynchronizer::getType(),
OrderSynchronizer::getType(),
ProductSynchronizer::getType(),
SupplySynchronizer::getType(),
PriceSynchronizer::getType(),
PriceListSynchronizer::getType(),
];
// Projdu serazene typy synchronizaci
foreach ($synchronizers as $synchronizer) {
// Pokud neni typ povoleny, tak skipuju
if (!in_array($synchronizer, $this->configuration->getEnabledSynchronizerTypes())) {
continue;
}
$output->writeln(sprintf('[FlexiBee] Running synchronization: %s...', $synchronizer));
$this->util->synchronize([$synchronizer]);
$output->writeln(sprintf('[FlexiBee] Completed synchronization: %s', $synchronizer));
}
}
}

View File

@@ -0,0 +1,229 @@
<?php
declare(strict_types=1);
namespace External\FlexiBeeBundle\Synchronizers;
use External\FlexiBeeBundle\Exception\FlexiBeeException;
use External\FlexiBeeBundle\Util\FlexiBeeApi;
use External\FlexiBeeBundle\Util\FlexiBeeConfiguration;
use External\FlexiBeeBundle\Util\FlexiBeeLogger;
use External\FlexiBeeBundle\Util\FlexiBeeUtil;
abstract class BaseSynchronizer implements SynchronizerInterface
{
public const MODE_NORMAL = 0;
public const MODE_FULL = 1;
protected static string $type;
protected static ?string $evidenceClass = null;
protected bool $logging = true;
protected int $mode = self::MODE_NORMAL;
protected FlexiBeeLogger $logger;
protected FlexiBeeConfiguration $configuration;
protected FlexiBeeApi $flexiBeeApi;
protected FlexiBeeUtil $flexiBeeUtil;
protected ?int $tmpLastSync = null;
public function __construct(
FlexiBeeConfiguration $configuration,
FlexiBeeApi $flexiBeeApi,
FlexiBeeUtil $flexiBeeUtil,
FlexiBeeLogger $logger,
) {
$this->configuration = $configuration;
$this->flexiBeeApi = $flexiBeeApi;
$this->flexiBeeUtil = $flexiBeeUtil;
$this->logger = $logger;
}
public static function getType(): string
{
return static::$type;
}
public function sync(): void
{
$this->process(
$this->getLastSyncTime()
);
$this->updateLastSyncTime();
}
public function syncSingleItem(int $id, ?int $flexiId = null): void
{
throw new FlexiBeeException(sprintf('Single item synchronization is not supported for type "%s"', static::getType()));
}
protected function process(?int $lastSyncTime = null): void
{
foreach ($this->getItems($lastSyncTime) as $item) {
$this->logItem((array) $item);
$this->processItem($item);
}
}
abstract protected function processItem(array $item): void;
protected function getItems(?int $lastSyncTime = null): iterable
{
if (!static::$evidenceClass) {
throw new FlexiBeeException(
sprintf('Class "%s" should overwrite "%s" method', get_class($this), __FUNCTION__)
);
}
// Pokud je nastavena evidence class, tak pro nacteni zmen pouziju ChangesAPI
$changesMaxVersion = $this->getLastVersion();
// ukladam si startTime, abych mohl limitovat maximalni dobu, kterou muze sync v ramci jednoho sync cyklu bezet
$startTime = microtime(true);
do {
// nactu si zmeny od posledni synchronizace
$changes = $this->flexiBeeApi->getChanges($changesMaxVersion, static::$evidenceClass);
// ulozim si maximalni verzi
$changesMaxVersion = (int) max($changesMaxVersion, !empty($changes) ? (max(array_map(fn ($x) => $x['@in-version'], $changes)) + 1) : 0);
// nactu si data podle ID zmen
$changesIds = array_unique(array_map(fn ($x) => $x['id'], $changes));
if (!empty($changesIds)) {
// zavolam preprocess kvuli pripadnym modifikacim dat
$changedItems = $this->preprocessChangedItems(
$this->flexiBeeApi->getEvidenceDataByIds(static::$evidenceClass, $changesIds, filters: $this->getItemsFilter())
);
// zacnu vracet polozky, aby se v synchronizaci zpracovali
foreach ($changedItems as $item) {
yield $item;
}
}
// aktualizuju si posledni sesynchronizovanou verzi
$this->updateLastVersion($changesMaxVersion);
$isTimedOut = (microtime(true) - $startTime) > 300;
// pokud sync bezi uz dele jak 5 minut, tak ji ukoncim
// pravdepodobne ve Flexi vzniklo hodne zmen, tak to postupne syncneme nez blokovat sync na dlouhou dobu
if ($this->mode === self::MODE_NORMAL && $isTimedOut) {
break;
}
} while (!empty($changes));
}
protected function preprocessChangedItems(array $items): array
{
return $items;
}
protected function logItem(array $item): void
{
if (!$this->logging) {
return;
}
$this->logger->data(
sprintf('[FlexiBee] Processing change of \'%s\'', static::getType()),
[
'Data' => $item,
'Type' => static::getType(),
]
);
}
protected function logUpdate(array $item): void
{
if (!$this->logging) {
return;
}
$this->logger->data(
sprintf('[FlexiBee] Sending to Flexi change of \'%s\'', static::getType()),
[
'Data' => $item,
'Type' => static::getType(),
]
);
}
protected function getLastVersion(?string $type = null): int
{
if ($this->mode === self::MODE_FULL) {
return 0;
}
$dbcfg = \Settings::getDefault();
$flexiBee = $dbcfg->loadValue('flexibee') ?: [];
return !empty($flexiBee['sync_versions'][$type ?: static::getType()]) ? (int) $flexiBee['sync_versions'][$type ?: static::getType()] : 0;
}
protected function updateLastVersion(int $version): void
{
if ($this->mode === self::MODE_FULL) {
return;
}
$dbcfg = \Settings::getDefault();
$flexiBee = $dbcfg->loadValue('flexibee') ?: [];
$flexiBee['sync_versions'][static::getType()] = $version;
$dbcfg->saveValue('flexibee', $flexiBee);
}
protected function getLastSyncTime(?string $type = null): ?int
{
if ($this->mode === self::MODE_FULL) {
return null;
}
$dbcfg = \Settings::getDefault();
$flexiBee = $dbcfg->loadValue('flexibee') ?: [];
$this->tmpLastSync = time() - (60 * 5);
return $flexiBee['timestamps'][$type ?: static::getType()] ?? null;
}
protected function updateLastSyncTime(): void
{
if ($this->mode === self::MODE_FULL) {
return;
}
$dbcfg = \Settings::getDefault();
$flexiBee = $dbcfg->loadValue('flexibee') ?: [];
$flexiBee['timestamps'][static::getType()] = $this->tmpLastSync ?: (time() - (60 * 5));
$dbcfg->saveValue('flexibee', $flexiBee);
}
protected function createDateTime(?int $timestamp): ?\DateTime
{
if (!$timestamp) {
return null;
}
$datetime = new \DateTime();
$datetime->setTimestamp($timestamp);
return $datetime;
}
public function setMode(int $mode): self
{
$this->mode = $mode;
return $this;
}
protected function getItemsFilter(): array
{
return [];
}
}

View File

@@ -0,0 +1,988 @@
<?php
declare(strict_types=1);
namespace External\FlexiBeeBundle\Synchronizers;
use AbraFlexi\ObjednavkaPrijata;
use External\FlexiBeeBundle\Exception\FlexiBeeException;
use KupShop\I18nBundle\Entity\Currency;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\KupShopBundle\Util\Database\QueryHint;
use KupShop\KupShopBundle\Util\Functional\Mapping;
use KupShop\OrderingBundle\Entity\Order\OrderItem;
use KupShop\OrderingBundle\Util\Order\OrderInfo;
use Query\Operator;
use Query\QueryBuilder;
class OrderSynchronizer extends BaseSynchronizer
{
protected static string $type = 'order';
protected static ?string $evidenceClass = ObjednavkaPrijata::class;
/** @required */
public UserSynchronizer $userSynchronizer;
protected array $recentlyUpdatedOrders = [];
public function syncSingleItem(int $id, ?int $flexiId = null): void
{
if ($flexiId = $this->flexiBeeUtil->getFlexiId(static::getType(), $id)) {
$items = $this->preprocessChangedItems([$this->flexiBeeApi->getOrder($flexiId)]);
$this->processItem(reset($items));
}
}
public function syncSingleItemToFlexi($item): bool
{
try {
$order = QueryHint::withRouteToMaster(fn () => \Order::get((int) $item['id']));
$orderIsPaidAtStart = $order->isPaid(true);
try {
$data = $this->getOrderData($order, $item['id_flexi'] ? (int) $item['id_flexi'] : null);
$flexiId = $this->flexiBeeApi->createOrUpdateOrder($data);
$this->logUpdate($data);
} catch (FlexiBeeException $e) {
$errorMessage = 'Objednávku se nepodařilo zapsat do FlexiBee';
if ($item['id_flexi']) {
$errorMessage = 'Objednávku se ve FlexiBee nepodařilo aktualizovat';
}
// zaznamenam neuspesnou sync
if ($errorStatus = $this->configuration->getOrderConfig()['error_status']) {
$attemptCount = $order->getData('flexiSyncAttemptCount');
$attemptCount = $attemptCount + 1;
$order->setData('flexiSyncAttemptCount', $attemptCount);
if ($attemptCount >= 10) {
$order->logHistory('[FlexiBee] Došlo k opakovanému problému se synchronizací objednávky, zkontrolujte Activity log');
$order->changeStatus($errorStatus);
}
}
// zalogovani do ActivityLogu
$this->logger->exception(
$e,
sprintf('[FlexiBee] Objednávka "%s": %s', $order->order_no, $errorMessage),
[
'orderId' => $order->id,
'flexiBeeError' => $e->getMessage(),
]
);
// nastavim orderUpdated na 0, protoze se aktualizace nepovedla, tak aby to furt nezkouselo znova
if ($item['id_flexi'] && !$errorStatus) {
$this->flexiBeeUtil->setFlexiOrderData(
(int) $order->id,
'orderUpdated',
0
);
}
return false;
}
// Pokud nemam flexiId, tak pravdepodobne nastala nejaka chyba, takze preskakuju
if (!$flexiId) {
return false;
}
// objednavka jeste nema mapping a tudiz jsme ji ted poprve zapsali do FlexiBee
if (!$this->flexiBeeUtil->getMapping(OrderSynchronizer::getType(), $flexiId)) {
$this->flexiBeeUtil->createMapping(OrderSynchronizer::getType(), $flexiId, (int) $order->id);
$order->logHistory('[FlexiBee] Objednávka byla nahrána do FlexiBee; ID: '.$flexiId);
return true;
}
// pokud objednavka uz ma mapping, tak to musi znamenat, ze jsme provadeli update objednavky
// takze pouze k objednavce zaloguju, ze byla objednavka ve FlexiBee aktualizovana
$order->logHistory('[FlexiBee] Objednávka byla ve FlexiBee zaktualizována');
// aktualizuju orderUpdated stav
// skoro vzdy nastavuju na 0, ale pokud by se stalo, ze objednavka nebyla zaplacena a zaplaceni bylo provedeno v prubehu
// odesilani objednavky do Flexi, tak nastavim na 1, aby se v dalsim cyklu synchronizace poslal update objednavky do Flexi
$this->flexiBeeUtil->setFlexiOrderData(
(int) $order->id,
'orderUpdated',
!$orderIsPaidAtStart && $order->isPaid(true) ? 1 : 0
);
// updatovanout objednavku si ulozim, abych ji v sync z flexi do shopu mohl preskocit
$this->recentlyUpdatedOrders[] = $order->id;
return true;
} catch (\Throwable $e) {
$this->logger->exception($e, '[FlexiBee] Během synchronizace objednávek se vyskytla chyba!', [
'orderId' => $item['id'],
]);
return false;
}
}
protected function process(?int $lastSyncTime = null): void
{
// Nahrani objednavek do FlexiBee
$this->processToFlexiBee();
// Zpracovani objednavek z FlexiBee
parent::process($lastSyncTime);
}
protected function processItem(array $item): void
{
// neznama objednavka, takze s ni nic nedelam
if (!($orderId = QueryHint::withRouteToMaster(fn () => $this->flexiBeeUtil->getMapping(static::getType(), $item['id'])))) {
return;
}
$order = QueryHint::withRouteToMaster(fn () => \Order::get($orderId));
// pokud je objednavka stornovana, tak ji neaktualizuju
if ($order->status_storno == 1) {
return;
}
// stornovani objednavky
if ($item['stavUzivK'] === 'stavDoklObch.storno') {
if ($order->status_storno == 0) {
$order->logHistory('[FlexiBee] Objednávka byla stornována ve FlexiBee');
$order->storno(false); // ORDER_STORNO event nastavi orderUpdated na 1
// nastavim orderUpdated na 0, aby synchronizace to nezkousela znovu stornovat ve FlexiBee
$this->flexiBeeUtil->setFlexiOrderData((int) $order->id, 'orderUpdated', 0);
}
return;
}
// pokud mam zapnutou oboustranou synchronizaci, tak sesynchronizuju zmeny na objednavce
if (($this->configuration->getOrderConfig()['duplex_sync'] ?? false) === 'Y') {
$this->processOrderChanges($order, $item);
}
// ulozit cislo baliku z FlexiBee k objednavce
$this->saveOrderPackageNumber($order, $item);
$statuses = $this->configuration->getOrderStatusesConfig();
// zmena stavu podle konfigurace v administraci
if ($status = ($statuses[$item['stavUzivK']] ?? false)) {
if (!empty($status['status']) || !empty($status['order_message'])) {
$this->changeOrderStatus(
$order,
$item['stavUzivK'],
!empty($status['status']) ? (int) $status['status'] : null,
!empty($status['order_message']) ? $this->getOrderMessageNameById((int) $status['order_message']) : null
);
}
}
}
protected function processToFlexiBee(): void
{
$this->processOrdersToFlexiBee();
}
private function processOrdersToFlexiBee(): void
{
if (isLocalDevelopment()) {
return;
}
// nacitam objednavky, ktere nejsou zapsane do FlexiBee nebo je u nich potreba provest aktualizaci
$qb = $this->getBaseOrdersQueryBuilder()
->andWhere(
Operator::andX(
Operator::orX(
// objednavka neni vyrizena
Operator::not(
Operator::inIntArray(getStatuses('handled'), 'o.status')
),
// nebo je to objednavka z poklady - pokladna ji muze vyridit hned
Operator::equals(['o.source' => OrderInfo::ORDER_SOURCE_POS])
),
Operator::orX(
Operator::andX(
Operator::isNull('fo.id_order'),
Operator::equals(['o.status_storno' => 0])
),
'JSON_VALUE(fo.data, \'$.orderUpdated\') = 1',
)
)
);
// pokud mam nastavenej error_status tak nechci porad dokola synchronizovat objednavky s chybou
$errorStatus = $this->configuration->getOrderConfig()['error_status'];
if ($errorStatus) {
$qb->andWhere(
Operator::not(
Operator::equals(['o.status' => $errorStatus]),
),
);
}
foreach ($qb->sendToMaster()->execute() as $item) {
$this->syncSingleItemToFlexi($item);
}
}
public function getOrderInvoiceData(\Order $order): array
{
$data = [
'formaUhradyCis' => $this->getPaymentTypeCode($order),
'formaDopravy' => $this->getDeliveryTypeCode($order),
'doprava' => $order->getDeliveryType()->name,
];
if ($paymentStatus = $this->getPaymentStatus($order)) {
$data['stavUhrK'] = $paymentStatus;
}
return $data;
}
public function getOrderRealizeData(\Order $order): array
{
$items = [];
foreach ($order->fetchItems() as $item) {
$items[] = [
'id' => $this->getItemId($order, $item),
'mj' => $item['pieces'],
];
}
$datePaid = new \DateTime();
$payments = $order->getPaymentsArray();
if ($payment = reset($payments)) {
$datePaid = $payment['date'];
}
return [
'id' => 'ext:OBP:'.$this->flexiBeeUtil->getOrderNumber($order),
'realizaceObj' => [
'@type' => 'faktura-vydana',
'id' => 'ext:FAV:'.$this->flexiBeeUtil->getOrderNumber($order),
'datUhr' => $datePaid->format('Y-m-d'),
'formaUhradyCis' => $this->getPaymentTypeCode($order),
'formaDopravy' => $this->getDeliveryTypeCode($order),
'doprava' => $order->getDeliveryType()->name,
'polozkyObchDokladu' => $items,
],
];
}
public function getOrderData(\Order $order, ?int $flexiBeeId = null): array
{
$items = [];
foreach ($order->getItems() as $item) {
$items[] = $this->getOrderItemData($order, $item);
}
$orderData = [
// externi identifikator
'id' => 'ext:'.$this->getDocumentType($order).':'.$this->flexiBeeUtil->getOrderNumber($order),
// cislo objednavky
'kod' => $this->getOrderNumber($order),
'cisDosle' => $this->getOrderNumber($order),
'typDokl' => 'code:'.$this->getDocumentType($order),
'datVyst' => $this->getOrderDate($order),
'stredisko' => $this->getOrderCentral($order),
'poznam' => $this->getOrderNote($order),
'varSym' => $this->getOrderVariableSymbol($order),
'zaokrNaSumK' => $this->getOrderRounding($order),
'zaokrJakSumK' => $this->getOrderRoundDirection($order),
'formaUhradyCis' => $this->getPaymentTypeCode($order),
'formaDopravy' => $this->getDeliveryTypeCode($order),
'mena' => 'code:'.$order->getCurrency(),
'kurz' => $order->currency_rate,
'kurzMnozstvi' => 1,
'kontaktEmail' => $order->invoice_email,
'kontaktJmeno' => $order->delivery_name.' '.$order->delivery_surname,
'kontaktTel' => $order->invoice_phone,
'nazFirmy' => $this->getOrderFirmName($order),
'ulice' => $order->invoice_street,
'mesto' => $order->invoice_city,
'psc' => $order->invoice_zip,
'ic' => $order->invoice_ico,
'dic' => $order->invoice_dic,
'stat' => 'code:'.(empty($order->invoice_country) ? 'CZ' : $order->invoice_country),
'statDph' => 'code:'.(empty($order->delivery_country) ? 'CZ' : $order->delivery_country),
'postovniShodna' => $this->isOrderDeliveryAddressSame($order),
'faNazev' => $this->getOrderFirmName($order, 'delivery'),
'faUlice' => $order->delivery_street,
'faMesto' => $order->delivery_city,
'faPsc' => $order->delivery_zip,
'faStat' => 'code:'.(empty($order->delivery_country) ? 'CZ' : $order->delivery_country),
'stavUzivK' => $this->getOrderStatus($order, $flexiBeeId),
'polozkyDokladu' => $order->status_storno == 1 ? [] : $items,
'polozkyObchDokladu@removeAll' => true,
];
if ($flexiUserId = $this->getOrderUser($order)) {
$orderData['firma'] = $flexiUserId;
}
if ($orderDescription = $this->getOrderDescription($order)) {
$orderData['popis'] = $orderDescription;
}
if (null !== ($orderFlags = $this->getOrderFlags($order))) {
$orderData['stitky'] = $orderFlags;
$orderData['stitky@removeAll'] = true;
}
if ($pointId = $order->getDeliveryType()->getDelivery()->getPointId()) {
$orderData['branchId'] = $pointId;
}
return $orderData;
}
protected function processOrderChanges(\Order $order, array $flexiOrder): void
{
// pokud jsem objednavku prave updatoval, tak nema smysl se snazit ji hned zpetne updatovat z flexi
if (in_array($order->id, $this->recentlyUpdatedOrders)) {
unset($this->recentlyUpdatedOrders[$order->id]);
return;
}
// pokud je objednavka ve stavu hotovo, tak zmeny na objednavce nezpracovavame
if ($flexiOrder['stavUzivK'] === 'stavDoklObch.hotovo' || $flexiOrder['stavUzivK'] === 'stavDoklObch.storno') {
return;
}
$orderUpdate = [];
$invoiceDiff = array_diff(
[$flexiOrder['ic'], $flexiOrder['dic'], $flexiOrder['ulice'], $flexiOrder['mesto'], $flexiOrder['psc']],
[$order->invoice_ico, $order->invoice_dic, $order->invoice_street, $order->invoice_city, $order->invoice_zip]
);
if (!empty($invoiceDiff)) {
$orderUpdate = array_merge($orderUpdate, [
'invoice_ico' => $flexiOrder['ic'],
'invoice_dic' => $flexiOrder['dic'],
'invoice_street' => $flexiOrder['ulice'],
'invoice_city' => $flexiOrder['mesto'],
'invoice_zip' => $flexiOrder['psc'],
]);
}
$deliveryDiff = array_diff(
[$flexiOrder['faUlice'], $flexiOrder['faMesto'], $flexiOrder['faPsc']],
[$order->delivery_street, $order->delivery_city, $order->delivery_zip]
);
if (!empty($deliveryDiff)) {
$orderUpdate = array_merge($orderUpdate, [
'delivery_street' => $flexiOrder['faUlice'],
'delivery_city' => $flexiOrder['faMesto'],
'delivery_zip' => $flexiOrder['faPsc'],
]);
}
$activityLogChanges = [];
// pokud jsou ve FlexiBee jine fakturacni udaje, tak provadim aktualizaci
if (!empty($orderUpdate)) {
sqlQueryBuilder()
->update('orders')
->directValues($orderUpdate)
->where(Operator::equals(['id' => $order->id]))
->execute();
if (!empty($invoiceDiff)) {
$activityLogChanges['invoice'] = [
'old' => [
'invoice_ico' => $order->invoice_ico,
'invoice_dic' => $order->invoice_dic,
'invoice_street' => $order->invoice_street,
'invoice_city' => $order->invoice_city,
'invoice_zip' => $order->invoice_zip,
],
'new' => [
'invoice_ico' => $flexiOrder['ic'],
'invoice_dic' => $flexiOrder['dic'],
'invoice_street' => $flexiOrder['ulice'],
'invoice_city' => $flexiOrder['mesto'],
'invoice_zip' => $flexiOrder['psc'],
],
];
}
if (!empty($deliveryDiff)) {
$activityLogChanges['delivery'] = [
'old' => [
'delivery_street' => $order->delivery_street,
'delivery_city' => $order->delivery_city,
'delivery_zip' => $order->delivery_zip,
],
'new' => [
'delivery_street' => $flexiOrder['faUlice'],
'delivery_city' => $flexiOrder['faMesto'],
'delivery_zip' => $flexiOrder['faPsc'],
],
];
}
}
$currentOrderItems = Mapping::mapKeys($order->fetchItems(), fn ($k, $v) => [$k, false]);
foreach ($flexiOrder['items'] ?? [] as $flexiItem) {
$externalId = !empty($flexiItem['external-ids']) ? (string) $flexiItem['external-ids'] : null;
// pokud neni u polozky vyplnene externalId, tak je to polozka pridana ve FlexiBee
if (!$externalId) {
// kouknu zda ta polozka uz neni v objednavce pridana
$externalId = $order->getData('flexiBeeItems')[$flexiItem['id']] ?? null;
}
$flexiPiecePrice = \Decimal::create($flexiItem['cenaMj'])->addDiscount($flexiItem['slevaPol']);
if ($flexiItem['typCenyDphK'] === 'typCeny.sDph') {
$flexiPiecePrice = $flexiPiecePrice->removeVat($flexiItem['szbDph']);
}
// pokud nemam externalId, tak pridavam polozku
if (!$externalId) {
// pokud je to polozka s 0 ks, tak ji preskocim, protoze nema smysl ji pridavat do objednavky
if ($flexiItem['mnozMj'] == 0) {
continue;
}
$productId = null;
$variationId = null;
// pridat novou polozku do objednavky
if (!empty($flexiItem['cenik'])) {
// zkusim polozku naparovat na produkt / variantu
if (!empty($flexiItem['cenik']->ref)) {
if ($flexiCenikId = $this->flexiBeeUtil->getFlexiIdFromRef($flexiItem['cenik']->ref)) {
[$productId, $variationId] = $this->flexiBeeUtil->getItemMapping($flexiCenikId);
}
}
}
$newItemId = sqlGetConnection()->transactional(function () use ($order, $productId, $variationId, $flexiItem, $flexiPiecePrice) {
sqlQueryBuilder()
->insert('order_items')
->directValues(
[
'id_order' => $order->id,
'id_product' => $productId,
'id_variation' => $variationId,
'pieces' => $flexiItem['mnozMj'],
'pieces_reserved' => $flexiItem['mnozMj'],
'piece_price' => $flexiPiecePrice,
'total_price' => $flexiPiecePrice->mul(toDecimal($flexiItem['mnozMj'])),
'descr' => $flexiItem['nazev'],
'tax' => $flexiItem['szbDph'],
]
)->execute();
return (int) sqlInsertId();
});
// pridam mapovani pro nove pridany item
sqlGetConnection()->transactional(function () use ($order, $newItemId, $flexiItem) {
$newItems = $order->getData('flexiBeeItems');
$newItems[$flexiItem['id']] = $newItemId;
$order->setData('flexiBeeItems', $newItems);
});
$activityLogChanges['items'][$newItemId] = ['id' => $newItemId, 'name' => $flexiItem['nazev'], 'added' => true];
continue;
}
$parts = explode('-', (string) $externalId);
$itemId = (int) end($parts);
// pokud by tam ID nebylo, tak je to divny.. ale radsi fail-check a skipnu to
/** @var OrderItem $item */
if (!($item = ($order->fetchItems()[$itemId] ?? null))) {
continue;
}
// polozka je ve FlexiBee stornovana
if ($flexiItem['storno'] === true || $flexiItem['stornoPol'] === true) {
// pokracuju dal - tim polozku neoznacim v $currentOrderItems jako ze jsem ji nasel a timpadem bude smazana z objednavky
continue;
}
// oznacim si polozku, ze jsem ji nasel
$currentOrderItems[$itemId] = true;
$itemUpdate = [];
// pokud se zmenil pocet kusu
if ($item->getPieces() != $flexiItem['mnozMj']) {
$itemUpdate['pieces'] = $flexiItem['mnozMj'];
$itemUpdate['pieces_reserved'] = $flexiItem['mnozMj'];
}
// pokud se zmenilo DPH
if ($item->getVat() != $flexiItem['szbDph']) {
$itemUpdate['tax'] = $flexiItem['szbDph'];
}
// pokud se zmenila cena, nebo pocet kusu, tak potrebuju aktualizovat cenu
if ($item->getItem()['value_without_vat']->round(4)->asFloat() != $flexiPiecePrice->round(4)->asFloat() || $item->getPieces() != $flexiItem['mnozMj']) {
$itemUpdate['piece_price'] = $flexiPiecePrice->asFloat();
$itemUpdate['total_price'] = $flexiPiecePrice->mul(toDecimal($flexiItem['mnozMj']));
}
// provest aktualizaci polozky
if (!empty($itemUpdate)) {
sqlQueryBuilder()
->update('order_items')
->directValues($itemUpdate)
->where(Operator::equals(['id' => $itemId, 'id_order' => $order->id]))
->execute();
$activityLogChanges['items'][$itemId] = ['id' => $itemId, 'productId' => $item->getProductId(), 'name' => $item->getDescr(), 'updated' => true, 'changes' => $itemUpdate];
}
}
// projdu polozky, ktere ve FlexiBee odebrali z objednavky a smazu je i v e-shopove objednavce
$deletedItems = array_filter($currentOrderItems, fn ($x) => !$x);
foreach ($deletedItems as $deletedItemId => $deletedItem) {
if (!($item = ($order->fetchItems()[$deletedItemId] ?? null))) {
continue;
}
// smaznu item z objednavky
sqlQueryBuilder()
->delete('order_items')
->where(Operator::equals(['id' => $deletedItemId]))
->execute();
// zaloguju smazani do historie objednavky
$order->logHistory('[FlexiBee] Odebrána položka: '.$item->getDescr());
$activityLogChanges['items'][$deletedItemId] = ['id' => $deletedItemId, 'productId' => $item->getProductId(), 'name' => $item->getDescr(), 'deleted' => true];
}
// provest prepocitani cen, pokud se provedli nejake zmeny v polozkach
if (!empty($activityLogChanges['items'])) {
$order->recalculate();
}
// logovani
if (!empty($activityLogChanges)) {
// zaloguju zmeny do activity logu
$this->logger->activity(
sprintf('Aktualizace objednávky "%s" z FlexiBee', $order->order_no),
$activityLogChanges
);
// udelam sumarni report, ktery zaloguju primo k objednavce
$messages = [];
foreach ($activityLogChanges as $type => $changes) {
switch ($type) {
case 'invoice':
$messages[] = 'Provedena aktualizace fakturační adresy;';
break;
case 'delivery':
$messages[] = 'Provedena aktualizace dodací adresy;';
break;
case 'items':
$updated = 0;
$added = 0;
$deleted = 0;
foreach ((array) $changes as $change) {
if (($change['updated'] ?? false) === true) {
$updated++;
}
if (($change['deleted'] ?? false) === true) {
$deleted++;
}
if (($change['added'] ?? false) === true) {
$added++;
}
}
if ($updated || $added || $deleted) {
$messages[] = "Změny na položkách: přidání: {$added}x; aktualizace: {$updated}x; smazání: {$deleted}x;";
}
break;
}
}
if (!empty($messages)) {
$order->logChange(implode('<br>', array_merge(['[FlexiBee] Provedena aktualizace objednávky podle FlexiBee:'], $messages)));
}
}
}
protected function getOrderNumber(\Order $order): string
{
return $order->order_no;
}
protected function getOrderVariableSymbol(\Order $order): string
{
return preg_replace('/\D*/', '', $order->order_no);
}
protected function getOrderStatus(\Order $order, ?int $flexiBeeId = null): string
{
if ($flexiBeeId) {
// pokud je objednavka stornovana tak stornuju i ve flexi
if ($order->status_storno == 1) {
return 'stavDoklObch.storno';
}
// pokud je to update objednavky ve FlexiBee, tak chci zachovat stav objednavky, kterej je ve FlexiBee
if ($flexiStatus = ($this->flexiBeeApi->getOrder($flexiBeeId)['stavUzivK'] ?? false)) {
return $flexiStatus;
}
}
return 'stavDoklObch.nespec';
}
protected function getOrderNote(\Order $order): string
{
return $order->note_user ?: '';
}
protected function getOrderRoundDirection(\Order $order): string
{
$currency = Contexts::get(CurrencyContext::class)->getOrDefault($order->getCurrency());
return match ($currency->getPriceRoundDirection()) {
'up' => 'zaokrJak.nahoru',
'down' => 'zaokrJak.dolu',
default => 'zaokrJak.matem',
};
}
protected function getOrderRounding(\Order $order): string
{
/** @var Currency $currency */
$currency = Contexts::get(CurrencyContext::class)->getOrDefault($order->getCurrency());
switch ($currency->getPriceRoundOrder() / 100) {
case 1:
return 'zaokrNa.jednotky';
case 0.01:
return 'zaokrNa.setiny';
case 0.001:
return 'zaokrNa.tisiciny';
case 0.05:
return 'zaokrNa.5setiny';
case 0.1:
return 'zaokrNa.desetiny';
case 0.5:
return 'zaokrNa.5desetiny';
case -0.05:
return 'zaokrNa.5jednotky';
case -0.1:
return 'zaokrNa.desitky';
}
return 'zaokrNa.zadne';
}
protected function getOrderDescription(\Order $order): ?string
{
return null;
}
/** Stitky do FlexiBee */
protected function getOrderFlags(\Order $order): ?string
{
return null;
}
protected function getOrderItemData(\Order $order, OrderItem $item): array
{
$itemFlexiId = null;
$itemType = 'typPolozky.obecny';
if ($item->getProductId()) {
// pokud mam FlexiID, tak je to katalogova polozka
if ($itemFlexiId = $this->flexiBeeUtil->getProductFlexiId($item->getProductId(), $item->getVariationId())) {
$itemType = 'typPolozky.katalog';
}
}
// pripravim pole s datama polozky
$itemData = [
'id' => $this->getItemId($order, $item),
'typPolozkyK' => $itemType,
'kod' => $item->getCode(),
'eanKod' => $item->getEAN(),
'nazev' => $item->getDescr(),
'mnozMj' => $order->status_storno == 1 ? 0 : $item->getPieces(),
'typCenyDphK' => 'typCeny.sDph',
'cenaMj' => $item->getPiecePrice()->getPriceWithVat(false)->asFloat(),
'szbDph' => $item->getVat(),
];
// pokud mam flexi ID, tak doplnim vazbu na cenik, sklad a reknu, ze se ma rezervovat
if ($itemFlexiId) {
$itemData += $this->addFlexiBeeParams($order, $item, $itemFlexiId);
}
return $itemData;
}
protected function addFlexiBeeParams(\Order $order, OrderItem $item, $itemFlexiId): array
{
$result['cenik'] = $itemFlexiId;
$result['sklad'] = $this->getOrderStore(
$order,
$item->getProductId(),
$item->getVariationId()
);
if ($item->getPieces() > 0 && $order->status_storno != 1) {
$result['rezervovat'] = true;
}
return $result;
}
protected function saveOrderPackageNumber(\Order $order, array $item): void
{
if (empty($item['doprava'])) {
return;
}
$packageId = explode(';', $item['doprava']);
$packageId = reset($packageId);
if (!empty($packageId) && $order->package_id !== $packageId) {
// ulozim package id na objekt - napr. kvuli naslednemu odeslani mailu
$order->package_id = $packageId;
// ulozim package id k objednavce v DB
sqlQueryBuilder()
->update('orders')
->directValues(['package_id' => $packageId])
->where(Operator::equals(['id' => $order->id]))
->execute();
// zaloguju cislo baliku k objednavce
$order->logHistory(sprintf('[FlexiBee] Číslo balíku: %s', $packageId));
}
}
protected function changeOrderStatus(\Order $order, string $flexiStatus, ?int $status, ?string $orderMessage = null): void
{
// Chci odessilat jen mail, bez zmeny stavu
if ($status === null && $orderMessage !== null) {
$order->logHistory('[FlexiBee] Odeslání e-mailu: '.$orderMessage);
$order->sendEmail(null, $orderMessage);
return;
}
// Pokud neni co menit, tak jdu pryc
if ($status === null || $order->status == $status) {
return;
}
// zmenit stav a pripadne odeslat i mail
$order->logHistory('[FlexiBee] Změna stavu: '.$flexiStatus);
$order->changeStatus($status, null, null, $orderMessage);
}
private function getOrderFirmName(\Order $order, string $type = 'invoice'): string
{
if (!empty($order->{$type.'_firm'})) {
return $order->{$type.'_firm'};
}
return $order->{$type.'_name'}.' '.$order->{$type.'_surname'};
}
protected function getOrderUser(\Order $order): ?int
{
// registrovany uzivatel
if ($flexiUserId = $this->getFlexiUser($order)) {
return $flexiUserId;
}
// nejaky default uzivatel ve FlexiBee pro objednavky bez registrace
$defaultFlexiUserId = $this->configuration->getOrderConfig()['id_user'] ?? null;
if (!empty($defaultFlexiUserId)) {
return (int) $defaultFlexiUserId;
}
// pokud nechteji pouzivat default uzivatele (napr. kvuli tomu, ze by ty udaje byly videt na fakture ve FlexiBee), tak musim
// do FlexiBee zapsat i uzivatele bez registrace - primarne je to kvuli rezervacim, protoze bez uzivatele se ve FlexiBee neudela rezervace
if (!($flexiUserId = $this->flexiBeeApi->getUserIdByCode('WPJ-ORDER'.$order->id))) {
$flexiUserId = $this->userSynchronizer->processUserToFlexiBee(
$this->userSynchronizer->getOrderUserData($order)
);
}
return $flexiUserId;
}
protected function getOrderCentral(\Order $order): ?int
{
if ($centralId = ($this->configuration->getOrderConfig()['central'] ?? null)) {
return (int) $centralId;
}
return null;
}
protected function getOrderStore(\Order $order, int $productId, ?int $variationId = null): ?int
{
$storeId = sqlQueryBuilder()
->select('id_store')
->from('stores_items')
->where(Operator::equalsNullable(['id_product' => $productId, 'id_variation' => $variationId]))
->execute()->fetchOne();
if (!$storeId) {
$storeId = sqlQueryBuilder()
->select('id_store')
->from('stores_items')
->where(Operator::equalsNullable(['id_product' => $productId]))
->execute()->fetchOne();
}
if ($storeId) {
return $this->flexiBeeUtil->getFlexiId(StoreSynchronizer::getType(), (int) $storeId);
}
return $this->getDefaultStoreId($order);
}
private function isOrderDeliveryAddressSame(\Order $order): bool
{
$invoice = [$order->invoice_firm, $order->invoice_name, $order->invoice_country, $order->invoice_street, $order->invoice_city, $order->invoice_zip];
$delivery = [$order->delivery_firm, $order->delivery_name, $order->delivery_country, $order->delivery_street, $order->delivery_city, $order->delivery_zip];
return $invoice === $delivery;
}
protected function getDefaultStoreId(\Order $order): ?int
{
return null;
}
protected function getFlexiUser(\Order $order): ?int
{
if ($order->id_user) {
$flexiId = sqlQueryBuilder()
->select('id_flexi')
->from('flexi_users')
->where(Operator::equals(['id_user' => $order->id_user]))
->execute()->fetchOne();
if ($flexiId) {
return (int) $flexiId;
}
}
return null;
}
protected function getPaymentStatus(\Order $order): ?string
{
if ($deliveryType = $order->getDeliveryType()) {
$payment = $deliveryType->getPayment();
if ($payment instanceof \Hotovost) {
if ($order->isPaid()) {
return 'stavUhr.uhrazenoRucne';
}
}
}
return null;
}
protected function getPaymentTypeCode(\Order $order): string
{
if ($deliveryType = $order->getDeliveryType()) {
if ($code = ($this->configuration->getPaymentsConfig()[$deliveryType->id_payment] ?? false)) {
return 'code:'.$code;
}
}
return '';
}
protected function getDeliveryTypeCode(\Order $order): string
{
if ($deliveryType = $order->getDeliveryType()) {
$delivery = $deliveryType->getDelivery();
if ($code = ($this->configuration->getDeliveriesConfig()[$delivery->id] ?? false)) {
return 'code:'.$code;
}
}
return '';
}
protected function getBaseOrdersQueryBuilder(): QueryBuilder
{
return sqlQueryBuilder()
->select('o.id, fo.id_flexi, fo.data')
->from('orders', 'o')
->leftJoin('o', 'flexi_orders', 'fo', 'fo.id_order = o.id');
}
protected function preprocessChangedItems(array $items): array
{
// nafetchuju polozky objednavek pokud mam zapnutou obousmernou synchronizaci
if (($this->configuration->getOrderConfig()['duplex_sync'] ?? false) === 'Y') {
if (!empty($items)) {
$orderItems = $this->flexiBeeApi->getOrdersItems(array_map(fn ($x) => $x['id'], $items));
foreach ($items as &$item) {
$item['items'] = $orderItems[$item['id']] ?? [];
}
}
}
return $items;
}
private function getOrderMessageNameById(int $orderMessageId): ?string
{
static $orderMessageCache = [];
if (($orderMessageCache[$orderMessageId] ?? false) === false) {
$orderMessageName = sqlQueryBuilder()
->select('name')
->from('emails')
->where(Operator::equals(['id' => $orderMessageId]))
->execute()->fetchOne();
if (!$orderMessageName) {
$orderMessageName = null;
}
$orderMessageCache[$orderMessageId] = $orderMessageName;
}
return $orderMessageCache[$orderMessageId];
}
protected function getItemId(\Order $order, OrderItem $item): string
{
return 'ext:OBP-POL-ESHOP:'.$order->order_no.'-'.$item->getId();
}
public function getOrderDate(\Order $order): string
{
return $order->date_created->format('Y-m-d');
}
protected function getDocumentType(\Order $order): string
{
return 'OBP';
}
}

View File

@@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
namespace External\FlexiBeeBundle\Synchronizers;
use External\FlexiBeeBundle\AbraFlexiTypes\Odberatel;
use KupShop\PricelistBundle\Util\PriceListWorker;
use Query\Operator;
use Symfony\Contracts\Service\Attribute\Required;
class PriceListSynchronizer extends BaseSynchronizer
{
protected static string $type = 'pricelist';
protected static ?string $evidenceClass = Odberatel::class;
protected PriceListWorker $priceListWorker;
#[Required]
final public function setPriceListWorker(PriceListWorker $priceListWorker): void
{
$this->priceListWorker = $priceListWorker;
}
public function syncSingleItem(int $id, ?int $flexiId = null): void
{
$flexiId ??= $this->flexiBeeUtil->getProductFlexiId($id);
if (!$flexiId) {
return;
}
foreach ($this->flexiBeeApi->getPricelistPrices(flexiProductIds: [$flexiId]) as $item) {
$this->processItem($item);
}
}
protected function processItem(array $item): void
{
// zajimaji nas jen zaznamy s nastavenou Cenikovou skupinou, protoze bez toho nepozname o ktery cenik se jedna
if (empty($item['skupCen']) || empty($item['skupCen']->ref)) {
return;
}
$flexiId = $this->flexiBeeUtil->getFlexiIdFromRef($item['skupCen']->ref);
if (!in_array($flexiId, $this->configuration->getEnabledPricelists())
&& !in_array(0, $this->configuration->getEnabledPricelists())) {
return;
}
if (!($priceListId = $this->flexiBeeUtil->getMapping(static::getType(), $flexiId))) {
$priceListId = $this->createOrAssignPriceList($flexiId, $item);
}
$this->updateProductPriceListPrice($priceListId, $this->getInfo($item, 'cenik'), $item['prodejCena']);
}
protected function getItems(?int $lastSyncTime = null): iterable
{
if ($this->mode === self::MODE_NORMAL) {
return parent::getItems($lastSyncTime);
}
if ($this->mode === self::MODE_FULL) {
return $this->flexiBeeApi->getPricelistPrices();
}
return [];
}
protected function createOrAssignPriceList(int $id, array $item): int
{
$name = $this->getInfo($item, 'skupCen');
$currencyCode = $this->flexiBeeUtil->parseFlexiCode($item['mena']?->value) ?? 'CZK';
$priceList = $this->priceListWorker->findPriceList($name, $currencyCode);
if (!$priceList) {
return sqlGetConnection()->transactional(function () use ($id, $name, $currencyCode) {
sqlQueryBuilder()
->insert('pricelists')
->directValues(
[
'name' => $name,
'price_history' => 0,
'currency' => $currencyCode,
]
)
->execute();
$priceListId = (int) sqlInsertId();
$this->flexiBeeUtil->createMapping(static::getType(), $id, $priceListId);
return $priceListId;
});
}
$this->flexiBeeUtil->createMapping(static::getType(), $id, $priceList);
return $priceList;
}
protected function getInfo(array $item, string $key): string
{
return explode(':', $item[$key]->value, 2)[1];
}
protected function updateProductPriceListPrice(int $priceListId, string $productCode, float $price, bool $removeVat = true): void
{
static $productVatCache = [];
if (!($mapping = $this->flexiBeeUtil->findItemByCode($productCode))) {
return;
}
[$productId, $variationId] = $mapping;
$price = \Decimal::create($price, 4);
if ($removeVat) {
if (!($productVatCache[$productId] ?? false)) {
$vat = sqlQueryBuilder()
->select('v.vat')
->from('products', 'p')
->join('p', 'vats', 'v', 'v.id = p.vat')
->where(Operator::equals(['p.id' => $productId]))
->sendToMaster()
->execute()->fetchOne();
if ($vat === false) {
$vat = getVat();
}
$productVatCache[$productId] = toDecimal((float) $vat);
}
$price = $price->removeVat($productVatCache[$productId]);
}
$this->priceListWorker->updatePriceList($priceListId, $productId, $variationId, $price, withDiscountUpdate: false);
}
}

View File

@@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
namespace External\FlexiBeeBundle\Synchronizers;
use AbraFlexi\Cenik;
use Query\Operator;
class PriceSynchronizer extends BaseSynchronizer
{
protected static string $type = 'price';
protected static ?string $evidenceClass = Cenik::class;
protected function process(?int $lastSyncTime = null): void
{
parent::process($lastSyncTime);
$this->updateProductsPriceByVariations();
}
protected function getItems(?int $lastSyncTime = null): iterable
{
if ($this->mode === self::MODE_NORMAL) {
return parent::getItems($lastSyncTime);
}
if ($this->mode === self::MODE_FULL) {
return $this->flexiBeeApi->getPrices();
}
return [];
}
public function syncSingleItem(int $id, ?int $flexiId = null): void
{
if ($flexiId = $this->flexiBeeUtil->getFlexiId(ProductSynchronizer::getType(), $id)) {
$items = $this->preprocessChangedItems([$this->flexiBeeApi->getProductPrices($flexiId)]);
$this->processItem(reset($items));
}
}
protected function processItem(array $item): void
{
if (!($mapping = $this->flexiBeeUtil->getItemMapping($item['id']))) {
return;
}
[$productId, $variationId] = $mapping;
if ($variationId) {
sqlQueryBuilder()
->update('products_variations')
->directValues(['price' => $item['cenaZaklBezDph']])
->where(Operator::equals(['id' => $variationId]))
->execute();
} else {
sqlQueryBuilder()
->update('products')
->directValues(['price' => $item['cenaZaklBezDph']])
->where(Operator::equals(['id' => $productId]))
->execute();
}
}
private function updateProductsPriceByVariations(): void
{
sqlQuery('UPDATE products p
JOIN products_variations pv on p.id = pv.id_product
SET p.price = (
SELECT COALESCE(MIN(IF(pv2.price=0, null, pv2.price)), p.price)
FROM products_variations pv2
WHERE pv2.id_product = p.id
);');
}
}

View File

@@ -0,0 +1,334 @@
<?php
declare(strict_types=1);
namespace External\FlexiBeeBundle\Synchronizers;
use AbraFlexi\Cenik;
use External\FlexiBeeBundle\Exception\FlexiBeeException;
use External\FlexiBeeBundle\Util\FlexiBeeUtil;
use KupShop\CatalogBundle\Product\ProductFinder;
use KupShop\KupShopBundle\Query\JsonOperator;
use KupShop\OSSVatsBundle\Util\VatsUtil;
use Query\Operator;
use Symfony\Contracts\Service\Attribute\Required;
class ProductSynchronizer extends BaseSynchronizer
{
protected static string $type = 'product';
protected static ?string $evidenceClass = Cenik::class;
private ProductFinder $productFinder;
private VatsUtil $vatsUtil;
private SupplySynchronizer $supplySynchronizer;
private PriceListSynchronizer $priceListSynchronizer;
#[Required]
public function setProductFinder(ProductFinder $productFinder): void
{
$this->productFinder = $productFinder;
}
#[Required]
public function setVatsUtil(VatsUtil $vatsUtil): void
{
$this->vatsUtil = $vatsUtil;
}
#[Required]
public function setFlexiBeeUtil(FlexiBeeUtil $flexiBeeUtil): void
{
$this->flexiBeeUtil = $flexiBeeUtil;
}
#[Required]
final public function setSupplySynchronizer(SupplySynchronizer $supplySynchronizer): void
{
$this->supplySynchronizer = $supplySynchronizer;
}
#[Required]
final public function setPriceListSynchronizer(PriceListSynchronizer $priceListSynchronizer): void
{
$this->priceListSynchronizer = $priceListSynchronizer;
}
protected function processItem(array $item): void
{
if (empty($item['kod'])) {
return;
}
$flexiId = (int) $item['id'];
// kouknout, zda uz mam pro tenhle produkt vytvoreny mapping
if (!($mapping = $this->flexiBeeUtil->getItemMapping($flexiId))) {
// pokud mapping nemam, tak zkusim najit variantu podle kodu zbozi
if ($variationId = $this->flexiBeeUtil->getVariationId($item['kod'])) {
$productId = sqlQueryBuilder()
->select('id_product')
->from('products_variations')
->where(Operator::equals(['id' => $variationId]))
->execute()->fetchColumn();
$mapping = [(int) $productId, $variationId];
// nasel jsem variantu, takze vytvorim mapping
$this->flexiBeeUtil->createItemMapping($flexiId, $productId, $variationId);
// zkusim najit produkt podle kodu zbozi
} elseif ($productId = $this->flexiBeeUtil->getProductId($item['kod'])) {
$mapping = [$productId, null];
sqlQueryBuilder()
->update('products')
->directValues(['data' => json_encode(['masterCode' => $item['kod']])])
->where(Operator::equals(['id' => $productId]))
->execute();
// smazu pripadny existujici mapping, protoze se mohlo stat, ze mapping uz existoval
// stacilo ve flexi vytvorit produkt, sesynchronizovat a pak ho smazat ve flexi
// na eshopu uz mapping zustal, ale navic se vytvoril spravny mapping
sqlQueryBuilder()
->delete('flexi_products')
->where(Operator::equals(['id_product' => $productId]))
->execute();
// vytvorim mapping, protoze jsem nasel produkt
$this->flexiBeeUtil->createItemMapping($flexiId, $productId);
}
// nemam mapping a ani jsem nanasel produkt / variantu podle kodu
$isForEshop = $this->isItemForEshop($item);
if (!$mapping && (is_null($isForEshop) || $isForEshop)) {
// vytvorim product / variantu
[$productId, $variationId] = $this->createItem($item);
$mapping = [$productId, $variationId];
// vytvorim mapping
$this->flexiBeeUtil->createItemMapping($flexiId, $productId, $variationId);
}
if ($mapping) {
$this->onItemCreated($item, ...$mapping);
}
}
if (!$mapping) {
return;
}
[$productId, $variationId] = $mapping;
// zpracuju produkt nebo variantu
if ($variationId) {
$this->processVariation($productId, $variationId, $item);
} else {
$this->processProduct($productId, $item);
}
}
public function processProduct(int $productId, array $item): void
{
$values = $this->getProductUpdateValues($item);
sqlQueryBuilder()
->update('products')
->directValues($values)
->where(Operator::equals(['id' => $productId]))
->execute();
}
public function processVariation(int $productId, int $variationId, array $item): void
{
// update variation product
sqlQueryBuilder()
->update('products')
->directValues(
[
'vat' => $this->productFinder->findVAT($this->getVatByType($item['typSzbDphK'])),
]
)
->where(Operator::equals(['id' => $productId]))
->execute();
// update variation
$values = $this->getVariationUpdateValues($item);
sqlQueryBuilder()
->update('products_variations')
->directValues($values)
->where(Operator::equals(['id' => $variationId]))
->execute();
}
protected function getItems(?int $lastSyncTime = null): iterable
{
if ($this->mode === self::MODE_NORMAL) {
return parent::getItems($lastSyncTime);
}
if ($this->mode === self::MODE_FULL) {
return $this->flexiBeeApi->getProducts();
}
return [];
}
protected function createItem(array $item): array
{
if ($this->isVariation($item)) {
$masterCode = $this->getVariationMasterCode($item);
$productId = $this->getProductId($item, $masterCode);
[$labelId, $labelValue] = $this->getVariationParts($item);
$id = (int) \Variations::createProductVariation($productId, [
$labelId => $labelValue,
]);
$result = [$productId, $id];
} else {
$id = $this->getProductId($item);
$result = [$id, null];
}
return $result;
}
protected function onItemCreated(array $item, int $productId, ?int $variationId): void
{
// synchronize product store
$this->supplySynchronizer->syncSingleItem($productId, (int) $item['id']);
// synchronize product pricelists
$this->priceListSynchronizer->syncSingleItem($productId, (int) $item['id']);
}
private function getProductId(array $item, ?string $masterCode = null): int
{
$qb = sqlQueryBuilder()
->select('id')
->from('products');
if ($masterCode) {
$qb->andWhere(
Operator::orX(
Operator::equals([JsonOperator::value('data', 'masterCode') => $masterCode]),
Operator::equals([JsonOperator::value('data', 'masterCode') => $item['kod']])
)
);
} else {
$qb->andWhere(Operator::equals(['code' => $item['kod']]));
}
$id = $qb->execute()->fetchColumn();
if (!$id) {
$id = sqlGetConnection()->transactional(
function () use ($masterCode, $item) {
$values = [
'title' => $item['nazev'],
'vat' => 1,
'data' => json_encode(['masterCode' => $masterCode ? $masterCode : $item['kod']]),
'date_added' => (new \DateTime())->format('Y-m-d H:i:s'),
];
sqlQueryBuilder()
->insert('products')
->directValues($values)
->execute();
return sqlInsertId();
}
);
}
return (int) $id;
}
protected function getVariationParts(array $item): ?array
{
throw new FlexiBeeException('Not implemented');
}
protected function getVariationMasterCode(array $item): ?string
{
return null;
}
protected function isVariation(array $item): bool
{
return false;
}
protected function getVatByType(string $type): float
{
switch ($type) {
case 'typSzbDph.dphZakl':
return 21;
case 'typSzbDph.dphOsv':
return 0;
case 'typSzbDph.dphSniz':
return 12;
case 'typSzbDph.dphSniz2':
return 10;
}
return 21;
}
protected function isItemForEshop(array $item): ?bool
{
return $item['exportNaEshop'] ?? null;
}
protected function getVariationUpdateValues(array $item): array
{
$values = [
'code' => $item['kod'],
'ean' => empty($item['eanKod']) ? null : $item['eanKod'],
'updated' => (new \DateTime())->format('Y-m-d H:i:s'),
];
$isForEshop = $this->isItemForEshop($item);
if (!is_null($isForEshop)) {
$values['figure'] = $isForEshop ? 'Y' : 'N';
}
return $values;
}
protected function getProductUpdateValues(array $item): array
{
$values = [
'code' => $item['kod'],
'ean' => empty($item['eanKod']) ? null : $item['eanKod'],
'title' => $item['nazev'],
'vat' => $this->productFinder->findVAT($this->getVatByType($item['typSzbDphK'])),
'updated' => (new \DateTime())->format('Y-m-d H:i:s'),
];
$isForEshop = $this->isItemForEshop($item);
if (!is_null($isForEshop)) {
$values['figure'] = $isForEshop ? 'Y' : 'O';
}
if (findModule(\Modules::OSS_VATS) && !empty($item['nomen']->value)) {
$values['id_cn'] = $this->vatsUtil->getCNKey($this->flexiBeeUtil->parseFlexiCode($item['nomen']->value));
}
return $values;
}
private function updateProductsPriceByVariations(): void
{
sqlQuery('UPDATE products p
JOIN products_variations pv on p.id = pv.id_product
SET p.price = (
SELECT COALESCE(MIN(IF(pv2.price=0, null, pv2.price)), p.price)
FROM products_variations pv2
WHERE pv2.id_product = p.id
);');
}
}

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace External\FlexiBeeBundle\Synchronizers;
use Query\Operator;
class StoreSynchronizer extends BaseSynchronizer
{
protected static string $type = 'store';
protected function processItem(array $item): void
{
if (!($storeId = $this->flexiBeeUtil->getMapping(static::getType(), $item['id']))) {
$storeId = $this->createStore($item['id'], $this->getStoreName($item));
}
sqlQueryBuilder()
->update('stores')
->directValues(
[
'name' => $this->getStoreName($item),
]
)
->where(Operator::equals(['id' => $storeId]))
->execute();
}
protected function createStore(int $flexiId, string $name): int
{
return sqlGetConnection()->transactional(function () use ($flexiId, $name) {
sqlQueryBuilder()
->insert('stores')
->directValues(
[
'name' => $name,
]
)
->execute();
$storeId = (int) sqlInsertId();
$this->flexiBeeUtil->createMapping(static::getType(), $flexiId, $storeId);
return $storeId;
});
}
protected function getStoreName(array $item): string
{
return '[FlexiBee] '.$item['nazev'];
}
protected function getItems(?int $lastSyncTime = null): iterable
{
return $this->flexiBeeApi->getStores();
}
}

View File

@@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
namespace External\FlexiBeeBundle\Synchronizers;
use AbraFlexi\SkladovaKarta;
use KupShop\StoresBundle\Utils\StoresInStore;
class SupplySynchronizer extends BaseSynchronizer
{
protected static string $type = 'supply';
protected static ?string $evidenceClass = SkladovaKarta::class;
private ?StoresInStore $storesInStore = null;
/**
* @required
*/
public function setStoresInStore(?StoresInStore $storesInStore): void
{
$this->storesInStore = $storesInStore;
}
public function syncSingleItem(int $id, ?int $flexiId = null): void
{
$flexiId ??= $this->flexiBeeUtil->getProductFlexiId($id);
if (!$flexiId) {
return;
}
foreach ($this->flexiBeeApi->getStockCards(flexiProductIds: [$flexiId]) as $item) {
$this->processItem($item);
}
}
protected function process(?int $lastSyncTime = null): void
{
parent::process($lastSyncTime);
$this->flexiBeeUtil->recalculateStores();
}
protected function getItems(?int $lastSyncTime = null): iterable
{
if ($this->mode === self::MODE_NORMAL) {
return parent::getItems($lastSyncTime);
}
if ($this->mode === self::MODE_FULL) {
return $this->flexiBeeApi->getStockCards();
}
return [];
}
protected function processItem(array $item): void
{
$flexiId = $this->flexiBeeUtil->getFlexiIdFromRef($item['cenik']->ref);
// nemame product
if (!($mapping = $this->flexiBeeUtil->getItemMapping($flexiId))) {
return;
}
[$productId, $variationId] = $mapping;
// najdu ID skladu
$storeId = $this->getStoreId(
$this->flexiBeeUtil->getFlexiIdFromRef($item['sklad']->ref)
);
// pokud nemam ID skladu, tak ten sklad pravdepodobne nesynchronizujeme, takze muzu ignorovat
if (!$storeId) {
return;
}
// aktualizuju sklad produktu na danem skladu
$this->storesInStore->updateStoreItem(
[
'id_store' => $storeId,
'id_product' => $productId,
'id_variation' => $variationId,
// use floor so for example 0.5 is floored to 0
'quantity' => floor((float) $item['dostupMj']),
],
false
);
}
protected function getStoreId(int $flexiId): ?int
{
static $storeIdCache = [];
if ($storeIdCache[$flexiId] ?? false) {
return $storeIdCache[$flexiId];
}
if (!($storeId = $this->flexiBeeUtil->getMapping(StoreSynchronizer::getType(), $flexiId))) {
return null;
}
return $storeIdCache[$flexiId] = $storeId;
}
protected function getItemsFilter(): array
{
return ['ucetObdobi' => 'code:'.$this->flexiBeeApi->getCurrentAccountingPeriod()];
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace External\FlexiBeeBundle\Synchronizers;
interface SynchronizerInterface
{
public static function getType(): string;
public function sync(): void;
public function syncSingleItem(int $id): void;
public function setMode(int $mode);
}

View File

@@ -0,0 +1,207 @@
<?php
declare(strict_types=1);
namespace External\FlexiBeeBundle\Synchronizers;
use AbraFlexi\RO;
use Query\Operator;
class UserSynchronizer extends BaseSynchronizer
{
protected static string $type = 'user';
protected function process(?int $lastSyncTime = null): void
{
$this->processToFlexiBee($lastSyncTime);
}
protected function processItem(array $item): void
{
}
protected function getItems(?int $lastSyncTime = null): iterable
{
return [];
}
private function processToFlexiBee(?int $lastSyncTime): void
{
if (isLocalDevelopment()) {
return;
}
$qb = sqlQueryBuilder()
->select('u.id, u.email, fu.id_flexi')
->from('users', 'u')
->leftJoin('u', 'flexi_users', 'fu', 'u.id = fu.id_user')
->where(Operator::equals(['u.figure' => 'Y']));
$specs[] = 'fu.id_flexi IS NULL';
if ($this->mode === self::MODE_NORMAL && $lastSyncTime) {
$dateTime = $this->createDateTime($lastSyncTime);
$specs[] = 'u.date_updated > :lastSyncTime';
$qb->setParameter('lastSyncTime', $dateTime->format('Y-m-d H:i:s'));
}
$qb->andWhere(Operator::orX($specs));
foreach ($qb->execute() as $item) {
$user = \User::createFromId($item['id']);
$user->fetchAddresses();
try {
$flexiId = $item['id_flexi'] ? (int) $item['id_flexi'] : null;
// pokud ho eshop jeste nezapsal do Flexi, tak ho zkusim ve Flexi najit
if (!$flexiId) {
$flexiId = $this->flexiBeeApi->getUserIdByEmail($item['email']);
}
// provedu zalozeni nebo aktualizaci uzivatele ve Flexi
$flexiId = $this->processUserToFlexiBee(
$this->getUserData($user, $flexiId)
);
// pokud uzivatel jeste nema mapping, tak ho vytvorim
if ($flexiId && !$item['id_flexi']) {
$this->flexiBeeUtil->createMapping(UserSynchronizer::getType(), $flexiId, (int) $user->id);
}
} catch (\Throwable $e) {
$this->logger->exception($e, '[FlexiBee] Během synchronizace uživatele se vyskytla chyba!', [
'email' => $user->email,
]);
}
}
}
public function processUserToFlexiBee(array $userData): ?int
{
try {
if ($flexiId = $this->flexiBeeApi->createOrUpdateUser($userData)) {
return $flexiId;
}
} catch (\Throwable $e) {
$this->logger->exception($e, '[FlexiBee] Během synchronizace uživatele se vyskytla chyba!', [
'email' => $userData['email'],
]);
}
return null;
}
public function getUserData(\User $user, ?int $flexiId = null): array
{
$invoiceName = $this->getUserName($user);
if (empty($invoiceName)) {
// invoice name cannot be empty
$invoiceName = $user->email;
}
$item = [
'typVztahuK' => 'typVztahu.odberatel',
'email' => $user->email,
'canceled' => $user->figure == 'N',
'nazev' => $invoiceName,
'ulice' => $user->invoice['street'],
'mesto' => $user->invoice['city'],
'psc' => $user->invoice['zip'],
'ic' => $user->invoice['ico'],
'dic' => $user->invoice['dic'],
'stat' => $user->invoice['country'] ? 'code:'.$user->invoice['country'] : '',
'postovniShodna' => $this->isUserDeliveryAddressSame($user),
'faNazev' => $this->getUserName($user, 'delivery'),
'faUlice' => $user->delivery['street'],
'faMesto' => $user->delivery['city'],
'faPsc' => $user->delivery['zip'],
'faStat' => $user->delivery['country'] ? 'code:'.$user->delivery['country'] : '',
];
if ($user->id_pricelist && ($flexiPriceListId = $this->flexiBeeUtil->getFlexiId('pricelist', $user->id_pricelist))) {
$item['skupCen'] = $flexiPriceListId;
}
if ($flexiId) {
// pokud uzivatel uz existuje, tak vyplnim id, aby se provedl jeho update
$item['id'] = $flexiId;
} else {
// pokud uzivatel jeste neexistuje, tak vyplnim jen kod, abychom ho zalozili s nejakym identifikatorem
if ($user->id === null && !empty($user->orderId)) {
// kvuli uzivatelum v objednavkach bez registrace
$item['kod'] = 'WPJ-ORDER'.$user->orderId;
} else {
// standartni uzivatel
$item['kod'] = 'WPJ'.$user->id;
}
}
$dateTime = !empty($user->date_reg) ? \DateTime::createFromFormat('Y-m-d H:i:s', $user->date_reg) : new \DateTime();
if ($dateTime && $dateTime->format('Y-m-d H:i:s') !== '0000-00-00 00:00:00') {
$item['datZaloz'] = RO::dateToFlexiDate($dateTime);
}
return $item;
}
/** Vytvorim dummy uzivatele pro objednavku bez uzivatele */
public function getOrderUserData(\Order $order): array
{
$user = new \User();
$user->id = null;
$user->orderId = (int) $order->id;
$user->email = $order->invoice_email;
$user->figure = 'Y';
$user->invoice = [
'name' => $order->invoice_name,
'surname' => $order->invoice_surname,
'firm' => $order->invoice_firm,
'street' => $order->invoice_street,
'city' => $order->invoice_city,
'zip' => $order->invoice_zip,
'ico' => $order->invoice_ico,
'dic' => $order->invoice_dic,
'country' => $order->invoice_country,
];
$user->delivery = [
'name' => $order->delivery_name,
'surname' => $order->delivery_surname,
'firm' => $order->delivery_firm,
'street' => $order->delivery_street,
'city' => $order->delivery_city,
'zip' => $order->delivery_zip,
'country' => $order->delivery_country,
];
return $this->getUserData($user);
}
private function getUserName(\User $user, string $type = 'invoice'): string
{
$parts = [];
if (!empty($user->{$type}['firm'])) {
$parts[] = $user->{$type}['firm'];
}
$parts[] = $user->{$type}['name'].' '.$user->{$type}['surname'];
return trim(implode(', ', array_filter($parts)));
}
private function isUserDeliveryAddressSame(\User $user): bool
{
$invoice = [$user->invoice['firm'], $user->invoice['name'], $user->invoice['country'], $user->invoice['street'], $user->invoice['city'], $user->invoice['zip']];
$delivery = array_filter([$user->delivery['firm'], $user->delivery['name'], $user->delivery['country'], $user->delivery['street'], $user->delivery['city'], $user->delivery['zip']]);
if (empty($delivery) || count($delivery) == 1) {
return true;
}
return $invoice === $delivery;
}
}

View File

@@ -0,0 +1,613 @@
<?php
declare(strict_types=1);
namespace External\FlexiBeeBundle\Util;
use AbraFlexi\Adresar;
use AbraFlexi\Cenik;
use AbraFlexi\Changes;
use AbraFlexi\Company;
use AbraFlexi\FakturaVydana;
use AbraFlexi\ObjednavkaPrijata;
use AbraFlexi\ObjednavkaPrijataPolozka;
use AbraFlexi\SkladovaKarta;
use AbraFlexi\StromCenik;
use AbraFlexi\UcetniObdobi;
use External\FlexiBeeBundle\AbraFlexiTypes\CenikovaSkupina;
use External\FlexiBeeBundle\AbraFlexiTypes\FakturaVydanaPolozka;
use External\FlexiBeeBundle\AbraFlexiTypes\FormaDopravy;
use External\FlexiBeeBundle\AbraFlexiTypes\Odberatel;
use External\FlexiBeeBundle\AbraFlexiTypes\Sklad;
use External\FlexiBeeBundle\AbraFlexiTypes\Stredisko;
use External\FlexiBeeBundle\Exception\FlexiBeeException;
class FlexiBeeApi
{
/**
* Data, které u jednotlivých typů načítáme.
*
* https://demo.flexibee.eu/c/demo/{evidence}/properties
*/
protected array $defaultColumnsByEvidence = [
'adresar' => [
'id', 'kod', 'typVztahuK', 'email', 'canceled', 'nazev', 'ulice', 'mesto', 'psc', 'tel', 'mobil', 'ic',
'dic', 'stat', 'postovniShodna', 'faJmenoFirmy', 'faUlice', 'faMesto', 'faPsc', 'faStat', 'stitky', 'splatDny', 'skupCen',
],
'skladova-karta' => [
'lastUpdate', 'sklad', 'cenik', 'stavMJ', 'dostupMj',
],
'strom-cenik' => [
'id', 'idZaznamu', 'uzel',
],
'cenik' => [
'id', 'lastUpdate', 'typZasobyK', 'kod', 'eanKod', 'nazev', 'cenaZaklBezDph', 'typSzbDphK', 'sumDostupMj', 'hmotMj', 'hmotObal', 'exportNaEshop',
],
'objednavka-prijata' => [
'id', 'lastUpdate', 'kod', 'cisDosle', 'mena', 'stavUzivK', 'storno', 'formaDopravy', 'formaUhradyCis',
'kontaktEmail', 'kontaktJmeno', 'kontaktTel', 'nazFirmy', 'ulice', 'mesto', 'psc', 'ic', 'dic', 'stat',
'postovniShodna', 'faNazev', 'faUlice', 'faMesto', 'faPsc', 'faStat', 'doprava',
],
'objednavka-prijata-polozka' => [
'id', 'lastUpdate', 'doklObch', 'kod', 'nazev', 'cisRad', 'typPolozkyK', 'mnozMj', 'typCenyDphK', 'szbDph', 'cenaMj', 'cenik', 'storno', 'stornoPol', 'slevaPol',
],
'odberatel' => [
'id', 'lastUpdate', 'kodIndi', 'prodejCena', 'cenik', 'skupCen', 'mena',
],
];
private FlexiBeeConfiguration $configuration;
private FlexiBeeUtil $flexiBeeUtil;
public function __construct(FlexiBeeConfiguration $configuration, FlexiBeeUtil $flexiBeeUtil)
{
$this->configuration = $configuration;
$this->flexiBeeUtil = $flexiBeeUtil;
}
/**
* Zkontroluje připojení k FlexiBee - ověří správnost údajů vyplněných v administraci.
*/
public function isConnectionValid(): bool
{
try {
$company = $this->getApiWorker(Company::class);
$data = $company->getFlexiData();
} catch (FlexiBeeException $e) {
return false;
}
return !empty($data);
}
public function isChangesAPIEnabled(): bool
{
$changes = $this->getApiWorker(Changes::class);
return (bool) $changes->getStatus();
}
public function changesAPIEnable(): bool
{
$changes = $this->getApiWorker(Changes::class);
return (bool) $changes->enable();
}
/**
* Vrací seznam změněných objektů ve FlexiBee.
*/
public function getChanges(int $start, ?string $evidenceClass = null, int $limit = 500): array
{
$changes = $this->getApiWorker(Changes::class);
// Musim to takhle hacknout, protoze jinak to zacne hazet chybu, ze to nezna "evidence" column
// To changes API je asi pomerne novy a ta knihovna to nema uplne posefeny
$changes->urlParams = array_merge($changes->urlParams, [
'evidence' => ['type' => 'string', 'description' => 'Evidence selection'],
]);
$evidence = null;
// Pokud chci zmeny pro pouze urcitou evidenci
if ($evidenceClass) {
$evidence = $this->getApiWorker($evidenceClass)->getEvidence();
}
$filter = array_filter([
'start' => $start,
'evidence' => $evidence,
'limit' => $limit,
]);
return $changes->getAllFromAbraFlexi(!empty($filter) ? $filter : null);
}
public function createOrUpdateUser(array $user): int
{
$adresar = $this->getApiWorker(Adresar::class);
$adresar->insertToAbraFlexi($user);
return (int) $adresar->getLastInsertedId();
}
public function getUserIdByEmail(string $email): ?int
{
$adresar = $this->getApiWorker(Adresar::class);
$results = $adresar->getAllFromAbraFlexi(
$this->getFilters(['id'], ['email' => $email])
);
$result = reset($results);
return $result ? (int) $result['id'] : null;
}
public function getUserIdByCode(string $code): ?int
{
$adresar = $this->getApiWorker(Adresar::class);
$results = $adresar->getAllFromAbraFlexi(
$this->getFilters(['id'], ['kod' => $code])
);
$result = reset($results);
return $result ? (int) $result['id'] : null;
}
public function getUsers(?\DateTime $lastUpdate = null): iterable
{
$adresar = $this->getApiWorker(Adresar::class);
return $this->withPagination(
$adresar,
$this->getFilters(
$this->getColumnsByWorker($adresar),
[],
$lastUpdate
)
);
}
/**
* Vrátí položky podle zadaných ID z dané evidence.
*/
public function getEvidenceDataByIds(string $evidenceClass, array $ids, ?array $columns = null, array $filters = []): array
{
$worker = $this->getApiWorker($evidenceClass);
if ($columns === null) {
$columns = $this->getColumnsByWorker($worker);
}
return $worker->getAllFromAbraFlexi(
$this->getFilters($columns, array_merge(['limit' => 0, 'id IN ('.implode(',', $ids).')'], $filters))
);
}
public function getOrderInvoice(string $id): ?array
{
return $this->fetchInvoiceByField('id', 'ext:FAV:'.$id);
}
/**
* Vrátí pdf fakturu.
*/
public function getOrderInvoicePdf(\Order $order): ?string
{
$invoice = $this->fetchInvoiceByField('cisObj', $order->order_no);
if (!$invoice) {
return null;
}
$invoiceWorker = $this->getApiWorker(FakturaVydana::class);
$invoiceWorker->processInit($invoice);
return $invoiceWorker->getInFormat('pdf', 'fakturaKB$$SUM', $order->getLanguage());
}
private function fetchInvoiceByField(string $field, string $value): ?array
{
$apiWorker = $this->getApiWorker(FakturaVydana::class);
$results = $apiWorker->getAllFromAbraFlexi(
$this->getFilters([], [$field => $value])
);
$result = reset($results);
return $result ?: null;
}
public function updateOrderInvoice(array $data): int
{
$fakturaVydana = $this->getApiWorker(FakturaVydana::class);
$fakturaVydana->insertToAbraFlexi($data);
if (!($flexiInvoiceId = $fakturaVydana->getLastInsertedId())) {
throw new FlexiBeeException('Invoice was not updated');
}
return (int) $flexiInvoiceId;
}
public function realizeOrder(string $invoiceNumber, array $dataRealize, array $dataInvoice): int
{
$objednavkaPrijata = $this->getApiWorker(ObjednavkaPrijata::class);
$objednavkaPrijata->insertToAbraFlexi($dataRealize);
if (!($flexiOrderId = $objednavkaPrijata->getLastInsertedId())) {
throw new FlexiBeeException('Order failure! Order was not successfully realized in Flexi');
}
// give Flexi some time
sleep(1);
if ($invoice = $this->getOrderInvoice($invoiceNumber)) {
if (!empty($invoice['id'])) {
$dataInvoice['id'] = $invoice['id'];
$this->updateOrderInvoice($dataInvoice);
}
}
return (int) $flexiOrderId;
}
public function createOrUpdateOrder(array $data): int
{
$objednavkaPrijata = $this->getApiWorker(ObjednavkaPrijata::class);
$objednavkaPrijata->insertToAbraFlexi($data);
if (!($flexiOrderId = $objednavkaPrijata->getLastInsertedId())) {
throw new FlexiBeeException('Something went wrong during sending order to FlexiBee! Last inserted ID was not returned.');
}
return (int) $flexiOrderId;
}
public function getOrder(int $flexiOrderId): ?array
{
$objednavkaPrijata = $this->getApiWorker(ObjednavkaPrijata::class);
$data = $objednavkaPrijata->getAllFromAbraFlexi(
$this->getFilters($this->getColumnsByWorker($objednavkaPrijata), ['limit' => 0, 'id' => $flexiOrderId])
);
$data = reset($data);
if (empty($data)) {
return null;
}
return $data;
}
public function getOrdersItems(array $ids): array
{
$objednavkaPrijataPolozka = $this->getApiWorker(ObjednavkaPrijataPolozka::class);
$items = $objednavkaPrijataPolozka->getAllFromAbraFlexi(
$this->getFilters($this->getColumnsByWorker($objednavkaPrijataPolozka), ['limit' => 0, 'doklObch IN ('.implode(',', $ids).')'])
);
$itemsByOrders = [];
foreach ($items as $item) {
$itemsByOrders[$this->flexiBeeUtil->getFlexiIdFromRef($item['doklObch']->ref)][] = $item;
}
return $itemsByOrders;
}
public function getOrderItems(int $flexiOrderId): array
{
$objednavkaPrijataPolozka = $this->getApiWorker(ObjednavkaPrijataPolozka::class);
return $objednavkaPrijataPolozka->getAllFromAbraFlexi(
$this->getFilters($this->getColumnsByWorker($objednavkaPrijataPolozka), ['limit' => 0, 'doklObch' => $flexiOrderId])
);
}
public function getInvoiceItems(int $flexiOrderId): array
{
$fakturaVydanaPolozka = $this->getapiWorker(FakturaVydanaPolozka::class);
return $fakturaVydanaPolozka->getAllFromAbraFlexi(
$this->getFilters($this->getColumnsByWorker($fakturaVydanaPolozka), ['limit' => 0, 'doklFak' => $flexiOrderId])
);
}
/**
* Vrátí všechna střediska ve FlexiBee.
*/
public function getCentrals(): array
{
return $this->getApiWorker(Stredisko::class)
->getAllFromAbraFlexi();
}
public function getDeliveries(): array
{
return $this->getApiWorker(FormaDopravy::class)
->getAllFromAbraFlexi();
}
/**
* Vrátí seznam skladů ve FlexiBee.
*
* @param bool $all - pokud je false, tak se sklady filtrují podle nastavení v administraci
*/
public function getStores(bool $all = false): array
{
$store = $this->getApiWorker(Sklad::class);
$filter = null;
if (!$all) {
$filter = ['id IN ('.implode(',', $this->configuration->getEnabledStoreIds()).')'];
}
return $store->getAllFromAbraFlexi($filter);
}
/**
* Vrátí seznam ceníků ve FlexiBee.
*/
public function getPriceLists(): array
{
$priceList = $this->getApiWorker(CenikovaSkupina::class);
return $priceList->getAllFromAbraFlexi();
}
/**
* Vrátí skladové karty z FlexiBee.
*/
public function getStockCards(?\DateTime $lastUpdate = null, ?array $flexiProductIds = null): iterable
{
$skladovaKarta = $this->getApiWorker(SkladovaKarta::class);
$filter = [
'limit' => 1000,
'sklad in ('.implode(',', $this->configuration->getEnabledStoreIds()).')',
'no-ext-ids' => true,
'ucetObdobi' => 'code:'.$this->getCurrentAccountingPeriod(),
];
if ($flexiProductIds !== null) {
$filter[] = 'cenik in ('.implode(',', $flexiProductIds).')';
}
return $this->withPagination(
$skladovaKarta,
$this->getFilters(
$this->getColumnsByWorker($skladovaKarta),
$filter,
$lastUpdate
)
);
}
/**
* Vrátí aktuální účetní období fe FlexiBee.
*/
public function getCurrentAccountingPeriod(bool $force = false): string
{
$cacheKey = 'flexiBee_currentAccountingPeriod';
if (!$force && ($currentAccountingPeriod = getCache($cacheKey))) {
return $currentAccountingPeriod;
}
$ucetniObdobi = $this->getApiWorker(UcetniObdobi::class);
$date = (new \DateTime())->format('Y-m-d');
$filters = [
'platiOdData <= "'.$date.'" and platiDoData >= "'.$date.'"',
];
$accountingPeriod = $ucetniObdobi->getAllFromAbraFlexi(
$this->getFilters(['id', 'kod'], $filters)
);
$accountingPeriod = reset($accountingPeriod);
if (!$accountingPeriod) {
throw new FlexiBeeException('Current accounting period not found!');
}
setCache($cacheKey, $accountingPeriod['kod'], 3600);
return $accountingPeriod['kod'];
}
public function getProductsSections(): array
{
$stromCenik = $this->getApiWorker(StromCenik::class);
$filter = [
'limit' => 0,
];
return $stromCenik->getAllFromAbraFlexi(
$this->getFilters(
$this->getColumnsByWorker($stromCenik),
$filter
)
);
}
/**
* Vrátí produkty z FlexiBee.
*/
public function getProducts(?\DateTime $lastUpdate = null): iterable
{
$cenik = $this->getApiWorker(Cenik::class);
$filter = [
'limit' => 500,
'typZasobyK in ("typZasoby.zbozi", "typZasoby.vyrobek")',
];
return $this->withPagination(
$cenik,
$this->getFilters(
$this->getColumnsByWorker($cenik),
$filter,
$lastUpdate
)
);
}
public function getPricelistPrices(?\DateTime $lastUpdate = null, ?array $flexiProductIds = null): iterable
{
$worker = $this->getApiWorker(Odberatel::class);
$filter = [
'limit' => 500,
'skupCen is not null',
];
if ($flexiProductIds !== null) {
$filter[] = 'cenik in ('.implode(',', $flexiProductIds).')';
}
return $this->withPagination(
$worker,
$this->getFilters(
$this->getColumnsByWorker($worker),
$filter,
$lastUpdate,
)
);
}
public function getProductByCode(string $code): ?array
{
$cenik = $this->getApiWorker(Cenik::class);
$filter = [
'limit' => 0,
'kod' => $code,
];
$products = $cenik->getAllFromAbraFlexi(
$this->getFilters($this->getColumnsByWorker($cenik), $filter)
);
$product = reset($products);
if ($product) {
return $product;
}
return null;
}
public function getProductPrices(int $flexiProductId): ?array
{
$cenik = $this->getApiWorker(Cenik::class);
$data = $cenik->getAllFromAbraFlexi(
$this->getFilters($this->getColumnsByWorker($cenik), ['limit' => 0, 'id' => $flexiProductId])
);
$data = reset($data);
if (empty($data)) {
return null;
}
return $data;
}
/**
* Vrátí ceny produktů ve FlexiBee.
*/
public function getPrices(?\DateTime $lastUpdate = null): iterable
{
$cenik = $this->getApiWorker(Cenik::class);
$columns = [
'id',
'lastUpdate',
'kod',
'cenaZaklBezDph',
'typSzbDphK',
];
$filter = [
'limit' => 500,
'typZasobyK in ("typZasoby.zbozi", "typZasoby.vyrobek")',
];
return $this->withPagination(
$cenik,
$this->getFilters($columns, $filter, $lastUpdate)
);
}
/**
* Pomocná funkce pro stránkování.
*/
private function withPagination(FlexiBeeApiWorker $worker, array $filters = []): iterable
{
$totalRows = $this->getDataRowCount($worker, $filters);
$limit = $filters['limit'] ?? 100;
$iterationLimit = $totalRows / $limit;
for ($i = 0; $i < $iterationLimit; $i++) {
// posunout offset
$filters['start'] = $i * $limit;
// nacist data
foreach ($worker->getAllFromAbraFlexi($filters) as $item) {
yield $item;
}
}
}
/**
* Pomocná funkce, která vrací celkový počet položek pro daný filtr.
*/
private function getDataRowCount(FlexiBeeApiWorker $worker, array $filters): int
{
$filters['limit'] = 1;
$filters['add-row-count'] = 'true';
$worker->getAllFromAbraFlexi($filters);
return (int) $worker->rowCount;
}
protected function getColumnsByWorker(FlexiBeeApiWorker $worker): array
{
$evidence = $worker->getEvidence();
$columns = $this->defaultColumnsByEvidence[$evidence] ?? [];
if (findModule(\Modules::OSS_VATS) && $evidence == 'cenik') {
$columns[] = 'nomen';
}
return $columns;
}
private function getFilters(array $columns = [], array $filters = [], ?\DateTime $lastUpdate = null): array
{
if (!empty($columns)) {
$filters['detail'] = 'custom:'.implode(',', $columns);
}
if ($lastUpdate) {
$filters['lastUpdate'] = '> '.$lastUpdate->format('Y-m-d H:i:s');
}
return $filters;
}
private function getApiWorker(string $worker): FlexiBeeApiWorker
{
return FlexiBeeApiWorker::create($worker, $this->configuration->getAPIConfig());
}
}

View File

@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace External\FlexiBeeBundle\Util;
use AbraFlexi\Exception;
use AbraFlexi\RO;
use AbraFlexi\RW;
use External\FlexiBeeBundle\Exception\FlexiBeeException;
/**
* Class FlexiBeeApiWorker.
*
* @property $rowCount
* @property $urlParams
*
* @method updateColumnsInfo($columnsInfo = null, $evidence = null)
* @method insertToAbraFlexi($data = null)
* @method getAllFromAbraFlexi($conditions = null, $indexBy = null)
* @method deleteFromAbraFlexi($id = null)
* @method dateToFlexiDate(\DateTime $date)
* @method getNextRecordID($conditions = [])
* @method getEvidence()
* @method getLastInsertedId()
* @method getFlexiData(string $suffix = '', $conditions = null)
* @method getFlexiRow($recordID)
* @method getData()
* @method processInit($init)
* @method getInFormat($format, $reportName = null, $lang = null, $sign = false)
*/
class FlexiBeeApiWorker
{
private ?RO $worker = null;
public static function create(string $worker, array $config): self
{
$wrapper = new static();
return $wrapper->setWorker(
new $worker(null, $config)
);
}
public function setWorker(RO $worker): self
{
$this->worker = $worker;
return $this;
}
public function __get($name)
{
if (property_exists($this, $name)) {
return $this->{$name};
}
if (property_exists($this->worker, $name)) {
return $this->worker->{$name};
}
return null;
}
public function __set($name, $value)
{
if (property_exists($this->worker, $name)) {
return $this->worker->{$name} = $value;
}
return $this->{$name} = $value;
}
public function __call($name, $arguments)
{
try {
$result = call_user_func_array([$this->worker, $name], $arguments);
} catch (Exception $e) {
// ta knihovna ma nejaky rozbity message, ktere to vypisuje v exceptione, takze to tady jen catchnu
// a vyhodim nasi FlexiBeeException se spravnou message
foreach ($e->getErrorMessages() as $error) {
throw new FlexiBeeException(
sprintf('%s: %s', $error['code'] ?? 'ERROR', $error['message']),
!empty($error['code']) && is_numeric($error['code']) ? (int) $error['code'] : 0,
$e
);
}
throw new FlexiBeeException($e->getMessage(), $e->getCode(), $e);
}
return $result;
}
public function __destruct()
{
if ($this->worker instanceof RW) {
$this->worker->disconnect();
}
unset($this->worker);
}
}

View File

@@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace External\FlexiBeeBundle\Util;
use External\FlexiBeeBundle\Exception\FlexiBeeException;
use KupShop\SynchronizationBundle\Util\SynchronizationConfiguration;
class FlexiBeeConfiguration
{
private SynchronizationConfiguration $configuration;
public function __construct(SynchronizationConfiguration $configuration)
{
$this->configuration = $configuration;
}
/**
* Vrací konfiguraci pro API potřebnou pro připojení (url, user, password, company).
*/
public function getAPIConfig(): array
{
$api = $this->getConfig()['api'] ?? [];
foreach (['company', 'url', 'user', 'password'] as $requiredField) {
if (empty($api[$requiredField])) {
throw new FlexiBeeException(
sprintf('Missing required value in API config: %s', $requiredField)
);
}
}
return $api;
}
/**
* Vrací konfiguraci objednávek.
*/
public function getOrderConfig(): array
{
return $this->getConfig()['config']['order'] ?? [];
}
/**
* Vrací konfiguraci doprav.
*/
public function getDeliveriesConfig(): array
{
return $this->getConfig()['config']['delivery'] ?? [];
}
/**
* Vrací konfiguraci plateb.
*/
public function getPaymentsConfig(): array
{
return $this->getConfig()['config']['payment'] ?? [];
}
/**
* Vrací konfiguraci stavů objednávek.
*/
public function getOrderStatusesConfig(): array
{
return $this->getOrderConfig()['statuses'] ?? [];
}
/**
* Vrací povolené typy synchronizací, které se mají spouštět.
*/
public function getEnabledSynchronizerTypes(): array
{
return $this->getConfig()['config']['enabled_types'] ?? [];
}
/**
* Vrací ID skladů ve FlexiBee, které máme synchronizovat.
*/
public function getEnabledStoreIds(): array
{
return array_map(fn ($x) => (int) $x, $this->getConfig()['config']['stores'] ?? [0]);
}
public function getEnabledPricelists(): array
{
return array_map(fn ($x) => (int) $x, $this->getConfig()['config']['pricelists'] ?? [-1]);
}
public function refresh(): void
{
$this->configuration->clear('flexibee');
}
/**
* Vrací konfiguraci z settings tabulky.
*/
private function getConfig(): array
{
$dbcfg = \Settings::getDefault();
if (!$this->configuration->has('flexibee')) {
$this->configuration->set('flexibee', fn () => $dbcfg->loadValue('flexibee') ?: []);
}
return $this->configuration->get('flexibee');
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace External\FlexiBeeBundle\Util;
use External\FlexiBeeBundle\Exception\FlexiBeeException;
use External\FlexiBeeBundle\Synchronizers\SynchronizerInterface;
use Symfony\Component\DependencyInjection\ServiceLocator;
class FlexiBeeLocator
{
private ServiceLocator $locator;
public function __construct(ServiceLocator $locator)
{
$this->locator = $locator;
}
public function getTypes(): array
{
return $this->locator->getProvidedServices();
}
public function getServiceByType(string $type): SynchronizerInterface
{
if (!$this->locator->has($type)) {
throw new FlexiBeeException(
sprintf('Unknown synchronizer type \'%s\'!', $type)
);
}
return $this->locator->get($type);
}
}

View File

@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace External\FlexiBeeBundle\Util;
use Doctrine\DBAL\Exception\DeadlockException;
use Doctrine\DBAL\Exception\LockWaitTimeoutException;
use External\FlexiBeeBundle\Exception\FlexiBeeException;
use KupShop\AdminBundle\Util\ActivityLog;
use KupShop\KupShopBundle\Util\Logging\SentryLogger;
use Psr\Log\LoggerInterface;
class FlexiBeeLogger
{
private ActivityLog $activityLog;
private SentryLogger $sentryLogger;
private LoggerInterface $logger;
public function __construct(ActivityLog $activityLog, SentryLogger $sentryLogger, LoggerInterface $logger)
{
$this->activityLog = $activityLog;
$this->sentryLogger = $sentryLogger;
$this->logger = $logger;
}
/**
* Zaloguje exceptionu do ActivityLogu, pripadne do Sentry.
*/
public function exception(\Throwable $e, ?string $message = null, array $data = []): void
{
if (isLocalDevelopment()) {
throw $e;
}
if (!($e instanceof FlexiBeeException)) {
$this->sentryLogger->captureException($e);
}
if ($message === null) {
$message = $e->getMessage();
}
$data = array_merge(
[
'exception_message' => $e->getMessage(),
],
$data
);
// Lock wait timeout a deadlock nechci logovat do ActivityLogu
if ($e instanceof LockWaitTimeoutException || $e instanceof DeadlockException) {
return;
}
$this->activityLog->addActivityLog(
ActivityLog::SEVERITY_ERROR,
ActivityLog::TYPE_SYNC,
$message,
array_unique($data)
);
}
public function activity(string $message, array $data = [], string $severity = ActivityLog::SEVERITY_NOTICE): void
{
$this->activityLog->addActivityLog(
$severity,
ActivityLog::TYPE_SYNC,
'[FlexiBee] '.$message,
$data
);
}
/**
* Zaloguje data do kibany.
*/
public function data(string $message, array $data): void
{
if (isLocalDevelopment()) {
return;
}
$this->logger->notice($message, $data);
}
}

View File

@@ -0,0 +1,293 @@
<?php
declare(strict_types=1);
namespace External\FlexiBeeBundle\Util;
use AbraFlexi\Relation;
use External\FlexiBeeBundle\Synchronizers\BaseSynchronizer;
use KupShop\CatalogBundle\Util\SetsUpdateStore;
use Query\Operator;
class FlexiBeeUtil
{
public function __construct(
private readonly FlexiBeeLocator $synchronizerLocator,
private readonly FlexiBeeLogger $logger,
private readonly SetsUpdateStore $setsUpdateStore,
) {
}
public function getOrderNumber(\Order $order): string
{
return 'WPJ-'.$order->order_no;
}
public function getMapping(string $type, int $flexiId): ?int
{
$id = sqlQueryBuilder()
->select('id_'.$type)
->from('flexi_'.$type.'s')
->where(Operator::equals(['id_flexi' => $flexiId]))
->execute()->fetchOne();
if (!$id) {
return null;
}
return (int) $id;
}
public function getFlexiId(string $type, int $shopId): ?int
{
$flexiId = sqlQueryBuilder()
->select('id_flexi')
->from('flexi_'.$type.'s')
->where(Operator::equals(['id_'.$type => $shopId]))
->execute()->fetchOne();
if (!$flexiId) {
return null;
}
return (int) $flexiId;
}
public function deleteMapping(string $type, int $flexiId): void
{
sqlQueryBuilder()
->delete('flexi_'.$type.'s')
->andWhere(Operator::equals(['id_flexi' => $flexiId]))
->execute();
}
public function createMapping(string $type, int $flexiId, int $shopId): void
{
sqlQueryBuilder()
->insert('flexi_'.$type.'s')
->directValues(
[
'id_flexi' => $flexiId,
'id_'.$type => $shopId,
]
)
->execute();
}
public function createItemMapping(int $flexiId, int $productId, ?int $variationId = null): void
{
sqlQueryBuilder()
->insert('flexi_products')
->directValues(
[
'id_flexi' => $flexiId,
'id_product' => $productId,
'id_variation' => $variationId,
]
)->execute();
}
public function setFlexiOrderData(int $orderId, string $key, $value): void
{
sqlGetConnection()->transactional(function () use ($orderId, $key, $value) {
$data = sqlQueryBuilder()
->select('data')
->from('flexi_orders')
->where(Operator::equalsNullable(['id_order' => $orderId]))
->execute()->fetchOne();
$data = json_decode($data ?: '', true) ?? [];
$data[$key] = $value;
sqlQueryBuilder()
->update('flexi_orders')
->directValues(['data' => json_encode($data)])
->where(Operator::equals(['id_order' => $orderId]))
->execute();
});
}
public function getProductFlexiId(int $productId, ?int $variationId = null): ?int
{
$flexiId = sqlQueryBuilder()
->select('id_flexi')
->from('flexi_products')
->where(
Operator::equalsNullable(
[
'id_product' => $productId,
'id_variation' => $variationId,
]
)
)
->execute()->fetchColumn();
if (!$flexiId) {
return null;
}
return (int) $flexiId;
}
public function getItemMapping(int $flexiId): ?array
{
$data = sqlQueryBuilder()
->select('id_product, id_variation')
->from('flexi_products')
->where(Operator::equals(['id_flexi' => $flexiId]))
->execute()->fetch();
if (!$data) {
return null;
}
return [(int) $data['id_product'], $data['id_variation'] ? (int) $data['id_variation'] : null];
}
public function getProductId(string $code): ?int
{
$id = sqlQueryBuilder()
->select('id')
->from('products')
->where(Operator::equals(['code' => $code]))
->execute()->fetchOne();
if (!$id) {
return null;
}
return (int) $id;
}
public function getVariationId(string $code): ?int
{
$id = sqlQueryBuilder()
->select('id')
->from('products_variations')
->where(Operator::equals(['code' => $code]))
->execute()->fetchOne();
if (!$id) {
return null;
}
return (int) $id;
}
public function getFlexiIdFromRef(string $ref): int
{
$parts = explode('/', $ref);
return (int) end($parts);
}
public function parseFlexiCode($itemPart): ?string
{
if ($itemPart instanceof Relation) {
$itemPart = $itemPart->value;
}
if (is_array($itemPart)) {
$itemPart = reset($itemPart);
}
$code = explode(':', $itemPart);
$code = end($code);
return !empty($code) ? $code : null;
}
public function recalculateStores(): void
{
$getQuantitySubQuery = function (bool $variations = false) {
$alias = 'p';
$productIdColumn = 'id';
if ($variations) {
$alias = 'pv';
$productIdColumn = 'id_product';
}
$qb = sqlQueryBuilder()
->select('COALESCE(SUM(GREATEST(si.quantity, 0)), 0)')
->from('stores_items', 'si')
->where('si.id_product = '.$alias.'.'.$productIdColumn);
if ($variations) {
$qb->andWhere('si.id_variation = pv.id');
} else {
$qb->andWhere('si.id_variation IS NULL');
}
return $qb;
};
$productsSubQuery = $getQuantitySubQuery();
$productsQuantityQb = sqlQueryBuilder()
->update('products', 'p')
->leftJoin('p', 'products_variations', 'pv', 'pv.id_product = p.id')
->set('p.in_store', "({$productsSubQuery->getSQL()})")
->andWhere('pv.id IS NULL AND p.in_store != ('.$productsSubQuery->getSQL().')')
->addQueryBuilderParameters($productsSubQuery);
// ignorujeme sety - protoze ty se prepocitaji zvlast pomoci `setsUpdateStore->updateSetsStore()`
if (findModule(\Modules::PRODUCT_SETS, \Modules::SUB_CALCULATE_STOCK)) {
$productsQuantityQb->leftJoin('p', 'products_sets', 'ps', 'ps.id_product = p.id')
->andWhere('ps.id_product IS NULL');
}
$productsQuantityQb->execute();
$variationsSubQuery = $getQuantitySubQuery(true);
sqlQuery(
'UPDATE products_variations pv
SET pv.in_store = ('.$variationsSubQuery->getSQL().')
WHERE pv.in_store != ('.$variationsSubQuery->getSQL().');', $variationsSubQuery->getParameters(), $variationsSubQuery->getParameterTypes());
\Variations::recalcInStore();
if (findModule(\Modules::PRODUCT_SETS, \Modules::SUB_CALCULATE_STOCK)) {
$this->setsUpdateStore->updateSetsStore();
}
}
public function synchronize(array $types, int $mode = BaseSynchronizer::MODE_NORMAL): void
{
foreach ($types as $type) {
try {
$synchronizer = $this->synchronizerLocator->getServiceByType($type);
$synchronizer->setMode($mode);
$synchronizer->sync();
} catch (\Throwable $e) {
if (isDevelopment()) {
throw $e;
}
$this->logger->exception($e, '[FlexiBee] Během synchronizace se vyskytla chyba!', [
'type' => $type,
'mode' => $mode,
]);
}
}
}
public function findItemByCode(string $code): ?array
{
$result = null;
if ($variationId = $this->getVariationId($code)) {
$productId = sqlQueryBuilder()
->select('id_product')
->from('products_variations')
->andWhere(Operator::equals(['id' => $variationId]))
->execute()->fetchOne();
$result = [(int) $productId, $variationId];
} elseif ($productId = $this->getProductId($code)) {
$result = [$productId, null];
}
return $result;
}
}