public const TYPE_WRITE_OFFS = 'writeOffs'; // Тип операции: списания
private const CATEGORY_LOOKBACK_MONTHS = 3; // Период для анализа категорий (месяцы)
private const LOOKBACK_MONTHS = 2; // Отступаемый шаг от плановой даты перед расчетами
+ const SPECIAL_PRODUCT = '2b72702a-792f-11e8-9edd-1c6f659fb563';
/**
* Получение списка видимых магазинов
? BouquetComposition::REGION_MSK
: BouquetComposition::REGION_NN;
}
- $priceRecords = PricesDynamic::find()
- ->select(['product_id', 'price'])
- ->where(['product_id' => $productsIds])
- ->andWhere(['active' => 1])
- ->andWhere(['region_id' => $region])
- ->indexBy('product_id')
- ->asArray()
- ->all();
- foreach ($priceRecords as $id => $record) {
- if ($goal == 0 || (int)$record['price'] == 0) {
+ $pricesMap = self::buildPricesMap($productsIds, self::SPECIAL_PRODUCT, $region);
+
+ foreach ($pricesMap as $id => $price) {
+ if ($goal == 0 || (float)$price == 0) {
$forecast = 0;
} else {
- $forecast = round(max($goal / (float)$record['price'], 1), 0);
+ $forecast = round(max($goal / (float)$price, 1), 0);
}
$speciesProductForecast[] = [
'category' => $category,
'subcategory' => $subcategory,
'species' => $species,
- 'product_id' => $record['product_id'],
+ 'product_id' => $id,
'name' => $products[$id]['name'],
- 'price' => $record['price'] ?? $goal ?? 1,
+ 'price' => $price ?? $goal ?? 1,
'goal' => $goal ?? 0,
'forecast' => $forecast
$year,
$goals,
$productSalesShare
-
);
return $result;
}
- /**
- * @param array|int $storeIds
- * @param int $month
- * @param int $year
- * @param int $regionId
- * @return array список строк с полями:
- * sale_id, sale_date, product_id, product_name,
- * component_guid, component_name, component_category,
- * quantity, price, cost
- */
- public function getUnmarkedProductsComponents(int $storeId, string $month, string $year, int $regionId = null, $typeFilter = null): array
- {
- $storeParams = CityStoreParams::find()
- ->where(['store_id' => $storeId])
- ->one();
-
- $region = $storeParams?->address_region;
-
- if (!$region && !$regionId) {
- $cityId = CityStore::find()->select('city_id')->where(['id' => $storeId])->scalar();
- $region = $cityId == 1
- ? BouquetComposition::REGION_MSK
- : BouquetComposition::REGION_NN;
- }
-
- $monthStart = sprintf('%04d-%02d-01 00:00:00', $year, $month);
- $daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year);
- $monthEnd = sprintf('%04d-%02d-%02d 23:59:59', $year, $month, $daysInMonth);
- $salesProducts = Sales::find()
- ->alias('s')
- ->select(['s.id', 's.date', 'sp.product_id', 'p1c.type', 'p1c.components' , 'p1c.name'])
- ->innerJoin(
- ['sp' => SalesProducts::tableName()],
- 's.id = sp.check_id'
- )
- ->innerJoin(
- ['p1c' => Products1c::tableName()],
- 'p1c.id = sp.product_id'
- )
- ->leftJoin(
- ['nom' => Products1cNomenclature::tableName()],
- 'nom.id = sp.product_id'
- )
- ->andWhere(['s.store_id' => $storeId])
- ->andWhere(['between', 's.date', $monthStart, $monthEnd])
- ->andWhere(['not', ['p1c.components' => '']])
- ->andWhere(['nom.category' => null])
- ->asArray()
- ->all();
-
- $components = [];
- $rows = [];
- foreach ($salesProducts as $sp) {
- /** @var SalesProducts $sp */
- $js = trim($sp['components']);
- if ($js === '' || $js[0] !== '{') {
- continue;
- }
- $data = @json_decode($js, true);
- if (!is_array($data)) {
- continue;
- }
- foreach ($data as $guid => $qty) {
- $qty = (int)$qty;
- if ($qty <= 0) {
- continue;
- }
-
- $components[$guid] = true;
-
- $rows[] = [
- 'sale_id' => $sp['id'],
- 'sale_date' => $sp['date'],
- 'product_id' => $sp['product_id'],
- 'product_name'=> $sp['name'],
- 'component_guid' => $guid,
- 'quantity' => $qty,
- ];
- }
- }
-
- if (empty($rows)) {
- return [];
- }
- $guids = array_keys($components);
-
- $nomenclatures = Products1cNomenclature::find()
- ->andWhere(['id' => $guids])
- ->indexBy('id')
- ->all();
-
- $priceDynamics = PricesDynamic::find()
- ->andWhere(['region_id' => $region])
- ->andWhere(['product_id' => array_values( ArrayHelper::getColumn($nomenclatures, 'id') )])
- ->orderBy(['date_from' => SORT_DESC])
- ->all();
-
- $pricesByProduct = [];
- foreach ($priceDynamics as $pd) {
- /** @var PricesDynamic $pd */
- $pid = $pd->product_id;
- $pricesByProduct[$pid][] = $pd;
- }
-
- $result = [];
- foreach ($rows as $r) {
- $guid = $r['component_guid'];
- $n = $nomenclatures[$guid] ?? null;
- $pid = $n?->id;
- $price = 0;
- if ($pid && isset($pricesByProduct[$pid])) {
- foreach ($pricesByProduct[$pid] as $pd) {
- if ($pd->date_from <= $r['sale_date'] && $pd->date_to >= $r['sale_date']) {
- $price = $pd->price;
- break;
- }
- }
- }
- $cost = $r['quantity'] * $price;
-
- $result[] = [
- 'sale_id' => $r['sale_id'],
- 'sale_date' => $r['sale_date'],
- 'product_id' => $r['product_id'],
- 'product_name' => $r['product_name'],
- 'component_guid' => $guid,
- 'component_name' => $n?->name,
- 'component_category' => $n?->category,
- 'quantity' => $r['quantity'],
- 'price' => $price,
- 'cost' => $cost,
- ];
- }
-
- return $result;
- }
public function calculateProductForecastInPiecesProductsWithHistory(
int $storeId,
? BouquetComposition::REGION_MSK
: BouquetComposition::REGION_NN;
}
- $priceRecords = PricesDynamic::find()
- ->where([
- 'product_id' => array_keys($productSalesShare),
- 'region_id' => $region,
- 'active' => 1,
- ])
- ->indexBy('product_id')->asArray()->all();
+
+ $pricesMap = self::buildPricesMap(array_keys($productSalesShare), self::SPECIAL_PRODUCT, $region);
foreach ($productSalesShare as $productId => $data) {
$share = $data['share'] ?? 0.0;
continue;
}
- if (!$priceRecords[$productId] || $priceRecords[$productId]['price'] <= 0) {
+ if (!$pricesMap[$productId] || $pricesMap[$productId] <= 0) {
continue;
}
$cat = $data['category'];
$sub = $data['subcategory'];
$spec = $data['species'];
- $price = $priceRecords[$productId]['price'];
+ $price = $pricesMap[$productId];
if (
! isset(
$goalsMap[$storeId],
->all();
$products = ArrayHelper::getColumn($productShares, 'product_id');
- $prices = PricesDynamic::find()
- ->select(['product_id', 'price', 'region_id'])
- ->where(['product_id' => $products])
- ->andWhere(['active' => 1])
- ->asArray()
- ->all();
+ $pricesMap = self::buildPricesMap($products, self::SPECIAL_PRODUCT);
- $pricesMap = [];
- foreach ($prices as $price) {
- $pricesMap[$price['product_id']][$price['region_id']][] = $price['price'];
- }
foreach ($productShares as $shareItem) {
$storeId = $shareItem['store_id'];
$region = $regions[$storeId]['address_region']
?? BouquetComposition::REGION_NN;
- $priceList = $pricesMap[$shareItem['product_id']][$region] ?? null;
- $price = is_array($priceList) && count($priceList) > 0
- ? $priceList[0] ?? 1
- : 1;
-
+ if (!$pricesMap[$shareItem['product_id']] || $pricesMap[$shareItem['product_id']][$region] <= 0) {
+ continue;
+ }
+ $price = $pricesMap[$shareItem['product_id']][$region];
$key = implode('|', [
$shareItem['store_id'],
$price = 0;
$dailyPrices = [];
foreach ($pricesByProduct[$productId] ?? [] as $priceRecordForProduct) {
- if ($productId == '2b72702a-792f-11e8-9edd-1c6f659fb563') {
+ if ($productId == self::SPECIAL_PRODUCT) {
$saleDay = (new \DateTime($componentDataRecord['sale_date']))->format('Y-m-d');
$fromDay = (new \DateTime($priceRecordForProduct->date_from))->modify('-1 day')->format('Y-m-d');
$toDay = (new \DateTime($priceRecordForProduct->date_to ))->modify('+1 day')->format('Y-m-d');
return array_values($filtered);
}
+
+ /**
+ * Строит карту цен за последние 20 дней.
+ *
+ * @param array $productIds Список product_id для выборки
+ * @param string $specialProductId GUID товара, для которого нужен min
+ * @param int|null $regionId Если указан — один регион, иначе — несколько
+ * @return array
+ */
+ public static function buildPricesMap(
+ array $productIds,
+ string $specialProductId,
+ ?int $regionId = null
+ ): array {
+ $periodEnd = (new \DateTime())->format('Y-m-d H:i:s');
+ $periodStart = (new \DateTime())->modify('-20 days')->format('Y-m-d H:i:s');
+
+
+ $query = PricesDynamic::find()
+ ->select(['product_id', 'region_id', 'price', 'active', 'date_from', 'date_to'])
+ ->where(['product_id' => $productIds])
+ ->andWhere(['<=', 'date_from', $periodEnd])
+ ->andWhere(['>=', 'date_to', $periodStart])
+ ->orderBy(['date_from' => SORT_ASC]);
+
+ if ($regionId !== null) {
+ $query->andWhere(['region_id' => $regionId]);
+ }
+
+ $priceRecords = $query->asArray()->all();
+ $pricesMap = [];
+ $multiRegion = $regionId === null;
+
+ foreach ($priceRecords as $priceRecord) {
+ $productId = $priceRecord['product_id'];
+ $price = (float)$priceRecord['price'];
+
+ $regionId = $multiRegion
+ ? $priceRecord['region_id']
+ : null;
+
+ if ($productId === $specialProductId) {
+ if ($multiRegion) {
+ if (!isset($pricesMap[$productId][$regionId])) {
+ $pricesMap[$productId][$regionId] = [$price];
+ } else {
+ $pricesMap[$productId][$regionId] = min(
+ $pricesMap[$productId][$regionId],
+ $price
+ );
+ }
+ } else {
+ if (!isset($pricesMap[$productId])) {
+ $pricesMap[$productId] = [$price];
+ } else {
+ $pricesMap[$productId] = min(
+ $pricesMap[$productId],
+ $price
+ );
+ }
+ }
+ continue;
+ }
+
+ if (isset($priceRecord['active']) && $priceRecord['active'] !== 1) {
+ continue;
+ }
+
+ if ($multiRegion) {
+ $pricesMap[$productId][$regionId] = $price;
+ } else {
+ $pricesMap[$productId] = $price;
+ }
+ }
+
+ return $pricesMap;
+ }
+
}
\ No newline at end of file