fixes-form #166
15
api/alembic/versions/0031_form2_ahr_satellite_sync.py
Normal file
15
api/alembic/versions/0031_form2_ahr_satellite_sync.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
from sql_migration import run_sql_file_migration
|
||||||
|
|
||||||
|
|
||||||
|
revision = "0031"
|
||||||
|
down_revision = "0030"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
run_sql_file_migration("0031_form2_ahr_satellite_sync.sql")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
pass
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
from sql_migration import run_sql_file_migration
|
||||||
|
|
||||||
|
|
||||||
|
revision = "0032"
|
||||||
|
down_revision = "0031"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
run_sql_file_migration("0032_empty_name_on_budget_line_create.sql")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
pass
|
||||||
1322
api/alembic/versions/sql/0031_form2_ahr_satellite_sync.sql
Normal file
1322
api/alembic/versions/sql/0031_form2_ahr_satellite_sync.sql
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,212 @@
|
|||||||
|
-- Новые пользовательские строки создаются с пустым наименованием.
|
||||||
|
-- Заполнение header.name остается через обычную проверку этапов/ролей:
|
||||||
|
-- v3.can_edit(..., 'header.name', ...).
|
||||||
|
-- Сателлитные листы AHR_RENT/AHR_UTILITY/AHR_SECURITY оставлены в прежней логике.
|
||||||
|
-- Используем пустую строку, а не NULL: представления формы показывают
|
||||||
|
-- справочное имя статьи через COALESCE(budget_line.name, expense_item.name).
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION v3.add_budget_line(p_form_id integer, p_expense_item_id integer DEFAULT NULL::integer, p_sheet character varying DEFAULT NULL::character varying, p_item_id character varying DEFAULT NULL::character varying, p_section_code character varying DEFAULT NULL::character varying, p_direction character varying DEFAULT NULL::character varying, p_name character varying DEFAULT NULL::character varying, p_internal_order character varying DEFAULT NULL::character varying, p_vsp_id integer DEFAULT NULL::integer, p_project_id integer DEFAULT NULL::integer, p_justification character varying DEFAULT NULL::character varying, p_contract_number character varying DEFAULT NULL::character varying, p_contract_end_date date DEFAULT NULL::date)
|
||||||
|
RETURNS TABLE(row_type character varying, depth integer, sort_order bigint, data jsonb)
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $function$
|
||||||
|
DECLARE
|
||||||
|
v_form_type VARCHAR;
|
||||||
|
v_eid INT;
|
||||||
|
v_sheet VARCHAR;
|
||||||
|
v_auto_dir VARCHAR;
|
||||||
|
v_final_dir VARCHAR;
|
||||||
|
v_new_id INT;
|
||||||
|
v_cnt INT;
|
||||||
|
v_sat_table TEXT;
|
||||||
|
v_sat_eid INT;
|
||||||
|
v_parent_line INT;
|
||||||
|
v_decision JSONB;
|
||||||
|
v_msg TEXT;
|
||||||
|
v_user_id INT;
|
||||||
|
BEGIN
|
||||||
|
SELECT form_type_code INTO v_form_type FROM v3.budget_form WHERE id = p_form_id;
|
||||||
|
IF v_form_type IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'budget_form id=% не существует', p_form_id;
|
||||||
|
END IF;
|
||||||
|
IF v_form_type = 'FORM_3' THEN
|
||||||
|
RAISE EXCEPTION 'FORM_3 не использует budget_line — см. add_form3_line (TBD)';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
v_user_id := v3.current_user_id();
|
||||||
|
v_decision := v3.can_edit(p_form_id, p_sheet, p_direction, NULL, v_user_id);
|
||||||
|
|
||||||
|
IF NOT (v_decision->>'ok')::boolean THEN
|
||||||
|
IF v_decision->>'code' = 'role_not_allowed' THEN
|
||||||
|
RAISE EXCEPTION 'role_not_allowed: Невозможно вставить строку';
|
||||||
|
ELSIF v_decision->>'code' = 'org_not_assigned' THEN
|
||||||
|
RAISE EXCEPTION 'org_not_assigned: Невозможно вставить строку';
|
||||||
|
ELSIF v_decision->>'code' = 'window_closed' THEN
|
||||||
|
IF v_decision ? 'opens_at' THEN
|
||||||
|
v_msg := format('window_closed: opens %s', v_decision->>'opens_at');
|
||||||
|
ELSIF v_decision->>'closes_at' IS NOT NULL THEN
|
||||||
|
v_msg := format('window_closed: closed %s', v_decision->>'closes_at');
|
||||||
|
ELSE
|
||||||
|
v_msg := 'window_closed: no phase';
|
||||||
|
END IF;
|
||||||
|
RAISE EXCEPTION '%', v_msg;
|
||||||
|
ELSE
|
||||||
|
RAISE EXCEPTION '%', v_decision->>'code';
|
||||||
|
END IF;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_sheet IN ('AHR_RENT','AHR_UTILITY','AHR_SECURITY') THEN
|
||||||
|
v_sat_table := CASE p_sheet
|
||||||
|
WHEN 'AHR_RENT' THEN 'rent_detail'
|
||||||
|
WHEN 'AHR_UTILITY' THEN 'utility_detail'
|
||||||
|
WHEN 'AHR_SECURITY' THEN 'security_detail'
|
||||||
|
END;
|
||||||
|
v_sat_eid := CASE p_sheet
|
||||||
|
WHEN 'AHR_RENT' THEN 293
|
||||||
|
WHEN 'AHR_UTILITY' THEN 799
|
||||||
|
WHEN 'AHR_SECURITY' THEN 31
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF p_vsp_id IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'p_vsp_id обязателен для сателлитного листа %', p_sheet;
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM v3.vsp WHERE id = p_vsp_id) THEN
|
||||||
|
RAISE EXCEPTION 'vsp id=% не существует', p_vsp_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT id INTO v_parent_line
|
||||||
|
FROM v3.budget_line
|
||||||
|
WHERE budget_form_id = p_form_id AND expense_item_id = v_sat_eid
|
||||||
|
LIMIT 1;
|
||||||
|
IF v_parent_line IS NULL THEN
|
||||||
|
INSERT INTO v3.budget_line (budget_form_id, expense_item_id, direction)
|
||||||
|
VALUES (
|
||||||
|
p_form_id, v_sat_eid,
|
||||||
|
CASE v_form_type WHEN 'FORM_2' THEN 'Support' WHEN 'FORM_4' THEN 'Development' ELSE NULL END
|
||||||
|
) RETURNING id INTO v_parent_line;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
EXECUTE format(
|
||||||
|
'INSERT INTO v3.%I (line_id, vsp_id, contract_number, contract_end_date) '
|
||||||
|
|| 'VALUES ($1, $2, $3, $4) RETURNING id',
|
||||||
|
v_sat_table
|
||||||
|
) USING v_parent_line, p_vsp_id, p_contract_number, p_contract_end_date
|
||||||
|
INTO v_new_id;
|
||||||
|
|
||||||
|
PERFORM v3.sync_form2_ahr_satellite_line(p_form_id, p_sheet, v_new_id);
|
||||||
|
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT v.row_type, v.depth, v.sort_order, v.data
|
||||||
|
FROM v3.v_form_view(p_form_id, p_sheet, NULL, NULL) v
|
||||||
|
WHERE v.row_type = 'INPUT' AND (v.data->>'id')::INT = v_new_id;
|
||||||
|
RETURN;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF p_expense_item_id IS NOT NULL THEN
|
||||||
|
SELECT ei.id, ei.sheet INTO v_eid, v_sheet
|
||||||
|
FROM v3.expense_item ei WHERE ei.id = p_expense_item_id;
|
||||||
|
IF v_eid IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'expense_item id=% не существует', p_expense_item_id;
|
||||||
|
END IF;
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM v3.expense_item_form_type
|
||||||
|
WHERE expense_item_id = v_eid AND form_type_code = v_form_type
|
||||||
|
) THEN
|
||||||
|
RAISE EXCEPTION 'expense_item id=% не привязан к form_type %', v_eid, v_form_type;
|
||||||
|
END IF;
|
||||||
|
ELSE
|
||||||
|
IF p_sheet IS NULL OR p_item_id IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'нужен либо p_expense_item_id, либо (p_sheet + p_item_id)';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT count(*) INTO v_cnt
|
||||||
|
FROM v3.expense_item ei
|
||||||
|
JOIN v3.expense_item_form_type eift ON eift.expense_item_id = ei.id
|
||||||
|
WHERE ei.item_id = p_item_id
|
||||||
|
AND ei.sheet = p_sheet
|
||||||
|
AND eift.form_type_code = v_form_type
|
||||||
|
AND (p_section_code IS NULL OR ei.section_code = p_section_code)
|
||||||
|
AND (p_direction IS NULL OR ei.direction IS NULL OR ei.direction = p_direction);
|
||||||
|
|
||||||
|
IF v_cnt = 0 THEN
|
||||||
|
RAISE EXCEPTION 'expense_item не найден: sheet=%, item_id=%, section=%, direction=%, form=%',
|
||||||
|
p_sheet, p_item_id, p_section_code, p_direction, v_form_type;
|
||||||
|
END IF;
|
||||||
|
IF v_cnt > 1 THEN
|
||||||
|
RAISE EXCEPTION 'expense_item не уникален (% совпадений): sheet=%, item_id=%, section=%, direction=%. Уточните p_section_code или передайте p_expense_item_id напрямую',
|
||||||
|
v_cnt, p_sheet, p_item_id, p_section_code, p_direction;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SELECT ei.id, ei.sheet INTO v_eid, v_sheet
|
||||||
|
FROM v3.expense_item ei
|
||||||
|
JOIN v3.expense_item_form_type eift ON eift.expense_item_id = ei.id
|
||||||
|
WHERE ei.item_id = p_item_id
|
||||||
|
AND ei.sheet = p_sheet
|
||||||
|
AND eift.form_type_code = v_form_type
|
||||||
|
AND (p_section_code IS NULL OR ei.section_code = p_section_code)
|
||||||
|
AND (p_direction IS NULL OR ei.direction IS NULL OR ei.direction = p_direction);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
v_auto_dir := CASE
|
||||||
|
WHEN v_form_type = 'FORM_2' THEN 'Support'
|
||||||
|
WHEN v_form_type = 'FORM_4' THEN 'Development'
|
||||||
|
WHEN v_form_type = 'FORM_1' AND v_sheet = 'OPER' THEN NULL
|
||||||
|
ELSE NULL
|
||||||
|
END;
|
||||||
|
|
||||||
|
IF v_form_type = 'FORM_1' AND v_sheet IN ('AHR','CAP') AND p_direction IS NULL THEN
|
||||||
|
RAISE EXCEPTION 'p_direction обязателен для FORM_1 / sheet=% (Support|Development)', v_sheet;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_form_type IN ('FORM_2','FORM_4') AND p_direction IS NOT NULL AND p_direction <> v_auto_dir THEN
|
||||||
|
RAISE EXCEPTION 'p_direction=% несовместим с form_type=% (фиксирован=%)',
|
||||||
|
p_direction, v_form_type, v_auto_dir;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF v_form_type = 'FORM_1' AND v_sheet = 'OPER' AND p_direction IS NOT NULL THEN
|
||||||
|
RAISE EXCEPTION 'p_direction должен быть NULL для FORM_1 / sheet=OPER (получено: %)', p_direction;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
v_final_dir := COALESCE(p_direction, v_auto_dir);
|
||||||
|
|
||||||
|
IF p_vsp_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM v3.vsp WHERE id = p_vsp_id) THEN
|
||||||
|
RAISE EXCEPTION 'vsp id=% не существует', p_vsp_id;
|
||||||
|
END IF;
|
||||||
|
IF p_project_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM v3.form4_project WHERE id = p_project_id) THEN
|
||||||
|
RAISE EXCEPTION 'form4_project id=% не существует', p_project_id;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
INSERT INTO v3.budget_line (
|
||||||
|
budget_form_id, expense_item_id, name, internal_order,
|
||||||
|
vsp_id, project_id, direction, justification
|
||||||
|
) VALUES (
|
||||||
|
p_form_id, v_eid, '', p_internal_order,
|
||||||
|
p_vsp_id, p_project_id, v_final_dir, p_justification
|
||||||
|
) RETURNING id INTO v_new_id;
|
||||||
|
|
||||||
|
PERFORM v3.log_event(
|
||||||
|
'ROW_CREATE', 'ROW',
|
||||||
|
jsonb_build_object(
|
||||||
|
'sheet', v_sheet,
|
||||||
|
'line_id', v_new_id,
|
||||||
|
'expense_item_id', v_eid,
|
||||||
|
'direction', v_final_dir,
|
||||||
|
'name', '',
|
||||||
|
'vsp_id', p_vsp_id,
|
||||||
|
'project_id', p_project_id
|
||||||
|
),
|
||||||
|
p_form_id
|
||||||
|
);
|
||||||
|
|
||||||
|
DECLARE v_ei_path INT[];
|
||||||
|
BEGIN
|
||||||
|
v_ei_path := v3._expense_item_path_set(ARRAY[v_eid]);
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT v.row_type, v.depth, v.sort_order, v.data
|
||||||
|
FROM v3.v_form_view(p_form_id, v_sheet, NULL, v_final_dir) v
|
||||||
|
WHERE (v.row_type IN ('ROOT','GROUP','ITEM','SUB_ITEM')
|
||||||
|
AND (v.data->'header'->>'expense_item_id')::INT = ANY(v_ei_path))
|
||||||
|
OR (v.row_type = 'INPUT' AND (v.data->>'line_id')::INT = v_new_id)
|
||||||
|
ORDER BY v.sort_order;
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
$function$;
|
||||||
@ -33,6 +33,13 @@ from src.services.user_service import UserService
|
|||||||
|
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
|
|
||||||
|
FORM_2_AHR_SATELLITE_SHEETS = {
|
||||||
|
"AHR_LIMIT",
|
||||||
|
"AHR_RENT",
|
||||||
|
"AHR_UTILITY",
|
||||||
|
"AHR_SECURITY",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def get_db_session(user_id: int | None = None):
|
async def get_db_session(user_id: int | None = None):
|
||||||
@ -215,6 +222,27 @@ class ConnectionManager:
|
|||||||
except:
|
except:
|
||||||
pass # Игнорируем недоступные соединения
|
pass # Игнорируем недоступные соединения
|
||||||
|
|
||||||
|
async def broadcast_to_form_sheet(
|
||||||
|
self,
|
||||||
|
data: dict,
|
||||||
|
form_id: int,
|
||||||
|
sheet: str,
|
||||||
|
):
|
||||||
|
data = data.copy()
|
||||||
|
data["is_self"] = None
|
||||||
|
message = dumps(data, default=_convert_error, ensure_ascii=False)
|
||||||
|
|
||||||
|
for key, connections in self.connections[ConnectionKeyEnum.FORM].items():
|
||||||
|
key_data = dict(key)
|
||||||
|
if key_data.get("form_id") != form_id or key_data.get("sheet") != sheet:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for con_info in connections:
|
||||||
|
try:
|
||||||
|
await con_info.ws.send_text(message)
|
||||||
|
except:
|
||||||
|
pass # Игнорируем недоступные соединения
|
||||||
|
|
||||||
async def send_back(self, data: dict, websocket: WebSocket):
|
async def send_back(self, data: dict, websocket: WebSocket):
|
||||||
data = data.copy()
|
data = data.copy()
|
||||||
data["is_self"] = True
|
data["is_self"] = True
|
||||||
@ -742,6 +770,93 @@ class ProjectEventProcess:
|
|||||||
manager = ConnectionManager()
|
manager = ConnectionManager()
|
||||||
|
|
||||||
|
|
||||||
|
def get_satellite_source_row_id(data: dict) -> int | None:
|
||||||
|
event_name = data.get("event")
|
||||||
|
|
||||||
|
if event_name == "cell_updated":
|
||||||
|
return data.get("data", {}).get("line_id")
|
||||||
|
|
||||||
|
if event_name == "row_deleted":
|
||||||
|
return data.get("data", {}).get("row_id")
|
||||||
|
|
||||||
|
if event_name == "row_added":
|
||||||
|
result = data.get("result")
|
||||||
|
if not isinstance(result, dict):
|
||||||
|
return None
|
||||||
|
|
||||||
|
rows = result.get("data") or []
|
||||||
|
for row in rows:
|
||||||
|
if len(row) >= 4 and isinstance(row[3], dict) and row[3].get("id") is not None:
|
||||||
|
return row[3]["id"]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def notify_ahr_about_satellite_change(
|
||||||
|
form_id: int,
|
||||||
|
source_sheet: str,
|
||||||
|
source_row_id: int,
|
||||||
|
source_event: str,
|
||||||
|
user_id: int,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
async with get_db_session(user_id=user_id) as db:
|
||||||
|
ahr_delta = await SheetService(db).get_form2_ahr_satellite_delta(
|
||||||
|
form_id=form_id,
|
||||||
|
source_sheet=source_sheet,
|
||||||
|
source_row_id=source_row_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not ahr_delta:
|
||||||
|
return
|
||||||
|
|
||||||
|
ahr_event = {
|
||||||
|
"event": "cell_updated",
|
||||||
|
"data": {
|
||||||
|
"source_sheet": source_sheet,
|
||||||
|
},
|
||||||
|
"result": ahr_delta,
|
||||||
|
}
|
||||||
|
|
||||||
|
if source_event == "row_added":
|
||||||
|
new_row = next(
|
||||||
|
(
|
||||||
|
row for row in ahr_delta
|
||||||
|
if row[0] == "INPUT"
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if new_row is not None:
|
||||||
|
new_line_id = new_row[3].get("line_id")
|
||||||
|
ahr_event = {
|
||||||
|
"event": "row_added",
|
||||||
|
"data": {
|
||||||
|
"source_sheet": source_sheet,
|
||||||
|
"expense_item_id": new_row[3].get("header", {}).get("expense_item_id"),
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"data": ahr_delta,
|
||||||
|
"new_line_id": new_line_id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
await manager.broadcast_to_form_sheet(
|
||||||
|
data=ahr_event,
|
||||||
|
form_id=form_id,
|
||||||
|
sheet="AHR",
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.exception(
|
||||||
|
"Ошибка отправки на лист АХР",
|
||||||
|
extra={
|
||||||
|
"form_id": form_id,
|
||||||
|
"source_sheet": source_sheet,
|
||||||
|
"source_row_id": source_row_id,
|
||||||
|
"source_event": source_event,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _convert_error(o):
|
def _convert_error(o):
|
||||||
try:
|
try:
|
||||||
return asdict(o)
|
return asdict(o)
|
||||||
@ -910,6 +1025,16 @@ async def process_websocket(
|
|||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
**kwargs_process
|
**kwargs_process
|
||||||
)
|
)
|
||||||
|
should_refresh_ahr = (
|
||||||
|
con_key == ConnectionKeyEnum.FORM
|
||||||
|
and kwargs_process.get("sheet") in FORM_2_AHR_SATELLITE_SHEETS
|
||||||
|
and data.get("event") in {"cell_updated", "row_added", "row_deleted"}
|
||||||
|
)
|
||||||
|
ahr_source_row_id = (
|
||||||
|
get_satellite_source_row_id(data)
|
||||||
|
if should_refresh_ahr
|
||||||
|
else None
|
||||||
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
db_ms = (time.perf_counter() - t0) * 1000
|
db_ms = (time.perf_counter() - t0) * 1000
|
||||||
|
|
||||||
@ -956,6 +1081,17 @@ async def process_websocket(
|
|||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if should_refresh_ahr and ahr_source_row_id is not None:
|
||||||
|
asyncio.create_task(
|
||||||
|
notify_ahr_about_satellite_change(
|
||||||
|
form_id=kwargs_process["form_id"],
|
||||||
|
source_sheet=kwargs_process["sheet"],
|
||||||
|
source_row_id=ahr_source_row_id,
|
||||||
|
source_event=data["event"],
|
||||||
|
user_id=user_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
except BasicAppException as e:
|
except BasicAppException as e:
|
||||||
data["error"] = e.description or "Неизвестная ошибка"
|
data["error"] = e.description or "Неизвестная ошибка"
|
||||||
await manager.send_back(
|
await manager.send_back(
|
||||||
|
|||||||
@ -380,3 +380,31 @@ class SheetRepository:
|
|||||||
)
|
)
|
||||||
).all()
|
).all()
|
||||||
]
|
]
|
||||||
|
|
||||||
|
async def get_form2_ahr_satellite_delta(
|
||||||
|
self,
|
||||||
|
form_id: int,
|
||||||
|
source_sheet: str,
|
||||||
|
source_row_id: int,
|
||||||
|
) -> list[tuple]:
|
||||||
|
return [
|
||||||
|
tuple(r) for r in (
|
||||||
|
await self.db.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
SELECT row_type, depth, sort_order, data
|
||||||
|
FROM v3.get_form2_ahr_satellite_delta(
|
||||||
|
:form_id,
|
||||||
|
:source_sheet,
|
||||||
|
:source_row_id
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{
|
||||||
|
"form_id": form_id,
|
||||||
|
"source_sheet": source_sheet,
|
||||||
|
"source_row_id": source_row_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
]
|
||||||
|
|||||||
@ -274,3 +274,15 @@ class SheetService:
|
|||||||
row_id=row_id,
|
row_id=row_id,
|
||||||
direction=direction,
|
direction=direction,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def get_form2_ahr_satellite_delta(
|
||||||
|
self,
|
||||||
|
form_id: int,
|
||||||
|
source_sheet: str,
|
||||||
|
source_row_id: int,
|
||||||
|
) -> list[tuple]:
|
||||||
|
return await self.sheet_repo.get_form2_ahr_satellite_delta(
|
||||||
|
form_id=form_id,
|
||||||
|
source_sheet=source_sheet,
|
||||||
|
source_row_id=source_row_id,
|
||||||
|
)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user