Merge remote-tracking branch 'origin/dev' into main_laptop_1
This commit is contained in:
@@ -50,4 +50,4 @@
|
||||
- 실무 레시피 350개 중 품셈 근거문구 보유는 82개뿐이다. 근거 없는 268개는 현행값으로 임의 확정하지 않는다.
|
||||
- 과거 임도품셈 절 번호가 현행 산림사업 표준품셈에서 크게 바뀌었다. 번호만 같은 다른 절을 자동 연결하지 않는다.
|
||||
- JSON의 `수량문자_확인`은 후보 절 본문에 같은 숫자가 있는지를 세는 1차 검색값이다. `일치` 확정은 위 두 항목처럼 조건과 행을 직접 대조한 경우만 한다.
|
||||
- 후속 수동 대조는 근거 절이 확인된 11개부터 하고, 복수 절 참조 69개는 과거판-현행판 절 대응표가 있어야 안전하다.
|
||||
- 후속 판 대응표는 `33_임도품셈_판_대응표.md`와 `30_원자료/임도품셈_판_대응표.csv`에 작성했다. 확정 2·추정 11·미확인 1이며, 추정·미확인은 자동 치환에 쓰지 않는다.
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# 과거 임도품셈 ↔ 2026 현행 산림사업 표준품셈 대응표
|
||||
|
||||
작성일: 2026-09-13
|
||||
산출물: `30_원자료/임도품셈_판_대응표.csv`
|
||||
현행 기준: 산림청고시 제2025-82호, 2026-01-01 시행
|
||||
|
||||
## 결과
|
||||
|
||||
STmate 레시피에 남은 임도계 품셈 인용 **14개를 전수 등록**했다.
|
||||
|
||||
| 판정 | 수 | 뜻 |
|
||||
|---|---:|---|
|
||||
| 확정 | 2 | 명칭·적용조건과 관측 수량을 현행 원문에서 직접 대조 |
|
||||
| 추정 | 11 | 명칭·공종 맥락으로 현행 후보를 특정했으나 과거 원문판 또는 수량 대조가 부족 |
|
||||
| 미확인 | 1 | 현행 후보가 작업방식까지 같다는 근거가 없음 |
|
||||
|
||||
확정은 `2-3 제근 → 9-21 제근`과, STmate에서 `4-16-2`로 인용한 **절취없는 단끊기** → `5-16-1 단끊기` 두 건이다. 후자는 보통인부 `0.34·0.36·1.17인/100m`가 현행 세 공정과 각각 일치하는 범위에서만 확정했다.
|
||||
|
||||
## 코드 사용 규칙
|
||||
|
||||
- 대응표의 현행 절은 `work_item_master_2026-01-01.json`의 `FP-*` 코드와 표 ID를 함께 기록했다.
|
||||
- 과거 한 절이 현행 여러 절로 나뉜 후보는 슬래시로 모두 보존했다. 대표 코드 하나를 임의 선택하지 않는다.
|
||||
- `추정`과 `미확인`은 코드 자동 치환에 사용하지 않는다. 값도 옮기지 않고 확인 대기로 세운다.
|
||||
- 과거 판본 원문이 확보되면 표 제목·주석·수량을 대조해 `추정 → 확정` 또는 `미확인`으로 갱신한다.
|
||||
|
||||
## 경계
|
||||
|
||||
저장소에는 2026 현행판만 있고 STmate가 인용한 과거 임도품셈 원문판은 없다. 따라서 같은 이름이라는 이유만으로 확정하지 않았다. 특히 `7-3 노면정리`는 현행 `3-4-3`의 인력식 노면정지·노면굴기와 작업방식이 달라 현행 절을 `미확인`으로 남겼다.
|
||||
@@ -0,0 +1,15 @@
|
||||
과거판,과거_절,과거_인용명,STmate_적용공종,현행판,현행_절,현행_FP코드,현행_명칭,현행_표ID,판정,근거와_경계
|
||||
"판본 미확인(STmate 2024~2025 출력)","번호 미기재","되메우기 및 다짐","구조물 되메우기(사질양토) 기계100%(굴삭기 0.7㎥)","산림청고시 2025-82호(2026-01-01)","9-14-1","FP-09-14-01","되메우기","F0279","추정","공종명과 적용맥락 일치. 과거 절 번호·원문 미확보"
|
||||
"판본 미확인(STmate 2024~2025 출력)","5-1-2 사","암터파기","구조물터파기(육상암 0~1m) 기계100%(굴삭기 0.7㎥)","산림청고시 2025-82호(2026-01-01)","9-13-7","FP-09-13-07","육상 암절취(0~1m)","F0267","추정","구조물·육상암·0~1m 조건 일치. 수량 전수 대조 전"
|
||||
"판본 미확인(STmate 2024~2025 출력)","7-3","노면정리(건설품셈 기계 시공 적용)","노면정리","산림청고시 2025-82호(2026-01-01)","미확인","미확인","미확인","미확인","미확인","현행 3-4-3은 노면정지·노면굴기 인력 보수표라 기계식 노면정리와 동일하다고 볼 수 없음"
|
||||
"판본 미확인(STmate 2024~2025 출력)","4-16-2","선떼붙이기공","단끊기","산림청고시 2025-82호(2026-01-01)","5-16 / 5-16-1 / 5-16-2","FP-05-16 / FP-05-16-01 / FP-05-16-02","선떼붙이기공 / 단끊기 / 선떼붙이기","F0121 / F0122","추정","과거 한 절이 현행 부모와 두 하위 절로 분리된 후보. 단끊기 일부만 별도 확정"
|
||||
"판본 미확인(STmate 2024~2025 출력)","4-16-2","절취없는 단끊기","단끊기 절취없음","산림청고시 2025-82호(2026-01-01)","5-16-1","FP-05-16-01","단끊기","F0121","확정","보통인부 0.34·0.36·1.17인/100m가 현행 세 공정과 각각 일치. 적용범위는 절취없는 구성에 한정"
|
||||
"판본 미확인(STmate 2024~2025 출력)","3-6 가","임도성토면 다짐","성토사면다짐 B/H0.7+콤팩터","산림청고시 2025-82호(2026-01-01)","9-17-1","FP-09-17-01","비탈면 다짐","미확인","추정","성토사면과 현행 비탈면 다짐의 기능상 후보. 현행 마스터에 직접 표가 없어 수량 대조 필요"
|
||||
"판본 미확인(STmate 2024~2025 출력)","9-3","임도성토면다짐","성토사면다짐 굴삭기0.7㎥","산림청고시 2025-82호(2026-01-01)","9-17-1","FP-09-17-01","비탈면 다짐","미확인","추정","같은 STmate 자료 안에서도 과거 절 인용이 3-6과 9-3으로 갈림. 과거 판본부터 확인 필요"
|
||||
"판본 미확인(STmate 2024~2025 출력)","4-16-4","파종공","씨뿌리기(성토면) 줄파종","산림청고시 2025-82호(2026-01-01)","5-18","FP-05-18","씨뿌리기(줄)","F0125","추정","줄파종·씨뿌리기 명칭과 적용맥락 일치. 과거 원문·수량 미대조"
|
||||
"판본 미확인(STmate 2024~2025 출력)","3-3-2","굴삭기 적용 암절취","암절취 굴삭기0.7㎥+대형브레카","산림청고시 2025-82호(2026-01-01)","9-4 / 9-4-1 / 9-4-2","FP-09-04 / FP-09-04-01 / FP-09-04-02","암절취 / 암파쇄 / 집토","F0241 / F0242","추정","현행은 암절취를 암파쇄+집토 단계합산형으로 분리. 과거 한 절과 일대다 후보"
|
||||
"판본 미확인(STmate 2024~2025 출력)","2-3","제근(100㎡당)","제근","산림청고시 2025-82호(2026-01-01)","9-21","FP-09-21","제근","F0294","확정","밀림 보통인부 0.05인이 현행 9-21과 직접 일치"
|
||||
"판본 미확인(STmate 2024~2025 출력)","5-1-1 나","암절취","측구터파기(암) 기계100%(굴삭기 0.4㎥)","산림청고시 2025-82호(2026-01-01)","9-12-2","FP-09-12-02","암절취","F0259","추정","측구터파기·암 조건으로 현행 절 후보 특정. 수량·장비조건 미대조"
|
||||
"판본 미확인(STmate 2024~2025 출력)","3-7 가","층따기","층따기","산림청고시 2025-82호(2026-01-01)","9-18","FP-09-18","층따기","F0287","추정","명칭 직접 일치하지만 과거 원문판이 없어 절 동일성 확정 불가"
|
||||
"판본 미확인(STmate 2024~2025 출력)","3-3-1","토사 굴삭기 적용","토사절취(흙깎기) 굴삭기0.7㎥","산림청고시 2025-82호(2026-01-01)","9-3-2","FP-09-03-02","기계","F0240","추정","현행 9-3 토사깍기의 기계 하위 절 후보. 수량·토질조건 미대조"
|
||||
"판본 미확인(STmate 2024~2025 출력)","4-39","쇄석·혼합석 부설","혼합석부설 T=0.1m","산림청고시 2025-82호(2026-01-01)","11-4","FP-11-04","쇄석․혼합석 부설","F0328","추정","명칭 직접 일치하지만 두께·장비·수량 조건 미대조"
|
||||
|
@@ -102,7 +102,7 @@
|
||||
| `jb_shapes.py` · `jb_block_map.py` · `jb_expansion_diff.py` | JB 43개 형태·블록·확장 차이 |
|
||||
| `rate_grid_defs.py` · `settings_axis_map.py` | 폼에서 그리드·선택축 추출 |
|
||||
|
||||
후속 레시피 분석은 `20_분석/31_일위대가_레시피_사전.md`와 `20_분석/32_특수산식과_현행품셈_대조.md`를 본다.
|
||||
후속 레시피 분석은 `20_분석/31_일위대가_레시피_사전.md`부터 `20_분석/33_임도품셈_판_대응표.md`까지 본다.
|
||||
|
||||
## 다음
|
||||
|
||||
|
||||
@@ -210,6 +210,77 @@ def test_재귀는_5단까지() -> None:
|
||||
assert six["complete"] is False # 윗 표 → d1 → … → d5 = 6단
|
||||
|
||||
|
||||
def test_수동_단가는_막힌_줄을_세우고_미확정으로_센다() -> None:
|
||||
"""PLAN 3장 ③ — 모르터(코드 미정)에 수동 단가 · 0.02349㎥ × 재료 80,000 = 1,879.2."""
|
||||
manual = {
|
||||
"2": {
|
||||
"material": 80000,
|
||||
"labor": 0,
|
||||
"expense": 0,
|
||||
"source": "견적",
|
||||
"entered_at": "2026-09-13",
|
||||
}
|
||||
}
|
||||
table = unit_price_table(TEMPLATE, _sheet(), BOOK, _variant, (), manual)
|
||||
mortar = next(row for row in table["rows"] if row["seq"] == 2)
|
||||
assert mortar["manual"] is True and mortar["manual_source"] == "견적"
|
||||
assert mortar["material"] == pytest.approx(1879.2) and mortar["reason"] == ""
|
||||
assert table["blocked"] == 0 and table["complete"] is True and table["unconfirmed"] == 1
|
||||
# 단가표에 값이 있는 줄도 수동 단가가 이김 — 그래도 미확정.
|
||||
manual["1"] = {"material": 1, "labor": 0, "expense": 0}
|
||||
again = unit_price_table(TEMPLATE, _sheet(), BOOK, _variant, (), manual)
|
||||
assert again["rows"][0]["total"] == pytest.approx(2.6) and again["unconfirmed"] == 2
|
||||
|
||||
|
||||
def test_줄_조합_저장본은_양식과_같으면_지우고_수동_단가는_남은_줄만() -> None:
|
||||
from B08_Quantity.B08_Quantity_Engine_StructureUnitPrice import (
|
||||
save_manual,
|
||||
save_rows,
|
||||
with_rows,
|
||||
)
|
||||
|
||||
template = {"type_id": "masonry_wet", **TEMPLATE}
|
||||
rows = TEMPLATE["unit_price"]["rows"]
|
||||
edited = [*rows[:2], {"seq": 4, "name": "추가", "quantity": 1.0, "ref_code": "B-FP-12-25"}]
|
||||
merged, changed = save_rows({"other": []}, "masonry_wet", template, edited)
|
||||
assert changed and merged["masonry_wet"] == edited and "other" in merged
|
||||
back, changed = save_rows(merged, "masonry_wet", template, [dict(row) for row in rows])
|
||||
assert changed and back == {"other": []}
|
||||
assert with_rows(template, edited)["unit_price"]["rows"] == edited
|
||||
assert with_rows({"code": "x"}, None)["unit_price"]["rows"] == []
|
||||
|
||||
price = {"material": 10.0, "labor": 0.0, "expense": 0.0, "source": "견적"}
|
||||
first = save_manual({}, "masonry_wet", edited, {"2": price, "3": price}, "2026-09-01")
|
||||
assert list(first["masonry_wet"]) == ["2"] # 뺀 줄(3)의 값은 안 남음
|
||||
same = save_manual(first, "masonry_wet", edited, {"2": price}, "2026-09-13")
|
||||
assert same["masonry_wet"]["2"]["entered_at"] == "2026-09-01" # 값이 같으면 옛 날짜
|
||||
moved = save_manual(
|
||||
first, "masonry_wet", edited, {"2": {**price, "material": 11.0}}, "2026-09-13"
|
||||
)
|
||||
assert moved["masonry_wet"]["2"]["entered_at"] == "2026-09-13"
|
||||
assert save_manual(first, "masonry_wet", edited, {}, "2026-09-13") == {}
|
||||
|
||||
|
||||
def test_고르개는_갈래별로_낱말이_모두_든_항목과_단가를_낸다() -> None:
|
||||
from B08_Quantity.B08_Quantity_Engine_StructureUnitPrice import search_titles
|
||||
from B09_Estimation.B09_Estimation_PriceBook import PriceBook, PriceKind, PriceTitle
|
||||
|
||||
book = PriceBook()
|
||||
slots = [None] * 5
|
||||
book.add_title(
|
||||
PriceTitle("M-001", PriceKind.MATERIAL, "시멘트", "40kg", "포", [*slots, Decimal(5000)])
|
||||
)
|
||||
book.add_title(PriceTitle("M-002", PriceKind.MATERIAL, "시멘트 모르터", "1:3", "m3"))
|
||||
book.add_title(PriceTitle("B-FP-01", PriceKind.UNIT_PRICE, "모르터 비비기", "", "m3"))
|
||||
found = search_titles(book, "시멘트", "resource")
|
||||
assert [item["code"] for item in found] == ["M-001", "M-002"]
|
||||
assert found[0]["price"] == pytest.approx(5000.0) and found[1]["price"] is None
|
||||
assert found[1]["unit"] == "㎥"
|
||||
assert [item["code"] for item in search_titles(book, "모르터 1:3", "resource")] == ["M-002"]
|
||||
assert [item["code"] for item in search_titles(book, "모르터", "work")] == ["B-FP-01"]
|
||||
assert search_titles(book, " ", "work") == []
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> TestClient:
|
||||
root = tmp_path / "project"
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
"""구조물도 일위대가 줄 고치기 창구 (2026-09-13, PLAN 3장 ③).
|
||||
|
||||
겨누는 것
|
||||
① 줄 더하기·빼기를 저장하면 표가 그 조합으로 서고, 양식대로 보내면 저장본이 지워짐
|
||||
② 수동 단가는 막힌 줄을 세우고 「미확정」으로 셈 · 넣은 날짜는 서버가 붙임
|
||||
③ ⛔ 저장 자리는 둘 — [내 라이브러리에 저장]은 줄 조합만 싣고 수동 단가는 안 실음
|
||||
④ 다른 양식을 가져오면 그 종류의 줄 조합·수동 단가가 비워짐
|
||||
⑤ 고르개가 실제 단가표에서 품셈·자원을 찾음 · 차례가 겹치면 거절
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
import B08_Quantity.B08_Quantity_Engine_StructureLibrary as library_module # noqa: E402
|
||||
import B08_Quantity.B08_Quantity_Router_Material as material_module # noqa: E402
|
||||
import B08_Quantity.B08_Quantity_Router_StructureSheet as router_module # noqa: E402
|
||||
from B05_Profile.B05_Profile_Structures_Repository import save_structures # noqa: E402
|
||||
from B05_Profile.B05_Profile_Structures_Schema import StructureInstance # noqa: E402
|
||||
from B08_Quantity.B08_Quantity_Engine_StructureTemplate import load_template # noqa: E402
|
||||
from common_util.common_util_auth import verify_session # noqa: E402
|
||||
from common_util.common_util_project_settings import quantity_settings # noqa: E402
|
||||
|
||||
PROJECT_ID = "55555555-5555-5555-5555-555555555555"
|
||||
SHEETS = f"/api/projects/{PROJECT_ID}/quantity/structure-sheets"
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def project(tmp_path: Path) -> Path:
|
||||
root = tmp_path / "project"
|
||||
root.mkdir()
|
||||
wall = StructureInstance.model_validate(
|
||||
{
|
||||
"type_id": "masonry_wet",
|
||||
"placement": "interval",
|
||||
"start_m": 0.0,
|
||||
"end_m": 10.0,
|
||||
"options": {"height_m": 2.5, "back_len_cm": 45},
|
||||
}
|
||||
)
|
||||
save_structures(str(root), [wall], base_revision=0)
|
||||
return root
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def client(project: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> TestClient:
|
||||
async def fake_root(project_id):
|
||||
return str(project)
|
||||
|
||||
async def no_route(project_id):
|
||||
return {}
|
||||
|
||||
async def real_build(project_id):
|
||||
from B09_Estimation.B09_Estimation_UnitPrice import cached_build
|
||||
|
||||
return cached_build()
|
||||
|
||||
monkeypatch.setattr(library_module, "STORAGE_BASE_DIR", str(tmp_path / "storage"))
|
||||
monkeypatch.setattr(router_module, "_project_root", fake_root)
|
||||
monkeypatch.setattr(router_module, "_price_build", real_build)
|
||||
monkeypatch.setattr(material_module, "_section_modes", no_route)
|
||||
monkeypatch.setattr(material_module, "_ground_types", no_route)
|
||||
app = FastAPI()
|
||||
app.include_router(router_module.router)
|
||||
app.dependency_overrides[verify_session] = lambda: {"company_id": 7, "user_id": 42}
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def _key(client: TestClient) -> str:
|
||||
return client.get(SHEETS).json()["sheets"][0]["key"]
|
||||
|
||||
|
||||
def _table(client: TestClient, key: str) -> dict:
|
||||
response = client.get(f"{SHEETS}/unit-price", params={"sheet_key": key})
|
||||
assert response.status_code == 200, response.text
|
||||
return response.json()
|
||||
|
||||
|
||||
def test_줄을_고치고_수동_단가를_넣으면_저장_자리가_둘로_갈린다(
|
||||
client: TestClient, project: Path, tmp_path: Path
|
||||
) -> None:
|
||||
key = _key(client)
|
||||
before = _table(client, key)
|
||||
assert before["editor"]["edited"] is False and before["unit_price"]["unconfirmed"] == 0
|
||||
rows = before["editor"]["rows"]
|
||||
# 기초잡석(3) 빼고 · 고정 수량 1.5 줄 더하기 · 모르터(2)에 수동 단가.
|
||||
edited = [
|
||||
rows[0],
|
||||
rows[1],
|
||||
{"seq": 4, "name": "추가 잡석", "quantity": 1.5, "ref_code": "B-FP-12-25"},
|
||||
]
|
||||
saved = client.put(
|
||||
f"{SHEETS}/unit-price",
|
||||
json={
|
||||
"sheet_key": key,
|
||||
"rows": edited,
|
||||
"manual_prices": {
|
||||
"2": {"material": 80000, "source": "견적 3곳 평균"},
|
||||
"3": {"labor": 1},
|
||||
},
|
||||
},
|
||||
)
|
||||
assert saved.status_code == 200, saved.text
|
||||
assert saved.json()["edited"] is True and saved.json()["manual_prices"] == 1
|
||||
|
||||
after = _table(client, key)
|
||||
table = after["unit_price"]
|
||||
assert [row["seq"] for row in table["rows"]] == [1, 2, 4]
|
||||
mortar = table["rows"][1]
|
||||
assert mortar["manual"] is True and mortar["manual_entered_at"]
|
||||
assert table["unconfirmed"] == 1 and table["complete"] is True
|
||||
assert table["rows"][2]["quantity"] == pytest.approx(1.5) and table["rows"][2]["total"] > 0
|
||||
|
||||
# 저장 자리 둘 — 줄 조합은 종류별, 수동 단가는 프로젝트 칸.
|
||||
settings = quantity_settings(str(project))
|
||||
assert [row["seq"] for row in settings["structure_unit_price_rows"]["masonry_wet"]] == [1, 2, 4]
|
||||
assert settings["structure_manual_prices"]["masonry_wet"]["2"]["source"] == "견적 3곳 평균"
|
||||
|
||||
# ⛔ 내 라이브러리에는 줄 조합만 — 수동 단가 흔적이 없음.
|
||||
mine = client.put(f"{SHEETS}/library/personal", json={"sheet_key": key})
|
||||
assert mine.status_code == 200, mine.text
|
||||
folder = tmp_path / "storage" / "7" / "42" / "library"
|
||||
item = json.loads((folder / f"{mine.json()['code']}.json").read_text(encoding="utf-8"))
|
||||
assert [row["seq"] for row in item["unit_price"]["rows"]] == [1, 2, 4]
|
||||
assert "80000" not in json.dumps(item) and "견적" not in json.dumps(item)
|
||||
|
||||
# 양식대로 되돌리면 줄 조합 저장본이 지워짐.
|
||||
back = client.put(
|
||||
f"{SHEETS}/unit-price",
|
||||
json={"sheet_key": key, "rows": after["editor"]["default_rows"], "manual_prices": {}},
|
||||
)
|
||||
assert back.json()["edited"] is False
|
||||
assert "masonry_wet" not in quantity_settings(str(project))["structure_unit_price_rows"]
|
||||
|
||||
|
||||
def test_다른_양식을_가져오면_줄_조합과_수동_단가가_비워진다(
|
||||
client: TestClient, project: Path
|
||||
) -> None:
|
||||
key = _key(client)
|
||||
rows = load_template("masonry_wet")["unit_price"]["rows"][:2]
|
||||
client.put(
|
||||
f"{SHEETS}/unit-price",
|
||||
json={"sheet_key": key, "rows": rows, "manual_prices": {"2": {"material": 1}}},
|
||||
)
|
||||
taken = client.put(
|
||||
f"{SHEETS}/library/import",
|
||||
json={
|
||||
"type_id": "masonry_wet",
|
||||
"tier": "program",
|
||||
"code": load_template("masonry_wet")["code"],
|
||||
},
|
||||
)
|
||||
assert taken.status_code == 200, taken.text
|
||||
assert (
|
||||
taken.json()["cleared_unit_price_rows"] is True
|
||||
and taken.json()["cleared_manual_prices"] == 1
|
||||
)
|
||||
settings = quantity_settings(str(project))
|
||||
assert "masonry_wet" not in settings["structure_unit_price_rows"]
|
||||
assert "masonry_wet" not in settings["structure_manual_prices"]
|
||||
|
||||
|
||||
def test_고르개와_차례_겹침_거절(client: TestClient) -> None:
|
||||
found = client.get(f"{SHEETS}/price-search", params={"q": "FP-12-25", "kind": "work"})
|
||||
assert found.status_code == 200, found.text
|
||||
assert any(item["code"] == "B-FP-12-25" for item in found.json()["items"])
|
||||
resource = client.get(f"{SHEETS}/price-search", params={"q": "보통인부", "kind": "resource"})
|
||||
assert resource.json()["items"], "노임 보통인부가 자원 갈래에 있어야 함"
|
||||
|
||||
twice = [{"seq": 1, "name": "a", "quantity": 1}, {"seq": 1, "name": "b", "quantity": 1}]
|
||||
bad = client.put(f"{SHEETS}/unit-price", json={"sheet_key": _key(client), "rows": twice})
|
||||
assert bad.status_code == 422
|
||||
Reference in New Issue
Block a user