260 lines
12 KiB
PHP
260 lines
12 KiB
PHP
<?php
|
|
|
|
namespace KupShop\POSBundle\Util;
|
|
|
|
use KupShop\ContentBundle\View\Exception\ValidationException;
|
|
use KupShop\GraphQLBundle\ApiPos\Types\Warehouse\PosWarehouseCollection;
|
|
use KupShop\GraphQLBundle\ApiPos\Types\Warehouse\PosWarehouseItem;
|
|
use KupShop\KupShopBundle\Context\PosContext;
|
|
use KupShop\KupShopBundle\Util\Contexts;
|
|
use KupShop\KupShopBundle\Util\Database\QueryHint;
|
|
use KupShop\KupShopBundle\Util\LoggingContext;
|
|
use KupShop\OrderingBundle\Entity\Purchase\PurchaseState;
|
|
use KupShop\WarehouseBundle\Entity\StoreItem;
|
|
use KupShop\WarehouseBundle\Util\StoreItemWorker;
|
|
use Query\Operator;
|
|
use Query\QueryBuilder;
|
|
|
|
class PosWarehouseUtil
|
|
{
|
|
public function __construct(
|
|
protected LoggingContext $loggingContext,
|
|
protected ?StoreItemWorker $storeItemWorker = null,
|
|
) {
|
|
}
|
|
|
|
public function getWarehouseItem(PosEntity $pos, \Product|\Variation $product, ?int $forceVariationId = null): ?PosWarehouseCollection
|
|
{
|
|
if (!findModule(\Modules::WAREHOUSE)) {
|
|
return null;
|
|
}
|
|
|
|
$posWarehouseCollection = [];
|
|
$storePositions = $this->storeItemWorker->getProductPositions(function (QueryBuilder $qb) use ($product, $pos, $forceVariationId) {
|
|
$qb->addSelect('wpos.id AS id_position');
|
|
$qb->andWhere(Operator::inStringArray($pos->getWarehouseLocations(), 'wl.id'));
|
|
|
|
if ($forceVariationId) {
|
|
$qb->andWhere(Operator::equals(['wp.id_product' => $product->id, 'wp.id_variation' => $forceVariationId]));
|
|
} elseif ($product instanceof \Variation) {
|
|
$qb->andWhere(Operator::equals(['wp.id_product' => $product->id, 'wp.id_variation' => $product->variationId]));
|
|
} else {
|
|
$qb->andWhere(Operator::equals(['wp.id_product' => $product->id]));
|
|
}
|
|
});
|
|
|
|
foreach ($storePositions as $position) {
|
|
$posWarehouseCollection[] = new PosWarehouseItem($position['code'], $position['pieces'], null, $position['id_position']);
|
|
}
|
|
|
|
return new PosWarehouseCollection($posWarehouseCollection);
|
|
}
|
|
|
|
public function checkPurchaseStateItemsStockIn(PurchaseState $purchaseState, PosEntity $pos, ?PurchaseState $previousPurchaseState = null): array
|
|
{
|
|
$productsInPositions = [];
|
|
foreach ($this->getPurchaseItemsFromPurchaseState($purchaseState) as $purchaseItem) {
|
|
// Textové položky se nekontrolují
|
|
if (!$purchaseItem->getIdProduct()) {
|
|
continue;
|
|
}
|
|
|
|
// Pokud mám produkt již v objednávce, tak kontroluju pouze počet přidaných kusů
|
|
// Od kusů z purchase statu odečítám kusy z vytvořené objednávky (x > 0 = kontroluju rozdíl, x < 0 - odebírám a chci aby kontrola prošla)
|
|
$checkPiecesCount = $purchaseItem->getPieces();
|
|
if ($previousPurchaseState && key_exists($purchaseItem->getId(), $previousPurchaseState->getProducts())) {
|
|
$checkPiecesCount = $checkPiecesCount - ($previousPurchaseState->getProducts()[$purchaseItem->getId()]->getPieces() ?? 0);
|
|
}
|
|
|
|
// Pokud jde o dárek, tak se stejně tak musi kontrolovat rozdílná skladovost při editaci
|
|
if ($previousPurchaseState && key_exists($purchaseItem->getId(), $previousPurchaseState->getDiscounts())) {
|
|
$checkPiecesCount = $checkPiecesCount - ($previousPurchaseState->getDiscounts()[$purchaseItem->getId()]->getPieces() ?? 0);
|
|
}
|
|
|
|
// Pokud kontroluju na produktu 0 kusů, tak vůbec neprovádím kontroly -> přeskakuji
|
|
if ($checkPiecesCount == 0) {
|
|
continue;
|
|
}
|
|
|
|
// Kontrola jestli jsou kusy dostupné na skladě/skladech -
|
|
// in_store na produktu kontroluju jen v pripade ze neni modul stores - protoze pokladna muze byt na externim sklade a pak by to tu nikdy neproslo
|
|
if (findModule(\Modules::STORES)) {
|
|
if ($checkPiecesCount > $this->loadOverallInStoreForPos($purchaseItem->getProduct(), $pos)) {
|
|
throw new ValidationException("Produkt {$purchaseItem->getName()} není v požadovaném množství na skladu.");
|
|
}
|
|
} else {
|
|
// Kontrola jestli je celkem k dispozici více kusů než je v obj.
|
|
if ($checkPiecesCount > $purchaseItem->getProduct()->inStore) {
|
|
throw new ValidationException("Produkt {$purchaseItem->getName()} není dostupný v požadovaném množství.");
|
|
}
|
|
}
|
|
|
|
// Kontrola jestli jsou kusy na přiřazených pozicích
|
|
if (findModule(\Modules::WAREHOUSE)) {
|
|
$piecesSum = 0;
|
|
|
|
$productsInPositions[$purchaseItem->getId()] = $this->searchProductInLocations($purchaseItem->id_product, $purchaseItem->id_variation, $pos);
|
|
$piecesSum += array_sum(array_column($productsInPositions[$purchaseItem->getId()], 'pieces'));
|
|
|
|
if ($checkPiecesCount > $piecesSum) {
|
|
throw new ValidationException("Produkt {$purchaseItem->getName()} není v požadovaném množství na pozicích.");
|
|
}
|
|
}
|
|
}
|
|
|
|
return $productsInPositions;
|
|
}
|
|
|
|
private function getPurchaseItemsFromPurchaseState(PurchaseState $purchaseState): array
|
|
{
|
|
return QueryHint::withRouteToMaster(function () use ($purchaseState) {
|
|
$purchaseState->getProductList()
|
|
->fetchVariations(true, false)
|
|
->getProducts()
|
|
->fetchStoresInStore(findModule(\Modules::PRODUCTS_VARIATIONS));
|
|
|
|
return $purchaseState->getProducts();
|
|
});
|
|
}
|
|
|
|
public function loadOverallInStoreForPos(\Product|\Variation $product, PosEntity $pos, ?int $forceVariationId = null): float|int
|
|
{
|
|
if (!findModule(\Modules::STORES)) {
|
|
return $product->variations[$forceVariationId]['in_store'] ?? $product->inStore ?? 0;
|
|
}
|
|
|
|
if ($forceVariationId) {
|
|
$storesInStores = $product->storesInStoreByVariations[$forceVariationId] ?? [];
|
|
} elseif ($product instanceof \Variation) {
|
|
$storesInStores = $product->storesInStoreByVariations[$product->variationId] ?? [];
|
|
} else {
|
|
$storesInStores = $product->storesInStore ?? [];
|
|
}
|
|
|
|
$searchStores = $pos->getStores();
|
|
$posStores = array_filter($storesInStores, function ($key) use ($searchStores) {
|
|
return in_array($key, $searchStores);
|
|
}, ARRAY_FILTER_USE_KEY);
|
|
|
|
return array_sum(array_column($posStores, 'in_store'));
|
|
}
|
|
|
|
public function updateBoxToMatchOrder(\Order $order, array $warehouseDiffs, int $idBox): void
|
|
{
|
|
$entity = Contexts::get(PosContext::class)->getActive();
|
|
$this->loggingContext->setIdOrders($order->id);
|
|
|
|
// Prochází se všechny rozdíly mezi boxem a objednávkou
|
|
foreach ($warehouseDiffs as $item) {
|
|
// Tahle podmínka by teoreticky neměla nastat, protže by to
|
|
if ($item['pieces'] === $item['pieces_box']) {
|
|
throw new ValidationException('V porovnání se objevil produkt, který nemá rozdíl položek');
|
|
}
|
|
|
|
// V objednávce mám méně položek než je v boxu, a proto se zásoba přehodí na stůl pro vrácení
|
|
if ($item['pieces'] < $item['pieces_box']) {
|
|
$idReturnTable = $entity->getIdReturnTable();
|
|
$toRemove = $item['pieces_box'] - $item['pieces'];
|
|
|
|
// Pokud mám šarže, tak vyhazuju ty s nejedlším datem expirace, aby se prodaly ty s nejbližším
|
|
if (findModule(\Modules::PRODUCTS_BATCHES)) {
|
|
// Seřadím šarže podle date expirace
|
|
usort($item['batches_in_box'], fn ($a, $b) => $b['date_expiry'] <=> $a['date_expiry']);
|
|
foreach ($item['batches_in_box'] as $batch) {
|
|
if ($toRemove === 0) {
|
|
continue;
|
|
}
|
|
|
|
// Když je na pozici méně kusů než potřebuju přesunout, tak přesunu max možný počet a pokračuju na další dostupnou pozici
|
|
$item['id_product_batch'] = $batch['id_product_batch'];
|
|
$this->storeItemWorker->moveBetweenPositions(
|
|
storeItem: new StoreItem($item),
|
|
pieces: ($batch['pieces'] < $toRemove) ? $batch['pieces'] : $toRemove,
|
|
old_position: $idBox,
|
|
new_position: $idReturnTable
|
|
);
|
|
$toRemove -= ($batch['pieces'] < $toRemove) ? $batch['pieces'] : $toRemove;
|
|
}
|
|
} else {
|
|
// Nemám šarže, takže přesouvám vše co můžu
|
|
$this->storeItemWorker->moveBetweenPositions(
|
|
storeItem: new StoreItem($item),
|
|
pieces: $toRemove,
|
|
old_position: $idBox,
|
|
new_position: $idReturnTable
|
|
);
|
|
}
|
|
} elseif ($item['pieces'] > $item['pieces_box']) {
|
|
// V objednávce mám více položek než je v boxu, a proto přenesu z pozic do boxu
|
|
// Všechny dostupné pozice pro produkt/variantu
|
|
$availablePositions = $this->searchProductInLocations($item['id_product'], $item['id_variation'], $entity);
|
|
$toMove = $item['pieces'] - $item['pieces_box'];
|
|
$movedPieces = 0;
|
|
foreach ($availablePositions as $position) {
|
|
// Už je vše přesunuto -> přeskakuju další dostupné pozice
|
|
if ($movedPieces === $toMove) {
|
|
continue;
|
|
}
|
|
|
|
// Pokud mám šarže, tak ji přiřadím
|
|
if (findModule(\Modules::PRODUCTS_BATCHES)) {
|
|
$item['id_product_batch'] = $position['id_product_batch'];
|
|
}
|
|
|
|
// Když je na pozici méně kusů než potřebuju přesunout, tak přesunu max možný počet a pokračuju na další dostupnou pozici
|
|
$this->storeItemWorker->moveBetweenPositions(
|
|
storeItem: new StoreItem($item),
|
|
pieces: ($position['pieces'] < ($toMove - $movedPieces)) ? $position['pieces'] : ($toMove - $movedPieces),
|
|
old_position: $position['id_position'],
|
|
new_position: $idBox
|
|
);
|
|
$movedPieces += ($position['pieces'] < ($toMove - $movedPieces)) ? $position['pieces'] : ($toMove - $movedPieces);
|
|
}
|
|
|
|
if ($toMove != $movedPieces) {
|
|
throw new ValidationException("Pokladna nebyla schopna zpracovat: {$item['message']}");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public function createWarehouseOrderRecord(\Order $order): void
|
|
{
|
|
$entity = Contexts::get(PosContext::class)->getActive();
|
|
sqlQueryBuilder()
|
|
->insert('warehouse_orders')
|
|
->values([
|
|
'id_order' => $order->id,
|
|
'id_position' => $entity->getVirtualBox(),
|
|
'date_start' => 'NOW()',
|
|
'date_finish' => 'NOW()',
|
|
])
|
|
->execute();
|
|
}
|
|
|
|
public function searchProductInLocations($id_product, $id_variation, PosEntity $pos)
|
|
{
|
|
$qb = sqlQueryBuilder()
|
|
->select('wp.*')
|
|
->from('warehouse_products', 'wp')
|
|
->join('wp', 'warehouse_positions', 'wpos', 'wpos.id=wp.id_position')
|
|
->where(Operator::equalsNullable(
|
|
[
|
|
'wp.id_product' => $id_product,
|
|
'wp.id_variation' => $id_variation,
|
|
]))
|
|
->andWhere('pieces != 0');
|
|
|
|
if (findModule(\Modules::PRODUCTS_BATCHES)) {
|
|
$qb->leftJoin('wp', 'products_batches', 'pb', 'wp.id_product_batch = pb.id')
|
|
->addOrderBy('pb.date_expiry');
|
|
}
|
|
|
|
$qb->andWhere('wpos.id IN (:positions)')
|
|
->setParameter('positions', $pos->getWarehousePositions(), \Doctrine\DBAL\Connection::PARAM_INT_ARRAY)
|
|
->addOrderBy('FIELD(wpos.id, :positions)');
|
|
|
|
return $qb->execute()->fetchAllAssociative();
|
|
}
|
|
}
|