food-market/tests/test_ui_smoke.py

1010 lines
37 KiB
Python
Raw Normal View History

"""Дымовые тесты интерфейса: окно собирается, экраны рисуются, формы отдают данные.
Гоняются в offscreen-режиме, поэтому идут и в консоли, и без монитора.
Диалоги не показываются через exec() вместо этого проверяется их содержимое
и то, что из него получается.
"""
import os
from datetime import timedelta
from decimal import Decimal
import pytest
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from PySide6.QtWidgets import QApplication # noqa: E402
from app import journal # noqa: E402
from app.models import KIND_FRIEND, KIND_RETAIL, KIND_SELF # noqa: E402
from app.ui import theme # noqa: E402
from app.ui.main_window import MainWindow # noqa: E402
from app.ui.page_batches import BatchDialog # noqa: E402
from app.ui.page_journal import JournalPage, describe # noqa: E402
from app.ui.page_products import PriceDialog, ProductDialog # noqa: E402
from app.ui.page_sales import SaleDialog, price_source_for # noqa: E402
@pytest.fixture(scope="session")
def qapp():
app = QApplication.instance() or QApplication([])
theme.apply(app)
return app
@pytest.fixture
def window(qapp, vault, buns, today, in_two_weeks):
"""Окно на живых данных: партия, розница, друг в долг, съеденное."""
batch = journal.create_batch(
vault,
today,
in_two_weeks,
[
{"product_id": buns["повидло"].id, "qty": 5, "unit_cost": 30},
{"product_id": buns["мак"].id, "qty": 10, "unit_cost": 20},
{"product_id": buns["корица"].id, "qty": 20, "unit_cost": 25},
],
)
vasya = journal.create_counterparty(vault, "Вася")
journal.create_sale(
vault, today, KIND_RETAIL, [{"product_id": buns["повидло"].id, "qty": 3, "unit_price": 50}]
)
journal.create_sale(
vault, today, KIND_FRIEND, [{"product_id": buns["мак"].id, "qty": 5, "unit_price": 20}],
counterparty_id=vasya.id, paid_amount=0,
)
journal.create_sale(
vault, today, KIND_SELF, [{"product_id": buns["корица"].id, "qty": 2, "unit_price": 0}]
)
win = MainWindow(vault)
win.batch = batch
win.vasya = vasya
yield win
win.autosave_timer.stop()
win.sync_timer.stop()
win.close()
win.deleteLater()
# --- окно целиком ---------------------------------------------------------
Раздел статистики Шесть разрезов: периоды (заданные вручную, то есть партии), недели, месяцы, годы, товары и люди. У каждого график и таблица с итоговой строкой. Главное решение — две базы расчёта одновременно. Смешивать «когда продал» и «когда получил деньги» нельзя: ответы расходятся, как только появляется хоть один долг. Выручка, наценка, съеденное и чаевые считаются по дате продажи, «Собрано» — по дате платежа, «Закуплено» и «Отдано пекарне» — по своим датам. У разреза «Периоды» основание третье, привязка к партии, потому что вопрос там другой: покрыл ли я её к сроку. Количество нигде не сводится в одно число — только по единицам, «1,5 л · 4 шт». Сумма литров с килограммами выглядит как настоящая цифра, но смысла не имеет; ровно эту ошибку пришлось чинить и в разрезе по партиям. График на QPainter: QtCharts исключён из сборки, он тянет десятки мегабайт ради одной диаграммы. Убыток рисуется вниз от нулевой линии. Больше 24 корзин на график не помещается, и про срез написано над ним — молча показанная часть выглядела бы как всё. Тесты экранов больше не адресуют разделы по номеру в меню: новый раздел ломал половину проверок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 06:33:11 +07:00
def page_of(window, title: str):
"""Найти экран по названию.
Раньше тесты адресовали экраны по номеру в списке, и любой новый раздел
в боковом меню ломал половину проверок.
"""
return next(page for name, page in window.pages if name == title)
def test_window_builds_with_all_pages(window):
titles = [title for title, _ in window.pages]
Раздел статистики Шесть разрезов: периоды (заданные вручную, то есть партии), недели, месяцы, годы, товары и люди. У каждого график и таблица с итоговой строкой. Главное решение — две базы расчёта одновременно. Смешивать «когда продал» и «когда получил деньги» нельзя: ответы расходятся, как только появляется хоть один долг. Выручка, наценка, съеденное и чаевые считаются по дате продажи, «Собрано» — по дате платежа, «Закуплено» и «Отдано пекарне» — по своим датам. У разреза «Периоды» основание третье, привязка к партии, потому что вопрос там другой: покрыл ли я её к сроку. Количество нигде не сводится в одно число — только по единицам, «1,5 л · 4 шт». Сумма литров с килограммами выглядит как настоящая цифра, но смысла не имеет; ровно эту ошибку пришлось чинить и в разрезе по партиям. График на QPainter: QtCharts исключён из сборки, он тянет десятки мегабайт ради одной диаграммы. Убыток рисуется вниз от нулевой линии. Больше 24 корзин на график не помещается, и про срез написано над ним — молча показанная часть выглядела бы как всё. Тесты экранов больше не адресуют разделы по номеру в меню: новый раздел ломал половину проверок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 06:33:11 +07:00
assert titles == [
"Сводка", "Закупки", "Продажи", "Долги",
"Товары", "Статистика", "Журнал", "Настройки",
]
assert window.stack.count() == len(titles)
def test_every_page_refreshes_without_errors(window):
for index in range(len(window.pages)):
window.sidebar.setCurrentRow(index)
window.pages[index][1].refresh()
def test_changed_rebuilds_the_report(window, buns):
before = window.report.summary.owed_to_bakery
journal.add_bakery_payment(window.vault, 100, batch_id=window.batch.id)
window.changed()
assert window.report.summary.owed_to_bakery == before - 100
def test_autosave_writes_only_after_a_change(window):
window.save_now()
stamp = window.vault.path.stat().st_mtime_ns
window._autosave()
assert window.vault.path.stat().st_mtime_ns == stamp # файл не трогали
journal.create_product(window.vault, "Булка с изюмом", cost_price=22, retail_price=38)
assert window.vault.revision != window._saved_revision
window._autosave()
assert window.vault.revision == window._saved_revision
def test_sync_is_skipped_without_a_remote(window):
"""Без адреса репозитория фоновый тик не должен ничего делать."""
window.sync_now(quiet=True)
assert window._syncing is False
def test_git_always_targets_the_data_folder(window):
"""Приложение не должно распоряжаться репозиторием с исходниками.
exe обычно лежит в корне клона с кодом, и папка data внутри него.
Если git начать с корня приложения, оно перепишет исходникам origin
и запушит их в репозиторий данных. Ровно это однажды и произошло.
"""
from app import paths
sync = window._git()
assert sync.repo_dir == paths.data_dir()
assert sync.repo_dir != paths.app_root()
assert sync.rel_path == "vault.fmdb"
assert "/" not in sync.rel_path # база лежит в корне своего репозитория
def test_no_other_place_builds_git_by_hand():
"""Единственная точка создания GitSync — MainWindow._git()."""
from pathlib import Path
source = Path("app/ui/main_window.py").read_text(encoding="utf-8")
# Одно вхождение — внутри самого _git(); остальные зовут его.
assert source.count("GitSync(") == 1
assert "paths.app_root()" not in source
# --- таблицы наполняются --------------------------------------------------
def test_batches_table_shows_the_batch(window):
Раздел статистики Шесть разрезов: периоды (заданные вручную, то есть партии), недели, месяцы, годы, товары и люди. У каждого график и таблица с итоговой строкой. Главное решение — две базы расчёта одновременно. Смешивать «когда продал» и «когда получил деньги» нельзя: ответы расходятся, как только появляется хоть один долг. Выручка, наценка, съеденное и чаевые считаются по дате продажи, «Собрано» — по дате платежа, «Закуплено» и «Отдано пекарне» — по своим датам. У разреза «Периоды» основание третье, привязка к партии, потому что вопрос там другой: покрыл ли я её к сроку. Количество нигде не сводится в одно число — только по единицам, «1,5 л · 4 шт». Сумма литров с килограммами выглядит как настоящая цифра, но смысла не имеет; ровно эту ошибку пришлось чинить и в разрезе по партиям. График на QPainter: QtCharts исключён из сборки, он тянет десятки мегабайт ради одной диаграммы. Убыток рисуется вниз от нулевой линии. Больше 24 корзин на график не помещается, и про срез написано над ним — молча показанная часть выглядела бы как всё. Тесты экранов больше не адресуют разделы по номеру в меню: новый раздел ломал половину проверок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 06:33:11 +07:00
page = page_of(window, "Закупки")
page.refresh()
assert page.table.rowCount() == 1
assert page.table.item(0, 0).text() == "1"
assert "850" in page.table.item(0, 4).text()
def test_batch_details_show_coverage(window):
Раздел статистики Шесть разрезов: периоды (заданные вручную, то есть партии), недели, месяцы, годы, товары и люди. У каждого график и таблица с итоговой строкой. Главное решение — две базы расчёта одновременно. Смешивать «когда продал» и «когда получил деньги» нельзя: ответы расходятся, как только появляется хоть один долг. Выручка, наценка, съеденное и чаевые считаются по дате продажи, «Собрано» — по дате платежа, «Закуплено» и «Отдано пекарне» — по своим датам. У разреза «Периоды» основание третье, привязка к партии, потому что вопрос там другой: покрыл ли я её к сроку. Количество нигде не сводится в одно число — только по единицам, «1,5 л · 4 шт». Сумма литров с килограммами выглядит как настоящая цифра, но смысла не имеет; ровно эту ошибку пришлось чинить и в разрезе по партиям. График на QPainter: QtCharts исключён из сборки, он тянет десятки мегабайт ради одной диаграммы. Убыток рисуется вниз от нулевой линии. Больше 24 корзин на график не помещается, и про срез написано над ним — молча показанная часть выглядела бы как всё. Тесты экранов больше не адресуют разделы по номеру в меню: новый раздел ломал половину проверок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 06:33:11 +07:00
page = page_of(window, "Закупки")
page.refresh()
page.table.selectRow(0)
page._show_details()
assert "Партия" in page.details_title.text()
assert page.progress.value() == 17 # 150 из 850
assert page.contents.rowCount() == 3
def test_sales_table_lists_every_kind(window):
Раздел статистики Шесть разрезов: периоды (заданные вручную, то есть партии), недели, месяцы, годы, товары и люди. У каждого график и таблица с итоговой строкой. Главное решение — две базы расчёта одновременно. Смешивать «когда продал» и «когда получил деньги» нельзя: ответы расходятся, как только появляется хоть один долг. Выручка, наценка, съеденное и чаевые считаются по дате продажи, «Собрано» — по дате платежа, «Закуплено» и «Отдано пекарне» — по своим датам. У разреза «Периоды» основание третье, привязка к партии, потому что вопрос там другой: покрыл ли я её к сроку. Количество нигде не сводится в одно число — только по единицам, «1,5 л · 4 шт». Сумма литров с килограммами выглядит как настоящая цифра, но смысла не имеет; ровно эту ошибку пришлось чинить и в разрезе по партиям. График на QPainter: QtCharts исключён из сборки, он тянет десятки мегабайт ради одной диаграммы. Убыток рисуется вниз от нулевой линии. Больше 24 корзин на график не помещается, и про срез написано над ним — молча показанная часть выглядела бы как всё. Тесты экранов больше не адресуют разделы по номеру в меню: новый раздел ломал половину проверок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 06:33:11 +07:00
page = page_of(window, "Продажи")
page.refresh()
assert page.table.rowCount() == 3
def test_sales_filter_narrows_the_list(window):
Раздел статистики Шесть разрезов: периоды (заданные вручную, то есть партии), недели, месяцы, годы, товары и люди. У каждого график и таблица с итоговой строкой. Главное решение — две базы расчёта одновременно. Смешивать «когда продал» и «когда получил деньги» нельзя: ответы расходятся, как только появляется хоть один долг. Выручка, наценка, съеденное и чаевые считаются по дате продажи, «Собрано» — по дате платежа, «Закуплено» и «Отдано пекарне» — по своим датам. У разреза «Периоды» основание третье, привязка к партии, потому что вопрос там другой: покрыл ли я её к сроку. Количество нигде не сводится в одно число — только по единицам, «1,5 л · 4 шт». Сумма литров с килограммами выглядит как настоящая цифра, но смысла не имеет; ровно эту ошибку пришлось чинить и в разрезе по партиям. График на QPainter: QtCharts исключён из сборки, он тянет десятки мегабайт ради одной диаграммы. Убыток рисуется вниз от нулевой линии. Больше 24 корзин на график не помещается, и про срез написано над ним — молча показанная часть выглядела бы как всё. Тесты экранов больше не адресуют разделы по номеру в меню: новый раздел ломал половину проверок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 06:33:11 +07:00
page = page_of(window, "Продажи")
page.filter.setCurrentIndex(page.filter.findData(KIND_SELF))
page.refresh()
assert page.table.rowCount() == 1
def test_debts_tree_groups_by_person(window):
Раздел статистики Шесть разрезов: периоды (заданные вручную, то есть партии), недели, месяцы, годы, товары и люди. У каждого график и таблица с итоговой строкой. Главное решение — две базы расчёта одновременно. Смешивать «когда продал» и «когда получил деньги» нельзя: ответы расходятся, как только появляется хоть один долг. Выручка, наценка, съеденное и чаевые считаются по дате продажи, «Собрано» — по дате платежа, «Закуплено» и «Отдано пекарне» — по своим датам. У разреза «Периоды» основание третье, привязка к партии, потому что вопрос там другой: покрыл ли я её к сроку. Количество нигде не сводится в одно число — только по единицам, «1,5 л · 4 шт». Сумма литров с килограммами выглядит как настоящая цифра, но смысла не имеет; ровно эту ошибку пришлось чинить и в разрезе по партиям. График на QPainter: QtCharts исключён из сборки, он тянет десятки мегабайт ради одной диаграммы. Убыток рисуется вниз от нулевой линии. Больше 24 корзин на график не помещается, и про срез написано над ним — молча показанная часть выглядела бы как всё. Тесты экранов больше не адресуют разделы по номеру в меню: новый раздел ломал половину проверок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 06:33:11 +07:00
page = page_of(window, "Долги")
page.refresh()
assert page.tree.topLevelItemCount() == 1
person = page.tree.topLevelItem(0)
assert person.text(0) == "Вася"
assert "100" in person.text(4)
assert person.childCount() == 1
def test_products_table_shows_stock_and_margin(window):
Раздел статистики Шесть разрезов: периоды (заданные вручную, то есть партии), недели, месяцы, годы, товары и люди. У каждого график и таблица с итоговой строкой. Главное решение — две базы расчёта одновременно. Смешивать «когда продал» и «когда получил деньги» нельзя: ответы расходятся, как только появляется хоть один долг. Выручка, наценка, съеденное и чаевые считаются по дате продажи, «Собрано» — по дате платежа, «Закуплено» и «Отдано пекарне» — по своим датам. У разреза «Периоды» основание третье, привязка к партии, потому что вопрос там другой: покрыл ли я её к сроку. Количество нигде не сводится в одно число — только по единицам, «1,5 л · 4 шт». Сумма литров с килограммами выглядит как настоящая цифра, но смысла не имеет; ровно эту ошибку пришлось чинить и в разрезе по партиям. График на QPainter: QtCharts исключён из сборки, он тянет десятки мегабайт ради одной диаграммы. Убыток рисуется вниз от нулевой линии. Больше 24 корзин на график не помещается, и про срез написано над ним — молча показанная часть выглядела бы как всё. Тесты экранов больше не адресуют разделы по номеру в меню: новый раздел ломал половину проверок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 06:33:11 +07:00
page = page_of(window, "Товары")
page.refresh()
names = [page.table.item(r, 0).text() for r in range(page.table.rowCount())]
assert "Булка с маком" in names
row = names.index("Булка с маком")
Отдельный репозиторий для базы, фасовки товара, ввод истории Три правки по замечаниям. 1. База и код разъехались по разным репозиториям. Автокоммит и раньше трогал только vault.fmdb, но лежал он в той же ветке, что и исходники, поэтому пуш тащил всю историю программы. Теперь data/ — самостоятельный клон food-records со своим .git, а исходники его игнорируют целиком. При первом запуске приложение встаёт на уже существующую историю сервера, а не заводит параллельную. 2. Фасовки товара. Пачка печенья 10 шт за 200 ₽ и та же печенька поштучно за 30 ₽ — один товар с двумя фасовками. Остатки, себестоимость и FIFO считаются в базовых единицах, поэтому поштучные продажи вычитаются из купленных пачек. Размер фасовки хранится в документе слепком. Миграция схемы 1→2 не меняет поведение уже заведённых данных. 3. Быстрый ввод продаж за период: выбор диапазона дат, строка таблицы — отдельная продажа, ввод с клавиатуры, запись всех разом. Незнакомое имя заводится как контрагент, повтор периода подсвечивается. Тесты больше не имеют настроенного remote, чтобы не ходить в сеть. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 01:25:39 +07:00
assert "15" in page.table.item(row, 5).text() # наценка: 35 20
assert page.table.item(row, 6).text() == "5" # 10 закуплено 5 отдано
def test_archived_products_are_hidden_by_default(window, buns):
Раздел статистики Шесть разрезов: периоды (заданные вручную, то есть партии), недели, месяцы, годы, товары и люди. У каждого график и таблица с итоговой строкой. Главное решение — две базы расчёта одновременно. Смешивать «когда продал» и «когда получил деньги» нельзя: ответы расходятся, как только появляется хоть один долг. Выручка, наценка, съеденное и чаевые считаются по дате продажи, «Собрано» — по дате платежа, «Закуплено» и «Отдано пекарне» — по своим датам. У разреза «Периоды» основание третье, привязка к партии, потому что вопрос там другой: покрыл ли я её к сроку. Количество нигде не сводится в одно число — только по единицам, «1,5 л · 4 шт». Сумма литров с килограммами выглядит как настоящая цифра, но смысла не имеет; ровно эту ошибку пришлось чинить и в разрезе по партиям. График на QPainter: QtCharts исключён из сборки, он тянет десятки мегабайт ради одной диаграммы. Убыток рисуется вниз от нулевой линии. Больше 24 корзин на график не помещается, и про срез написано над ним — молча показанная часть выглядела бы как всё. Тесты экранов больше не адресуют разделы по номеру в меню: новый раздел ломал половину проверок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 06:33:11 +07:00
page = page_of(window, "Товары")
journal.update_product(window.vault, buns["мак"].id, archived=True)
window.changed()
visible = [page.table.item(r, 0).text() for r in range(page.table.rowCount())]
assert "Булка с маком" not in visible
page.show_archived.setChecked(True)
visible = [page.table.item(r, 0).text() for r in range(page.table.rowCount())]
assert "Булка с маком" in visible
# --- сводка ---------------------------------------------------------------
def test_dashboard_renders_metrics_and_deadline(window):
from PySide6.QtWidgets import QLabel, QProgressBar
Раздел статистики Шесть разрезов: периоды (заданные вручную, то есть партии), недели, месяцы, годы, товары и люди. У каждого график и таблица с итоговой строкой. Главное решение — две базы расчёта одновременно. Смешивать «когда продал» и «когда получил деньги» нельзя: ответы расходятся, как только появляется хоть один долг. Выручка, наценка, съеденное и чаевые считаются по дате продажи, «Собрано» — по дате платежа, «Закуплено» и «Отдано пекарне» — по своим датам. У разреза «Периоды» основание третье, привязка к партии, потому что вопрос там другой: покрыл ли я её к сроку. Количество нигде не сводится в одно число — только по единицам, «1,5 л · 4 шт». Сумма литров с килограммами выглядит как настоящая цифра, но смысла не имеет; ровно эту ошибку пришлось чинить и в разрезе по партиям. График на QPainter: QtCharts исключён из сборки, он тянет десятки мегабайт ради одной диаграммы. Убыток рисуется вниз от нулевой линии. Больше 24 корзин на график не помещается, и про срез написано над ним — молча показанная часть выглядела бы как всё. Тесты экранов больше не адресуют разделы по номеру в меню: новый раздел ломал половину проверок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 06:33:11 +07:00
page = page_of(window, "Сводка")
page.refresh()
texts = " ".join(label.text() for label in page.body.findChildren(QLabel))
assert "Долг пекарне" in texts
assert "850,00 ₽" in texts # вся себестоимость партии ещё не возвращена
assert "Мне должны" in texts
assert "100,00 ₽" in texts # долг Васи
# Остаток показан деньгами и позициями, а не суммой разных единиц.
assert "610,00 ₽" in texts
assert "позиций: 3" in texts
bars = page.body.findChildren(QProgressBar)
assert len(bars) == 1
assert bars[0].value() == 17 # собрано 150 из 850
def test_dashboard_shows_a_warning_when_oversold(window, buns):
Раздел статистики Шесть разрезов: периоды (заданные вручную, то есть партии), недели, месяцы, годы, товары и люди. У каждого график и таблица с итоговой строкой. Главное решение — две базы расчёта одновременно. Смешивать «когда продал» и «когда получил деньги» нельзя: ответы расходятся, как только появляется хоть один долг. Выручка, наценка, съеденное и чаевые считаются по дате продажи, «Собрано» — по дате платежа, «Закуплено» и «Отдано пекарне» — по своим датам. У разреза «Периоды» основание третье, привязка к партии, потому что вопрос там другой: покрыл ли я её к сроку. Количество нигде не сводится в одно число — только по единицам, «1,5 л · 4 шт». Сумма литров с килограммами выглядит как настоящая цифра, но смысла не имеет; ровно эту ошибку пришлось чинить и в разрезе по партиям. График на QPainter: QtCharts исключён из сборки, он тянет десятки мегабайт ради одной диаграммы. Убыток рисуется вниз от нулевой линии. Больше 24 корзин на график не помещается, и про срез написано над ним — молча показанная часть выглядела бы как всё. Тесты экранов больше не адресуют разделы по номеру в меню: новый раздел ломал половину проверок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 06:33:11 +07:00
page = page_of(window, "Сводка")
journal.create_sale(
window.vault, window.report.today, KIND_RETAIL,
[{"product_id": buns["повидло"].id, "qty": 99, "unit_price": 50}],
)
window.changed()
assert any("больше" in warning for warning in window.report.warnings)
# --- журнал ---------------------------------------------------------------
def test_journal_page_lists_entries(window):
Раздел статистики Шесть разрезов: периоды (заданные вручную, то есть партии), недели, месяцы, годы, товары и люди. У каждого график и таблица с итоговой строкой. Главное решение — две базы расчёта одновременно. Смешивать «когда продал» и «когда получил деньги» нельзя: ответы расходятся, как только появляется хоть один долг. Выручка, наценка, съеденное и чаевые считаются по дате продажи, «Собрано» — по дате платежа, «Закуплено» и «Отдано пекарне» — по своим датам. У разреза «Периоды» основание третье, привязка к партии, потому что вопрос там другой: покрыл ли я её к сроку. Количество нигде не сводится в одно число — только по единицам, «1,5 л · 4 шт». Сумма литров с килограммами выглядит как настоящая цифра, но смысла не имеет; ровно эту ошибку пришлось чинить и в разрезе по партиям. График на QPainter: QtCharts исключён из сборки, он тянет десятки мегабайт ради одной диаграммы. Убыток рисуется вниз от нулевой линии. Больше 24 корзин на график не помещается, и про срез написано над ним — молча показанная часть выглядела бы как всё. Тесты экранов больше не адресуют разделы по номеру в меню: новый раздел ломал половину проверок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 06:33:11 +07:00
page = page_of(window, "Журнал")
page.refresh()
assert page.table.rowCount() == len(window.vault.doc.journal)
def test_journal_search_filters(window, buns):
Раздел статистики Шесть разрезов: периоды (заданные вручную, то есть партии), недели, месяцы, годы, товары и люди. У каждого график и таблица с итоговой строкой. Главное решение — две базы расчёта одновременно. Смешивать «когда продал» и «когда получил деньги» нельзя: ответы расходятся, как только появляется хоть один долг. Выручка, наценка, съеденное и чаевые считаются по дате продажи, «Собрано» — по дате платежа, «Закуплено» и «Отдано пекарне» — по своим датам. У разреза «Периоды» основание третье, привязка к партии, потому что вопрос там другой: покрыл ли я её к сроку. Количество нигде не сводится в одно число — только по единицам, «1,5 л · 4 шт». Сумма литров с килограммами выглядит как настоящая цифра, но смысла не имеет; ровно эту ошибку пришлось чинить и в разрезе по партиям. График на QPainter: QtCharts исключён из сборки, он тянет десятки мегабайт ради одной диаграммы. Убыток рисуется вниз от нулевой линии. Больше 24 корзин на график не помещается, и про срез написано над ним — молча показанная часть выглядела бы как всё. Тесты экранов больше не адресуют разделы по номеру в меню: новый раздел ломал половину проверок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 06:33:11 +07:00
page = page_of(window, "Журнал")
journal.change_prices(window.vault, buns["мак"].id, cost_price=25, retail_price=40)
window.changed()
page.search.setText("цены изменены")
page.refresh()
assert page.table.rowCount() == 1
assert "20,00 ₽ → 25,00 ₽" in page.table.item(0, 4).text()
def test_journal_search_matches_what_is_displayed(window, buns):
"""Формат на экране и то, что ищется, должны совпадать до пробела."""
Раздел статистики Шесть разрезов: периоды (заданные вручную, то есть партии), недели, месяцы, годы, товары и люди. У каждого график и таблица с итоговой строкой. Главное решение — две базы расчёта одновременно. Смешивать «когда продал» и «когда получил деньги» нельзя: ответы расходятся, как только появляется хоть один долг. Выручка, наценка, съеденное и чаевые считаются по дате продажи, «Собрано» — по дате платежа, «Закуплено» и «Отдано пекарне» — по своим датам. У разреза «Периоды» основание третье, привязка к партии, потому что вопрос там другой: покрыл ли я её к сроку. Количество нигде не сводится в одно число — только по единицам, «1,5 л · 4 шт». Сумма литров с килограммами выглядит как настоящая цифра, но смысла не имеет; ровно эту ошибку пришлось чинить и в разрезе по партиям. График на QPainter: QtCharts исключён из сборки, он тянет десятки мегабайт ради одной диаграммы. Убыток рисуется вниз от нулевой линии. Больше 24 корзин на график не помещается, и про срез написано над ним — молча показанная часть выглядела бы как всё. Тесты экранов больше не адресуют разделы по номеру в меню: новый раздел ломал половину проверок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 06:33:11 +07:00
page = page_of(window, "Журнал")
journal.change_prices(window.vault, buns["мак"].id, cost_price=1500, retail_price=2000)
window.changed()
page.search.setText("1 500,00")
page.refresh()
assert page.table.rowCount() == 1
def test_journal_entity_filter(window):
Раздел статистики Шесть разрезов: периоды (заданные вручную, то есть партии), недели, месяцы, годы, товары и люди. У каждого график и таблица с итоговой строкой. Главное решение — две базы расчёта одновременно. Смешивать «когда продал» и «когда получил деньги» нельзя: ответы расходятся, как только появляется хоть один долг. Выручка, наценка, съеденное и чаевые считаются по дате продажи, «Собрано» — по дате платежа, «Закуплено» и «Отдано пекарне» — по своим датам. У разреза «Периоды» основание третье, привязка к партии, потому что вопрос там другой: покрыл ли я её к сроку. Количество нигде не сводится в одно число — только по единицам, «1,5 л · 4 шт». Сумма литров с килограммами выглядит как настоящая цифра, но смысла не имеет; ровно эту ошибку пришлось чинить и в разрезе по партиям. График на QPainter: QtCharts исключён из сборки, он тянет десятки мегабайт ради одной диаграммы. Убыток рисуется вниз от нулевой линии. Больше 24 корзин на график не помещается, и про срез написано над ним — молча показанная часть выглядела бы как всё. Тесты экранов больше не адресуют разделы по номеру в меню: новый раздел ломал половину проверок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 06:33:11 +07:00
page = page_of(window, "Журнал")
page.entity.setCurrentIndex(page.entity.findData("batch"))
page.refresh()
assert page.table.rowCount() == 1
def test_describe_renders_a_transition():
from app.models import Change, JournalEntry
entry = JournalEntry(
id="j_1", ts="", host="", action="product.price_change",
entity="product", entity_id="p_1", label="Булка",
changes=[Change("себестоимость", "20,00 ₽", "25,00 ₽")],
)
assert describe(entry) == "себестоимость: 20,00 ₽ → 25,00 ₽"
# --- формы ----------------------------------------------------------------
def test_batch_dialog_round_trips_lines(window):
dialog = BatchDialog(window, window.batch)
lines = dialog.lines.lines("unit_cost")
assert len(lines) == 3
assert dialog.lines.total() == Decimal("850.00")
assert lines[0]["unit_cost"] == Decimal("30.00")
dialog.deleteLater()
def test_batch_dialog_defaults_the_deadline_from_settings(window):
window.vault.doc.settings.default_period_days = 21
dialog = BatchDialog(window)
assert (dialog.due.get_date() - dialog.date.get_date()).days == 21
dialog.deleteLater()
def test_batch_dialog_deadline_follows_the_purchase_date(window):
dialog = BatchDialog(window)
dialog.date.set_date(dialog.date.get_date() + timedelta(days=5))
days = window.vault.doc.settings.default_period_days
assert (dialog.due.get_date() - dialog.date.get_date()).days == days
dialog.deleteLater()
def test_sale_dialog_substitutes_price_by_kind(window, buns):
"""Ради этого и заведены типы: другу — себестоимость, себе — ноль."""
dialog = SaleDialog(window)
dialog.lines.table.cellWidget(0, 0).select_product(buns["мак"].id)
dialog.kind.setCurrentIndex(0) # розница
assert dialog.lines.lines("unit_price")[0]["unit_price"] == Decimal("35.00")
dialog.kind.setCurrentIndex(1) # другу по себестоимости
assert dialog.lines.lines("unit_price")[0]["unit_price"] == Decimal("20.00")
dialog.kind.setCurrentIndex(2) # съел сам
assert dialog.lines.lines("unit_price")[0]["unit_price"] == Decimal("0.00")
dialog.deleteLater()
def test_sale_dialog_disables_payment_for_consumption(window):
dialog = SaleDialog(window)
dialog.kind.setCurrentIndex(2) # съел сам
assert dialog.paid.isEnabled() is False
assert dialog.paid.value_decimal() == 0
dialog.deleteLater()
def test_sale_dialog_prefills_full_payment(window, buns):
dialog = SaleDialog(window)
dialog.lines.table.cellWidget(0, 0).select_product(buns["мак"].id)
dialog.lines.table.cellWidget(0, 1).set_decimal(4)
assert dialog.paid.value_decimal() == Decimal("140.00") # 4 × 35
assert "полностью" in dialog.paid_hint.text()
dialog.deleteLater()
def test_price_source_for_each_kind(buns):
Отдельный репозиторий для базы, фасовки товара, ввод истории Три правки по замечаниям. 1. База и код разъехались по разным репозиториям. Автокоммит и раньше трогал только vault.fmdb, но лежал он в той же ветке, что и исходники, поэтому пуш тащил всю историю программы. Теперь data/ — самостоятельный клон food-records со своим .git, а исходники его игнорируют целиком. При первом запуске приложение встаёт на уже существующую историю сервера, а не заводит параллельную. 2. Фасовки товара. Пачка печенья 10 шт за 200 ₽ и та же печенька поштучно за 30 ₽ — один товар с двумя фасовками. Остатки, себестоимость и FIFO считаются в базовых единицах, поэтому поштучные продажи вычитаются из купленных пачек. Размер фасовки хранится в документе слепком. Миграция схемы 1→2 не меняет поведение уже заведённых данных. 3. Быстрый ввод продаж за период: выбор диапазона дат, строка таблицы — отдельная продажа, ввод с клавиатуры, запись всех разом. Незнакомое имя заводится как контрагент, повтор периода подсвечивается. Тесты больше не имеют настроенного remote, чтобы не ходить в сеть. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 01:25:39 +07:00
bun = buns["мак"]
assert price_source_for(KIND_RETAIL)(bun, "шт") == Decimal("35.00")
assert price_source_for(KIND_FRIEND)(bun, "шт") == Decimal("20.00")
assert price_source_for(KIND_SELF)(bun, "шт") == 0
def test_price_source_uses_the_pack_price(vault, buns):
"""Пачка стоит своих денег, а не десяти поштучных цен."""
from app import journal as j
cookies = j.create_product(vault, "Печенье", unit="шт", cost_price=20, retail_price=30)
j.set_packs(vault, cookies.id, [{"name": "пачка", "size": 10, "retail_price": 200}])
cookies = vault.doc.product(cookies.id)
assert price_source_for(KIND_RETAIL)(cookies, "шт") == Decimal("30.00")
assert price_source_for(KIND_RETAIL)(cookies, "пачка") == Decimal("200.00")
assert price_source_for(KIND_FRIEND)(cookies, "пачка") == Decimal("200.00") # 10 × 20
def test_price_dialog_shows_history(window, buns):
journal.change_prices(window.vault, buns["мак"].id, cost_price=25, retail_price=40)
dialog = PriceDialog("", window.vault.doc.product(buns["мак"].id))
assert dialog.cost.value_decimal() == Decimal("25.00")
dialog.deleteLater()
def test_product_dialog_hides_prices_when_editing(window, buns):
"""Цены правятся только через историю — иначе она бы врала."""
editing = ProductDialog("", window.vault.doc.product(buns["мак"].id))
assert not hasattr(editing, "cost")
editing.deleteLater()
creating = ProductDialog("")
assert hasattr(creating, "cost")
creating.deleteLater()
Отдельный репозиторий для базы, фасовки товара, ввод истории Три правки по замечаниям. 1. База и код разъехались по разным репозиториям. Автокоммит и раньше трогал только vault.fmdb, но лежал он в той же ветке, что и исходники, поэтому пуш тащил всю историю программы. Теперь data/ — самостоятельный клон food-records со своим .git, а исходники его игнорируют целиком. При первом запуске приложение встаёт на уже существующую историю сервера, а не заводит параллельную. 2. Фасовки товара. Пачка печенья 10 шт за 200 ₽ и та же печенька поштучно за 30 ₽ — один товар с двумя фасовками. Остатки, себестоимость и FIFO считаются в базовых единицах, поэтому поштучные продажи вычитаются из купленных пачек. Размер фасовки хранится в документе слепком. Миграция схемы 1→2 не меняет поведение уже заведённых данных. 3. Быстрый ввод продаж за период: выбор диапазона дат, строка таблицы — отдельная продажа, ввод с клавиатуры, запись всех разом. Незнакомое имя заводится как контрагент, повтор периода подсвечивается. Тесты больше не имеют настроенного remote, чтобы не ходить в сеть. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 01:25:39 +07:00
def test_lines_editor_switches_price_with_the_pack(window):
"""Выбрал пачку — цена и подпись меняются, товар остаётся тем же."""
from app import journal as j
cookies = j.create_product(window.vault, "Печенье", unit="шт", cost_price=20, retail_price=30)
j.set_packs(window.vault, cookies.id, [{"name": "пачка", "size": 10, "retail_price": 200}])
window.changed()
dialog = SaleDialog(window)
editor = dialog.lines
editor.table.cellWidget(0, editor.COL_PRODUCT).select_product(cookies.id)
uom = editor.table.cellWidget(0, editor.COL_UOM)
assert [uom.itemText(i) for i in range(uom.count())] == ["шт", "пачка"]
assert uom.isEnabled() is True
assert editor.lines("unit_price")[0]["unit_price"] == Decimal("30.00")
uom.setCurrentText("пачка")
line = editor.lines("unit_price")[0]
assert line["uom"] == "пачка"
assert line["unit_price"] == Decimal("200.00")
dialog.deleteLater()
Цена по фасовке не подставлялась после удаления строки Обработчики строк запоминали номер строки в момент создания. После удаления любой строки всё, что было ниже, съезжает вверх, и запомненный номер начинает указывать на соседа или за пределы таблицы. Строка молча переставала работать целиком: выбираешь фасовку — цена не меняется, меняешь товар — не пересобирается список фасовок. Приходилось править цену руками. Теперь обработчики привязаны к самому виджету, а строка ищется по нему в момент вызова. То же исправлено в быстром вводе, где строк больше и удаляют их чаще. Заодно найдена мина, которую посадил я сам в разделе статистики: self.metric = QComboBox() на QWidget затеняет метод QWidget.metric(), который Qt зовёт при смене стиля. Падало это в чужом месте и с невнятным «object is not callable». Такая же история была с self.layout в сводке. Оба переименованы, добавлен тест, который обходит все виджеты и проверяет, что ни один атрибут не затеняет метод Qt. И подсказка в карточке товара: себестоимость фасовки нигде не вводится, она выводится из цены базовой единицы. Теперь под таблицей фасовок прямо написано, какая сумма подставится в закупку. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 21:50:42 +07:00
def test_pack_price_still_applies_after_deleting_a_row(window, buns):
"""Обработчики строк не должны держаться за номер строки.
После удаления строки всё, что было ниже, съезжает вверх. Запомненный
номер начинал указывать на соседа или за пределы таблицы, и строка
молча переставала работать: выбираешь фасовку, а цена не меняется.
"""
from app import journal as j
cookies = j.create_product(window.vault, "Печенье", unit="шт", cost_price=20, retail_price=30)
j.set_packs(window.vault, cookies.id, [{"name": "пачка", "size": 10, "retail_price": 200}])
window.changed()
dialog = BatchDialog(window)
editor = dialog.lines
editor.add_line()
editor.table.cellWidget(1, editor.COL_PRODUCT).select_product(cookies.id)
# Удаляем первую строку — печенье переезжает на её место.
editor.table.setCurrentCell(0, 0)
editor.remove_current()
assert editor.table.rowCount() == 1
editor.table.cellWidget(0, editor.COL_UOM).setCurrentText("пачка")
line = editor.lines("unit_cost")[0]
assert line["uom"] == "пачка"
assert line["unit_cost"] == Decimal("200.00") # 20 за штуку × 10
dialog.deleteLater()
def test_product_change_still_refills_packs_after_deleting_a_row(window, buns):
from app import journal as j
cookies = j.create_product(window.vault, "Печенье", unit="шт", cost_price=20, retail_price=30)
j.set_packs(window.vault, cookies.id, [{"name": "пачка", "size": 10, "retail_price": 200}])
window.changed()
dialog = SaleDialog(window)
editor = dialog.lines
editor.add_line()
editor.table.setCurrentCell(0, 0)
editor.remove_current()
# Строка переехала наверх — выбор товара обязан пересобрать её фасовки.
editor.table.cellWidget(0, editor.COL_PRODUCT).select_product(cookies.id)
uom = editor.table.cellWidget(0, editor.COL_UOM)
assert [uom.itemText(i) for i in range(uom.count())] == ["шт", "пачка"]
assert editor.lines("unit_price")[0]["unit_price"] == Decimal("30.00")
dialog.deleteLater()
def test_row_of_finds_the_current_position(qapp):
from PySide6.QtWidgets import QLineEdit, QTableWidget
from app.ui import widgets as wd
table = QTableWidget(3, 1)
fields = [QLineEdit() for _ in range(3)]
for index, field in enumerate(fields):
table.setCellWidget(index, 0, field)
assert wd.row_of(table, fields[2]) == 2
table.removeRow(0)
assert wd.row_of(table, fields[2]) == 1 # съехал вверх
assert wd.row_of(table, fields[0]) == -1 # удалён
table.deleteLater()
Отдельный репозиторий для базы, фасовки товара, ввод истории Три правки по замечаниям. 1. База и код разъехались по разным репозиториям. Автокоммит и раньше трогал только vault.fmdb, но лежал он в той же ветке, что и исходники, поэтому пуш тащил всю историю программы. Теперь data/ — самостоятельный клон food-records со своим .git, а исходники его игнорируют целиком. При первом запуске приложение встаёт на уже существующую историю сервера, а не заводит параллельную. 2. Фасовки товара. Пачка печенья 10 шт за 200 ₽ и та же печенька поштучно за 30 ₽ — один товар с двумя фасовками. Остатки, себестоимость и FIFO считаются в базовых единицах, поэтому поштучные продажи вычитаются из купленных пачек. Размер фасовки хранится в документе слепком. Миграция схемы 1→2 не меняет поведение уже заведённых данных. 3. Быстрый ввод продаж за период: выбор диапазона дат, строка таблицы — отдельная продажа, ввод с клавиатуры, запись всех разом. Незнакомое имя заводится как контрагент, повтор периода подсвечивается. Тесты больше не имеют настроенного remote, чтобы не ходить в сеть. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 01:25:39 +07:00
def test_pack_column_is_disabled_without_packs(window, buns):
dialog = SaleDialog(window)
editor = dialog.lines
editor.table.cellWidget(0, editor.COL_PRODUCT).select_product(buns["мак"].id)
uom = editor.table.cellWidget(0, editor.COL_UOM)
assert uom.currentText() == "шт"
assert uom.isEnabled() is False
dialog.deleteLater()
Цена по фасовке не подставлялась после удаления строки Обработчики строк запоминали номер строки в момент создания. После удаления любой строки всё, что было ниже, съезжает вверх, и запомненный номер начинает указывать на соседа или за пределы таблицы. Строка молча переставала работать целиком: выбираешь фасовку — цена не меняется, меняешь товар — не пересобирается список фасовок. Приходилось править цену руками. Теперь обработчики привязаны к самому виджету, а строка ищется по нему в момент вызова. То же исправлено в быстром вводе, где строк больше и удаляют их чаще. Заодно найдена мина, которую посадил я сам в разделе статистики: self.metric = QComboBox() на QWidget затеняет метод QWidget.metric(), который Qt зовёт при смене стиля. Падало это в чужом месте и с невнятным «object is not callable». Такая же история была с self.layout в сводке. Оба переименованы, добавлен тест, который обходит все виджеты и проверяет, что ни один атрибут не затеняет метод Qt. И подсказка в карточке товара: себестоимость фасовки нигде не вводится, она выводится из цены базовой единицы. Теперь под таблицей фасовок прямо написано, какая сумма подставится в закупку. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 21:50:42 +07:00
def test_packs_editor_shows_the_derived_cost(window):
"""Себестоимость фасовки нигде не вводится — надо показать, откуда она.
Иначе легко указать в карточке цену за пачку вместо цены за штуку,
и подставляемая в закупку сумма окажется больше в размер фасовки.
"""
from app import journal as j
from app.ui.page_products import ProductDialog as PD
cookies = j.create_product(window.vault, "Печенье", unit="шт", cost_price=20, retail_price=30)
j.set_packs(window.vault, cookies.id, [{"name": "пачка", "size": 10, "retail_price": 200}])
dialog = PD("", window.vault.doc.product(cookies.id))
assert "пачка = 10 шт" in dialog.packs.derived.text()
assert "200,00 ₽" in dialog.packs.derived.text() # 20 за штуку × 10
dialog.deleteLater()
def test_derived_cost_follows_the_price_field(window):
from app.ui.page_products import ProductDialog as PD
dialog = PD("")
dialog.cost.set_decimal(20)
dialog.packs.add_pack("пачка", 10, 200)
assert "200,00 ₽" in dialog.packs.derived.text()
dialog.cost.set_decimal(25)
assert "250,00 ₽" in dialog.packs.derived.text()
dialog.deleteLater()
Отдельный репозиторий для базы, фасовки товара, ввод истории Три правки по замечаниям. 1. База и код разъехались по разным репозиториям. Автокоммит и раньше трогал только vault.fmdb, но лежал он в той же ветке, что и исходники, поэтому пуш тащил всю историю программы. Теперь data/ — самостоятельный клон food-records со своим .git, а исходники его игнорируют целиком. При первом запуске приложение встаёт на уже существующую историю сервера, а не заводит параллельную. 2. Фасовки товара. Пачка печенья 10 шт за 200 ₽ и та же печенька поштучно за 30 ₽ — один товар с двумя фасовками. Остатки, себестоимость и FIFO считаются в базовых единицах, поэтому поштучные продажи вычитаются из купленных пачек. Размер фасовки хранится в документе слепком. Миграция схемы 1→2 не меняет поведение уже заведённых данных. 3. Быстрый ввод продаж за период: выбор диапазона дат, строка таблицы — отдельная продажа, ввод с клавиатуры, запись всех разом. Незнакомое имя заводится как контрагент, повтор периода подсвечивается. Тесты больше не имеют настроенного remote, чтобы не ходить в сеть. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 01:25:39 +07:00
def test_packs_editor_round_trip(window):
from app import journal as j
from app.ui.page_products import ProductDialog as PD
cookies = j.create_product(window.vault, "Печенье", unit="шт", cost_price=20, retail_price=30)
j.set_packs(window.vault, cookies.id, [{"name": "пачка", "size": 10, "retail_price": 200}])
dialog = PD("", window.vault.doc.product(cookies.id))
assert dialog.packs.packs() == [
{"name": "пачка", "size": Decimal("10.000"), "retail_price": Decimal("200.00")}
]
dialog.deleteLater()
def test_lines_editor_add_and_remove(window, buns):
dialog = BatchDialog(window)
editor = dialog.lines
start = editor.table.rowCount()
editor.add_line(buns["мак"].id, 7, 20)
assert len(editor.lines("unit_cost")) == start + 1
editor.table.setCurrentCell(editor.table.rowCount() - 1, 0)
editor.remove_current()
assert len(editor.lines("unit_cost")) == start
dialog.deleteLater()
Пекарня может простить остаток Надо было отдать 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_payment_dialog_offers_to_write_off_the_remainder(window):
"""Надо было 850, отдал 800 — полсотни предлагается списать."""
from app.ui.page_batches import PaymentDialog
batch = window.vault.doc.batches[0]
dialog = PaymentDialog(window, batch, suggested=850)
dialog.amount.set_decimal(800)
assert dialog.remainder == Decimal("50.00")
assert dialog.wants_write_off() is False
assert "останется долг" in dialog.hint.text().lower()
dialog.write_off.setChecked(True)
assert dialog.wants_write_off() is True
assert "скидку пекарни" in dialog.hint.text()
dialog.deleteLater()
def test_full_payment_leaves_nothing_to_write_off(window):
from app.ui.page_batches import PaymentDialog
batch = window.vault.doc.batches[0]
dialog = PaymentDialog(window, batch, suggested=850)
dialog.write_off.setChecked(True)
assert dialog.remainder == 0
assert dialog.wants_write_off() is False # списывать нечего
assert "закроется полностью" in dialog.hint.text()
dialog.deleteLater()
def test_write_off_closes_the_batch_end_to_end(window):
from app import ledger
from app import journal as j
batch = window.vault.doc.batches[0]
j.add_bakery_payment(window.vault, 800, batch_id=batch.id)
j.write_off_batch(window.vault, batch.id, 50)
window.changed()
report = window.report.batch_report(batch.id)
assert report.paid_to_bakery == Decimal("800.00")
assert report.discount == Decimal("50.00")
assert report.remaining_to_bakery == 0
assert report.status == ledger.STATUS_SETTLED
assert window.report.summary.owed_to_bakery == 0
def test_batches_table_shows_the_discount(window):
from app import journal as j
batch = window.vault.doc.batches[0]
j.write_off_batch(window.vault, batch.id, 50)
window.changed()
page = page_of(window, "Закупки")
page.refresh()
assert "50,00" in page.table.item(0, 6).text()
page.table.selectRow(0)
page._show_details()
assert "пекарня простила" in page.details_line.text()
def test_dashboard_mentions_forgiven_money(window):
from PySide6.QtWidgets import QLabel
from app import journal as j
batch = window.vault.doc.batches[0]
j.write_off_batch(window.vault, batch.id, 50)
window.changed()
page = page_of(window, "Сводка")
page.refresh()
texts = " ".join(label.text() for label in page.body.findChildren(QLabel))
assert "прощено 50,00 ₽" in texts.lower()
def test_stats_periods_show_the_discount(window):
from app import journal as j
batch = window.vault.doc.batches[0]
j.write_off_batch(window.vault, batch.id, 50)
window.changed()
page = page_of(window, "Статистика")
page.tabs.setCurrentIndex(0)
page.refresh()
tab = page.sections[0][1]
assert "50,00" in tab.table.item(0, tab.COLUMNS.index("Прощено")).text()
Раздел статистики Шесть разрезов: периоды (заданные вручную, то есть партии), недели, месяцы, годы, товары и люди. У каждого график и таблица с итоговой строкой. Главное решение — две базы расчёта одновременно. Смешивать «когда продал» и «когда получил деньги» нельзя: ответы расходятся, как только появляется хоть один долг. Выручка, наценка, съеденное и чаевые считаются по дате продажи, «Собрано» — по дате платежа, «Закуплено» и «Отдано пекарне» — по своим датам. У разреза «Периоды» основание третье, привязка к партии, потому что вопрос там другой: покрыл ли я её к сроку. Количество нигде не сводится в одно число — только по единицам, «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_stats_page_has_every_section(window):
page = page_of(window, "Статистика")
titles = [page.tabs.tabText(i) for i in range(page.tabs.count())]
assert titles == ["Периоды", "Недели", "Месяцы", "Годы", "Товары", "Люди"]
def test_stats_sections_all_render(window):
page = page_of(window, "Статистика")
for index in range(page.tabs.count()):
page.tabs.setCurrentIndex(index)
page.refresh()
assert page.sections[index][1].table.rowCount() >= 0
def test_stats_periods_show_the_batch(window):
page = page_of(window, "Статистика")
page.tabs.setCurrentIndex(0)
page.refresh()
tab = page.sections[0][1]
assert tab.table.rowCount() == 1
assert tab.table.item(0, 0).text() == "№1"
assert "850" in tab.table.item(0, 4).text() # долг по партии
assert "Периодов: 1" in tab.headline.text()
def test_stats_months_carry_a_totals_row(window):
"""Последняя строка — итог, иначе цифры приходится складывать в уме."""
page = page_of(window, "Статистика")
page.tabs.setCurrentIndex(2)
page.refresh()
tab = page.sections[2][1]
assert tab.table.rowCount() == len(tab.buckets) + 1
assert tab.table.item(tab.table.rowCount() - 1, 0).text() == "Итого"
def test_stats_metric_switch_redraws_the_chart(window):
page = page_of(window, "Статистика")
page.tabs.setCurrentIndex(2)
page.refresh()
tab = page.sections[2][1]
Цена по фасовке не подставлялась после удаления строки Обработчики строк запоминали номер строки в момент создания. После удаления любой строки всё, что было ниже, съезжает вверх, и запомненный номер начинает указывать на соседа или за пределы таблицы. Строка молча переставала работать целиком: выбираешь фасовку — цена не меняется, меняешь товар — не пересобирается список фасовок. Приходилось править цену руками. Теперь обработчики привязаны к самому виджету, а строка ищется по нему в момент вызова. То же исправлено в быстром вводе, где строк больше и удаляют их чаще. Заодно найдена мина, которую посадил я сам в разделе статистики: self.metric = QComboBox() на QWidget затеняет метод QWidget.metric(), который Qt зовёт при смене стиля. Падало это в чужом месте и с невнятным «object is not callable». Такая же история была с self.layout в сводке. Оба переименованы, добавлен тест, который обходит все виджеты и проверяет, что ни один атрибут не затеняет метод Qt. И подсказка в карточке товара: себестоимость фасовки нигде не вводится, она выводится из цены базовой единицы. Теперь под таблицей фасовок прямо написано, какая сумма подставится в закупку. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 21:50:42 +07:00
tab.metric_box.setCurrentIndex(1) # Выручка
Раздел статистики Шесть разрезов: периоды (заданные вручную, то есть партии), недели, месяцы, годы, товары и люди. У каждого график и таблица с итоговой строкой. Главное решение — две базы расчёта одновременно. Смешивать «когда продал» и «когда получил деньги» нельзя: ответы расходятся, как только появляется хоть один долг. Выручка, наценка, съеденное и чаевые считаются по дате продажи, «Собрано» — по дате платежа, «Закуплено» и «Отдано пекарне» — по своим датам. У разреза «Периоды» основание третье, привязка к партии, потому что вопрос там другой: покрыл ли я её к сроку. Количество нигде не сводится в одно число — только по единицам, «1,5 л · 4 шт». Сумма литров с килограммами выглядит как настоящая цифра, но смысла не имеет; ровно эту ошибку пришлось чинить и в разрезе по партиям. График на QPainter: QtCharts исключён из сборки, он тянет десятки мегабайт ради одной диаграммы. Убыток рисуется вниз от нулевой линии. Больше 24 корзин на график не помещается, и про срез написано над ним — молча показанная часть выглядела бы как всё. Тесты экранов больше не адресуют разделы по номеру в меню: новый раздел ломал половину проверок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 06:33:11 +07:00
assert "Выручка за всё время" in tab.headline.text()
assert len(tab.chart.bars) == len(tab.buckets)
def test_stats_products_rank_by_margin(window):
"""Таблица и график должны отвечать одинаково: сортировка по заработку."""
page = page_of(window, "Статистика")
page.tabs.setCurrentIndex(4)
page.refresh()
tab = page.sections[4][1]
names = [tab.table.item(r, 0).text() for r in range(tab.table.rowCount())]
assert names[0] == "Булка с повидлом" # единственная с наценкой
assert names[0] == tab.chart.bars[0].label
def test_stats_people_table_matches_its_chart(window):
page = page_of(window, "Статистика")
page.tabs.setCurrentIndex(5)
page.refresh()
tab = page.sections[5][1]
names = [tab.table.item(r, 0).text() for r in range(tab.table.rowCount())]
assert names == [bar.label for bar in tab.chart.bars]
def test_stats_people_show_the_debt(window):
page = page_of(window, "Статистика")
page.tabs.setCurrentIndex(5)
page.refresh()
tab = page.sections[5][1]
rows = {tab.table.item(r, 0).text(): r for r in range(tab.table.rowCount())}
assert "100,00 ₽" in tab.table.item(rows["Вася"], 5).text()
assert "должны в сумме: 100,00 ₽" in tab.headline.text()
def test_stats_survive_an_empty_database(qapp, vault):
"""Пустая база не должна ронять ни один разрез."""
from app.ui.main_window import MainWindow as MW
win = MW(vault)
win.autosave_timer.stop()
win.sync_timer.stop()
page = next(p for name, p in win.pages if name == "Статистика")
for index in range(page.tabs.count()):
page.tabs.setCurrentIndex(index)
page.refresh()
assert page.sections[index][1].table.rowCount() == 0
win.close()
win.deleteLater()
def test_chart_handles_losses(qapp):
"""Убыток — нормальная величина, столбик должен уходить вниз от нуля."""
from decimal import Decimal as D
from app.ui.chart import Bar, BarChart
chart = BarChart()
chart.set_bars([Bar("янв", D("100")), Bar("фев", D("-40")), Bar("мар", D("0"))])
chart.resize(400, 200)
chart.grab() # отрисовка не должна падать
assert len(chart.bars) == 3
chart.deleteLater()
def test_chart_handles_all_zeroes(qapp):
from decimal import Decimal as D
from app.ui.chart import Bar, BarChart
chart = BarChart()
chart.set_bars([Bar("янв", D("0")), Bar("фев", D("0"))])
chart.resize(400, 200)
chart.grab()
chart.deleteLater()
def test_empty_chart_says_so(qapp):
from app.ui.chart import BarChart
chart = BarChart()
chart.resize(400, 200)
chart.grab()
assert chart.bars == []
chart.deleteLater()
Чаевые Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела бы закрытой, когда за булки ещё должны. Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе поле дёргалось бы посреди набора. То же при приёме оплаты по долгу: заплатили больше, чем оставалось, — излишек становится чаевыми, а не теряется, как было раньше. В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят в выручку и не создают долга. Продажа, списанная с нескольких партий, делит чаевые между ними пропорционально выручке. По съеденному и подаренному чаевых не бывает — поле отключается. Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления этой фичи не заметит. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
# --- чаевые ---------------------------------------------------------------
def test_overpayment_moves_into_tips(window, buns):
"""Булка 70, дали 100 — разница должна уехать в чаевые сама."""
dialog = SaleDialog(window)
editor = dialog.lines
editor.table.cellWidget(0, editor.COL_PRODUCT).select_product(buns["мак"].id)
editor.table.cellWidget(0, editor.COL_QTY).set_decimal(2)
dialog.paid.set_decimal(100)
dialog._absorb_overpayment()
assert dialog.paid.value_decimal() == Decimal("70.00")
assert dialog.tip.value_decimal() == Decimal("30.00")
assert "чаевые" in dialog.paid_hint.text().lower()
dialog.deleteLater()
def test_underpayment_is_left_as_debt(window, buns):
"""Недоплата — это долг, а не отрицательные чаевые."""
dialog = SaleDialog(window)
editor = dialog.lines
editor.table.cellWidget(0, editor.COL_PRODUCT).select_product(buns["мак"].id)
editor.table.cellWidget(0, editor.COL_QTY).set_decimal(2)
dialog.paid.set_decimal(40)
dialog._absorb_overpayment()
assert dialog.paid.value_decimal() == Decimal("40.00")
assert dialog.tip.value_decimal() == 0
assert "долг" in dialog.paid_hint.text().lower()
dialog.deleteLater()
def test_consumption_disables_tips(window, buns):
dialog = SaleDialog(window)
dialog.kind.setCurrentIndex(2) # съел сам
assert dialog.tip.isEnabled() is False
assert dialog.tip.value_decimal() == 0
dialog.deleteLater()
def test_existing_sale_can_have_its_tip_edited(window, buns):
"""Сдачу могли оставить и после того, как продажу занесли."""
sale = window.vault.doc.sales[0]
journal.set_sale_tip(window.vault, sale.id, 15)
window.changed()
dialog = SaleDialog(window, sale)
assert dialog.tip.value_decimal() == Decimal("15.00")
assert dialog.paid is None # платежи правятся на экране долгов
dialog.deleteLater()
def test_sales_table_shows_tips(window, buns):
sale = window.vault.doc.sales[0]
journal.set_sale_tip(window.vault, sale.id, 25)
window.changed()
Раздел статистики Шесть разрезов: периоды (заданные вручную, то есть партии), недели, месяцы, годы, товары и люди. У каждого график и таблица с итоговой строкой. Главное решение — две базы расчёта одновременно. Смешивать «когда продал» и «когда получил деньги» нельзя: ответы расходятся, как только появляется хоть один долг. Выручка, наценка, съеденное и чаевые считаются по дате продажи, «Собрано» — по дате платежа, «Закуплено» и «Отдано пекарне» — по своим датам. У разреза «Периоды» основание третье, привязка к партии, потому что вопрос там другой: покрыл ли я её к сроку. Количество нигде не сводится в одно число — только по единицам, «1,5 л · 4 шт». Сумма литров с килограммами выглядит как настоящая цифра, но смысла не имеет; ровно эту ошибку пришлось чинить и в разрезе по партиям. График на QPainter: QtCharts исключён из сборки, он тянет десятки мегабайт ради одной диаграммы. Убыток рисуется вниз от нулевой линии. Больше 24 корзин на график не помещается, и про срез написано над ним — молча показанная часть выглядела бы как всё. Тесты экранов больше не адресуют разделы по номеру в меню: новый раздел ломал половину проверок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 06:33:11 +07:00
page = page_of(window, "Продажи")
Чаевые Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела бы закрытой, когда за булки ещё должны. Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе поле дёргалось бы посреди набора. То же при приёме оплаты по долгу: заплатили больше, чем оставалось, — излишек становится чаевыми, а не теряется, как было раньше. В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят в выручку и не создают долга. Продажа, списанная с нескольких партий, делит чаевые между ними пропорционально выручке. По съеденному и подаренному чаевых не бывает — поле отключается. Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления этой фичи не заметит. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
page.refresh()
tips = [page.table.item(r, 6).text() for r in range(page.table.rowCount())]
assert any("25,00" in t for t in tips)
# У продаж без чаевых колонка пустая, а не «0,00 ₽».
assert "" in tips
def test_dashboard_mentions_tips_only_when_there_are_any(window, buns):
from PySide6.QtWidgets import QLabel
Раздел статистики Шесть разрезов: периоды (заданные вручную, то есть партии), недели, месяцы, годы, товары и люди. У каждого график и таблица с итоговой строкой. Главное решение — две базы расчёта одновременно. Смешивать «когда продал» и «когда получил деньги» нельзя: ответы расходятся, как только появляется хоть один долг. Выручка, наценка, съеденное и чаевые считаются по дате продажи, «Собрано» — по дате платежа, «Закуплено» и «Отдано пекарне» — по своим датам. У разреза «Периоды» основание третье, привязка к партии, потому что вопрос там другой: покрыл ли я её к сроку. Количество нигде не сводится в одно число — только по единицам, «1,5 л · 4 шт». Сумма литров с килограммами выглядит как настоящая цифра, но смысла не имеет; ровно эту ошибку пришлось чинить и в разрезе по партиям. График на QPainter: QtCharts исключён из сборки, он тянет десятки мегабайт ради одной диаграммы. Убыток рисуется вниз от нулевой линии. Больше 24 корзин на график не помещается, и про срез написано над ним — молча показанная часть выглядела бы как всё. Тесты экранов больше не адресуют разделы по номеру в меню: новый раздел ломал половину проверок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 06:33:11 +07:00
page = page_of(window, "Сводка")
Чаевые Деньги сверх стоимости товара. Хранятся на продаже отдельным полем, а не внутри платежей: иначе чаевые раздували бы «оплачено», и продажа выглядела бы закрытой, когда за булки ещё должны. Ввёл оплату больше суммы — булка 70, дали 100 — разница уходит в чаевые сама. Разбор делается по окончании ввода, а не на каждом нажатии, иначе поле дёргалось бы посреди набора. То же при приёме оплаты по долгу: заплатили больше, чем оставалось, — излишек становится чаевыми, а не теряется, как было раньше. В расчётах чаевые идут в покрытие партии (это живые деньги, ими так же рассчитываются с пекарней) и целиком добавляются в прибыль, но не входят в выручку и не создают долга. Продажа, списанная с нескольких партий, делит чаевые между ними пропорционально выручке. По съеденному и подаренному чаевых не бывает — поле отключается. Миграция схемы 2→3 проставляет ноль: у кого чаевых нет, тот появления этой фичи не заметит. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 04:56:26 +07:00
page.refresh()
texts = " ".join(label.text() for label in page.body.findChildren(QLabel))
assert "чаевые" not in texts.lower()
journal.set_sale_tip(window.vault, window.vault.doc.sales[0].id, 40)
window.changed()
texts = " ".join(label.text() for label in page.body.findChildren(QLabel))
assert "чаевые 40,00 ₽" in texts.lower()
# --- дробные количества ---------------------------------------------------
def type_into(spin, text: str):
"""Набрать текст в поле так, как это делает пользователь."""
spin.clear()
spin.lineEdit().setText(text)
spin.interpretText()
return spin
def test_dot_is_accepted_as_decimal_separator(qapp):
"""На цифровой клавиатуре точка, а локаль русская — ждёт запятую.
Без нормализации символ молча не появлялся бы в поле, и выглядело бы это
так, будто дробные значения вводить нельзя вообще.
"""
from PySide6.QtGui import QValidator
from app.ui.widgets import MoneySpin, QtySpin
for spin in (QtySpin(), MoneySpin()):
for text in ("0,5", "0.5", "12.75", "12,75"):
state, fixed, _ = spin.validate(text, len(text))
assert state != QValidator.Invalid, f"{type(spin).__name__} отверг {text!r}"
assert "." not in fixed
spin.deleteLater()
def test_both_separators_give_the_same_value(qapp):
from app.ui.widgets import QtySpin
assert type_into(QtySpin(), "0.5").value_decimal() == Decimal("0.500")
assert type_into(QtySpin(), "0,5").value_decimal() == Decimal("0.500")
def test_group_separators_are_tolerated(qapp):
from app.ui.widgets import MoneySpin
assert type_into(MoneySpin(), "1 200,50").value_decimal() == Decimal("1200.50")
def test_letters_are_still_rejected(qapp):
from PySide6.QtGui import QValidator
from app.ui.widgets import QtySpin
state, _, _ = QtySpin().validate("abc", 3)
assert state == QValidator.Invalid
def test_fractional_line_round_trips_through_the_form(window):
"""Пол-литра сока, введённые в форме, должны дойти до документа."""
from app import journal as j
juice = j.create_product(window.vault, "Сок", unit="л", cost_price=80, retail_price=120)
window.changed()
dialog = SaleDialog(window)
editor = dialog.lines
editor.table.cellWidget(0, editor.COL_PRODUCT).select_product(juice.id)
type_into(editor.table.cellWidget(0, editor.COL_QTY), "0.5")
line = editor.lines("unit_price")[0]
assert line["qty"] == Decimal("0.500")
assert editor.total() == Decimal("60.00") # 0,5 × 120
dialog.deleteLater()
def test_fractional_pack_size_round_trips(window):
from app import journal as j
from app.ui.page_products import ProductDialog as PD
juice = j.create_product(window.vault, "Сок", unit="л", cost_price=80, retail_price=120)
j.set_packs(window.vault, juice.id, [{"name": "бутылка", "size": "1.5", "retail_price": 170}])
dialog = PD("", window.vault.doc.product(juice.id))
assert dialog.packs.packs()[0]["size"] == Decimal("1.500")
dialog.deleteLater()
def test_fractional_quantity_is_displayed_without_trailing_zeros(window):
from app import journal as j
from app.models import KIND_RETAIL as RETAIL
juice = j.create_product(window.vault, "Сок", unit="л", cost_price=80, retail_price=120)
j.create_sale(
window.vault, window.report.today, RETAIL,
[{"product_id": juice.id, "qty": "0.5", "unit_price": 120}],
)
window.changed()
Раздел статистики Шесть разрезов: периоды (заданные вручную, то есть партии), недели, месяцы, годы, товары и люди. У каждого график и таблица с итоговой строкой. Главное решение — две базы расчёта одновременно. Смешивать «когда продал» и «когда получил деньги» нельзя: ответы расходятся, как только появляется хоть один долг. Выручка, наценка, съеденное и чаевые считаются по дате продажи, «Собрано» — по дате платежа, «Закуплено» и «Отдано пекарне» — по своим датам. У разреза «Периоды» основание третье, привязка к партии, потому что вопрос там другой: покрыл ли я её к сроку. Количество нигде не сводится в одно число — только по единицам, «1,5 л · 4 шт». Сумма литров с килограммами выглядит как настоящая цифра, но смысла не имеет; ровно эту ошибку пришлось чинить и в разрезе по партиям. График на QPainter: QtCharts исключён из сборки, он тянет десятки мегабайт ради одной диаграммы. Убыток рисуется вниз от нулевой линии. Больше 24 корзин на график не помещается, и про срез написано над ним — молча показанная часть выглядела бы как всё. Тесты экранов больше не адресуют разделы по номеру в меню: новый раздел ломал половину проверок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 06:33:11 +07:00
page = page_of(window, "Продажи")
page.refresh()
texts = [page.table.item(r, 3).text() for r in range(page.table.rowCount())]
assert any("Сок × 0,5 л" in t for t in texts)
# --- настройки ------------------------------------------------------------
def test_settings_shows_database_facts(window):
Раздел статистики Шесть разрезов: периоды (заданные вручную, то есть партии), недели, месяцы, годы, товары и люди. У каждого график и таблица с итоговой строкой. Главное решение — две базы расчёта одновременно. Смешивать «когда продал» и «когда получил деньги» нельзя: ответы расходятся, как только появляется хоть один долг. Выручка, наценка, съеденное и чаевые считаются по дате продажи, «Собрано» — по дате платежа, «Закуплено» и «Отдано пекарне» — по своим датам. У разреза «Периоды» основание третье, привязка к партии, потому что вопрос там другой: покрыл ли я её к сроку. Количество нигде не сводится в одно число — только по единицам, «1,5 л · 4 шт». Сумма литров с килограммами выглядит как настоящая цифра, но смысла не имеет; ровно эту ошибку пришлось чинить и в разрезе по партиям. График на QPainter: QtCharts исключён из сборки, он тянет десятки мегабайт ради одной диаграммы. Убыток рисуется вниз от нулевой линии. Больше 24 корзин на график не помещается, и про срез написано над ним — молча показанная часть выглядела бы как всё. Тесты экранов больше не адресуют разделы по номеру в меню: новый раздел ломал половину проверок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 06:33:11 +07:00
page = page_of(window, "Настройки")
page.refresh()
assert "товаров 3" in page.db_info.text()
assert "закупок 1" in page.db_info.text()
def test_settings_saves_git_without_wiping_the_token(window):
"""Пустое поле токена означает «не трогать», а не «стереть»."""
Раздел статистики Шесть разрезов: периоды (заданные вручную, то есть партии), недели, месяцы, годы, товары и люди. У каждого график и таблица с итоговой строкой. Главное решение — две базы расчёта одновременно. Смешивать «когда продал» и «когда получил деньги» нельзя: ответы расходятся, как только появляется хоть один долг. Выручка, наценка, съеденное и чаевые считаются по дате продажи, «Собрано» — по дате платежа, «Закуплено» и «Отдано пекарне» — по своим датам. У разреза «Периоды» основание третье, привязка к партии, потому что вопрос там другой: покрыл ли я её к сроку. Количество нигде не сводится в одно число — только по единицам, «1,5 л · 4 шт». Сумма литров с килограммами выглядит как настоящая цифра, но смысла не имеет; ровно эту ошибку пришлось чинить и в разрезе по партиям. График на QPainter: QtCharts исключён из сборки, он тянет десятки мегабайт ради одной диаграммы. Убыток рисуется вниз от нулевой линии. Больше 24 корзин на график не помещается, и про срез написано над ним — молча показанная часть выглядела бы как всё. Тесты экранов больше не адресуют разделы по номеру в меню: новый раздел ломал половину проверок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 06:33:11 +07:00
page = page_of(window, "Настройки")
journal.update_settings(window.vault, git={"token": "glpat-keepme"})
page.refresh()
page.remote.setText("https://gt.ser.gay/kizya/food-market.git")
page.token.clear()
page.save_git()
assert window.vault.doc.settings.git.token == "glpat-keepme"
assert window.vault.doc.settings.git.remote_url == "https://gt.ser.gay/kizya/food-market.git"
def test_conflict_block_is_hidden_until_there_is_one(window):
Раздел статистики Шесть разрезов: периоды (заданные вручную, то есть партии), недели, месяцы, годы, товары и люди. У каждого график и таблица с итоговой строкой. Главное решение — две базы расчёта одновременно. Смешивать «когда продал» и «когда получил деньги» нельзя: ответы расходятся, как только появляется хоть один долг. Выручка, наценка, съеденное и чаевые считаются по дате продажи, «Собрано» — по дате платежа, «Закуплено» и «Отдано пекарне» — по своим датам. У разреза «Периоды» основание третье, привязка к партии, потому что вопрос там другой: покрыл ли я её к сроку. Количество нигде не сводится в одно число — только по единицам, «1,5 л · 4 шт». Сумма литров с килограммами выглядит как настоящая цифра, но смысла не имеет; ровно эту ошибку пришлось чинить и в разрезе по партиям. График на QPainter: QtCharts исключён из сборки, он тянет десятки мегабайт ради одной диаграммы. Убыток рисуется вниз от нулевой линии. Больше 24 корзин на график не помещается, и про срез написано над ним — молча показанная часть выглядела бы как всё. Тесты экранов больше не адресуют разделы по номеру в меню: новый раздел ломал половину проверок. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 06:33:11 +07:00
page = page_of(window, "Настройки")
page.refresh()
assert page.conflict_box.isVisibleTo(page) is False
window.conflict_message = "версии разошлись"
page.refresh()
assert page.conflict_box.isVisibleTo(page) is True