fix(form2): лимиты АХР — годовые нормативы делятся на 4, сумма без НДС, тыс. руб. в АХР
AURORA-1897: лист «АХР лимиты» формы 2 дублировал годовой норматив ДМС/оргтехники в каждом квартале вместо деления на 4, считал сумму от лимита с НДС (смета ведётся без НДС) и переносил рубли в план АХР, где всё остальное в тыс. руб. Миграция 0052_form2_ahr_limit_period_vat_units: - v3.limit_template.period (YEAR/QUARTER/MONTH) + точечные правки справочника (id 24/25 ДМС и id 13-15 принтеры/МФУ/копиры — YEAR; id 7/8 SIM-карты — заполнен limit_without_vat; id 21 — unit по аналогии с бумагой). - v3._form2_ahr_limit_amount(period, limit, qty) — рубли без НДС, без проверки по названию; старая 4-арг. сигнатура удалена. - v3.v_form2_view (ветка AHR_LIMIT), v3.sync_form2_ahr_satellite_lines(_line) пересозданы: amount/plan считаются от limit_without_vat, план АХР делится на 1000, добавлены amount_year и строка «Итого» (id=0, row_type ROOT). - v3._limit_template_path_set дописывает id=0 в path-set, чтобы «Итого» попадала в diff PATCH-ответа. - Пересинхронизированы все существующие формы 2. Ветки аренды/коммуналки/охраны и add_budget_line (AURORA-1887/1909) не тронуты. Тесты: api/tests/integration/test_form2_ahr_limit.py (7 тестов, числа из тикета: id24 qty10→69645/69.645, id3→2000/2.0, id7→7500/7.5, id13→6471.875/ 6.471875, id17→7500/7.5, «Итого», регрессия АХР_RENT без деления). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
91cbe99c32
commit
ab765f028d
@ -0,0 +1,15 @@
|
||||
from sql_migration import run_sql_file_migration
|
||||
|
||||
|
||||
revision = "0052"
|
||||
down_revision = "0051"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
run_sql_file_migration("0052_form2_ahr_limit_period_vat_units.sql")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
@ -0,0 +1,914 @@
|
||||
-- AURORA-1897: лист «АХР лимиты» формы 2.
|
||||
-- 1) годовые нормативы (ДМС/оргтехника) делятся на 4 квартала, а не
|
||||
-- дублируются в каждом квартале;
|
||||
-- 2) сумма на листе лимитов и в плане АХР считается от лимита БЕЗ НДС;
|
||||
-- 3) план лимитных строк в АХР переводится в тыс. руб. (как аренда/
|
||||
-- коммуналка/охрана/Смета), а не остаётся в рублях.
|
||||
--
|
||||
-- CREATE OR REPLACE для v3.v_form2_view, v3.sync_form2_ahr_satellite_lines и
|
||||
-- v3.sync_form2_ahr_satellite_line ниже воспроизводят тело функций, актуальное
|
||||
-- на голове миграций этой ветки (0051): сами функции последний раз целиком
|
||||
-- переопределены в 0050_form2_ahr_spod_column.sql (ради колонки СПОД),
|
||||
-- 0051_form2_ahr_satellite_new_line.sql их не трогает (проверено grep —
|
||||
-- он редактирует только v3.add_budget_line). Правится только ветка
|
||||
-- AHR_LIMIT; ветки AHR/CAP/OPER/AHR_RENT/AHR_UTILITY/AHR_SECURITY/OTCH9F/SMETA
|
||||
-- и переносы аренды/коммуналки/охраны копируются из 0050 без изменений.
|
||||
|
||||
|
||||
-- 1. Справочник: period — YEAR / QUARTER / MONTH, по умолчанию QUARTER.
|
||||
ALTER TABLE v3.limit_template
|
||||
ADD COLUMN IF NOT EXISTS period varchar NOT NULL DEFAULT 'QUARTER';
|
||||
|
||||
ALTER TABLE v3.limit_template
|
||||
ADD CONSTRAINT ck_v3_limit_template_period CHECK (period IN ('YEAR','QUARTER','MONTH'));
|
||||
|
||||
-- Backfill по тому же правилу, что раньше зашивал фактор прямо в формулу.
|
||||
UPDATE v3.limit_template SET period = 'YEAR' WHERE unit LIKE '%/год';
|
||||
UPDATE v3.limit_template SET period = 'MONTH' WHERE unit LIKE '%/месяц';
|
||||
|
||||
|
||||
-- 2. Точечные правки справочника (см. разбор в тикете AURORA-1897).
|
||||
DO $do$
|
||||
BEGIN
|
||||
UPDATE v3.limit_template
|
||||
SET unit = '1 чел/год', period = 'YEAR'
|
||||
WHERE id = 24 AND name = '- стандарт РФ';
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'limit_template.id=24 (- стандарт РФ) не найден';
|
||||
END IF;
|
||||
|
||||
UPDATE v3.limit_template
|
||||
SET unit = '1 чел/год', period = 'YEAR'
|
||||
WHERE id = 25 AND name = '- стандарт 1 РФ';
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'limit_template.id=25 (- стандарт 1 РФ) не найден';
|
||||
END IF;
|
||||
|
||||
UPDATE v3.limit_template
|
||||
SET unit = '1 шт/год', period = 'YEAR',
|
||||
limit_with_vat = 6213, limit_without_vat = 5177.5
|
||||
WHERE id = 13 AND name = '- принтеры';
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'limit_template.id=13 (- принтеры) не найден';
|
||||
END IF;
|
||||
|
||||
UPDATE v3.limit_template
|
||||
SET unit = '1 шт/год', period = 'YEAR',
|
||||
limit_with_vat = 6213, limit_without_vat = 5177.5
|
||||
WHERE id = 14 AND name = '- МФУ';
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'limit_template.id=14 (- МФУ) не найден';
|
||||
END IF;
|
||||
|
||||
UPDATE v3.limit_template
|
||||
SET unit = '1 шт/год', period = 'YEAR',
|
||||
limit_with_vat = 6213, limit_without_vat = 5177.5
|
||||
WHERE id = 15 AND name = '- копировальные аппараты';
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'limit_template.id=15 (- копировальные аппараты) не найден';
|
||||
END IF;
|
||||
|
||||
UPDATE v3.limit_template
|
||||
SET unit = '1 чел/квартал'
|
||||
WHERE id = 21 AND name = '- Канцелярские принадлежности';
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'limit_template.id=21 (- Канцелярские принадлежности) не найден';
|
||||
END IF;
|
||||
|
||||
UPDATE v3.limit_template
|
||||
SET limit_without_vat = 375
|
||||
WHERE id = 7 AND name = '- SIM-карта для планшета (легкий фронт)';
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'limit_template.id=7 (SIM-карта для планшета) не найден';
|
||||
END IF;
|
||||
|
||||
UPDATE v3.limit_template
|
||||
SET limit_without_vat = 375
|
||||
WHERE id = 8 AND name = '- SIM-карта для уполномоченного работника';
|
||||
IF NOT FOUND THEN
|
||||
RAISE EXCEPTION 'limit_template.id=8 (SIM-карта для уполномоченного работника) не найден';
|
||||
END IF;
|
||||
END;
|
||||
$do$;
|
||||
|
||||
|
||||
-- 3. Новая формула: рубли без НДС, период вместо regexp по названию unit.
|
||||
CREATE OR REPLACE FUNCTION v3._form2_ahr_limit_amount(
|
||||
p_period character varying,
|
||||
p_limit numeric,
|
||||
p_qty numeric
|
||||
)
|
||||
RETURNS numeric
|
||||
LANGUAGE sql
|
||||
IMMUTABLE
|
||||
AS $function$
|
||||
SELECT COALESCE(p_qty, 0) * COALESCE(p_limit, 0) *
|
||||
CASE p_period
|
||||
WHEN 'YEAR' THEN 0.25
|
||||
WHEN 'MONTH' THEN 3
|
||||
ELSE 1
|
||||
END;
|
||||
$function$;
|
||||
|
||||
|
||||
-- 4а. v3.v_form2_view: ветка AHR_LIMIT — amount от limit_without_vat,
|
||||
-- amount_year, period в data, последняя строка «Итого» (row_type ROOT).
|
||||
CREATE OR REPLACE FUNCTION v3.v_form2_view(
|
||||
p_form_id INT,
|
||||
p_sheet VARCHAR,
|
||||
p_sections TEXT[] DEFAULT NULL
|
||||
)
|
||||
RETURNS TABLE (
|
||||
row_type VARCHAR,
|
||||
depth INT,
|
||||
sort_order BIGINT,
|
||||
data JSONB
|
||||
)
|
||||
AS $function$
|
||||
BEGIN
|
||||
|
||||
|
||||
IF p_sheet IN ('AHR','CAP','OPER') THEN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
j.row_type,
|
||||
j.depth,
|
||||
ROW_NUMBER() OVER (ORDER BY j._sort_path) AS sort_order,
|
||||
jsonb_build_object(
|
||||
'line_id', j.line_id,
|
||||
'header', j.header,
|
||||
'plan', j.plan_data,
|
||||
'seq_dfip', j.seq_dfip_data,
|
||||
'seq_ssp', j.seq_ssp_data,
|
||||
'approved', j.approved_data,
|
||||
'contract', j.contract_data,
|
||||
'booking', j.booking_data,
|
||||
'q1', j.q1_data,
|
||||
'q2', j.q2_data,
|
||||
'q3', j.q3_data,
|
||||
'q4', j.q4_data,
|
||||
'totals', j.totals_data
|
||||
) AS data
|
||||
FROM v3.v_form2_sheet_jsonb(p_form_id, p_sheet, p_sections) j
|
||||
ORDER BY j._sort_path;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
|
||||
IF p_sheet = 'AHR_LIMIT' THEN
|
||||
RETURN QUERY
|
||||
WITH RECURSIVE tree AS (
|
||||
SELECT id AS root_id, id AS desc_id FROM v3.limit_template
|
||||
UNION
|
||||
SELECT t.root_id, child.id
|
||||
FROM tree t
|
||||
JOIN v3.limit_template child ON child.parent_id = t.desc_id
|
||||
),
|
||||
leaf_amt AS (
|
||||
|
||||
SELECT lt.id,
|
||||
fl.qty_q1, fl.qty_q2, fl.qty_q3, fl.qty_q4,
|
||||
v3._form2_ahr_limit_amount(lt.period, lt.limit_without_vat, fl.qty_q1) AS amount_q1,
|
||||
v3._form2_ahr_limit_amount(lt.period, lt.limit_without_vat, fl.qty_q2) AS amount_q2,
|
||||
v3._form2_ahr_limit_amount(lt.period, lt.limit_without_vat, fl.qty_q3) AS amount_q3,
|
||||
v3._form2_ahr_limit_amount(lt.period, lt.limit_without_vat, fl.qty_q4) AS amount_q4
|
||||
FROM v3.limit_template lt
|
||||
LEFT JOIN v3.form_limit fl
|
||||
ON fl.template_id = lt.id AND fl.budget_form_id = p_form_id
|
||||
WHERE lt.row_type = 'LEAF'
|
||||
),
|
||||
leaf_amt_year AS (
|
||||
SELECT la.*,
|
||||
(COALESCE(la.amount_q1,0)+COALESCE(la.amount_q2,0)
|
||||
+COALESCE(la.amount_q3,0)+COALESCE(la.amount_q4,0)) AS amount_year
|
||||
FROM leaf_amt la
|
||||
),
|
||||
agg AS (
|
||||
SELECT t.root_id,
|
||||
SUM(la.qty_q1) AS qty_q1, SUM(la.amount_q1) AS amount_q1,
|
||||
SUM(la.qty_q2) AS qty_q2, SUM(la.amount_q2) AS amount_q2,
|
||||
SUM(la.qty_q3) AS qty_q3, SUM(la.amount_q3) AS amount_q3,
|
||||
SUM(la.qty_q4) AS qty_q4, SUM(la.amount_q4) AS amount_q4,
|
||||
SUM(la.amount_year) AS amount_year
|
||||
FROM tree t
|
||||
JOIN leaf_amt_year la ON la.id = t.desc_id
|
||||
GROUP BY t.root_id
|
||||
),
|
||||
rows AS (
|
||||
SELECT
|
||||
|
||||
CASE lt.row_type WHEN 'SECTION' THEN 'ROOT'
|
||||
WHEN 'GROUP' THEN 'GROUP'
|
||||
WHEN 'LEAF' THEN 'INPUT' END::VARCHAR AS row_type,
|
||||
CASE lt.row_type WHEN 'SECTION' THEN 0 WHEN 'GROUP' THEN 1 ELSE 2 END AS depth,
|
||||
lt.sort_order::BIGINT AS sort_order,
|
||||
jsonb_build_object(
|
||||
'id', lt.id,
|
||||
'section_no', lt.section_no,
|
||||
'expense_item_code',lt.expense_item_code,
|
||||
'name', lt.name,
|
||||
'unit', lt.unit,
|
||||
'period', lt.period,
|
||||
'limit_with_vat', lt.limit_with_vat,
|
||||
'limit_without_vat',lt.limit_without_vat,
|
||||
'qty_q1', a.qty_q1, 'amount_q1', a.amount_q1,
|
||||
'qty_q2', a.qty_q2, 'amount_q2', a.amount_q2,
|
||||
'qty_q3', a.qty_q3, 'amount_q3', a.amount_q3,
|
||||
'qty_q4', a.qty_q4, 'amount_q4', a.amount_q4,
|
||||
'amount_year', a.amount_year,
|
||||
'comment', fl.comment
|
||||
) AS data
|
||||
FROM v3.limit_template lt
|
||||
LEFT JOIN agg a ON a.root_id = lt.id
|
||||
LEFT JOIN v3.form_limit fl
|
||||
ON fl.template_id = lt.id AND fl.budget_form_id = p_form_id
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT
|
||||
'ROOT'::VARCHAR AS row_type,
|
||||
0 AS depth,
|
||||
999999::BIGINT AS sort_order,
|
||||
jsonb_build_object(
|
||||
'id', 0,
|
||||
'name', 'Итого',
|
||||
'amount_q1', totals.amount_q1,
|
||||
'amount_q2', totals.amount_q2,
|
||||
'amount_q3', totals.amount_q3,
|
||||
'amount_q4', totals.amount_q4,
|
||||
'amount_year', totals.amount_year
|
||||
) AS data
|
||||
FROM (
|
||||
SELECT
|
||||
SUM(la.amount_q1) AS amount_q1,
|
||||
SUM(la.amount_q2) AS amount_q2,
|
||||
SUM(la.amount_q3) AS amount_q3,
|
||||
SUM(la.amount_q4) AS amount_q4,
|
||||
SUM(la.amount_year) AS amount_year
|
||||
FROM leaf_amt_year la
|
||||
) totals
|
||||
)
|
||||
SELECT rows.row_type, rows.depth, rows.sort_order, rows.data
|
||||
FROM rows
|
||||
ORDER BY rows.sort_order;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
|
||||
IF p_sheet = 'AHR_RENT' THEN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
'INPUT'::VARCHAR,
|
||||
0,
|
||||
ROW_NUMBER() OVER (ORDER BY rd.id) AS sort_order,
|
||||
jsonb_build_object(
|
||||
'id', rd.id,
|
||||
'line_id', rd.line_id,
|
||||
'vsp_id', rd.vsp_id,
|
||||
'address', v.address,
|
||||
'object_type', v.format,
|
||||
'rented_area', v.total_area,
|
||||
'contract_number', rd.contract_number,
|
||||
'contract_end_date',rd.contract_end_date,
|
||||
'plan', jsonb_build_object('q1', rd.plan_q1, 'q2', rd.plan_q2, 'q3', rd.plan_q3, 'q4', rd.plan_q4,
|
||||
'year', COALESCE(rd.plan_q1,0)+COALESCE(rd.plan_q2,0)+COALESCE(rd.plan_q3,0)+COALESCE(rd.plan_q4,0)),
|
||||
'fact_q1', jsonb_build_object('jan', rd.actual_jan, 'feb', rd.actual_feb, 'mar', rd.actual_mar,
|
||||
'total', COALESCE(rd.actual_jan,0)+COALESCE(rd.actual_feb,0)+COALESCE(rd.actual_mar,0)),
|
||||
'fact_q2', jsonb_build_object('apr', rd.actual_apr, 'may', rd.actual_may, 'jun', rd.actual_jun,
|
||||
'total', COALESCE(rd.actual_apr,0)+COALESCE(rd.actual_may,0)+COALESCE(rd.actual_jun,0)),
|
||||
'fact_q3', jsonb_build_object('jul', rd.actual_jul, 'aug', rd.actual_aug, 'sep', rd.actual_sep,
|
||||
'total', COALESCE(rd.actual_jul,0)+COALESCE(rd.actual_aug,0)+COALESCE(rd.actual_sep,0)),
|
||||
'fact_q4', jsonb_build_object('oct', rd.actual_oct, 'nov', rd.actual_nov, 'dec', rd.actual_dec, 'spod', rd.actual_spod,
|
||||
'total', COALESCE(rd.actual_oct,0)+COALESCE(rd.actual_nov,0)+COALESCE(rd.actual_dec,0)+COALESCE(rd.actual_spod,0)),
|
||||
'comment', rd.comment
|
||||
)
|
||||
FROM v3.rent_detail rd
|
||||
JOIN v3.budget_line bl ON bl.id = rd.line_id
|
||||
JOIN v3.vsp v ON v.id = rd.vsp_id
|
||||
WHERE bl.budget_form_id = p_form_id
|
||||
ORDER BY rd.id;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
|
||||
IF p_sheet = 'AHR_UTILITY' THEN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
'INPUT'::VARCHAR,
|
||||
0,
|
||||
ROW_NUMBER() OVER (ORDER BY ud.id) AS sort_order,
|
||||
jsonb_build_object(
|
||||
'id', ud.id,
|
||||
'line_id', ud.line_id,
|
||||
'vsp_id', ud.vsp_id,
|
||||
'address', v.address,
|
||||
'object_type', v.format,
|
||||
'object_area', v.total_area,
|
||||
'contract_number', ud.contract_number,
|
||||
'contract_end_date',ud.contract_end_date,
|
||||
'plan', jsonb_build_object('q1', ud.plan_q1, 'q2', ud.plan_q2, 'q3', ud.plan_q3, 'q4', ud.plan_q4,
|
||||
'year', COALESCE(ud.plan_q1,0)+COALESCE(ud.plan_q2,0)+COALESCE(ud.plan_q3,0)+COALESCE(ud.plan_q4,0)),
|
||||
'fact_q1', jsonb_build_object('jan', ud.actual_jan, 'feb', ud.actual_feb, 'mar', ud.actual_mar,
|
||||
'total', COALESCE(ud.actual_jan,0)+COALESCE(ud.actual_feb,0)+COALESCE(ud.actual_mar,0)),
|
||||
'fact_q2', jsonb_build_object('apr', ud.actual_apr, 'may', ud.actual_may, 'jun', ud.actual_jun,
|
||||
'total', COALESCE(ud.actual_apr,0)+COALESCE(ud.actual_may,0)+COALESCE(ud.actual_jun,0)),
|
||||
'fact_q3', jsonb_build_object('jul', ud.actual_jul, 'aug', ud.actual_aug, 'sep', ud.actual_sep,
|
||||
'total', COALESCE(ud.actual_jul,0)+COALESCE(ud.actual_aug,0)+COALESCE(ud.actual_sep,0)),
|
||||
'fact_q4', jsonb_build_object('oct', ud.actual_oct, 'nov', ud.actual_nov, 'dec', ud.actual_dec, 'spod', ud.actual_spod,
|
||||
'total', COALESCE(ud.actual_oct,0)+COALESCE(ud.actual_nov,0)+COALESCE(ud.actual_dec,0)+COALESCE(ud.actual_spod,0)),
|
||||
'comment', ud.comment
|
||||
)
|
||||
FROM v3.utility_detail ud
|
||||
JOIN v3.budget_line bl ON bl.id = ud.line_id
|
||||
JOIN v3.vsp v ON v.id = ud.vsp_id
|
||||
WHERE bl.budget_form_id = p_form_id
|
||||
ORDER BY ud.id;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
|
||||
IF p_sheet = 'AHR_SECURITY' THEN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
'INPUT'::VARCHAR,
|
||||
0,
|
||||
ROW_NUMBER() OVER (ORDER BY sd.id) AS sort_order,
|
||||
jsonb_build_object(
|
||||
'id', sd.id,
|
||||
'line_id', sd.line_id,
|
||||
'vsp_id', sd.vsp_id,
|
||||
'address', v.address,
|
||||
'object_type', v.format,
|
||||
'object_area', v.total_area,
|
||||
'contract_number', sd.contract_number,
|
||||
'contract_end_date',sd.contract_end_date,
|
||||
'plan', jsonb_build_object('q1', sd.plan_q1, 'q2', sd.plan_q2, 'q3', sd.plan_q3, 'q4', sd.plan_q4,
|
||||
'year', COALESCE(sd.plan_q1,0)+COALESCE(sd.plan_q2,0)+COALESCE(sd.plan_q3,0)+COALESCE(sd.plan_q4,0)),
|
||||
'fact_q1', jsonb_build_object('jan', sd.actual_jan, 'feb', sd.actual_feb, 'mar', sd.actual_mar,
|
||||
'total', COALESCE(sd.actual_jan,0)+COALESCE(sd.actual_feb,0)+COALESCE(sd.actual_mar,0)),
|
||||
'fact_q2', jsonb_build_object('apr', sd.actual_apr, 'may', sd.actual_may, 'jun', sd.actual_jun,
|
||||
'total', COALESCE(sd.actual_apr,0)+COALESCE(sd.actual_may,0)+COALESCE(sd.actual_jun,0)),
|
||||
'fact_q3', jsonb_build_object('jul', sd.actual_jul, 'aug', sd.actual_aug, 'sep', sd.actual_sep,
|
||||
'total', COALESCE(sd.actual_jul,0)+COALESCE(sd.actual_aug,0)+COALESCE(sd.actual_sep,0)),
|
||||
'fact_q4', jsonb_build_object('oct', sd.actual_oct, 'nov', sd.actual_nov, 'dec', sd.actual_dec, 'spod', sd.actual_spod,
|
||||
'total', COALESCE(sd.actual_oct,0)+COALESCE(sd.actual_nov,0)+COALESCE(sd.actual_dec,0)+COALESCE(sd.actual_spod,0)),
|
||||
'comment', sd.comment
|
||||
)
|
||||
FROM v3.security_detail sd
|
||||
JOIN v3.budget_line bl ON bl.id = sd.line_id
|
||||
JOIN v3.vsp v ON v.id = sd.vsp_id
|
||||
WHERE bl.budget_form_id = p_form_id
|
||||
ORDER BY sd.id;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
|
||||
IF p_sheet = 'OTCH9F' THEN
|
||||
RETURN QUERY
|
||||
WITH base AS (
|
||||
SELECT far.id, far.budget_form_id, far.expense_item_id,
|
||||
far.equipment_name, far.month,
|
||||
far.acquired_qty_604, far.acquired_amt_604,
|
||||
far.disposed_qty_604, far.disposed_amt_604,
|
||||
far.acquired_qty_60415, far.acquired_amt_60415,
|
||||
far.transferred_qty_60415, far.transferred_amt_60415,
|
||||
far.go_balance_only_amt,
|
||||
|
||||
FIRST_VALUE(COALESCE(far.opening_qty_604,0))
|
||||
OVER w AS base_qty_604,
|
||||
FIRST_VALUE(COALESCE(far.opening_amt_604,0))
|
||||
OVER w AS base_amt_604,
|
||||
FIRST_VALUE(COALESCE(far.opening_qty_60415,0))
|
||||
OVER w AS base_qty_60415,
|
||||
FIRST_VALUE(COALESCE(far.opening_amt_60415,0))
|
||||
OVER w AS base_amt_60415,
|
||||
|
||||
COALESCE(SUM(COALESCE(far.acquired_qty_604,0) - COALESCE(far.disposed_qty_604,0))
|
||||
OVER w_prev, 0) AS cum_dq_604,
|
||||
COALESCE(SUM(COALESCE(far.acquired_amt_604,0) - COALESCE(far.disposed_amt_604,0))
|
||||
OVER w_prev, 0) AS cum_da_604,
|
||||
COALESCE(SUM(COALESCE(far.acquired_qty_60415,0) - COALESCE(far.transferred_qty_60415,0))
|
||||
OVER w_prev, 0) AS cum_dq_60415,
|
||||
COALESCE(SUM(COALESCE(far.acquired_amt_60415,0) - COALESCE(far.transferred_amt_60415,0))
|
||||
OVER w_prev, 0) AS cum_da_60415
|
||||
FROM v3.fixed_asset_report far
|
||||
WHERE far.budget_form_id = p_form_id
|
||||
WINDOW w AS (PARTITION BY far.budget_form_id, far.expense_item_id, far.equipment_name
|
||||
ORDER BY far.month),
|
||||
w_prev AS (PARTITION BY far.budget_form_id, far.expense_item_id, far.equipment_name
|
||||
ORDER BY far.month
|
||||
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING)
|
||||
),
|
||||
eff AS (
|
||||
SELECT b.*,
|
||||
(b.base_qty_604 + b.cum_dq_604) AS opening_qty_604_eff,
|
||||
(b.base_amt_604 + b.cum_da_604) AS opening_amt_604_eff,
|
||||
(b.base_qty_60415 + b.cum_dq_60415) AS opening_qty_60415_eff,
|
||||
(b.base_amt_60415 + b.cum_da_60415) AS opening_amt_60415_eff
|
||||
FROM base b
|
||||
)
|
||||
SELECT
|
||||
'INPUT'::VARCHAR,
|
||||
0,
|
||||
ROW_NUMBER() OVER (ORDER BY eff.expense_item_id, eff.month) AS sort_order,
|
||||
jsonb_build_object(
|
||||
'id', eff.id,
|
||||
'expense_item_id', ei.id,
|
||||
'item_id', ei.item_id,
|
||||
'section_code', ei.section_code,
|
||||
'equipment_name', eff.equipment_name,
|
||||
'month', eff.month,
|
||||
'b_604', jsonb_build_object(
|
||||
'opening_qty', eff.opening_qty_604_eff,
|
||||
'opening_amt', eff.opening_amt_604_eff,
|
||||
'acquired_qty', eff.acquired_qty_604,
|
||||
'acquired_amt', eff.acquired_amt_604,
|
||||
'disposed_qty', eff.disposed_qty_604,
|
||||
'disposed_amt', eff.disposed_amt_604
|
||||
),
|
||||
'b_60415', jsonb_build_object(
|
||||
'opening_qty', eff.opening_qty_60415_eff,
|
||||
'opening_amt', eff.opening_amt_60415_eff,
|
||||
'acquired_qty', eff.acquired_qty_60415,
|
||||
'acquired_amt', eff.acquired_amt_60415,
|
||||
'transferred_qty',eff.transferred_qty_60415,
|
||||
'transferred_amt',eff.transferred_amt_60415
|
||||
),
|
||||
'totals', jsonb_build_object(
|
||||
|
||||
'acquired_total', COALESCE(eff.acquired_amt_604,0)+COALESCE(eff.acquired_amt_60415,0)
|
||||
+COALESCE(eff.go_balance_only_amt,0)-COALESCE(eff.transferred_amt_60415,0),
|
||||
'go_balance_only',eff.go_balance_only_amt,
|
||||
|
||||
'disposed_total', COALESCE(eff.disposed_amt_604,0),
|
||||
|
||||
'balance_qty', (eff.opening_qty_604_eff + COALESCE(eff.acquired_qty_604,0) - COALESCE(eff.disposed_qty_604,0))
|
||||
+ (eff.opening_qty_60415_eff + COALESCE(eff.acquired_qty_60415,0) - COALESCE(eff.transferred_qty_60415,0)),
|
||||
'balance_amt', (eff.opening_amt_604_eff + COALESCE(eff.acquired_amt_604,0) - COALESCE(eff.disposed_amt_604,0))
|
||||
+ (eff.opening_amt_60415_eff + COALESCE(eff.acquired_amt_60415,0) - COALESCE(eff.transferred_amt_60415,0))
|
||||
)
|
||||
)
|
||||
FROM eff
|
||||
JOIN v3.expense_item ei ON ei.id = eff.expense_item_id
|
||||
ORDER BY eff.expense_item_id, eff.month;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
|
||||
IF p_sheet = 'SMETA' THEN
|
||||
RETURN QUERY
|
||||
SELECT
|
||||
sm.row_type,
|
||||
sm.depth,
|
||||
ROW_NUMBER() OVER (ORDER BY sm.section_code) AS sort_order,
|
||||
jsonb_build_object(
|
||||
'section_code', sm.section_code,
|
||||
'name', sm.name,
|
||||
'plan', jsonb_build_object(
|
||||
'support', jsonb_build_object('q1', sm.plan_supp_q1, 'q2', sm.plan_supp_q2, 'q3', sm.plan_supp_q3, 'q4', sm.plan_supp_q4, 'year', sm.plan_supp_year),
|
||||
'development', jsonb_build_object('q1', sm.plan_dev_q1, 'q2', sm.plan_dev_q2, 'q3', sm.plan_dev_q3, 'q4', sm.plan_dev_q4, 'year', sm.plan_dev_year)
|
||||
),
|
||||
'approved', jsonb_build_object(
|
||||
'support', jsonb_build_object('q1', sm.appr_supp_q1, 'q2', sm.appr_supp_q2, 'q3', sm.appr_supp_q3, 'q4', sm.appr_supp_q4, 'year', sm.appr_supp_year),
|
||||
'development', jsonb_build_object('q1', sm.appr_dev_q1, 'q2', sm.appr_dev_q2, 'q3', sm.appr_dev_q3, 'q4', sm.appr_dev_q4, 'year', sm.appr_dev_year)
|
||||
),
|
||||
'fact', jsonb_build_object(
|
||||
'support', jsonb_build_object('q1', sm.fact_supp_q1, 'q2', sm.fact_supp_q2, 'q3', sm.fact_supp_q3, 'q4', sm.fact_supp_q4, 'year', sm.fact_supp_year),
|
||||
'development', jsonb_build_object('q1', sm.fact_dev_q1, 'q2', sm.fact_dev_q2, 'q3', sm.fact_dev_q3, 'q4', sm.fact_dev_q4, 'year', sm.fact_dev_year)
|
||||
),
|
||||
|
||||
'corrected', jsonb_build_object(
|
||||
'support', jsonb_build_object('q2', sm.corr_supp_q2, 'q3', sm.corr_supp_q3, 'q4', sm.corr_supp_q4),
|
||||
'development', jsonb_build_object('q2', sm.corr_dev_q2, 'q3', sm.corr_dev_q3, 'q4', sm.corr_dev_q4)
|
||||
)
|
||||
)
|
||||
FROM v3.v_form2_smeta(p_form_id) sm
|
||||
ORDER BY sm.section_code;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
RAISE EXCEPTION 'Unknown sheet: %', p_sheet
|
||||
USING HINT = 'Use AHR / CAP / OPER / AHR_LIMIT / AHR_RENT / AHR_UTILITY / AHR_SECURITY / OTCH9F / SMETA';
|
||||
|
||||
END;
|
||||
$function$
|
||||
LANGUAGE plpgsql STABLE;
|
||||
|
||||
|
||||
-- 4б. v3.sync_form2_ahr_satellite_lines: лимитная ветка — amount от
|
||||
-- limit_without_vat, деление на 1000 при переносе в план АХР. Ветки
|
||||
-- AHR_RENT/AHR_UTILITY/AHR_SECURITY не трогаются (AURORA-1887/1909).
|
||||
CREATE OR REPLACE FUNCTION v3.sync_form2_ahr_satellite_lines(p_form_id integer)
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_form_type varchar;
|
||||
v_line_id integer;
|
||||
v_rec record;
|
||||
BEGIN
|
||||
SELECT bf.form_type_code
|
||||
INTO v_form_type
|
||||
FROM v3.budget_form bf
|
||||
WHERE bf.id = p_form_id;
|
||||
|
||||
IF v_form_type IS DISTINCT FROM 'FORM_2' THEN
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
|
||||
|
||||
FOR v_rec IN
|
||||
WITH limit_amounts AS (
|
||||
SELECT
|
||||
ei.id AS expense_item_id,
|
||||
regexp_replace(lt.name, '^[[:space:]]*-[[:space:]]*', '')::varchar AS line_name,
|
||||
v3._form2_ahr_limit_amount(lt.period, lt.limit_without_vat, fl.qty_q1) / 1000 AS plan_q1,
|
||||
v3._form2_ahr_limit_amount(lt.period, lt.limit_without_vat, fl.qty_q2) / 1000 AS plan_q2,
|
||||
v3._form2_ahr_limit_amount(lt.period, lt.limit_without_vat, fl.qty_q3) / 1000 AS plan_q3,
|
||||
v3._form2_ahr_limit_amount(lt.period, lt.limit_without_vat, fl.qty_q4) / 1000 AS plan_q4
|
||||
FROM v3.limit_template lt
|
||||
LEFT JOIN v3.form_limit fl
|
||||
ON fl.template_id = lt.id
|
||||
AND fl.budget_form_id = p_form_id
|
||||
JOIN v3.expense_item ei
|
||||
ON ei.sheet = 'AHR'
|
||||
AND ei.direction = 'Support'
|
||||
AND (
|
||||
(ei.depth = 3 AND ei.item_id = lt.expense_item_code)
|
||||
OR ei.num_group_id = lt.expense_item_code
|
||||
)
|
||||
JOIN v3.expense_item_form_type eft
|
||||
ON eft.expense_item_id = ei.id
|
||||
AND eft.form_type_code = 'FORM_2'
|
||||
WHERE lt.row_type = 'LEAF'
|
||||
)
|
||||
SELECT * FROM limit_amounts
|
||||
LOOP
|
||||
PERFORM v3._upsert_form2_ahr_limit_sync_values(
|
||||
p_form_id, v_rec.expense_item_id, v_rec.line_name,
|
||||
v_rec.plan_q1, v_rec.plan_q2, v_rec.plan_q3, v_rec.plan_q4
|
||||
);
|
||||
END LOOP;
|
||||
|
||||
|
||||
FOR v_line_id IN
|
||||
SELECT m.target_line_id
|
||||
FROM v3.form2_ahr_satellite_line_source m
|
||||
JOIN v3.budget_line bl ON bl.id = m.target_line_id
|
||||
WHERE m.source_sheet = 'AHR_RENT'
|
||||
AND bl.budget_form_id = p_form_id
|
||||
AND NOT EXISTS (SELECT 1 FROM v3.rent_detail rd WHERE rd.id = m.source_id)
|
||||
LOOP
|
||||
DELETE FROM v3.plan WHERE line_id = v_line_id;
|
||||
DELETE FROM v3.budget_line_quarter WHERE line_id = v_line_id;
|
||||
DELETE FROM v3.budget_line WHERE id = v_line_id;
|
||||
END LOOP;
|
||||
|
||||
FOR v_rec IN
|
||||
SELECT
|
||||
rd.id AS source_id,
|
||||
rd.line_id AS existing_line_id,
|
||||
rd.vsp_id,
|
||||
COALESCE(v.format, 'АХР Аренда #' || rd.id)::varchar AS line_name,
|
||||
rd.contract_number::varchar AS internal_order,
|
||||
rd.plan_q1, rd.plan_q2, rd.plan_q3, rd.plan_q4,
|
||||
rd.actual_jan, rd.actual_feb, rd.actual_mar,
|
||||
rd.actual_apr, rd.actual_may, rd.actual_jun,
|
||||
rd.actual_jul, rd.actual_aug, rd.actual_sep,
|
||||
rd.actual_oct, rd.actual_nov, rd.actual_dec, rd.actual_spod
|
||||
FROM v3.rent_detail rd
|
||||
JOIN v3.budget_line bl ON bl.id = rd.line_id
|
||||
LEFT JOIN v3.vsp v ON v.id = rd.vsp_id
|
||||
WHERE bl.budget_form_id = p_form_id
|
||||
ORDER BY rd.id
|
||||
LOOP
|
||||
PERFORM v3._upsert_form2_ahr_detail_sync_values(
|
||||
p_form_id, 314, 'AHR_RENT', v_rec.source_id,
|
||||
v_rec.line_name, v_rec.internal_order, v_rec.vsp_id, v_rec.existing_line_id,
|
||||
v_rec.plan_q1, v_rec.plan_q2, v_rec.plan_q3, v_rec.plan_q4,
|
||||
v_rec.actual_jan, v_rec.actual_feb, v_rec.actual_mar,
|
||||
v_rec.actual_apr, v_rec.actual_may, v_rec.actual_jun,
|
||||
v_rec.actual_jul, v_rec.actual_aug, v_rec.actual_sep,
|
||||
v_rec.actual_oct, v_rec.actual_nov, v_rec.actual_dec, v_rec.actual_spod
|
||||
);
|
||||
|
||||
UPDATE v3.rent_detail rd
|
||||
SET line_id = m.target_line_id
|
||||
FROM v3.form2_ahr_satellite_line_source m
|
||||
WHERE rd.id = v_rec.source_id
|
||||
AND m.source_sheet = 'AHR_RENT'
|
||||
AND m.source_id = rd.id;
|
||||
END LOOP;
|
||||
|
||||
|
||||
FOR v_line_id IN
|
||||
SELECT m.target_line_id
|
||||
FROM v3.form2_ahr_satellite_line_source m
|
||||
JOIN v3.budget_line bl ON bl.id = m.target_line_id
|
||||
WHERE m.source_sheet = 'AHR_UTILITY'
|
||||
AND bl.budget_form_id = p_form_id
|
||||
AND NOT EXISTS (SELECT 1 FROM v3.utility_detail ud WHERE ud.id = m.source_id)
|
||||
LOOP
|
||||
DELETE FROM v3.plan WHERE line_id = v_line_id;
|
||||
DELETE FROM v3.budget_line_quarter WHERE line_id = v_line_id;
|
||||
DELETE FROM v3.budget_line WHERE id = v_line_id;
|
||||
END LOOP;
|
||||
|
||||
FOR v_rec IN
|
||||
SELECT
|
||||
ud.id AS source_id,
|
||||
ud.line_id AS existing_line_id,
|
||||
ud.vsp_id,
|
||||
COALESCE(v.format, 'АХР Коммунальные услуги #' || ud.id)::varchar AS line_name,
|
||||
ud.contract_number::varchar AS internal_order,
|
||||
ud.plan_q1, ud.plan_q2, ud.plan_q3, ud.plan_q4,
|
||||
ud.actual_jan, ud.actual_feb, ud.actual_mar,
|
||||
ud.actual_apr, ud.actual_may, ud.actual_jun,
|
||||
ud.actual_jul, ud.actual_aug, ud.actual_sep,
|
||||
ud.actual_oct, ud.actual_nov, ud.actual_dec, ud.actual_spod
|
||||
FROM v3.utility_detail ud
|
||||
JOIN v3.budget_line bl ON bl.id = ud.line_id
|
||||
LEFT JOIN v3.vsp v ON v.id = ud.vsp_id
|
||||
WHERE bl.budget_form_id = p_form_id
|
||||
ORDER BY ud.id
|
||||
LOOP
|
||||
PERFORM v3._upsert_form2_ahr_detail_sync_values(
|
||||
p_form_id, 24, 'AHR_UTILITY', v_rec.source_id,
|
||||
v_rec.line_name, v_rec.internal_order, v_rec.vsp_id, v_rec.existing_line_id,
|
||||
v_rec.plan_q1, v_rec.plan_q2, v_rec.plan_q3, v_rec.plan_q4,
|
||||
v_rec.actual_jan, v_rec.actual_feb, v_rec.actual_mar,
|
||||
v_rec.actual_apr, v_rec.actual_may, v_rec.actual_jun,
|
||||
v_rec.actual_jul, v_rec.actual_aug, v_rec.actual_sep,
|
||||
v_rec.actual_oct, v_rec.actual_nov, v_rec.actual_dec, v_rec.actual_spod
|
||||
);
|
||||
|
||||
UPDATE v3.utility_detail ud
|
||||
SET line_id = m.target_line_id
|
||||
FROM v3.form2_ahr_satellite_line_source m
|
||||
WHERE ud.id = v_rec.source_id
|
||||
AND m.source_sheet = 'AHR_UTILITY'
|
||||
AND m.source_id = ud.id;
|
||||
END LOOP;
|
||||
|
||||
|
||||
FOR v_line_id IN
|
||||
SELECT m.target_line_id
|
||||
FROM v3.form2_ahr_satellite_line_source m
|
||||
JOIN v3.budget_line bl ON bl.id = m.target_line_id
|
||||
WHERE m.source_sheet = 'AHR_SECURITY'
|
||||
AND bl.budget_form_id = p_form_id
|
||||
AND NOT EXISTS (SELECT 1 FROM v3.security_detail sd WHERE sd.id = m.source_id)
|
||||
LOOP
|
||||
DELETE FROM v3.plan WHERE line_id = v_line_id;
|
||||
DELETE FROM v3.budget_line_quarter WHERE line_id = v_line_id;
|
||||
DELETE FROM v3.budget_line WHERE id = v_line_id;
|
||||
END LOOP;
|
||||
|
||||
FOR v_rec IN
|
||||
SELECT
|
||||
sd.id AS source_id,
|
||||
sd.line_id AS existing_line_id,
|
||||
sd.vsp_id,
|
||||
COALESCE(v.format, 'АХР Безопасность #' || sd.id)::varchar AS line_name,
|
||||
sd.contract_number::varchar AS internal_order,
|
||||
sd.plan_q1, sd.plan_q2, sd.plan_q3, sd.plan_q4,
|
||||
sd.actual_jan, sd.actual_feb, sd.actual_mar,
|
||||
sd.actual_apr, sd.actual_may, sd.actual_jun,
|
||||
sd.actual_jul, sd.actual_aug, sd.actual_sep,
|
||||
sd.actual_oct, sd.actual_nov, sd.actual_dec, sd.actual_spod
|
||||
FROM v3.security_detail sd
|
||||
JOIN v3.budget_line bl ON bl.id = sd.line_id
|
||||
LEFT JOIN v3.vsp v ON v.id = sd.vsp_id
|
||||
WHERE bl.budget_form_id = p_form_id
|
||||
ORDER BY sd.id
|
||||
LOOP
|
||||
PERFORM v3._upsert_form2_ahr_detail_sync_values(
|
||||
p_form_id, 779, 'AHR_SECURITY', v_rec.source_id,
|
||||
v_rec.line_name, v_rec.internal_order, v_rec.vsp_id, v_rec.existing_line_id,
|
||||
v_rec.plan_q1, v_rec.plan_q2, v_rec.plan_q3, v_rec.plan_q4,
|
||||
v_rec.actual_jan, v_rec.actual_feb, v_rec.actual_mar,
|
||||
v_rec.actual_apr, v_rec.actual_may, v_rec.actual_jun,
|
||||
v_rec.actual_jul, v_rec.actual_aug, v_rec.actual_sep,
|
||||
v_rec.actual_oct, v_rec.actual_nov, v_rec.actual_dec, v_rec.actual_spod
|
||||
);
|
||||
|
||||
UPDATE v3.security_detail sd
|
||||
SET line_id = m.target_line_id
|
||||
FROM v3.form2_ahr_satellite_line_source m
|
||||
WHERE sd.id = v_rec.source_id
|
||||
AND m.source_sheet = 'AHR_SECURITY'
|
||||
AND m.source_id = sd.id;
|
||||
END LOOP;
|
||||
END;
|
||||
$function$;
|
||||
|
||||
|
||||
-- 4в. v3.sync_form2_ahr_satellite_line: тот же набор правок для синка одной строки.
|
||||
CREATE OR REPLACE FUNCTION v3.sync_form2_ahr_satellite_line(p_form_id integer, p_sheet character varying, p_source_id integer)
|
||||
RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
v_rec record;
|
||||
BEGIN
|
||||
IF p_sheet = 'AHR_LIMIT' THEN
|
||||
SELECT
|
||||
ei.id AS expense_item_id,
|
||||
regexp_replace(lt.name, '^[[:space:]]*-[[:space:]]*', '')::varchar AS line_name,
|
||||
v3._form2_ahr_limit_amount(lt.period, lt.limit_without_vat, fl.qty_q1) / 1000 AS plan_q1,
|
||||
v3._form2_ahr_limit_amount(lt.period, lt.limit_without_vat, fl.qty_q2) / 1000 AS plan_q2,
|
||||
v3._form2_ahr_limit_amount(lt.period, lt.limit_without_vat, fl.qty_q3) / 1000 AS plan_q3,
|
||||
v3._form2_ahr_limit_amount(lt.period, lt.limit_without_vat, fl.qty_q4) / 1000 AS plan_q4
|
||||
INTO v_rec
|
||||
FROM v3.limit_template lt
|
||||
LEFT JOIN v3.form_limit fl
|
||||
ON fl.template_id = lt.id
|
||||
AND fl.budget_form_id = p_form_id
|
||||
JOIN v3.expense_item ei
|
||||
ON ei.sheet = 'AHR'
|
||||
AND ei.direction = 'Support'
|
||||
AND (
|
||||
(ei.depth = 3 AND ei.item_id = lt.expense_item_code)
|
||||
OR ei.num_group_id = lt.expense_item_code
|
||||
)
|
||||
JOIN v3.expense_item_form_type eft
|
||||
ON eft.expense_item_id = ei.id
|
||||
AND eft.form_type_code = 'FORM_2'
|
||||
WHERE lt.id = p_source_id
|
||||
AND lt.row_type = 'LEAF';
|
||||
|
||||
IF v_rec.expense_item_id IS NOT NULL THEN
|
||||
PERFORM v3._upsert_form2_ahr_limit_sync_values(
|
||||
p_form_id, v_rec.expense_item_id, v_rec.line_name,
|
||||
v_rec.plan_q1, v_rec.plan_q2, v_rec.plan_q3, v_rec.plan_q4
|
||||
);
|
||||
END IF;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
IF p_sheet = 'AHR_RENT' THEN
|
||||
SELECT
|
||||
rd.id AS source_id,
|
||||
rd.line_id AS existing_line_id,
|
||||
rd.vsp_id,
|
||||
COALESCE(v.format, 'АХР Аренда #' || rd.id)::varchar AS line_name,
|
||||
rd.contract_number::varchar AS internal_order,
|
||||
rd.plan_q1, rd.plan_q2, rd.plan_q3, rd.plan_q4,
|
||||
rd.actual_jan, rd.actual_feb, rd.actual_mar,
|
||||
rd.actual_apr, rd.actual_may, rd.actual_jun,
|
||||
rd.actual_jul, rd.actual_aug, rd.actual_sep,
|
||||
rd.actual_oct, rd.actual_nov, rd.actual_dec, rd.actual_spod
|
||||
INTO v_rec
|
||||
FROM v3.rent_detail rd
|
||||
JOIN v3.budget_line bl ON bl.id = rd.line_id
|
||||
LEFT JOIN v3.vsp v ON v.id = rd.vsp_id
|
||||
WHERE rd.id = p_source_id
|
||||
AND bl.budget_form_id = p_form_id;
|
||||
|
||||
IF v_rec.source_id IS NOT NULL THEN
|
||||
PERFORM v3._upsert_form2_ahr_detail_sync_values(
|
||||
p_form_id, 314, 'AHR_RENT', v_rec.source_id,
|
||||
v_rec.line_name, v_rec.internal_order, v_rec.vsp_id, v_rec.existing_line_id,
|
||||
v_rec.plan_q1, v_rec.plan_q2, v_rec.plan_q3, v_rec.plan_q4,
|
||||
v_rec.actual_jan, v_rec.actual_feb, v_rec.actual_mar,
|
||||
v_rec.actual_apr, v_rec.actual_may, v_rec.actual_jun,
|
||||
v_rec.actual_jul, v_rec.actual_aug, v_rec.actual_sep,
|
||||
v_rec.actual_oct, v_rec.actual_nov, v_rec.actual_dec, v_rec.actual_spod
|
||||
);
|
||||
|
||||
UPDATE v3.rent_detail rd
|
||||
SET line_id = m.target_line_id
|
||||
FROM v3.form2_ahr_satellite_line_source m
|
||||
WHERE rd.id = v_rec.source_id
|
||||
AND m.source_sheet = 'AHR_RENT'
|
||||
AND m.source_id = rd.id;
|
||||
END IF;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
IF p_sheet = 'AHR_UTILITY' THEN
|
||||
SELECT
|
||||
ud.id AS source_id,
|
||||
ud.line_id AS existing_line_id,
|
||||
ud.vsp_id,
|
||||
COALESCE(v.format, 'АХР Коммунальные услуги #' || ud.id)::varchar AS line_name,
|
||||
ud.contract_number::varchar AS internal_order,
|
||||
ud.plan_q1, ud.plan_q2, ud.plan_q3, ud.plan_q4,
|
||||
ud.actual_jan, ud.actual_feb, ud.actual_mar,
|
||||
ud.actual_apr, ud.actual_may, ud.actual_jun,
|
||||
ud.actual_jul, ud.actual_aug, ud.actual_sep,
|
||||
ud.actual_oct, ud.actual_nov, ud.actual_dec, ud.actual_spod
|
||||
INTO v_rec
|
||||
FROM v3.utility_detail ud
|
||||
JOIN v3.budget_line bl ON bl.id = ud.line_id
|
||||
LEFT JOIN v3.vsp v ON v.id = ud.vsp_id
|
||||
WHERE ud.id = p_source_id
|
||||
AND bl.budget_form_id = p_form_id;
|
||||
|
||||
IF v_rec.source_id IS NOT NULL THEN
|
||||
PERFORM v3._upsert_form2_ahr_detail_sync_values(
|
||||
p_form_id, 24, 'AHR_UTILITY', v_rec.source_id,
|
||||
v_rec.line_name, v_rec.internal_order, v_rec.vsp_id, v_rec.existing_line_id,
|
||||
v_rec.plan_q1, v_rec.plan_q2, v_rec.plan_q3, v_rec.plan_q4,
|
||||
v_rec.actual_jan, v_rec.actual_feb, v_rec.actual_mar,
|
||||
v_rec.actual_apr, v_rec.actual_may, v_rec.actual_jun,
|
||||
v_rec.actual_jul, v_rec.actual_aug, v_rec.actual_sep,
|
||||
v_rec.actual_oct, v_rec.actual_nov, v_rec.actual_dec, v_rec.actual_spod
|
||||
);
|
||||
|
||||
UPDATE v3.utility_detail ud
|
||||
SET line_id = m.target_line_id
|
||||
FROM v3.form2_ahr_satellite_line_source m
|
||||
WHERE ud.id = v_rec.source_id
|
||||
AND m.source_sheet = 'AHR_UTILITY'
|
||||
AND m.source_id = ud.id;
|
||||
END IF;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
IF p_sheet = 'AHR_SECURITY' THEN
|
||||
SELECT
|
||||
sd.id AS source_id,
|
||||
sd.line_id AS existing_line_id,
|
||||
sd.vsp_id,
|
||||
COALESCE(v.format, 'АХР Безопасность #' || sd.id)::varchar AS line_name,
|
||||
sd.contract_number::varchar AS internal_order,
|
||||
sd.plan_q1, sd.plan_q2, sd.plan_q3, sd.plan_q4,
|
||||
sd.actual_jan, sd.actual_feb, sd.actual_mar,
|
||||
sd.actual_apr, sd.actual_may, sd.actual_jun,
|
||||
sd.actual_jul, sd.actual_aug, sd.actual_sep,
|
||||
sd.actual_oct, sd.actual_nov, sd.actual_dec, sd.actual_spod
|
||||
INTO v_rec
|
||||
FROM v3.security_detail sd
|
||||
JOIN v3.budget_line bl ON bl.id = sd.line_id
|
||||
LEFT JOIN v3.vsp v ON v.id = sd.vsp_id
|
||||
WHERE sd.id = p_source_id
|
||||
AND bl.budget_form_id = p_form_id;
|
||||
|
||||
IF v_rec.source_id IS NOT NULL THEN
|
||||
PERFORM v3._upsert_form2_ahr_detail_sync_values(
|
||||
p_form_id, 779, 'AHR_SECURITY', v_rec.source_id,
|
||||
v_rec.line_name, v_rec.internal_order, v_rec.vsp_id, v_rec.existing_line_id,
|
||||
v_rec.plan_q1, v_rec.plan_q2, v_rec.plan_q3, v_rec.plan_q4,
|
||||
v_rec.actual_jan, v_rec.actual_feb, v_rec.actual_mar,
|
||||
v_rec.actual_apr, v_rec.actual_may, v_rec.actual_jun,
|
||||
v_rec.actual_jul, v_rec.actual_aug, v_rec.actual_sep,
|
||||
v_rec.actual_oct, v_rec.actual_nov, v_rec.actual_dec, v_rec.actual_spod
|
||||
);
|
||||
|
||||
UPDATE v3.security_detail sd
|
||||
SET line_id = m.target_line_id
|
||||
FROM v3.form2_ahr_satellite_line_source m
|
||||
WHERE sd.id = v_rec.source_id
|
||||
AND m.source_sheet = 'AHR_SECURITY'
|
||||
AND m.source_id = sd.id;
|
||||
END IF;
|
||||
END IF;
|
||||
END;
|
||||
$function$;
|
||||
|
||||
|
||||
-- 5. Старая 4-аргументная сигнатура больше не вызывается — удаляем.
|
||||
DROP FUNCTION v3._form2_ahr_limit_amount(character varying, character varying, numeric, numeric);
|
||||
|
||||
|
||||
-- 6. Строка «Итого» (id=0) должна попадать в path-set, чтобы upd_form_cells
|
||||
-- включал её в diff при любом изменении ячейки на листе AHR_LIMIT.
|
||||
CREATE OR REPLACE FUNCTION v3._limit_template_path_set(p_ids integer[])
|
||||
RETURNS integer[]
|
||||
LANGUAGE sql
|
||||
STABLE
|
||||
AS $function$
|
||||
WITH RECURSIVE up AS (
|
||||
SELECT id, parent_id FROM v3.limit_template WHERE id = ANY(p_ids)
|
||||
UNION
|
||||
SELECT p.id, p.parent_id
|
||||
FROM v3.limit_template p
|
||||
JOIN up u ON p.id = u.parent_id
|
||||
),
|
||||
agg AS (
|
||||
SELECT array_agg(DISTINCT id) AS ids FROM up
|
||||
)
|
||||
SELECT CASE
|
||||
WHEN p_ids IS NULL OR array_length(p_ids, 1) IS NULL THEN agg.ids
|
||||
ELSE COALESCE(agg.ids, ARRAY[]::integer[]) || ARRAY[0]
|
||||
END
|
||||
FROM agg;
|
||||
$function$;
|
||||
|
||||
|
||||
-- 7. Пересинхронизировать все существующие формы 2 с новой формулой.
|
||||
DO $do$
|
||||
DECLARE
|
||||
v_form_id integer;
|
||||
BEGIN
|
||||
FOR v_form_id IN
|
||||
SELECT id
|
||||
FROM v3.budget_form
|
||||
WHERE form_type_code = 'FORM_2'
|
||||
LOOP
|
||||
PERFORM v3.sync_form2_ahr_satellite_lines(v_form_id);
|
||||
END LOOP;
|
||||
END;
|
||||
$do$;
|
||||
@ -13,6 +13,10 @@ class LimitTemplate(Base):
|
||||
"row_type IN ('SECTION','GROUP','LEAF')",
|
||||
name="ck_v3_limit_template_row_type",
|
||||
),
|
||||
CheckConstraint(
|
||||
"period IN ('YEAR','QUARTER','MONTH')",
|
||||
name="ck_v3_limit_template_period",
|
||||
),
|
||||
Index("ix_v3_limit_template_parent", "parent_id"),
|
||||
{"schema": "v3"},
|
||||
)
|
||||
@ -25,6 +29,9 @@ class LimitTemplate(Base):
|
||||
expense_item_code: Mapped[str | None] = mapped_column(String)
|
||||
name: Mapped[str] = mapped_column(String)
|
||||
unit: Mapped[str | None] = mapped_column(String)
|
||||
# Период норматива: YEAR — делится на 4 квартала, MONTH — умножается на 3,
|
||||
# QUARTER (по умолчанию) — считается как есть. AURORA-1897.
|
||||
period: Mapped[str] = mapped_column(String, nullable=False, server_default="QUARTER")
|
||||
limit_with_vat: Mapped[float | None] = mapped_column(Numeric)
|
||||
limit_without_vat: Mapped[float | None] = mapped_column(Numeric)
|
||||
|
||||
|
||||
212
api/tests/integration/test_form2_ahr_limit.py
Normal file
212
api/tests/integration/test_form2_ahr_limit.py
Normal file
@ -0,0 +1,212 @@
|
||||
"""AURORA-1897: лист «АХР лимиты» формы 2.
|
||||
|
||||
Три правки одной формулой/миграцией (0052_form2_ahr_limit_period_vat_units):
|
||||
1) годовые нормативы (ДМС id 23/24/25, оргтехника id 13/14/15) делятся на
|
||||
4 квартала (period=YEAR), а не дублируются в каждом квартале;
|
||||
2) сумма на листе лимитов и в плане АХР считается от лимита БЕЗ НДС
|
||||
(limit_without_vat), а не с НДС;
|
||||
3) план лимитных строк в АХР переводится в тыс. руб. (как аренда/
|
||||
коммуналка/охрана/Смета формы 2), а не остаётся в рублях.
|
||||
|
||||
Сессия тестов работает в rollback-транзакции (см. tests/integration/conftest.py
|
||||
client fixture), поэтому изменения не переживают отдельный тест.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
# АХР Аренда синхронизируется в АХР под статьёй с этим expense_item_id
|
||||
# (v3.sync_form2_ahr_satellite_line(s), api/alembic/versions/sql/
|
||||
# 0050_form2_ahr_spod_column.sql) — используется только в регрессионном
|
||||
# тесте ветки аренды (эта миграция её не трогает).
|
||||
AHR_RENT_PARENT_EXPENSE_ITEM = 314
|
||||
|
||||
|
||||
def _auth_headers(tokens: dict) -> dict:
|
||||
return {"Authorization": f"Bearer {tokens['access_token']}"}
|
||||
|
||||
|
||||
def _get_form2_id(client, admin_tokens) -> int | None:
|
||||
response = client.get(
|
||||
"/api/v1/form/",
|
||||
params={"form_type": "FORM_2"},
|
||||
headers=_auth_headers(admin_tokens),
|
||||
)
|
||||
assert response.status_code == 200
|
||||
forms = response.json()["result"]
|
||||
return forms[0]["id"] if forms else None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def form2_id(client, admin_tokens):
|
||||
form_id = _get_form2_id(client, admin_tokens)
|
||||
if form_id is None:
|
||||
pytest.skip("В фикстуре нет формы 2 (FORM_2) — тест лимитов АХР пропущен")
|
||||
return form_id
|
||||
|
||||
|
||||
def _get_sheet_rows(client, admin_tokens, form2_id, sheet) -> list[dict]:
|
||||
response = client.get(
|
||||
f"/api/v1/form/{form2_id}/sheet/{sheet}",
|
||||
headers=_auth_headers(admin_tokens),
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
return response.json()["result"]
|
||||
|
||||
|
||||
def _patch_cell(client, admin_tokens, form2_id, sheet, line_id, column, value) -> list[dict]:
|
||||
response = client.patch(
|
||||
f"/api/v1/form/{form2_id}/sheet/{sheet}/cell",
|
||||
json={"line_id": line_id, "column": column, "value": value},
|
||||
headers=_auth_headers(admin_tokens),
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
return response.json()["result"]
|
||||
|
||||
|
||||
def _find_by_id(rows: list[dict], target_id: int) -> dict:
|
||||
# v_form_view может отдавать META-строку ({"editable": [...], ...}) без
|
||||
# "id" — её пропускаем, т.к. (data->>'id')::INT там NULL.
|
||||
for row in rows:
|
||||
data = row.get("data")
|
||||
if isinstance(data, dict) and data.get("id") == target_id:
|
||||
return row
|
||||
raise AssertionError(f"строка с id={target_id} не найдена среди {rows!r}")
|
||||
|
||||
|
||||
def _find_ahr_row_by_name(rows: list[dict], name: str) -> dict:
|
||||
for row in rows:
|
||||
if row.get("row_type") != "INPUT":
|
||||
continue
|
||||
data = row.get("data") or {}
|
||||
if (data.get("header") or {}).get("name") == name:
|
||||
return row
|
||||
raise AssertionError(f"строка АХР с именем {name!r} не найдена среди {rows!r}")
|
||||
|
||||
|
||||
def _find_ahr_row_by_expense_item(rows: list[dict], expense_item_id: int) -> dict:
|
||||
for row in rows:
|
||||
if row.get("row_type") != "INPUT":
|
||||
continue
|
||||
data = row.get("data") or {}
|
||||
if (data.get("header") or {}).get("expense_item_id") == expense_item_id:
|
||||
return row
|
||||
raise AssertionError(
|
||||
f"строка АХР с expense_item_id={expense_item_id} не найдена среди {rows!r}"
|
||||
)
|
||||
|
||||
|
||||
def test_annual_dms_limit_divided_by_four(client, admin_tokens, form2_id):
|
||||
# id=24 «- стандарт РФ»: ДМС, лимит 27858 руб/год без НДС, period=YEAR —
|
||||
# 10 * 27858 / 4 = 69645 руб. в заполненный квартал (а не дублирование
|
||||
# годовой суммы 278580, как было до миграции).
|
||||
rows = _patch_cell(client, admin_tokens, form2_id, "AHR_LIMIT", 24, "qty_q1", 10)
|
||||
row = _find_by_id(rows, 24)
|
||||
assert row["data"]["amount_q1"] == 69645
|
||||
assert row["data"]["amount_year"] == 69645
|
||||
|
||||
ahr_rows = _get_sheet_rows(client, admin_tokens, form2_id, "AHR")
|
||||
ahr_row = _find_ahr_row_by_name(ahr_rows, "стандарт РФ")
|
||||
assert ahr_row["data"]["plan"]["q1"] == pytest.approx(69.645)
|
||||
|
||||
|
||||
def test_quarterly_limit_uses_amount_without_vat(client, admin_tokens, form2_id):
|
||||
# id=3 «- Директор»: 1 чел/квартал, 2400 руб. с НДС / 2000 без НДС —
|
||||
# сумма должна считаться от лимита БЕЗ НДС.
|
||||
rows = _patch_cell(client, admin_tokens, form2_id, "AHR_LIMIT", 3, "qty_q2", 1)
|
||||
row = _find_by_id(rows, 3)
|
||||
assert row["data"]["amount_q2"] == 2000
|
||||
|
||||
ahr_rows = _get_sheet_rows(client, admin_tokens, form2_id, "AHR")
|
||||
ahr_row = _find_ahr_row_by_name(ahr_rows, "Директор")
|
||||
assert ahr_row["data"]["plan"]["q2"] == pytest.approx(2.0)
|
||||
|
||||
|
||||
def test_sim_card_without_vat_limit_backfilled_is_not_zero(client, admin_tokens, form2_id):
|
||||
# id=7 «- SIM-карта для планшета (легкий фронт)»: limit_without_vat был
|
||||
# NULL до миграции — она заполнила его 375 (450 / 1.2). Сумма не должна
|
||||
# стать 0 после перехода на расчёт без НДС.
|
||||
rows = _patch_cell(client, admin_tokens, form2_id, "AHR_LIMIT", 7, "qty_q1", 20)
|
||||
row = _find_by_id(rows, 7)
|
||||
assert row["data"]["amount_q1"] == 7500
|
||||
|
||||
ahr_rows = _get_sheet_rows(client, admin_tokens, form2_id, "AHR")
|
||||
ahr_row = _find_ahr_row_by_name(ahr_rows, "SIM-карта для планшета (легкий фронт)")
|
||||
assert ahr_row["data"]["plan"]["q1"] == pytest.approx(7.5)
|
||||
|
||||
|
||||
def test_printers_annual_limit_without_name_lookup(client, admin_tokens, form2_id):
|
||||
# id=13 «- принтеры»: раньше считалось по захардкоженному имени
|
||||
# (qty * 0.57 * 10900 / 4), теперь — общей формулой через period=YEAR,
|
||||
# limit_without_vat=5177.5 (миграция переписала справочник).
|
||||
rows = _patch_cell(client, admin_tokens, form2_id, "AHR_LIMIT", 13, "qty_q1", 5)
|
||||
row = _find_by_id(rows, 13)
|
||||
assert row["data"]["amount_q1"] == pytest.approx(6471.875)
|
||||
|
||||
ahr_rows = _get_sheet_rows(client, admin_tokens, form2_id, "AHR")
|
||||
ahr_row = _find_ahr_row_by_name(ahr_rows, "принтеры")
|
||||
assert ahr_row["data"]["plan"]["q1"] == pytest.approx(6.471875)
|
||||
|
||||
|
||||
def test_quarterly_norm_unchanged_other_than_vat_source(client, admin_tokens, form2_id):
|
||||
# id=17 «- счетчики/сортировщики банкнот»: квартальный норматив (period
|
||||
# остаётся QUARTER по умолчанию) считается как раньше — поменялся только
|
||||
# источник суммы (НДС).
|
||||
rows = _patch_cell(client, admin_tokens, form2_id, "AHR_LIMIT", 17, "qty_q3", 2)
|
||||
row = _find_by_id(rows, 17)
|
||||
assert row["data"]["amount_q3"] == 7500
|
||||
|
||||
ahr_rows = _get_sheet_rows(client, admin_tokens, form2_id, "AHR")
|
||||
ahr_row = _find_ahr_row_by_name(ahr_rows, "счетчики/сортировщики банкнот")
|
||||
assert ahr_row["data"]["plan"]["q3"] == pytest.approx(7.5)
|
||||
|
||||
|
||||
def test_total_row_is_last_root_row_summing_all_leaves(client, admin_tokens, form2_id):
|
||||
patch_rows = _patch_cell(client, admin_tokens, form2_id, "AHR_LIMIT", 24, "qty_q1", 10)
|
||||
# Ответ PATCH должен содержать строку «Итого» (data.id = 0) в diff, т.к.
|
||||
# v3._limit_template_path_set дописывает 0 к path-set любого изменения.
|
||||
total_in_patch = _find_by_id(patch_rows, 0)
|
||||
assert total_in_patch["row_type"] == "ROOT"
|
||||
|
||||
full_rows = _get_sheet_rows(client, admin_tokens, form2_id, "AHR_LIMIT")
|
||||
leaf_rows = [
|
||||
row
|
||||
for row in full_rows
|
||||
if row.get("row_type") == "INPUT" and (row.get("data") or {}).get("id") is not None
|
||||
]
|
||||
total_row = next(
|
||||
row
|
||||
for row in full_rows
|
||||
if row.get("row_type") == "ROOT" and (row.get("data") or {}).get("id") == 0
|
||||
)
|
||||
|
||||
assert total_row["data"]["name"] == "Итого"
|
||||
expected_amount_q1 = sum((row["data"].get("amount_q1") or 0) for row in leaf_rows)
|
||||
assert total_row["data"]["amount_q1"] == pytest.approx(expected_amount_q1)
|
||||
assert total_row["data"]["amount_q1"] == pytest.approx(69645)
|
||||
# «Итого» должна идти последней строкой листа (sort_order = 999999).
|
||||
assert full_rows[-1] is total_row
|
||||
|
||||
|
||||
def test_rent_plan_still_passed_through_without_division(client, admin_tokens, form2_id):
|
||||
# Регрессия: AURORA-1897 не должна задевать перенос аренды/коммуналки/
|
||||
# охраны — только лимитные строки АХР лимитов.
|
||||
response = client.post(
|
||||
f"/api/v1/form/{form2_id}/sheet/AHR_RENT/line",
|
||||
json={"vsp_id": 1},
|
||||
headers=_auth_headers(admin_tokens),
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
rent_rows = response.json()["result"]
|
||||
assert len(rent_rows) == 1
|
||||
rent_line_id = rent_rows[0]["data"]["id"]
|
||||
|
||||
response = client.patch(
|
||||
f"/api/v1/form/{form2_id}/sheet/AHR_RENT/cell",
|
||||
json={"line_id": rent_line_id, "column": "plan.q1", "value": 100},
|
||||
headers=_auth_headers(admin_tokens),
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
|
||||
ahr_rows = _get_sheet_rows(client, admin_tokens, form2_id, "AHR")
|
||||
ahr_row = _find_ahr_row_by_expense_item(ahr_rows, AHR_RENT_PARENT_EXPENSE_ITEM)
|
||||
assert ahr_row["data"]["plan"]["q1"] == 100
|
||||
Loading…
x
Reference in New Issue
Block a user