839 lines
30 KiB
Python
839 lines
30 KiB
Python
"""API кормоцеха (FeedDispenser type=mill): без периодов, рецепты-сироты."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import tempfile
|
||
import unittest
|
||
import uuid
|
||
from datetime import datetime
|
||
|
||
from sqlalchemy import func, select
|
||
|
||
from app import create_app, db
|
||
from app.models import (
|
||
Component,
|
||
FeedDispenser,
|
||
FeedingPeriod,
|
||
Ingredient,
|
||
PeriodRecipe,
|
||
Recipe,
|
||
SyncQueue,
|
||
UnloadingGroup,
|
||
)
|
||
from config import TestingConfig
|
||
|
||
|
||
class FeedMillApiConfig(TestingConfig):
|
||
_TMP_DIR = tempfile.mkdtemp(prefix="wesp-feed-mill-api-")
|
||
SQLALCHEMY_DATABASE_URI = f"sqlite:///{os.path.join(_TMP_DIR, 'recipes_test.db')}"
|
||
SQLALCHEMY_BINDS = {"reports": f"sqlite:///{os.path.join(_TMP_DIR, 'reports_test.db')}"}
|
||
AUTH_LOGIN = "mill-api-admin"
|
||
AUTH_PASSWORD = "mill-api-secret"
|
||
|
||
|
||
class FeedMillApiTests(unittest.TestCase):
|
||
def setUp(self) -> None:
|
||
self.app = create_app(FeedMillApiConfig)
|
||
self.client = self.app.test_client()
|
||
self.ctx = self.app.app_context()
|
||
self.ctx.push()
|
||
db.create_all()
|
||
login = self.client.post(
|
||
"/api/auth/login", json={"login": "mill-api-admin", "password": "mill-api-secret"}
|
||
)
|
||
self.assertEqual(login.status_code, 200)
|
||
|
||
def tearDown(self) -> None:
|
||
db.session.remove()
|
||
db.drop_all()
|
||
self.ctx.pop()
|
||
|
||
def _create_mill(self, *, name: str = "Кормоцех 1") -> str:
|
||
resp = self.client.post(
|
||
"/api/feed_dispensers",
|
||
json={
|
||
"name": name,
|
||
"farm": "Ферма А",
|
||
"operator": "Иванов",
|
||
"type": "mill",
|
||
},
|
||
)
|
||
self.assertEqual(resp.status_code, 201, resp.get_data(as_text=True))
|
||
return resp.get_json()["id"]
|
||
|
||
def _create_component(self, name: str = "Компонент А") -> str:
|
||
comp = Component(
|
||
id=str(uuid.uuid4()),
|
||
name=name,
|
||
type="grain",
|
||
dry_matter=50.0,
|
||
protein=0.0,
|
||
energy=0.0,
|
||
price=0.0,
|
||
)
|
||
db.session.add(comp)
|
||
db.session.commit()
|
||
return comp.id
|
||
|
||
def test_mill_crud_and_list_contract(self) -> None:
|
||
mill_id = self._create_mill()
|
||
|
||
get_one = self.client.get(f"/api/feed_dispensers/{mill_id}")
|
||
self.assertEqual(get_one.status_code, 200)
|
||
body = get_one.get_json()
|
||
self.assertEqual(body["type"], "mill")
|
||
self.assertEqual(body["name"], "Кормоцех 1")
|
||
self.assertEqual(body["periods"], [])
|
||
|
||
update = self.client.put(
|
||
f"/api/feed_dispensers/{mill_id}",
|
||
json={"name": "Кормоцех обновлён", "operator": "Петров"},
|
||
)
|
||
self.assertEqual(update.status_code, 200)
|
||
|
||
listed = self.client.get("/api/feed_dispensers?limit=50&offset=0")
|
||
self.assertEqual(listed.status_code, 200)
|
||
rows = listed.get_json()
|
||
mill_rows = [r for r in rows if r["id"] == mill_id]
|
||
self.assertEqual(len(mill_rows), 1)
|
||
self.assertEqual(mill_rows[0]["type"], "mill")
|
||
self.assertEqual(mill_rows[0]["name"], "Кормоцех обновлён")
|
||
|
||
delete = self.client.delete(f"/api/feed_dispensers/{mill_id}")
|
||
self.assertEqual(delete.status_code, 200)
|
||
gone = self.client.get(f"/api/feed_dispensers/{mill_id}")
|
||
self.assertEqual(gone.status_code, 404)
|
||
|
||
def test_mill_periods_empty_without_period_rows(self) -> None:
|
||
mill_id = self._create_mill()
|
||
periods = self.client.get(f"/api/feed_dispensers/{mill_id}/periods")
|
||
self.assertEqual(periods.status_code, 200)
|
||
self.assertEqual(periods.get_json(), [])
|
||
|
||
def test_mill_recipes_list_only_orphans(self) -> None:
|
||
mill_id = self._create_mill()
|
||
disp = FeedDispenser(
|
||
id="d-disp-mill-api",
|
||
name="Раздатчик",
|
||
farm="F",
|
||
operator="O",
|
||
type="dispenser",
|
||
)
|
||
period = FeedingPeriod(id="p-disp-mill-api", name="Утро", dispenser_id=disp.id)
|
||
r_orphan = Recipe(id="r-orphan-mill", name="Сирота", heads_per_trip=1, mixing_time=1)
|
||
r_linked = Recipe(id="r-linked-mill", name="В периоде", heads_per_trip=2, mixing_time=2)
|
||
db.session.add_all([disp, period, r_orphan, r_linked])
|
||
db.session.flush()
|
||
db.session.add(
|
||
PeriodRecipe(
|
||
period_id=period.id,
|
||
recipe_id=r_linked.id,
|
||
order=1,
|
||
created_at=datetime.now(),
|
||
created_by="system",
|
||
updated_by="system",
|
||
)
|
||
)
|
||
db.session.commit()
|
||
|
||
mill_recipes = self.client.get(f"/api/feed_dispensers/{mill_id}/recipes")
|
||
self.assertEqual(mill_recipes.status_code, 200)
|
||
ids = [x["id"] for x in mill_recipes.get_json()]
|
||
self.assertIn("r-orphan-mill", ids)
|
||
self.assertNotIn("r-linked-mill", ids)
|
||
|
||
disp_recipes = self.client.get("/api/feed_dispensers/d-disp-mill-api/recipes")
|
||
self.assertEqual(disp_recipes.status_code, 200)
|
||
disp_ids = [x["id"] for x in disp_recipes.get_json()]
|
||
self.assertEqual(disp_ids, ["r-linked-mill"])
|
||
|
||
def test_create_recipe_for_mill_without_period(self) -> None:
|
||
comp_id = self._create_component()
|
||
create = self.client.post(
|
||
"/api/recipes",
|
||
json={
|
||
"name": "Рецепт кормоцеха",
|
||
"headsPerTrip": 15,
|
||
"mixingTime": 8,
|
||
"tripPercent": 100,
|
||
"target_component_id": comp_id,
|
||
"ingredients": [
|
||
{
|
||
"component_id": comp_id,
|
||
"amount": 10,
|
||
"dry_matter": 50,
|
||
"weight_per_head": 1.0,
|
||
}
|
||
],
|
||
},
|
||
)
|
||
self.assertEqual(create.status_code, 201, create.get_data(as_text=True))
|
||
recipe_id = create.get_json()["id"]
|
||
|
||
get_recipe = self.client.get(f"/api/recipes/{recipe_id}")
|
||
self.assertEqual(get_recipe.status_code, 200)
|
||
body = get_recipe.get_json()
|
||
self.assertEqual(body["name"], "Рецепт кормоцеха")
|
||
self.assertEqual(body["target_component_id"], comp_id)
|
||
self.assertEqual(len(body.get("ingredients") or []), 1)
|
||
|
||
period_links = db.session.execute(
|
||
select(func.count())
|
||
.select_from(PeriodRecipe)
|
||
.where(PeriodRecipe.recipe_id == recipe_id, PeriodRecipe.is_deleted.is_(False))
|
||
).scalar()
|
||
self.assertEqual(period_links, 0)
|
||
|
||
def test_mill_recipe_visible_in_mill_list_after_create(self) -> None:
|
||
mill_id = self._create_mill()
|
||
comp_id = self._create_component("Зерно")
|
||
create = self.client.post(
|
||
"/api/recipes",
|
||
json={
|
||
"name": "Новый для цеха",
|
||
"headsPerTrip": 5,
|
||
"mixingTime": 3,
|
||
"target_component_id": comp_id,
|
||
"ingredients": [{"component_id": comp_id, "amount": 1}],
|
||
},
|
||
)
|
||
self.assertEqual(create.status_code, 201)
|
||
recipe_id = create.get_json()["id"]
|
||
|
||
listed = self.client.get(f"/api/feed_dispensers/{mill_id}/recipes")
|
||
self.assertEqual(listed.status_code, 200)
|
||
ids = [r["id"] for r in listed.get_json()]
|
||
self.assertIn(recipe_id, ids)
|
||
|
||
def test_mill_recipe_update_and_global_delete(self) -> None:
|
||
comp_id = self._create_component()
|
||
create = self.client.post(
|
||
"/api/recipes",
|
||
json={
|
||
"name": "До правки",
|
||
"headsPerTrip": 4,
|
||
"mixingTime": 2,
|
||
"target_component_id": comp_id,
|
||
"ingredients": [{"component_id": comp_id, "amount": 1}],
|
||
},
|
||
)
|
||
recipe_id = create.get_json()["id"]
|
||
|
||
update = self.client.put(
|
||
f"/api/recipes/{recipe_id}",
|
||
json={
|
||
"name": "После правки",
|
||
"heads_count": 6,
|
||
"mixing_time": 4,
|
||
"trip_percent": 100.0,
|
||
"target_component_id": comp_id,
|
||
"dry_matter_locked": False,
|
||
"unloading_link_broken": False,
|
||
"ingredients": [
|
||
{
|
||
"component_id": comp_id,
|
||
"weight_per_head": 1.0,
|
||
"amount": 2,
|
||
"dry_matter": 50.0,
|
||
"order": 1,
|
||
}
|
||
],
|
||
"unloading_groups": [
|
||
{
|
||
"name": "Г1",
|
||
"distribution_type": "percent",
|
||
"value": 100.0,
|
||
"weight": 2.0,
|
||
"order": 1,
|
||
}
|
||
],
|
||
},
|
||
)
|
||
self.assertEqual(update.status_code, 200)
|
||
self.assertTrue(update.get_json().get("success"))
|
||
|
||
get_after = self.client.get(f"/api/recipes/{recipe_id}")
|
||
self.assertEqual(get_after.get_json()["name"], "После правки")
|
||
|
||
recipe_update_task = db.session.execute(
|
||
select(SyncQueue).where(
|
||
SyncQueue.table_name == "recipe",
|
||
SyncQueue.record_id == recipe_id,
|
||
SyncQueue.action == "update",
|
||
)
|
||
).scalar_one_or_none()
|
||
self.assertIsNotNone(recipe_update_task)
|
||
|
||
delete = self.client.delete(f"/api/recipes/{recipe_id}")
|
||
self.assertEqual(delete.status_code, 200)
|
||
self.assertTrue(delete.get_json().get("success"))
|
||
|
||
gone = self.client.get(f"/api/recipes/{recipe_id}")
|
||
self.assertEqual(gone.status_code, 404)
|
||
|
||
def test_get_recipe_returns_unloading_link_broken_after_update(self) -> None:
|
||
comp_id = self._create_component()
|
||
create = self.client.post(
|
||
"/api/recipes",
|
||
json={
|
||
"name": "Связь выгрузки",
|
||
"headsPerTrip": 4,
|
||
"mixingTime": 2,
|
||
"target_component_id": comp_id,
|
||
"ingredients": [{"component_id": comp_id, "amount": 1}],
|
||
},
|
||
)
|
||
self.assertEqual(create.status_code, 201)
|
||
recipe_id = create.get_json()["id"]
|
||
|
||
update = self.client.put(
|
||
f"/api/recipes/{recipe_id}",
|
||
json={
|
||
"name": "Связь выгрузки",
|
||
"heads_count": 4,
|
||
"mixing_time": 2,
|
||
"trip_percent": 100.0,
|
||
"target_component_id": comp_id,
|
||
"dry_matter_locked": False,
|
||
"unloading_link_broken": True,
|
||
"ingredients": [
|
||
{
|
||
"component_id": comp_id,
|
||
"weight_per_head": 1.0,
|
||
"amount": 1,
|
||
"dry_matter": 50.0,
|
||
"order": 1,
|
||
}
|
||
],
|
||
"unloading_groups": [
|
||
{
|
||
"name": "Г1",
|
||
"distribution_type": "percent",
|
||
"value": 100.0,
|
||
"weight": 1.0,
|
||
"order": 1,
|
||
}
|
||
],
|
||
},
|
||
)
|
||
self.assertEqual(update.status_code, 200)
|
||
|
||
row = db.session.get(Recipe, recipe_id)
|
||
self.assertIsNotNone(row)
|
||
self.assertTrue(row.unloading_link_broken)
|
||
|
||
get_after = self.client.get(f"/api/recipes/{recipe_id}")
|
||
self.assertEqual(get_after.status_code, 200)
|
||
payload = get_after.get_json()
|
||
self.assertTrue(payload.get("unloading_link_broken"))
|
||
self.assertTrue(payload.get("unloadingLinkBroken"))
|
||
|
||
def test_get_recipe_serializes_unloading_groups_for_copy(self) -> None:
|
||
comp_id = self._create_component()
|
||
create = self.client.post(
|
||
"/api/recipes",
|
||
json={
|
||
"name": "Копия групп",
|
||
"headsPerTrip": 10,
|
||
"mixingTime": 2,
|
||
"target_component_id": comp_id,
|
||
"ingredients": [{"component_id": comp_id, "amount": 1}],
|
||
"unloadingGroups": [
|
||
{
|
||
"name": "Г1",
|
||
"distributionType": "heads",
|
||
"value": 5,
|
||
"weight": 100,
|
||
"order": 1,
|
||
}
|
||
],
|
||
},
|
||
)
|
||
self.assertEqual(create.status_code, 201)
|
||
recipe_id = create.get_json()["id"]
|
||
|
||
payload = self.client.get(f"/api/recipes/{recipe_id}").get_json()
|
||
self.assertIn("unloading_groups", payload)
|
||
self.assertEqual(len(payload["unloading_groups"]), 1)
|
||
grp = payload["unloading_groups"][0]
|
||
self.assertEqual(grp["distribution_type"], "heads")
|
||
self.assertEqual(grp["name"], "Г1")
|
||
self.assertIn("order", payload["ingredients"][0])
|
||
|
||
def test_detach_from_period_makes_recipe_visible_on_mill(self) -> None:
|
||
mill_id = self._create_mill()
|
||
disp = FeedDispenser(
|
||
id="d-detach", name="D", farm="F", operator="O", type="dispenser"
|
||
)
|
||
period = FeedingPeriod(id="p-detach", name="P", dispenser_id=disp.id)
|
||
recipe = Recipe(id="r-detach", name="R", heads_per_trip=1, mixing_time=1)
|
||
db.session.add_all([disp, period, recipe])
|
||
db.session.flush()
|
||
db.session.add(
|
||
PeriodRecipe(
|
||
period_id=period.id,
|
||
recipe_id=recipe.id,
|
||
order=1,
|
||
created_at=datetime.now(),
|
||
created_by="system",
|
||
updated_by="system",
|
||
)
|
||
)
|
||
db.session.commit()
|
||
|
||
before = self.client.get(f"/api/feed_dispensers/{mill_id}/recipes")
|
||
self.assertNotIn("r-detach", [x["id"] for x in before.get_json()])
|
||
|
||
detach = self.client.delete(
|
||
f"/api/feed_dispensers/d-detach/periods/p-detach/recipes/r-detach"
|
||
)
|
||
self.assertEqual(detach.status_code, 200)
|
||
|
||
after = self.client.get(f"/api/feed_dispensers/{mill_id}/recipes")
|
||
self.assertIn("r-detach", [x["id"] for x in after.get_json()])
|
||
|
||
def test_transfer_to_mill_rejected(self) -> None:
|
||
mill_id = self._create_mill()
|
||
disp = FeedDispenser(
|
||
id="d-from-xfer", name="From", farm="F", operator="O", type="dispenser"
|
||
)
|
||
p_from = FeedingPeriod(id="p-from-xfer", name="FromP", dispenser_id=disp.id)
|
||
p_to = FeedingPeriod(id="p-to-xfer", name="ToP", dispenser_id=mill_id)
|
||
recipe = Recipe(id="r-xfer-mill", name="Move", heads_per_trip=1, mixing_time=1)
|
||
db.session.add_all([disp, p_from, p_to, recipe])
|
||
db.session.flush()
|
||
db.session.add(
|
||
PeriodRecipe(
|
||
period_id=p_from.id,
|
||
recipe_id=recipe.id,
|
||
order=1,
|
||
created_at=datetime.now(),
|
||
created_by="system",
|
||
updated_by="system",
|
||
)
|
||
)
|
||
db.session.commit()
|
||
|
||
resp = self.client.post(
|
||
f"/api/feed_dispensers/{mill_id}/periods/p-to-xfer/recipes/r-xfer-mill/transfer",
|
||
json={
|
||
"from_dispenser_id": "d-from-xfer",
|
||
"from_period_id": "p-from-xfer",
|
||
"to_index": 0,
|
||
},
|
||
)
|
||
self.assertEqual(resp.status_code, 400)
|
||
self.assertIn("кормоцех", (resp.get_json().get("message") or "").lower())
|
||
|
||
def test_mill_recipe_update_deleted_ingredient_ids_syncs_delete(self) -> None:
|
||
comp1 = self._create_component("К1")
|
||
comp2 = self._create_component("К2")
|
||
create = self.client.post(
|
||
"/api/recipes",
|
||
json={
|
||
"name": "Два компонента",
|
||
"heads_count": 5,
|
||
"mixing_time": 3,
|
||
"trip_percent": 100.0,
|
||
"target_component_id": comp1,
|
||
"dry_matter_locked": False,
|
||
"unloading_link_broken": False,
|
||
"ingredients": [
|
||
{
|
||
"component_id": comp1,
|
||
"weight_per_head": 1.0,
|
||
"amount": 5,
|
||
"dry_matter": 50,
|
||
"order": 1,
|
||
},
|
||
{
|
||
"component_id": comp2,
|
||
"weight_per_head": 2.0,
|
||
"amount": 10,
|
||
"dry_matter": 60,
|
||
"order": 2,
|
||
},
|
||
],
|
||
"unloading_groups": [
|
||
{
|
||
"name": "Г1",
|
||
"distribution_type": "percent",
|
||
"value": 100.0,
|
||
"weight": 15.0,
|
||
"order": 1,
|
||
}
|
||
],
|
||
},
|
||
)
|
||
self.assertEqual(create.status_code, 201)
|
||
recipe_id = create.get_json()["id"]
|
||
ings = db.session.execute(
|
||
select(Ingredient).where(
|
||
Ingredient.recipe_id == recipe_id, Ingredient.is_deleted.is_(False)
|
||
)
|
||
).scalars().all()
|
||
self.assertEqual(len(ings), 2)
|
||
remove_id = sorted(ings, key=lambda x: x.order)[0].id
|
||
keep = sorted(ings, key=lambda x: x.order)[1]
|
||
|
||
update = self.client.put(
|
||
f"/api/recipes/{recipe_id}",
|
||
json={
|
||
"name": "Один компонент",
|
||
"heads_count": 5,
|
||
"mixing_time": 3,
|
||
"trip_percent": 100.0,
|
||
"target_component_id": comp2,
|
||
"dry_matter_locked": False,
|
||
"unloading_link_broken": False,
|
||
"deleted_ingredient_ids": [remove_id],
|
||
"ingredients": [
|
||
{
|
||
"id": keep.id,
|
||
"component_id": comp2,
|
||
"weight_per_head": 2.0,
|
||
"amount": 10,
|
||
"dry_matter": 60,
|
||
"order": 1,
|
||
}
|
||
],
|
||
"unloading_groups": [
|
||
{
|
||
"name": "Г1",
|
||
"distribution_type": "percent",
|
||
"value": 100.0,
|
||
"weight": 10.0,
|
||
"order": 1,
|
||
}
|
||
],
|
||
},
|
||
)
|
||
self.assertEqual(update.status_code, 200, update.get_data(as_text=True))
|
||
|
||
active = db.session.execute(
|
||
select(Ingredient).where(
|
||
Ingredient.recipe_id == recipe_id, Ingredient.is_deleted.is_(False)
|
||
)
|
||
).scalars().all()
|
||
self.assertEqual(len(active), 1)
|
||
self.assertEqual(active[0].id, keep.id)
|
||
|
||
delete_task = db.session.execute(
|
||
select(SyncQueue).where(
|
||
SyncQueue.table_name == "ingredient",
|
||
SyncQueue.record_id == remove_id,
|
||
SyncQueue.action == "delete",
|
||
)
|
||
).scalar_one_or_none()
|
||
self.assertIsNotNone(delete_task)
|
||
|
||
def test_mill_recipe_update_change_component_syncs_ingredient_update(self) -> None:
|
||
comp1 = self._create_component("К1")
|
||
comp2 = self._create_component("К2")
|
||
create = self.client.post(
|
||
"/api/recipes",
|
||
json={
|
||
"name": "Смена компонента",
|
||
"heads_count": 5,
|
||
"mixing_time": 3,
|
||
"trip_percent": 100.0,
|
||
"target_component_id": comp1,
|
||
"dry_matter_locked": False,
|
||
"unloading_link_broken": False,
|
||
"ingredients": [
|
||
{
|
||
"component_id": comp1,
|
||
"weight_per_head": 1.0,
|
||
"amount": 5,
|
||
"dry_matter": 50,
|
||
"order": 1,
|
||
}
|
||
],
|
||
"unloading_groups": [],
|
||
},
|
||
)
|
||
self.assertEqual(create.status_code, 201)
|
||
recipe_id = create.get_json()["id"]
|
||
ing = db.session.execute(
|
||
select(Ingredient).where(
|
||
Ingredient.recipe_id == recipe_id, Ingredient.is_deleted.is_(False)
|
||
)
|
||
).scalar_one()
|
||
|
||
update = self.client.put(
|
||
f"/api/recipes/{recipe_id}",
|
||
json={
|
||
"name": "Смена компонента",
|
||
"heads_count": 5,
|
||
"mixing_time": 3,
|
||
"trip_percent": 100.0,
|
||
"target_component_id": comp2,
|
||
"dry_matter_locked": False,
|
||
"unloading_link_broken": False,
|
||
"ingredients": [
|
||
{
|
||
"id": ing.id,
|
||
"component_id": comp2,
|
||
"weight_per_head": 1.0,
|
||
"amount": 5,
|
||
"dry_matter": 50,
|
||
"order": 1,
|
||
}
|
||
],
|
||
"unloading_groups": [],
|
||
},
|
||
)
|
||
self.assertEqual(update.status_code, 200)
|
||
self.assertEqual(str(db.session.get(Ingredient, ing.id).component_id), comp2)
|
||
self.assertIsNotNone(
|
||
db.session.execute(
|
||
select(SyncQueue).where(
|
||
SyncQueue.table_name == "ingredient",
|
||
SyncQueue.record_id == ing.id,
|
||
SyncQueue.action == "update",
|
||
)
|
||
).scalar_one_or_none()
|
||
)
|
||
|
||
def test_mill_recipe_update_omit_ingredient_syncs_delete(self) -> None:
|
||
comp1 = self._create_component("К1")
|
||
comp2 = self._create_component("К2")
|
||
create = self.client.post(
|
||
"/api/recipes",
|
||
json={
|
||
"name": "Два",
|
||
"heads_count": 5,
|
||
"mixing_time": 3,
|
||
"trip_percent": 100.0,
|
||
"target_component_id": comp1,
|
||
"dry_matter_locked": False,
|
||
"unloading_link_broken": False,
|
||
"ingredients": [
|
||
{"component_id": comp1, "weight_per_head": 1.0, "amount": 5, "dry_matter": 50, "order": 1},
|
||
{"component_id": comp2, "weight_per_head": 2.0, "amount": 10, "dry_matter": 60, "order": 2},
|
||
],
|
||
"unloading_groups": [],
|
||
},
|
||
)
|
||
recipe_id = create.get_json()["id"]
|
||
ings = sorted(
|
||
db.session.execute(
|
||
select(Ingredient).where(
|
||
Ingredient.recipe_id == recipe_id, Ingredient.is_deleted.is_(False)
|
||
)
|
||
).scalars().all(),
|
||
key=lambda x: x.order,
|
||
)
|
||
keep = ings[1]
|
||
|
||
update = self.client.put(
|
||
f"/api/recipes/{recipe_id}",
|
||
json={
|
||
"name": "Один",
|
||
"heads_count": 5,
|
||
"mixing_time": 3,
|
||
"trip_percent": 100.0,
|
||
"target_component_id": comp2,
|
||
"dry_matter_locked": False,
|
||
"unloading_link_broken": False,
|
||
"ingredients": [
|
||
{
|
||
"id": keep.id,
|
||
"component_id": comp2,
|
||
"weight_per_head": 2.0,
|
||
"amount": 10,
|
||
"dry_matter": 60,
|
||
"order": 1,
|
||
}
|
||
],
|
||
"unloading_groups": [],
|
||
},
|
||
)
|
||
self.assertEqual(update.status_code, 200)
|
||
self.assertTrue(db.session.get(Ingredient, ings[0].id).is_deleted)
|
||
self.assertIsNotNone(
|
||
db.session.execute(
|
||
select(SyncQueue).where(
|
||
SyncQueue.table_name == "ingredient",
|
||
SyncQueue.record_id == ings[0].id,
|
||
SyncQueue.action == "delete",
|
||
)
|
||
).scalar_one_or_none()
|
||
)
|
||
|
||
def test_mill_recipe_update_deleted_group_ids_syncs_delete(self) -> None:
|
||
comp = self._create_component()
|
||
create = self.client.post(
|
||
"/api/recipes",
|
||
json={
|
||
"name": "Группы",
|
||
"heads_count": 5,
|
||
"mixing_time": 3,
|
||
"trip_percent": 100.0,
|
||
"target_component_id": comp,
|
||
"dry_matter_locked": False,
|
||
"unloading_link_broken": False,
|
||
"ingredients": [
|
||
{"component_id": comp, "weight_per_head": 1.0, "amount": 5, "dry_matter": 50, "order": 1},
|
||
],
|
||
"unloading_groups": [
|
||
{"name": "Г1", "distribution_type": "percent", "value": 50.0, "weight": 5.0, "order": 1},
|
||
{"name": "Г2", "distribution_type": "percent", "value": 50.0, "weight": 5.0, "order": 2},
|
||
],
|
||
},
|
||
)
|
||
recipe_id = create.get_json()["id"]
|
||
groups = sorted(
|
||
db.session.execute(
|
||
select(UnloadingGroup).where(
|
||
UnloadingGroup.recipe_id == recipe_id, UnloadingGroup.is_deleted.is_(False)
|
||
)
|
||
).scalars().all(),
|
||
key=lambda g: g.order,
|
||
)
|
||
remove_id, keep = groups[0].id, groups[1]
|
||
ing = db.session.execute(
|
||
select(Ingredient).where(
|
||
Ingredient.recipe_id == recipe_id, Ingredient.is_deleted.is_(False)
|
||
)
|
||
).scalar_one()
|
||
|
||
update = self.client.put(
|
||
f"/api/recipes/{recipe_id}",
|
||
json={
|
||
"name": "Группы",
|
||
"heads_count": 5,
|
||
"mixing_time": 3,
|
||
"trip_percent": 100.0,
|
||
"target_component_id": comp,
|
||
"dry_matter_locked": False,
|
||
"unloading_link_broken": False,
|
||
"deleted_unloading_group_ids": [remove_id],
|
||
"ingredients": [
|
||
{
|
||
"id": ing.id,
|
||
"component_id": comp,
|
||
"weight_per_head": 1.0,
|
||
"amount": 5,
|
||
"dry_matter": 50,
|
||
"order": 1,
|
||
}
|
||
],
|
||
"unloading_groups": [
|
||
{
|
||
"id": keep.id,
|
||
"name": keep.name,
|
||
"distribution_type": keep.distribution_type,
|
||
"value": keep.value,
|
||
"weight": keep.weight,
|
||
"order": 1,
|
||
}
|
||
],
|
||
},
|
||
)
|
||
self.assertEqual(update.status_code, 200)
|
||
self.assertTrue(db.session.get(UnloadingGroup, remove_id).is_deleted)
|
||
self.assertIsNotNone(
|
||
db.session.execute(
|
||
select(SyncQueue).where(
|
||
SyncQueue.table_name == "unloading_group",
|
||
SyncQueue.record_id == remove_id,
|
||
SyncQueue.action == "delete",
|
||
)
|
||
).scalar_one_or_none()
|
||
)
|
||
|
||
def test_mill_recipe_delete_cascades_children_sync(self) -> None:
|
||
comp = self._create_component()
|
||
create = self.client.post(
|
||
"/api/recipes",
|
||
json={
|
||
"name": "Каскад",
|
||
"heads_count": 3,
|
||
"mixing_time": 2,
|
||
"trip_percent": 100.0,
|
||
"target_component_id": comp,
|
||
"dry_matter_locked": False,
|
||
"unloading_link_broken": False,
|
||
"ingredients": [
|
||
{"component_id": comp, "weight_per_head": 1.0, "amount": 3, "dry_matter": 50, "order": 1},
|
||
],
|
||
"unloading_groups": [
|
||
{"name": "Г1", "distribution_type": "percent", "value": 100.0, "weight": 3.0, "order": 1},
|
||
],
|
||
},
|
||
)
|
||
recipe_id = create.get_json()["id"]
|
||
ing = db.session.execute(
|
||
select(Ingredient).where(Ingredient.recipe_id == recipe_id)
|
||
).scalar_one()
|
||
grp = db.session.execute(
|
||
select(UnloadingGroup).where(UnloadingGroup.recipe_id == recipe_id)
|
||
).scalar_one()
|
||
|
||
delete = self.client.delete(f"/api/recipes/{recipe_id}")
|
||
self.assertEqual(delete.status_code, 200)
|
||
|
||
self.assertTrue(db.session.get(Recipe, recipe_id).is_deleted)
|
||
self.assertTrue(db.session.get(Ingredient, ing.id).is_deleted)
|
||
self.assertTrue(db.session.get(UnloadingGroup, grp.id).is_deleted)
|
||
self.assertIsNotNone(
|
||
db.session.execute(
|
||
select(SyncQueue).where(
|
||
SyncQueue.table_name == "recipe",
|
||
SyncQueue.record_id == recipe_id,
|
||
SyncQueue.action == "delete",
|
||
)
|
||
).scalar_one_or_none()
|
||
)
|
||
self.assertGreaterEqual(
|
||
db.session.scalar(
|
||
select(func.count()).select_from(SyncQueue).where(
|
||
SyncQueue.table_name == "ingredient",
|
||
SyncQueue.record_id == ing.id,
|
||
SyncQueue.action == "update",
|
||
)
|
||
),
|
||
1,
|
||
)
|
||
|
||
def test_recipe_create_enqueues_sync_without_period_recipes(self) -> None:
|
||
comp_id = self._create_component()
|
||
before = db.session.scalar(select(func.count()).select_from(SyncQueue)) or 0
|
||
|
||
create = self.client.post(
|
||
"/api/recipes",
|
||
json={
|
||
"name": "Sync mill recipe",
|
||
"headsPerTrip": 3,
|
||
"mixingTime": 1,
|
||
"ingredients": [{"component_id": comp_id, "amount": 1}],
|
||
},
|
||
)
|
||
self.assertEqual(create.status_code, 201)
|
||
recipe_id = create.get_json()["id"]
|
||
|
||
after = db.session.scalar(select(func.count()).select_from(SyncQueue)) or 0
|
||
self.assertGreater(after, before)
|
||
|
||
recipe_tasks = db.session.execute(
|
||
select(SyncQueue).where(
|
||
SyncQueue.table_name == "recipe",
|
||
SyncQueue.record_id == recipe_id,
|
||
)
|
||
).scalars().all()
|
||
self.assertTrue(recipe_tasks)
|
||
|
||
period_tasks = db.session.execute(
|
||
select(SyncQueue).where(
|
||
SyncQueue.table_name == "period_recipes",
|
||
SyncQueue.record_id.like(f"%{recipe_id}%"),
|
||
)
|
||
).scalars().all()
|
||
self.assertEqual(period_tasks, [])
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|