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

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