Compare commits
2 Commits
f27ea41875
...
ab2c1151fa
| Author | SHA1 | Date | |
|---|---|---|---|
| ab2c1151fa | |||
| c8dbba7e57 |
180
api/alembic/versions/0007_form_constraints.py
Normal file
180
api/alembic/versions/0007_form_constraints.py
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
revision = "0007"
|
||||||
|
down_revision = "0006"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.execute("""
|
||||||
|
CREATE OR REPLACE FUNCTION v3.v_form1_smeta(p_year integer, p_org_unit_id integer)
|
||||||
|
RETURNS TABLE(row_type character varying, depth integer, section_code character varying, name character varying, supp_plan_q1 numeric, supp_plan_q2 numeric, supp_plan_q3 numeric, supp_plan_q4 numeric, supp_plan_year numeric, dev_plan_q1 numeric, dev_plan_q2 numeric, dev_plan_q3 numeric, dev_plan_q4 numeric, dev_plan_year numeric, total_plan_year numeric, supp_appr_q1 numeric, supp_appr_q2 numeric, supp_appr_q3 numeric, supp_appr_q4 numeric, supp_appr_year numeric, dev_appr_q1 numeric, dev_appr_q2 numeric, dev_appr_q3 numeric, dev_appr_q4 numeric, dev_appr_year numeric, total_appr_year numeric, supp_act_q1 numeric, supp_act_q2 numeric, supp_act_q3 numeric, supp_act_q4 numeric, supp_act_year numeric, dev_act_q1 numeric, dev_act_q2 numeric, dev_act_q3 numeric, dev_act_q4 numeric, dev_act_year numeric, total_act_year numeric, supp_corr_q2 numeric, supp_corr_q3 numeric, supp_corr_q4 numeric, dev_corr_q2 numeric, dev_corr_q3 numeric, dev_corr_q4 numeric)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
STABLE
|
||||||
|
SET search_path TO 'v3', 'pg_catalog'
|
||||||
|
AS $function$
|
||||||
|
#variable_conflict use_column
|
||||||
|
DECLARE
|
||||||
|
v_ahr INT; v_cap INT; v_oper INT;
|
||||||
|
v_sections TEXT[] := ARRAY['plan','approved','q1','q2','q3','q4'];
|
||||||
|
BEGIN
|
||||||
|
-- Одна budget_form на лист содержит ОБА направления (budget_line.direction).
|
||||||
|
-- Support/Development различаются параметром p_direction, а не отдельной формой.
|
||||||
|
WITH ranked AS (
|
||||||
|
SELECT bf.id AS fid, ei.sheet AS sh
|
||||||
|
FROM budget_form bf
|
||||||
|
JOIN budget_line bl ON bl.budget_form_id = bf.id
|
||||||
|
JOIN expense_item ei ON ei.id = bl.expense_item_id
|
||||||
|
WHERE bf.year = p_year AND bf.org_unit_id = p_org_unit_id AND bf.form_type_code = 'FORM_1'
|
||||||
|
GROUP BY bf.id, ei.sheet
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
MAX(CASE WHEN sh='AHR' THEN fid END),
|
||||||
|
MAX(CASE WHEN sh='CAP' THEN fid END),
|
||||||
|
MAX(CASE WHEN sh='OPER' THEN fid END)
|
||||||
|
INTO v_ahr, v_cap, v_oper
|
||||||
|
FROM ranked;
|
||||||
|
|
||||||
|
RETURN QUERY
|
||||||
|
WITH
|
||||||
|
ahr_s AS (
|
||||||
|
SELECT col_C_section AS sc, col_F_name AS nm, depth, row_type,
|
||||||
|
col_H_plan_q1 AS pq1, col_I_plan_q2 AS pq2, col_J_plan_q3 AS pq3, col_K_plan_q4 AS pq4, col_L_plan_year AS pyr,
|
||||||
|
col_AM_approved_q1 AS aq1, col_AN_approved_q2 AS aq2, col_AO_approved_q3 AS aq3, col_AP_approved_q4 AS aq4, col_AQ_approved_year AS ayr,
|
||||||
|
col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3,
|
||||||
|
COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4,
|
||||||
|
col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4
|
||||||
|
FROM v_form1_sheet_sections(v_ahr, 'AHR', 'Support', v_sections)
|
||||||
|
WHERE row_type IN ('ROOT','GROUP','ITEM')
|
||||||
|
),
|
||||||
|
ahr_d AS (
|
||||||
|
SELECT col_C_section AS sc, col_F_name AS nm, depth, row_type,
|
||||||
|
col_H_plan_q1 AS pq1, col_I_plan_q2 AS pq2, col_J_plan_q3 AS pq3, col_K_plan_q4 AS pq4, col_L_plan_year AS pyr,
|
||||||
|
col_AM_approved_q1 AS aq1, col_AN_approved_q2 AS aq2, col_AO_approved_q3 AS aq3, col_AP_approved_q4 AS aq4, col_AQ_approved_year AS ayr,
|
||||||
|
col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3,
|
||||||
|
COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4,
|
||||||
|
col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4
|
||||||
|
FROM v_form1_sheet_sections(v_ahr, 'AHR', 'Development', v_sections)
|
||||||
|
WHERE row_type IN ('ROOT','GROUP','ITEM')
|
||||||
|
),
|
||||||
|
cap_s AS (
|
||||||
|
SELECT col_C_section AS sc, col_F_name AS nm, depth, row_type,
|
||||||
|
col_H_plan_q1 AS pq1, col_I_plan_q2 AS pq2, col_J_plan_q3 AS pq3, col_K_plan_q4 AS pq4, col_L_plan_year AS pyr,
|
||||||
|
col_AM_approved_q1 AS aq1, col_AN_approved_q2 AS aq2, col_AO_approved_q3 AS aq3, col_AP_approved_q4 AS aq4, col_AQ_approved_year AS ayr,
|
||||||
|
col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3,
|
||||||
|
COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4,
|
||||||
|
col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4
|
||||||
|
FROM v_form1_sheet_sections(v_cap, 'CAP', 'Support', v_sections)
|
||||||
|
WHERE row_type IN ('ROOT','GROUP','ITEM')
|
||||||
|
),
|
||||||
|
cap_d AS (
|
||||||
|
SELECT col_C_section AS sc, col_F_name AS nm, depth, row_type,
|
||||||
|
col_H_plan_q1 AS pq1, col_I_plan_q2 AS pq2, col_J_plan_q3 AS pq3, col_K_plan_q4 AS pq4, col_L_plan_year AS pyr,
|
||||||
|
col_AM_approved_q1 AS aq1, col_AN_approved_q2 AS aq2, col_AO_approved_q3 AS aq3, col_AP_approved_q4 AS aq4, col_AQ_approved_year AS ayr,
|
||||||
|
col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3,
|
||||||
|
COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4,
|
||||||
|
col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4
|
||||||
|
FROM v_form1_sheet_sections(v_cap, 'CAP', 'Development', v_sections)
|
||||||
|
WHERE row_type IN ('ROOT','GROUP','ITEM')
|
||||||
|
),
|
||||||
|
oper_s AS (
|
||||||
|
SELECT col_C_section AS sc, col_F_name AS nm, depth, row_type,
|
||||||
|
col_H_plan_q1 AS pq1, col_I_plan_q2 AS pq2, col_J_plan_q3 AS pq3, col_K_plan_q4 AS pq4, col_L_plan_year AS pyr,
|
||||||
|
col_AM_approved_q1 AS aq1, col_AN_approved_q2 AS aq2, col_AO_approved_q3 AS aq3, col_AP_approved_q4 AS aq4, col_AQ_approved_year AS ayr,
|
||||||
|
col_CS_q1_actual_quarter AS fq1, col_EB_q2_actual_quarter AS fq2, col_FJ_q3_actual_quarter AS fq3,
|
||||||
|
COALESCE(col_GQ_q4_actual_quarter,0) + COALESCE(col_GR_q4_actual_spod,0) AS fq4,
|
||||||
|
col_DH_q2_new_plan AS np2, col_EP_q3_new_plan AS np3, col_FW_q4_new_plan AS np4
|
||||||
|
FROM v_form1_sheet_sections(v_oper, 'OPER', 'Support', v_sections)
|
||||||
|
WHERE row_type IN ('ROOT','GROUP','ITEM')
|
||||||
|
),
|
||||||
|
merged AS (
|
||||||
|
SELECT COALESCE(s.row_type, d.row_type) AS row_type,
|
||||||
|
COALESCE(s.depth, d.depth) AS depth,
|
||||||
|
COALESCE(s.sc, d.sc) AS sc,
|
||||||
|
COALESCE(s.nm, d.nm) AS nm,
|
||||||
|
s.pq1, s.pq2, s.pq3, s.pq4, s.pyr,
|
||||||
|
d.pq1 AS d_pq1, d.pq2 AS d_pq2, d.pq3 AS d_pq3, d.pq4 AS d_pq4, d.pyr AS d_pyr,
|
||||||
|
s.aq1, s.aq2, s.aq3, s.aq4, s.ayr,
|
||||||
|
d.aq1 AS d_aq1, d.aq2 AS d_aq2, d.aq3 AS d_aq3, d.aq4 AS d_aq4, d.ayr AS d_ayr,
|
||||||
|
s.fq1, s.fq2, s.fq3, s.fq4,
|
||||||
|
d.fq1 AS d_fq1, d.fq2 AS d_fq2, d.fq3 AS d_fq3, d.fq4 AS d_fq4,
|
||||||
|
s.np2, s.np3, s.np4,
|
||||||
|
d.np2 AS d_np2, d.np3 AS d_np3, d.np4 AS d_np4
|
||||||
|
FROM ahr_s s FULL OUTER JOIN ahr_d d ON s.sc = d.sc
|
||||||
|
UNION ALL
|
||||||
|
SELECT COALESCE(s.row_type, d.row_type), COALESCE(s.depth, d.depth),
|
||||||
|
COALESCE(s.sc, d.sc), COALESCE(s.nm, d.nm),
|
||||||
|
s.pq1, s.pq2, s.pq3, s.pq4, s.pyr,
|
||||||
|
d.pq1, d.pq2, d.pq3, d.pq4, d.pyr,
|
||||||
|
s.aq1, s.aq2, s.aq3, s.aq4, s.ayr,
|
||||||
|
d.aq1, d.aq2, d.aq3, d.aq4, d.ayr,
|
||||||
|
s.fq1, s.fq2, s.fq3, s.fq4,
|
||||||
|
d.fq1, d.fq2, d.fq3, d.fq4,
|
||||||
|
s.np2, s.np3, s.np4,
|
||||||
|
d.np2, d.np3, d.np4
|
||||||
|
FROM cap_s s FULL OUTER JOIN cap_d d ON s.sc = d.sc
|
||||||
|
UNION ALL
|
||||||
|
SELECT s.row_type, s.depth, s.sc, s.nm,
|
||||||
|
s.pq1, s.pq2, s.pq3, s.pq4, s.pyr,
|
||||||
|
NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC,
|
||||||
|
s.aq1, s.aq2, s.aq3, s.aq4, s.ayr,
|
||||||
|
NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC,
|
||||||
|
s.fq1, s.fq2, s.fq3, s.fq4,
|
||||||
|
NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC,
|
||||||
|
s.np2, s.np3, s.np4,
|
||||||
|
NULL::NUMERIC, NULL::NUMERIC, NULL::NUMERIC
|
||||||
|
FROM oper_s s
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
m.row_type, m.depth, m.sc, m.nm,
|
||||||
|
m.pq1, m.pq2, m.pq3, m.pq4, m.pyr,
|
||||||
|
m.d_pq1, m.d_pq2, m.d_pq3, m.d_pq4, m.d_pyr,
|
||||||
|
COALESCE(m.pyr,0) + COALESCE(m.d_pyr,0) AS total_plan_year,
|
||||||
|
m.aq1, m.aq2, m.aq3, m.aq4, m.ayr,
|
||||||
|
m.d_aq1, m.d_aq2, m.d_aq3, m.d_aq4, m.d_ayr,
|
||||||
|
COALESCE(m.ayr,0) + COALESCE(m.d_ayr,0) AS total_appr_year,
|
||||||
|
m.fq1, m.fq2, m.fq3, m.fq4, COALESCE(m.fq1,0)+COALESCE(m.fq2,0)+COALESCE(m.fq3,0)+COALESCE(m.fq4,0),
|
||||||
|
m.d_fq1, m.d_fq2, m.d_fq3, m.d_fq4, COALESCE(m.d_fq1,0)+COALESCE(m.d_fq2,0)+COALESCE(m.d_fq3,0)+COALESCE(m.d_fq4,0),
|
||||||
|
COALESCE(m.fq1,0)+COALESCE(m.fq2,0)+COALESCE(m.fq3,0)+COALESCE(m.fq4,0)
|
||||||
|
+ COALESCE(m.d_fq1,0)+COALESCE(m.d_fq2,0)+COALESCE(m.d_fq3,0)+COALESCE(m.d_fq4,0),
|
||||||
|
m.np2, m.np3, m.np4,
|
||||||
|
m.d_np2, m.d_np3, m.d_np4
|
||||||
|
FROM merged m
|
||||||
|
ORDER BY m.sc;
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
;
|
||||||
|
""")
|
||||||
|
for t_name in (
|
||||||
|
"reserve", "contract_detail", "ckk", "allocation", "sequestration", "contract_summary",
|
||||||
|
"collegial_approval", "rent_detail", "utility_detail", "plan", "budget_line_quarter"
|
||||||
|
):
|
||||||
|
op.execute(f"""
|
||||||
|
delete from v3.{t_name} where line_id in (
|
||||||
|
select id from v3.budget_line where budget_form_id not in (select min(fid) from (
|
||||||
|
SELECT bf.id AS fid, bf.year as yr, bf.form_type_code as ftc, bf.org_unit_id as oui
|
||||||
|
FROM v3.budget_form bf
|
||||||
|
) ranked
|
||||||
|
group by yr, ftc, oui));
|
||||||
|
""")
|
||||||
|
op.execute("""
|
||||||
|
delete from v3.budget_line where budget_form_id not in (select min(fid) from (
|
||||||
|
SELECT bf.id AS fid, bf.year as yr, bf.form_type_code as ftc, bf.org_unit_id as oui
|
||||||
|
FROM v3.budget_form bf
|
||||||
|
) ranked
|
||||||
|
group by yr, ftc, oui);
|
||||||
|
""")
|
||||||
|
op.execute("""
|
||||||
|
delete from v3.budget_form where id not in (select min(fid) from (
|
||||||
|
SELECT bf.id AS fid, bf.year as yr, bf.form_type_code as ftc, bf.org_unit_id as oui
|
||||||
|
FROM v3.budget_form bf
|
||||||
|
) ranked
|
||||||
|
group by yr, ftc, oui);
|
||||||
|
""")
|
||||||
|
op.execute("ALTER TABLE v3.budget_form ADD CONSTRAINT uq_ftype_org_year UNIQUE (form_type_code, org_unit_id, year)")
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
pass
|
||||||
@ -441,7 +441,7 @@ async def add_form(
|
|||||||
)
|
)
|
||||||
raise HTTPException(404, f"ССП {not_found_ids} не найдены")
|
raise HTTPException(404, f"ССП {not_found_ids} не найдены")
|
||||||
|
|
||||||
new_forms = await bf_service.bulk_create(
|
new_forms = await bf_service.bulk_create_if_not_exists(
|
||||||
form_type_code=form.form_type_code,
|
form_type_code=form.form_type_code,
|
||||||
year=form.year,
|
year=form.year,
|
||||||
org_unit_ids=org_unit_ids,
|
org_unit_ids=org_unit_ids,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
from sqlalchemy import func, select, text
|
from sqlalchemy import func, select, text
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.db.models.form_type import FormTypeEnum
|
||||||
from src.db.models.budget_form import BudgetForm
|
from src.db.models.budget_form import BudgetForm
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
|
|
||||||
@ -58,19 +59,34 @@ class BudgetFormRepository:
|
|||||||
|
|
||||||
async def get(
|
async def get(
|
||||||
self,
|
self,
|
||||||
budget_form_id: int,
|
budget_form_id: int | None = None,
|
||||||
org_unit: int | list[int] | None = None,
|
org_unit: int | list[int] | None = None,
|
||||||
|
form_type_code: FormTypeEnum | str | None = None,
|
||||||
|
year: int | None = None,
|
||||||
load_form_type: bool = False,
|
load_form_type: bool = False,
|
||||||
load_org: bool = False,
|
load_org: bool = False,
|
||||||
) -> BudgetForm | None:
|
) -> BudgetForm | None:
|
||||||
|
assert budget_form_id or all((form_type_code, year, org_unit))
|
||||||
query = select(BudgetForm)
|
query = select(BudgetForm)
|
||||||
|
|
||||||
where = [BudgetForm.id == budget_form_id]
|
where = []
|
||||||
|
if budget_form_id is not None:
|
||||||
|
where.append(BudgetForm.id == budget_form_id)
|
||||||
|
|
||||||
if org_unit is not None:
|
if org_unit is not None:
|
||||||
if isinstance(org_unit, int):
|
if isinstance(org_unit, int):
|
||||||
where.append(BudgetForm.org_unit_id == org_unit)
|
where.append(BudgetForm.org_unit_id == org_unit)
|
||||||
else:
|
else:
|
||||||
where.append(BudgetForm.org_unit_id.in_(org_unit))
|
where.append(BudgetForm.org_unit_id.in_(org_unit))
|
||||||
|
|
||||||
|
if form_type_code is not None:
|
||||||
|
if isinstance(form_type_code, FormTypeEnum):
|
||||||
|
where.append(BudgetForm.form_type_code == form_type_code.value)
|
||||||
|
else:
|
||||||
|
where.append(BudgetForm.form_type_code == form_type_code)
|
||||||
|
|
||||||
|
if year is not None:
|
||||||
|
where.append(BudgetForm.year == year)
|
||||||
|
|
||||||
query = query.where(*where)
|
query = query.where(*where)
|
||||||
if load_org:
|
if load_org:
|
||||||
|
|||||||
@ -79,6 +79,25 @@ class BudgetFormService:
|
|||||||
year=year,
|
year=year,
|
||||||
org_unit_id=org_unit_id,
|
org_unit_id=org_unit_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def create_if_not_exists(
|
||||||
|
self,
|
||||||
|
form_type_code: str,
|
||||||
|
year: int,
|
||||||
|
org_unit_id: int,
|
||||||
|
) -> BudgetForm | None:
|
||||||
|
existing_bf = await self.bf_repo.get(
|
||||||
|
form_type_code=form_type_code,
|
||||||
|
year=year,
|
||||||
|
org_unit=org_unit_id,
|
||||||
|
)
|
||||||
|
if existing_bf:
|
||||||
|
return existing_bf
|
||||||
|
return await self.bf_repo.create(
|
||||||
|
form_type_code=form_type_code,
|
||||||
|
year=year,
|
||||||
|
org_unit_id=org_unit_id,
|
||||||
|
)
|
||||||
|
|
||||||
async def bulk_create(
|
async def bulk_create(
|
||||||
self,
|
self,
|
||||||
@ -96,3 +115,20 @@ class BudgetFormService:
|
|||||||
for org_unit_id in org_unit_ids
|
for org_unit_id in org_unit_ids
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def bulk_create_if_not_exists(
|
||||||
|
self,
|
||||||
|
form_type_code: str,
|
||||||
|
year: int,
|
||||||
|
org_unit_ids: list[int],
|
||||||
|
) -> BudgetForm | None:
|
||||||
|
return await asyncio.gather(
|
||||||
|
*[
|
||||||
|
self.create_if_not_exists(
|
||||||
|
form_type_code=form_type_code,
|
||||||
|
year=year,
|
||||||
|
org_unit_id=org_unit_id,
|
||||||
|
)
|
||||||
|
for org_unit_id in org_unit_ids
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|||||||
@ -161,6 +161,25 @@ def test_forms_add_form_success(client, admin_tokens, auth_headers):
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_forms_add_form_existing_unit(client, admin_tokens, auth_headers):
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1/form/",
|
||||||
|
json={"year": 2026, "form_type_code": "FORM_2", "org_unit_ids": [1, 2]},
|
||||||
|
headers=auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
|
||||||
|
assert "result" in payload
|
||||||
|
assert isinstance(payload["result"], list)
|
||||||
|
assert len(payload["result"]) == 2
|
||||||
|
assert "year" in payload["result"][0]
|
||||||
|
assert payload["result"][0]["year"] == 2026
|
||||||
|
|
||||||
|
assert "id" in payload["result"][1]
|
||||||
|
assert payload["result"][1]["id"] == 4
|
||||||
|
|
||||||
|
|
||||||
def test_forms_add_form_org_unit_not_found(client, admin_tokens, auth_headers):
|
def test_forms_add_form_org_unit_not_found(client, admin_tokens, auth_headers):
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/form/",
|
"/api/v1/form/",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user