tech-num-fix: поправил формирование технического номера проекта, исправил шаблоны для формы 4, поправил получение стадий у проектов #104
82
api/alembic/versions/0018_technical_number.py
Normal file
82
api/alembic/versions/0018_technical_number.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
|
||||||
|
revision = "0018"
|
||||||
|
down_revision = "0017"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
_DOLLAR_TAG_RE = re.compile(r"\$\w+\$")
|
||||||
|
|
||||||
|
|
||||||
|
def _find_dollar_tag(line: str) -> str | None:
|
||||||
|
m = _DOLLAR_TAG_RE.search(line.strip())
|
||||||
|
return m.group(0) if m else None
|
||||||
|
|
||||||
|
|
||||||
|
def _split_statements(sql: str) -> list[str]:
|
||||||
|
statements: list[str] = []
|
||||||
|
current: list[str] = []
|
||||||
|
in_dollar = False
|
||||||
|
dollar_tag: str | None = None
|
||||||
|
|
||||||
|
for line in sql.split("\n"):
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped.startswith("--"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not in_dollar:
|
||||||
|
tag = _find_dollar_tag(stripped)
|
||||||
|
if tag and tag.endswith("$") and tag.startswith("$"):
|
||||||
|
dollar_tag = tag
|
||||||
|
in_dollar = True
|
||||||
|
current.append(line)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if in_dollar and dollar_tag and stripped.startswith(dollar_tag):
|
||||||
|
after = stripped[len(dollar_tag):].strip()
|
||||||
|
if after == ";" or after == "":
|
||||||
|
in_dollar = False
|
||||||
|
dollar_tag = None
|
||||||
|
if after == ";":
|
||||||
|
current.append(line)
|
||||||
|
statements.append("\n".join(current))
|
||||||
|
current = []
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not in_dollar and stripped.rstrip().endswith(";"):
|
||||||
|
current.append(line)
|
||||||
|
statements.append("\n".join(current))
|
||||||
|
current = []
|
||||||
|
continue
|
||||||
|
|
||||||
|
current.append(line)
|
||||||
|
|
||||||
|
remaining = "\n".join(current).strip()
|
||||||
|
if remaining:
|
||||||
|
statements.append(remaining)
|
||||||
|
|
||||||
|
return statements
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
ddl_path = os.path.join(os.path.dirname(__file__), "sql", "0018_technical_number.sql")
|
||||||
|
with open(ddl_path) as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
statements = _split_statements(content)
|
||||||
|
for stmt in statements:
|
||||||
|
stripped = stmt.strip().rstrip(";").strip()
|
||||||
|
if not stripped:
|
||||||
|
continue
|
||||||
|
if all(l.strip().startswith("--") or not l.strip() for l in stripped.split("\n")):
|
||||||
|
continue
|
||||||
|
op.execute(stripped)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
pass
|
||||||
232
api/alembic/versions/sql/0018_technical_number.sql
Normal file
232
api/alembic/versions/sql/0018_technical_number.sql
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
-- МИГРАЦИЯ: Автогенерация technical_number + запрет ручного редактирования
|
||||||
|
-- Формат: YYMM_<код РФ/ВСП 4 цифры><порядковый счётчик 2 цифры>
|
||||||
|
-- Пример: 2606_180801 (26 — год, 06 — месяц, 1808 — org_unit id, 01 — счётчик)
|
||||||
|
-- ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
-- Уникальность технического номера проекта
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_v3_project_technical_number
|
||||||
|
ON v3.project (technical_number);
|
||||||
|
|
||||||
|
-- add_project: генерируем technical_number автоматически при создании проекта
|
||||||
|
CREATE OR REPLACE FUNCTION v3.add_project(p_name character varying, p_year integer, p_org_unit_id integer, p_level character varying DEFAULT 'project'::character varying, p_parent_id integer DEFAULT NULL::integer, p_project_type character varying DEFAULT NULL::character varying, p_vsp_format character varying DEFAULT NULL::character varying, p_placement_type character varying DEFAULT NULL::character varying, p_object_address character varying DEFAULT NULL::character varying, p_staff_count integer DEFAULT NULL::integer, p_total_area numeric DEFAULT NULL::numeric, p_system_code character varying DEFAULT NULL::character varying)
|
||||||
|
RETURNS TABLE(project_id integer, limit_report_id integer, current_expenses_report_id integer)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_pid INT;
|
||||||
|
v_lim INT;
|
||||||
|
v_cur INT;
|
||||||
|
v_seq INT;
|
||||||
|
v_technical_number VARCHAR;
|
||||||
|
BEGIN
|
||||||
|
IF p_name IS NULL OR length(trim(p_name)) = 0 THEN
|
||||||
|
RAISE EXCEPTION 'p_name обязателен';
|
||||||
|
END IF;
|
||||||
|
IF p_year IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'p_year обязателен';
|
||||||
|
END IF;
|
||||||
|
IF p_org_unit_id IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'p_org_unit_id обязателен';
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM v3.org_unit ou WHERE id = p_org_unit_id AND ou.is_active) THEN
|
||||||
|
RAISE EXCEPTION 'org_unit id=% не существует', p_org_unit_id;
|
||||||
|
END IF;
|
||||||
|
IF p_parent_id IS NOT NULL AND
|
||||||
|
NOT EXISTS (SELECT 1 FROM v3.project WHERE id = p_parent_id AND level = 'program') THEN
|
||||||
|
RAISE EXCEPTION 'parent project id=% не существует или не имеет level=program', p_parent_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
-- Порядковый счётчик проекта для данного РФ/ВСП (org_unit)
|
||||||
|
SELECT COALESCE(MAX(right(technical_number, 2)::int), 0) + 1
|
||||||
|
INTO v_seq
|
||||||
|
FROM v3.project
|
||||||
|
WHERE org_unit_id = p_org_unit_id
|
||||||
|
AND technical_number IS NOT NULL;
|
||||||
|
|
||||||
|
v_technical_number := to_char(now(), 'YYMM') || '_'
|
||||||
|
|| lpad(p_org_unit_id::text, 4, '0')
|
||||||
|
|| lpad(v_seq::text, 2, '0');
|
||||||
|
|
||||||
|
INSERT INTO v3.project (
|
||||||
|
name, level, parent_id, org_unit_id,
|
||||||
|
project_type, vsp_format, placement_type, object_address, staff_count, total_area,
|
||||||
|
system_code, technical_number
|
||||||
|
) VALUES (
|
||||||
|
p_name, p_level, p_parent_id, p_org_unit_id,
|
||||||
|
p_project_type, p_vsp_format, p_placement_type, p_object_address, p_staff_count, p_total_area,
|
||||||
|
p_system_code, v_technical_number
|
||||||
|
) RETURNING id INTO v_pid;
|
||||||
|
|
||||||
|
-- Первый год: 2 отчёта (LIMIT + CURRENT_EXPENSES) + snapshot фаз + сетка статей.
|
||||||
|
SELECT y.limit_report_id, y.current_expenses_report_id
|
||||||
|
INTO v_lim, v_cur
|
||||||
|
FROM v3.add_project_year(v_pid, p_year, false) y;
|
||||||
|
|
||||||
|
-- Аудит: PROJECT_CREATE — все заполненные поля + id отчётов
|
||||||
|
PERFORM v3.log_event(
|
||||||
|
'PROJECT_CREATE', 'PROJECT',
|
||||||
|
jsonb_strip_nulls(jsonb_build_object(
|
||||||
|
'project_id', v_pid,
|
||||||
|
'name', p_name,
|
||||||
|
'year', p_year,
|
||||||
|
'level', p_level,
|
||||||
|
'parent_id', p_parent_id,
|
||||||
|
'org_unit_id', p_org_unit_id,
|
||||||
|
'project_type', p_project_type,
|
||||||
|
'vsp_format', p_vsp_format,
|
||||||
|
'placement_type', p_placement_type,
|
||||||
|
'object_address', p_object_address,
|
||||||
|
'system_code', p_system_code,
|
||||||
|
'technical_number', v_technical_number,
|
||||||
|
'staff_count', p_staff_count,
|
||||||
|
'total_area', p_total_area,
|
||||||
|
'limit_report_id', v_lim,
|
||||||
|
'current_expenses_report_id', v_cur
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
|
RETURN QUERY SELECT v_pid, v_lim, v_cur;
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
;
|
||||||
|
|
||||||
|
-- upd_project: убираем параметр technical_number (редактировать больше нельзя)
|
||||||
|
DROP FUNCTION IF EXISTS v3.upd_project(integer, varchar, varchar, varchar, varchar, varchar, varchar, varchar, integer, numeric, integer, date, date, date, date, varchar);
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION v3.upd_project(
|
||||||
|
p_project_id integer,
|
||||||
|
p_name character varying DEFAULT NULL,
|
||||||
|
p_status character varying DEFAULT NULL,
|
||||||
|
p_project_type character varying DEFAULT NULL,
|
||||||
|
p_vsp_format character varying DEFAULT NULL,
|
||||||
|
p_placement_type character varying DEFAULT NULL,
|
||||||
|
p_object_address character varying DEFAULT NULL,
|
||||||
|
p_staff_count integer DEFAULT NULL,
|
||||||
|
p_total_area numeric DEFAULT NULL,
|
||||||
|
p_org_unit_id integer DEFAULT NULL,
|
||||||
|
p_krf_decision_date date DEFAULT NULL,
|
||||||
|
p_fk_decision_date date DEFAULT NULL,
|
||||||
|
p_board_decision_date date DEFAULT NULL,
|
||||||
|
p_open_relocate_close_date date DEFAULT NULL,
|
||||||
|
p_funding_by_ko_decision character varying DEFAULT NULL
|
||||||
|
)
|
||||||
|
RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_old v3.project%ROWTYPE;
|
||||||
|
v_changes JSONB := '{}'::jsonb;
|
||||||
|
BEGIN
|
||||||
|
SELECT * INTO v_old FROM v3.project WHERE id = p_project_id;
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RAISE EXCEPTION 'project id=% не существует', p_project_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_name IS NOT NULL AND p_name IS DISTINCT FROM v_old.name THEN
|
||||||
|
UPDATE v3.project SET name = p_name WHERE id = p_project_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object('name',
|
||||||
|
jsonb_build_object('before', v3._diff_val(v_old.name), 'after', v3._diff_val(p_name)));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_status IS NOT NULL AND p_status IS DISTINCT FROM v_old.status THEN
|
||||||
|
UPDATE v3.project SET status = p_status WHERE id = p_project_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object('status',
|
||||||
|
jsonb_build_object('before', v3._diff_val(v_old.status), 'after', v3._diff_val(p_status)));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_project_type IS NOT NULL AND p_project_type IS DISTINCT FROM v_old.project_type THEN
|
||||||
|
UPDATE v3.project SET project_type = p_project_type WHERE id = p_project_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object('project_type',
|
||||||
|
jsonb_build_object('before', v3._diff_val(v_old.project_type), 'after', v3._diff_val(p_project_type)));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_vsp_format IS NOT NULL AND p_vsp_format IS DISTINCT FROM v_old.vsp_format THEN
|
||||||
|
UPDATE v3.project SET vsp_format = p_vsp_format WHERE id = p_project_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object('vsp_format',
|
||||||
|
jsonb_build_object('before', v3._diff_val(v_old.vsp_format), 'after', v3._diff_val(p_vsp_format)));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_placement_type IS NOT NULL AND p_placement_type IS DISTINCT FROM v_old.placement_type THEN
|
||||||
|
UPDATE v3.project SET placement_type = p_placement_type WHERE id = p_project_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object('placement_type',
|
||||||
|
jsonb_build_object('before', v3._diff_val(v_old.placement_type), 'after', v3._diff_val(p_placement_type)));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_object_address IS NOT NULL AND p_object_address IS DISTINCT FROM v_old.object_address THEN
|
||||||
|
UPDATE v3.project SET object_address = p_object_address WHERE id = p_project_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object('object_address',
|
||||||
|
jsonb_build_object('before', v3._diff_val(v_old.object_address), 'after', v3._diff_val(p_object_address)));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_staff_count IS NOT NULL AND p_staff_count IS DISTINCT FROM v_old.staff_count THEN
|
||||||
|
UPDATE v3.project SET staff_count = p_staff_count WHERE id = p_project_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object('staff_count',
|
||||||
|
jsonb_build_object('before', v3._diff_val(v_old.staff_count), 'after', v3._diff_val(p_staff_count)));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_total_area IS NOT NULL AND p_total_area IS DISTINCT FROM v_old.total_area THEN
|
||||||
|
UPDATE v3.project SET total_area = p_total_area WHERE id = p_project_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object('total_area',
|
||||||
|
jsonb_build_object('before', v3._diff_val(v_old.total_area), 'after', v3._diff_val(p_total_area)));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_org_unit_id IS NOT NULL AND p_org_unit_id IS DISTINCT FROM v_old.org_unit_id THEN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM v3.org_unit WHERE id = p_org_unit_id) THEN
|
||||||
|
RAISE EXCEPTION 'org_unit id=% не существует', p_org_unit_id;
|
||||||
|
END IF;
|
||||||
|
UPDATE v3.project SET org_unit_id = p_org_unit_id WHERE id = p_project_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object('org_unit_id',
|
||||||
|
jsonb_build_object('before', v3._diff_val(v_old.org_unit_id), 'after', v3._diff_val(p_org_unit_id)));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_krf_decision_date IS NOT NULL AND p_krf_decision_date IS DISTINCT FROM v_old.krf_decision_date THEN
|
||||||
|
UPDATE v3.project SET krf_decision_date = p_krf_decision_date WHERE id = p_project_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object('krf_decision_date',
|
||||||
|
jsonb_build_object('before', v3._diff_val(v_old.krf_decision_date), 'after', v3._diff_val(p_krf_decision_date)));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_fk_decision_date IS NOT NULL AND p_fk_decision_date IS DISTINCT FROM v_old.fk_decision_date THEN
|
||||||
|
UPDATE v3.project SET fk_decision_date = p_fk_decision_date WHERE id = p_project_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object('fk_decision_date',
|
||||||
|
jsonb_build_object('before', v3._diff_val(v_old.fk_decision_date), 'after', v3._diff_val(p_fk_decision_date)));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_board_decision_date IS NOT NULL AND p_board_decision_date IS DISTINCT FROM v_old.board_decision_date THEN
|
||||||
|
UPDATE v3.project SET board_decision_date = p_board_decision_date WHERE id = p_project_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object('board_decision_date',
|
||||||
|
jsonb_build_object('before', v3._diff_val(v_old.board_decision_date), 'after', v3._diff_val(p_board_decision_date)));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_open_relocate_close_date IS NOT NULL AND p_open_relocate_close_date IS DISTINCT FROM v_old.open_relocate_close_date THEN
|
||||||
|
UPDATE v3.project SET open_relocate_close_date = p_open_relocate_close_date WHERE id = p_project_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object('open_relocate_close_date',
|
||||||
|
jsonb_build_object('before', v3._diff_val(v_old.open_relocate_close_date), 'after', v3._diff_val(p_open_relocate_close_date)));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_funding_by_ko_decision IS NOT NULL AND p_funding_by_ko_decision IS DISTINCT FROM v_old.funding_by_ko_decision THEN
|
||||||
|
UPDATE v3.project SET funding_by_ko_decision = p_funding_by_ko_decision WHERE id = p_project_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object('funding_by_ko_decision',
|
||||||
|
jsonb_build_object('before', v3._diff_val(v_old.funding_by_ko_decision), 'after', v3._diff_val(p_funding_by_ko_decision)));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_changes <> '{}'::jsonb THEN
|
||||||
|
PERFORM v3.log_event(
|
||||||
|
'PROJECT_UPDATE', 'PROJECT',
|
||||||
|
jsonb_build_object(
|
||||||
|
'project_id', p_project_id,
|
||||||
|
'entity_id', p_project_id,
|
||||||
|
'changes', v_changes
|
||||||
|
), null, null, COALESCE(p_org_unit_id, v_old.org_unit_id)
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
;
|
||||||
|
|
||||||
|
|
||||||
|
UPDATE v3.phase_template
|
||||||
|
SET column_keys = '{plan.q1,plan.q2,plan.q3,plan.q4,plan.year,plan.comment,contract_summary.total,contract_summary.reference,contract_summary.counterparty,contract_summary.deadline,contract_summary.comment,contract_summary.future_y1,contract_summary.future_y2,contract_summary.other_ssp,contract_summary.centralized_flag,allocation.contract_ref,allocation.allocation_purpose,seq_dfip.q1,seq_dfip.q2,seq_dfip.q3,seq_dfip.q4,seq_dfip.year,seq_dfip.justification,reserve.q1,reserve.q2,reserve.q3,reserve.q4,reserve.year,reserve.justification,approved.q1,approved.q2,approved.q3,approved.q4,approved.year,collegial.approved,collegial.protocol,collegial.note,ckk.ceiling,ckk.q1,ckk.q2,ckk.q3,ckk.q4,ckk.rf_schedule,ckk.deadline,ckk.proc_plan,ckk.proc_method,ckk.comment,contract.counterparty,contract.reference,contract.addenda,contract.subject,contract.currency,contract.ceiling,contract.q1,contract.q2,contract.q3,contract.q4,contract.rf_schedule,contract.vat_rate,contract.exchange_rate,contract.amount_foreign,contract.deadline,contract.scheme,contract.act,contract.comment,q1.adj_current,q1.adj_ssp,q1.adj_rf,q1.adj_reserve,q1.adj_comment,q1.corrected_plan,q1.pay_date,q1.pay_amount,q1.pay_ho,q1.pay_rf,q1.pay_comment,q1.pay_act,q1.booking,q1.actual_m1,q1.actual_m2,q1.actual_m3,q1.actual_quarter,q1.residual_after_booking,q1.residual_after_actual,q1.transfer_q2,q1.transfer_q3,q1.transfer_q4,q1.transfer_econ,q1.total,q2.target_change,q2.base_correction,q2.revision_inc,q2.revision_seq,q2.revision_comment,q2.new_plan,q2.adj_current,q2.adj_ssp,q2.adj_rf,q2.adj_reserve,q2.corrected_plan,q2.pay_date,q2.pay_amount,q2.pay_ho,q2.pay_rf,q2.pay_comment,q2.pay_act,q2.booking,q2.actual_m1,q2.actual_m2,q2.actual_m3,q2.actual_quarter,q2.residual_after_booking,q2.residual_after_actual,q2.transfer_q3,q2.transfer_q4,q2.transfer_econ,q2.total,q3.target_change,q3.base_correction,q3.revision_inc,q3.revision_seq,q3.revision_comment,q3.new_plan,q3.adj_current,q3.adj_ssp,q3.adj_rf,q3.adj_reserve,q3.corrected_plan,q3.pay_date,q3.pay_amount,q3.pay_ho,q3.pay_rf,q3.pay_comment,q3.pay_act,q3.booking,q3.actual_m1,q3.actual_m2,q3.actual_m3,q3.actual_quarter,q3.residual_after_booking,q3.residual_after_actual,q3.transfer_q4,q3.transfer_econ,q3.total,q4.target_change,q4.base_correction,q4.revision_inc,q4.revision_seq,q4.revision_comment,q4.new_plan,q4.adj_current,q4.adj_ssp,q4.adj_rf,q4.adj_reserve,q4.corrected_plan,q4.pay_date,q4.pay_amount,q4.pay_ho,q4.pay_rf,q4.pay_comment,q4.pay_act,q4.booking,q4.actual_m1,q4.actual_m2,q4.actual_m3,q4.actual_quarter,q4.actual_spod,q4.residual_after_booking,q4.residual_after_actual,q4.transfer_econ,q4.total,totals.fact_year}'
|
||||||
|
WHERE form_type = 'FORM_4';
|
||||||
|
|
||||||
|
ALTER TABLE v3.form_phase DROP CONSTRAINT chk_v4_form_phase_no_admin;
|
||||||
@ -207,7 +207,7 @@ async def get_form3_phases(
|
|||||||
)
|
)
|
||||||
if phases:
|
if phases:
|
||||||
project = await project_service.get_instance(user=current_user, project_id=project_id)
|
project = await project_service.get_instance(user=current_user, project_id=project_id)
|
||||||
org_unit = await org_unit_service.get(user=current_user, org_unit_id=project.org_unit_id)
|
org_unit = await org_unit_service.get(org_unit_id=project.org_unit_id)
|
||||||
result = [Form3PhaseResponse.model_validate(p) for p in phases]
|
result = [Form3PhaseResponse.model_validate(p) for p in phases]
|
||||||
if org_unit.utc_offset:
|
if org_unit.utc_offset:
|
||||||
tz = timezone_from_offset(org_unit.utc_offset)
|
tz = timezone_from_offset(org_unit.utc_offset)
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import enum
|
|||||||
from typing import Any, Dict, Generic, List, Literal, Optional, TypeVar
|
from typing import Any, Dict, Generic, List, Literal, Optional, TypeVar
|
||||||
from sqlalchemy.exc import MissingGreenlet
|
from sqlalchemy.exc import MissingGreenlet
|
||||||
|
|
||||||
FormPhaseRole = Literal["DFIP", "EXECUTOR_RF"]
|
FormPhaseRole = Literal["DFIP", "EXECUTOR_RF", "ADMIN"]
|
||||||
|
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator, model_validator
|
from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator, model_validator
|
||||||
@ -384,7 +384,6 @@ class UpdProjectBody(BaseModel):
|
|||||||
pattern=r"^[^+\-\/\\=&*\s]{1,30}$",
|
pattern=r"^[^+\-\/\\=&*\s]{1,30}$",
|
||||||
)
|
)
|
||||||
status: Optional[Literal["created", "agreed", "archived"]] = None
|
status: Optional[Literal["created", "agreed", "archived"]] = None
|
||||||
technical_number: Optional[str] = None
|
|
||||||
project_type: Optional[ProjectTypeLiteral] = None
|
project_type: Optional[ProjectTypeLiteral] = None
|
||||||
vsp_format: Optional[VspFormatLiteral] = None
|
vsp_format: Optional[VspFormatLiteral] = None
|
||||||
placement_type: Optional[PlacementTypeLiteral] = None
|
placement_type: Optional[PlacementTypeLiteral] = None
|
||||||
|
|||||||
@ -31,6 +31,8 @@ class ProjectRepository:
|
|||||||
"total_area": float(project.total_area) if project.total_area is not None else None,
|
"total_area": float(project.total_area) if project.total_area is not None else None,
|
||||||
"org_unit_id": project.org_unit_id,
|
"org_unit_id": project.org_unit_id,
|
||||||
"org_unit_name": org_unit_name,
|
"org_unit_name": org_unit_name,
|
||||||
|
"status": project.status,
|
||||||
|
"technical_number": project.technical_number,
|
||||||
"created_at": project.created_at.isoformat() if project.created_at else None,
|
"created_at": project.created_at.isoformat() if project.created_at else None,
|
||||||
"report_count": int(report_count),
|
"report_count": int(report_count),
|
||||||
"years": years,
|
"years": years,
|
||||||
@ -443,7 +445,6 @@ class ProjectRepository:
|
|||||||
CAST(:project_id AS INT),
|
CAST(:project_id AS INT),
|
||||||
CAST(:name AS VARCHAR),
|
CAST(:name AS VARCHAR),
|
||||||
CAST(:status AS VARCHAR),
|
CAST(:status AS VARCHAR),
|
||||||
CAST(:technical_number AS VARCHAR),
|
|
||||||
CAST(:project_type AS VARCHAR),
|
CAST(:project_type AS VARCHAR),
|
||||||
CAST(:vsp_format AS VARCHAR),
|
CAST(:vsp_format AS VARCHAR),
|
||||||
CAST(:placement_type AS VARCHAR),
|
CAST(:placement_type AS VARCHAR),
|
||||||
@ -463,7 +464,6 @@ class ProjectRepository:
|
|||||||
"project_id": project_id,
|
"project_id": project_id,
|
||||||
"name": data.get("name"),
|
"name": data.get("name"),
|
||||||
"status": data.get("status"),
|
"status": data.get("status"),
|
||||||
"technical_number": data.get("technical_number"),
|
|
||||||
"project_type": data.get("project_type"),
|
"project_type": data.get("project_type"),
|
||||||
"vsp_format": data.get("vsp_format"),
|
"vsp_format": data.get("vsp_format"),
|
||||||
"placement_type": data.get("placement_type"),
|
"placement_type": data.get("placement_type"),
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -262,12 +263,17 @@ def _get_project_with_reports(client, auth_headers, admin_tokens, project_id):
|
|||||||
def test_upd_project_smoke(client, admin_tokens, auth_headers):
|
def test_upd_project_smoke(client, admin_tokens, auth_headers):
|
||||||
project_id, name = _create_project(client, admin_tokens, auth_headers)
|
project_id, name = _create_project(client, admin_tokens, auth_headers)
|
||||||
|
|
||||||
|
before = _get_project_with_reports(client, auth_headers, admin_tokens, project_id)
|
||||||
|
assert before is not None
|
||||||
|
assert before["technical_number"]
|
||||||
|
technical_number_before = before["technical_number"]
|
||||||
|
|
||||||
response = client.patch(
|
response = client.patch(
|
||||||
f"/api/v1/project/{project_id}",
|
f"/api/v1/project/{project_id}",
|
||||||
json={
|
json={
|
||||||
"name": f"{name}_U",
|
"name": f"{name}_U",
|
||||||
"status": "agreed",
|
"status": "agreed",
|
||||||
"technical_number": "2606_180801",
|
"technical_number": "9999_999999",
|
||||||
"krf_decision_date": "2026-06-11",
|
"krf_decision_date": "2026-06-11",
|
||||||
"fk_decision_date": "2026-06-18",
|
"fk_decision_date": "2026-06-18",
|
||||||
"board_decision_date": "2026-07-02",
|
"board_decision_date": "2026-07-02",
|
||||||
@ -283,7 +289,9 @@ def test_upd_project_smoke(client, admin_tokens, auth_headers):
|
|||||||
assert project is not None
|
assert project is not None
|
||||||
assert project["name"] == f"{name}_U"
|
assert project["name"] == f"{name}_U"
|
||||||
assert project["status"] == "agreed"
|
assert project["status"] == "agreed"
|
||||||
assert project["technical_number"] == "2606_180801"
|
# technical_number генерируется автоматически и не редактируется
|
||||||
|
assert project["technical_number"] == technical_number_before
|
||||||
|
assert re.fullmatch(r"\d{4}_\d{6}", project["technical_number"])
|
||||||
assert project["krf_decision_date"] == "2026-06-11"
|
assert project["krf_decision_date"] == "2026-06-11"
|
||||||
assert project["fk_decision_date"] == "2026-06-18"
|
assert project["fk_decision_date"] == "2026-06-18"
|
||||||
assert project["board_decision_date"] == "2026-07-02"
|
assert project["board_decision_date"] == "2026-07-02"
|
||||||
@ -293,6 +301,15 @@ def test_upd_project_smoke(client, admin_tokens, auth_headers):
|
|||||||
assert project["placement_type"] == "Собственность"
|
assert project["placement_type"] == "Собственность"
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_project_auto_generates_technical_number(client, admin_tokens, auth_headers):
|
||||||
|
project_id, _ = _create_project(client, admin_tokens, auth_headers)
|
||||||
|
|
||||||
|
project = _get_project_with_reports(client, auth_headers, admin_tokens, project_id)
|
||||||
|
assert project is not None
|
||||||
|
assert project["technical_number"]
|
||||||
|
assert re.fullmatch(r"\d{4}_\d{6}", project["technical_number"])
|
||||||
|
|
||||||
|
|
||||||
def test_upd_smeta_smoke(client, admin_tokens, auth_headers):
|
def test_upd_smeta_smoke(client, admin_tokens, auth_headers):
|
||||||
project_id, _ = _create_project(client, admin_tokens, auth_headers, year=2026)
|
project_id, _ = _create_project(client, admin_tokens, auth_headers, year=2026)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user