187 lines
6.8 KiB
Python
187 lines
6.8 KiB
Python
"""API /api/analytics."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import unittest
|
|
from datetime import datetime
|
|
|
|
from app import create_app, db
|
|
from app.models import Component, LoadingReport, LoadingReportComponent, Recipe
|
|
from app.services.setup_state import mark_setup_complete
|
|
from tests.helpers.zootech_test_helpers import ZootechTestConfig
|
|
|
|
|
|
class AnalyticsApiTests(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.app = create_app(ZootechTestConfig)
|
|
self.client = self.app.test_client()
|
|
self.ctx = self.app.app_context()
|
|
self.ctx.push()
|
|
db.create_all()
|
|
mark_setup_complete(self.app)
|
|
self.client.post(
|
|
"/api/auth/login",
|
|
json={"login": "zootech-test-admin", "password": "zootech-test-secret"},
|
|
)
|
|
db.session.add(
|
|
Component(
|
|
id="api-an-c1",
|
|
name="Силос",
|
|
type="forage",
|
|
dry_matter=30.0,
|
|
protein=0.0,
|
|
energy=0.0,
|
|
price=3.0,
|
|
)
|
|
)
|
|
db.session.add(
|
|
Recipe(id="api-an-r1", name="Рейс", heads_per_trip=1, content_hash="")
|
|
)
|
|
report = LoadingReport(
|
|
id="api-an-lr",
|
|
recipe_id="api-an-r1",
|
|
recipe_name="Рейс",
|
|
start_time=datetime.now(),
|
|
target_mixing_time=60,
|
|
actual_mixing_time=60,
|
|
total_weight=100.0,
|
|
created_by="system",
|
|
updated_by="system",
|
|
)
|
|
db.session.add(report)
|
|
db.session.flush()
|
|
db.session.add(
|
|
LoadingReportComponent(
|
|
report_id=report.id,
|
|
component_id="api-an-c1",
|
|
component_name="Силос",
|
|
target_weight=100.0,
|
|
actual_weight=150.0,
|
|
overload=50.0,
|
|
loading_order=1,
|
|
created_by="system",
|
|
updated_by="system",
|
|
)
|
|
)
|
|
db.session.commit()
|
|
self.today = datetime.now().strftime("%Y-%m-%d")
|
|
|
|
def tearDown(self) -> None:
|
|
db.session.remove()
|
|
db.drop_all()
|
|
self.ctx.pop()
|
|
|
|
def test_finance_endpoint(self) -> None:
|
|
resp = self.client.get(
|
|
f"/api/analytics/finance?date_from={self.today}&date_to={self.today}"
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
body = resp.get_json()
|
|
self.assertEqual(body["overloadRub"], 150.0)
|
|
self.assertIn("dominantIssue", body)
|
|
self.assertEqual(body["dominantIssue"], "overload")
|
|
|
|
def test_plan_fact_endpoint(self) -> None:
|
|
resp = self.client.get(
|
|
f"/api/analytics/plan-fact?date_from={self.today}&date_to={self.today}"
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
body = resp.get_json()
|
|
self.assertEqual(body["reportCount"], 1)
|
|
self.assertEqual(body["items"][0]["loadingReportId"], "api-an-lr")
|
|
|
|
def test_stock_forecast_endpoint(self) -> None:
|
|
resp = self.client.get("/api/analytics/stock-forecast")
|
|
self.assertEqual(resp.status_code, 200)
|
|
body = resp.get_json()
|
|
self.assertIn("items", body)
|
|
self.assertIn("alertBanner", body)
|
|
|
|
def test_finance_export_three_sheets(self) -> None:
|
|
resp = self.client.get(
|
|
f"/api/analytics/finance/export?date_from={self.today}&date_to={self.today}"
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
from openpyxl import load_workbook
|
|
import io
|
|
|
|
wb = load_workbook(io.BytesIO(resp.data))
|
|
self.assertEqual(set(wb.sheetnames), {"Итоги", "Сравнение", "Подробно"})
|
|
|
|
def test_analytics_export_xlsx(self) -> None:
|
|
resp = self.client.get(
|
|
f"/api/analytics/export?format=xlsx&date_from={self.today}&date_to={self.today}"
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertIn("spreadsheetml", resp.content_type or resp.mimetype or "")
|
|
|
|
def test_analytics_export_comparison_sheet_layout(self) -> None:
|
|
resp = self.client.get(
|
|
f"/api/analytics/export?format=xlsx§ion=comparison&date_from={self.today}&date_to={self.today}"
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
from openpyxl import load_workbook
|
|
from openpyxl.utils import get_column_letter
|
|
import io
|
|
|
|
wb = load_workbook(io.BytesIO(resp.data))
|
|
self.assertEqual(wb.sheetnames, ["Сравнение"])
|
|
ws = wb["Сравнение"]
|
|
headers = [cell.value for cell in ws[1]]
|
|
self.assertIn("Разница План/Факт, кг", headers)
|
|
self.assertNotIn("Факс", "".join(str(h) for h in headers if h))
|
|
|
|
for header in (
|
|
"По рецепту (База), кг",
|
|
"На сегодня (План), кг",
|
|
"Разница База/Факт, кг",
|
|
"Разница План/Факт, кг",
|
|
):
|
|
col_idx = headers.index(header) + 1
|
|
width = ws.column_dimensions[get_column_letter(col_idx)].width
|
|
self.assertGreaterEqual(width, 18, msg=f"Column {header} too narrow: {width}")
|
|
|
|
self.assertGreater(len(ws.conditional_formatting), 0)
|
|
|
|
def test_analytics_export_pdf(self) -> None:
|
|
resp = self.client.get(
|
|
f"/api/analytics/export?format=pdf&date_from={self.today}&date_to={self.today}"
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
self.assertEqual(resp.content_type, "application/pdf")
|
|
self.assertTrue(resp.data.startswith(b"%PDF"))
|
|
|
|
def test_analytics_export_summary_only_xlsx(self) -> None:
|
|
resp = self.client.get(
|
|
f"/api/analytics/export?format=xlsx§ion=summary&date_from={self.today}&date_to={self.today}"
|
|
)
|
|
self.assertEqual(resp.status_code, 200)
|
|
from openpyxl import load_workbook
|
|
import io
|
|
|
|
wb = load_workbook(io.BytesIO(resp.data))
|
|
self.assertEqual(wb.sheetnames, ["Итоги"])
|
|
ws = wb["Итоги"]
|
|
header_row = next(
|
|
row[0].row
|
|
for row in ws.iter_rows(min_row=1, max_col=1)
|
|
if row[0].value == "Ферма"
|
|
)
|
|
header_values = [cell.value for cell in ws[header_row]]
|
|
self.assertIn("Ферма", header_values)
|
|
self.assertIn("Перерасход, ₽", header_values)
|
|
|
|
net_row = next(
|
|
row[0].row
|
|
for row in ws.iter_rows(min_row=1, max_col=1)
|
|
if row[0].value == "Итого по деньгам, ₽"
|
|
)
|
|
net_value = ws.cell(row=net_row, column=2).value
|
|
self.assertGreaterEqual(float(net_value or 0), 0)
|
|
hint = ws.cell(row=net_row + 1, column=1).value
|
|
self.assertIn("перерасход", str(hint).lower())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|