food-market/tests/test_stats.py

499 lines
22 KiB
Python
Raw Permalink Normal View History

Раздел статистики Шесть разрезов: периоды (заданные вручную, то есть партии), недели, месяцы, годы, товары и люди. У каждого график и таблица с итоговой строкой. Главное решение — две базы расчёта одновременно. Смешивать «когда продал» и «когда получил деньги» нельзя: ответы расходятся, как только появляется хоть один долг. Выручка, наценка, съеденное и чаевые считаются по дате продажи, «Собрано» — по дате платежа, «Закуплено» и «Отдано пекарне» — по своим датам. У разреза «Периоды» основание третье, привязка к партии, потому что вопрос там другой: покрыл ли я её к сроку. Количество нигде не сводится в одно число — только по единицам, «1,5 л · 4 шт». Сумма литров с килограммами выглядит как настоящая цифра, но смысла не имеет; ровно эту ошибку пришлось чинить и в разрезе по партиям. График на QPainter: QtCharts исключён из сборки, он тянет десятки мегабайт ради одной диаграммы. Убыток рисуется вниз от нулевой линии. Больше 24 корзин на график не помещается, и про срез написано над ним — молча показанная часть выглядела бы как всё. Тесты экранов больше не адресуют разделы по номеру в меню: новый раздел ломал половину проверок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 06:33:11 +07:00
"""Сводные цифры по периодам, товарам и людям."""
from datetime import date, timedelta
from decimal import Decimal
import pytest
from app import journal, ledger, stats
from app.models import KIND_FRIEND, KIND_RETAIL, KIND_SELF
def line(product, qty, price):
return {"product_id": product.id, "qty": qty, "unit_price": price}
def buy(product, qty, cost):
return {"product_id": product.id, "qty": qty, "unit_cost": cost}
def report_for(vault, today=None):
return ledger.build(vault.doc, today or date.today())
# --- границы периодов -----------------------------------------------------
def test_week_bounds_run_monday_to_sunday():
# 19 августа 2026 — среда.
start, end = stats.week_bounds(date(2026, 8, 19))
assert (start, end) == (date(2026, 8, 17), date(2026, 8, 23))
assert start.isoweekday() == 1 and end.isoweekday() == 7
def test_month_bounds():
assert stats.month_bounds(date(2026, 2, 14)) == (date(2026, 2, 1), date(2026, 2, 28))
assert stats.month_bounds(date(2024, 2, 14)) == (date(2024, 2, 1), date(2024, 2, 29))
assert stats.month_bounds(date(2026, 12, 5)) == (date(2026, 12, 1), date(2026, 12, 31))
def test_year_bounds():
assert stats.year_bounds(date(2026, 7, 7)) == (date(2026, 1, 1), date(2026, 12, 31))
def test_labels_are_readable():
_, week, _, _ = stats.bucket_of(date(2026, 8, 19), stats.GRAIN_WEEK)
assert week == "Неделя 34 · 1723 августа 2026"
_, month, _, _ = stats.bucket_of(date(2026, 8, 19), stats.GRAIN_MONTH)
assert month == "Август 2026"
_, year, _, _ = stats.bucket_of(date(2026, 8, 19), stats.GRAIN_YEAR)
assert year == "2026 год"
def test_week_label_spanning_two_months():
_, label, _, _ = stats.bucket_of(date(2026, 10, 1), stats.GRAIN_WEEK)
assert label == "Неделя 40 · 28 сентября 4 октября 2026"
def test_week_label_spanning_two_years():
_, label, _, _ = stats.bucket_of(date(2026, 12, 31), stats.GRAIN_WEEK)
assert "2026" in label and "2027" in label
def test_keys_sort_chronologically():
keys = [
stats.bucket_of(date(2026, 1, 5), stats.GRAIN_WEEK)[0],
stats.bucket_of(date(2026, 10, 5), stats.GRAIN_WEEK)[0],
]
assert keys == sorted(keys) # W02 против W41, а не «10» перед «2»
def test_unknown_grain_is_rejected():
with pytest.raises(ValueError):
stats.bucket_of(date.today(), "квартал")
# --- корзины --------------------------------------------------------------
@pytest.fixture
def three_months(vault, buns):
"""Три месяца торговли с долгом, чаевыми и съеденным."""
june, july, august = date(2026, 6, 10), date(2026, 7, 10), date(2026, 8, 10)
journal.create_batch(vault, june, june + timedelta(days=20), [buy(buns["мак"], 100, 20)])
journal.create_sale(vault, june, KIND_RETAIL, [line(buns["мак"], 10, 35)])
journal.create_sale(vault, july, KIND_RETAIL, [line(buns["мак"], 20, 35)], tip=50)
journal.create_sale(vault, july, KIND_SELF, [line(buns["мак"], 2, 0)])
journal.create_sale(vault, august, KIND_FRIEND, [line(buns["мак"], 5, 20)])
return vault
def buckets_by_key(vault, grain, today=None):
result = stats.build_buckets(vault.doc, report_for(vault, today), grain)
return {b.key: b for b in result}
def test_months_are_split_correctly(three_months):
months = buckets_by_key(three_months, stats.GRAIN_MONTH, date(2026, 8, 31))
assert list(months) == ["2026-06", "2026-07", "2026-08"]
june = months["2026-06"]
assert june.revenue == Decimal("350.00") # 10 × 35
assert june.cogs == Decimal("200.00") # 10 × 20
assert june.margin == Decimal("150.00")
assert june.purchased == Decimal("2000.00") # партия взята в июне
def test_tips_and_consumption_land_in_the_right_month(three_months):
july = buckets_by_key(three_months, stats.GRAIN_MONTH, date(2026, 8, 31))["2026-07"]
assert july.tips == Decimal("50.00")
assert july.consumed_cost == Decimal("40.00") # 2 × 20
assert july.margin == Decimal("300.00") # 20 × 15
assert july.profit == Decimal("310.00") # 300 + 50 40
def test_friend_price_gives_no_margin(three_months):
august = buckets_by_key(three_months, stats.GRAIN_MONTH, date(2026, 8, 31))["2026-08"]
assert august.revenue == Decimal("100.00")
assert august.margin == 0
assert august.profit == 0
def test_empty_months_are_skipped(vault, buns):
"""В марте не торговали — выдуманного нуля в таблице быть не должно."""
journal.create_batch(vault, date(2026, 1, 5), date(2026, 1, 20), [buy(buns["мак"], 10, 20)])
journal.create_sale(vault, date(2026, 4, 5), KIND_RETAIL, [line(buns["мак"], 1, 35)])
keys = list(buckets_by_key(vault, stats.GRAIN_MONTH, date(2026, 5, 1)))
assert keys == ["2026-01", "2026-04"]
def test_years_roll_up_months(three_months, buns):
"""Год обязан быть в точности суммой своих месяцев."""
journal.create_sale(
three_months, date(2025, 12, 20), KIND_RETAIL, [line(buns["мак"], 1, 35)]
)
report = report_for(three_months, date(2026, 12, 31))
years = {b.key: b for b in stats.build_buckets(three_months.doc, report, stats.GRAIN_YEAR)}
months = stats.build_buckets(three_months.doc, report, stats.GRAIN_MONTH)
assert list(years) == ["2025", "2026"]
of_2026 = [b for b in months if b.start.year == 2026]
assert years["2026"].revenue == sum(b.revenue for b in of_2026)
assert years["2026"].profit == sum(b.profit for b in of_2026)
assert years["2026"].sales_count == sum(b.sales_count for b in of_2026)
# 10×35 в июне + 20×35 в июле + 5×20 другу в августе
assert years["2026"].revenue == Decimal("1150.00")
def test_weeks_split_by_iso_week(vault, buns):
monday, sunday = date(2026, 8, 17), date(2026, 8, 23)
journal.create_batch(vault, monday, sunday, [buy(buns["мак"], 50, 20)])
journal.create_sale(vault, monday, KIND_RETAIL, [line(buns["мак"], 1, 35)])
journal.create_sale(vault, sunday, KIND_RETAIL, [line(buns["мак"], 1, 35)])
journal.create_sale(vault, sunday + timedelta(days=1), KIND_RETAIL, [line(buns["мак"], 1, 35)])
weeks = buckets_by_key(vault, stats.GRAIN_WEEK, date(2026, 9, 1))
assert weeks["2026-W34"].sales_count == 2
assert weeks["2026-W35"].sales_count == 1
# --- касса против начислений ----------------------------------------------
def test_money_lands_in_the_month_it_arrived(vault, buns):
"""Долг за август, возвращённый в сентябре, — сентябрьские деньги."""
august, september = date(2026, 8, 10), date(2026, 9, 5)
journal.create_batch(vault, august, august + timedelta(days=20), [buy(buns["мак"], 50, 20)])
sale = journal.create_sale(
vault, august, KIND_RETAIL, [line(buns["мак"], 10, 35)], paid_amount=0
)
journal.add_sale_payment(vault, sale.id, 350, september)
months = buckets_by_key(vault, stats.GRAIN_MONTH, date(2026, 9, 30))
assert months["2026-08"].revenue == Decimal("350.00") # наторговал в августе
assert months["2026-08"].collected == 0 # но денег в августе не видел
assert months["2026-09"].collected == Decimal("350.00")
assert months["2026-09"].revenue == 0
def test_bakery_payments_land_by_their_own_date(vault, buns):
august = date(2026, 8, 10)
batch = journal.create_batch(
vault, august, august + timedelta(days=20), [buy(buns["мак"], 50, 20)]
)
journal.add_bakery_payment(vault, 400, date(2026, 9, 3), batch_id=batch.id)
months = buckets_by_key(vault, stats.GRAIN_MONTH, date(2026, 9, 30))
assert months["2026-08"].purchased == Decimal("1000.00")
assert months["2026-08"].paid_to_bakery == 0
assert months["2026-09"].paid_to_bakery == Decimal("400.00")
Пекарня может простить остаток Надо было отдать 5 500, взяли 5 000, полтысячи оставили. Долг закрыт, но денег никто не отдавал. У расчёта с пекарней появился вид: деньги или прощённый остаток. Свалить их в одну кучу нельзя — тогда «Отдано пекарне» в статистике показывало бы суммы, которых не платил. Поэтому «Отдано» — только живые деньги, «Прощено» — отдельная колонка везде: в закупках, в сводке, в статистике по периодам и по времени. В прибыль прощённое идёт целиком: эти деньги предназначались пекарне, а остались у тебя. «Осталось собрать» на ту же сумму уменьшается — собирать под прощённый остаток уже не надо. Простить больше долга нельзя: лишнее не засчитывается и, в отличие от переплаты деньгами, никуда не переливается. В диалоге платежа галочка «Остаток простили — закрыть партию»: вводишь сколько отдал, остаток уходит отдельной записью, партия закрывается сама, потому что её долг становится нулём. Миграция схемы 3→4 помечает всё записанное раньше как живые деньги. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 06:44:19 +07:00
def test_forgiven_remainder_is_not_money_paid(vault, buns):
"""«Отдано пекарне» и «Прощено» — разные колонки, и это принципиально."""
august = date(2026, 8, 10)
batch = journal.create_batch(
vault, august, august + timedelta(days=20), [buy(buns["мак"], 275, 20)]
)
journal.add_bakery_payment(vault, 5000, august, batch_id=batch.id)
journal.write_off_batch(vault, batch.id, 500, august)
month = buckets_by_key(vault, stats.GRAIN_MONTH, date(2026, 8, 31))["2026-08"]
assert month.paid_to_bakery == Decimal("5000.00")
assert month.bakery_discount == Decimal("500.00")
def test_forgiven_remainder_adds_to_period_profit(vault, buns):
august = date(2026, 8, 10)
batch = journal.create_batch(
vault, august, august + timedelta(days=20), [buy(buns["мак"], 10, 20)]
)
journal.create_sale(vault, august, KIND_RETAIL, [line(buns["мак"], 10, 35)])
journal.write_off_batch(vault, batch.id, 50, august)
month = buckets_by_key(vault, stats.GRAIN_MONTH, date(2026, 8, 31))["2026-08"]
assert month.margin == Decimal("150.00")
assert month.profit == Decimal("200.00")
def test_forgiven_remainder_lands_by_its_own_date(vault, buns):
august, september = date(2026, 8, 10), date(2026, 9, 4)
batch = journal.create_batch(
vault, august, august + timedelta(days=20), [buy(buns["мак"], 10, 20)]
)
journal.write_off_batch(vault, batch.id, 200, september)
months = buckets_by_key(vault, stats.GRAIN_MONTH, date(2026, 9, 30))
assert months["2026-08"].bakery_discount == 0
assert months["2026-09"].bakery_discount == Decimal("200.00")
def test_totals_include_the_discount(vault, buns):
august = date(2026, 8, 10)
batch = journal.create_batch(
vault, august, august + timedelta(days=20), [buy(buns["мак"], 10, 20)]
)
journal.write_off_batch(vault, batch.id, 200, august)
buckets = stats.build_buckets(vault.doc, report_for(vault, date(2026, 8, 31)), stats.GRAIN_MONTH)
assert stats.totals(buckets).bakery_discount == Decimal("200.00")
def test_period_profit_counts_the_discount(vault, buns, today, in_two_weeks):
batch = journal.create_batch(vault, today, in_two_weeks, [buy(buns["мак"], 10, 20)])
journal.create_sale(vault, today, KIND_RETAIL, [line(buns["мак"], 4, 35)])
journal.write_off_batch(vault, batch.id, 60, today)
period = stats.build_periods(vault.doc, report_for(vault, today))[0]
assert period.margin == Decimal("60.00")
assert period.profit == Decimal("120.00") # 60 наценки + 60 прощённых
Раздел статистики Шесть разрезов: периоды (заданные вручную, то есть партии), недели, месяцы, годы, товары и люди. У каждого график и таблица с итоговой строкой. Главное решение — две базы расчёта одновременно. Смешивать «когда продал» и «когда получил деньги» нельзя: ответы расходятся, как только появляется хоть один долг. Выручка, наценка, съеденное и чаевые считаются по дате продажи, «Собрано» — по дате платежа, «Закуплено» и «Отдано пекарне» — по своим датам. У разреза «Периоды» основание третье, привязка к партии, потому что вопрос там другой: покрыл ли я её к сроку. Количество нигде не сводится в одно число — только по единицам, «1,5 л · 4 шт». Сумма литров с килограммами выглядит как настоящая цифра, но смысла не имеет; ровно эту ошибку пришлось чинить и в разрезе по партиям. График на QPainter: QtCharts исключён из сборки, он тянет десятки мегабайт ради одной диаграммы. Убыток рисуется вниз от нулевой линии. Больше 24 корзин на график не помещается, и про срез написано над ним — молча показанная часть выглядела бы как всё. Тесты экранов больше не адресуют разделы по номеру в меню: новый раздел ломал половину проверок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 06:33:11 +07:00
def test_tips_count_as_money_at_sale_time(vault, buns):
"""Чаевые дают из рук в руки, своей даты у них нет."""
day = date(2026, 8, 10)
journal.create_batch(vault, day, day + timedelta(days=14), [buy(buns["мак"], 50, 20)])
journal.create_sale(vault, day, KIND_RETAIL, [line(buns["мак"], 2, 35)], paid_amount=0, tip=30)
month = buckets_by_key(vault, stats.GRAIN_MONTH, date(2026, 8, 31))["2026-08"]
assert month.collected == Decimal("30.00")
# --- количества и проценты ------------------------------------------------
def test_quantities_are_not_mixed_across_units(vault, buns, today, in_two_weeks):
juice = journal.create_product(vault, "Сок", unit="л", cost_price=80, retail_price=120)
journal.create_batch(
vault, today, in_two_weeks,
[buy(buns["мак"], 10, 20), {"product_id": juice.id, "qty": 5, "unit_cost": 80}],
)
journal.create_sale(vault, today, KIND_RETAIL, [line(buns["мак"], 3, 35)])
journal.create_sale(vault, today, KIND_RETAIL, [line(juice, "1.5", 120)])
bucket = stats.build_buckets(vault.doc, report_for(vault, today), stats.GRAIN_MONTH)[0]
assert bucket.qty_by_unit == {"шт": Decimal("3.000"), "л": Decimal("1.500")}
assert bucket.qty_text == "1,5 л · 3 шт"
def test_packs_are_counted_in_base_units(vault, today, in_two_weeks):
cookies = journal.create_product(vault, "Печенье", cost_price=20, retail_price=30)
journal.set_packs(vault, cookies.id, [{"name": "пачка", "size": 10, "retail_price": 200}])
journal.create_batch(
vault, today, in_two_weeks,
[{"product_id": cookies.id, "qty": 5, "uom": "пачка", "unit_cost": 200}],
)
journal.create_sale(
vault, today, KIND_RETAIL,
[{"product_id": cookies.id, "qty": 2, "uom": "пачка", "unit_price": 200}],
)
bucket = stats.build_buckets(vault.doc, report_for(vault, today), stats.GRAIN_MONTH)[0]
assert bucket.qty_by_unit == {"пачка": Decimal("20.000")}
def test_margin_percentage(vault, buns, today, in_two_weeks):
journal.create_batch(vault, today, in_two_weeks, [buy(buns["мак"], 10, 20)])
journal.create_sale(vault, today, KIND_RETAIL, [line(buns["мак"], 10, 35)])
bucket = stats.build_buckets(vault.doc, report_for(vault, today), stats.GRAIN_MONTH)[0]
assert bucket.margin_pct == Decimal("42.86") # 150 из 350
def test_margin_percentage_without_revenue_is_zero(vault):
assert stats.Bucket(key="x", label="x", start=date.today(), end=date.today()).margin_pct == 0
# --- итоги ----------------------------------------------------------------
def test_totals_sum_every_bucket(three_months):
buckets = stats.build_buckets(three_months.doc, report_for(three_months, date(2026, 8, 31)), stats.GRAIN_MONTH)
total = stats.totals(buckets)
assert total.revenue == sum(b.revenue for b in buckets)
assert total.profit == sum(b.profit for b in buckets)
assert total.sales_count == 4
assert total.start == buckets[0].start
assert total.end == buckets[-1].end
def test_totals_of_nothing_are_zero(vault):
total = stats.totals([])
assert total.revenue == 0 and total.profit == 0 and total.sales_count == 0
def test_totals_keep_units_apart(vault, buns, today, in_two_weeks):
juice = journal.create_product(vault, "Сок", unit="л", cost_price=80, retail_price=120)
journal.create_batch(
vault, today, in_two_weeks,
[buy(buns["мак"], 10, 20), {"product_id": juice.id, "qty": 5, "unit_cost": 80}],
)
journal.create_sale(vault, today, KIND_RETAIL, [line(buns["мак"], 3, 35)])
journal.create_sale(vault, today, KIND_RETAIL, [line(juice, 2, 120)])
buckets = stats.build_buckets(vault.doc, report_for(vault, today), stats.GRAIN_MONTH)
total = stats.totals(buckets)
assert total.qty_by_unit == {"шт": Decimal("3.000"), "л": Decimal("2.000")}
# --- разрез по партиям ----------------------------------------------------
def test_periods_carry_batch_economics(vault, buns, today, in_two_weeks):
batch = journal.create_batch(vault, today, in_two_weeks, [buy(buns["мак"], 10, 20)])
journal.create_sale(vault, today, KIND_RETAIL, [line(buns["мак"], 4, 35)], tip=25)
journal.create_sale(vault, today, KIND_SELF, [line(buns["мак"], 1, 0)])
period = stats.build_periods(vault.doc, report_for(vault, today))[0]
assert period.batch.id == batch.id
assert period.margin == Decimal("60.00") # 4 × 15
assert period.tips == Decimal("25.00")
assert period.report.consumed_cost == Decimal("20.00")
assert period.profit == Decimal("65.00") # 60 + 25 20
assert period.days_total == 14
def test_period_tips_split_between_batches(vault, buns, today):
first = journal.create_batch(
vault, today - timedelta(days=5), today + timedelta(days=9), [buy(buns["мак"], 3, 20)]
)
second = journal.create_batch(
vault, today, today + timedelta(days=14), [buy(buns["мак"], 7, 20)]
)
journal.create_sale(vault, today, KIND_RETAIL, [line(buns["мак"], 4, 35)], tip=40)
periods = {p.batch.id: p for p in stats.build_periods(vault.doc, report_for(vault, today))}
assert periods[first.id].tips == Decimal("30.00") # три штуки из четырёх
assert periods[second.id].tips == Decimal("10.00")
def test_period_quantities_keep_units_apart(vault, buns, today, in_two_weeks):
"""В партии и сок в литрах, и булки в штуках — складывать их нельзя."""
juice = journal.create_product(vault, "Сок", unit="л", cost_price=80, retail_price=120)
journal.create_batch(
vault, today, in_two_weeks,
[buy(buns["мак"], 10, 20), {"product_id": juice.id, "qty": 6, "unit_cost": 80}],
)
journal.create_sale(vault, today, KIND_RETAIL, [line(buns["мак"], 4, 35)])
journal.create_sale(vault, today, KIND_RETAIL, [line(juice, "1.5", 120)])
period = stats.build_periods(vault.doc, report_for(vault, today))[0]
assert period.sold_by_unit == {"шт": Decimal("4.000"), "л": Decimal("1.500")}
assert period.left_by_unit == {"шт": Decimal("6.000"), "л": Decimal("4.500")}
assert period.sold_text == "1,5 л · 4 шт"
assert period.left_text == "4,5 л · 6 шт"
def test_period_quantities_include_what_was_eaten(vault, buns, today, in_two_weeks):
journal.create_batch(vault, today, in_two_weeks, [buy(buns["мак"], 10, 20)])
journal.create_sale(vault, today, KIND_RETAIL, [line(buns["мак"], 3, 35)])
journal.create_sale(vault, today, KIND_SELF, [line(buns["мак"], 2, 0)])
period = stats.build_periods(vault.doc, report_for(vault, today))[0]
assert period.sold_by_unit == {"шт": Decimal("5.000")} # выбыло всего
assert period.left_by_unit == {"шт": Decimal("5.000")}
def test_units_text_hides_empty_units(vault, buns, today, in_two_weeks):
journal.create_batch(vault, today, in_two_weeks, [buy(buns["мак"], 10, 20)])
period = stats.build_periods(vault.doc, report_for(vault, today))[0]
assert period.sold_text == "" # ничего не продано — и нечего показывать
assert period.left_text == "10 шт"
def test_periods_are_listed_even_without_sales(vault, buns, today, in_two_weeks):
journal.create_batch(vault, today, in_two_weeks, [buy(buns["мак"], 10, 20)])
period = stats.build_periods(vault.doc, report_for(vault, today))[0]
assert period.margin == 0
assert period.report.cost_total == Decimal("200.00")
# --- разрез по товарам ----------------------------------------------------
def test_products_ranked_by_margin(vault, buns, today, in_two_weeks):
journal.create_batch(
vault, today, in_two_weeks,
[buy(buns["мак"], 20, 20), buy(buns["повидло"], 20, 30)],
)
journal.create_sale(vault, today, KIND_RETAIL, [line(buns["мак"], 2, 35)]) # 30
journal.create_sale(vault, today, KIND_RETAIL, [line(buns["повидло"], 10, 50)]) # 200
rows = stats.build_products(vault.doc, report_for(vault, today))
assert [r.name for r in rows][:2] == ["Булка с повидлом", "Булка с маком"]
assert rows[0].margin == Decimal("200.00")
assert rows[0].sold_qty == Decimal("10.000")
assert rows[0].margin_pct == Decimal("40.00") # 200 из 500
def test_products_separate_sold_from_eaten(vault, buns, today, in_two_weeks):
journal.create_batch(vault, today, in_two_weeks, [buy(buns["мак"], 20, 20)])
journal.create_sale(vault, today, KIND_RETAIL, [line(buns["мак"], 5, 35)])
journal.create_sale(vault, today, KIND_SELF, [line(buns["мак"], 3, 0)])
row = next(r for r in stats.build_products(vault.doc, report_for(vault, today)) if r.name == "Булка с маком")
assert row.sold_qty == Decimal("5.000")
assert row.consumed_qty == Decimal("3.000")
assert row.consumed_cost == Decimal("60.00")
assert row.stock_qty == Decimal("12.000")
def test_products_show_untouched_stock(vault, buns, today, in_two_weeks):
journal.create_batch(vault, today, in_two_weeks, [buy(buns["корица"], 8, 25)])
row = next(r for r in stats.build_products(vault.doc, report_for(vault, today)) if r.name == "Булка с корицей")
assert row.sold_qty == 0
assert row.stock_qty == Decimal("8.000")
assert row.stock_cost == Decimal("200.00")
# --- разрез по людям ------------------------------------------------------
def test_people_ranked_by_revenue(vault, buns, today, in_two_weeks):
journal.create_batch(vault, today, in_two_weeks, [buy(buns["мак"], 50, 20)])
vasya = journal.create_counterparty(vault, "Вася")
petya = journal.create_counterparty(vault, "Петя")
journal.create_sale(vault, today, KIND_RETAIL, [line(buns["мак"], 2, 35)], counterparty_id=vasya.id)
journal.create_sale(
vault, today, KIND_RETAIL, [line(buns["мак"], 10, 35)],
counterparty_id=petya.id, paid_amount=100, tip=20,
)
rows = stats.build_people(vault.doc)
assert [r.name for r in rows] == ["Петя", "Вася"]
assert rows[0].revenue == Decimal("350.00")
assert rows[0].paid == Decimal("100.00")
assert rows[0].tips == Decimal("20.00")
assert rows[0].debt == Decimal("250.00")
assert rows[0].sales_count == 1
def test_walk_in_sales_grouped_separately(vault, buns, today, in_two_weeks):
journal.create_batch(vault, today, in_two_weeks, [buy(buns["мак"], 50, 20)])
journal.create_sale(vault, today, KIND_RETAIL, [line(buns["мак"], 2, 35)])
rows = stats.build_people(vault.doc)
assert [r.name for r in rows] == ["Без контрагента"]
def test_consumption_is_not_a_person(vault, buns, today, in_two_weeks):
"""Съел сам — это не покупатель, в списке людей ему не место."""
journal.create_batch(vault, today, in_two_weeks, [buy(buns["мак"], 50, 20)])
journal.create_sale(vault, today, KIND_SELF, [line(buns["мак"], 2, 0)])
assert stats.build_people(vault.doc) == []
def test_empty_database_produces_empty_sections(vault, today):
report = report_for(vault, today)
assert stats.build_buckets(vault.doc, report, stats.GRAIN_MONTH) == []
assert stats.build_periods(vault.doc, report) == []
assert stats.build_products(vault.doc, report) == []
assert stats.build_people(vault.doc) == []