15
api/alembic/versions/0062_vsp_clear_closed_at.py
Normal file
15
api/alembic/versions/0062_vsp_clear_closed_at.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from sql_migration import run_sql_file_migration
|
||||||
|
|
||||||
|
|
||||||
|
revision = "0062"
|
||||||
|
down_revision = "0061"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
run_sql_file_migration("0062_vsp_clear_closed_at.sql")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
pass
|
||||||
195
api/alembic/versions/sql/0062_vsp_clear_closed_at.sql
Normal file
195
api/alembic/versions/sql/0062_vsp_clear_closed_at.sql
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
-- AURORA-1886: p_clear_closed_at позволяет снять дату закрытия ВСП (NULL в p_closed_at означает «не менять»).
|
||||||
|
DROP FUNCTION v3.upd_vsp(int4, int4, varchar, varchar, varchar, date, varchar, int4, numeric, date, bool, bool, varchar, int4, varchar, text, varchar, date);
|
||||||
|
|
||||||
|
CREATE FUNCTION v3.upd_vsp(p_vsp_id integer, p_branch_id integer DEFAULT NULL::integer, p_reg_number character varying DEFAULT NULL::character varying, p_address character varying DEFAULT NULL::character varying, p_format character varying DEFAULT NULL::character varying, p_opened_at date DEFAULT NULL::date, p_placement_type character varying DEFAULT NULL::character varying, p_staff_count integer DEFAULT NULL::integer, p_total_area numeric DEFAULT NULL::numeric, p_closed_at date DEFAULT NULL::date, p_is_active boolean DEFAULT NULL::boolean, p_is_deleted boolean DEFAULT NULL::boolean, p_system_code character varying DEFAULT NULL::character varying, p_updated_by integer DEFAULT NULL::integer, p_vsp_type character varying DEFAULT NULL::character varying, p_notes text DEFAULT NULL::text, p_rent_contract_num character varying DEFAULT NULL::character varying, p_rent_end_date date DEFAULT NULL::date, p_clear_closed_at boolean DEFAULT false)
|
||||||
|
RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_old RECORD;
|
||||||
|
v_changes JSONB := '{}'::jsonb;
|
||||||
|
|
||||||
|
v_branch_name_before VARCHAR;
|
||||||
|
v_branch_name_after VARCHAR;
|
||||||
|
|
||||||
|
v_actor_id INT;
|
||||||
|
v_actor_email VARCHAR;
|
||||||
|
v_actor_full_name VARCHAR;
|
||||||
|
BEGIN
|
||||||
|
SELECT v.branch_id, v.reg_number, v.address, v.format, v.opened_at,
|
||||||
|
v.placement_type, v.staff_count, v.total_area, v.closed_at,
|
||||||
|
v.is_active, v.is_deleted, v.system_code,
|
||||||
|
v.vsp_type, v.notes, v.rent_contract_num, v.rent_end_date
|
||||||
|
INTO v_old
|
||||||
|
FROM v3.vsp v
|
||||||
|
WHERE v.id = p_vsp_id;
|
||||||
|
|
||||||
|
IF NOT FOUND THEN
|
||||||
|
RAISE EXCEPTION 'vsp #% не существует', p_vsp_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT ou.title
|
||||||
|
INTO v_branch_name_before
|
||||||
|
FROM v3.org_unit ou
|
||||||
|
WHERE ou.id = v_old.branch_id;
|
||||||
|
|
||||||
|
v_branch_name_after := v_branch_name_before;
|
||||||
|
|
||||||
|
v_actor_id := COALESCE(p_updated_by, v3.current_user_id());
|
||||||
|
IF v_actor_id IS NOT NULL THEN
|
||||||
|
SELECT u.email, u.full_name
|
||||||
|
INTO v_actor_email, v_actor_full_name
|
||||||
|
FROM v3.app_user u
|
||||||
|
WHERE u.id = v_actor_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_branch_id IS NOT NULL AND p_branch_id IS DISTINCT FROM v_old.branch_id THEN
|
||||||
|
SELECT ou.title INTO v_branch_name_after
|
||||||
|
FROM v3.org_unit ou
|
||||||
|
WHERE ou.id = p_branch_id;
|
||||||
|
|
||||||
|
UPDATE v3.vsp SET branch_id = p_branch_id WHERE id = p_vsp_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object(
|
||||||
|
'branch_id', jsonb_build_object(
|
||||||
|
'before', v3._diff_val(v_old.branch_id),
|
||||||
|
'after', v3._diff_val(p_branch_id)
|
||||||
|
),
|
||||||
|
'branch_name', jsonb_build_object(
|
||||||
|
'before', v3._diff_val(v_branch_name_before),
|
||||||
|
'after', v3._diff_val(v_branch_name_after)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_reg_number IS NOT NULL AND p_reg_number IS DISTINCT FROM v_old.reg_number THEN
|
||||||
|
UPDATE v3.vsp SET reg_number = p_reg_number WHERE id = p_vsp_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object('reg_number',
|
||||||
|
jsonb_build_object('before', v3._diff_val(v_old.reg_number),
|
||||||
|
'after', v3._diff_val(p_reg_number)));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_address IS NOT NULL AND p_address IS DISTINCT FROM v_old.address THEN
|
||||||
|
UPDATE v3.vsp SET address = p_address WHERE id = p_vsp_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object('address',
|
||||||
|
jsonb_build_object('before', v3._diff_val(v_old.address),
|
||||||
|
'after', v3._diff_val(p_address)));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_format IS NOT NULL AND p_format IS DISTINCT FROM v_old.format THEN
|
||||||
|
UPDATE v3.vsp SET format = p_format WHERE id = p_vsp_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object('format',
|
||||||
|
jsonb_build_object('before', v3._diff_val(v_old.format),
|
||||||
|
'after', v3._diff_val(p_format)));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_opened_at IS NOT NULL AND p_opened_at IS DISTINCT FROM v_old.opened_at THEN
|
||||||
|
UPDATE v3.vsp SET opened_at = p_opened_at WHERE id = p_vsp_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object('opened_at',
|
||||||
|
jsonb_build_object('before', v3._diff_val(v_old.opened_at),
|
||||||
|
'after', v3._diff_val(p_opened_at)));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_placement_type IS NOT NULL AND p_placement_type IS DISTINCT FROM v_old.placement_type THEN
|
||||||
|
UPDATE v3.vsp SET placement_type = p_placement_type WHERE id = p_vsp_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_staff_count IS NOT NULL AND p_staff_count IS DISTINCT FROM v_old.staff_count THEN
|
||||||
|
UPDATE v3.vsp SET staff_count = p_staff_count WHERE id = p_vsp_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.vsp SET total_area = p_total_area WHERE id = p_vsp_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_clear_closed_at AND p_closed_at IS NULL AND v_old.closed_at IS NOT NULL THEN
|
||||||
|
UPDATE v3.vsp SET closed_at = NULL WHERE id = p_vsp_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object('closed_at',
|
||||||
|
jsonb_build_object('before', v3._diff_val(v_old.closed_at),
|
||||||
|
'after', v3._diff_val(NULL::date)));
|
||||||
|
ELSIF p_closed_at IS NOT NULL AND p_closed_at IS DISTINCT FROM v_old.closed_at THEN
|
||||||
|
UPDATE v3.vsp SET closed_at = p_closed_at WHERE id = p_vsp_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object('closed_at',
|
||||||
|
jsonb_build_object('before', v3._diff_val(v_old.closed_at),
|
||||||
|
'after', v3._diff_val(p_closed_at)));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_is_active IS NOT NULL AND p_is_active IS DISTINCT FROM v_old.is_active THEN
|
||||||
|
UPDATE v3.vsp SET is_active = p_is_active WHERE id = p_vsp_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object('is_active',
|
||||||
|
jsonb_build_object('before', v3._diff_val(v_old.is_active),
|
||||||
|
'after', v3._diff_val(p_is_active)));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_is_deleted IS NOT NULL AND p_is_deleted IS DISTINCT FROM v_old.is_deleted THEN
|
||||||
|
UPDATE v3.vsp SET is_deleted = p_is_deleted WHERE id = p_vsp_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object('is_deleted',
|
||||||
|
jsonb_build_object('before', v3._diff_val(v_old.is_deleted),
|
||||||
|
'after', v3._diff_val(p_is_deleted)));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_system_code IS NOT NULL AND p_system_code IS DISTINCT FROM v_old.system_code THEN
|
||||||
|
UPDATE v3.vsp SET system_code = p_system_code WHERE id = p_vsp_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object('system_code',
|
||||||
|
jsonb_build_object('before', v3._diff_val(v_old.system_code),
|
||||||
|
'after', v3._diff_val(p_system_code)));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_vsp_type IS NOT NULL AND p_vsp_type IS DISTINCT FROM v_old.vsp_type THEN
|
||||||
|
UPDATE v3.vsp SET vsp_type = p_vsp_type WHERE id = p_vsp_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object('vsp_type',
|
||||||
|
jsonb_build_object('before', v3._diff_val(v_old.vsp_type),
|
||||||
|
'after', v3._diff_val(p_vsp_type)));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_notes IS NOT NULL AND p_notes IS DISTINCT FROM v_old.notes THEN
|
||||||
|
UPDATE v3.vsp SET notes = p_notes WHERE id = p_vsp_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object('notes',
|
||||||
|
jsonb_build_object('before', v3._diff_val(v_old.notes),
|
||||||
|
'after', v3._diff_val(p_notes)));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_rent_contract_num IS NOT NULL AND p_rent_contract_num IS DISTINCT FROM v_old.rent_contract_num THEN
|
||||||
|
UPDATE v3.vsp SET rent_contract_num = p_rent_contract_num WHERE id = p_vsp_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object('rent_contract_num',
|
||||||
|
jsonb_build_object('before', v3._diff_val(v_old.rent_contract_num),
|
||||||
|
'after', v3._diff_val(p_rent_contract_num)));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_rent_end_date IS NOT NULL AND p_rent_end_date IS DISTINCT FROM v_old.rent_end_date THEN
|
||||||
|
UPDATE v3.vsp SET rent_end_date = p_rent_end_date WHERE id = p_vsp_id;
|
||||||
|
v_changes := v_changes || jsonb_build_object('rent_end_date',
|
||||||
|
jsonb_build_object('before', v3._diff_val(v_old.rent_end_date),
|
||||||
|
'after', v3._diff_val(p_rent_end_date)));
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_changes <> '{}'::jsonb THEN
|
||||||
|
UPDATE v3.vsp
|
||||||
|
SET updated_at = now(),
|
||||||
|
updated_by = p_updated_by
|
||||||
|
WHERE id = p_vsp_id;
|
||||||
|
|
||||||
|
PERFORM v3.log_event(
|
||||||
|
'VSP_UPDATE', 'VSP',
|
||||||
|
jsonb_build_object(
|
||||||
|
'vsp_id', p_vsp_id,
|
||||||
|
'entity_id', p_vsp_id,
|
||||||
|
'branch_id', COALESCE(p_branch_id, v_old.branch_id),
|
||||||
|
'branch_name', COALESCE(v_branch_name_after, v_branch_name_before),
|
||||||
|
'changes', v_changes,
|
||||||
|
'user_email', v_actor_email,
|
||||||
|
'user_full_name', v_actor_full_name
|
||||||
|
), null, null, COALESCE(p_branch_id, v_old.branch_id)
|
||||||
|
);
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$function$
|
||||||
|
;
|
||||||
@ -207,6 +207,7 @@ class VSPRepository:
|
|||||||
params = {"p_vsp_id": vsp.id, "p_updated_by": updated_by}
|
params = {"p_vsp_id": vsp.id, "p_updated_by": updated_by}
|
||||||
for field, param in FIELD_TO_PARAM.items():
|
for field, param in FIELD_TO_PARAM.items():
|
||||||
params[param] = data.get(field)
|
params[param] = data.get(field)
|
||||||
|
params["p_clear_closed_at"] = "closed_at" in data and data["closed_at"] is None
|
||||||
|
|
||||||
await self.db.execute(
|
await self.db.execute(
|
||||||
text("""
|
text("""
|
||||||
@ -215,7 +216,7 @@ class VSPRepository:
|
|||||||
:p_format, :p_opened_at, :p_placement_type, :p_staff_count,
|
:p_format, :p_opened_at, :p_placement_type, :p_staff_count,
|
||||||
:p_total_area, :p_closed_at, :p_is_active, :p_is_deleted,
|
:p_total_area, :p_closed_at, :p_is_active, :p_is_deleted,
|
||||||
:p_system_code, :p_updated_by, :p_vsp_type, :p_notes,
|
:p_system_code, :p_updated_by, :p_vsp_type, :p_notes,
|
||||||
:p_rent_contract_num, :p_rent_end_date
|
:p_rent_contract_num, :p_rent_end_date, :p_clear_closed_at
|
||||||
)
|
)
|
||||||
"""),
|
"""),
|
||||||
params,
|
params,
|
||||||
|
|||||||
@ -267,6 +267,9 @@ class VSPService:
|
|||||||
effective_close_date = data.get("closed_at", vsp.closed_at)
|
effective_close_date = data.get("closed_at", vsp.closed_at)
|
||||||
if effective_close_date is not None and effective_close_date <= date.today():
|
if effective_close_date is not None and effective_close_date <= date.today():
|
||||||
data["is_active"] = False
|
data["is_active"] = False
|
||||||
|
elif "closed_at" in data:
|
||||||
|
# дату сняли или перенесли на будущее: ВСП возвращается в действующие
|
||||||
|
data["is_active"] = True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return await self.vsp_repo.update(
|
return await self.vsp_repo.update(
|
||||||
|
|||||||
@ -353,3 +353,82 @@ def test_vsp_update_past_close_date_deactivates(client, admin_tokens, auth_heade
|
|||||||
listing = client.get("/api/v1/dict/info", headers=auth_headers(admin_tokens))
|
listing = client.get("/api/v1/dict/info", headers=auth_headers(admin_tokens))
|
||||||
assert listing.status_code == 200
|
assert listing.status_code == 200
|
||||||
assert vsp_id not in [item["id"] for item in listing.json()["result"]]
|
assert vsp_id not in [item["id"] for item in listing.json()["result"]]
|
||||||
|
|
||||||
|
|
||||||
|
def _put_vsp(client, admin_tokens, auth_headers, vsp_id, **body):
|
||||||
|
response = client.put(
|
||||||
|
f"/api/v1/dict/info/{vsp_id}", json=body, headers=auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
return response.json()["result"]
|
||||||
|
|
||||||
|
|
||||||
|
def _closed_vsp(client, admin_tokens, auth_headers) -> int:
|
||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
vsp_id = _create_vsp(client, admin_tokens, auth_headers, open_date="2020-01-01")
|
||||||
|
result = _put_vsp(
|
||||||
|
client, admin_tokens, auth_headers, vsp_id,
|
||||||
|
close_date=(date.today() - timedelta(days=1)).isoformat(),
|
||||||
|
)
|
||||||
|
assert result["is_active"] is False
|
||||||
|
return vsp_id
|
||||||
|
|
||||||
|
|
||||||
|
def test_vsp_update_clear_close_date_reopens(client, admin_tokens, auth_headers):
|
||||||
|
vsp_id = _closed_vsp(client, admin_tokens, auth_headers)
|
||||||
|
|
||||||
|
result = _put_vsp(client, admin_tokens, auth_headers, vsp_id, close_date=None)
|
||||||
|
|
||||||
|
assert result["close_date"] is None
|
||||||
|
assert result["is_active"] is True
|
||||||
|
listing = client.get("/api/v1/dict/info", headers=auth_headers(admin_tokens))
|
||||||
|
assert vsp_id in [item["id"] for item in listing.json()["result"]]
|
||||||
|
|
||||||
|
|
||||||
|
def test_vsp_update_future_close_date_reopens(client, admin_tokens, auth_headers):
|
||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
vsp_id = _closed_vsp(client, admin_tokens, auth_headers)
|
||||||
|
future = (date.today() + timedelta(days=30)).isoformat()
|
||||||
|
|
||||||
|
result = _put_vsp(client, admin_tokens, auth_headers, vsp_id, close_date=future)
|
||||||
|
|
||||||
|
assert result["close_date"] == future
|
||||||
|
assert result["is_active"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_vsp_update_without_close_date_keeps_it(client, admin_tokens, auth_headers):
|
||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
vsp_id = _closed_vsp(client, admin_tokens, auth_headers)
|
||||||
|
yesterday = (date.today() - timedelta(days=1)).isoformat()
|
||||||
|
|
||||||
|
result = _put_vsp(client, admin_tokens, auth_headers, vsp_id, address="AURORA-1886 new")
|
||||||
|
|
||||||
|
assert result["close_date"] == yesterday
|
||||||
|
assert result["is_active"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_vsp_clear_close_date_is_audited(client, admin_tokens, auth_headers, db_session):
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
vsp_id = _closed_vsp(client, admin_tokens, auth_headers)
|
||||||
|
_put_vsp(client, admin_tokens, auth_headers, vsp_id, close_date=None)
|
||||||
|
|
||||||
|
async def _last_changes():
|
||||||
|
row = (
|
||||||
|
await db_session.execute(
|
||||||
|
text(
|
||||||
|
"SELECT event_data->'changes' FROM v3.audit_log "
|
||||||
|
"WHERE event = 'VSP_UPDATE' AND (event_data->>'vsp_id')::int = :id "
|
||||||
|
"ORDER BY id DESC LIMIT 1"
|
||||||
|
),
|
||||||
|
{"id": vsp_id},
|
||||||
|
)
|
||||||
|
).first()
|
||||||
|
return row[0]
|
||||||
|
|
||||||
|
changes = client.portal.call(_last_changes)
|
||||||
|
assert changes["closed_at"]["before"] is not None
|
||||||
|
assert changes["closed_at"]["after"] is None
|
||||||
|
|||||||
@ -34,7 +34,7 @@ async def test_update_past_close_date_sets_inactive():
|
|||||||
assert data["is_active"] is False
|
assert data["is_active"] is False
|
||||||
|
|
||||||
|
|
||||||
async def test_update_future_close_date_keeps_active_key_absent():
|
async def test_update_future_close_date_sets_active():
|
||||||
vsp = MagicMock(closed_at=None)
|
vsp = MagicMock(closed_at=None)
|
||||||
service = _service(vsp)
|
service = _service(vsp)
|
||||||
payload = VSPUpdate(close_date=date.today() + timedelta(days=30))
|
payload = VSPUpdate(close_date=date.today() + timedelta(days=30))
|
||||||
@ -42,4 +42,16 @@ async def test_update_future_close_date_keeps_active_key_absent():
|
|||||||
await service.update(vsp_id=1, payload=payload, user=_admin())
|
await service.update(vsp_id=1, payload=payload, user=_admin())
|
||||||
|
|
||||||
data = service.vsp_repo.update.await_args.kwargs["data"]
|
data = service.vsp_repo.update.await_args.kwargs["data"]
|
||||||
assert "is_active" not in data
|
assert data["is_active"] is True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_null_close_date_sets_active():
|
||||||
|
vsp = MagicMock(closed_at=date.today() - timedelta(days=1))
|
||||||
|
service = _service(vsp)
|
||||||
|
payload = VSPUpdate(close_date=None)
|
||||||
|
|
||||||
|
await service.update(vsp_id=1, payload=payload, user=_admin())
|
||||||
|
|
||||||
|
data = service.vsp_repo.update.await_args.kwargs["data"]
|
||||||
|
assert data["closed_at"] is None
|
||||||
|
assert data["is_active"] is True
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user