@@ -0,0 +1,186 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user