test(api): свод 2 — засев, эталон прежнего ответа и тесты-оракулы нового режима (красные)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
174ec73732
commit
b134cfd118
13254
api/tests/fixtures/svod2_legacy_golden.json
vendored
Normal file
13254
api/tests/fixtures/svod2_legacy_golden.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@ -4,6 +4,7 @@ import os
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy import text
|
||||
|
||||
|
||||
from src.db.base import SessionLocal
|
||||
@ -30,6 +31,7 @@ def client():
|
||||
app.dependency_overrides[get_db] = _get_db
|
||||
with TestClient(app) as test_client:
|
||||
try:
|
||||
test_client.db_session = db_ref
|
||||
yield test_client
|
||||
finally:
|
||||
if db_ref is not None and loop_ref:
|
||||
@ -79,3 +81,54 @@ def isp_tokens(client, isp_password: str):
|
||||
)
|
||||
assert response.status_code == 200
|
||||
return response.json()
|
||||
|
||||
|
||||
SVOD2_YEAR = 2031
|
||||
SVOD2_SEED_PATH = os.path.join(os.path.dirname(__file__), "..", "sqls", "svod2_seed.sql")
|
||||
|
||||
|
||||
def legacy_full(client, sheet: str, headers: dict, org_ids=None) -> list[dict]:
|
||||
"""Прежний ответ свода 2 без параметров нового режима (оракульная база)."""
|
||||
url = f"/api/v1/svod/2/{sheet}/{SVOD2_YEAR}"
|
||||
if org_ids is not None:
|
||||
url += f"?org_ids={org_ids}"
|
||||
response = client.get(url, headers=headers)
|
||||
assert response.status_code == 200, response.text
|
||||
return response.json()["result"]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def svod2_sql(client):
|
||||
"""Выполняет text(sql) на сессии клиента (тот же цикл событий, что у запросов)."""
|
||||
|
||||
def _run(sql: str, params: dict | None = None):
|
||||
async def _exec():
|
||||
result = await client.db_session.execute(text(sql), params or {})
|
||||
return list(result.fetchall()) if result.returns_rows else []
|
||||
|
||||
return client.portal.call(_exec)
|
||||
|
||||
return _run
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def svod2_data(client, svod2_sql) -> dict:
|
||||
"""Засевает данные свода 2 (откатываются вместе с сессией клиента)."""
|
||||
with open(SVOD2_SEED_PATH, "r", encoding="utf-8") as f:
|
||||
script = f.read()
|
||||
for statement in script.split(";"):
|
||||
statement = statement.strip()
|
||||
if statement:
|
||||
svod2_sql(statement)
|
||||
# id строки contract_detail: contract_detail.line_id = 910002, технический id,
|
||||
# который попадает в шапку строки, — expense_item_id этой строки
|
||||
cd_id = svod2_sql("SELECT expense_item_id FROM v3.budget_line WHERE id = 910002")[0][0]
|
||||
return {
|
||||
"year": SVOD2_YEAR,
|
||||
"org_a": 910001,
|
||||
"org_b": 910002,
|
||||
"org_b_twin": 910003,
|
||||
"org_b_case": 910004,
|
||||
"cd_id": cd_id,
|
||||
"ckk_only_value": "777.25",
|
||||
}
|
||||
|
||||
70
api/tests/integration/test_svod2_legacy_golden.py
Normal file
70
api/tests/integration/test_svod2_legacy_golden.py
Normal file
@ -0,0 +1,70 @@
|
||||
"""Эталон прежнего ответа свода 2 (байт-в-байт).
|
||||
|
||||
Эталон записан один раз на коде «до» (SVOD2_WRITE_GOLDEN=1) и больше не перезаписывается.
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
GOLDEN_PATH = Path(__file__).resolve().parent.parent / "fixtures" / "svod2_legacy_golden.json"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GoldenCase:
|
||||
name: str
|
||||
query: str = ""
|
||||
who: str = "admin"
|
||||
|
||||
def url(self, sheet: str, data: dict) -> str:
|
||||
query = self.query.format(org_a=data["org_a"])
|
||||
base = f"/api/v1/svod/2/{sheet}/{data['year']}"
|
||||
return f"{base}?{query}" if query else base
|
||||
|
||||
def headers(self, auth_headers, admin_tokens, isp_tokens) -> dict:
|
||||
return auth_headers(isp_tokens if self.who == "isp" else admin_tokens)
|
||||
|
||||
|
||||
GOLDEN_CASES = [
|
||||
GoldenCase("admin_all"),
|
||||
GoldenCase("admin_one", "org_ids={org_a}"),
|
||||
GoldenCase("isp_all", who="isp"),
|
||||
GoldenCase("admin_page50", "offset=0&limit=50"),
|
||||
GoldenCase("admin_two", "offset=0&limit=2"),
|
||||
]
|
||||
|
||||
|
||||
def _golden() -> dict:
|
||||
with open(GOLDEN_PATH, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def _record(key: str, content: bytes) -> None:
|
||||
data = json.loads(GOLDEN_PATH.read_text(encoding="utf-8")) if GOLDEN_PATH.exists() else {}
|
||||
data[key] = {"sha256": hashlib.sha256(content).hexdigest(), "body": json.loads(content)}
|
||||
GOLDEN_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
GOLDEN_PATH.write_text(
|
||||
json.dumps(dict(sorted(data.items())), ensure_ascii=False, indent=1) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sheet", ["GO", "RF"])
|
||||
@pytest.mark.parametrize("case", GOLDEN_CASES, ids=lambda c: c.name)
|
||||
def test_legacy_response_is_byte_identical(
|
||||
client, svod2_data, admin_tokens, isp_tokens, auth_headers, sheet, case
|
||||
):
|
||||
r = client.get(
|
||||
case.url(sheet, svod2_data),
|
||||
headers=case.headers(auth_headers, admin_tokens, isp_tokens),
|
||||
)
|
||||
assert r.status_code == 200
|
||||
if case.name == "admin_all":
|
||||
assert r.json()["result"], "засев не дал строк свода 2"
|
||||
key = f"{sheet}-{case.name}"
|
||||
if os.getenv("SVOD2_WRITE_GOLDEN") == "1":
|
||||
_record(key, r.content)
|
||||
assert hashlib.sha256(r.content).hexdigest() == _golden()[key]["sha256"]
|
||||
260
api/tests/integration/test_svod2_paging_api.py
Normal file
260
api/tests/integration/test_svod2_paging_api.py
Normal file
@ -0,0 +1,260 @@
|
||||
"""Тесты-оракулы нового режима API свода 2 (постраничная выдача, поиск, фильтры, варианты, compact).
|
||||
|
||||
Оракул — функции экрана «до» из tests.svod2_oracle, применённые к прежнему полному ответу.
|
||||
"""
|
||||
import pytest
|
||||
|
||||
from tests.integration.conftest import SVOD2_YEAR, legacy_full
|
||||
from tests.svod2_oracle import (
|
||||
compact_projection,
|
||||
screen_facets,
|
||||
screen_found,
|
||||
screen_rows,
|
||||
)
|
||||
|
||||
SMETA_TYPE = "КВ"
|
||||
SMETA_TYPE_VARIANT = SMETA_TYPE.lower() + " "
|
||||
|
||||
SEARCHES = [
|
||||
"аРЕНДА", "ё", "Ё", "ромаш", "лютик", "ромашка плюс", "1234.5", "1234.50", " аренда ",
|
||||
" ", "1", ",", "777.25", "<cd_id>", "я" * 5000,
|
||||
]
|
||||
|
||||
FILTERS = {
|
||||
"type-case-space": {"smeta_type": SMETA_TYPE_VARIANT},
|
||||
"org-twin": {"org_name": "Филиал Ёлки"},
|
||||
"org-case-variant": {"org_name": "филиал ёлки "},
|
||||
"unknown": {"smeta_type": "нет такого"},
|
||||
"dir+org": {"smeta_direction": "Развитие", "org_name": "Филиал Ёлки"},
|
||||
"search+type": {"search": "аренда", "smeta_type": SMETA_TYPE_VARIANT},
|
||||
"search+type-hit": {"search": "о", "smeta_type": SMETA_TYPE_VARIANT},
|
||||
"long": {"org_name": "я" * 5000},
|
||||
}
|
||||
|
||||
SHEETS = ["GO", "RF"]
|
||||
|
||||
|
||||
def _url(sheet: str, suffix: str = "") -> str:
|
||||
return f"/api/v1/svod/2/{sheet}/{SVOD2_YEAR}{suffix}"
|
||||
|
||||
|
||||
def _line_ids(rows: list[dict]) -> list[int]:
|
||||
return [r["data"]["line_id"] for r in rows]
|
||||
|
||||
|
||||
def _resolve_search(search: str, data: dict) -> str:
|
||||
return str(data["cd_id"]) if search == "<cd_id>" else search
|
||||
|
||||
|
||||
def _split(params: dict) -> tuple[str, dict]:
|
||||
filters = dict(params)
|
||||
search = filters.pop("search", "")
|
||||
return search, filters
|
||||
|
||||
|
||||
def _all_pages(client, sheet, headers, limit, **params) -> tuple[list[dict], list[int]]:
|
||||
"""Все страницы нового режима: (строки, total каждой страницы)."""
|
||||
rows: list[dict] = []
|
||||
totals: list[int] = []
|
||||
offset = 0
|
||||
while True:
|
||||
r = client.get(
|
||||
_url(sheet),
|
||||
params={**params, "offset": offset, "limit": limit},
|
||||
headers=headers,
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
body = r.json()
|
||||
totals.append(body["total"])
|
||||
rows.extend(body["result"])
|
||||
if len(body["result"]) < limit:
|
||||
return rows, totals
|
||||
offset += limit
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def headers(admin_tokens, auth_headers):
|
||||
return auth_headers(admin_tokens)
|
||||
|
||||
|
||||
def test_legacy_mode_has_no_total(client, svod2_data, headers):
|
||||
r = client.get(_url("GO", "?offset=0&limit=2"), headers=headers)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["count"] <= 2
|
||||
assert len(body["result"]) <= 2
|
||||
assert "total" not in body
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"q", ["offset=-1", "limit=0", "sort_by=bad", "sort_direction=bad", "view=bad"]
|
||||
)
|
||||
def test_invalid_params_422(client, svod2_data, headers, q):
|
||||
r = client.get(_url("GO", f"?{q}"), headers=headers)
|
||||
assert r.status_code == 422
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sheet", SHEETS)
|
||||
def test_pages_stitch_to_full_list(client, svod2_data, headers, sheet):
|
||||
full = legacy_full(client, sheet, headers)
|
||||
assert full
|
||||
rows, totals = _all_pages(client, sheet, headers, 2, view="full")
|
||||
assert _line_ids(rows) == _line_ids(full)
|
||||
assert totals and all(t == len(full) for t in totals)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sheet", SHEETS)
|
||||
def test_page_past_end_has_total(client, svod2_data, headers, sheet):
|
||||
full = legacy_full(client, sheet, headers)
|
||||
r = client.get(
|
||||
_url(sheet),
|
||||
params={"view": "full", "offset": len(full), "limit": 2},
|
||||
headers=headers,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["result"] == []
|
||||
assert body["count"] == 0
|
||||
assert body["total"] == len(full)
|
||||
|
||||
|
||||
def test_view_full_rows_equal_legacy_rows(client, svod2_data, headers):
|
||||
full = legacy_full(client, "RF", headers)
|
||||
r = client.get(
|
||||
_url("RF"), params={"view": "full", "offset": 0, "limit": 50}, headers=headers
|
||||
)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert "total" in body
|
||||
assert body["result"] == full[:50]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sheet", SHEETS)
|
||||
@pytest.mark.parametrize("search", SEARCHES, ids=[str(i) for i in range(len(SEARCHES))])
|
||||
def test_search_matches_screen_oracle(client, svod2_data, headers, sheet, search):
|
||||
search = _resolve_search(search, svod2_data)
|
||||
full = legacy_full(client, sheet, headers)
|
||||
expected = screen_rows(full, search)
|
||||
rows, totals = _all_pages(client, sheet, headers, 3, view="compact", search=search)
|
||||
assert _line_ids(rows) == _line_ids(expected)
|
||||
assert all(t == len(expected) for t in totals)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sheet", SHEETS)
|
||||
@pytest.mark.parametrize("name", list(FILTERS))
|
||||
def test_filters_match_screen_oracle(client, svod2_data, headers, sheet, name):
|
||||
params = FILTERS[name]
|
||||
search, filters = _split(params)
|
||||
full = legacy_full(client, sheet, headers)
|
||||
expected = screen_rows(full, search, filters)
|
||||
rows, totals = _all_pages(client, sheet, headers, 3, view="compact", **params)
|
||||
assert _line_ids(rows) == _line_ids(expected)
|
||||
assert all(t == len(expected) for t in totals)
|
||||
|
||||
|
||||
def _facets_to_dict(result: dict) -> dict:
|
||||
out = {"total": result["total"], "search_total": result["search_total"]}
|
||||
for col in ("smeta_type", "smeta_direction", "org_name"):
|
||||
out[col] = {item["value"]: item["count"] for item in result[col]}
|
||||
return out
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sheet", SHEETS)
|
||||
@pytest.mark.parametrize("name", list(FILTERS))
|
||||
@pytest.mark.parametrize("search", SEARCHES[:6], ids=[str(i) for i in range(6)])
|
||||
def test_facets_match_screen_oracle(client, svod2_data, headers, sheet, name, search):
|
||||
_, filters = _split(FILTERS[name])
|
||||
full = legacy_full(client, sheet, headers)
|
||||
expected = screen_facets(full, search, filters)
|
||||
r = client.get(
|
||||
_url(sheet, "/facets"), params={"search": search, **filters}, headers=headers
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
assert _facets_to_dict(r.json()["result"]) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sheet", SHEETS)
|
||||
@pytest.mark.parametrize("search", SEARCHES, ids=[str(i) for i in range(len(SEARCHES))])
|
||||
def test_found_matches_screen_oracle(client, svod2_data, headers, sheet, search):
|
||||
search = _resolve_search(search, svod2_data)
|
||||
full = legacy_full(client, sheet, headers)
|
||||
r = client.get(_url(sheet, "/facets"), params={"search": search}, headers=headers)
|
||||
assert r.status_code == 200, r.text
|
||||
result = r.json()["result"]
|
||||
assert (result["search_total"], result["total"]) == screen_found(full, search)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sheet", SHEETS)
|
||||
def test_compact_is_projection_of_full(client, svod2_data, headers, sheet):
|
||||
params = {"offset": 0, "limit": 5}
|
||||
full = client.get(_url(sheet), params={**params, "view": "full"}, headers=headers)
|
||||
compact = client.get(_url(sheet), params={**params, "view": "compact"}, headers=headers)
|
||||
assert full.status_code == 200 and compact.status_code == 200
|
||||
full_rows = full.json()["result"]
|
||||
compact_rows = compact.json()["result"]
|
||||
assert full_rows and len(full_rows) == len(compact_rows)
|
||||
for f, c in zip(full_rows, compact_rows):
|
||||
assert (c["row_type"], c["depth"], c["sort_order"]) == (
|
||||
f["row_type"], f["depth"], f["sort_order"],
|
||||
)
|
||||
assert c["data"] == compact_projection(f["data"])
|
||||
|
||||
|
||||
def test_rf_ckk_only_value_is_searchable(client, svod2_data, headers):
|
||||
r = client.get(
|
||||
_url("RF"),
|
||||
params={"view": "compact", "search": svod2_data["ckk_only_value"]},
|
||||
headers=headers,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["total"] >= 1
|
||||
assert body["result"], "строка с значением только из ККК не найдена поиском"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sheet", SHEETS)
|
||||
def test_isp_foreign_org_is_empty(client, svod2_data, isp_tokens, auth_headers, sheet):
|
||||
isp = auth_headers(isp_tokens)
|
||||
r = client.get(
|
||||
_url(sheet),
|
||||
params={"view": "full", "org_ids": svod2_data["org_b"], "offset": 0, "limit": 50},
|
||||
headers=isp,
|
||||
)
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["result"] == []
|
||||
assert body["total"] == 0
|
||||
|
||||
f = client.get(
|
||||
_url(sheet, "/facets"), params={"org_ids": svod2_data["org_b"]}, headers=isp
|
||||
)
|
||||
assert f.status_code == 200
|
||||
result = f.json()["result"]
|
||||
assert result["total"] == 0 and result["search_total"] == 0
|
||||
assert result["smeta_type"] == []
|
||||
assert result["smeta_direction"] == []
|
||||
assert result["org_name"] == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sheet", SHEETS)
|
||||
def test_isp_sees_only_own_org(
|
||||
client, svod2_data, headers, isp_tokens, auth_headers, sheet
|
||||
):
|
||||
admin_rows = legacy_full(client, sheet, headers)
|
||||
own = [
|
||||
r for r in admin_rows
|
||||
if r["data"]["header"]["source_org_unit_id"] == svod2_data["org_a"]
|
||||
]
|
||||
assert own
|
||||
r = client.get(
|
||||
_url(sheet),
|
||||
params={"view": "full", "offset": 0, "limit": 50},
|
||||
headers=auth_headers(isp_tokens),
|
||||
)
|
||||
assert r.status_code == 200
|
||||
assert r.json()["total"] == len(own)
|
||||
|
||||
|
||||
def test_facets_unknown_sheet_404(client, svod2_data, headers):
|
||||
r = client.get(_url("XX", "/facets"), headers=headers)
|
||||
assert r.status_code == 404
|
||||
57
api/tests/sqls/svod2_seed.sql
Normal file
57
api/tests/sqls/svod2_seed.sql
Normal file
@ -0,0 +1,57 @@
|
||||
-- Засев свода 2 (год 2031). Скрипт режется по точке с запятой, поэтому без двоеточий и фигурных скобок в тексте.
|
||||
INSERT INTO v3.org_unit (id, title, is_active, is_ssp) VALUES
|
||||
(910001, 'Тестовый филиал Альфа', true, false),
|
||||
(910002, 'Филиал Ёлки', true, false),
|
||||
(910003, 'Филиал Ёлки', true, false),
|
||||
(910004, 'филиал ёлки ', true, false);
|
||||
|
||||
INSERT INTO v3.user_org (id, user_id, org_unit_id) VALUES (910001, 2, 910001);
|
||||
|
||||
INSERT INTO v3.budget_form (id, form_type_code, "year", org_unit_id) VALUES
|
||||
(910001, 'FORM_1', 2031, 910001),
|
||||
(910002, 'FORM_4', 2031, 910001),
|
||||
(910003, 'FORM_2', 2031, 910001),
|
||||
(910004, 'FORM_1', 2031, 910002),
|
||||
(910005, 'FORM_2', 2031, 910002),
|
||||
(910006, 'FORM_1', 2031, 910003),
|
||||
(910007, 'FORM_1', 2031, 910004);
|
||||
|
||||
INSERT INTO v3.budget_line (id, budget_form_id, expense_item_id, "name", direction) VALUES
|
||||
(910001, 910001, (SELECT id FROM v3.expense_item WHERE sheet = 'AHR' ORDER BY id OFFSET 3 LIMIT 1), 'Аренда помещения', 'Support'),
|
||||
(910002, 910001, (SELECT id FROM v3.expense_item WHERE sheet = 'CAP' ORDER BY id OFFSET 3 LIMIT 1), NULL, 'Development'),
|
||||
(910003, 910001, (SELECT id FROM v3.expense_item WHERE sheet = 'OPER' ORDER BY id OFFSET 3 LIMIT 1), 'ЁЛКА новогодняя', NULL),
|
||||
(910004, 910001, (SELECT id FROM v3.expense_item WHERE sheet = 'AHR' ORDER BY id OFFSET 5 LIMIT 1), 'Договор с Лютиком', 'Support'),
|
||||
(910005, 910002, (SELECT id FROM v3.expense_item WHERE sheet = 'CAP' ORDER BY id OFFSET 5 LIMIT 1), 'Капвложения форма 4', 'Development'),
|
||||
(910006, 910002, (SELECT id FROM v3.expense_item WHERE sheet = 'AHR' ORDER BY id OFFSET 7 LIMIT 1), NULL, NULL),
|
||||
(910007, 910003, (SELECT id FROM v3.expense_item WHERE sheet = 'AHR' ORDER BY id OFFSET 3 LIMIT 1), 'Аренда помещения РФ', 'Support'),
|
||||
(910008, 910003, (SELECT id FROM v3.expense_item WHERE sheet = 'CAP' ORDER BY id OFFSET 3 LIMIT 1), 'Только ККК', 'Development'),
|
||||
(910009, 910003, (SELECT id FROM v3.expense_item WHERE sheet = 'OPER' ORDER BY id OFFSET 3 LIMIT 1), 'Операционные РФ', NULL),
|
||||
(910010, 910004, (SELECT id FROM v3.expense_item WHERE sheet = 'AHR' ORDER BY id OFFSET 3 LIMIT 1), 'Аренда филиал Б', 'Development'),
|
||||
(910011, 910005, (SELECT id FROM v3.expense_item WHERE sheet = 'OPER' ORDER BY id OFFSET 5 LIMIT 1), 'РФ филиал Б', 'Support'),
|
||||
(910012, 910006, (SELECT id FROM v3.expense_item WHERE sheet = 'CAP' ORDER BY id OFFSET 3 LIMIT 1), 'Двойник Ёлки', 'Support'),
|
||||
(910013, 910007, (SELECT id FROM v3.expense_item WHERE sheet = 'OPER' ORDER BY id OFFSET 3 LIMIT 1), 'Вариант регистра', 'Development');
|
||||
|
||||
INSERT INTO v3.plan (line_id, plan_q1, plan_q2, plan_q3, plan_q4, "comment") VALUES
|
||||
(910001, 100.00, 0, 0, 0, NULL),
|
||||
(910002, 250.50, 10, 0, 0, 'комментарий к плану'),
|
||||
(910003, 0, 0, 0, 0, 'только комментарий'),
|
||||
(910005, 5, 6, 7, 8, NULL),
|
||||
(910006, 1, 0, 0, 0, NULL),
|
||||
(910007, 300, 0, 0, 0, NULL),
|
||||
(910009, 42, 0, 0, 0, NULL),
|
||||
(910010, 11, 12, 0, 0, NULL),
|
||||
(910011, 9, 0, 0, 0, NULL),
|
||||
(910012, 15, 0, 0, 0, NULL),
|
||||
(910013, 17, 0, 0, 0, NULL);
|
||||
|
||||
INSERT INTO v3.contract_summary (line_id, total_amount, counterparty, reference) VALUES
|
||||
(910001, 500.00, 'ООО Ромашка', 'ДС-1'),
|
||||
(910004, 70.00, 'ООО Лютик', 'ДС-2');
|
||||
|
||||
INSERT INTO v3.contract_detail (line_id, counterparty, reference, ceiling_amount, expenses_q1, expenses_q2) VALUES
|
||||
(910001, NULL, 'Д-1', 1234.50, 20, 30),
|
||||
(910002, 'ООО Ромашка Плюс', 'Д-2', 99, 1, 2),
|
||||
(910008, NULL, 'Д-3', NULL, NULL, NULL);
|
||||
|
||||
INSERT INTO v3.ckk (line_id, ceiling_amount, expenses_q1, expenses_q2) VALUES
|
||||
(910008, 1000, 777.25, 5);
|
||||
Loading…
x
Reference in New Issue
Block a user