Flatten budget-app contents into repo root
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
from io import BytesIO
|
||||
|
||||
import openpyxl
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.main import app
|
||||
|
||||
H = {"X-Requested-With": "budget-app"}
|
||||
|
||||
|
||||
def login(email, password):
|
||||
c = TestClient(app)
|
||||
c.headers.update(H)
|
||||
assert c.post("/api/auth/login", json={"email": email, "password": password}).status_code == 200
|
||||
return c
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def org(client, imported):
|
||||
"""IT (with the imported 2027 budget) and F&B, one editor and one approver in each."""
|
||||
it = client.get("/api/departments").json()[0]
|
||||
fb = client.post("/api/departments", json={"name": "F&B", "categories_from": "defaults"}).json()
|
||||
users = {}
|
||||
for key, role, dept in (("it_ed", "editor", it), ("it_ap", "approver", it),
|
||||
("fb_ed", "editor", fb), ("fb_ap", "approver", fb)):
|
||||
r = client.post("/api/users", json={"email": f"{key}@t.local", "name": key, "role": role,
|
||||
"password": "password-123", "department_id": dept["id"]})
|
||||
assert r.status_code == 200, r.text
|
||||
users[key] = login(f"{key}@t.local", "password-123")
|
||||
return {"it": it, "fb": fb, "year": imported, **users}
|
||||
|
||||
|
||||
def test_categories_are_per_department(client, org):
|
||||
it_cats = client.get(f"/api/categories?department_id={org['it']['id']}").json()
|
||||
fb_cats = client.get(f"/api/categories?department_id={org['fb']['id']}").json()
|
||||
assert len(it_cats) == len(fb_cats) == 50
|
||||
assert not {c["id"] for c in it_cats} & {c["id"] for c in fb_cats}
|
||||
# a line in an IT budget cannot use an F&B category
|
||||
r = client.post(f"/api/years/{org['year']}/lines", json={"category_id": fb_cats[0]["id"], "name": "x"})
|
||||
assert r.status_code == 400 and r.json()["detail"] == "invalid_category"
|
||||
# the same year can exist once per department
|
||||
r = org["fb_ed"].post("/api/years", json={"year": 2027})
|
||||
assert r.status_code == 200 and r.json()["department"] == "F&B"
|
||||
|
||||
|
||||
def test_other_departments_can_only_view(org):
|
||||
fb = org["fb_ed"]
|
||||
y = fb.get(f"/api/years/{org['year']}").json()
|
||||
assert y["department"] == "IT" and y["actions"]["edit"] is False
|
||||
line = y["categories"][1]["lines"][0]
|
||||
assert fb.put(f"/api/lines/{line['id']}", json={"name": "hack"}).status_code == 403
|
||||
assert fb.post(f"/api/years/{org['year']}/status", json={"action": "submit"}).status_code == 403
|
||||
assert fb.get(f"/api/years/{org['year']}/reports/cost-report.xlsx").status_code == 200
|
||||
assert org["it_ed"].put(f"/api/lines/{line['id']}", json={"remarks": "ok"}).status_code == 200
|
||||
|
||||
|
||||
def test_custom_workflow(client, org):
|
||||
it = org["it"]["id"]
|
||||
users = {u["email"]: u["id"] for u in client.get("/api/users").json()}
|
||||
r = client.put(f"/api/departments/{it}/workflow", json={"steps": [
|
||||
{"name": "Department head", "approver_mode": "role"},
|
||||
{"name": "Finance", "approver_mode": "users", "approver_ids": [users["fb_ap@t.local"]]},
|
||||
]})
|
||||
assert r.status_code == 200 and [s["name"] for s in r.json()["steps"]] == ["Department head", "Finance"]
|
||||
y = org["year"]
|
||||
assert org["it_ed"].post(f"/api/years/{y}/status", json={"action": "submit"}).json()["current_step"] == 0
|
||||
# step 1: only IT approvers; the F&B approver is not on this step
|
||||
assert org["fb_ap"].post(f"/api/years/{y}/status", json={"action": "approve"}).status_code == 403
|
||||
assert org["it_ed"].post(f"/api/years/{y}/status", json={"action": "approve"}).status_code == 403
|
||||
r = org["it_ap"].post(f"/api/years/{y}/status", json={"action": "approve"})
|
||||
assert r.json()["status"] == "in_review" and r.json()["current_step"] == 1
|
||||
# step 2: only the named user
|
||||
assert org["it_ap"].post(f"/api/years/{y}/status", json={"action": "approve"}).status_code == 403
|
||||
r = org["fb_ap"].post(f"/api/years/{y}/status", json={"action": "approve"})
|
||||
assert r.json()["status"] == "approved"
|
||||
# the workflow cannot be changed while a budget is in review
|
||||
org["it_ap"].post(f"/api/years/{y}/status", json={"action": "reopen", "comment": "fix a line"})
|
||||
org["it_ed"].post(f"/api/years/{y}/status", json={"action": "submit"})
|
||||
assert client.put(f"/api/departments/{it}/workflow", json={"steps": []}).status_code == 409
|
||||
|
||||
|
||||
def test_template_roundtrip(client, org):
|
||||
fb = org["fb"]["id"]
|
||||
r = org["fb_ed"].get(f"/api/departments/{fb}/template.xlsx?lang=pt&year=2028&rate=3")
|
||||
assert r.status_code == 200
|
||||
wb = openpyxl.load_workbook(BytesIO(r.content))
|
||||
assert wb.sheetnames == ["Instruções", "Linhas", "Listas"]
|
||||
ws = wb["Linhas"]
|
||||
rows = [
|
||||
["Formação", "Curso de barista", "", "Chiado", 1000, "=4*300", "Percentagem", 10, "", "nota"],
|
||||
["Vinhos", "Carta de vinhos", "", "", 5000, 5000, "Fator", 1.2, "Preços", ""],
|
||||
["Receitas", "Eventos", "", "Grupo", 0, None, None, None, "", ""] + [100] * 12,
|
||||
["Nova categoria", "Algo novo", "", "Lua", 10, 10, "", None, "", ""],
|
||||
]
|
||||
for i, row in enumerate(rows):
|
||||
for j, v in enumerate(row):
|
||||
ws.cell(9 + i, j + 1, v)
|
||||
buf = BytesIO()
|
||||
wb.save(buf)
|
||||
r = org["fb_ed"].post("/api/years/import", files={"file": ("t.xlsx", buf.getvalue())},
|
||||
data={"department_id": str(fb)})
|
||||
assert r.status_code == 200, r.text
|
||||
res = r.json()
|
||||
assert res["lines"] == 4 and res["categories_created"] == ["Nova categoria"]
|
||||
assert any("Lua" in w for w in res["warnings"])
|
||||
y = client.get(f"/api/years/{res['year']['id']}").json()
|
||||
assert y["year"] == 2028 and y["default_rate"] == pytest.approx(0.03) and y["department"] == "F&B"
|
||||
lines = {l["name"]: l for c in y["categories"] for l in c["lines"]}
|
||||
assert lines["Curso de barista"]["adjusted"] == 1320
|
||||
assert lines["Carta de vinhos"]["adjusted"] == 6000
|
||||
assert lines["Eventos"]["adjusted"] == 1200 and lines["Eventos"]["monthly_kind"] == "manual"
|
||||
assert lines["Algo novo"]["adjusted"] == pytest.approx(10.3)
|
||||
# the IT editor cannot import into F&B
|
||||
r = org["it_ed"].post("/api/years/import", files={"file": ("t.xlsx", buf.getvalue())},
|
||||
data={"department_id": str(fb), "replace": "true"})
|
||||
assert r.status_code == 403
|
||||
|
||||
|
||||
def test_prefilled_template_reimports_same_totals(client, org):
|
||||
it = org["it"]["id"]
|
||||
r = client.get(f"/api/departments/{it}/template.xlsx?lang=en&from_year_id={org['year']}&year=2029&rate=5.5")
|
||||
res = client.post("/api/years/import", files={"file": ("t.xlsx", r.content)}, data={"department_id": str(it)}).json()
|
||||
assert res["lines"] == 54
|
||||
a = client.get(f"/api/years/{org['year']}").json()["costs"]
|
||||
b = client.get(f"/api/years/{res['year']['id']}").json()["costs"]
|
||||
assert a["adjusted"] == pytest.approx(b["adjusted"], abs=0.01) and a["prior"] == b["prior"]
|
||||
Reference in New Issue
Block a user