3namespace Bitrix\Catalog\Component;
6use Bitrix\Catalog\Access\AccessController;
7use Bitrix\Catalog\Access\ActionDictionary;
8use Bitrix\Catalog\ProductTable;
9use Bitrix\Catalog\StoreDocumentElementTable;
10use Bitrix\Catalog\StoreDocumentTable;
11use Bitrix\Catalog\StoreProductTable;
12use Bitrix\Main\Entity\Base;
13use Bitrix\Main\Entity\ReferenceField;
14use Bitrix\Main\Loader;
15use Bitrix\Main\Localization\Loc;
16use Bitrix\Main\ORM\Fields\ExpressionField;
17use Bitrix\Main\ORM\Fields\Relations\Reference;
18use Bitrix\Main\ORM\Query\Join;
19use Bitrix\Main\ORM\Query\Query;
20use Bitrix\Main\Type\DateTime;
21use Bitrix\Sale\Internals\BasketTable;
22use Bitrix\Sale\Internals\ShipmentItemStoreTable;
23use Bitrix\Sale\Internals\ShipmentItemTable;
24use Bitrix\Sale\Internals\ShipmentTable;
53 return Loc::getMessage(
'CATALOG_REPORT_PRODUCT_LIST_NO_PRODUCTS');
60 return parent::onPrepareComponentParams(
$arParams);
74 $this->arResult[
'ERROR_MESSAGES'][] = Loc::getMessage(
'CATALOG_REPORT_PRODUCT_LIST_NO_READ_RIGHTS_ERROR');
86 if (empty($this->arResult[
'STORE_TITLE']))
88 $this->arResult[
'STORE_TITLE'] = Loc::getMessage(
'CATALOG_REPORT_PRODUCT_LIST_DEFAULT_STORE_NAME');
95 'FILTER_PRESETS' => [],
96 'ENABLE_LABEL' =>
true,
106 if (!Loader::includeModule(
'catalog'))
108 $this->arResult[
'ERROR_MESSAGES'][] =
'Module Catalog is not installed';
113 if (!Loader::includeModule(
'report'))
115 $this->arResult[
'ERROR_MESSAGES'][] =
'Module Report is not installed';
133 $this->catalogData = $this->loadCatalog(array_column($productData,
'PRODUCT_ID'));
140 $receivedQuantityAmountDifferenceData = [];
141 $outgoingQuantityAmountDifferenceData = [];
142 $amountSoldAmountDifferenceData = [];
144 if (!empty($formattedFilter[
'REPORT_INTERVAL']))
146 $differenceFilter = $formattedFilter;
148 $filterTimeTo =
new DateTime($differenceFilter[
'REPORT_INTERVAL'][
'TO']);
149 if ($currentTime > $filterTimeTo)
151 $differenceFilter[
'REPORT_INTERVAL'][
'FROM'] = $differenceFilter[
'REPORT_INTERVAL'][
'TO'];
152 $differenceFilter[
'REPORT_INTERVAL'][
'TO'] = (
new DateTime())->toString();
155 $amountSoldAmountDifferenceData = $this->
getAmountSoldData($this->storeId, $differenceFilter);
159 foreach ($productData as
$key => $item)
161 $receivedQuantityAmountDifference = (float)($receivedQuantityAmountDifferenceData[$item[
'PRODUCT_ID']] ?? 0);
162 $outgoingQuantityAmountDifference = (float)($outgoingQuantityAmountDifferenceData[$item[
'PRODUCT_ID']] ?? 0);
163 $amountSoldAmountDifference = (float)($amountSoldAmountDifferenceData[$item[
'PRODUCT_ID']] ?? 0);
166 - $receivedQuantityAmountDifference
167 + $outgoingQuantityAmountDifference
168 + $amountSoldAmountDifference
171 $receivedQuantity = (float)($receivedQuantityData[$item[
'PRODUCT_ID']] ?? 0);
172 $outgoingQuantity = (float)($outgoingQuantityData[$item[
'PRODUCT_ID']] ?? 0);
173 $amountSold = (float)($amountSoldData[$item[
'PRODUCT_ID']] ?? 0);
174 $item[
'STARTING_QUANTITY'] = (float)$item[
'AMOUNT'] - $receivedQuantity + $outgoingQuantity + $amountSold;
175 $item[
'RECEIVED_QUANTITY'] = (float)($receivedQuantityData[$item[
'PRODUCT_ID']] ?? 0);
176 $item[
'AMOUNT_SOLD'] = (float)($amountSoldData[$item[
'PRODUCT_ID']] ?? 0);
177 $item[
'QUANTITY'] = (float)$item[
'AMOUNT'] - (
float)$item[
'QUANTITY_RESERVED'];
190 $navParams = $this->gridOptions->getNavParams();
193 $pageNavigation = new \Bitrix\Main\UI\PageNavigation($this->navParamName);
194 $pageNavigation->allowAllRecords(
false)->setPageSize(
$pageSize)->initFromUri();
204 'STUB' =>
$totalCount <= 0 ? [
'title' => static::getEmptyStub()] :
null,
207 'CURRENT_PAGE' => $pageNavigation->getCurrentPage(),
208 'NAV_OBJECT' => $pageNavigation,
211 'ALLOW_ROWS_SORT' =>
false,
212 'AJAX_OPTION_JUMP' =>
'N',
213 'AJAX_OPTION_STYLE' =>
'N',
214 'AJAX_OPTION_HISTORY' =>
'N',
217 'SHOW_NAVIGATION_PANEL' =>
true,
218 'SHOW_PAGESIZE' =>
true,
221 [
'NAME' =>
'10',
'VALUE' =>
'10'],
222 [
'NAME' =>
'20',
'VALUE' =>
'20'],
223 [
'NAME' =>
'50',
'VALUE' =>
'50'],
224 [
'NAME' =>
'100',
'VALUE' =>
'100'],
225 [
'NAME' =>
'200',
'VALUE' =>
'200'],
226 [
'NAME' =>
'500',
'VALUE' =>
'500'],
229 'SHOW_ROW_CHECKBOXES' =>
false,
230 'SHOW_CHECK_ALL_CHECKBOXES' =>
false,
231 'SHOW_ACTION_PANEL' =>
false,
232 'SHOW_GRID_SETTINGS_MENU' =>
false,
233 'SHOW_SELECTED_COUNTER' =>
false,
234 'HANDLE_RESPONSE_ERRORS' =>
true,
235 'ALLOW_STICKED_COLUMNS' =>
true,
241 $navParams = $this->gridOptions->getNavParams();
243 $gridSort = $this->gridOptions->GetSorting([
'sort' => $this->defaultGridSort]);
245 $pageNavigation = new \Bitrix\Main\UI\PageNavigation($this->navParamName);
246 $pageNavigation->allowAllRecords(
false)->setPageSize(
$pageSize)->initFromUri();
248 $this->arResult[
'GRID'][
'ROWS'] = [];
250 $offset = $pageNavigation->getOffset();
251 $order = $gridSort[
'sort'];
252 $limit = $pageNavigation->getLimit();
256 return $query->exec()->fetchAll();
275 $baseStoreFilterValues =
$filter[
'=STORE_ID'] ??
'';
277 $reportInterval =
$filter[
'REPORT_INTERVAL'] ?? [];
278 unset(
$filter[
'REPORT_INTERVAL']);
280 $storeDocsFilter = [
'=DOCUMENT.STATUS' =>
'Y'];
281 $shipmentsFilter = [
'=ORDER_DELIVERY.DEDUCTED' =>
'Y'];
283 if (!empty($reportInterval))
285 $storeDocsFilter += [
286 '<=DOCUMENT.DATE_STATUS' =>
new DateTime($reportInterval[
'TO']),
288 $shipmentsFilter += [
289 '<=ORDER_DELIVERY.DATE_DEDUCTED' =>
new DateTime($reportInterval[
'TO']),
295 $storeDocsFilter[] = [
297 '=DOCS_ELEMENT.STORE_FROM' =>
$storeId,
298 '=DOCS_ELEMENT.STORE_TO' =>
$storeId,
301 $shipmentsFilter[
'=STORE_BARCODE.STORE_ID'] =
$storeId;
304 elseif (!empty($baseStoreFilterValues))
306 $storeDocsFilter[] = $baseStoreFilterValues;
307 $shipmentsFilter[
'=STORE_BARCODE.STORE_ID'] = $baseStoreFilterValues;
308 $filter[] = [
'=STORE_ID' => $baseStoreFilterValues];
318 $storeQuery->setSelect([
'ID' ,
'PRODUCT_ID',
'AMOUNT',
'QUANTITY_RESERVED',
'MEASURE_ID' =>
'PRODUCT.MEASURE']);
319 $storeQuery->registerRuntimeField(
322 StoreDocumentElementTable::class,
323 Join::on(
'this.PRODUCT_ID',
'ref.ELEMENT_ID')
326 $storeQuery->registerRuntimeField(
329 StoreDocumentTable::class,
330 Join::on(
'this.DOCS_ELEMENT.DOC_ID',
'ref.ID')
333 $storeQuery->registerRuntimeField(
337 Join::on(
'this.PRODUCT_ID',
'ref.PRODUCT_ID')
340 $storeQuery->registerRuntimeField(
343 ShipmentItemTable::class,
344 Join::on(
'this.BASKET.ID',
'ref.BASKET_ID')
347 $storeQuery->registerRuntimeField(
350 ShipmentItemStoreTable::class,
351 Join::on(
'this.SHIPMENT_ITEM.ID',
'ref.ORDER_DELIVERY_BASKET_ID')
354 $storeQuery->registerRuntimeField(
357 ShipmentTable::class,
358 Join::on(
'this.SHIPMENT_ITEM.ORDER_DELIVERY_ID',
'ref.ID')
362 $storeQuery->setFilter(
$filter);
363 $storeQuery->setDistinct();
369 $storeQuery->setOrder(
$order);
373 $storeQuery->setLimit($limit);
377 $storeQuery->setOffset($offset);
380 $storeQuery->countTotal(
true);
386 $allStoreQuery->registerRuntimeField(
'',
389 Base::getInstanceByQuery($storeQuery),
390 [
'this.ID' =>
'ref.PRODUCT_ID'],
391 [
'join_type' =>
'INNER']
395 $allStoreQuery->registerRuntimeField(
403 $allStoreQuery->registerRuntimeField(
407 'SUBQUERY.QUANTITY_RESERVED'
411 $allStoreQuery->setSelect([
413 'PRODUCT_ID' =>
'ID',
416 'MEASURE_ID' =>
'MEASURE',
421 $allStoreQuery->setOrder(
$order);
426 $allStoreQuery->setLimit($limit);
431 $allStoreQuery->setOffset($offset);
434 $allStoreQuery->countTotal(
true);
436 return $allStoreQuery;
443 $incomingFilter = $this->arParams[
'INCOMING_FILTER'] ?? [];
445 if (!empty($incomingFilter))
447 if (!empty($incomingFilter[
'PRODUCTS']))
454 !empty($incomingFilter[
'REPORT_INTERVAL_from'])
455 && !empty($incomingFilter[
'REPORT_INTERVAL_to'])
459 'FROM' => $incomingFilter[
'REPORT_INTERVAL_from'],
460 'TO' => $incomingFilter[
'REPORT_INTERVAL_to'],
466 $result[
'STORE_ID'] = $incomingFilter[
'STORES'];
472 if (!empty($getListFilter[
'=PRODUCT_ID']))
474 $result[
'PRODUCTS'] = $getListFilter[
'=PRODUCT_ID'];
480 !empty($userFilter[
'>=REPORT_INTERVAL'])
481 && !empty($userFilter[
'<=REPORT_INTERVAL'])
485 'FROM' => $userFilter[
'>=REPORT_INTERVAL'],
486 'TO' => $userFilter[
'<=REPORT_INTERVAL'],
500 foreach ([
'STARTING_QUANTITY',
'RECEIVED_QUANTITY',
'AMOUNT',
'QUANTITY_RESERVED',
'QUANTITY',
'AMOUNT_SOLD'] as $totalField)
505 unset($column[
'MEASURE_ID']);
507 $column[
'QUANTITY_RESERVED'] = $this->
getReservedDealListLink((
int)$item[
'PRODUCT_ID'], $column[
'QUANTITY_RESERVED']);
516 class="main-grid-cell-content-store-amount-reserved-quantity"
518 >' . $quantityReservedView . '</a>'
522 protected function getReservedDealsSliderLink(int $productId): string
524 $sliderUrl = \CComponentEngine::makeComponentPath('bitrix:catalog.productcard.reserved.deal.list');
525 $sliderUrl = getLocalPath('components'.$sliderUrl.'/slider.php');
526 $sliderUrlEntity = new \Bitrix\Main\Web\Uri($sliderUrl);
527 $sliderUrlEntity->addParams([
528 'storeId' => $this->storeId,
529 'productId' => $productId,
532 return $sliderUrlEntity->getUri();
535 protected function formatNumberWithMeasure($number, int $measureId)
539 $measureId = $this->getDefaultMeasure()['ID'];
541 return Loc::getMessage(
542 'CATALOG_REPORT_PRODUCT_LIST_MEASURE_TEMPLATE',
544 '#NUMBER#' => $number,
545 '#MEASURE_SYMBOL#' => $this->getMeasureSymbol($measureId),
550 protected function getMeasureSymbol(int $measureId): string
552 return htmlspecialcharsbx($this->measures[$measureId]['SYMBOL']);
555 protected function getProductView(array $column): string
559 $product = $this->catalogData[(int)$column['PRODUCT_ID']];
562 $APPLICATION->IncludeComponent(
563 'bitrix:catalog.grid.product.field',
566 'BUILDER_CONTEXT' => $this->arParams['BUILDER_CONTEXT'],
567 'GRID_ID' => $this->getGridId(),
568 'ROW_ID' => $column['ID'],
569 'GUID' => 'catalog_document_grid_' . $column['ID'],
570 'PRODUCT_FIELDS' => [
571 'ID' => $product['FIELDS']['PRODUCT_ID'],
572 'NAME' => $product['FIELDS']['NAME'],
573 'IBLOCK_ID' => $product['FIELDS']['IBLOCK_ID'],
574 'SKU_IBLOCK_ID' => $product['FIELDS']['OFFERS_IBLOCK_ID'],
575 'SKU_ID' => $product['FIELDS']['OFFER_ID'] ?? null,
576 'BASE_PRICE_ID' => $product['FIELDS']['BASE_PRICE_ID'] ?? null,
578 'SKU_TREE' => $product['FIELDS']['SKU_TREE'],
580 'VIEW_FORMAT' => 'short',
581 'ENABLE_SEARCH' => false,
582 'ENABLE_IMAGE_CHANGE_SAVING' => false,
583 'ENABLE_IMAGE_INPUT' => false,
584 'ENABLE_INPUT_DETAIL_LINK' => true,
585 'ENABLE_EMPTY_PRODUCT_ERROR' => false,
586 'ENABLE_SKU_SELECTION' => false,
587 'HIDE_UNSELECTED_ITEMS' => true,
592 return ob_get_clean();
595 protected function init(): void
597 $this->storeId = $this->arParams['STORE_ID'];
598 $this->gridOptions = new \Bitrix\Main\Grid\Options($this->getGridId());
600 if ($this->arParams['OPENED_FROM_REPORT'])
602 $this->getFilterOptions()->reset();
605 if (isset($this->arParams['INCOMING_FILTER']) && is_array($this->arParams['INCOMING_FILTER']))
607 $this->initFilterFromIncomingData($this->arParams['INCOMING_FILTER']);
611 protected function isAllStoresGrid(): bool
613 return $this->storeId <= 0;
616 protected function prepareFilterIncomingData(array $incomingFilter): array
619 if (isset($incomingFilter['PRODUCTS'], $incomingFilter['PRODUCTS_label']))
621 $filterFields['PRODUCTS'] = $incomingFilter['PRODUCTS'];
622 $filterFields['PRODUCTS_label'] = $incomingFilter['PRODUCTS_label'];
625 return $filterFields;
628 protected function initFilterFromIncomingData(array $incomingFilter): void
630 $filterFields = $this->prepareFilterIncomingData($incomingFilter);
632 if (count($filterFields) > 0)
634 $this->setFilterFields($filterFields);
638 protected function setFilterFields(array $filterFields): void
640 $filterOptions = $this->getFilterOptions();
641 $currentFilterSettings = $filterOptions->getFilterSettings('tmp_filter');
642 $currentFilterSettings['fields'] = $filterFields;
643 $filterOptions->setFilterSettings(
644 \Bitrix\Main\UI\Filter\Options::TMP_FILTER,
645 $currentFilterSettings,
649 $filterOptions->save();
652 protected function checkDocumentReadRights(): bool
655 !AccessController::getCurrent()->check(ActionDictionary::ACTION_CATALOG_READ)
656 || !AccessController::getCurrent()->check(ActionDictionary::ACTION_INVENTORY_MANAGEMENT_ACCESS)
663 $this->arParams['STORE_ID'] > 0
664 ? AccessController::getCurrent()->checkByValue(ActionDictionary::ACTION_STORE_VIEW, $this->arParams['STORE_ID'])
665 : AccessController::getCurrent()->check(ActionDictionary::ACTION_STORE_VIEW)
669 protected function getTotalCount(): int
671 return $this->buildDataQuery()->exec()->getCount();
674 protected function getListFilter(): array
676 if (!$this->isAllStoresGrid())
679 '=STORE_ID' => $this->storeId,
685 AccessController::getCurrent()
687 ActionDictionary::ACTION_STORE_VIEW,
688 StoreProductTable::class
692 $incomingFilter = $this->arParams['INCOMING_FILTER'] ?? [];
693 if (isset($incomingFilter['STORES']))
695 $filter['=STORE_ID'] = $incomingFilter['STORES'];
699 $searchString = trim($this->getFilterOptions()->getSearchString());
702 $filter['%PRODUCT.IBLOCK_ELEMENT.SEARCHABLE_CONTENT'] = mb_strtoupper($searchString);
705 $userFilter = $this->getUserFilter();
706 if (!empty($userFilter['PRODUCTS']))
708 $filter['=PRODUCT_ID'] = $this->prepareProductFilter($userFilter['PRODUCTS']);
713 !empty($userFilter['>=REPORT_INTERVAL'])
714 && !empty($userFilter['<=REPORT_INTERVAL'])
717 $filter['REPORT_INTERVAL'] = [
718 'FROM' => $userFilter['>=REPORT_INTERVAL'],
719 'TO' => $userFilter['<=REPORT_INTERVAL'],
723 $filter['@PRODUCT.TYPE'] = [
724 ProductTable::TYPE_PRODUCT,
725 ProductTable::TYPE_OFFER,
731 protected function getStoreTitle(): string
733 $storeData = \Bitrix\Catalog\StoreTable::getList([
734 'select' => ['TITLE'],
735 'filter' => ['=ID' => $this->storeId],
739 return $storeData['TITLE'] ?? '';
742 protected function getFilterFields(): array
746 if (Loader::includeModule('crm'))
749 'id' => 'product_variation',
751 'iblockId' => \Bitrix\Crm\Product\Catalog::getDefaultId(),
752 'basePriceId' => Catalog\GroupTable::getBasePriceTypeId(),
753 'showPriceInCaption' => false,
761 'name' => Loc::getMessage('CATALOG_REPORT_PRODUCT_LIST_FILTER_PRODUCTS_TITLE'),
762 'type' => 'entity_selector',
767 'showDialogOnEmptyInput' => false,
768 'dropdownMode' => true,
770 'hideOnSelect' => false,
771 'context' => $this->getProductFilterDialogContext(),
772 'entities' => $entities,
773 'recentTabOptions' => [
776 'title' => Loc::getMessage('CATALOG_REPORT_PRODUCT_LIST_PRODUCT_FILTER_STUB'),
780 'onBeforeSearch' => 'onBeforeDialogSearch',
788 protected function getUserFilter(): array
790 $filterOptions = $this->getFilterOptions();
791 $filterFields = $this->getFilterFields();
793 return $filterOptions->getFilterLogic($filterFields);
796 protected function getFilterOptions(): \Bitrix\Main\UI\Filter\Options
798 static $filterOptions = null;
799 if (is_null($filterOptions))
801 $filterOptions = new \Bitrix\Main\UI\Filter\Options($this->getFilterId());
804 return $filterOptions;
onPrepareComponentParams($arParams)
getAmountSoldData(int $storeId, array $formattedFilter)
getReceivedQuantityData(int $storeId, array $formattedFilter)
buildDataQuery($order=null, $limit=null, $offset=null)
getReservedDealListLink(int $productId, string $quantityReservedView)
getProductView(array $column)
getOutgoingQuantityData(int $storeId, array $formattedFilter)
prepareProductFilter(array $productIds)
formatNumberWithMeasure($number, int $measureId)
string $reportFilterClass
Bitrix Main Grid Options $gridOptions
prepareItemColumn(array $item)
getReservedDealsSliderLink(int $productId)
getProductFilterDialogContext()
checkDocumentReadRights()
static GetComponentID($componentName, $componentTemplate, $additionalID)
includeComponentTemplate($templatePage="", $customTemplatePath="")
</td ></tr ></table ></td ></tr >< tr >< td class="bx-popup-label bx-width30"><?=GetMessage("PAGE_NEW_TAGS")?> array( $site)
htmlspecialcharsbx($string, $flags=ENT_COMPAT, $doubleEncode=true)
if( $daysToExpire >=0 &&$daysToExpire< 60 elseif)( $daysToExpire< 0)
if(empty($signedUserToken)) $key