first commit
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
$txt_str['productsBatches'] = [
|
||||
'requireSerialNumber' => 'Požadovat sériové číslo',
|
||||
'searchProduct' => 'Produkt',
|
||||
'flapMain' => 'Šarže',
|
||||
'searchNameCode' => 'Název / Kód produktu',
|
||||
'filterByProduct' => 'Podle produktu',
|
||||
'filterBasic' => 'Základní vyhledávání',
|
||||
'searchVariation' => 'Varianty',
|
||||
'searchBtn' => 'Hledat',
|
||||
'delete' => 'Vymazat',
|
||||
'search' => 'Vyhledávání',
|
||||
'productBatch' => 'Kód šarže',
|
||||
'dateExpiry' => 'Datum expirace',
|
||||
'navigation' => 'Šarže',
|
||||
'activityEdited' => 'Upravena šarže',
|
||||
'titleEdit' => 'Upravit šarži',
|
||||
'onlyInStock' => 'Pouze šarže skladem',
|
||||
'dateFrom' => 'Datum od',
|
||||
'dateTo' => 'Datum do',
|
||||
'filterByDate' => 'Podle datumu',
|
||||
'product' => 'Produkt',
|
||||
'toolbar_list' => 'Seznam Šarží',
|
||||
'toolbar_add' => 'Přidat šarži',
|
||||
'batchesNearExpiration' => 'Šarže s blížící se expirací (%s)',
|
||||
'batchesAfterExpiration' => 'Expirované šarže (%s)',
|
||||
];
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
$txt_str['productsBatches'] = [
|
||||
'requireSerialNumber' => 'Require serial number',
|
||||
'searchProduct' => 'Search product',
|
||||
'flapMain' => 'Serial number',
|
||||
'searchNameCode' => 'Name / code',
|
||||
'filterByProduct' => 'By product',
|
||||
'filterBasic' => 'Basic search',
|
||||
'searchVariation' => 'Variations',
|
||||
'searchBtn' => 'Search',
|
||||
'delete' => 'Delete',
|
||||
'search' => 'Search',
|
||||
'serialNumber' => 'Serial number',
|
||||
'searchOrder' => 'Order',
|
||||
'navigation' => 'Serial numbers',
|
||||
'titleEdit' => 'Edit serial number',
|
||||
'batchesNearExpiration' => 'Batches near expiration (%s)',
|
||||
'batchesAfterExpiration' => 'Expired batches (%s)',
|
||||
];
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\ProductsBatchesBundle\Admin\lists;
|
||||
|
||||
use KupShop\AdminBundle\AdminList\BaseList;
|
||||
use KupShop\AdminBundle\AdminList\FiltersStorage;
|
||||
use KupShop\AdminBundle\Query\Invert;
|
||||
use KupShop\DevelopmentBundle\Query\QueryBuilder;
|
||||
use KupShop\KupShopBundle\Util\HtmlBuilder\HTML;
|
||||
use KupShop\StoresBundle\Utils\StoresInStore;
|
||||
use Query\Operator;
|
||||
|
||||
class ProductsBatchesList extends BaseList
|
||||
{
|
||||
use FiltersStorage;
|
||||
|
||||
protected $tableDef = [
|
||||
'id' => 'pb.id',
|
||||
'fields' => [
|
||||
'Šarže' => ['field' => 'code', 'size' => 0.3],
|
||||
'Datum expirace' => ['field' => 'date_expiry', 'size' => 0.3, 'render' => 'renderDate'],
|
||||
'Produkt' => ['field' => 'p_title', 'render' => 'renderName', 'size' => 2, 'type' => 'product', 'type_id' => 'p_id'],
|
||||
'Kód produktu' => ['field' => 'pv_code', 'size' => 0.4],
|
||||
'EAN produktu' => ['field' => 'pv_ean', 'size' => 0.4],
|
||||
'Ve skladu' => ['field' => 'wp_pieces_total', 'size' => 0.4],
|
||||
],
|
||||
];
|
||||
|
||||
public function customizeTableDef($tableDef)
|
||||
{
|
||||
$tableDef = parent::customizeTableDef($tableDef);
|
||||
if (findModule(\Modules::STORES)) {
|
||||
$tableDef['fields']['Externí sklad'] = ['field' => 'si_pieces_total', 'render' => 'renderStores', 'size' => 0.5, 'class' => 'left tiny-list-items'];
|
||||
}
|
||||
|
||||
return $tableDef;
|
||||
}
|
||||
|
||||
public function renderName($values, $column)
|
||||
{
|
||||
$name = [$values['p_title'], $values['pv_title']];
|
||||
|
||||
return join(' - ', array_filter($name));
|
||||
}
|
||||
|
||||
public function renderStores($values): HTML
|
||||
{
|
||||
$qb = sqlQueryBuilder()
|
||||
->select("s.name, COALESCE(SUM(COALESCE(JSON_EXTRACT(custom_data, CONCAT('$.batch_numbers.', {$values['id']})), 0)),0) quantity")
|
||||
->from('stores_items', 'si')
|
||||
->join('si', 'stores', 's', 's.id = si.id_store')
|
||||
->where(
|
||||
Operator::andX(
|
||||
Operator::equals(['si.id_product' => $values['id_product']])),
|
||||
Operator::equalsNullable(['si.id_variation' => $values['id_variation']])
|
||||
)
|
||||
->andWhere(Operator::equals(['s.type' => StoresInStore::TYPE_EXTERNAL_STORE]))
|
||||
->having('quantity > 0')
|
||||
->groupBy('si.id_store')
|
||||
->orderBy('si.id_store');
|
||||
|
||||
$output = HTML::create('span');
|
||||
foreach ($qb->execute() as $item) {
|
||||
$output
|
||||
->tag('div')
|
||||
->text($item['name'].': '.$item['quantity'])
|
||||
->end();
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
public function getQuery()
|
||||
{
|
||||
/** @var QueryBuilder $qb */
|
||||
$qb = sqlQueryBuilder()
|
||||
->select(
|
||||
'pb.*',
|
||||
'p.title p_title',
|
||||
'p.id p_id',
|
||||
'pv.title pv_title',
|
||||
'pv.id pv_id',
|
||||
'COALESCE(pv.code, p.code) pv_code',
|
||||
'COALESCE(pv.ean, p.ean) pv_ean',
|
||||
)->from('products_batches', 'pb')
|
||||
->leftJoin('pb', 'products', 'p', 'p.id = pb.id_product')
|
||||
->leftJoin('pb', 'products_variations', 'pv', 'pv.id = pb.id_variation')
|
||||
->groupBy('pb.id, p.id, pv.id');
|
||||
|
||||
if (findModule(\Modules::STORES)) {
|
||||
$externalSubquery = sqlQueryBuilder()->select("COALESCE(SUM(COALESCE(JSON_EXTRACT(custom_data, CONCAT('$.batch_numbers.',pb.id)), 0)),0) si_pieces_total")
|
||||
->from('stores_items', 'si')
|
||||
->where('pb.id_product = si.id_product && pb.id_variation <=> si.id_variation');
|
||||
|
||||
$qb->addSubselect($externalSubquery, 'si_pieces_total');
|
||||
}
|
||||
|
||||
if (findModule(\Modules::WAREHOUSE)) {
|
||||
$qb->addSelect('SUM(COALESCE(wp.pieces, 0)) wp_pieces_total')
|
||||
->leftJoin('pb', 'warehouse_products', 'wp', 'pb.id = wp.id_product_batch');
|
||||
}
|
||||
|
||||
if ($productBatch = getVal('productBatch')) {
|
||||
$qb->andWhere(
|
||||
Invert::checkInvert(\Query\Operator::equals(['pb.code' => $productBatch]), getVal('productBatch_invert'))
|
||||
);
|
||||
}
|
||||
|
||||
if ($dateFrom = getVal('dateFrom')) {
|
||||
$qb->andWhere('pb.date_expiry >= :pb_date_expiry_from')->setParameter('pb_date_expiry_from',
|
||||
$this->prepareDate($dateFrom).' 00:00:00');
|
||||
}
|
||||
|
||||
if ($dateTo = getVal('dateTo')) {
|
||||
$qb->andWhere('pb.date_expiry <= :pb_date_expiry_to')->setParameter('pb_date_expiry_to',
|
||||
$this->prepareDate($dateTo).' 23:59:59');
|
||||
}
|
||||
|
||||
if (getVal('onlyInStock')) {
|
||||
if (findModule(\Modules::STORES)) {
|
||||
$qb->andHaving('wp_pieces_total > 0 || si_pieces_total > 0');
|
||||
} else {
|
||||
$qb->andHaving('wp_pieces_total > 0');
|
||||
}
|
||||
}
|
||||
|
||||
if ($productId = getVal('idProduct')) {
|
||||
$qb->andWhere(Operator::equals(['pb.id_product' => $productId]));
|
||||
|
||||
if ($variationId = getVal('idVariation')) {
|
||||
if (intval($variationId) > 0) {
|
||||
$qb->andWhere(Operator::equals(['pb.id_variation ' => $variationId]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
public function renderPrint($values, $column)
|
||||
{
|
||||
$href = 'launch.php?s=printCenter.php&type=product&template_type=serialNumber&ID='.$values['p_id'].
|
||||
'&IDv='.$values['pv_id'].
|
||||
'&sn='.$values['psn_serial_number'];
|
||||
|
||||
return HTML::create('a')
|
||||
->attr('title', 'Vytisknout')
|
||||
->attr('href', $href)
|
||||
->tag('span')
|
||||
->class('badge')
|
||||
->tag('i')
|
||||
->class('glyphicon glyphicon-print')
|
||||
->end()
|
||||
->end();
|
||||
}
|
||||
}
|
||||
|
||||
$main_class = ProductsBatchesList::class;
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\ProductsBatchesBundle\Admin;
|
||||
|
||||
use Query\Operator;
|
||||
|
||||
class productsBatches extends \Window
|
||||
{
|
||||
protected $template = 'window/productsBatches.tpl';
|
||||
|
||||
protected $tableName = 'products_batches';
|
||||
|
||||
protected $nameField = 'code';
|
||||
|
||||
public function get_vars()
|
||||
{
|
||||
$vars = parent::get_vars();
|
||||
$pageVars = getVal('body', $vars);
|
||||
|
||||
$data = &$pageVars['data'];
|
||||
if (!empty($data['id_product'])) {
|
||||
$item = sqlQueryBuilder()->select('p.title product_title', 'pv.title variation_title')
|
||||
->from('products', 'p')
|
||||
->leftJoin('p', 'products_variations', 'pv', 'p.id = pv.id_product')
|
||||
->where(Operator::equalsNullable(['p.id' => $data['id_product'], 'pv.id' => $data['id_variation']]))
|
||||
->execute()->fetch();
|
||||
$data['product_title'] = $item['product_title'];
|
||||
$data['variation_title'] = $item['variation_title'];
|
||||
}
|
||||
|
||||
$vars['body'] = $pageVars;
|
||||
|
||||
return $vars;
|
||||
}
|
||||
|
||||
public function getData()
|
||||
{
|
||||
$data = parent::getData();
|
||||
|
||||
if (getVal('Submit')) {
|
||||
$data['date_expiry'] = $this->prepareDate($data['date_expiry']);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
return productsBatches::class;
|
||||
@@ -0,0 +1,94 @@
|
||||
{extends "[shared]/menu.tpl"}
|
||||
|
||||
|
||||
{block name="menu-items" append}
|
||||
|
||||
<li class="nav-header smaller"><i class="glyphicon glyphicon-search"></i><span>{'search'|translate}</span></li>
|
||||
<li class="with_caret "><a href="#" class="opener"><span>{'filterBasic'|translate}</span></a></li>
|
||||
|
||||
<li class="pill-content">
|
||||
<ul class="nav-sub nav-pills">
|
||||
<form id='search' target="mainFrame" method="get" action="launch.php" class="form-inline">
|
||||
<input type="hidden" name="type" value="productsBatches"/><input type="hidden" name="s" value="list.php">
|
||||
|
||||
<div class="form-group">
|
||||
<div class="input-group invert">
|
||||
<input type="text" class="form-control input-sm" name="productBatch" maxlength="20" value=""
|
||||
placeholder="{'productBatch'|translate}"/>
|
||||
{inversion field="productBatch"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<input type="reset" value="{'delete'|translate}" class="btn btn-danger btn-sm"/>
|
||||
<input type="submit" value="{'searchBtn'|translate}" class="btn btn-primary btn-sm"/>
|
||||
</div>
|
||||
</form>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="with_caret"><a href="#" class="opener"><span>{'filterByProduct'|translate}</span></a></li>
|
||||
<li class="pill-content">
|
||||
<ul class="nav-sub nav-pills">
|
||||
<form id='search' target="mainFrame" method="get" action="launch.php" class="form-inline">
|
||||
<input type="hidden" name="type" value="{$type}"/><input type="hidden" name="s" value="list.php">
|
||||
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control input-sm" name="idProduct" maxlength="100" value="" onKeyPress="checkInputData('int')"
|
||||
placeholder="{'searchNameCode'|translate}"/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<select name="idVariation" class="input-sm form-control"></select>
|
||||
<script type="text/javascript">
|
||||
$(function () {
|
||||
initAutocompleteVariation('[name=idProduct]', '[name=idVariation]');
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" class="input" id="onlyInStock0" name="onlyInStock" value="1"><label for="onlyInStock0">{'onlyInStock'|translate}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<input type="reset" id="resetBtn" value="{'delete'|translate}" class="btn btn-danger btn-sm"/>
|
||||
<input type="submit" value="{'searchBtn'|translate}" class="btn btn-primary btn-sm"/>
|
||||
</div>
|
||||
</form>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="with_caret"><a href="#" class="opener"><span>{'filterByDate'|translate}</span></a></li>
|
||||
<li class="pill-content">
|
||||
<ul class="nav-sub nav-pills">
|
||||
<form id='search' target="mainFrame" method="get" action="launch.php" class="form-inline">
|
||||
<input type="hidden" name="type" value="{$type}"/><input type="hidden" name="s" value="list.php">
|
||||
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control input-sm" name="dateFrom" id="dateFrom" maxlength="10" value=""
|
||||
placeholder="{'dateFrom'|translate}" autocomplete="off"/>
|
||||
{insert_calendar selector='#dateFrom' format='date'}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control input-sm" name="dateTo" id="dateTo" maxlength="10" value=""
|
||||
placeholder="{'dateTo'|translate}" autocomplete="off"/>
|
||||
{insert_calendar selector='#dateTo' format='date'}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<input type="checkbox" class="input" id="onlyInStock1" name="onlyInStock" value="1"><label for="onlyInStock1">{'onlyInStock'|translate}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<input type="reset" id="resetBtn" value="{'delete'|translate}" class="btn btn-danger btn-sm"/>
|
||||
<input type="submit" value="{'searchBtn'|translate}" class="btn btn-primary btn-sm"/>
|
||||
</div>
|
||||
</form>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
{/block}
|
||||
@@ -0,0 +1,61 @@
|
||||
{extends "[shared]/window.tpl"}
|
||||
|
||||
{block tabs}
|
||||
{windowTab id='flapMain'}
|
||||
{/block}
|
||||
|
||||
{block tabsContent}
|
||||
<div id="flapMain" class="tab-pane fade active in boxFlex">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="code" class="col-md-2 control-label">
|
||||
{'productBatch'|translate}
|
||||
</label>
|
||||
<div class="col-md-5">
|
||||
<input type="text" class="form-control" id="code" maxlength="100" name="data[code]" value="{$body.data.code}"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="code" class="col-md-2 control-label">
|
||||
{'dateExpiry'|translate}
|
||||
</label>
|
||||
<div class="col-md-5">
|
||||
<input type="text" class="form-control " name="data[date_expiry]" id="date_issued" value="{$body.data.date_expiry|format_date:'admin'}" autocomplete="off" />
|
||||
{insert_calendar selector='#date_issued' format='date'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="code" class="col-md-2 control-label">
|
||||
{'product'|translate}
|
||||
</label>
|
||||
{if empty($body.data.id_product)}
|
||||
<div class="col-md-5">
|
||||
<input type="text" class="form-control input-sm" id="id_product" name="data[id_product]" maxlength="100"
|
||||
value="{$body.data.id_product}" onKeyPress="checkInputData('int')"
|
||||
placeholder="{'searchNameCode'|translate:'orders'}"/>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<select id="id_variation" name="data[id_variation]" class="input-sm form-control selecter"></select>
|
||||
<script type="text/javascript">
|
||||
$(function () {
|
||||
initAutocompleteVariation('#id_product', '#id_variation');
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
{else}
|
||||
<div class="col-md-4">
|
||||
<p class="input-height"><a href="javascript:nw('product', {$body.data.id_product})">{$body.data.product_title} </a></p>
|
||||
</div>
|
||||
<div class="col-md-2 control-label"><label>Varianta</label></div>
|
||||
<div class="col-md-3">
|
||||
<p class=" input-height">{if $body.data.id_variation}{$body.data.variation_title}{else}Nemá{/if}</p>
|
||||
</div>
|
||||
<input type="hidden" name="data[id_product]" value="{$body.data.id_product}"/>
|
||||
<input type="hidden" name="data[id_variation]" value="{$body.data.id_variation}"/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{/block}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\ProductsBatchesBundle\AdminRegister;
|
||||
|
||||
use KupShop\AdminBundle\AdminRegister\AdminRegister;
|
||||
use KupShop\AdminBundle\AdminRegister\IAdminRegisterDynamic;
|
||||
use KupShop\AdminBundle\AdminRegister\IAdminRegisterStatic;
|
||||
|
||||
class ProductsBatchesAdminRegister extends AdminRegister implements IAdminRegisterDynamic, IAdminRegisterStatic
|
||||
{
|
||||
public function getDynamicMenu(): array
|
||||
{
|
||||
return [
|
||||
static::createMenuItem(
|
||||
'stockMenu',
|
||||
[
|
||||
'name' => 'ProductsBatches',
|
||||
'title' => translate('navigation', 'productsBatches'),
|
||||
'left' => 's=menu.php&type=productsBatches',
|
||||
'right' => 's=list.php&type=productsBatches',
|
||||
]
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getPermissions(): array
|
||||
{
|
||||
return [
|
||||
static::createPermissions('ProductsBatches', [\Modules::PRODUCTS_BATCHES], ['PBATCHES']),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\ProductsBatchesBundle\EventListener;
|
||||
|
||||
use KupShop\OrderingBundle\Event\OrderEvent;
|
||||
use KupShop\WarehouseBundle\Entity\StoreItem;
|
||||
use KupShop\WarehouseBundle\Event\WarehouseOrderEvent;
|
||||
use KupShop\WarehouseBundle\Util\StoreItemWorker;
|
||||
use Query\Operator;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class OrderEventListener implements EventSubscriberInterface
|
||||
{
|
||||
/**
|
||||
* @var StoreItemWorker
|
||||
*/
|
||||
protected $worker;
|
||||
|
||||
/**
|
||||
* @required
|
||||
*/
|
||||
public function setWorker(?StoreItemWorker $worker): void
|
||||
{
|
||||
$this->worker = $worker;
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents()
|
||||
{
|
||||
return [
|
||||
OrderEvent::ORDER_STORNO => [
|
||||
['stornoProductsBatchesNumbers', -1],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function stornoProductsBatchesNumbers(OrderEvent $event)
|
||||
{
|
||||
$order = $event->getOrder();
|
||||
|
||||
$warehouse_order = sqlQueryBuilder()
|
||||
->select('*')
|
||||
->from('warehouse_orders')
|
||||
->where(Operator::equals(['id_order' => $order->id]))
|
||||
->andWhere('id_position IS NULL')
|
||||
->andWhere('date_finish IS NOT NULL')
|
||||
->execute()
|
||||
->fetch();
|
||||
|
||||
// Check if order is finished and does not have any box assigned
|
||||
if ($warehouse_order) {
|
||||
$logProducts = sqlQueryBuilder()->select('*')
|
||||
->from('warehouse_log', 'wl')
|
||||
->where(Operator::equals(['wl.id_order' => $order->id]))
|
||||
->andWhere('wl.new_id_position IS NULL')
|
||||
->andWhere('wl.id_product_batch IS NOT NULL')->execute()->fetchAll();
|
||||
|
||||
$event = new WarehouseOrderEvent();
|
||||
$position = $this->worker->getPositionIdByCode($event->getReturnPosition());
|
||||
|
||||
// Select only items that have batch number, subtract them from generated items without batch, and create them again with batch id
|
||||
foreach ($logProducts as $item) {
|
||||
$itemNoBatch = $item;
|
||||
$itemNoBatch['id_product_batch'] = null;
|
||||
$this->worker->updateOnPosition(new StoreItem($itemNoBatch), $position, -$itemNoBatch['pieces']);
|
||||
$this->worker->createStoreItem(new StoreItem($item), $item['pieces'], $position);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\ProductsBatchesBundle\Inspections;
|
||||
|
||||
use KupShop\SystemInspectionBundle\Inspections\Inspection;
|
||||
use KupShop\SystemInspectionBundle\Inspections\User\UserInspectionInterface;
|
||||
use KupShop\SystemInspectionBundle\InspectionWriters\MessageTypes\HtmlDataMessage;
|
||||
use KupShop\SystemInspectionBundle\InspectionWriters\MessageTypes\InspectionMessage;
|
||||
|
||||
class BatchesExpirationInspection extends Inspection implements UserInspectionInterface
|
||||
{
|
||||
/**
|
||||
* @return InspectionMessage[]|null
|
||||
*/
|
||||
public function runInspection(): ?array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
$oldBatches = sqlQueryBuilder()->select('pb.id, pb.code kod, pb.date_expiry expirace, SUM(wp.pieces) fyzicky_skladem')
|
||||
->from('products_batches', 'pb')
|
||||
->innerJoin('pb', 'warehouse_products', 'wp', 'wp.id_product_batch = pb.id')
|
||||
->where('pb.date_expiry < (NOW() + INTERVAL 2 MONTH)')
|
||||
->groupBy('pb.id')
|
||||
->having('fyzicky_skladem > 0');
|
||||
|
||||
$nearExpiration = (clone $oldBatches)->andWhere('pb.date_expiry > NOW()')->execute()->fetchAllAssociative();
|
||||
$afterExpiration = $oldBatches->andWhere('pb.date_expiry <= NOW()')->execute()->fetchAllAssociative();
|
||||
|
||||
$addIdHref = function ($data) {
|
||||
foreach ($data as &$row) {
|
||||
$row['id'] = "<a href=\"javascript:nw('productsBatches',{$row['id']}, '')\">{$row['id']}</a>";
|
||||
}
|
||||
|
||||
return $data;
|
||||
};
|
||||
|
||||
if ($nearExpiration) {
|
||||
$errors[] = new HtmlDataMessage(sprintf(translate('batchesNearExpiration', 'productsBatches', false, true), count($nearExpiration)), $addIdHref($nearExpiration));
|
||||
}
|
||||
|
||||
if ($afterExpiration) {
|
||||
$errors[] = new HtmlDataMessage(sprintf(translate('batchesAfterExpiration', 'productsBatches', false, true), count($afterExpiration)), $addIdHref($afterExpiration));
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
public function isEnabled(): bool
|
||||
{
|
||||
return findModule(\Modules::PRODUCTS_BATCHES) && findModule(\Modules::WAREHOUSE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\ProductsBatchesBundle;
|
||||
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
|
||||
class ProductsBatchesBundle extends Bundle
|
||||
{
|
||||
}
|
||||
31
bundles/KupShop/ProductsBatchesBundle/Query/BatchesQuery.php
Normal file
31
bundles/KupShop/ProductsBatchesBundle/Query/BatchesQuery.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\ProductsBatchesBundle\Query;
|
||||
|
||||
use Query\QueryBuilder;
|
||||
|
||||
class BatchesQuery
|
||||
{
|
||||
public static function joinStoreBatches(): callable
|
||||
{
|
||||
return function (QueryBuilder $qb) {
|
||||
$subtractBatches = sqlQueryBuilder()->select("SUM(JSON_EXTRACT(si2.custom_data, CONCAT('$.batch_numbers.', pb2.id)))")
|
||||
->from('stores_items', 'si2')
|
||||
->innerJoin('si2',
|
||||
'products_batches',
|
||||
'pb2',
|
||||
"si2.id_product = pb2.id_product AND (si2.id_variation = pb2.id_variation OR (si2.id_variation IS NULL AND pb2.id_variation IS NULL)) AND
|
||||
JSON_CONTAINS_PATH(si2.custom_data, 'one', CONCAT('$.batch_numbers.', pb2.id))")
|
||||
->where('si2.id = si.id')->getSQL();
|
||||
|
||||
$qb->addSelect('pb.code batch_code',
|
||||
' pb.date_expiry batch_expiry',
|
||||
"COALESCE(JSON_EXTRACT(si.custom_data, CONCAT('$.batch_numbers.', pb.id)), si.quantity - COALESCE(({$subtractBatches}), 0)) AS batch_quantity")
|
||||
->innerJoin('si',
|
||||
'(SELECT id, id_product, id_variation, code, date_expiry FROM products_batches UNION SELECT null,null,null,null,null)',
|
||||
'pb',
|
||||
"(si.id_product = pb.id_product AND (si.id_variation = pb.id_variation OR (si.id_variation IS NULL AND pb.id_variation IS NULL)) AND
|
||||
JSON_CONTAINS_PATH(si.custom_data, 'one', CONCAT('$.batch_numbers.', pb.id))) OR pb.id IS NULL");
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
services:
|
||||
_defaults:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
|
||||
KupShop\ProductsBatchesBundle\:
|
||||
resource: ../../{AdminRegister,EventListener,Util,Inspections}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
|
||||
use KupShop\ProductsBatchesBundle\Util\ProductsBatchesUtil;
|
||||
|
||||
function smarty_function_get_product_nearest_expiration_batch($params, &$smarty)
|
||||
{
|
||||
if (empty($params['id_product'])) {
|
||||
throw new InvalidArgumentException("Missing parameter 'id_product' in smarty function 'get_product_batches'!");
|
||||
}
|
||||
|
||||
/**
|
||||
* @var ProductsBatchesUtil $batchesUtil
|
||||
*/
|
||||
$batchesUtil = ServiceContainer::getService(ProductsBatchesUtil::class);
|
||||
|
||||
$batches = $batchesUtil->getProductNearestExpirationBatches($params['id_product']);
|
||||
|
||||
if (!empty($params['assign'])) {
|
||||
$smarty->assign($params['assign'], $batches);
|
||||
} else {
|
||||
return $batches;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\ProductsBatchesBundle\Resources\upgrade;
|
||||
|
||||
class ProductBatchesUpgrade extends \UpgradeNew
|
||||
{
|
||||
public function check_productBatchesTable()
|
||||
{
|
||||
return $this->checkTableExists('products_batches');
|
||||
}
|
||||
|
||||
/** Add products_batches table */
|
||||
public function upgrade_productBatchesTable()
|
||||
{
|
||||
sqlQuery('CREATE TABLE products_batches
|
||||
(
|
||||
id INT AUTO_INCREMENT,
|
||||
id_product INT(11) NOT NULL,
|
||||
id_variation INT(11) NULL,
|
||||
code VARCHAR(100) NOT NULL,
|
||||
date_expiry DATETIME NULL,
|
||||
CONSTRAINT products_batches_pk
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT products_batches_unique
|
||||
UNIQUE (id_product, code),
|
||||
CONSTRAINT products_batches_products_id_fk
|
||||
FOREIGN KEY (id_product) REFERENCES products (id)
|
||||
ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT products_batches_products_variations_id_fk
|
||||
FOREIGN KEY (id_variation) REFERENCES products_variations (id)
|
||||
ON UPDATE CASCADE ON DELETE SET NULL
|
||||
);
|
||||
');
|
||||
|
||||
$this->upgradeOK();
|
||||
}
|
||||
|
||||
public function check_productBatchesWarehouseProducts()
|
||||
{
|
||||
return findModule(\Modules::WAREHOUSE) && $this->checkColumnExists('warehouse_products', 'id_product_batch');
|
||||
}
|
||||
|
||||
/** Add products_batches column into warehouse */
|
||||
public function upgrade_productBatchesWarehouseProducts()
|
||||
{
|
||||
sqlQuery('ALTER TABLE warehouse_products ADD COLUMN id_product_batch INT DEFAULT NULL');
|
||||
sqlQuery('ALTER TABLE warehouse_log ADD COLUMN id_product_batch INT DEFAULT NULL');
|
||||
|
||||
$this->upgradeOK();
|
||||
}
|
||||
|
||||
public function check_productBatchesWarehouseConstraint()
|
||||
{
|
||||
return findModule(\Modules::WAREHOUSE) && $this->checkForeignKeyExists('warehouse_products', 'id_product_batch');
|
||||
}
|
||||
|
||||
/** Add id_product_batch foreign key into warehouse_products */
|
||||
public function upgrade_productBatchesWarehouseConstraint()
|
||||
{
|
||||
sqlQuery('alter table warehouse_products
|
||||
add constraint warehouse_products_products_batches_id_fk
|
||||
foreign key (id_product_batch) references products_batches (id)
|
||||
on update cascade;
|
||||
|
||||
');
|
||||
|
||||
$this->upgradeOK();
|
||||
}
|
||||
|
||||
public function check_productBatchesWarehouseRemoveIndexWarehouse_products()
|
||||
{
|
||||
return findModule(\Modules::WAREHOUSE) && !$this->checkIndexNameExists('warehouse_products', 'warehouse_products');
|
||||
}
|
||||
|
||||
/** Modify warehouse_products remove old index which is replaced with integrity_unique */
|
||||
public function upgrade_productBatchesWarehouseRemoveIndexWarehouse_products()
|
||||
{
|
||||
sqlQuery('create index warehouse_products_id_product_index on warehouse_products (id_product);');
|
||||
sqlQuery('drop index warehouse_products on warehouse_products;');
|
||||
|
||||
$this->upgradeOK();
|
||||
}
|
||||
|
||||
public function check_productProductsTableRequire()
|
||||
{
|
||||
return $this->checkColumnExists('products', 'batch_number_require');
|
||||
}
|
||||
|
||||
/** Add batch_number_require to products table */
|
||||
public function upgrade_productProductsTableRequire()
|
||||
{
|
||||
sqlQuery("alter table products add batch_number_require enum('Y', 'N') default 'N' null;");
|
||||
|
||||
$this->upgradeOK();
|
||||
}
|
||||
|
||||
public function check_tableProductBatchesUnique()
|
||||
{
|
||||
return findModule(\Modules::WAREHOUSE) && !$this->checkIndexNameExists('products_batches', 'products_batches_unique');
|
||||
}
|
||||
|
||||
/** Modify products_batches remove products_batches_unique which is replaced with integrity_unique */
|
||||
public function upgrade_tableProductBatchesUnique()
|
||||
{
|
||||
sqlQuery('create index products_batches_id_product_index on products_batches (id_product);');
|
||||
sqlQuery('drop index products_batches_unique on products_batches;');
|
||||
sqlQuery('create index products_batches_code_index on products_batches (code);');
|
||||
|
||||
$this->upgradeOK();
|
||||
}
|
||||
|
||||
public function check_tableProductBatchesOnDeleteConstraint()
|
||||
{
|
||||
return $this->checkConstraintRule('products_batches', 'products_batches_products_variations_id_fk', 'CASCADE');
|
||||
}
|
||||
|
||||
public function upgrade_tableProductBatchesOnDeleteConstraint()
|
||||
{
|
||||
sqlQuery('alter table products_batches
|
||||
drop foreign key products_batches_products_variations_id_fk');
|
||||
|
||||
sqlQuery('alter table products_batches
|
||||
add constraint products_batches_products_variations_id_fk
|
||||
foreign key (id_variation) references products_variations (id)
|
||||
on update cascade on delete cascade');
|
||||
}
|
||||
|
||||
public function check_fixWarehouseProductsRemovePersistentIntegirityUnique()
|
||||
{
|
||||
return findModule(\Modules::WAREHOUSE) && !$this->checkColumnExists('warehouse_products',
|
||||
'integrity_unique_product_variation_position_batch');
|
||||
}
|
||||
|
||||
/** Remove warehouse_products persistent integrity_unique */
|
||||
public function upgrade_fixWarehouseProductsRemovePersistentIntegirityUnique()
|
||||
{
|
||||
sqlQuery('alter table warehouse_products
|
||||
drop column integrity_unique_product_variation_position_batch; ');
|
||||
|
||||
$this->upgradeOK();
|
||||
}
|
||||
|
||||
public function check_addWarehouseProductsUniqueVirtualColumn()
|
||||
{
|
||||
return findModule(\Modules::WAREHOUSE) && $this->checkColumnExists('warehouse_products',
|
||||
'integrity_unique');
|
||||
}
|
||||
|
||||
/** Add warehouse_products virtual integrity_unique */
|
||||
public function upgrade_addWarehouseProductsUniqueVirtualColumn()
|
||||
{
|
||||
sqlQuery("ALTER TABLE warehouse_products ADD integrity_unique varchar(50) as (CONCAT(id_product, '-', COALESCE(id_variation, ''), '-', id_position, '-',
|
||||
COALESCE(id_product_batch, ''))) VIRTUAL UNIQUE; ");
|
||||
|
||||
$this->upgradeOK();
|
||||
}
|
||||
|
||||
public function check_fixProductsBatchesUniqueVirtualColumn()
|
||||
{
|
||||
return !$this->checkColumnExists('products_batches',
|
||||
'integrity_unique_product_variation_code');
|
||||
}
|
||||
|
||||
/** Remove products_batches persistent integrity_unique */
|
||||
public function upgrade_fixProductsBatchesUniqueVirtualColumn()
|
||||
{
|
||||
sqlQuery('alter table products_batches
|
||||
drop column integrity_unique_product_variation_code; ');
|
||||
|
||||
$this->upgradeOK();
|
||||
}
|
||||
|
||||
public function check_addProductsBatchesUniqueVirtualColumn()
|
||||
{
|
||||
return $this->checkColumnExists('products_batches', 'integrity_unique');
|
||||
}
|
||||
|
||||
/** Add products_batches virtual integrity_unique */
|
||||
public function upgrade_addProductsBatchesUniqueVirtualColumn()
|
||||
{
|
||||
sqlQuery("ALTER TABLE products_batches
|
||||
ADD COLUMN integrity_unique VARCHAR(50) AS
|
||||
(CONCAT(id_product, '-', COALESCE(id_variation, ''), '-', code)) VIRTUAL UNIQUE;"
|
||||
);
|
||||
|
||||
$this->upgradeOK();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\ProductsBatchesBundle\Resources\upgrade;
|
||||
|
||||
use Query\Operator;
|
||||
|
||||
class StoreBatchesUpgrade extends \UpgradeNew
|
||||
{
|
||||
protected $priority = 110;
|
||||
|
||||
public function check_storesCustomData()
|
||||
{
|
||||
return $this->checkModule(\Modules::STORES) && $this->checkColumnExists('stores_items', 'custom_data');
|
||||
}
|
||||
|
||||
public function upgrade_storesCustomData()
|
||||
{
|
||||
sqlQuery("
|
||||
alter table stores_items
|
||||
add custom_data text default '' null;");
|
||||
}
|
||||
|
||||
public function check_storesBatchesMoveToCustomData()
|
||||
{
|
||||
return $this->checkModule(\Modules::STORES) && !$this->checkColumnExists('stores_items', 'batches') && !$this->checkColumnExists('stores_items', 'custom_data');
|
||||
}
|
||||
|
||||
public function upgrade_storesBatchesMoveToCustomData()
|
||||
{
|
||||
$storeItems = sqlQueryBuilder()->select('id, batches')
|
||||
->from('stores_items', 'si')->execute()->fetchAllAssociative();
|
||||
|
||||
foreach ($storeItems as $storeItem) {
|
||||
try {
|
||||
$batches = json_decode_strict($storeItem['batches'], true);
|
||||
} catch (\Exception $e) {
|
||||
$batches = [];
|
||||
}
|
||||
|
||||
if (!empty($batches)) {
|
||||
sqlQueryBuilder()->update('stores_items')
|
||||
->set('custom_data', ':customData')
|
||||
->setParameter('customData', json_encode(['batch_numbers' => $batches]))
|
||||
->where(Operator::equals(['id' => $storeItem['id']]))
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
sqlQuery('alter table stores_items
|
||||
drop column batches;
|
||||
');
|
||||
|
||||
$this->upgradeOK();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"products_batches": [
|
||||
{
|
||||
"id": 1,
|
||||
"id_product": 1,
|
||||
"id_variation": null,
|
||||
"code": "200124",
|
||||
"date_expiry": "2024-01-20 00:00:00"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"id_product": 2,
|
||||
"id_variation": 10,
|
||||
"code": "2/22",
|
||||
"date_expiry": "2022-02-28 00:00:00"
|
||||
}
|
||||
], "stores_transfers": [
|
||||
{
|
||||
"id": 1,
|
||||
"id_store_from": 1,
|
||||
"id_store_to": 1,
|
||||
"date_created": "2021-09-09 13:44:10",
|
||||
"state": "R",
|
||||
"note": null,
|
||||
"checkouts": "",
|
||||
"id_position": null
|
||||
}
|
||||
]
|
||||
, "stores_transfers_items": [
|
||||
{
|
||||
"id": 1,
|
||||
"id_stores_transfer": 1,
|
||||
"id_product": 1,
|
||||
"id_variation": null,
|
||||
"quantity": 5,
|
||||
"custom_data": ""
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"id_stores_transfer": 1,
|
||||
"id_product": 4,
|
||||
"id_variation": 1,
|
||||
"quantity": 3,
|
||||
"custom_data": ""
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\ProductsBatchesBundle\Tests;
|
||||
|
||||
use KupShop\KupShopBundle\Util\Compat\ServiceContainer;
|
||||
use KupShop\ProductsBatchesBundle\Util\ProductsBatchesUtil;
|
||||
use Query\Operator;
|
||||
|
||||
class ProductsBatchesCustomDataTest extends \DatabaseTestCase
|
||||
{
|
||||
/**
|
||||
* @var ProductsBatchesUtil
|
||||
*/
|
||||
private $productBatchesUtil;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->productBatchesUtil = ServiceContainer::getService(ProductsBatchesUtil::class);
|
||||
|
||||
parent::setUp();
|
||||
}
|
||||
|
||||
public function testCreateBatch()
|
||||
{
|
||||
$batches = $this->createBatches();
|
||||
|
||||
self::assertEquals(4, $batches[0]['id_product']);
|
||||
self::assertEquals(1, $batches[0]['id_variation']);
|
||||
self::assertEquals('1234', $batches[0]['code']);
|
||||
self::assertEquals('2022-02-10 00:00:00', $batches[0]['date_expiry']);
|
||||
|
||||
$alreadyExists = $this->productBatchesUtil->createBatchNumber([
|
||||
'id_product' => 1,
|
||||
'code' => '4321',
|
||||
'date_expiry' => '2019-09-1 00:00:00',
|
||||
]);
|
||||
|
||||
unset($alreadyExists['id']);
|
||||
|
||||
self::assertEquals([
|
||||
'id_product' => 1,
|
||||
'id_variation' => null,
|
||||
'code' => '4321',
|
||||
'date_expiry' => '2022-03-28 00:00:00',
|
||||
], $alreadyExists);
|
||||
|
||||
// getBatchNumber returns same value as createBatchNumber
|
||||
foreach ($batches as $batch) {
|
||||
self::assertEquals($this->productBatchesUtil->getBatchNumber($batch['id']), $batch);
|
||||
}
|
||||
}
|
||||
|
||||
public function testBatchesInsertCustomData()
|
||||
{
|
||||
$where1 = [
|
||||
'id_stores_transfer' => 1,
|
||||
'id_product' => 1,
|
||||
'id_variation' => null,
|
||||
];
|
||||
|
||||
$where2 = [
|
||||
'id_stores_transfer' => 1,
|
||||
'id_product' => 4,
|
||||
'id_variation' => 1,
|
||||
];
|
||||
|
||||
$this->productBatchesUtil->addBatchesToCustomData('stores_transfers_items', $where1, [1 => 3]);
|
||||
$this->productBatchesUtil->addBatchesToCustomData('stores_transfers_items', $where1, [2 => 2]);
|
||||
$this->productBatchesUtil->addBatchesToCustomData('stores_transfers_items', $where2, [3 => 1, 8 => 2]);
|
||||
|
||||
$customData = $this->getCustomData($where1);
|
||||
self::assertEquals(3, $customData['batch_numbers'][1]);
|
||||
self::assertEquals(2, $customData['batch_numbers'][2]);
|
||||
|
||||
$customData = $this->getCustomData($where2);
|
||||
self::assertEquals(1, $customData['batch_numbers'][3]);
|
||||
self::assertEquals(2, $customData['batch_numbers'][8]);
|
||||
|
||||
$this->productBatchesUtil->addBatchesToCustomData('stores_transfers_items', $where1, [1 => -1, 2 => -3]);
|
||||
$customData = $this->getCustomData($where1);
|
||||
self::assertEquals(2, $customData['batch_numbers'][1]);
|
||||
self::assertArrayNotHasKey(2, $customData['batch_numbers']);
|
||||
|
||||
$this->productBatchesUtil->addBatchesToCustomData('stores_transfers_items', $where1, [2 => -1]);
|
||||
$customData = $this->getCustomData($where1);
|
||||
self::assertArrayNotHasKey(2, $customData['batch_numbers']);
|
||||
|
||||
$this->productBatchesUtil->setBatchesToCustomData('stores_transfers_items', $where1, [1 => 20]);
|
||||
$this->productBatchesUtil->setBatchesToCustomData('stores_transfers_items', $where1, [2 => 15]);
|
||||
$this->productBatchesUtil->setBatchesToCustomData('stores_transfers_items', $where2, [8 => -1]);
|
||||
|
||||
$customData = $this->getCustomData($where1);
|
||||
self::assertEquals(20, $customData['batch_numbers'][1]);
|
||||
self::assertEquals(15, $customData['batch_numbers'][2]);
|
||||
|
||||
$customData = $this->getCustomData($where2);
|
||||
self::assertArrayNotHasKey(8, $customData['batch_numbers']);
|
||||
|
||||
$this->productBatchesUtil->setBatchesToCustomData('stores_transfers_items', $where2, [8 => -5]);
|
||||
$customData = $this->getCustomData($where2);
|
||||
self::assertArrayNotHasKey(8, $customData['batch_numbers']);
|
||||
|
||||
$this->productBatchesUtil->setBatchesToCustomData('stores_transfers_items', $where1, [1 => 1, 2 => -1]);
|
||||
$customData = $this->getCustomData($where1);
|
||||
self::assertEquals(1, $customData['batch_numbers'][1]);
|
||||
self::assertArrayNotHasKey(2, $customData['batch_numbers']);
|
||||
|
||||
$this->productBatchesUtil->clearBatchesCustomData('stores_transfers_items', $where1);
|
||||
$customData = $this->getCustomData($where1);
|
||||
self::assertEmpty($customData['batch_numbers']);
|
||||
|
||||
$this->productBatchesUtil->clearBatchesCustomData('stores_transfers_items', $where2);
|
||||
$customData = $this->getCustomData($where2);
|
||||
self::assertEmpty($customData['batch_numbers']);
|
||||
}
|
||||
|
||||
public function data_BatchesInitCustomData(): array
|
||||
{
|
||||
return [
|
||||
['addBatchesToCustomData'],
|
||||
['setBatchesToCustomData'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider data_BatchesInitCustomData
|
||||
*/
|
||||
public function testBatchesInitCustomData($method)
|
||||
{
|
||||
$where = [
|
||||
'id_stores_transfer' => 1,
|
||||
'id_product' => 1,
|
||||
'id_variation' => null,
|
||||
];
|
||||
|
||||
$this->setCustomData('ABC', $where);
|
||||
$this->productBatchesUtil->$method('stores_transfers_items', $where, [1 => 3]);
|
||||
self::assertEquals(3, $this->getCustomData($where)['batch_numbers'][1]);
|
||||
|
||||
$this->setCustomData('', $where);
|
||||
$this->productBatchesUtil->$method('stores_transfers_items', $where, [1 => 3]);
|
||||
self::assertEquals(3, $this->getCustomData($where)['batch_numbers'][1]);
|
||||
|
||||
$this->setCustomData('{}', $where);
|
||||
$this->productBatchesUtil->$method('stores_transfers_items', $where, [1 => 3]);
|
||||
self::assertEquals(3, $this->getCustomData($where)['batch_numbers'][1]);
|
||||
|
||||
$this->setCustomData('{"something": 555}', $where);
|
||||
$this->productBatchesUtil->$method('stores_transfers_items', $where, [1 => 3]);
|
||||
self::assertEquals(555, $this->getCustomData($where)['something']);
|
||||
self::assertEquals(3, $this->getCustomData($where)['batch_numbers'][1]);
|
||||
}
|
||||
|
||||
public function testClearCustomData()
|
||||
{
|
||||
$where = [
|
||||
'id_stores_transfer' => 1,
|
||||
'id_product' => 1,
|
||||
'id_variation' => null,
|
||||
];
|
||||
|
||||
$this->setCustomData('ABC', $where);
|
||||
$this->productBatchesUtil->clearBatchesCustomData('stores_transfers_items', $where);
|
||||
self::assertEmpty($this->getCustomData($where));
|
||||
|
||||
$this->setCustomData('', $where);
|
||||
$this->productBatchesUtil->clearBatchesCustomData('stores_transfers_items', $where);
|
||||
self::assertEmpty($this->getCustomData($where));
|
||||
|
||||
$this->setCustomData('{"batch_numbers": {"56": 1}}', $where);
|
||||
$this->productBatchesUtil->clearBatchesCustomData('stores_transfers_items', $where);
|
||||
self::assertEmpty($this->getCustomData($where)['batch_numbers']);
|
||||
|
||||
$this->setCustomData('{"something": 555, "batch_numbers": {"56": 1}}', $where);
|
||||
$this->productBatchesUtil->clearBatchesCustomData('stores_transfers_items', $where);
|
||||
self::assertEmpty($this->getCustomData($where)['batch_numbers']);
|
||||
self::assertEquals(555, $this->getCustomData($where)['something']);
|
||||
}
|
||||
|
||||
public function getDataSet()
|
||||
{
|
||||
return $this->getJsonDataSetFromFile();
|
||||
}
|
||||
|
||||
private function setCustomData($string, $where)
|
||||
{
|
||||
sqlQueryBuilder()->update('stores_transfers_items')->directValues(['custom_data' => $string])
|
||||
->where(Operator::equalsNullable($where))->execute();
|
||||
}
|
||||
|
||||
private function getCustomData($where)
|
||||
{
|
||||
return json_decode(sqlQueryBuilder()->select('custom_data')->from('stores_transfers_items')
|
||||
->where(Operator::equalsNullable($where))->execute()->fetchOne(),
|
||||
true);
|
||||
}
|
||||
|
||||
private function createBatches()
|
||||
{
|
||||
$batches = [];
|
||||
$batches[] = $this->productBatchesUtil->createBatchNumber([
|
||||
'id_product' => 4,
|
||||
'id_variation' => 1,
|
||||
'code' => '1234',
|
||||
'date_expiry' => '2022-02-10 00:00:00',
|
||||
]);
|
||||
|
||||
$batches[] = $this->productBatchesUtil->createBatchNumber([
|
||||
'id_product' => 1,
|
||||
'code' => '4321',
|
||||
'date_expiry' => '2022-03-28 00:00:00',
|
||||
]);
|
||||
|
||||
return $batches;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
namespace KupShop\ProductsBatchesBundle\Util;
|
||||
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use KupShop\CatalogBundle\ProductList\ProductCollection;
|
||||
use KupShop\KupShopBundle\Util\Database\QueryHint;
|
||||
use KupShop\StoresBundle\Utils\StoresInStore;
|
||||
use Query\Operator;
|
||||
|
||||
class ProductsBatchesUtil
|
||||
{
|
||||
public function getBatchNumber($id)
|
||||
{
|
||||
return sqlQueryBuilder()->select('id', 'id_product', 'id_variation', 'code', 'date_expiry')
|
||||
->from('products_batches')
|
||||
->where(Operator::equals(['id' => $id]))->execute()->fetch();
|
||||
}
|
||||
|
||||
public function getProductNearestExpirationBatches($idProduct)
|
||||
{
|
||||
$subQuery = sqlQueryBuilder()->select('pb2.id, MIN(pb2.date_expiry) min_date_expiry')
|
||||
->from('products_batches', 'pb2')
|
||||
->innerJoin('pb2', 'warehouse_products', 'wp2', 'pb2.id = wp2.id_product_batch')
|
||||
->innerJoin('wp2', 'warehouse_positions', 'wpos2', 'wp2.id_position = wpos2.id')
|
||||
->where('pb2.date_expiry IS NOT NULL')
|
||||
->andWhere('wp2.pieces > 0')
|
||||
->andWhere("wpos2.code NOT LIKE '%BOX%'")
|
||||
->groupBy('pb2.id');
|
||||
|
||||
$qb = sqlQueryBuilder()->select('pb.id', 'pb.id_variation', 'pb.code', 'pb.date_expiry')
|
||||
->from('products_batches', 'pb')
|
||||
->innerJoin('pb', '('.$subQuery->getSQL().')', 'pb2', 'pb.id = pb2.id AND pb.date_expiry = pb2.min_date_expiry')
|
||||
->where(Operator::equals(['pb.id_product' => $idProduct]))
|
||||
->groupBy('pb.id_variation');
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($qb->execute() as $row) {
|
||||
$vId = $row['id_variation'];
|
||||
|
||||
$batch = [
|
||||
'id' => $row['id'],
|
||||
'code' => $row['code'],
|
||||
'date_expiry' => $row['date_expiry'],
|
||||
];
|
||||
|
||||
if ($vId) {
|
||||
$result['variations'] = $result['variations'] ?? [];
|
||||
$result['variations'][$vId] = $batch;
|
||||
} else {
|
||||
$result['product'] = $batch;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function createBatchNumber($data)
|
||||
{
|
||||
$key = [
|
||||
'id_product' => $data['id_product'],
|
||||
'id_variation' => $data['id_variation'] ?? null,
|
||||
'code' => $data['code'],
|
||||
];
|
||||
|
||||
$alreadyExists = QueryHint::withRouteToMaster(function () use ($key) {
|
||||
return sqlQueryBuilder()->select('id', 'id_product', 'id_variation', 'code', 'date_expiry')
|
||||
->from('products_batches')
|
||||
->where(Operator::equalsNullable($key))->execute()->fetchAssociative();
|
||||
});
|
||||
|
||||
if (!$alreadyExists) {
|
||||
$key['date_expiry'] = $data['date_expiry'];
|
||||
$key['id'] = sqlGetConnection()->transactional(function () use ($key) {
|
||||
sqlQueryBuilder()->insert('products_batches')
|
||||
->directValues($key)
|
||||
->execute();
|
||||
|
||||
return sqlInsertId();
|
||||
});
|
||||
|
||||
return $key;
|
||||
} else {
|
||||
return $alreadyExists;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For each batch from $batches in format [$idBatch => $amount, $idBatch2=> $amount2] it updates custom_data batches.
|
||||
*
|
||||
* If batch already exists in the custom_data, it only increases/decreases its amount. Or removes the batch when its amount would be <= 0.
|
||||
* If batch doesn't exist, it creates the batch in the JSON structure.
|
||||
* If custom_data doesn't contain the batch_numbers structure, it creates the batch_numbers structure next to the previous content of the custom_data.
|
||||
* If custom data is null or empty, it creates the structure for the batch_numbers.
|
||||
*
|
||||
* @param array $where condition for update
|
||||
* @param array $batches batches numbers in format [$idBatch => $amount, $idBatch2=> $amount2]
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function addBatchesToCustomData(string $tableName, array $where, array $batches, $columnName = 'custom_data')
|
||||
{
|
||||
$this->modifyBatchesToCustomData($tableName, $where, $batches, true, $columnName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as method addBatchesToCustomData, but batch amount is set, not incremented.
|
||||
*
|
||||
* @param array $where condition for update
|
||||
* @param array $batches batches numbers in format [$idBatch => $amount, $idBatch2=> $amount2]
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function setBatchesToCustomData(string $tableName, array $where, array $batches, $columnName = 'custom_data')
|
||||
{
|
||||
$this->modifyBatchesToCustomData($tableName, $where, $batches, false, $columnName);
|
||||
}
|
||||
|
||||
protected function modifyBatchesToCustomData(string $tableName, array $where, array $batches, bool $incrementActualValue, $columnName)
|
||||
{
|
||||
if ($incrementActualValue) {
|
||||
$futureValue = "JSON_EXTRACT({$columnName}, CONCAT('$.batch_numbers.', :batchId)) + :batchAmount";
|
||||
} else {
|
||||
$futureValue = ':batchAmount';
|
||||
}
|
||||
|
||||
foreach ($batches as $idBatch => $amountBatch) {
|
||||
sqlQueryBuilder()->update($tableName)->where(Operator::equalsNullable($where))
|
||||
->set($columnName,
|
||||
"
|
||||
IF(JSON_CONTAINS_PATH(NULLIF({$columnName}, ''), 'one', CONCAT('$.batch_numbers.', :batchId)),
|
||||
IF({$futureValue} > 0,
|
||||
JSON_SET({$columnName}, CONCAT('$.batch_numbers.', :batchId), {$futureValue}),
|
||||
JSON_REMOVE({$columnName}, CONCAT('$.batch_numbers.', :batchId))
|
||||
)
|
||||
,
|
||||
JSON_REPLACE(
|
||||
IF(JSON_CONTAINS_PATH(NULLIF({$columnName}, ''), 'one', '$.batch_numbers'),
|
||||
{$columnName},
|
||||
IF(JSON_TYPE(NULLIF({$columnName}, '')) = 'OBJECT', JSON_INSERT({$columnName}, '$.batch_numbers', JSON_OBJECT()),
|
||||
JSON_OBJECT('batch_numbers', JSON_OBJECT()))
|
||||
)
|
||||
, '$.batch_numbers',
|
||||
JSON_MERGE_PATCH(COALESCE(JSON_EXTRACT(NULLIF({$columnName}, ''), '$.batch_numbers'), JSON_OBJECT()),
|
||||
IF(:batchAmount > 0, JSON_OBJECT(:batchId, :batchAmount), JSON_OBJECT())))
|
||||
)
|
||||
")
|
||||
->setParameter('batchId', $idBatch)
|
||||
->setParameter('batchAmount', (int) $amountBatch, ParameterType::INTEGER)->execute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear batch numbers object from custom_data of the table.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clearBatchesCustomData(string $tableName, array $where, $columnName = 'custom_data')
|
||||
{
|
||||
sqlQueryBuilder()->update($tableName)
|
||||
->set($columnName, "JSON_SET({$columnName}, CONCAT('$.batch_numbers'), JSON_OBJECT())")
|
||||
->where(Operator::equalsNullable($where))
|
||||
->execute();
|
||||
}
|
||||
|
||||
public function getProductBatches(ProductCollection $products, bool $withQuantity = false, bool $byVariations = false): ?array
|
||||
{
|
||||
$qb = sqlQueryBuilder()->select('pb.id, pb.id_product, pb.id_variation, pb.code, pb.date_expiry')
|
||||
->from('products_batches', 'pb')
|
||||
->where(Operator::inIntArray($products->getProductIds(), 'pb.id_product'))
|
||||
->groupBy('pb.code');
|
||||
|
||||
if ($withQuantity) {
|
||||
$qb->addSelect("
|
||||
COALESCE((SELECT SUM(DISTINCT JSON_EXTRACT(si.custom_data, CONCAT('$.batch_numbers.', pb.id)))
|
||||
FROM stores_items si LEFT JOIN stores s ON si.id_store = s.id
|
||||
WHERE JSON_EXTRACT(si.custom_data, CONCAT('$.batch_numbers.', pb.id)) IS NOT NULL
|
||||
AND si.id_product = pb.id_product AND si.id_variation <=> pb.id_variation AND s.type = :externalStoreType), 0)
|
||||
+ COALESCE(SUM(DISTINCT wp.pieces), 0) AS batch_quantity
|
||||
")
|
||||
->setParameter('externalStoreType', StoresInStore::TYPE_EXTERNAL_STORE);
|
||||
$qb->leftJoin('pb', 'warehouse_products', 'wp', 'pb.id = wp.id_product_batch');
|
||||
}
|
||||
|
||||
if ($byVariations) {
|
||||
$qb->addGroupBy('pb.id_variation');
|
||||
}
|
||||
|
||||
return $qb->execute()->fetchAllAssociative();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user