Your IP : 216.73.216.86


Current Path : /var/www/homesaver/www/bitrix/modules/catalog/lib/integration/report/storestock/
Upload File :
Current File : /var/www/homesaver/www/bitrix/modules/catalog/lib/integration/report/storestock/storestocksale.php

<?php

namespace Bitrix\Catalog\Integration\Report\StoreStock;

\Bitrix\Main\Loader::includeModule('sale');

use Bitrix\Catalog\Access\AccessController;
use Bitrix\Catalog\Access\ActionDictionary;
use Bitrix\Catalog\Integration\Report\StoreStock\Entity\ProductInfo;
use Bitrix\Catalog\Integration\Report\StoreStock\Entity\Store\StoreInfo;
use Bitrix\Catalog\Integration\Report\StoreStock\Entity\Store\StoreWithProductsInfo;
use Bitrix\Catalog\Product\Store\CostPriceCalculator;
use Bitrix\Catalog\StoreBatchDocumentElementTable;
use Bitrix\Catalog\StoreDocumentTable;
use Bitrix\Catalog\StoreBatchTable;
use Bitrix\Main\ORM\Fields\ExpressionField;
use Bitrix\Sale\Internals\ShipmentItemStoreTable;
use Bitrix\Sale\Internals\ShipmentItemTable;
use Bitrix\Catalog\StoreProductTable;
use Bitrix\Catalog\ProductTable;
use Bitrix\Currency\CurrencyManager;
use Bitrix\Currency\CurrencyTable;
use Bitrix\Main\Type\DateTime;
use Bitrix\Main\ORM\Fields\Relations\Reference;
use Bitrix\Main\ORM\Query\Join;
use Bitrix\Main\Entity\Base;

/** @internal - use at your own risk */
final class StoreStockSale
{
	protected const DEFAULT_DATE_INTERVAL = '-30D';

	protected static $defaultCurrency;

	protected static $productPrice;

	public static function getProductsSoldAmountForStores($filter = []): array
	{
		$soldProductsDbResult = self::getProductsSoldAmountFromShipmentsList($filter);
		$result = [];
		while ($soldProduct = $soldProductsDbResult->fetch())
		{
			$storeId = (int)$soldProduct['STORE_ID'];
			if (!isset($result[$storeId]))
			{
				$result[$storeId] = [];
			}

			$measureId = (int)$soldProduct['MEASURE_ID'] ?: \CCatalogMeasure::getDefaultMeasure(true)['ID'];
			if (!isset($result[$storeId][$measureId]))
			{
				$result[$storeId][$measureId] = 0.0;
			}

			$result[$storeId][$measureId] += (float)$soldProduct['QUANTITY_SUM'];
		}

		return $result;
	}

	public static function getProductsSoldAmountForProducts($filter = []): array
	{
		$shipmentsDbResult = self::getProductsSoldAmountFromShipmentsList($filter);
		$result = [];

		while ($row = $shipmentsDbResult->fetch())
		{
			$result[$row['PRODUCT_ID']] ??= 0;
			$result[$row['PRODUCT_ID']] += (float)$row['QUANTITY_SUM'];
		}

		return $result;
	}
	public static function getProductsSoldAmountForProductsOnStore(int $storeId, $filter = []): array
	{
		$filter['STORES'] = $storeId;

		return self::getProductsSoldAmountForProducts($filter);
	}

	public static function getProductsSoldPricesForStores(array $filter = []): array
	{
		$soldProductsDbResult = self::getProductsSoldPricesFromShipmentsList($filter);
		$result = [];
		while ($soldProduct = $soldProductsDbResult->fetch())
		{
			$storeId = (int)$soldProduct['STORE_ID'];
			$result[$storeId] ??= [];
			$currencyId = $soldProduct['CURRENCY'];
			$result[$storeId][$currencyId] ??= [];
			$result[$storeId][$currencyId]['COST_PRICE'] ??= 0.0;
			$result[$storeId][$currencyId]['COST_PRICE'] += (float)$soldProduct['COST_PRICE_SUM'];
			$result[$storeId][$currencyId]['TOTAL_SOLD'] ??= 0.0;
			$result[$storeId][$currencyId]['TOTAL_SOLD'] += (float)$soldProduct['TOTAL_SOLD'];
		}

		return $result;
	}

	public static function getProductsSoldPricesForProducts($filter = []): array
	{
		$shipmentsDbResult = self::getProductsSoldPricesFromShipmentsList($filter);
		$result = [];

		while ($row = $shipmentsDbResult->fetch())
		{
			$currencyId = $row['CURRENCY'];
			$result[$row['PRODUCT_ID']][$currencyId] ??= [];
			$result[$row['PRODUCT_ID']][$currencyId]['COST_PRICE'] ??= 0.0;
			$result[$row['PRODUCT_ID']][$currencyId]['COST_PRICE'] += (float)$row['COST_PRICE_SUM'];
			$result[$row['PRODUCT_ID']][$currencyId]['TOTAL_SOLD'] ??= 0.0;
			$result[$row['PRODUCT_ID']][$currencyId]['TOTAL_SOLD'] += (float)$row['TOTAL_SOLD'];
		}

		return $result;
	}

	public static function getProductsSoldPricesForProductsOnStore(int $storeId, $filter = []): array
	{
		$filter['=STORES'] = $storeId;

		return self::getProductsSoldPricesForProducts($filter);
	}

	public static function getProductsSoldPricesForDeductedPeriod(array $filter = []): array
	{
		$getListParameters = self::getShippedDataListParameters($filter);

		$getListParameters['select']['CURRENCY'] = 'BASKET.CURRENCY';
		$getListParameters['select']['BASKET_PRICE'] = 'BASKET.PRICE';
		$getListParameters['select']['DATE_DEDUCTED'] = 'DELIVERY.DATE_DEDUCTED';

		$getListParameters['runtime'][] = new Reference(
			'S_PRODUCT_BATCH_SHIPMENT',
			StoreBatchDocumentElementTable::class,
			Join::on('this.S_BARCODE.ID', 'ref.SHIPMENT_ITEM_STORE_ID')
		);

		$getListParameters['select']['COST_PRICE'] = 'S_PRODUCT_BATCH_SHIPMENT.BATCH_PRICE';
		$getListParameters['select']['BASKET_QUANTITY'] = 'S_PRODUCT_BATCH_SHIPMENT.AMOUNT';

		return ShipmentItemTable::getList($getListParameters)->fetchAll();
	}

	/**
	 * @param array $filter
	 * @return \Bitrix\Main\ORM\Query\Result
	 */
	private static function getProductsSoldAmountFromShipmentsList(array $filter = []): \Bitrix\Main\ORM\Query\Result
	{
		$getListParameters = self::getShippedDataListParameters($filter);

		$getListParameters['select']['MEASURE_ID'] = 'BASKET.PRODUCT.MEASURE';
		$getListParameters['select'][] = 'QUANTITY_SUM';
		$getListParameters['group'] = ['BASKET.PRODUCT_ID', 'S_BARCODE.STORE_ID'];
		$getListParameters['runtime'][] = new ExpressionField(
			'QUANTITY_SUM',
			'SUM(%s)',
			['QUANTITY']
		);

		return ShipmentItemTable::getList($getListParameters);
	}

	/**
	 * @param array $filter
	 * @return \Bitrix\Main\ORM\Query\Result
	 */
	private static function getProductsSoldPricesFromShipmentsList(array $filter = []): \Bitrix\Main\ORM\Query\Result
	{
		$getListParameters = self::getShippedDataListParameters($filter);

		$storeBatchQuery = StoreBatchDocumentElementTable::query();
		$storeBatchQuery->registerRuntimeField(
			new ExpressionField(
				'COST_PRICE_SUM',
				'SUM(%s * %s * -1)',
				['AMOUNT', 'BATCH_PRICE']
			)
		);
		$storeBatchQuery->registerRuntimeField(
			new ExpressionField(
				'SUM_AMOUNT',
				'SUM(%s * -1)',
				['AMOUNT']
			)
		);
		$storeBatchQuery->setSelect([
			'SHIPMENT_ITEM_STORE_ID',
			'COST_PRICE_SUM',
			'BATCH_CURRENCY',
			'SUM_AMOUNT'
		]);
		$storeBatchQuery->setGroup(['SHIPMENT_ITEM_STORE_ID', 'BATCH_CURRENCY']);


		$getListParameters['runtime'][] = new Reference(
			'SUBQUERY',
			Base::getInstanceByQuery($storeBatchQuery),
			Join::on('this.S_BARCODE.ID', 'ref.SHIPMENT_ITEM_STORE_ID')
		);

		$getListParameters['select']['COST_PRICE_SUM'] = 'SUBQUERY.COST_PRICE_SUM';
		$getListParameters['select']['CURRENCY'] = 'SUBQUERY.BATCH_CURRENCY';
		$getListParameters['runtime'][] = new ExpressionField(
			'TOTAL_SOLD',
			'SUM(%s * %s)',
			['BASKET.PRICE', 'SUBQUERY.SUM_AMOUNT']
		);

		$getListParameters['select'][] = 'TOTAL_SOLD';
		$getListParameters['group'] = ['BASKET.PRODUCT_ID', 'S_BARCODE.STORE_ID', 'CURRENCY'];

		return ShipmentItemTable::getList($getListParameters);
	}

	public static function getStoreStockSaleData(bool $isOneField, array $filter): array
	{
		$filter = self::prepareFilter($filter);
		$reservedData = self::getReservedData($filter);

		$productIds = array_column($reservedData, 'PRODUCT_ID');
		self::initProductPrice($productIds);

		$storeIds =
			$filter['STORES']
			?? array_column($reservedData, 'STORE_ID')
		;

		$storesData = [];
		if ($isOneField)
		{
			$storesData = self::formField($reservedData);
			$storesData['STORE_IDS'] = $storeIds;
		}
		else
		{
			$storesPositionData = array_fill_keys(
				$storeIds,
				[
					'reservedData' => [],
				]
			);

			foreach ($reservedData as $reservedPosition)
			{
				$storesPositionData[$reservedPosition['STORE_ID']]['reservedData'][] = $reservedPosition;
			}

			foreach ($storesPositionData as $storeId => $fieldData)
			{
				$storesData[] = self::formField($fieldData['reservedData'], $storeId);
			}
		}

		return $storesData;
	}

	public static function getDefaultReportInterval(): array
	{
		$currentDate = new DateTime();
		$intervalStartDate = new DateTime();
		$intervalStartDate->add(self::DEFAULT_DATE_INTERVAL);

		return [
			'FROM' => $intervalStartDate->toString(),
			'TO' => $currentDate->toString(),
		];
	}

	private static function getProductPrice(int $productId): float
	{
		if (!isset(self::$productPrice[$productId]))
		{
			self::initProductPrice([$productId]);
			self::$productPrice[$productId] ??= 0;
		}

		return self::$productPrice[$productId];
	}

	private static function prepareFilter(array $filter): array
	{
		if (isset($filter['REPORT_INTERVAL_from']) && isset($filter['REPORT_INTERVAL_to']))
		{
			$filter['REPORT_INTERVAL'] = [
				'FROM' => $filter['REPORT_INTERVAL_from'],
				'TO' => $filter['REPORT_INTERVAL_to'],
			];
		}

		$accessController = AccessController::getCurrent();
		if (!$accessController->checkCompleteRight(ActionDictionary::ACTION_STORE_VIEW))
		{
			$availableStores = $accessController->getPermissionValue(ActionDictionary::ACTION_STORE_VIEW) ?? [];

			if (isset($filter['STORES']) && is_array($filter['STORES']))
			{
				$filter['STORES'] = array_values(array_intersect($availableStores, $filter['STORES']));
			}
			else
			{
				$filter['STORES'] = $availableStores;
			}
		}

		if
		(
			!isset($filter['REPORT_INTERVAL'])
			|| !isset($filter['REPORT_INTERVAL']['FROM'])
			|| !isset($filter['REPORT_INTERVAL']['TO'])
		)
		{
			$filter['REPORT_INTERVAL'] = self::getDefaultReportInterval();
		}

		$filter['INNER_MOVEMENT'] = (bool)($filter['INNER_MOVEMENT'] ?? true);

		return $filter;
	}


	/**
	 * Return array of stores that was involved in realization documents and match by filter <b>$filter</b>
	 * @param array $filter
	 * @return array <b>array</b> of instances <b>StoreInfo</b>
	 * @throws \Bitrix\Main\ArgumentException
	 * @throws \Bitrix\Main\ObjectPropertyException
	 * @throws \Bitrix\Main\SystemException
	 */
	public static function getShippedData(array $filter): array
	{
		$getListParameters = self::getShippedDataListParameters($filter);
		$getListParameters['select']['QUANTITY'] = 'QUANTITY';

		return self::formStoresListFromStoresData($filter, ShipmentItemTable::getList($getListParameters)->fetchAll());
	}

	protected static function formStoresListFromStoresData(array $filter, array $storesData): array
	{
		ProductInfo::initBasePrice(...array_column($storesData, 'PRODUCT_ID'));
		StoreInfo::loadStoreName(...array_column($storesData, 'STORE_ID'));

		$storesInfo = [];

		if (isset($filter['STORES']))
		{
			$storesInfo = array_fill_keys($filter['STORES'], []);
		}
		foreach ($storesData as $shipmentItem)
		{
			$storeId = $shipmentItem['STORE_ID'];
			$productId = $shipmentItem['PRODUCT_ID'];
			if (!isset($storesInfo[$storeId]))
			{
				$storesInfo[$storeId] = [];
			}

			if (!isset($storesInfo[$storeId][$productId]))
			{
				$storesInfo[$storeId][$productId] = (float)$shipmentItem['QUANTITY'];
			}
			else
			{
				$storesInfo[$storeId][$productId] += (float)$shipmentItem['QUANTITY'];
			}
		}

		$stores = [];
		foreach ($storesInfo as $storeId => $storeInfo)
		{
			$store = new StoreWithProductsInfo($storeId);
			foreach ($storeInfo as $productId => $quantity)
			{
				$store->addProduct(new ProductInfo($productId, $quantity));
			}
			$stores[] = $store;
		}

		return $stores;
	}

	protected static function getShippedDataListParameters(array $filter): array
	{
		$filter = self::prepareFilter($filter);

		return [
			'select' => [
				'STORE_ID' => 'S_BARCODE.STORE_ID',
				'PRODUCT_ID' => 'BASKET.PRODUCT_ID',
			],

			'filter' => self::formShipmentDataFilter($filter),

			'runtime' => [
				(new Reference(
					'S_BARCODE',
					ShipmentItemStoreTable::class,
					Join::on('this.ID', 'ref.ORDER_DELIVERY_BASKET_ID')
				))->configureJoinType(Join::TYPE_LEFT),
			],
		];
	}

	private static function formShipmentDataFilter(array $filter): array
	{
		$formedFilter = [
			'=DELIVERY.DEDUCTED' => 'Y',
			'>S_BARCODE.STORE_ID' => 0,
		];

		if (isset($filter['STORES']))
		{
			$formedFilter['=S_BARCODE.STORE_ID'] = $filter['STORES'];
		}

		if (isset($filter['=STORES']))
		{
			$formedFilter['=S_BARCODE.STORE_ID'] = $filter['=STORES'];
		}

		if (isset($filter['PRODUCTS']))
		{
			$formedFilter['=BASKET.PRODUCT_ID'] = $filter['PRODUCTS'];
		}

		if (isset($filter['REPORT_INTERVAL']))
		{
			$formedFilter['>=DELIVERY.DATE_DEDUCTED'] = new DateTime($filter['REPORT_INTERVAL']['FROM']);
			$formedFilter['<=DELIVERY.DATE_DEDUCTED'] = new DateTime($filter['REPORT_INTERVAL']['TO']);
		}

		return $formedFilter;
	}

	/**
	 * Return array of stores that was involved in arrived documents and match by filter <b>$filter</b>
	 * @param array $filter
	 * @return array <b>array</b> of instances <b>ProductStorage</b>
	 * @see \Bitrix\Catalog\Integration\Report\StoreStock\Entity\ProductStorage
	 */
	public static function getArrivedData(array $filter): array
	{
		$getListParameters = self::getArrivedDataListParameters($filter);
		return self::formStoresListFromStoresData($filter, StoreDocumentTable::getList($getListParameters)->fetchAll());
	}

	/**
	 * Return computed percent of sold products from store
	 *
	 * @param float $shippedSum
	 * @param float $arrivedSum
	 * @param int $precision
	 * @return float
	 */
	public static function computeSoldPercent(float $shippedSum, float $arrivedSum, int $precision = 2): float
	{

		if ($shippedSum === 0.0)
		{
			$soldPercent = 0;
		}
		elseif ($arrivedSum === 0.0)
		{
			$soldPercent = 100;
		}
		else
		{
			$soldPercent = ($shippedSum / $arrivedSum) * 100;
		}

		return round($soldPercent, $precision);
	}

	protected static function getArrivedDataListParameters(array $filter): array
	{
		$filter = self::prepareFilter($filter);

		return [
			'select' => [
				'PRODUCT_ID' => 'ELEMENTS.ELEMENT_ID',
				'QUANTITY' => 'ELEMENTS.AMOUNT',
				'STORE_ID' => 'ELEMENTS.STORE_TO',
			],

			'filter' => self::formArrivedDataFilter($filter),
		];
	}

	private static function formArrivedDataFilter(array $filter): array
	{
		$docTypes = [StoreDocumentTable::TYPE_ARRIVAL, StoreDocumentTable::TYPE_STORE_ADJUSTMENT];
		if ($filter['INNER_MOVEMENT'])
		{
			$docTypes[] = StoreDocumentTable::TYPE_MOVING;
		}
		$formedFilter = [
			'=DOC_TYPE' => $docTypes,
			'=STATUS' => 'Y',
			'>ELEMENTS.ELEMENT_ID' => 0,
		];

		if (isset($filter['STORES']))
		{
			$formedFilter['=ELEMENTS.STORE_TO'] = $filter['STORES'];
		}

		if (isset($filter['PRODUCTS']))
		{
			$formedFilter['=ELEMENTS.ELEMENT_ID'] = $filter['PRODUCTS'];
		}

		if (isset($filter['REPORT_INTERVAL']))
		{
			$formedFilter['>=DATE_STATUS'] = new DateTime($filter['REPORT_INTERVAL']['FROM']);
			$formedFilter['<=DATE_STATUS'] = new DateTime($filter['REPORT_INTERVAL']['TO']);
		}

		return $formedFilter;
	}

	public static function getReservedData(array $filter): array
	{
		$reservedData = StoreProductTable::getList([
			'select' => [
				'PRODUCT_ID',
				'STORE_ID',
				'QUANTITY' => 'AMOUNT',
			],
			'filter' => self::formReservedDataFilter($filter),
		])->fetchAll();

		return self::formStoresListFromStoresData($filter, $reservedData);
	}

	private static function formReservedDataFilter(array $filter): array
	{
		$formedFilter = [
			'>STORE_ID' => 0,
		];

		$filter = self::prepareFilter($filter);
		if (isset($filter['STORES']))
		{
			$formedFilter['=STORE_ID'] = $filter['STORES'];
		}
		else
		{
			$formedFilter['!=QUANTITY'] = 0;
		}

		if (isset($filter['PRODUCTS']))
		{
			$formedFilter['=PRODUCT_ID'] = $filter['PRODUCTS'];
		}

		return $formedFilter;
	}

	private static function combineUniqueColumnElements(array $arraysList, string $columnKey): array
	{
		$combineColumnElements = [];
		foreach ($arraysList as $item)
		{
			$columnElements = array_column($item, $columnKey);
			array_push($combineColumnElements, ...$columnElements);
		}

		return array_unique($combineColumnElements);
	}

	protected static function formField(array $storeReservedData, int $storeId = null): array
	{
		$storedSum = 0.0;
		foreach ($storeReservedData as $storePosition)
		{
			$storedSum += self::getPositionPrice($storePosition['PRODUCT_ID'], $storePosition['QUANTITY']);
		}

		$result = [
			'SUM_STORED' => $storedSum,
		];

		if ($storeId !== null)
		{
			$result['STORE_ID'] = $storeId;
		}

		return $result;
	}

	protected static function getPositionPrice(int $productId, float $productCount): float
	{
		return self::getProductPrice($productId) * $productCount;
	}

	protected static function initProductPrice(array $productIds): void
	{
		$defaultCurrency = CurrencyManager::getBaseCurrency();
		$productsData = ProductTable::getList([
			'select' => [
				'ID',
				'PURCHASING_PRICE',
				'PURCHASING_CURRENCY',
				'PURCHASING_CURRENCY_AMOUNT' => 'CURRENCY_TABLE.CURRENT_BASE_RATE',
			],
			'filter' => [
				'=ID' => $productIds,
			],
			'runtime' => [
				(new Reference(
					'CURRENCY_TABLE',
					CurrencyTable::class,
					Join::on('this.PURCHASING_CURRENCY', 'ref.CURRENCY')
				))->configureJoinType(Join::TYPE_LEFT),
			],
		])->fetchAll();

		foreach ($productsData as $product)
		{
			self::$productPrice[$product['ID']] = (float)$product['PURCHASING_PRICE'];
			if ($product['PURCHASING_CURRENCY'] !== $defaultCurrency)
			{
				$defaultCurrencyAmount = (float)\CCurrency::getCurrency($defaultCurrency)['CURRENT_BASE_RATE'];
				$currentCurrencyAmount = (float)$product['PURCHASING_CURRENCY_AMOUNT'];

				self::$productPrice[$product['ID']] *= $currentCurrencyAmount;
				self::$productPrice[$product['ID']] /= $defaultCurrencyAmount;
			}
		}
	}
}