analytics/sql_db_mag_pbi/mag_pbi_procedures.sql
Андрей Лебедев d31bd98f27 Update SQL schema from mag_pbi
2026-02-22 12:04:15 +00:00

3549 lines
131 KiB
Transact-SQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

USE [mag_pbi]
GO
/****** Object: StoredProcedure [analytics].[create_forecast_loop] Script Date: 2026-02-22 12:04:15 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE procedure [analytics].[create_forecast_loop] as begin
DECLARE @from_month DATE = DATEFROMPARTS(YEAR(GETDATE()), MONTH(GETDATE()), 1);
DECLARE @to_month_excl DATE = '2027-01-01';
DECLARE @scenario_id INT = 4;
DECLARE @path NVARCHAR(255);
DECLARE cur CURSOR LOCAL FAST_FORWARD FOR
SELECT [path]
FROM [pbi].[groups] g
WHERE [lvl] = 2
AND g.[path] NOT LIKE '*%' -- (опционально) исключить служебные группы
/*AND (
g.[path] LIKE N'Р%' OR
g.[path] LIKE N'С%' OR
g.[path] LIKE N'Т%' OR
g.[path] LIKE N'У%' OR
g.[path] LIKE N'Ф%' OR
g.[path] LIKE N'Х%' OR
g.[path] LIKE N'Ц%' OR
g.[path] LIKE N'Ч%' OR
g.[path] LIKE N'Ш%' OR
g.[path] LIKE N'Щ%' OR
g.[path] LIKE N'Э%' OR
g.[path] LIKE N'Ю%' OR
g.[path] LIKE N'Я%'
)*/
ORDER BY [path];
OPEN cur;
FETCH NEXT FROM cur INTO @path;
WHILE @@FETCH_STATUS = 0
BEGIN
PRINT CONCAT('Rebuild forecast for: ', @path);
EXEC [analytics].[sp_build_forecast_s4_by_group]
@path = @path,
@from_month = @from_month,
@to_month_excl = @to_month_excl,
@scenario_id = @scenario_id;
FETCH NEXT FROM cur INTO @path;
END
CLOSE cur;
DEALLOCATE cur;
END
GO
/****** Object: StoredProcedure [analytics].[create_seasonality_groups] Script Date: 2026-02-22 12:04:15 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE analytics.[create_seasonality_groups] as
BEGIN
/* 1) Таблица результата */
IF NOT EXISTS (
SELECT 1
FROM sys.tables t
JOIN sys.schemas s ON s.schema_id = t.schema_id
WHERE s.name = 'analytics' AND t.name = 'seasonality_groups'
)
BEGIN
CREATE TABLE [analytics].[seasonality_groups](
[group_1c_id] BINARY(16) NOT NULL,
[month] TINYINT NOT NULL, -- 1..12
[seasonal_koef] DECIMAL(18,6) NOT NULL,
CONSTRAINT [PK_seasonality_groups] PRIMARY KEY ([group_1c_id], [month])
);
CREATE INDEX [IX_seasonality_groups_month]
ON [analytics].[seasonality_groups]([month]);
END
TRUNCATE TABLE [analytics].[seasonality_groups];
/* 2) Пересчёт коэффициентов сезонности для g и g1 */
;WITH
-- Продажи по товарам за последние 24 месяца (кол-во)
[base_sales] AS (
SELECT
[n].[1c_id] AS [sku_1c_id],
[gcur].[g],
[gcur].[g1],
[v].[Период] AS [dt],
CAST([v].[Количество] AS DECIMAL(18,6)) AS [qty]
FROM [mag_pbi].[pbiProd].[СводныйСебестоимость Для PBI] AS [v]
JOIN [pbi].[nomenclature] AS [n]
ON [n].[1c_id] = [v].[1c_id]
LEFT JOIN [pbi].[groups] AS [gcur]
ON [gcur].[1c_id] = [n].[1c_group]
WHERE [v].[Статья] = N'Реализация'
AND [v].[Период] >= DATEADD(YEAR, -2, CAST(GETDATE() AS date))
),
-- Агрегация по месяцам и уровню g (lvl=0)
[lvl0_monthly] AS (
SELECT
[g0].[1c_id] AS [group_1c_id],
DATEFROMPARTS(YEAR([b].[dt]), MONTH([b].[dt]), 1) AS [month_start],
SUM([b].[qty]) AS [qty_m]
FROM [base_sales] AS [b]
JOIN [pbi].[groups] AS [g0]
ON [g0].[lvl] = 1
AND [g0].[g] = [b].[g]
GROUP BY [g0].[1c_id], DATEFROMPARTS(YEAR([b].[dt]), MONTH([b].[dt]), 1)
),
-- Агрегация по месяцам и уровню g1 (lvl=1)
[lvl1_monthly] AS (
SELECT
[g1].[1c_id] AS [group_1c_id],
DATEFROMPARTS(YEAR([b].[dt]), MONTH([b].[dt]), 1) AS [month_start],
SUM([b].[qty]) AS [qty_m]
FROM [base_sales] AS [b]
JOIN [pbi].[groups] AS [g1]
ON [g1].[lvl] = 2
AND [g1].[g1] = [b].[g1]
GROUP BY [g1].[1c_id], DATEFROMPARTS(YEAR([b].[dt]), MONTH([b].[dt]), 1)
),
-- Объединяем уровни
[monthly_union] AS (
SELECT * FROM [lvl0_monthly]
UNION ALL
SELECT * FROM [lvl1_monthly]
),
-- Средние по «месяцу года»
[per_moy] AS (
SELECT
[u].[group_1c_id],
MONTH([u].[month_start]) AS [month],
AVG([u].[qty_m]) AS [avg_qty_moy]
FROM [monthly_union] AS [u]
GROUP BY [u].[group_1c_id], MONTH([u].[month_start])
),
-- Общая средняя по группе
[overall] AS (
SELECT
[u].[group_1c_id],
AVG([u].[qty_m]) AS [overall_avg_monthly]
FROM [monthly_union] AS [u]
GROUP BY [u].[group_1c_id]
),
-- Черновой коэффициент
[raw_koef] AS (
SELECT
[p].[group_1c_id],
[p].[month],
CASE
WHEN [o].[overall_avg_monthly] = 0 THEN 0
ELSE [p].[avg_qty_moy] / [o].[overall_avg_monthly]
END AS [k_raw]
FROM [per_moy] AS [p]
JOIN [overall] AS [o]
ON [o].[group_1c_id] = [p].[group_1c_id]
),
[norm] AS (
SELECT
[r].[group_1c_id],
[r].[month],
[r].[k_raw] / NULLIF(AVG([r].[k_raw]) OVER (PARTITION BY [r].[group_1c_id]), 0) AS [seasonal_koef]
FROM [raw_koef] AS [r]
)
INSERT INTO [analytics].[seasonality_groups] ([group_1c_id], [month], [seasonal_koef])
SELECT [group_1c_id], [month], CAST([seasonal_koef] AS DECIMAL(18,6))
FROM [norm];
END
GO
/****** Object: StoredProcedure [analytics].[sp_build_deficit_proposal] Script Date: 2026-02-22 12:04:15 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
/* =======================================================================
[analytics].[sp_build_deficit_proposal]
— Дефицит и рекомендации к заказу с учётом:
• стартовых остатков (склад + МП),
• прихода из заказов (4 статуса, включая 'Согласован'),
• прогноза по сценарию,
НЕТ бэк-ордеров: спрос не «копится» ниже нуля,
остаток считается итеративно и не уходит в минус,
• размер заказа = прогноз окна [T .. T + @cover_months).
======================================================================= */
CREATE PROCEDURE [analytics].[sp_build_deficit_proposal]
@scenario_id INT = 4, -- ID сценария прогноза
@group_path NVARCHAR(255) = N'', -- path группы ('' = весь каталог)
@lead_time_m INT = 4, -- срок поставки (мес.)
@cover_months INT = 6, -- покрытие (мес.)
@from_month DATE = NULL, -- старт (1-е число месяца)
@to_month_excl DATE = '2028-01-01', -- полуинтервал [from, to)
@debug BIT = 0
AS
BEGIN
SET NOCOUNT ON;
/* 0) Чистим прошлые данные по сценарию */
DELETE FROM [analytics].[deficit_proposal] WHERE scenario_id = @scenario_id;
/* Нормализация параметров */
SET @group_path = LTRIM(RTRIM(REPLACE(@group_path, '''', N'')));
IF @from_month IS NULL
SET @from_month = DATEFROMPARTS(YEAR(GETDATE()), MONTH(GETDATE()), 1);
/* 1) Календарь */
IF OBJECT_ID('tempdb..#cal') IS NOT NULL DROP TABLE #cal;
CREATE TABLE #cal (month_start DATE NOT NULL PRIMARY KEY);
;WITH cal AS (
SELECT @from_month AS month_start
UNION ALL
SELECT DATEADD(MONTH, 1, month_start)
FROM cal
WHERE month_start < DATEADD(MONTH, -1, @to_month_excl)
)
INSERT INTO #cal(month_start)
SELECT month_start FROM cal
OPTION (MAXRECURSION 32767);
/* 2) SKU по group_path */
IF OBJECT_ID('tempdb..#skus') IS NOT NULL DROP TABLE #skus;
CREATE TABLE #skus (
sku_1c_id BINARY(16) NOT NULL PRIMARY KEY,
code NVARCHAR(36) NULL,
minAvail DECIMAL(18,6) NULL
);
INSERT INTO #skus(sku_1c_id, code, minAvail)
SELECT n.[1c_id], n.[code], n.[minAvailableQty]
FROM [pbi].[nomenclature] n
JOIN [pbi].[groups] g ON g.[1c_id] = n.[1c_group]
WHERE g.[path] LIKE @group_path + N'%';
IF NOT EXISTS (SELECT 1 FROM #skus)
BEGIN
RAISERROR(N'По path=%s не найдено SKU.', 16, 1, @group_path);
RETURN;
END
/* 3) Прогноз по сценарию */
IF OBJECT_ID('tempdb..#fcast') IS NOT NULL DROP TABLE #fcast;
CREATE TABLE #fcast (
sku_1c_id BINARY(16) NOT NULL,
[month] DATE NOT NULL,
qty DECIMAL(18,3) NOT NULL,
PRIMARY KEY (sku_1c_id, [month])
);
INSERT INTO #fcast(sku_1c_id, [month], qty)
SELECT f.[1c_id], f.[month], f.[value]
FROM [analytics].[forecast] f
JOIN #skus s ON s.sku_1c_id = f.[1c_id]
WHERE f.[scenario_id] = @scenario_id
AND f.[month] >= @from_month
AND f.[month] < @to_month_excl;
/* 4) Приходы из заказов (по месяцам) */
IF OBJECT_ID('tempdb..#inb_status') IS NOT NULL DROP TABLE #inb_status;
CREATE TABLE #inb_status (
sku_1c_id BINARY(16) NOT NULL,
[month] DATE NOT NULL,
[status] NVARCHAR(100) NOT NULL,
units DECIMAL(18,3) NOT NULL,
PRIMARY KEY (sku_1c_id, [month], [status])
);
INSERT INTO #inb_status(sku_1c_id, [month], [status], units)
SELECT
s.sku_1c_id,
DATEFROMPARTS(TRY_CONVERT(INT, LEFT(o.[month], 4)),
TRY_CONVERT(INT, SUBSTRING(o.[month], 6, 2)), 1),
o.[status],
SUM(COALESCE(o.[units], 0.0))
FROM [analytics].[get_orders_by_group] o
JOIN #skus s ON s.code = o.[code]
WHERE o.[status] IN (N'В пути', N'В производстве', N'Выгружен на складе'/*, N'Согласован'*/)
AND TRY_CONVERT(INT, LEFT(o.[month], 4)) IS NOT NULL
AND TRY_CONVERT(INT, SUBSTRING(o.[month], 6, 2)) BETWEEN 1 AND 12
GROUP BY s.sku_1c_id,
DATEFROMPARTS(TRY_CONVERT(INT, LEFT(o.[month], 4)),
TRY_CONVERT(INT, SUBSTRING(o.[month], 6, 2)), 1),
o.[status];
IF OBJECT_ID('tempdb..#inb') IS NOT NULL DROP TABLE #inb;
CREATE TABLE #inb (
sku_1c_id BINARY(16) NOT NULL,
[month] DATE NOT NULL,
units DECIMAL(18,3) NOT NULL,
PRIMARY KEY (sku_1c_id, [month])
);
INSERT INTO #inb(sku_1c_id, [month], units)
SELECT sku_1c_id, [month], SUM(units)
FROM #inb_status
WHERE [month] >= @from_month AND [month] < @to_month_excl
GROUP BY sku_1c_id, [month];
/* 5) Стартовые остатки: склад + МП */
IF OBJECT_ID('tempdb..#stock_base') IS NOT NULL DROP TABLE #stock_base;
CREATE TABLE #stock_base (sku_1c_id BINARY(16) NOT NULL PRIMARY KEY, qty DECIMAL(18,3) NOT NULL);
INSERT INTO #stock_base(sku_1c_id, qty)
SELECT s.sku_1c_id, COALESCE(b.qty, 0.0)
FROM #skus s
LEFT JOIN (
SELECT code, SUM(COALESCE(quantity_base,0.0)) AS qty
FROM [analytics].[get_quantity_by_group]
GROUP BY code
) b ON b.code = s.code;
IF OBJECT_ID('tempdb..#stock_mp') IS NOT NULL DROP TABLE #stock_mp;
CREATE TABLE #stock_mp (sku_1c_id BINARY(16) NOT NULL PRIMARY KEY, qty DECIMAL(18,3) NOT NULL);
INSERT INTO #stock_mp(sku_1c_id, qty)
SELECT s.sku_1c_id, COALESCE(m.qty, 0.0)
FROM #skus s
LEFT JOIN (
SELECT code, SUM(COALESCE(quantity_base,0.0)) AS qty
FROM [analytics].[get_mp_quantity_by_group]
GROUP BY code
) m ON m.code = s.code;
IF OBJECT_ID('tempdb..#stock0') IS NOT NULL DROP TABLE #stock0;
CREATE TABLE #stock0 (sku_1c_id BINARY(16) NOT NULL PRIMARY KEY, qty DECIMAL(18,3) NOT NULL);
INSERT INTO #stock0(sku_1c_id, qty)
SELECT s.sku_1c_id, COALESCE(b.qty,0) + COALESCE(mp.qty,0)
FROM #skus s
LEFT JOIN #stock_base b ON b.sku_1c_id = s.sku_1c_id
LEFT JOIN #stock_mp mp ON mp.sku_1c_id = s.sku_1c_id;
/* 6) Лента (сырой спрос/приход) */
IF OBJECT_ID('tempdb..#tl') IS NOT NULL DROP TABLE #tl;
CREATE TABLE #tl (
sku_1c_id BINARY(16) NOT NULL,
[month] DATE NOT NULL,
demand_m DECIMAL(18,3) NOT NULL,
inb_m DECIMAL(18,3) NOT NULL,
cum_d DECIMAL(38,3) NULL, -- кумулятив обслуженного спроса
cum_i DECIMAL(38,3) NULL, -- кумулятив прихода
net_stock DECIMAL(38,3) NULL, -- итоговый остаток месяца (>=0)
served_m DECIMAL(18,3) NULL, -- обслуженный спрос в месяце
lost_m DECIMAL(18,3) NULL, -- потерянный спрос в месяце
PRIMARY KEY (sku_1c_id, [month])
);
INSERT INTO #tl(sku_1c_id, [month], demand_m, inb_m)
SELECT s.sku_1c_id, c.month_start,
COALESCE(f.qty, 0.0),
COALESCE(i.units, 0.0)
FROM #skus s
CROSS JOIN #cal c
LEFT JOIN #fcast f ON f.sku_1c_id = s.sku_1c_id AND f.[month] = c.month_start
LEFT JOIN #inb i ON i.sku_1c_id = s.sku_1c_id AND i.[month] = c.month_start;
/* 6.1) Пронумеруем месяцы (для рекурсивного расчёта без бэк-ордеров) */
IF OBJECT_ID('tempdb..#seq') IS NOT NULL DROP TABLE #seq;
CREATE TABLE #seq (
sku_1c_id BINARY(16) NOT NULL,
[month] DATE NOT NULL,
demand_m DECIMAL(18,3) NOT NULL,
inb_m DECIMAL(18,3) NOT NULL,
rn INT NOT NULL,
PRIMARY KEY (sku_1c_id, rn)
);
INSERT INTO #seq(sku_1c_id, [month], demand_m, inb_m, rn)
SELECT t.sku_1c_id, t.[month], t.demand_m, t.inb_m,
ROW_NUMBER() OVER (PARTITION BY t.sku_1c_id ORDER BY t.[month])
FROM #tl t;
/* 6.2) Рекурсивный расчёт: served/lost/net_stock (без накопления спроса ниже нуля) */
IF OBJECT_ID('tempdb..#flow') IS NOT NULL DROP TABLE #flow;
CREATE TABLE #flow(
sku_1c_id BINARY(16) NOT NULL,
rn INT NOT NULL,
[month] DATE NOT NULL,
demand_m DECIMAL(18,3) NOT NULL,
inb_m DECIMAL(18,3) NOT NULL,
served_m DECIMAL(18,3) NOT NULL,
lost_m DECIMAL(18,3) NOT NULL,
net_stock DECIMAL(38,3) NOT NULL,
PRIMARY KEY (sku_1c_id, rn)
);
;WITH r AS (
/* первый месяц */
SELECT
s.sku_1c_id, s.rn, s.[month], s.demand_m, s.inb_m,
CAST(
CASE WHEN st.qty + s.inb_m >= s.demand_m
THEN s.demand_m
ELSE st.qty + s.inb_m
END
AS DECIMAL(18,3)) AS served_m,
CAST(
CASE WHEN st.qty + s.inb_m >= s.demand_m
THEN 0.0
ELSE s.demand_m - (st.qty + s.inb_m)
END
AS DECIMAL(18,3)) AS lost_m,
CAST(
CASE WHEN st.qty + s.inb_m - s.demand_m < 0
THEN 0
ELSE st.qty + s.inb_m - s.demand_m
END
AS DECIMAL(38,3)) AS net_stock
FROM #seq s
JOIN #stock0 st ON st.sku_1c_id = s.sku_1c_id
WHERE s.rn = 1
UNION ALL
/* последующие месяцы */
SELECT
s.sku_1c_id, s.rn, s.[month], s.demand_m, s.inb_m,
CAST(
CASE WHEN r.net_stock + s.inb_m >= s.demand_m
THEN s.demand_m
ELSE r.net_stock + s.inb_m
END
AS DECIMAL(18,3)) AS served_m,
CAST(
CASE WHEN r.net_stock + s.inb_m >= s.demand_m
THEN 0.0
ELSE s.demand_m - (r.net_stock + s.inb_m)
END
AS DECIMAL(18,3)) AS lost_m,
CAST(
CASE WHEN r.net_stock + s.inb_m - s.demand_m < 0
THEN 0
ELSE r.net_stock + s.inb_m - s.demand_m
END
AS DECIMAL(38,3)) AS net_stock
FROM r
JOIN #seq s
ON s.sku_1c_id = r.sku_1c_id
AND s.rn = r.rn + 1
)
INSERT INTO #flow
SELECT * FROM r
OPTION (MAXRECURSION 32767);
/* 6.3) Обновим #tl: кумулятивы по обслуженному спросу/приходу и итоговый остаток */
;WITH cd AS (
SELECT f.sku_1c_id, s.[month],
SUM(f.served_m) OVER (PARTITION BY f.sku_1c_id ORDER BY f.rn ROWS UNBOUNDED PRECEDING) AS cum_d_served,
SUM(s.inb_m) OVER (PARTITION BY s.sku_1c_id ORDER BY s.rn ROWS UNBOUNDED PRECEDING) AS cum_i_raw,
f.net_stock, f.served_m, f.lost_m
FROM #flow f
JOIN #seq s ON s.sku_1c_id = f.sku_1c_id AND s.rn = f.rn
)
UPDATE t
SET t.cum_d = cd.cum_d_served,
t.cum_i = cd.cum_i_raw,
t.net_stock = cd.net_stock,
t.served_m = cd.served_m,
t.lost_m = cd.lost_m
FROM #tl t
JOIN cd ON cd.sku_1c_id = t.sku_1c_id AND cd.[month] = t.[month];
/* 7) Первый месяц дефицита T (учитываем lead_time) */
DECLARE @start_T DATE =
DATEFROMPARTS(YEAR(DATEADD(MONTH, @lead_time_m, @from_month)),
MONTH(DATEADD(MONTH, @lead_time_m, @from_month)), 1);
IF OBJECT_ID('tempdb..#first_def') IS NOT NULL DROP TABLE #first_def;
CREATE TABLE #first_def (sku_1c_id BINARY(16) NOT NULL PRIMARY KEY, T DATE NULL);
INSERT INTO #first_def(sku_1c_id, T)
SELECT d.sku_1c_id, MIN(d.[month]) AS T
FROM (
SELECT t.sku_1c_id, t.[month]
FROM #tl t
JOIN #skus s ON s.sku_1c_id = t.sku_1c_id
WHERE t.net_stock < COALESCE(s.minAvail, 0.0)
AND t.[month] >= @start_T
) d
GROUP BY d.sku_1c_id;
/* 8) Якоря: T, T+C, T+2C ... */
IF OBJECT_ID('tempdb..#anchors') IS NOT NULL DROP TABLE #anchors;
CREATE TABLE #anchors (
sku_1c_id BINARY(16) NOT NULL,
T DATE NOT NULL,
PRIMARY KEY (sku_1c_id, T)
);
;WITH recur AS (
SELECT fd.sku_1c_id, fd.T
FROM #first_def fd
WHERE fd.T IS NOT NULL
UNION ALL
SELECT r.sku_1c_id, DATEADD(MONTH, @cover_months, r.T)
FROM recur r
WHERE DATEADD(MONTH, @cover_months, r.T) < @to_month_excl
)
INSERT INTO #anchors(sku_1c_id, T)
SELECT sku_1c_id, T
FROM recur
OPTION (MAXRECURSION 32767);
/* 9) Подготовка строк к вставке (размер заказа = прогноз окна) */
IF OBJECT_ID('tempdb..#rows_to_insert') IS NOT NULL DROP TABLE #rows_to_insert;
CREATE TABLE #rows_to_insert (
[scenario_id] INT,
[group_name] NVARCHAR(255),
[1c_id] BINARY(16),
[code] NVARCHAR(36),
[place_month] DATE,
[arrival_month] DATE,
[demand_window_C] DECIMAL(18,3),
[projected_stock_at_T] DECIMAL(18,3),
[order_qty] DECIMAL(18,3)
);
;WITH base AS (
SELECT
s.sku_1c_id,
s.code,
a.T,
st.qty AS stock0,
/* прогноз окна [T .. T+C) */
(SELECT SUM(demand_m)
FROM #tl z
WHERE z.sku_1c_id = s.sku_1c_id
AND z.[month] >= a.T
AND z.[month] < DATEADD(MONTH, @cover_months, a.T)
) AS demand_window,
/* кумулятивы до T-1 (обслуженный спрос и приход) */
(SELECT TOP (1) z.cum_i
FROM #tl z
WHERE z.sku_1c_id = s.sku_1c_id
AND z.[month] < a.T
ORDER BY z.[month] DESC
) AS cum_i_before,
(SELECT TOP (1) z.cum_d
FROM #tl z
WHERE z.sku_1c_id = s.sku_1c_id
AND z.[month] < a.T
ORDER BY z.[month] DESC
) AS cum_d_before,
/* приход именно в T */
(SELECT SUM(inb_m)
FROM #tl z
WHERE z.sku_1c_id = s.sku_1c_id
AND z.[month] = a.T
) AS inb_t
FROM #anchors a
JOIN #skus s ON s.sku_1c_id = a.sku_1c_id
JOIN #stock0 st ON st.sku_1c_id = s.sku_1c_id
)
INSERT INTO #rows_to_insert
SELECT
@scenario_id,
@group_path,
b.sku_1c_id,
b.code,
DATEADD(MONTH, -@lead_time_m, b.T) AS place_month,
b.T AS arrival_month,
CAST(ISNULL(b.demand_window,0) AS DECIMAL(18,3)) AS demand_window_C,
/* старт на T (после прихода T), снизу 0 */
CAST(
CASE
WHEN ( ISNULL(b.stock0,0)
+ ISNULL(b.cum_i_before,0)
+ ISNULL(b.inb_t,0)
- ISNULL(b.cum_d_before,0) ) < 0
THEN 0
ELSE ( ISNULL(b.stock0,0)
+ ISNULL(b.cum_i_before,0)
+ ISNULL(b.inb_t,0)
- ISNULL(b.cum_d_before,0) )
END
AS DECIMAL(18,3)) AS projected_stock_at_T,
/* размер заказа = прогноз окна */
CAST(ISNULL(b.demand_window,0) AS DECIMAL(18,3)) AS order_qty
FROM base AS b;
/* 10) Вставка рекомендаций (незначащие — отбрасываем) */
INSERT INTO [analytics].[deficit_proposal]
([scenario_id], [group_name], [1c_id], [code],
[place_month], [arrival_month],
[demand_window_C], [projected_stock_at_T], [order_qty],
[updated_at])
SELECT
scenario_id, group_name, [1c_id], [code],
[place_month], [arrival_month],
[demand_window_C], [projected_stock_at_T], [order_qty],
GETDATE()
FROM #rows_to_insert
WHERE [order_qty] > 0;
/* 11) Отладка по желанию */
IF @debug = 1
BEGIN
SELECT TOP (200)
r.[code], r.[arrival_month], r.[place_month],
r.[projected_stock_at_T], r.[demand_window_C], r.[order_qty]
FROM #rows_to_insert r
ORDER BY r.[arrival_month], r.[code];
END
END
GO
/****** Object: StoredProcedure [analytics].[sp_build_forecast_s4_by_group] Script Date: 2026-02-22 12:04:15 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [analytics].[sp_build_forecast_s4_by_group]
@path NVARCHAR(255), -- путь группы (например, N'Игрушки')
@from_month DATE = NULL, -- по умолчанию: 1-е число текущего месяца
@to_month_excl DATE = '2028-01-01', -- полуинтервал [from, to)
@scenario_id INT = 4,
@allow_fallback_all BIT = 0 -- 1 = если группа не найдена/пустая → считать весь каталог с дефолтной сезонностью
AS
BEGIN
SET NOCOUNT ON;
----------------------------------------------------------------
-- Нормализация пути: убираем лишние кавычки и пробелы
----------------------------------------------------------------
SET @path = LTRIM(RTRIM(REPLACE(@path, '''', N'')));
IF @from_month IS NULL
SET @from_month = DATEFROMPARTS(YEAR(GETDATE()), MONTH(GETDATE()), 1);
----------------------------------------------------------------
-- Идентификаторы групп для сезонности
----------------------------------------------------------------
DECLARE @season_group_id BINARY(16) = NULL;
DECLARE @DEFAULT_GROUP_ID BINARY(16) = 0x00000000000000000000000000000000;
-- Пытаемся найти группу ровно по path
SELECT @season_group_id = g.[1c_id]
FROM [pbi].[groups] g
WHERE g.[path] = @path;
-- Если нет точного совпадения, ищем верхний узел, начинающийся с @path
IF @season_group_id IS NULL
BEGIN
SELECT TOP (1) @season_group_id = g.[1c_id]
FROM [pbi].[groups] g
WHERE g.[path] LIKE @path + N'%'
ORDER BY g.[lvl] ASC;
END
-- Если группу так и не нашли: по умолчанию НЕ уходим на весь каталог (защита от опечаток)
IF @season_group_id IS NULL AND @allow_fallback_all = 0
BEGIN
RAISERROR (N'Группа с path = %s не найдена. Проверьте параметр @path или запустите с @allow_fallback_all=1 для расчёта по всему каталогу.', 16, 1, @path);
RETURN;
END
-- Если всё-таки нужно фолбэкнуть на дефолтную сезонность
IF @season_group_id IS NULL AND @allow_fallback_all = 1
SET @season_group_id = @DEFAULT_GROUP_ID;
----------------------------------------------------------------
-- TEMP: календарь месяцев [from, to)
----------------------------------------------------------------
IF OBJECT_ID('tempdb..#cal') IS NOT NULL DROP TABLE #cal;
CREATE TABLE #cal (month_start DATE NOT NULL PRIMARY KEY);
;WITH cal AS (
SELECT @from_month AS month_start
UNION ALL
SELECT DATEADD(MONTH, 1, month_start)
FROM cal
WHERE month_start < DATEADD(MONTH, -1, @to_month_excl)
)
INSERT INTO #cal(month_start)
SELECT month_start FROM cal
OPTION (MAXRECURSION 32767);
----------------------------------------------------------------
-- TEMP: SKU под деревом @path; если пусто и разрешён фолбэк → все SKU
----------------------------------------------------------------
IF OBJECT_ID('tempdb..#skus') IS NOT NULL DROP TABLE #skus;
CREATE TABLE #skus (
sku_1c_id BINARY(16) NOT NULL PRIMARY KEY,
code NVARCHAR(36) NULL
);
INSERT INTO #skus(sku_1c_id, code)
SELECT n.[1c_id], n.[code]
FROM [pbi].[nomenclature] n
JOIN [pbi].[groups] g ON g.[1c_id] = n.[1c_group]
WHERE g.[path] LIKE @path + N'%'
OPTION (RECOMPILE);
IF NOT EXISTS (SELECT 1 FROM #skus)
BEGIN
IF @allow_fallback_all = 1
BEGIN
INSERT INTO #skus(sku_1c_id, code)
SELECT n.[1c_id], n.[code]
FROM [pbi].[nomenclature] n;
SET @season_group_id = @DEFAULT_GROUP_ID;
END
ELSE
BEGIN
RAISERROR (N'По path = %s не найдено ни одного SKU. Расчёт не выполнялся.', 16, 1, @path);
RETURN;
END
END
----------------------------------------------------------------
-- TEMP: ставки продаж (шт/день)
----------------------------------------------------------------
IF OBJECT_ID('tempdb..#rate') IS NOT NULL DROP TABLE #rate;
CREATE TABLE #rate (
sku_1c_id BINARY(16) NOT NULL PRIMARY KEY,
rate_per_day DECIMAL(38,6) NOT NULL
);
INSERT INTO #rate(sku_1c_id, rate_per_day)
SELECT a.[1c_id], ISNULL(a.[Продажи шт / день],0)
FROM [analytics].[аналитика за 365 дн.] a
JOIN #skus s ON s.sku_1c_id = a.[1c_id];
----------------------------------------------------------------
-- TEMP: сезонность ТОЛЬКО по @season_group_id; если нет строк — используем дефолт
----------------------------------------------------------------
IF OBJECT_ID('tempdb..#season') IS NOT NULL DROP TABLE #season;
CREATE TABLE #season (
[month] TINYINT NOT NULL PRIMARY KEY, -- 1..12
seasonal_koef DECIMAL(18,6) NOT NULL
);
INSERT INTO #season([month], seasonal_koef)
/*SELECT sg.[month], sg.[seasonal_koef]
FROM [analytics].[seasonality_groups] sg
WHERE sg.[group_1c_id] = @season_group_id;*/
SELECT
[month]
, AVG([koef])
FROM [mag_pbi].[analytics].[seasonality_groups_summ_1]
GROUP BY [month];
/*IF @@ROWCOUNT = 0
BEGIN
INSERT INTO #season([month], seasonal_koef)
SELECT sg.[month], sg.[seasonal_koef]
FROM [analytics].[seasonality_groups] sg
WHERE sg.[group_1c_id] = @DEFAULT_GROUP_ID;
-- если и дефолта нет, #season останется пустой → в формуле будет 1.0
END
*/
----------------------------------------------------------------
-- Удаляем старые строки сценария (батчами, чтобы не держать тяжёлые блокировки)
----------------------------------------------------------------
WHILE 1 = 1
BEGIN
DELETE TOP (50000) f
FROM [analytics].[forecast] f
JOIN #skus s ON s.sku_1c_id = f.[1c_id]
WHERE f.[scenario_id] = @scenario_id
AND f.[month] >= @from_month
AND f.[month] < @to_month_excl;
IF @@ROWCOUNT = 0 BREAK;
END
----------------------------------------------------------------
-- Вставка прогноза: rate_per_day × дни_в_месяце × seasonal_koef(@path/@default)
----------------------------------------------------------------
INSERT INTO [analytics].[forecast]
([scenario_id], [1c_id], [code], [month], [value], [updated_at], [updated_by])
SELECT
@scenario_id,
s.sku_1c_id,
s.code,
c.month_start,
CAST(ISNULL(r.rate_per_day, 0)
* DAY(EOMONTH(c.month_start))
* ISNULL(se.seasonal_koef, 1.0) AS DECIMAL(18,3)) AS value,
GETDATE(),
SUSER_SNAME()
FROM #skus s
CROSS JOIN #cal c
LEFT JOIN #rate r ON r.sku_1c_id = s.sku_1c_id
LEFT JOIN #season se ON se.[month] = MONTH(c.month_start)
OPTION (RECOMPILE);
END
GO
/****** Object: StoredProcedure [analytics].[sp_create_analytics_365] Script Date: 2026-02-22 12:04:15 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [analytics].[sp_create_analytics_365]
AS
BEGIN
/*
... твой закомментированный блок без изменений ...
*/
-------------------------------------------------------------
-- 1) Базовая таблица аналитики за 365 дней
-------------------------------------------------------------
DROP TABLE [mag_pbi].[analytics].[аналитика за 365 дн.]
SELECT
s.[1c_id],
s.Code,
SUM(s.Количество) as [Продано шт]
INTO [mag_pbi].[analytics].[аналитика за 365 дн.]
FROM [pbiProd].[СводныйСебестоимость Для PBI] s
WHERE s.Статья = N'Реализация'
AND s.[Период] >= DATEADD(day, -365, GETDATE())
GROUP BY [1c_id], s.Code;
-------------------------------------------------------------
-- 2) Продажи шт / день
-------------------------------------------------------------
IF COL_LENGTH('analytics.аналитика за 365 дн.', 'Продажи шт / день') IS NULL
BEGIN
ALTER TABLE [analytics].[аналитика за 365 дн.]
ADD [Продажи шт / день] numeric(38,6) NULL;
/*ALTER TABLE [analytics].[аналитика за 365 дн.]
ADD [Продажи шт / день опт] numeric(38,6) NULL;*/
END;
;WITH bounds AS (
SELECT
CAST(DATEADD(day, -365, CAST(GETDATE() AS date)) AS date) AS start_date,
CAST(GETDATE() AS date) AS end_date
),
days365 AS (
SELECT
t.[_IDRREF] AS [1c_id],
SUM(t.[ostatok]) AS days_on_sale_365
--SUM(t.[ostatok10]) AS days_on_sale_365_opt
FROM [pbi].[w_ostatok_da_net] t
CROSS JOIN bounds b
WHERE t.[dt] >= b.start_date
AND t.[dt] < b.end_date -- исключаем текущий день
GROUP BY t.[_IDRREF]
)
UPDATE a
SET
[Продажи шт / день] =
CASE
WHEN d.days_on_sale_365 > 0
THEN CAST(a.[Продано шт] AS numeric(38,6)) / CAST(d.days_on_sale_365 AS numeric(38,6))
ELSE NULL
END
/*[Продажи шт / день опт] =
CASE
WHEN d.days_on_sale_365_opt > 0
THEN CAST(a.[Продано шт] AS numeric(38,6)) / CAST(d.days_on_sale_365_opt AS numeric(38,6))
ELSE NULL
END*/
FROM [analytics].[аналитика за 365 дн.] a
LEFT JOIN days365 d
ON d.[1c_id] = a.[1c_id];
-------------------------------------------------------------
-- 3) Остаток дней продаж
-------------------------------------------------------------
IF COL_LENGTH('analytics.аналитика за 365 дн.', 'Остаток дней продаж') IS NULL
BEGIN
ALTER TABLE [analytics].[аналитика за 365 дн.]
ADD [Остаток дней продаж] numeric(38,6) NULL;
/*ALTER TABLE [analytics].[аналитика за 365 дн.]
ADD [Остаток дней продаж опт] numeric(38,6) NULL;*/
END;
UPDATE a
SET
[Остаток дней продаж] =
CASE
WHEN a.[Продажи шт / день] > 0
THEN sb.[Остаток склад + МП, шт] / a.[Продажи шт / день]
ELSE NULL
END
/*[Остаток дней продаж опт] =
CASE
WHEN a.[Продажи шт / день опт] > 0
THEN sb.[Остаток склад + МП, шт] / a.[Продажи шт / день опт]
ELSE NULL
END*/
FROM [analytics].[аналитика за 365 дн.] a
LEFT JOIN (
SELECT
n.[1c_id],
s.[Остаток склад + МП, шт]
FROM [mag_pbi].[analytics].[stock_balance] s
INNER JOIN [mag_pbi].[pbi].[nomenclature] n
ON n.artic_id = s.artic_id
) sb
ON sb.[1c_id] = a.[1c_id];
-------------------------------------------------------------
-- 4) Сумма продаж, учетка, ТН (год)
-------------------------------------------------------------
IF COL_LENGTH('analytics.аналитика за 365 дн.', 'Продажи / год, руб.') IS NULL
BEGIN
ALTER TABLE [mag_pbi].[analytics].[аналитика за 365 дн.]
ADD [Продажи / год, руб.] decimal(38,2) NULL;
END;
IF COL_LENGTH('analytics.аналитика за 365 дн.', 'учетная сумма / год, руб.') IS NULL
BEGIN
ALTER TABLE [mag_pbi].[analytics].[аналитика за 365 дн.]
ADD [учетная сумма / год, руб.] decimal(38,2) NULL;
END;
IF COL_LENGTH('analytics.аналитика за 365 дн.', 'ТН / год, руб.') IS NULL
BEGIN
ALTER TABLE [mag_pbi].[analytics].[аналитика за 365 дн.]
ADD [ТН / год, руб.] decimal(38,2) NULL;
END;
IF COL_LENGTH('analytics.аналитика за 365 дн.', 'Стоимость МП год, руб.') IS NULL
BEGIN
ALTER TABLE [mag_pbi].[analytics].[аналитика за 365 дн.]
ADD [Стоимость МП год, руб.] decimal(38,2) NULL;
END;
IF COL_LENGTH('analytics.аналитика за 365 дн.', '%ТН год, руб.') IS NULL
BEGIN
ALTER TABLE [mag_pbi].[analytics].[аналитика за 365 дн.]
ADD [%ТН год, руб.] decimal(38,2) NULL;
END;
DECLARE @end date = CAST(GETDATE() AS date); -- сегодня (не включаем)
DECLARE @start date = DATEADD(day, -365, @end); -- 365 дней назад
;WITH Y AS (
SELECT
v.[1c_id],
SUM(v.[Сумма продаж]) AS sales_year,
SUM(v.[Учетная сумма]) AS acct_year,
SUM(v.[Стоимость обработки]) AS cost_mp,
SUM(v.[Торговая надбавка]) AS tn_year
FROM [analytics].[Продажи_Учёт_Маржа_по_дням] v
WHERE v.[d] >= @start AND v.[d] < @end
GROUP BY v.[1c_id]
)
UPDATE a
SET
a.[Продажи / год, руб.] = COALESCE(Y.sales_year, 0),
a.[учетная сумма / год, руб.] = COALESCE(Y.acct_year, 0),
a.[ТН / год, руб.] = COALESCE(Y.tn_year, 0),
a.[Стоимость МП год, руб.] = COALESCE(Y.cost_mp, 0),
a.[%ТН год, руб.] = CASE
WHEN NULLIF(COALESCE(Y.acct_year, 0), 0) IS NULL THEN NULL
ELSE
CAST(COALESCE(Y.tn_year, 0) AS decimal(19,6))
/ CAST(NULLIF(Y.acct_year, 0) AS decimal(19,6))
END
FROM [mag_pbi].[analytics].[аналитика за 365 дн.] AS a
LEFT JOIN Y
ON Y.[1c_id] = a.[1c_id];
-------------------------------------------------------------
-- 5) Дней в продаже / год и / квартал
-------------------------------------------------------------
SET @end = CAST(GETDATE() AS date);
DECLARE @startY date = DATEADD(day, -365, @end);
DECLARE @startQ date = DATEADD(month, -3, @end);
IF COL_LENGTH('mag_pbi.analytics.аналитика за 365 дн.', 'Дней в продаже / год') IS NULL
BEGIN
ALTER TABLE [mag_pbi].[analytics].[аналитика за 365 дн.]
ADD [Дней в продаже / год] bigint NULL;
END;
IF COL_LENGTH('mag_pbi.analytics.аналитика за 365 дн.', 'Дней в продаже / квартал') IS NULL
BEGIN
ALTER TABLE [mag_pbi].[analytics].[аналитика за 365 дн.]
ADD [Дней в продаже / квартал] bigint NULL;
END;
;WITH daysY AS (
SELECT [_IDRREF] AS [1c_id],
COUNT_BIG(*) AS days_in_sale_year
FROM [pbi].[w_ostatok_da_net]
WHERE [dt] >= @startY AND [dt] < @end
AND [ostatok] >= 1
GROUP BY [_IDRREF]
),
daysQ AS (
SELECT [_IDRREF] AS [1c_id],
COUNT_BIG(*) AS days_in_sale_quarter
FROM [pbi].[w_ostatok_da_net]
WHERE [dt] >= @startQ AND [dt] < @end
AND [ostatok] >= 1
GROUP BY [_IDRREF]
),
days AS (
SELECT COALESCE(y.[1c_id], q.[1c_id]) AS [1c_id],
y.days_in_sale_year,
q.days_in_sale_quarter
FROM daysY y
FULL OUTER JOIN daysQ q ON q.[1c_id] = y.[1c_id]
)
UPDATE a
SET
a.[Дней в продаже / год] = d.days_in_sale_year,
a.[Дней в продаже / квартал] = d.days_in_sale_quarter
FROM [mag_pbi].[analytics].[аналитика за 365 дн.] a
LEFT JOIN days d
ON d.[1c_id] = a.[1c_id];
-------------------------------------------------------------
-- 6) Сумма продаж, учетка, ТН (квартал)
-------------------------------------------------------------
IF COL_LENGTH('analytics.аналитика за 365 дн.', 'Продажи / квартал, руб.') IS NULL
BEGIN
ALTER TABLE [analytics].[аналитика за 365 дн.]
ADD [Продажи / квартал, руб.] decimal(38,2) NULL;
END;
IF COL_LENGTH('analytics.аналитика за 365 дн.', 'учетная сумма / квартал, руб.') IS NULL
BEGIN
ALTER TABLE [analytics].[аналитика за 365 дн.]
ADD [учетная сумма / квартал, руб.] decimal(38,2) NULL;
END;
IF COL_LENGTH('analytics.аналитика за 365 дн.', 'ТН / квартал, руб.') IS NULL
BEGIN
ALTER TABLE [analytics].[аналитика за 365 дн.]
ADD [ТН / квартал, руб.] decimal(38,2) NULL;
END;
SET @start = DATEADD(month, -3, @end);
;WITH Q AS (
SELECT
v.[1c_id],
SUM(v.[Сумма продаж]) AS sales_q,
SUM(v.[Учетная сумма]) AS acct_q,
SUM(v.[Торговая надбавка]) AS tn_q
FROM [analytics].[Продажи_Учёт_Маржа_по_дням] v
WHERE v.[d] >= @start AND v.[d] < @end
GROUP BY v.[1c_id]
)
UPDATE a
SET
a.[Продажи / квартал, руб.] = Q.sales_q,
a.[учетная сумма / квартал, руб.] = Q.acct_q,
a.[ТН / квартал, руб.] = Q.tn_q
FROM [analytics].[аналитика за 365 дн.] AS a
LEFT JOIN Q
ON Q.[1c_id] = a.[1c_id];
-------------------------------------------------------------
-- 7) ТН / месяц (по текущей скорости)
-------------------------------------------------------------
IF COL_LENGTH('mag_pbi.analytics.аналитика за 365 дн.', 'ТН / месяц, руб.') IS NULL
BEGIN
ALTER TABLE [mag_pbi].[analytics].[аналитика за 365 дн.]
ADD [ТН / месяц, руб.] decimal(38,6) NULL;
END;
UPDATE a
SET a.[ТН / месяц, руб.] =
ISNULL(a.[ТН / год, руб.], 0)
/ NULLIF(a.[Продано шт], 0)
* ISNULL(a.[Продажи шт / день], 0)
* 30
FROM [mag_pbi].[analytics].[аналитика за 365 дн.] a;
-------------------------------------------------------------------------------------
-- 8) ДОПОЛНИТЕЛЬНО: Оплаченный остаток и рентабельности (ROIC и по остатку в руб.)
-- (год назад, квартал назад и на будущий год) по 12-месячному окну
-------------------------------------------------------------------------------------
-- новые колонки
IF COL_LENGTH('mag_pbi.analytics.аналитика за 365 дн.', 'Оплаченный остаток') IS NULL
BEGIN
ALTER TABLE [mag_pbi].[analytics].[аналитика за 365 дн.]
ADD [Оплаченный остаток] decimal(38,2) NULL;
END;
IF COL_LENGTH('mag_pbi.analytics.аналитика за 365 дн.', 'Рентабельность / год') IS NULL
BEGIN
ALTER TABLE [mag_pbi].[analytics].[аналитика за 365 дн.]
ADD [Рентабельность / год] decimal(38,6) NULL;
END;
IF COL_LENGTH('mag_pbi.analytics.аналитика за 365 дн.', 'Рентабельность / квартал') IS NULL
BEGIN
ALTER TABLE [mag_pbi].[analytics].[аналитика за 365 дн.]
ADD [Рентабельность / квартал] decimal(38,6) NULL;
END;
IF COL_LENGTH('mag_pbi.analytics.аналитика за 365 дн.', 'Рентабельность / будущий год') IS NULL
BEGIN
ALTER TABLE [mag_pbi].[analytics].[аналитика за 365 дн.]
ADD [Рентабельность / будущий год] decimal(38,6) NULL;
END;
-- новые поля рентабельности по остатку в рублях (сразу в %)
IF COL_LENGTH('mag_pbi.analytics.аналитика за 365 дн.', 'Рентабельность по остатку / год') IS NULL
BEGIN
ALTER TABLE [mag_pbi].[analytics].[аналитика за 365 дн.]
ADD [Рентабельность по остатку / год] decimal(38,6) NULL;
END;
IF COL_LENGTH('mag_pbi.analytics.аналитика за 365 дн.', 'Рентабельность по остатку / квартал') IS NULL
BEGIN
ALTER TABLE [mag_pbi].[analytics].[аналитика за 365 дн.]
ADD [Рентабельность по остатку / квартал] decimal(38,6) NULL;
END;
IF COL_LENGTH('mag_pbi.analytics.аналитика за 365 дн.', 'Рентабельность по остатку / будущий год') IS NULL
BEGIN
ALTER TABLE [mag_pbi].[analytics].[аналитика за 365 дн.]
ADD [Рентабельность по остатку / будущий год] decimal(38,6) NULL;
END;
---------------------------------------------------------
-- 8.1 Окно 12 полных месяцев
---------------------------------------------------------
DECLARE @mp_today date = CAST(GETDATE() AS date);
DECLARE @mp_lastFullMonth date = EOMONTH(DATEADD(month, -1, @mp_today));
DECLARE @mp_firstMonthStart date = DATEADD(
month, -11,
DATEFROMPARTS(YEAR(@mp_lastFullMonth), MONTH(@mp_lastFullMonth), 1)
);
DECLARE @mp_curMonthStart date = @mp_firstMonthStart;
IF OBJECT_ID('tempdb..#mp_months') IS NOT NULL DROP TABLE #mp_months;
CREATE TABLE #mp_months (
MonthStart date NOT NULL PRIMARY KEY
);
WHILE @mp_curMonthStart <= DATEFROMPARTS(YEAR(@mp_lastFullMonth), MONTH(@mp_lastFullMonth), 1)
BEGIN
INSERT INTO #mp_months (MonthStart) VALUES (@mp_curMonthStart);
SET @mp_curMonthStart = DATEADD(month, 1, @mp_curMonthStart);
END;
---------------------------------------------------------
-- 8.2 Набор SKU из аналитики 365
---------------------------------------------------------
IF OBJECT_ID('tempdb..#mp_s') IS NOT NULL DROP TABLE #mp_s;
CREATE TABLE #mp_s(
[1c_id] binary(16) NOT NULL,
code nchar(11) NOT NULL,
CONSTRAINT PK_mp_s PRIMARY KEY ([1c_id], code)
);
INSERT INTO #mp_s([1c_id], code)
SELECT DISTINCT a.[1c_id], a.[Code]
FROM [mag_pbi].[analytics].[аналитика за 365 дн.] a;
---------------------------------------------------------
-- 8.3 Внешние остатки
---------------------------------------------------------
IF OBJECT_ID('tempdb..#mp_ext_stock') IS NOT NULL DROP TABLE #mp_ext_stock;
CREATE TABLE #mp_ext_stock(
Dt date NOT NULL,
code nchar(11) NOT NULL,
qty numeric(18,3) NOT NULL,
CONSTRAINT PK_mp_ext_stock PRIMARY KEY (Dt, code)
);
INSERT INTO #mp_ext_stock (Dt, code, qty)
SELECT
CAST(o.[Дата обновления] AS date) AS Dt,
o.code,
SUM(o.[Количество]) AS qty
FROM analytics.[Внешние остатки] o
JOIN #mp_s s
ON s.code = o.code
GROUP BY CAST(o.[Дата обновления] AS date), o.code;
---------------------------------------------------------
-- 8.4 Приходы (Закупка / Приход)
---------------------------------------------------------
IF OBJECT_ID('tempdb..#mp_incoming') IS NOT NULL DROP TABLE #mp_incoming;
CREATE TABLE #mp_incoming(
MonthStart date NOT NULL,
[1c_id] binary(16) NOT NULL,
qty numeric(18,3) NOT NULL,
CONSTRAINT PK_mp_incoming PRIMARY KEY (MonthStart, [1c_id])
);
INSERT INTO #mp_incoming (MonthStart, [1c_id], qty)
SELECT
DATEFROMPARTS(YEAR(s.[Период]), MONTH(s.[Период]), 1) AS MonthStart,
s.[1c_id],
SUM(s.[Количество]) AS qty
FROM pbiProd.[СводныйСебестоимость Для PBI] s
JOIN #mp_s sf
ON sf.[1c_id] = s.[1c_id]
WHERE s.[Статья] = N'Закупка'
AND s.[Вид операции] = N'Приход'
AND s.[Период] >= @mp_firstMonthStart
GROUP BY
DATEFROMPARTS(YEAR(s.[Период]), MONTH(s.[Период]), 1),
s.[1c_id];
---------------------------------------------------------
-- 8.5 Обязательства по месяцам
---------------------------------------------------------
IF OBJECT_ID('tempdb..#mp_obligations') IS NOT NULL DROP TABLE #mp_obligations;
CREATE TABLE #mp_obligations(
MonthStart date NOT NULL,
[1c_id] binary(16) NOT NULL,
obligation numeric(18,2) NOT NULL,
CONSTRAINT PK_mp_obligations PRIMARY KEY (MonthStart, [1c_id])
);
-- 8.5 Обязательства: считаем по manufacturer_payment_stage (поддержка 2, 3 и более этапов)
-- Этап «оплачен», если конец месяца M >= дата прихода + days. Сумма этапа = full_sum * percent/100.
-- Два шага через CTE, чтобы избежать Msg 8124 (correlated subquery с несколькими внешними ссылками).
;WITH base_oblig AS (
SELECT
m.MonthStart,
inc.MonthStart AS inc_month,
inc.[1c_id],
inc.qty * n2.[Цена учетная, руб] AS full_sum,
EOMONTH(inc.MonthStart) AS inc_eom,
EOMONTH(m.MonthStart) AS eval_eom,
man.id AS manufacturer_id
FROM #mp_months m
JOIN #mp_incoming inc ON 1 = 1
JOIN pbi.nomenclature n2 ON n2.[1c_id] = inc.[1c_id]
LEFT JOIN analytics.manufacturers man ON man.[manufacturer] = n2.Производитель
),
oblig_with_paid AS (
SELECT
b.MonthStart,
b.eval_eom,
b.inc_eom,
b.[1c_id],
b.full_sum,
ISNULL(SUM(
CASE WHEN b.eval_eom >= DATEADD(day, st.[days], b.inc_eom)
THEN b.full_sum * st.[percent] / 100.0 ELSE 0 END
), 0) AS paid_to_month
FROM base_oblig b
LEFT JOIN analytics.manufacturer_payment_stage st ON st.manufacturer_id = b.manufacturer_id
GROUP BY b.MonthStart, b.inc_month, b.[1c_id], b.full_sum, b.eval_eom, b.inc_eom, b.manufacturer_id
),
ObligRows AS (
SELECT
MonthStart,
[1c_id],
CASE
WHEN eval_eom < inc_eom THEN 0
WHEN full_sum - paid_to_month > 0 THEN full_sum - paid_to_month
ELSE 0
END AS ObligationPerIncoming
FROM oblig_with_paid
)
INSERT INTO #mp_obligations (MonthStart, [1c_id], obligation)
SELECT
MonthStart,
[1c_id],
SUM(ObligationPerIncoming) AS obligation
FROM ObligRows
GROUP BY
MonthStart,
[1c_id];
---------------------------------------------------------
-- 8.6 ТН по месяцам
---------------------------------------------------------
IF OBJECT_ID('tempdb..#mp_tn_monthly') IS NOT NULL DROP TABLE #mp_tn_monthly;
CREATE TABLE #mp_tn_monthly(
MonthStart date NOT NULL,
[1c_id] binary(16) NOT NULL,
tn_amount numeric(18,2) NULL,
CONSTRAINT PK_mp_tn_monthly PRIMARY KEY (MonthStart, [1c_id])
);
INSERT INTO #mp_tn_monthly (MonthStart, [1c_id], tn_amount)
SELECT
DATEFROMPARTS(YEAR(p.[d]), MONTH(p.[d]), 1) AS MonthStart,
p.[1c_id],
SUM(ISNULL(p.[Торговая надбавка],0)) AS tn_amount
FROM [mag_pbi].[analytics].[Продажи_Учёт_Маржа_по_дням] p
JOIN #mp_s s
ON s.[1c_id] = p.[1c_id]
WHERE p.[d] >= @mp_firstMonthStart
AND p.[d] <= @mp_lastFullMonth
GROUP BY
DATEFROMPARTS(YEAR(p.[d]), MONTH(p.[d]), 1),
p.[1c_id];
---------------------------------------------------------
-- 8.7 Помесячный план по всем SKU (остаток, оплаченный остаток, ТН, рубли-дни, остаток учетка руб)
---------------------------------------------------------
IF OBJECT_ID('tempdb..#mp_plan') IS NOT NULL DROP TABLE #mp_plan;
SELECT
s.[1c_id],
s.code,
m.MonthStart,
[Остаток шт] =
ISNULL(intStock.quantity, 0) + ISNULL(ext.qty, 0),
[Остаток учетка руб] =
(ISNULL(intStock.quantity, 0) + ISNULL(ext.qty, 0))
* ISNULL(n.[Цена учетная, руб], 0),
[Оплаченный остаток] =
CAST(
CASE
WHEN paid_raw < 0 THEN 0
ELSE paid_raw
END
AS decimal(18,2)
),
[ТН] = ISNULL(tn.tn_amount, 0),
[рубли-дни] =
CAST(
CASE
WHEN paid_raw < 0 THEN 0
ELSE paid_raw
END * DAY(EOMONTH(m.MonthStart))
AS decimal(18,2)
)
INTO #mp_plan
FROM #mp_months m
CROSS JOIN #mp_s s
OUTER APPLY (
SELECT TOP (1) w1.quantity
FROM [mag_pbi].[pbi].[w_ostatok_da_net] w1
WHERE w1._IDRREF = s.[1c_id]
AND w1.dt <= EOMONTH(m.MonthStart)
ORDER BY w1.dt DESC
) AS intStock
LEFT JOIN #mp_ext_stock ext
ON ext.code = s.code
AND ext.Dt = EOMONTH(m.MonthStart)
LEFT JOIN #mp_obligations ob
ON ob.[1c_id] = s.[1c_id]
AND ob.MonthStart = m.MonthStart
LEFT JOIN [mag_pbi].[pbi].[nomenclature] n
ON n.[1c_id] = s.[1c_id]
LEFT JOIN #mp_tn_monthly tn
ON tn.[1c_id] = s.[1c_id]
AND tn.MonthStart = m.MonthStart
CROSS APPLY (
SELECT
(ISNULL(intStock.quantity, 0) + ISNULL(ext.qty, 0))
* ISNULL(n.[Цена учетная, руб], 0)
- ISNULL(ob.obligation, 0) AS paid_raw
) p;
---------------------------------------------------------
-- 8.8 Агрегаты по SKU: ROIC (год/квартал/будущий) + рентабельность по остатку (год/квартал/будущий)
---------------------------------------------------------
-- старт квартала (последние 3 месяца окна)
DECLARE @mp_q_start date = DATEADD(
month, -2,
DATEFROMPARTS(YEAR(@mp_lastFullMonth), MONTH(@mp_lastFullMonth), 1)
);
;WITH AggYear AS (
SELECT
p.[1c_id],
tn_year = SUM(p.[ТН]),
rd_year = SUM(p.[рубли-дни]),
avg_cap_year = AVG(NULLIF(p.[Остаток учетка руб], 0.0))
FROM #mp_plan p
GROUP BY p.[1c_id]
),
AggQuarter AS (
SELECT
p.[1c_id],
tn_q = SUM(p.[ТН]),
rd_q = SUM(p.[рубли-дни]),
avg_cap_q = AVG(NULLIF(p.[Остаток учетка руб], 0.0))
FROM #mp_plan p
WHERE p.MonthStart >= @mp_q_start
GROUP BY p.[1c_id]
),
RoicPast AS (
SELECT
ay.[1c_id],
RoicValue =
CASE
WHEN ay.rd_year > 0
THEN ay.tn_year * 365.0 / ay.rd_year * 100.0
ELSE NULL
END,
ay.tn_year,
ay.avg_cap_year
FROM AggYear ay
),
RoicQuarterPast AS (
SELECT
aq.[1c_id],
RoicQ =
CASE
WHEN aq.rd_q > 0
THEN aq.tn_q * 365.0 / aq.rd_q * 100.0
ELSE NULL
END,
aq.tn_q,
aq.avg_cap_q
FROM AggQuarter aq
),
LastRow AS (
SELECT
p.[1c_id],
p.[Оплаченный остаток],
p.[Остаток шт],
p.[Остаток учетка руб],
ROW_NUMBER() OVER (
PARTITION BY p.[1c_id]
ORDER BY p.MonthStart DESC
) AS rn
FROM #mp_plan p
),
FutureBase AS (
SELECT
a.[1c_id],
a.[Продажи шт / день] AS sales_per_day,
a.[Продано шт] AS sold_year,
a.[ТН / год, руб.] AS tn_year_hist,
lr.[Оплаченный остаток] AS paid_stock_now,
lr.[Остаток шт] AS q_stock,
lr.[Остаток учетка руб] AS stock_rub_now
FROM [mag_pbi].[analytics].[аналитика за 365 дн.] a
LEFT JOIN LastRow lr
ON lr.[1c_id] = a.[1c_id]
AND lr.rn = 1
),
FutureInputs AS (
SELECT
f.[1c_id],
f.sales_per_day,
f.sold_year,
tn_year = COALESCE(f.tn_year_hist, rp.tn_year),
f.paid_stock_now,
f.q_stock,
f.stock_rub_now,
tn_per_unit =
CASE
WHEN f.sold_year > 0 AND COALESCE(f.tn_year_hist, rp.tn_year) IS NOT NULL
THEN COALESCE(f.tn_year_hist, rp.tn_year) / f.sold_year
ELSE NULL
END,
max_sell_1y =
CASE
WHEN f.sales_per_day IS NULL THEN NULL
ELSE f.sales_per_day * 365.0
END
FROM FutureBase f
LEFT JOIN RoicPast rp
ON rp.[1c_id] = f.[1c_id]
),
FutureCalc AS (
SELECT
fi.[1c_id],
RoicFuture =
CASE
WHEN fi.paid_stock_now IS NULL OR fi.paid_stock_now <= 0
OR fi.sales_per_day IS NULL OR fi.sales_per_day <= 0
OR fi.tn_per_unit IS NULL
OR fi.q_stock IS NULL OR fi.q_stock <= 0
OR fi.max_sell_1y IS NULL OR fi.max_sell_1y <= 0
THEN NULL
ELSE
(
(
fi.tn_per_unit *
(CASE
WHEN fi.q_stock <= fi.max_sell_1y THEN fi.q_stock
ELSE fi.max_sell_1y
END)
) * 365.0
/
(
CASE
WHEN fi.q_stock / fi.sales_per_day >= 365.0
THEN fi.paid_stock_now * 365.0
ELSE fi.paid_stock_now * (fi.q_stock / fi.sales_per_day) / 2.0
END
) * 100.0
)
END,
tn_future =
CASE
WHEN fi.tn_per_unit IS NULL OR fi.max_sell_1y IS NULL OR fi.q_stock IS NULL
THEN NULL
ELSE
fi.tn_per_unit *
(CASE
WHEN fi.q_stock <= fi.max_sell_1y THEN fi.q_stock
ELSE fi.max_sell_1y
END)
END,
q_future =
CASE
WHEN fi.max_sell_1y IS NULL OR fi.q_stock IS NULL THEN NULL
WHEN fi.q_stock <= fi.max_sell_1y THEN fi.q_stock
ELSE fi.max_sell_1y
END
FROM FutureInputs fi
),
CapRentFuture AS (
SELECT
fi.[1c_id],
RentCapFuturePct =
CASE
WHEN fc.tn_future IS NULL
OR fi.stock_rub_now IS NULL OR fi.stock_rub_now <= 0
OR fi.q_stock IS NULL OR fi.q_stock <= 0
OR fc.q_future IS NULL OR fc.q_future <= 0
THEN NULL
ELSE
CASE
WHEN (fi.stock_rub_now * (1.0 - (fc.q_future / fi.q_stock) / 2.0)) <= 0
THEN NULL
ELSE
fc.tn_future
/ (fi.stock_rub_now * (1.0 - (fc.q_future / fi.q_stock) / 2.0))
* 100.0
END
END
FROM FutureInputs fi
LEFT JOIN FutureCalc fc
ON fc.[1c_id] = fi.[1c_id]
)
UPDATE a
SET
a.[Рентабельность / год] = rp.RoicValue,
a.[Рентабельность / квартал] = rq.RoicQ,
a.[Оплаченный остаток] = lr.[Оплаченный остаток],
a.[Рентабельность / будущий год] = fc.RoicFuture,
a.[Рентабельность по остатку / год] =
CASE
WHEN rp.avg_cap_year IS NOT NULL AND rp.avg_cap_year > 0
AND rp.tn_year IS NOT NULL
THEN (rp.tn_year / rp.avg_cap_year) * 100.0
ELSE NULL
END,
a.[Рентабельность по остатку / квартал] =
CASE
WHEN rq.avg_cap_q IS NOT NULL AND rq.avg_cap_q > 0
AND rq.tn_q IS NOT NULL
THEN (rq.tn_q / rq.avg_cap_q) * 100.0
ELSE NULL
END,
a.[Рентабельность по остатку / будущий год] = cf.RentCapFuturePct
FROM [mag_pbi].[analytics].[аналитика за 365 дн.] a
LEFT JOIN RoicPast rp ON rp.[1c_id] = a.[1c_id]
LEFT JOIN RoicQuarterPast rq ON rq.[1c_id] = a.[1c_id]
LEFT JOIN LastRow lr ON lr.[1c_id] = a.[1c_id] AND lr.rn = 1
LEFT JOIN FutureCalc fc ON fc.[1c_id] = a.[1c_id]
LEFT JOIN CapRentFuture cf ON cf.[1c_id] = a.[1c_id];
-------------------------------------------------------------------------------------
-- 9) Индекс (как был)
-------------------------------------------------------------------------------------
CREATE NONCLUSTERED INDEX [analyticsаналитика за 365 дн.]
ON [analytics].[аналитика за 365 дн.] ([1c_id])
INCLUDE (
[Продажи шт / день],
[Остаток дней продаж],
[ТН / год, руб.],
[ТН / квартал, руб.],
[Рентабельность / год],
[Рентабельность / квартал],
[Дней в продаже / год],
[Дней в продаже / квартал]
);
END
GO
/****** Object: StoredProcedure [analytics].[sp_fill_deficit_money_request] Script Date: 2026-02-22 12:04:15 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [analytics].[sp_fill_deficit_money_request]
@scenario_id INT = NULL -- NULL = MAX(scenario_id) из deficit_proposal
AS
SET NOCOUNT ON;
DECLARE @scenario INT;
IF @scenario_id IS NOT NULL AND @scenario_id > 0
SET @scenario = @scenario_id;
ELSE
SET @scenario = (SELECT MAX(scenario_id) FROM [analytics].[deficit_proposal]);
IF @scenario IS NULL
BEGIN
RAISERROR(N'Нет данных в deficit_proposal. Выполните sp_build_deficit_proposal.', 16, 1);
RETURN;
END
-- Очищаем таблицу для данного сценария
DELETE FROM [analytics].[deficit_money_request] WHERE scenario_id = @scenario;
-- Заполняем: дефицит -> номенклатура (производитель, цена) -> manufacturers -> контрагент
-- Номенклатура: pbi.v_nomenclature_full содержит все коды, Производитель, учётную цену
INSERT INTO [analytics].[deficit_money_request] (
scenario_id,
manufacturer_id,
contractor_1c_id,
manufacturer_name,
contractor_name,
arrival_month,
amount_rub,
amount_usd,
currency,
order_qty_total,
sku_count,
updated_at
)
SELECT
@scenario AS scenario_id,
m.id AS manufacturer_id,
mcm.contractor_1c_id,
m.manufacturer AS manufacturer_name,
c.contractor_name,
CAST(FORMAT(dp.arrival_month, 'yyyy-MM-01') AS date) AS arrival_month,
SUM(dp.order_qty * ISNULL(n.[Цена учетная, руб], 0)) AS amount_rub,
SUM(dp.order_qty * ISNULL(n.[Цена учетная, usd], 0)) AS amount_usd,
CASE
WHEN SUM(dp.order_qty * ISNULL(n.[Цена учетная, usd], 0)) >
SUM(dp.order_qty * ISNULL(n.[Цена учетная, руб], 0)) * 0.01
THEN N'USD' ELSE N'руб.'
END AS currency,
SUM(dp.order_qty) AS order_qty_total,
COUNT(DISTINCT dp.code) AS sku_count,
SYSDATETIME() AS updated_at
FROM [analytics].[deficit_proposal] dp
-- Номенклатура: все коды, производитель и учётная цена (без фильтра по группе)
INNER JOIN [mag_pbi].[pbi].[v_nomenclature_full] n
ON n.[code] = dp.code
-- Производитель: сопоставление по имени (LTRIM/RTRIM как в contractorProducer)
INNER JOIN [analytics].[manufacturers] m
ON LTRIM(RTRIM(ISNULL(n.[Производитель], N''))) = LTRIM(RTRIM(m.manufacturer))
AND LTRIM(RTRIM(ISNULL(n.[Производитель], N''))) <> N''
LEFT JOIN [analytics].[manufacturer_counterparty_map] mcm
ON mcm.manufacturer_id = m.id
LEFT JOIN [analytics].[v_contractors] c
ON c.contractor_1c_id = mcm.contractor_1c_id
WHERE dp.scenario_id = @scenario
AND dp.order_qty > 0
AND (n.[Цена учетная, руб] IS NOT NULL OR n.[Цена учетная, usd] IS NOT NULL)
GROUP BY
m.id,
mcm.contractor_1c_id,
m.manufacturer,
c.contractor_name,
CAST(FORMAT(dp.arrival_month, 'yyyy-MM-01') AS date);
-- Логируем результат
DECLARE @rows INT = @@ROWCOUNT;
PRINT CONCAT(N'analytics.sp_fill_deficit_money_request: внесено ', @rows, N' записей для scenario_id=', @scenario);
GO
/****** Object: StoredProcedure [analytics].[sp_load_koef_groups] Script Date: 2026-02-22 12:04:15 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [analytics].[sp_load_koef_groups] as BEGIN
Print('Запускайте код процедуры руками')
RETURN 0
DROP TABLE #koef
CREATE TABLE #koef (
[group_1c_id] nvarchar(256) NOT NULL,
[seasonal_koef] [decimal](18, 6) NOT NULL,
[month] int NOT NULL
);
BULK INSERT #koef
FROM '\\192.168.35.3\admin3\Обмен\powerbi\k1.csv'
WITH (
FIRSTROW = 2, -- пропустить заголовок
FIELDTERMINATOR = ';',
ROWTERMINATOR = '\n',
CODEPAGE = '1251' -- если кириллица
);
-- Проверить наличие данных во вр таблице
select top 100 * from #koef
--DELETE FROM [analytics].[seasonality_groups]
INSERT INTO [analytics].[seasonality_groups]
SELECT
g.[1c_id],
k.month,
k.seasonal_koef
FROM #koef k
INNER JOIN pbi.groups g
ON g.[1c_id]=k.group_1c_id
------------------------------
SET NOCOUNT ON;
------------------------------------------------------------
-- 1. Таблица месяцев 1..12
------------------------------------------------------------
IF OBJECT_ID('tempdb..#Months') IS NOT NULL DROP TABLE #Months;
CREATE TABLE #Months (
[month] tinyint NOT NULL PRIMARY KEY
);
INSERT INTO #Months ([month])
VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11),(12);
------------------------------------------------------------
-- 2. База продаж 20242025 по gg1 × месяц
------------------------------------------------------------
IF OBJECT_ID('tempdb..#SalesByGroupMonth') IS NOT NULL DROP TABLE #SalesByGroupMonth;
SELECT
g.[g],
g.[g1],
[month] = MONTH(s.[Период]),
qty = /* SUM(s.Сумма) */ SUM(s.[Количество])
INTO #SalesByGroupMonth
FROM [mag_pbi].[pbiProd].[СводныйСебестоимость Для PBI] AS s
INNER JOIN [pbi].[nomenclature] n
ON n.[1c_id] = s.[1c_id]
INNER JOIN [pbi].[groups] g
ON g.[1c_id] = n.[1c_group]
WHERE
s.[Статья] = N'Реализация'
AND s.[Период] >= '2023-12-01'
AND s.[Период] < '2025-12-01'
AND g.[g] NOT LIKE N'*%'
GROUP BY
g.[g],
g.[g1],
MONTH(s.[Период]);
------------------------------------------------------------
-- 3. Список всех gg1, которые продавались
------------------------------------------------------------
IF OBJECT_ID('tempdb..#GroupList') IS NOT NULL DROP TABLE #GroupList;
SELECT DISTINCT
sbgm.[g],
sbgm.[g1]
INTO #GroupList
FROM #SalesByGroupMonth sbgm;
------------------------------------------------------------
-- 4. gg1 × все 12 месяцев (qty = 0, если продаж не было)
------------------------------------------------------------
IF OBJECT_ID('tempdb..#AllGroupMonths') IS NOT NULL DROP TABLE #AllGroupMonths;
SELECT
gl.[g],
gl.[g1],
m.[month],
qty = ISNULL(sbgm.qty, 0)
INTO #AllGroupMonths
FROM #GroupList gl
CROSS JOIN #Months m
LEFT JOIN #SalesByGroupMonth sbgm
ON sbgm.[g] = gl.[g]
AND sbgm.[g1] = gl.[g1]
AND sbgm.[month] = m.[month];
------------------------------------------------------------
-- 5. Общий объём продаж по gg1 за все 12 месяцев
------------------------------------------------------------
IF OBJECT_ID('tempdb..#GroupTotals') IS NOT NULL DROP TABLE #GroupTotals;
SELECT
agm.[g],
agm.[g1],
total_qty = SUM(agm.qty)
INTO #GroupTotals
FROM #AllGroupMonths agm
GROUP BY
agm.[g],
agm.[g1];
------------------------------------------------------------
-- 6. Финальный результат: g, g1, koef, month
-- Сумма koef по 12 месяцам для каждого gg1 = 1
------------------------------------------------------------
SELECT
agm.[g],
agm.[g1],
agm.[month],
koef = CASE
WHEN gt.total_qty = 0 THEN 0
ELSE CAST(agm.qty AS decimal(18,6)) / NULLIF(gt.total_qty, 0)
END
--INTO analytics .seasonality_groups_summ_1
FROM #AllGroupMonths agm
INNER JOIN #GroupTotals gt
ON gt.[g] = agm.[g]
AND gt.[g1] = agm.[g1]
ORDER BY
agm.[g],
agm.[g1],
agm.[month];
END
GO
/****** Object: StoredProcedure [analytics].[sp_rebuild_stock_plan_by_arrival] Script Date: 2026-02-22 12:04:15 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [analytics].[sp_rebuild_stock_plan_by_arrival]
@scenario_id INT,
@from_month DATE, -- например: DATEFROMPARTS(YEAR(GETDATE()), MONTH(GETDATE()), 1)
@to_month DATE -- последний месяц горизонта + 1 день (интервал [from, to) по месяцам)
AS
BEGIN
SET NOCOUNT ON;
-- Безопасная рамка
IF @from_month IS NULL SET @from_month = DATEFROMPARTS(YEAR(GETDATE()), MONTH(GETDATE()), 1);
IF @to_month IS NULL SET @to_month = DATEADD(MONTH, 12, @from_month); -- 12 мес по умолчанию
-- Чистим целевой диапазон
DELETE FROM [analytics].[stock_plan_by_arrival]
WHERE [scenario_id] = @scenario_id
AND [arrival_month] >= @from_month
AND [arrival_month] < @to_month;
;WITH months AS ( -- календарь месяцев [from, to)
SELECT CAST(@from_month AS DATE) AS m
UNION ALL
SELECT DATEADD(MONTH, 1, m) FROM months WHERE DATEADD(MONTH, 1, m) < @to_month
),
-- 1) Стартовый остаток на начало горизонта (берём последний известный quantity)
opening AS (
SELECT w._IDRREF AS [1c_id],
n.code AS [code],
CAST(@from_month AS DATE) AS [arrival_month],
CAST(
MAX(CASE WHEN w.dt = x.max_dt THEN w.quantity END)
AS DECIMAL(18,3)) AS opening_qty
FROM [pbi].[w_ostatok_da_net] w
JOIN (
SELECT _IDRREF, MAX(dt) AS max_dt
FROM [pbi].[w_ostatok_da_net]
WHERE dt < @from_month
GROUP BY _IDRREF
) x ON x._IDRREF = w._IDRREF AND x.max_dt = w.dt
JOIN [pbi].[nomenclature] n ON n.[1c_id] = w._IDRREF
GROUP BY w._IDRREF, n.code
),
-- 2) Подтвержденные приходы (ожидаемые заказы) по дате прихода → к первому дню месяца
inbound_confirmed AS (
SELECT
o.[1c_id],
n.code,
o.month AS arrival_month,
CAST(SUM(o.units) AS DECIMAL(18,3)) AS inbound_confirmed
FROM [mag_pbi].[analytics].[get_orders_by_group] o
JOIN [pbi].[nomenclature] n ON n.[1c_id] = o.[1c_id]
WHERE o.[status] IN (N'В пути', N'В производстве', N'Выгружен на складе', N'Согласован')
AND o.month >= @from_month
AND o.month < @to_month
GROUP BY o.[1c_id], n.code,
o.month
),
-- 3) Будущие заказы (дефицит → рекомендованные поставки) по месяцу прихода
inbound_deficit AS (
SELECT
d.[1c_id],
d.[code],
DATEFROMPARTS(YEAR(d.[arrival_month]), MONTH(d.[arrival_month]), 1) AS arrival_month,
CAST(SUM(d.[order_qty]) AS DECIMAL(18,3)) AS inbound_deficit
FROM [analytics].[deficit_proposal] d
WHERE d.[scenario_id] = @scenario_id
AND d.[arrival_month] >= @from_month
AND d.[arrival_month] < @to_month
GROUP BY d.[1c_id], d.[code],
DATEFROMPARTS(YEAR(d.[arrival_month]), MONTH(d.[arrival_month]), 1)
),
-- 4) Прогноз спроса по месяцам
forecast AS (
SELECT
f.[1c_id],
f.[code],
DATEFROMPARTS(YEAR(f.[month]), MONTH(f.[month]), 1) AS arrival_month,
CAST(SUM(f.[value]) AS DECIMAL(18,3)) AS forecast_demand
FROM [analytics].[forecast] f
WHERE f.[scenario_id] = @scenario_id
AND f.[month] >= @from_month
AND f.[month] < @to_month
GROUP BY f.[1c_id], f.[code],
DATEFROMPARTS(YEAR(f.[month]), MONTH(f.[month]), 1)
),
-- 5) Объединим каркас всех SKU × месяцы горизонта
sku_calendar AS (
SELECT DISTINCT n.[1c_id], n.[code], m.m AS arrival_month
FROM [pbi].[nomenclature] n
CROSS JOIN months m
),
base_union AS (
SELECT c.[1c_id], c.[code], c.[arrival_month],
COALESCE(op.opening_qty, 0) AS opening_qty,
COALESCE(ic.inbound_confirmed, 0) AS inbound_confirmed,
COALESCE(idf.inbound_deficit, 0) AS inbound_deficit,
COALESCE(fc.forecast_demand, 0) AS forecast_demand
FROM sku_calendar c
LEFT JOIN opening op ON op.[1c_id]=c.[1c_id] AND op.[code]=c.[code] AND op.[arrival_month]=@from_month
LEFT JOIN inbound_confirmed ic ON ic.[1c_id]=c.[1c_id] AND ic.[code]=c.[code] AND ic.[arrival_month]=c.[arrival_month]
LEFT JOIN inbound_deficit idf ON idf.[1c_id]=c.[1c_id] AND idf.[code]=c.[code] AND idf.[arrival_month]=c.[arrival_month]
LEFT JOIN forecast fc ON fc.[1c_id]=c.[1c_id] AND fc.[code]=c.[code] AND fc.[arrival_month]=c.[arrival_month]
WHERE c.[arrival_month] >= @from_month
AND c.[arrival_month] < @to_month
),
-- 6) Расчёт rolling opening/closing по месяцам
projected AS (
SELECT
@scenario_id AS scenario_id,
b.[1c_id],
b.[code],
b.[arrival_month],
-- opening: для первого месяца берем opening_qty из "opening", далее — прошлый closing
CAST(
CASE
WHEN b.[arrival_month] = @from_month
THEN b.[opening_qty]
ELSE 0
END
AS DECIMAL(18,3)) AS opening_qty,
b.[inbound_confirmed],
b.[inbound_deficit],
b.[forecast_demand]
FROM base_union b
)
-- Вставка построчно с расчетом closing через окно
INSERT INTO [analytics].[stock_plan_by_arrival]
([scenario_id],[arrival_month],[1c_id],[code],
[opening_qty],[inbound_confirmed],[inbound_deficit],[forecast_demand],[closing_qty],[updated_at])
SELECT
p.scenario_id,
p.arrival_month,
p.[1c_id],
p.[code],
-- opening по месяцу = LAG(closing) OVER (по SKU упорядочено по месяцу), для первого — opening_qty
CAST(
COALESCE(
LAG( closing_calc ) OVER (PARTITION BY p.[1c_id], p.[code] ORDER BY p.arrival_month),
p.opening_qty
)
AS DECIMAL(18,3)) AS opening_qty,
p.inbound_confirmed,
p.inbound_deficit,
p.forecast_demand,
-- closing = opening + inbound_confirmed + inbound_deficit - forecast
CAST(
(
COALESCE( LAG( closing_calc ) OVER (PARTITION BY p.[1c_id], p.[code] ORDER BY p.arrival_month), p.opening_qty)
+ p.inbound_confirmed
+ p.inbound_deficit
- p.forecast_demand
) AS DECIMAL(18,3)
) AS closing_qty,
SYSUTCDATETIME()
FROM (
-- промежуточное вычисление closing для использования в LAG
SELECT
scenario_id, [1c_id], [code], arrival_month, opening_qty, inbound_confirmed, inbound_deficit, forecast_demand,
CAST(opening_qty + inbound_confirmed + inbound_deficit - forecast_demand AS DECIMAL(18,3)) AS closing_calc
FROM projected
) p
OPTION (MAXRECURSION 0);
END
GO
/****** Object: StoredProcedure [analytics].[sp_recalc_roic] Script Date: 2026-02-22 12:04:15 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [analytics].[sp_recalc_roic]
@manufacturer_id INT = NULL
AS
SET NOCOUNT ON;
;WITH stage_agg AS (
SELECT
manufacturer_id,
SUM([percent] / 100.0 * [days]) AS effective_deferral_days,
SUM([percent] / 100.0) AS total_percent
FROM [analytics].[manufacturer_payment_stage]
WHERE @manufacturer_id IS NULL OR manufacturer_id = @manufacturer_id
GROUP BY manufacturer_id
),
calc AS (
SELECT
man.id,
man.days_of_sales,
man.logistics_days,
COALESCE(s.effective_deferral_days, 0) AS effective_deferral_days,
COALESCE(s.total_percent, 0) AS total_percent,
(ISNULL(man.logistics_days, 120) + ISNULL(man.days_of_sales, 180) / 2.0) AS avg_return_day
FROM [analytics].[manufacturers] man
LEFT JOIN stage_agg s ON s.manufacturer_id = man.id
WHERE @manufacturer_id IS NULL OR man.id = @manufacturer_id
),
roic_calc AS (
SELECT
id,
CASE
WHEN total_percent <= 0 THEN NULL
WHEN (avg_return_day - effective_deferral_days) <= 0 THEN NULL
ELSE ROUND(12.0 / ((avg_return_day - effective_deferral_days) / 30.0) * 100.0, 2)
END AS new_roic
FROM calc
)
UPDATE man
SET man.roic_norm = r.new_roic
FROM [analytics].[manufacturers] man
JOIN roic_calc r ON r.id = man.id;
SELECT @@ROWCOUNT AS updated_count;
GO
/****** Object: StoredProcedure [analytics].[sp_report_ROI] Script Date: 2026-02-22 12:04:15 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [analytics].[sp_report_ROI]
@scenario_id INT = 4,
@min_deficit_rub DECIMAL(18,2) = 2000,
@cutoff_month DATE = '2027-01-01',
@pathPattern NVARCHAR(400) = N'Кружево%'
AS
BEGIN
SET NOCOUNT ON;
;WITH stages_pivot AS (
SELECT manufacturer_id,
MAX(CASE WHEN rn = 1 THEN [percent] END) / 100.0 AS n_percent,
MAX(CASE WHEN rn = 1 THEN days END) AS n_days,
MAX(CASE WHEN rn = 2 THEN [percent] END) / 100.0 AS m_percent,
MAX(CASE WHEN rn = 2 THEN days END) AS m_days
FROM (
SELECT manufacturer_id, [percent], days,
ROW_NUMBER() OVER (PARTITION BY manufacturer_id ORDER BY sort_order) AS rn
FROM [analytics].[manufacturer_payment_stage]
) t WHERE rn <= 2
GROUP BY manufacturer_id
),
base AS (
SELECT
g.[path] AS grp
, n.[Производитель]
, d.place_month
, SUM(d.order_qty * ISNULL(n.[Цена учетная, руб], 0)) AS Deficit
, SUM(a.[%ТН год, руб.] * d.order_qty * ISNULL(n.[Цена учетная, руб], 0))
/ NULLIF(SUM(d.order_qty * ISNULL(n.[Цена учетная, руб], 0)), 0) AS [%ТН средн]
FROM [analytics].[deficit_proposal] d
INNER JOIN [mag_pbi].[pbi].[v_nomenclature_full] n ON n.[1c_id] = d.[1c_id]
INNER JOIN [analytics].[аналитика за 365 дн.] a ON a.[1c_id] = d.[1c_id]
INNER JOIN [mag_pbi].[pbi].[groups] g ON g.[1c_id] = n.[1c_group]
WHERE d.scenario_id = @scenario_id
AND ISNULL(n.[cenovaya_gruppa], N'') = N'Валютная'
AND g.[path] LIKE @pathPattern
GROUP BY g.[path], n.[Производитель], d.place_month
HAVING SUM(d.order_qty * ISNULL(n.[Цена учетная, руб], 0)) > @min_deficit_rub
)
SELECT
b.grp AS [g]
, b.[Производитель]
, b.place_month
, b.Deficit
, b.[%ТН средн]
, b.[%ТН средн] * ISNULL(m.roic_norm, 1.36) AS [%ROI заказа]
, ISNULL(m.roic_norm, 1.36) AS [ROI normalized]
, b.[%ТН средн] * ISNULL(m.roic_norm, 1.36) * b.Deficit AS [ROI руб]
, ISNULL(sp.n_percent, 0.30) AS n_percent
, ISNULL(sp.n_days, 1) AS n_days
, ISNULL(sp.m_percent, 0.70) AS m_percent
, ISNULL(sp.m_days, 60) AS m_days
FROM base b
LEFT JOIN [analytics].[manufacturers] m
ON LTRIM(RTRIM(ISNULL(b.[Производитель], N''))) = LTRIM(RTRIM(m.manufacturer))
AND LTRIM(RTRIM(ISNULL(b.[Производитель], N''))) <> N''
LEFT JOIN stages_pivot sp ON sp.manufacturer_id = m.id
ORDER BY [%ROI заказа] DESC, b.grp, b.Deficit DESC;
-- 2. Платежи по месяцам (из deficit_proposal по path — dmr не хранит path)
SELECT
FORMAT(d.arrival_month, 'yyyy-MM') AS [Дата платежа]
, SUM(d.order_qty * ISNULL(n.[Цена учетная, руб], 0)) AS [Сумма платежа]
FROM [analytics].[deficit_proposal] d
INNER JOIN [mag_pbi].[pbi].[v_nomenclature_full] n ON n.[1c_id] = d.[1c_id]
INNER JOIN [mag_pbi].[pbi].[groups] g ON g.[1c_id] = n.[1c_group]
WHERE d.scenario_id = @scenario_id
AND g.[path] LIKE @pathPattern
GROUP BY FORMAT(d.arrival_month, 'yyyy-MM')
ORDER BY FORMAT(d.arrival_month, 'yyyy-MM') ASC;
-- 3. Платежи до cutoff (из deficit_proposal по path)
SELECT
SUM(d.order_qty * ISNULL(n.[Цена учетная, руб], 0)) AS [Платежи в 2026]
FROM [analytics].[deficit_proposal] d
INNER JOIN [mag_pbi].[pbi].[v_nomenclature_full] n ON n.[1c_id] = d.[1c_id]
INNER JOIN [mag_pbi].[pbi].[groups] g ON g.[1c_id] = n.[1c_group]
WHERE d.scenario_id = @scenario_id
AND g.[path] LIKE @pathPattern
AND d.arrival_month < @cutoff_month;
-- 4. Прогноз выручки по месяцам
SELECT
t.[month]
, SUM(t.Выручка) AS revenue
, SUM(t.[Сумм. учет]) AS uchet
FROM (
SELECT
f.[month]
, f.[value] * ISNULL(n.[Цена учетная, руб], 0) AS [Сумм. учет]
, f.[value] * ISNULL(n.[Цена учетная, руб], 0) * (1 + a.[%ТН год, руб.]) AS Выручка
FROM [analytics].[forecast] f
INNER JOIN [analytics].[аналитика за 365 дн.] a ON a.[1c_id] = f.[1c_id]
INNER JOIN [mag_pbi].[pbi].[nomenclature] n ON n.[1c_id] = f.[1c_id]
INNER JOIN [mag_pbi].[pbi].[groups] g ON g.[1c_id] = n.[1c_group]
WHERE f.scenario_id = @scenario_id
AND a.[%ТН год, руб.] > 0
AND g.[path] LIKE @pathPattern
) t
GROUP BY t.[month]
ORDER BY t.[month];
-- 5. ROIC по группе (path pattern)
;WITH sku_in_group AS (
SELECT
a.[1c_id]
, a.[Code] AS code
, g.[path]
, a.[Оплаченный остаток] AS PaidCapital
, a.[Рентабельность по остатку / год] AS RoicPast
FROM [analytics].[аналитика за 365 дн.] a
INNER JOIN [mag_pbi].[pbi].[v_nomenclature_full] n ON n.[1c_id] = a.[1c_id]
INNER JOIN [mag_pbi].[pbi].[groups] g ON g.[1c_id] = n.[1c_group]
WHERE g.[path] LIKE @pathPattern
)
SELECT
@pathPattern AS [Фильтр path]
, SUM(PaidCapital) AS [Оплаченный остаток всего, руб]
, CASE
WHEN SUM(PaidCapital) > 0
THEN SUM(ISNULL(PaidCapital, 0) * ISNULL(RoicPast, 0)) / SUM(PaidCapital)
ELSE NULL
END AS [Рентабельность остатка / год назад, %]
FROM sku_in_group;
-- 6. Итоговые ROIC по всей аналитике
DECLARE @sumPaid DECIMAL(38,6);
DECLARE @sumWeightedPast DECIMAL(38,6);
DECLARE @sumWeightedFuture DECIMAL(38,6);
DECLARE @roicPastTotal DECIMAL(38,6);
DECLARE @roicFutureTotal DECIMAL(38,6);
SELECT
@sumPaid = SUM(CAST(ISNULL(a.[Оплаченный остаток], 0) AS DECIMAL(38,6)))
, @sumWeightedPast = SUM(
CAST(ISNULL(a.[Оплаченный остаток], 0) AS DECIMAL(38,6))
* CAST(ISNULL(a.[Рентабельность по остатку / год], 0) AS DECIMAL(38,6))
)
, @sumWeightedFuture = SUM(
CAST(ISNULL(a.[Оплаченный остаток], 0) AS DECIMAL(38,6))
* CAST(ISNULL(a.[Рентабельность / будущий год], 0) AS DECIMAL(38,6))
)
FROM [analytics].[аналитика за 365 дн.] a
INNER JOIN [mag_pbi].[pbi].[v_nomenclature_full] n ON n.[1c_id] = a.[1c_id]
INNER JOIN [mag_pbi].[pbi].[groups] g ON g.[1c_id] = n.[1c_group]
WHERE g.[path] LIKE @pathPattern;
SET @roicPastTotal = CASE WHEN @sumPaid > 0 THEN @sumWeightedPast / @sumPaid ELSE NULL END;
SET @roicFutureTotal = CASE WHEN @sumPaid > 0 THEN @sumWeightedFuture / @sumPaid ELSE NULL END;
SELECT
@sumPaid AS [Оплаченный остаток всего, руб]
, @roicPastTotal AS [Рентабельность остатка / год назад всего, %]
, @roicFutureTotal AS [Рентабельность остатка / будущий год всего, %];
END
GO
/****** Object: StoredProcedure [analytics].[sp_report_ROI_подробно] Script Date: 2026-02-22 12:04:15 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE procedure [analytics].[sp_report_ROI_подробно] as BEGIN
---------------------------------------------------------
-- 0. Параметры дат: 12 полных месяцев НАЗАД
---------------------------------------------------------
DECLARE @today date = CAST(GETDATE() AS date);
DECLARE @lastFullMonth date;
DECLARE @firstMonthStart date;
DECLARE @curMonthStart date;
-- последний полный месяц (месяц до текущего дня)
SET @lastFullMonth = EOMONTH(DATEADD(month, -1, @today));
-- первый из 12 полных месяцев
SET @firstMonthStart = DATEADD(
month, -11,
DATEFROMPARTS(YEAR(@lastFullMonth), MONTH(@lastFullMonth), 1)
);
SET @curMonthStart = @firstMonthStart;
---------------------------------------------------------
-- 1. Список месяцев (#months)
---------------------------------------------------------
IF OBJECT_ID('tempdb..#months') IS NOT NULL DROP TABLE #months;
CREATE TABLE #months (
MonthStart date NOT NULL PRIMARY KEY
);
WHILE @curMonthStart
<= DATEFROMPARTS(YEAR(@lastFullMonth), MONTH(@lastFullMonth), 1)
BEGIN
INSERT INTO #months (MonthStart)
VALUES (@curMonthStart);
SET @curMonthStart = DATEADD(month, 1, @curMonthStart);
END;
---------------------------------------------------------
-- 2. SKU (#s) здесь укажи нужный код товара
---------------------------------------------------------
IF OBJECT_ID('tempdb..#s') IS NOT NULL DROP TABLE #s;
CREATE TABLE #s (
[1c_id] binary(16) NOT NULL,
code nchar(11) NOT NULL
);
INSERT INTO #s ([1c_id], code)
SELECT n.[1c_id], n.code
FROM pbi.nomenclature n
WHERE n.code = N'УТ-00176242'; -- <=== поменяй при необходимости
---------------------------------------------------------
-- 3. Внешние остатки (#ext_stock) срезы на конец месяца
---------------------------------------------------------
IF OBJECT_ID('tempdb..#ext_stock') IS NOT NULL DROP TABLE #ext_stock;
CREATE TABLE #ext_stock (
Dt date NOT NULL,
code nchar(11) NOT NULL,
qty numeric(18,3) NOT NULL,
CONSTRAINT PK_ext_stock PRIMARY KEY (Dt, code)
);
INSERT INTO #ext_stock (Dt, code, qty)
SELECT
CAST(o.[Дата обновления] AS date) AS Dt,
o.code,
SUM(o.[Количество]) AS qty
FROM analytics.[Внешние остатки] o
JOIN #s s
ON s.code = o.code
GROUP BY CAST(o.[Дата обновления] AS date), o.code;
---------------------------------------------------------
-- 4. Приходы (#incoming) Закупка / Приход по месяцам
-- БЕРЁМ ТОЛЬКО ПРИХОДЫ ВНУТРИ НАШИХ 12 МЕСЯЦЕВ
---------------------------------------------------------
IF OBJECT_ID('tempdb..#incoming') IS NOT NULL DROP TABLE #incoming;
CREATE TABLE #incoming (
MonthStart date NOT NULL, -- месяц прихода (1-е число)
code nchar(11) NOT NULL,
qty numeric(18,3) NOT NULL,
CONSTRAINT PK_incoming PRIMARY KEY (MonthStart, code)
);
INSERT INTO #incoming (MonthStart, code, qty)
SELECT
DATEFROMPARTS(YEAR(s.[Период]), MONTH(s.[Период]), 1) AS MonthStart,
n.code,
SUM(s.[Количество]) AS qty
FROM pbiProd.[СводныйСебестоимость Для PBI] s
JOIN pbi.nomenclature n
ON n.[1c_id] = s.[1c_id]
JOIN #s sfilter
ON sfilter.code = n.code
WHERE s.[Статья] = N'Закупка'
AND s.[Вид операции] = N'Приход'
AND s.[Период] >= @firstMonthStart -- отключаем старые приходы
GROUP BY
DATEFROMPARTS(YEAR(s.[Период]), MONTH(s.[Период]), 1),
n.code;
---------------------------------------------------------
-- 5. Части платежей по каждому приходу (#pay_parts)
-- (1-й и 2-й платёж по производителю, проценты как ДОЛИ)
---------------------------------------------------------
IF OBJECT_ID('tempdb..#pay_parts') IS NOT NULL DROP TABLE #pay_parts;
CREATE TABLE #pay_parts (
MonthStart date NOT NULL, -- месяц прихода (ключ к #incoming)
code nchar(11) NOT NULL,
pay_date date NOT NULL, -- дата платежа
amount numeric(18,2) NOT NULL
);
-- первый платёж (n)
INSERT INTO #pay_parts (MonthStart, code, pay_date, amount)
SELECT
inc.MonthStart,
inc.code,
DATEADD(day, man.n_days - 90, EOMONTH(inc.MonthStart)) AS pay_date,
inc.qty * n.[Цена учетная, руб] * man.n_percent AS amount
FROM #incoming inc
JOIN pbi.nomenclature n
ON n.code = inc.code
JOIN analytics.manufacturers man
ON man.[manufacturer] = n.Производитель
WHERE man.n_percent IS NOT NULL
AND man.n_percent <> 0;
-- второй платёж (m)
INSERT INTO #pay_parts (MonthStart, code, pay_date, amount)
SELECT
inc.MonthStart,
inc.code,
DATEADD(day, man.m_days - 90, EOMONTH(inc.MonthStart)) AS pay_date,
inc.qty * n.[Цена учетная, руб] * man.m_percent AS amount
FROM #incoming inc
JOIN pbi.nomenclature n
ON n.code = inc.code
JOIN analytics.manufacturers man
ON man.[manufacturer] = n.Производитель
WHERE man.m_percent IS NOT NULL
AND man.m_percent <> 0;
---------------------------------------------------------
-- 6. Платежи по месяцам (#payments) для колонки [платеж]
---------------------------------------------------------
IF OBJECT_ID('tempdb..#payments') IS NOT NULL DROP TABLE #payments;
CREATE TABLE #payments (
MonthStart date NOT NULL, -- месяц платежа (1-е число)
code nchar(11) NOT NULL,
amount numeric(18,2) NOT NULL,
CONSTRAINT PK_payments PRIMARY KEY (MonthStart, code)
);
INSERT INTO #payments (MonthStart, code, amount)
SELECT
DATEFROMPARTS(YEAR(p.pay_date), MONTH(p.pay_date), 1) AS MonthStart,
p.code,
SUM(p.amount) AS amount
FROM #pay_parts p
GROUP BY
DATEFROMPARTS(YEAR(p.pay_date), MONTH(p.pay_date), 1),
p.code;
---------------------------------------------------------
-- 7. Обязательства по месяцам (#obligations)
-- Считаем только по приходам (#incoming)
---------------------------------------------------------
IF OBJECT_ID('tempdb..#obligations') IS NOT NULL DROP TABLE #obligations;
CREATE TABLE #obligations (
MonthStart date NOT NULL, -- месяц, на который считаем долг
code nchar(11) NOT NULL,
obligation numeric(18,2) NOT NULL,
CONSTRAINT PK_obligations PRIMARY KEY (MonthStart, code)
);
;WITH ObligRows AS (
SELECT
m.MonthStart,
inc.code,
CASE
-- если месяц ещё до прихода товара долга нет
WHEN EOMONTH(m.MonthStart) < EOMONTH(inc.MonthStart) THEN 0
ELSE
CASE
WHEN full_sum - paid_to_month > 0
THEN full_sum - paid_to_month
ELSE 0
END
END AS ObligationPerIncoming
FROM #months m
JOIN #incoming inc
ON 1 = 1 -- каждая пара (месяц, приход)
JOIN pbi.nomenclature n2
ON n2.code = inc.code
JOIN analytics.manufacturers man2
ON man2.[manufacturer] = n2.Производитель
CROSS APPLY (
SELECT
inc.qty * n2.[Цена учетная, руб] AS full_sum,
(
-- оплачено к концу месяца m.MonthStart (по обоим платежам)
(CASE
WHEN man2.n_percent IS NOT NULL
AND EOMONTH(m.MonthStart)
>= DATEADD(day, man2.n_days - 90,
EOMONTH(inc.MonthStart))
THEN inc.qty * n2.[Цена учетная, руб] * man2.n_percent
ELSE 0
END)
+
(CASE
WHEN man2.m_percent IS NOT NULL
AND EOMONTH(m.MonthStart)
>= DATEADD(day, man2.m_days - 90,
EOMONTH(inc.MonthStart))
THEN inc.qty * n2.[Цена учетная, руб] * man2.m_percent
ELSE 0
END)
) AS paid_to_month
) calc
)
INSERT INTO #obligations (MonthStart, code, obligation)
SELECT
MonthStart,
code,
SUM(ObligationPerIncoming) AS obligation
FROM ObligRows
GROUP BY
MonthStart,
code;
---------------------------------------------------------
-- 8. ТН по месяцам (#tn_monthly) из Продажи_Учёт_Маржа_по_дням
---------------------------------------------------------
IF OBJECT_ID('tempdb..#tn_monthly') IS NOT NULL DROP TABLE #tn_monthly;
CREATE TABLE #tn_monthly (
MonthStart date NOT NULL,
code nchar(11) NOT NULL,
tn_amount numeric(18,2) NOT NULL,
CONSTRAINT PK_tn_monthly PRIMARY KEY (MonthStart, code)
);
INSERT INTO #tn_monthly (MonthStart, code, tn_amount)
SELECT
DATEFROMPARTS(YEAR(p.[d]), MONTH(p.[d]), 1) AS MonthStart,
p.[Code],
SUM(p.[Торговая надбавка]) AS tn_amount
FROM [mag_pbi].[analytics].[Продажи_Учёт_Маржа_по_дням] p
JOIN #s s
ON s.code = p.[Code]
WHERE p.[d] >= @firstMonthStart
AND p.[d] <= @lastFullMonth
GROUP BY
DATEFROMPARTS(YEAR(p.[d]), MONTH(p.[d]), 1),
p.[Code];
---------------------------------------------------------
-- 9. Финальный SELECT ПО МЕСЯЦАМ (прошлый год)
---------------------------------------------------------
IF OBJECT_ID('tempdb..#month_plan') IS NOT NULL DROP TABLE #month_plan;
SELECT
s.[1c_id],
s.code,
n.Производитель,
n.[Цена учетная, руб],
n.[Цена учетная, руб]*(ISNULL(intStock.quantity, 0) + ISNULL(ext.qty, 0)) AS [Остаток учетка руб],
man.n_percent,
man.n_days,
man.m_percent,
man.m_days,
RIGHT('0' + CAST(MONTH(m.MonthStart) AS varchar(2)), 2)
+ '-' + CAST(YEAR(m.MonthStart) AS varchar(4)) AS [месяц], -- MM-YYYY (текст)
m.MonthStart AS [MonthStart], -- реальная дата месяца
ISNULL(intStock.quantity, 0) + ISNULL(ext.qty, 0) AS [остаток], -- шт
ISNULL(inc.qty, 0) AS [приход], -- шт
ISNULL(pay.amount, 0) AS [платеж], -- руб
ISNULL(ob.obligation, 0) AS [Обязательства],-- руб
CAST(
CASE
WHEN p.paid_raw < 0 THEN 0
ELSE p.paid_raw
END
AS numeric(18,2)
) AS [Оплаченный остаток], -- руб
ISNULL(tn.tn_amount, 0) AS [ТН], -- торговая надбавка за месяц, руб
CAST(
CASE
WHEN p.paid_raw < 0 THEN 0
ELSE p.paid_raw
END * DAY(EOMONTH(m.MonthStart))
AS numeric(18,2)
) AS [рубли-дни] -- оплаченный остаток × дней в месяце
INTO #month_plan
FROM #months m
CROSS JOIN #s s
OUTER APPLY (
SELECT TOP (1) w1.quantity
FROM pbi.[w_ostatok_da_net] w1
WHERE w1._IDRREF = s.[1c_id]
AND w1.dt <= EOMONTH(m.MonthStart)
ORDER BY w1.dt DESC
) AS intStock
LEFT JOIN #ext_stock ext
ON ext.code = s.code
AND ext.Dt = EOMONTH(m.MonthStart)
LEFT JOIN #incoming inc
ON inc.code = s.code
AND inc.MonthStart = m.MonthStart
LEFT JOIN pbi.nomenclature n
ON n.code = s.code
LEFT JOIN analytics.manufacturers man
ON man.[manufacturer] = n.Производитель
LEFT JOIN #payments pay
ON pay.code = s.code
AND pay.MonthStart = m.MonthStart
LEFT JOIN #obligations ob
ON ob.code = s.code
AND ob.MonthStart = m.MonthStart
LEFT JOIN #tn_monthly tn
ON tn.code = s.code
AND tn.MonthStart = m.MonthStart
CROSS APPLY ( -- сырые оплаченные рубли (до обрезки в 0)
SELECT
(ISNULL(intStock.quantity, 0) + ISNULL(ext.qty, 0))
* ISNULL(n.[Цена учетная, руб], 0)
- ISNULL(ob.obligation, 0) AS paid_raw
) p
ORDER BY m.MonthStart;
-- помесячная картина прошлого
SELECT * FROM #month_plan;
---------------------------------------------------------
-- 10. Рентабельность за прошлый год (ROIC как было)
---------------------------------------------------------
DECLARE @roic_past numeric(38,6);
DECLARE @tn_year numeric(38,6);
DECLARE @ruble_days_year numeric(38,6);
SELECT
@tn_year = SUM(t.[ТН]),
@ruble_days_year = SUM(t.[рубли-дни])
FROM #month_plan t;
SET @roic_past = CASE
WHEN @ruble_days_year > 0
THEN @tn_year * 365.0 / @ruble_days_year * 100.0
ELSE NULL
END;
---------------------------------------------------------
-- 10a. Рентабельность по остатку В РУБЛЯХ (год и квартал назад)
-- ТН / средний остаток учетка руб
---------------------------------------------------------
DECLARE @rent_cap_year numeric(38,6);
DECLARE @rent_cap_quarter numeric(38,6);
-- год: средний Остаток учетка руб по всем 12 месяцам
DECLARE @avg_cap_year numeric(38,6);
SELECT
@avg_cap_year = AVG(NULLIF(t.[Остаток учетка руб], 0.0))
FROM #month_plan t;
IF @avg_cap_year IS NOT NULL AND @avg_cap_year > 0 AND @tn_year IS NOT NULL
SET @rent_cap_year = @tn_year / @avg_cap_year;
ELSE
SET @rent_cap_year = NULL;
-- квартал: последние 3 месяца окна
DECLARE @q_start date = DATEADD(
month, -2,
DATEFROMPARTS(YEAR(@lastFullMonth), MONTH(@lastFullMonth), 1)
);
DECLARE @tn_q numeric(38,6);
DECLARE @avg_cap_q numeric(38,6);
SELECT
@tn_q = SUM(t.[ТН]),
@avg_cap_q = AVG(NULLIF(t.[Остаток учетка руб], 0.0))
FROM #month_plan t
WHERE t.MonthStart >= @q_start;
IF @avg_cap_q IS NOT NULL AND @avg_cap_q > 0 AND @tn_q IS NOT NULL
SET @rent_cap_quarter = @tn_q / @avg_cap_q;
ELSE
SET @rent_cap_quarter = NULL;
---------------------------------------------------------
-- 11. Рентабельность остатка на будущий год (ROIC вперёд)
---------------------------------------------------------
-- 11.1. Текущий SKU и его продажи / день
DECLARE @sku_1c_id binary(16);
SELECT TOP(1) @sku_1c_id = [1c_id] FROM #s;
DECLARE @sales_per_day numeric(38,6); -- Продажи шт / день
DECLARE @sold_year numeric(38,6); -- Продано шт за последние 12 мес
-- берём скорость продаж из аналитики за 365 дней (если есть)
SELECT
@sales_per_day = a.[Продажи шт / день]
FROM [mag_pbi].[analytics].[аналитика за 365 дн.] a
WHERE a.[1c_id] = @sku_1c_id;
-- продано шт за наши 12 месяцев (по себестоимости)
SELECT
@sold_year = SUM(s.[Количество])
FROM pbiProd.[СводныйСебестоимость Для PBI] s
WHERE s.[1c_id] = @sku_1c_id
AND s.[Статья] = N'Реализация'
AND s.[Период] >= @firstMonthStart
AND s.[Период] <= @lastFullMonth;
IF (@sales_per_day IS NULL OR @sales_per_day = 0) AND @sold_year IS NOT NULL
BEGIN
-- fallback, если ещё не прогоняли sp_create_analytics_365
SET @sales_per_day = @sold_year / 365.0;
END;
-- 11.2. Маржа на штуку по истории
DECLARE @tn_per_unit numeric(38,6);
SET @tn_per_unit = CASE
WHEN @sold_year IS NOT NULL AND @sold_year > 0
THEN @tn_year / @sold_year
ELSE NULL
END;
-- 11.3. Текущий оплаченный остаток, остаток шт и остаток учетка руб (последний месяц окна)
DECLARE @q_stock numeric(18,6); -- текущий остаток шт
DECLARE @paid_stock_today numeric(18,6); -- оплаченный остаток в руб
DECLARE @stock_rub_today numeric(18,6); -- остаток учетка руб на конец окна
SELECT TOP(1)
@q_stock = [остаток],
@paid_stock_today = [Оплаченный остаток],
@stock_rub_today = [Остаток учетка руб]
FROM #month_plan
ORDER BY MonthStart DESC; -- последний из 12 месяцев = @lastFullMonth
-- 11.4. Сколько успеем продать за будущий год и какая будет ТН
DECLARE @q_future numeric(18,6); -- сколько штук реально успеем продать за год
DECLARE @tn_future numeric(18,6); -- ожидаемая ТН за будущий год
IF @sales_per_day IS NOT NULL AND @sales_per_day > 0
BEGIN
DECLARE @max_sell_1y numeric(18,6);
SET @max_sell_1y = @sales_per_day * 365.0;
SET @q_future = CASE
WHEN @q_stock IS NULL THEN 0
WHEN @q_stock <= @max_sell_1y THEN @q_stock
ELSE @max_sell_1y
END;
END
ELSE
BEGIN
SET @q_future = 0;
END;
IF @tn_per_unit IS NOT NULL
SET @tn_future = @tn_per_unit * @q_future;
ELSE
SET @tn_future = NULL;
-- 11.5. Будущие рубли-дни и ROIC вперёд
DECLARE @days_to_sell numeric(18,6);
DECLARE @ruble_days_future numeric(18,6);
DECLARE @roic_future numeric(38,6);
IF @sales_per_day IS NOT NULL AND @sales_per_day > 0 AND @q_stock IS NOT NULL
SET @days_to_sell = @q_stock / @sales_per_day;
ELSE
SET @days_to_sell = NULL;
IF @days_to_sell IS NULL OR @paid_stock_today IS NULL OR @paid_stock_today <= 0 OR @tn_future IS NULL
BEGIN
SET @roic_future = NULL;
END
ELSE
BEGIN
IF @days_to_sell >= 365.0
-- остаток будет лежать весь год
SET @ruble_days_future = @paid_stock_today * 365.0;
ELSE
-- остаток линейно уходит до нуля за days_to_sell
SET @ruble_days_future = @paid_stock_today * @days_to_sell / 2.0;
IF @ruble_days_future > 0
SET @roic_future = @tn_future * 365.0 / @ruble_days_future * 100.0;
ELSE
SET @roic_future = NULL;
END;
---------------------------------------------------------
-- 11.6. Рентабельность по остатку В РУБЛЯХ на будущий год
-- (ТН_future / средний остаток учетка руб в горизонте)
---------------------------------------------------------
DECLARE @rent_cap_future numeric(38,6);
IF @stock_rub_today IS NULL OR @stock_rub_today <= 0
OR @q_stock IS NULL OR @q_stock <= 0
OR @q_future IS NULL OR @q_future <= 0
OR @tn_future IS NULL
BEGIN
SET @rent_cap_future = NULL;
END
ELSE
BEGIN
DECLARE @avg_cap_future numeric(38,6);
-- считаем, что капитал уходит линейно пропорционально количеству:
-- капитал "на продажу" = stock_rub_today * (q_future / q_stock)
-- средний капитал = stock_rub_today - 0.5 * капитал_на_продажу
SET @avg_cap_future =
@stock_rub_today * (1.0 - (@q_future / @q_stock) / 2.0);
IF @avg_cap_future > 0
SET @rent_cap_future = @tn_future / @avg_cap_future;
ELSE
SET @rent_cap_future = NULL;
END;
---------------------------------------------------------
-- 11.7. Таблица будущего по месяцам (#future_plan) — как было
---------------------------------------------------------
IF OBJECT_ID('tempdb..#future_plan') IS NOT NULL DROP TABLE #future_plan;
CREATE TABLE #future_plan (
PeriodStart date,
PeriodEnd date,
DaysInPeriod int,
QtyStart numeric(18,6),
Sold numeric(18,6),
QtyEnd numeric(18,6),
PaidCapitalAvg numeric(18,6),
RubleDays numeric(18,6),
TN_period numeric(18,6),
ROIC_period numeric(38,6)
);
DECLARE
@f_start date,
@f_end date,
@days int,
@rem_qty numeric(18,6),
@qty_start numeric(18,6),
@qty_end numeric(18,6),
@sold numeric(18,6),
@capital_start numeric(18,6),
@capital_end numeric(18,6),
@capital_avg numeric(18,6),
@tn_period numeric(18,6),
@ruble_days_m numeric(18,6),
@roic_m numeric(38,6),
@cum_days int;
SET @rem_qty = ISNULL(@q_stock, 0);
SET @f_start = @today;
SET @cum_days = 0;
WHILE @cum_days < 365 AND @rem_qty > 0 AND @sales_per_day IS NOT NULL AND @sales_per_day > 0
BEGIN
-- конец периода = конец месяца, в котором f_start
SET @f_end = EOMONTH(@f_start);
SET @days = DATEDIFF(day, @f_start, @f_end) + 1;
-- не выходим за 365 дней горизонта
IF @cum_days + @days > 365
BEGIN
SET @days = 365 - @cum_days;
SET @f_end = DATEADD(day, @days - 1, @f_start);
END;
SET @qty_start = @rem_qty;
DECLARE @can_sell numeric(18,6);
SET @can_sell = @sales_per_day * @days;
SET @sold = CASE
WHEN @rem_qty <= @can_sell THEN @rem_qty
ELSE @can_sell
END;
SET @qty_end = @qty_start - @sold;
IF @q_stock IS NOT NULL AND @q_stock > 0 AND @paid_stock_today IS NOT NULL
BEGIN
SET @capital_start = @paid_stock_today * (@qty_start / @q_stock);
SET @capital_end = @paid_stock_today * (@qty_end / @q_stock);
END
ELSE
BEGIN
SET @capital_start = 0;
SET @capital_end = 0;
END;
SET @capital_avg = (@capital_start + @capital_end) / 2.0;
IF @tn_per_unit IS NOT NULL
SET @tn_period = @tn_per_unit * @sold;
ELSE
SET @tn_period = NULL;
SET @ruble_days_m = @capital_avg * @days;
IF @ruble_days_m > 0 AND @tn_period IS NOT NULL
SET @roic_m = @tn_period * 365.0 / @ruble_days_m * 100.0;
ELSE
SET @roic_m = NULL;
INSERT INTO #future_plan (
PeriodStart, PeriodEnd, DaysInPeriod,
QtyStart, Sold, QtyEnd,
PaidCapitalAvg, RubleDays, TN_period, ROIC_period
)
VALUES (
@f_start, @f_end, @days,
@qty_start, @sold, @qty_end,
@capital_avg, @ruble_days_m, @tn_period, @roic_m
);
SET @rem_qty = @qty_end;
SET @cum_days = @cum_days + @days;
SET @f_start = DATEADD(day, 1, @f_end);
IF @cum_days >= 365 BREAK;
END;
---------------------------------------------------------
-- 12. Итог: прошлое и будущее + рентабельность по остатку (руб)
---------------------------------------------------------
SELECT
@roic_past AS [ROIC за год назад],
@roic_future AS [ROIC на будущий год],
@rent_cap_year AS [Рентабельность по остатку (руб) / год назад],
@rent_cap_quarter AS [Рентабельность по остатку (руб) / квартал назад],
@rent_cap_future AS [Рентабельность по остатку (руб) / будущий год];
-- Таблица будущего по месяцам / периодам (как было)
SELECT * FROM #future_plan ORDER BY PeriodStart;
END
GO
/****** Object: StoredProcedure [analytics].[sp_run_deficit_all_skus] Script Date: 2026-02-22 12:04:15 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [analytics].[sp_run_deficit_all_skus]
@scenario_id INT
as BEGIN
DELETE FROM [analytics].[deficit_proposal] where scenario_id = @scenario_id
EXEC [analytics].[sp_build_deficit_proposal]
@scenario_id = @scenario_id,
@group_path = N'', -- пусто = все группы
@lead_time_m = 4,
@cover_months = 6,
@from_month = '2025-10-01',
@to_month_excl = '2028-01-01',
@debug = 0; -- чтобы не заспамить Messages
END
GO
/****** Object: StoredProcedure [analytics].[sp_загрузка_прогнозаакупки] Script Date: 2026-02-22 12:04:15 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
/****** Script for SelectTopNRows command from SSMS ******/
CREATE PROCEDURE [analytics].[sp_загрузка_прогнозаакупки] AS BEGIN
SET NOCOUNT ON;
IF OBJECT_ID('tempdb..#plan2026') IS NOT NULL
DROP TABLE #plan2026;
CREATE TABLE #plan2026 (
code nvarchar(50) NOT NULL,
opt decimal(18,2) NULL,
mp decimal(18,2) NULL,
updated_by nvarchar(100) NULL
);
-- уникальный индекс по коду, дубли игнорируем
CREATE UNIQUE INDEX UX_plan2026_code
ON #plan2026(code)
WITH (IGNORE_DUP_KEY = ON);
BULK INSERT #plan2026
FROM '\\192.168.35.3\admin3\Обмен\powerbi\plan2026.csv'
WITH (
FIRSTROW = 2, -- пропускаем заголовок
FIELDTERMINATOR = ';',
ROWTERMINATOR = '\n',
CODEPAGE = '1251', -- или 1251, если файл не в UTF-8
TABLOCK
);
-- Проверка
SELECT COUNT(*) AS total_rows,
COUNT(DISTINCT code) AS distinct_codes
FROM #plan2026;
-- Проверка
SELECT TOP (20) *
FROM #plan2026
SELECT
month
, avg([koef])
FROM [mag_pbi].[analytics].[seasonality_groups_summ_1]
GROUP by month
order by month
------------------------------------------------------------
-- 1. Средняя сезонность по месяцам → #Seasonality
------------------------------------------------------------
IF OBJECT_ID('tempdb..#Seasonality') IS NOT NULL
DROP TABLE #Seasonality;
SELECT
[month]
, koef = AVG([koef])
INTO #Seasonality
FROM [mag_pbi].[analytics].[seasonality_groups_summ_1]
GROUP BY [month];
------------------------------------------------------------
-- 2. Почистить сценарий 8 (если нужно пересчитать)
------------------------------------------------------------
DELETE FROM [mag_pbi].[analytics].[forecast]
WHERE scenario_id = 8;
------------------------------------------------------------
-- 3. Записать прогноз в forecast для scenario_id = 8
-- value = opt_месяц + mp_месяц
------------------------------------------------------------
INSERT INTO [mag_pbi].[analytics].[forecast] (
scenario_id
, [1c_id]
, [code]
, [month]
, [value]
, [updated_at]
, [updated_by]
, [opt]
, [mp]
)
SELECT
8 AS scenario_id
, n.[1c_id]
, p.[code]
, DATEFROMPARTS(2026, s.[month], 1) AS [month]
, CAST( (p.opt * s.koef) + (p.mp * s.koef) AS decimal(18,3)) AS [value]
, GETDATE() AS updated_at
, p.[updated_by] AS updated_by
, CAST(p.opt * s.koef AS decimal(18,3)) AS [opt]
, CAST(p.mp * s.koef AS decimal(18,3)) AS [mp]
FROM #plan2026 AS p
INNER JOIN [mag_pbi].[pbi].[nomenclature] AS n
ON n.[code] = p.[code]
CROSS JOIN #Seasonality AS s;
-- всего строк и разных кодов
--EXEC [analytics].[sp_build_deficit_proposal] @scenario_id = 8
/*
/****** Script for SelectTopNRows command from SSMS ******/
INSERT INTO [mag_pbi].[analytics].[forecast] (
scenario_id
, [1c_id]
, [code]
, [month]
, [value]
, [updated_at]
, [updated_by]
, [opt]
, [mp]
)
SELECT
8 AS scenario_id
,[1c_id]
,[code]
,[month]
,[value]
,[updated_at]
,[updated_by]
,[opt]
,[mp]
FROM [mag_pbi].[analytics].[forecast]
WHERE scenario_id = 5 AND code not in (SELECT code FROM [mag_pbi].[analytics].[forecast] WHERE scenario_id = 8 )
*/
--EXEC [analytics].[sp_fill_deficit_money_request] @scenario_id = 8
END
GO
/****** Object: StoredProcedure [analytics].[usp_CreateForecastBasesKs] Script Date: 2026-02-22 12:04:15 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE analytics.usp_CreateForecastBasesKs
AS
BEGIN
SET NOCOUNT ON;
-- Удаляем вьюху, если существует
IF OBJECT_ID('analytics.ForecastBasesKs', 'V') IS NOT NULL
DROP VIEW analytics.ForecastBasesKs;
-- Создаём заново
EXEC('
CREATE VIEW [analytics].[ForecastBasesKs] AS
WITH MaxDate AS (
SELECT MAX(Период) AS max_date
FROM [pbi].[Себестоимость]
WHERE Статья = ''реализация''
),
LastFullMonth AS (
SELECT
YEAR(DATEADD(MONTH, -1, max_date)) AS last_year,
MONTH(DATEADD(MONTH, -1, max_date)) AS last_month
FROM MaxDate
),
-- 1. Продажи по SKU × месяц
Sales AS (
SELECT
artic_id,
YEAR(Период) AS Год,
MONTH(Период) AS Месяц,
SUM(Количество) AS total_sales
FROM [pbi].[Себестоимость]
WHERE Статья = ''реализация''
GROUP BY artic_id, YEAR(Период), MONTH(Период)
),
-- 2. Дни в продаже
Stock AS (
SELECT
artic_id,
YEAR(dt) AS Год,
MONTH(dt) AS Месяц,
SUM(CASE WHEN ostatok = 1 THEN 1 ELSE 0 END) AS days_available
FROM [pbi].[w_ostatok_da_net]
GROUP BY artic_id, YEAR(dt), MONTH(dt)
),
-- 3. База
Base AS (
SELECT
s.artic_id,
n.code,
n.description,
s.Год,
s.Месяц,
s.total_sales,
st.days_available,
sg.seasonal_koef,
(s.total_sales / NULLIF(sg.seasonal_koef,0)) AS Normalized_sales,
CASE WHEN st.days_available > 19 THEN 1 ELSE 0 END AS valid_month,
CASE WHEN st.days_available > 19
THEN (s.total_sales / NULLIF(sg.seasonal_koef,0))
ELSE NULL END AS normalized_valid_sales
FROM Sales s
LEFT JOIN Stock st
ON s.artic_id = st.artic_id
AND s.Год = st.Год
AND s.Месяц = st.Месяц
JOIN [pbi].[nomenclature] n
ON s.artic_id = n.artic_id
JOIN [analytics].[seasonality_groups] sg
ON n.[1c_group] = sg.group_1c_id
AND s.Месяц = sg.month
),
Windowed AS (
SELECT
b.*,
ROW_NUMBER() OVER (PARTITION BY b.artic_id ORDER BY (b.Год*100 + b.Месяц) DESC) AS rn_desc
FROM Base b
),
Aggregates AS (
SELECT
w.artic_id,
AVG(CASE WHEN rn_desc <= 12 THEN normalized_valid_sales END) AS Base_12M,
CASE
WHEN MIN(CASE WHEN rn_desc <= 3 THEN valid_month END) = 1
THEN AVG(CASE WHEN rn_desc <= 3 THEN normalized_valid_sales END)
ELSE NULL
END AS Base_3M
FROM Windowed w
CROSS JOIN LastFullMonth lm
WHERE (w.Год*100 + w.Месяц) <= (lm.last_year*100 + lm.last_month)
GROUP BY w.artic_id
),
TrendData AS (
SELECT
w.artic_id,
ROW_NUMBER() OVER (PARTITION BY w.artic_id ORDER BY (w.Год*100 + w.Месяц)) - 1 AS t,
LOG(w.normalized_valid_sales) AS ln_sales
FROM Windowed w
CROSS JOIN LastFullMonth lm
WHERE (w.Год*100 + w.Месяц) > (lm.last_year*100 + lm.last_month - 200)
AND (w.Год*100 + w.Месяц) <= (lm.last_year*100 + lm.last_month)
AND w.normalized_valid_sales > 0
),
TrendAgg AS (
SELECT
artic_id,
COUNT(*) AS n_obs,
EXP( (AVG(t*ln_sales) - AVG(t)*AVG(ln_sales)) / NULLIF(AVG(t*t) - AVG(t)*AVG(t),0) ) AS g_raw
FROM TrendData
GROUP BY artic_id
HAVING COUNT(*) >= 6
),
TrendFinal AS (
SELECT
artic_id,
POWER(
CASE
WHEN POWER(g_raw, 12) < 0.7 THEN 0.7
WHEN POWER(g_raw, 12) > 1.5 THEN 1.5
ELSE POWER(g_raw, 12)
END,
1.0/12
) AS Ktrend
FROM TrendAgg
),
YoYpairs AS (
SELECT
w.artic_id,
(w.Год*100 + w.Месяц) AS ym,
w.normalized_valid_sales AS v2,
LAG(w.normalized_valid_sales, 12) OVER (
PARTITION BY w.artic_id ORDER BY (w.Год*100 + w.Месяц)
) AS v1
FROM Windowed w
CROSS JOIN LastFullMonth lm
WHERE (w.Год*100 + w.Месяц) <= (lm.last_year*100 + lm.last_month)
),
YoYratios AS (
SELECT artic_id, (v2 / v1) AS ratio
FROM YoYpairs
WHERE v1 > 0 AND v2 > 0
),
YoYFinal AS (
SELECT DISTINCT
artic_id,
CASE
WHEN PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY ratio)
OVER (PARTITION BY artic_id) < 0.7 THEN 0.7
WHEN PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY ratio)
OVER (PARTITION BY artic_id) > 1.5 THEN 1.5
ELSE PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY ratio)
OVER (PARTITION BY artic_id)
END AS Kgrowth_YoY,
COUNT(ratio) OVER (PARTITION BY artic_id) AS cnt_pairs
FROM YoYratios
),
YoYFiltered AS (
SELECT artic_id, Kgrowth_YoY
FROM YoYFinal
WHERE cnt_pairs >= 3
)
SELECT
n.artic_id,
n.code,
n.description,
a.Base_12M,
a.Base_3M,
CASE
WHEN a.Base_3M IS NOT NULL THEN a.Base_3M
ELSE a.Base_12M
END AS Base_Selected,
tf.Ktrend,
yf.Kgrowth_YoY
FROM pbi.nomenclature n
LEFT JOIN Aggregates a ON n.artic_id = a.artic_id
LEFT JOIN TrendFinal tf ON n.artic_id = tf.artic_id
LEFT JOIN YoYFiltered yf ON n.artic_id = yf.artic_id;
');
END;
GO
/****** Object: StoredProcedure [analytics].[usp_InsertForecasts] Script Date: 2026-02-22 12:04:15 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE analytics.usp_InsertForecasts
AS
BEGIN
SET NOCOUNT ON;
-- Удаляем старые прогнозы
DELETE FROM analytics.forecast
WHERE scenario_id IN (5, 6);
-- Чистим временные таблицы
IF OBJECT_ID('tempdb..#Params') IS NOT NULL DROP TABLE #Params;
IF OBJECT_ID('tempdb..#Calendar') IS NOT NULL DROP TABLE #Calendar;
-- #Params
SELECT DATEFROMPARTS(
YEAR(DATEADD(MONTH, -1, MAX(Период))),
MONTH(DATEADD(MONTH, -1, MAX(Период))),
1
) AS LastMonth
INTO #Params
FROM pbi.Себестоимость
WHERE Статья = 'реализация';
-- #Calendar
SELECT DATEFROMPARTS(YEAR(d), MONTH(d), 1) AS MonthStart
INTO #Calendar
FROM (
SELECT TOP (1000)
DATEADD(MONTH, ROW_NUMBER() OVER (ORDER BY (SELECT NULL)),
(SELECT LastMonth FROM #Params)) AS d
FROM master..spt_values
) x
WHERE d <= '2026-12-01';
-- Вставка Trend (5)
INSERT INTO analytics.forecast (scenario_id, [1c_id], code, [month], value, updated_at, updated_by)
SELECT
5,
n.[1c_id],
f.code,
c.MonthStart,
CAST(
ROUND(
f.Base_Selected * sg.seasonal_koef *
POWER(f.Ktrend, DATEDIFF(MONTH, p.LastMonth, c.MonthStart)),
0
) AS numeric(18,0)
),
GETDATE(),
SUSER_SNAME()
FROM analytics.ForecastBasesKs f
JOIN pbi.nomenclature n ON f.artic_id = n.artic_id
CROSS JOIN #Params p
JOIN #Calendar c ON c.MonthStart > p.LastMonth
JOIN analytics.seasonality_groups sg
ON n.[1c_group] = sg.group_1c_id
AND sg.[month] = MONTH(c.MonthStart)
WHERE f.Base_Selected IS NOT NULL
AND f.Ktrend IS NOT NULL;
-- Вставка YoY (6)
INSERT INTO analytics.forecast (scenario_id, [1c_id], code, [month], value, updated_at, updated_by)
SELECT
6,
n.[1c_id],
f.code,
c.MonthStart,
CAST(
ROUND(
f.Base_Selected * sg.seasonal_koef * f.Kgrowth_YoY,
0
) AS numeric(18,0)
),
GETDATE(),
SUSER_SNAME()
FROM analytics.ForecastBasesKs f
JOIN pbi.nomenclature n ON f.artic_id = n.artic_id
CROSS JOIN #Params p
JOIN #Calendar c ON c.MonthStart > p.LastMonth
JOIN analytics.seasonality_groups sg
ON n.[1c_group] = sg.group_1c_id
AND sg.[month] = MONTH(c.MonthStart)
WHERE f.Base_Selected IS NOT NULL
AND f.Kgrowth_YoY IS NOT NULL;
END;
GO
/****** Object: StoredProcedure [analytics].[Подготовка таблицы продаж к прогнозу] Script Date: 2026-02-22 12:04:15 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE analytics.[Подготовка таблицы продаж к прогнозу] as
BEGIN
/* ========= ПАРАМЕТРЫ ========= */
DECLARE @FromDate date = '2023-01-01';
DECLARE @ToDate date = CAST(GETDATE() AS date);
/* ========= ВСПОМОГАТЕЛЬНЫЕ: НЕДЕЛЯ С ПОНЕДЕЛЬНИКА ========= */
IF OBJECT_ID('tempdb..#dates') IS NOT NULL DROP TABLE #dates;
WITH d AS (
SELECT @FromDate AS d
UNION ALL SELECT DATEADD(day,1,d) FROM d WHERE d < @ToDate
)
SELECT d AS [date],
DATEADD(day, - (DATEPART(weekday,d) + @@DATEFIRST + 5) % 7, d) AS week_start
INTO #dates
FROM d
OPTION (MAXRECURSION 0);
CREATE INDEX IX_dates_week ON #dates(week_start);
/* ========= 1) ПРОДАЖИ (только положительные) ========= */
IF OBJECT_ID('tempdb..#sales_raw') IS NOT NULL DROP TABLE #sales_raw;
SELECT
CAST(s.[Период] AS date) AS [date],
s.[1c_id] AS sku_id,
CAST(s.[КоличествоУпаковок] AS decimal(18,4)) AS qty
INTO #sales_raw
FROM [pbiProd].[СводныйСебестоимость Для PBI] s
WHERE
s.[Статья] = N'Реализация'
AND s.[Вид операции] = N'Расход'
AND s.[КоличествоУпаковок] > 0
AND s.[Период] >= @FromDate
AND s.[Период] <= @ToDate;
CREATE INDEX IX_sales_raw ON #sales_raw(sku_id, [date]);
/* ========= 2) ФИЛЬТРУЕМ ПРОДАЖИ ПО НАЛИЧИЮ (ostatok=1 из pbi.w_ostatok_da_net) ========= */
IF OBJECT_ID('tempdb..#sales_clean_daily') IS NOT NULL DROP TABLE #sales_clean_daily;
SELECT
sr.sku_id,
sr.[date],
sr.qty
INTO #sales_clean_daily
FROM #sales_raw sr
JOIN [pbi].[w_ostatok_da_net] a
ON a.[_IDRREF] = sr.sku_id
AND a.dt = sr.[date]
AND a.ostatok = 1;
CREATE INDEX IX_sales_clean_daily ON #sales_clean_daily(sku_id, [date]);
/* ========= 3) АГРЕГАЦИЯ В НЕДЕЛИ ========= */
/* 3.1 Продажи по неделям */
IF OBJECT_ID('tempdb..#sales_weekly') IS NOT NULL DROP TABLE #sales_weekly;
SELECT
scd.sku_id,
d.week_start,
SUM(scd.qty) AS qty_week
INTO #sales_weekly
FROM #sales_clean_daily scd
JOIN #dates d
ON d.[date] = scd.[date]
GROUP BY scd.sku_id, d.week_start;
CREATE INDEX IX_sales_weekly ON #sales_weekly(sku_id, week_start);
/* 3.2 Доля доступности по неделям (из pbi.w_ostatok_da_net) */
IF OBJECT_ID('tempdb..#avail_weekly') IS NOT NULL DROP TABLE #avail_weekly;
SELECT
a.[_IDRREF] AS sku_id,
d.week_start,
SUM(a.ostatok) AS days_available,
COUNT(*) AS days_total,
CAST(SUM(a.ostatok) * 1.0 / NULLIF(COUNT(*),0) AS decimal(6,4)) AS availability_rate
INTO #avail_weekly
FROM [pbi].[w_ostatok_da_net] a
JOIN #dates d
ON d.[date] = a.dt
WHERE a.dt BETWEEN @FromDate AND @ToDate
GROUP BY a.[_IDRREF], d.week_start;
CREATE INDEX IX_avail_weekly ON #avail_weekly(sku_id, week_start);
/* ========= 4) СКЛЕЙКА: оставляем недели без продаж, если товар был доступен ========= */
IF OBJECT_ID('tempdb..#weekly_base') IS NOT NULL DROP TABLE #weekly_base;
SELECT
COALESCE(sw.sku_id, aw.sku_id) AS sku_id,
COALESCE(sw.week_start, aw.week_start) AS week_start,
COALESCE(sw.qty_week, 0) AS qty_week,
COALESCE(aw.availability_rate, 0) AS availability_rate
INTO #weekly_base
FROM #sales_weekly sw
FULL JOIN #avail_weekly aw
ON aw.sku_id = sw.sku_id
AND aw.week_start = sw.week_start;
CREATE INDEX IX_weekly_base ON #weekly_base(sku_id, week_start);
/* ========= 5) МЕТАДАННЫЕ SKU (cat L1, minAvailableQty) ========= */
IF OBJECT_ID('tempdb..#sku_meta') IS NOT NULL DROP TABLE #sku_meta;
SELECT
n.[1c_id] AS sku_id,
n.minAvailableQty,
CAST(CASE
WHEN CHARINDEX(N' | ', g.path) > 0
THEN LEFT(g.path, CHARINDEX(N' | ', g.path) - 1)
ELSE g.path
END AS nvarchar(200)) AS category_l1
INTO #sku_meta
FROM [mag_pbi].[pbi].[nomenclature] n
LEFT JOIN [mag_pbi].[pbi].[groups] g
ON g.group_id = n.group_id;
CREATE INDEX IX_sku_meta ON #sku_meta(sku_id);
/* ========= 6) ВИТРИНА С ЛАГАМИ И ФИЧАМИ (analytics) ========= */
IF OBJECT_ID('analytics.sales_weekly_features', 'U') IS NOT NULL
DROP TABLE analytics.sales_weekly_features;
CREATE TABLE analytics.sales_weekly_features (
sku_id varbinary(16) NOT NULL,
week_start date NOT NULL,
qty_week decimal(18,4) NOT NULL,
availability_rate decimal(6,4) NOT NULL,
-- календарные
year_num int NOT NULL,
month_num tinyint NOT NULL,
iso_week tinyint NOT NULL,
quarter_num tinyint NOT NULL,
dow_monday1 tinyint NOT NULL,
-- справочники
category_l1 nvarchar(200) NULL,
minAvailableQty decimal(18,4) NULL,
-- лаги
lag_w1 decimal(18,4) NULL,
lag_w2 decimal(18,4) NULL,
lag_w4 decimal(18,4) NULL,
lag_w12 decimal(18,4) NULL,
lag_w26 decimal(18,4) NULL,
lag_w52 decimal(18,4) NULL,
-- скользящие средние
ma4 decimal(18,4) NULL,
ma12 decimal(18,4) NULL,
created_at datetime2 NOT NULL DEFAULT sysutcdatetime(),
CONSTRAINT PK_sales_weekly_features PRIMARY KEY (sku_id, week_start)
);
WITH base AS (
SELECT wb.sku_id,
wb.week_start,
wb.qty_week,
wb.availability_rate,
YEAR(wb.week_start) AS year_num,
MONTH(wb.week_start) AS month_num,
DATEPART(ISO_WEEK, wb.week_start) AS iso_week,
DATEPART(QUARTER, wb.week_start) AS quarter_num,
1 AS dow_monday1
FROM #weekly_base wb
),
enriched AS (
SELECT b.*,
m.category_l1,
m.minAvailableQty
FROM base b
LEFT JOIN #sku_meta m
ON m.sku_id = b.sku_id
),
lags AS (
SELECT
e.*,
LAG(e.qty_week, 1) OVER (PARTITION BY e.sku_id ORDER BY e.week_start) AS lag_w1,
LAG(e.qty_week, 2) OVER (PARTITION BY e.sku_id ORDER BY e.week_start) AS lag_w2,
LAG(e.qty_week, 4) OVER (PARTITION BY e.sku_id ORDER BY e.week_start) AS lag_w4,
LAG(e.qty_week, 12) OVER (PARTITION BY e.sku_id ORDER BY e.week_start) AS lag_w12,
LAG(e.qty_week, 26) OVER (PARTITION BY e.sku_id ORDER BY e.week_start) AS lag_w26,
LAG(e.qty_week, 52) OVER (PARTITION BY e.sku_id ORDER BY e.week_start) AS lag_w52,
CAST(AVG(e.qty_week) OVER (
PARTITION BY e.sku_id
ORDER BY e.week_start
ROWS BETWEEN 4 PRECEDING AND 1 PRECEDING
) AS decimal(18,4)) AS ma4,
CAST(AVG(e.qty_week) OVER (
PARTITION BY e.sku_id
ORDER BY e.week_start
ROWS BETWEEN 12 PRECEDING AND 1 PRECEDING
) AS decimal(18,4)) AS ma12
FROM enriched e
)
INSERT INTO analytics.sales_weekly_features (
sku_id, week_start, qty_week, availability_rate,
year_num, month_num, iso_week, quarter_num, dow_monday1,
category_l1, minAvailableQty,
lag_w1, lag_w2, lag_w4, lag_w12, lag_w26, lag_w52,
ma4, ma12
)
SELECT
sku_id, week_start, qty_week, availability_rate,
year_num, month_num, iso_week, quarter_num, dow_monday1,
category_l1, minAvailableQty,
lag_w1, lag_w2, lag_w4, lag_w12, lag_w26, lag_w52,
ma4, ma12
FROM lags;
CREATE INDEX IX_sales_weekly_features_sku ON analytics.sales_weekly_features(sku_id, week_start);
CREATE INDEX IX_sales_weekly_features_cat ON analytics.sales_weekly_features(category_l1, week_start);
END
GO