2026-08-22 21:29:53 +07:00
|
|
|
|
"""Закупки: партии булок, взятых в пекарне в долг, и их покрытие."""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
from datetime import timedelta
|
|
|
|
|
|
|
|
|
|
|
|
from PySide6.QtCore import Qt
|
Пекарня может простить остаток
Надо было отдать 5 500, взяли 5 000, полтысячи оставили. Долг закрыт,
но денег никто не отдавал.
У расчёта с пекарней появился вид: деньги или прощённый остаток. Свалить
их в одну кучу нельзя — тогда «Отдано пекарне» в статистике показывало бы
суммы, которых не платил. Поэтому «Отдано» — только живые деньги,
«Прощено» — отдельная колонка везде: в закупках, в сводке, в статистике
по периодам и по времени.
В прибыль прощённое идёт целиком: эти деньги предназначались пекарне,
а остались у тебя. «Осталось собрать» на ту же сумму уменьшается —
собирать под прощённый остаток уже не надо. Простить больше долга нельзя:
лишнее не засчитывается и, в отличие от переплаты деньгами, никуда
не переливается.
В диалоге платежа галочка «Остаток простили — закрыть партию»: вводишь
сколько отдал, остаток уходит отдельной записью, партия закрывается сама,
потому что её долг становится нулём.
Миграция схемы 3→4 помечает всё записанное раньше как живые деньги.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 06:44:19 +07:00
|
|
|
|
from decimal import Decimal
|
|
|
|
|
|
|
2026-08-22 21:29:53 +07:00
|
|
|
|
from PySide6.QtWidgets import (
|
Пекарня может простить остаток
Надо было отдать 5 500, взяли 5 000, полтысячи оставили. Долг закрыт,
но денег никто не отдавал.
У расчёта с пекарней появился вид: деньги или прощённый остаток. Свалить
их в одну кучу нельзя — тогда «Отдано пекарне» в статистике показывало бы
суммы, которых не платил. Поэтому «Отдано» — только живые деньги,
«Прощено» — отдельная колонка везде: в закупках, в сводке, в статистике
по периодам и по времени.
В прибыль прощённое идёт целиком: эти деньги предназначались пекарне,
а остались у тебя. «Осталось собрать» на ту же сумму уменьшается —
собирать под прощённый остаток уже не надо. Простить больше долга нельзя:
лишнее не засчитывается и, в отличие от переплаты деньгами, никуда
не переливается.
В диалоге платежа галочка «Остаток простили — закрыть партию»: вводишь
сколько отдал, остаток уходит отдельной записью, партия закрывается сама,
потому что её долг становится нулём.
Миграция схемы 3→4 помечает всё записанное раньше как живые деньги.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 06:44:19 +07:00
|
|
|
|
QCheckBox,
|
2026-08-22 21:29:53 +07:00
|
|
|
|
QDialog,
|
|
|
|
|
|
QFormLayout,
|
|
|
|
|
|
QHBoxLayout,
|
|
|
|
|
|
QLineEdit,
|
|
|
|
|
|
QMessageBox,
|
|
|
|
|
|
QProgressBar,
|
|
|
|
|
|
QSplitter,
|
|
|
|
|
|
QVBoxLayout,
|
|
|
|
|
|
QWidget,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
from .. import journal, ledger
|
|
|
|
|
|
from .. import money as m
|
|
|
|
|
|
from . import theme
|
|
|
|
|
|
from . import widgets as w
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BatchDialog(QDialog):
|
|
|
|
|
|
"""Форма партии: что взял, почём и до какого числа надо рассчитаться."""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self, ctx, batch=None, parent=None):
|
|
|
|
|
|
super().__init__(parent)
|
|
|
|
|
|
self.ctx = ctx
|
|
|
|
|
|
self.batch = batch
|
|
|
|
|
|
self.setWindowTitle(f"Закупка №{batch.number}" if batch else "Новая закупка")
|
|
|
|
|
|
self.setMinimumSize(640, 480)
|
|
|
|
|
|
|
|
|
|
|
|
layout = QVBoxLayout(self)
|
|
|
|
|
|
layout.setContentsMargins(20, 16, 20, 16)
|
|
|
|
|
|
layout.setSpacing(10)
|
|
|
|
|
|
|
|
|
|
|
|
form = QFormLayout()
|
|
|
|
|
|
form.setSpacing(8)
|
|
|
|
|
|
|
|
|
|
|
|
self.date = w.DateInput(batch.date if batch else None)
|
|
|
|
|
|
self.date.dateChanged.connect(self._shift_due_date)
|
|
|
|
|
|
form.addRow("Дата закупки", self.date)
|
|
|
|
|
|
|
|
|
|
|
|
self.due = w.DateInput(
|
|
|
|
|
|
batch.due_date
|
|
|
|
|
|
if batch
|
|
|
|
|
|
else self.date.get_date() + timedelta(days=ctx.vault.doc.settings.default_period_days)
|
|
|
|
|
|
)
|
|
|
|
|
|
form.addRow("Покрыть до", self.due)
|
|
|
|
|
|
|
|
|
|
|
|
self.note = QLineEdit(batch.note if batch else "")
|
|
|
|
|
|
self.note.setPlaceholderText("договорились по телефону")
|
|
|
|
|
|
form.addRow("Заметка", self.note)
|
|
|
|
|
|
layout.addLayout(form)
|
|
|
|
|
|
|
|
|
|
|
|
layout.addWidget(w.label("Что взял", "h2"))
|
|
|
|
|
|
products = [p for p in ctx.vault.doc.products if not p.archived]
|
|
|
|
|
|
self.lines = w.LinesEditor(
|
Отдельный репозиторий для базы, фасовки товара, ввод истории
Три правки по замечаниям.
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
|
|
|
|
products, "Себестоимость", lambda p, uom: p.cost_for(uom), ctx.currency
|
2026-08-22 21:29:53 +07:00
|
|
|
|
)
|
|
|
|
|
|
if batch:
|
|
|
|
|
|
self.lines.set_lines(batch.lines, "unit_cost")
|
|
|
|
|
|
elif products:
|
|
|
|
|
|
self.lines.add_line()
|
|
|
|
|
|
layout.addWidget(self.lines, 1)
|
|
|
|
|
|
|
|
|
|
|
|
if not products:
|
|
|
|
|
|
layout.addWidget(
|
|
|
|
|
|
w.label("Сначала заведи товары на вкладке «Товары».", "dim", theme.WARN)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
layout.addWidget(
|
|
|
|
|
|
w.row(None, w.button("Отмена", on_click=self.reject), w.button("Сохранить", "primary", self.accept))
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _shift_due_date(self) -> None:
|
|
|
|
|
|
"""При смене даты закупки двигаем дедлайн, сохраняя длину периода."""
|
|
|
|
|
|
if self.batch is not None:
|
|
|
|
|
|
return
|
|
|
|
|
|
days = self.ctx.vault.doc.settings.default_period_days
|
|
|
|
|
|
self.due.set_date(self.date.get_date() + timedelta(days=days))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PaymentDialog(QDialog):
|
Пекарня может простить остаток
Надо было отдать 5 500, взяли 5 000, полтысячи оставили. Долг закрыт,
но денег никто не отдавал.
У расчёта с пекарней появился вид: деньги или прощённый остаток. Свалить
их в одну кучу нельзя — тогда «Отдано пекарне» в статистике показывало бы
суммы, которых не платил. Поэтому «Отдано» — только живые деньги,
«Прощено» — отдельная колонка везде: в закупках, в сводке, в статистике
по периодам и по времени.
В прибыль прощённое идёт целиком: эти деньги предназначались пекарне,
а остались у тебя. «Осталось собрать» на ту же сумму уменьшается —
собирать под прощённый остаток уже не надо. Простить больше долга нельзя:
лишнее не засчитывается и, в отличие от переплаты деньгами, никуда
не переливается.
В диалоге платежа галочка «Остаток простили — закрыть партию»: вводишь
сколько отдал, остаток уходит отдельной записью, партия закрывается сама,
потому что её долг становится нулём.
Миграция схемы 3→4 помечает всё записанное раньше как живые деньги.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 06:44:19 +07:00
|
|
|
|
"""Расчёт с пекарней: сколько отдал и что делать с остатком."""
|
2026-08-22 21:29:53 +07:00
|
|
|
|
|
|
|
|
|
|
def __init__(self, ctx, batch=None, suggested=None, parent=None):
|
|
|
|
|
|
super().__init__(parent)
|
Пекарня может простить остаток
Надо было отдать 5 500, взяли 5 000, полтысячи оставили. Долг закрыт,
но денег никто не отдавал.
У расчёта с пекарней появился вид: деньги или прощённый остаток. Свалить
их в одну кучу нельзя — тогда «Отдано пекарне» в статистике показывало бы
суммы, которых не платил. Поэтому «Отдано» — только живые деньги,
«Прощено» — отдельная колонка везде: в закупках, в сводке, в статистике
по периодам и по времени.
В прибыль прощённое идёт целиком: эти деньги предназначались пекарне,
а остались у тебя. «Осталось собрать» на ту же сумму уменьшается —
собирать под прощённый остаток уже не надо. Простить больше долга нельзя:
лишнее не засчитывается и, в отличие от переплаты деньгами, никуда
не переливается.
В диалоге платежа галочка «Остаток простили — закрыть партию»: вводишь
сколько отдал, остаток уходит отдельной записью, партия закрывается сама,
потому что её долг становится нулём.
Миграция схемы 3→4 помечает всё записанное раньше как живые деньги.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 06:44:19 +07:00
|
|
|
|
self.ctx = ctx
|
|
|
|
|
|
self.batch = batch
|
|
|
|
|
|
self.owed = m.money(suggested or 0)
|
2026-08-22 21:29:53 +07:00
|
|
|
|
self.setWindowTitle("Платёж пекарне")
|
Пекарня может простить остаток
Надо было отдать 5 500, взяли 5 000, полтысячи оставили. Долг закрыт,
но денег никто не отдавал.
У расчёта с пекарней появился вид: деньги или прощённый остаток. Свалить
их в одну кучу нельзя — тогда «Отдано пекарне» в статистике показывало бы
суммы, которых не платил. Поэтому «Отдано» — только живые деньги,
«Прощено» — отдельная колонка везде: в закупках, в сводке, в статистике
по периодам и по времени.
В прибыль прощённое идёт целиком: эти деньги предназначались пекарне,
а остались у тебя. «Осталось собрать» на ту же сумму уменьшается —
собирать под прощённый остаток уже не надо. Простить больше долга нельзя:
лишнее не засчитывается и, в отличие от переплаты деньгами, никуда
не переливается.
В диалоге платежа галочка «Остаток простили — закрыть партию»: вводишь
сколько отдал, остаток уходит отдельной записью, партия закрывается сама,
потому что её долг становится нулём.
Миграция схемы 3→4 помечает всё записанное раньше как живые деньги.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 06:44:19 +07:00
|
|
|
|
self.setMinimumWidth(430)
|
2026-08-22 21:29:53 +07:00
|
|
|
|
|
|
|
|
|
|
layout = QVBoxLayout(self)
|
|
|
|
|
|
layout.setContentsMargins(20, 16, 20, 16)
|
|
|
|
|
|
layout.setSpacing(10)
|
|
|
|
|
|
|
|
|
|
|
|
form = QFormLayout()
|
|
|
|
|
|
form.setSpacing(8)
|
|
|
|
|
|
|
|
|
|
|
|
self.date = w.DateInput()
|
|
|
|
|
|
form.addRow("Дата", self.date)
|
|
|
|
|
|
|
|
|
|
|
|
self.amount = w.MoneySpin(ctx.currency)
|
|
|
|
|
|
if suggested is not None:
|
|
|
|
|
|
self.amount.set_decimal(suggested)
|
Пекарня может простить остаток
Надо было отдать 5 500, взяли 5 000, полтысячи оставили. Долг закрыт,
но денег никто не отдавал.
У расчёта с пекарней появился вид: деньги или прощённый остаток. Свалить
их в одну кучу нельзя — тогда «Отдано пекарне» в статистике показывало бы
суммы, которых не платил. Поэтому «Отдано» — только живые деньги,
«Прощено» — отдельная колонка везде: в закупках, в сводке, в статистике
по периодам и по времени.
В прибыль прощённое идёт целиком: эти деньги предназначались пекарне,
а остались у тебя. «Осталось собрать» на ту же сумму уменьшается —
собирать под прощённый остаток уже не надо. Простить больше долга нельзя:
лишнее не засчитывается и, в отличие от переплаты деньгами, никуда
не переливается.
В диалоге платежа галочка «Остаток простили — закрыть партию»: вводишь
сколько отдал, остаток уходит отдельной записью, партия закрывается сама,
потому что её долг становится нулём.
Миграция схемы 3→4 помечает всё записанное раньше как живые деньги.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 06:44:19 +07:00
|
|
|
|
self.amount.valueChanged.connect(lambda _: self._update_hint())
|
|
|
|
|
|
form.addRow("Отдал", self.amount)
|
2026-08-22 21:29:53 +07:00
|
|
|
|
|
|
|
|
|
|
self.note = QLineEdit()
|
|
|
|
|
|
form.addRow("Заметка", self.note)
|
|
|
|
|
|
layout.addLayout(form)
|
|
|
|
|
|
|
Пекарня может простить остаток
Надо было отдать 5 500, взяли 5 000, полтысячи оставили. Долг закрыт,
но денег никто не отдавал.
У расчёта с пекарней появился вид: деньги или прощённый остаток. Свалить
их в одну кучу нельзя — тогда «Отдано пекарне» в статистике показывало бы
суммы, которых не платил. Поэтому «Отдано» — только живые деньги,
«Прощено» — отдельная колонка везде: в закупках, в сводке, в статистике
по периодам и по времени.
В прибыль прощённое идёт целиком: эти деньги предназначались пекарне,
а остались у тебя. «Осталось собрать» на ту же сумму уменьшается —
собирать под прощённый остаток уже не надо. Простить больше долга нельзя:
лишнее не засчитывается и, в отличие от переплаты деньгами, никуда
не переливается.
В диалоге платежа галочка «Остаток простили — закрыть партию»: вводишь
сколько отдал, остаток уходит отдельной записью, партия закрывается сама,
потому что её долг становится нулём.
Миграция схемы 3→4 помечает всё записанное раньше как живые деньги.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 06:44:19 +07:00
|
|
|
|
# Пекарня может взять меньше, чем причиталось: надо было 5 500, взяли
|
|
|
|
|
|
# 5 000. Долг при этом закрыт, но денег никто не отдавал — поэтому
|
|
|
|
|
|
# остаток списывается отдельной записью, а не подгонкой суммы.
|
|
|
|
|
|
self.write_off = QCheckBox("Остаток простили — закрыть партию")
|
|
|
|
|
|
self.write_off.toggled.connect(lambda _: self._update_hint())
|
2026-08-22 21:29:53 +07:00
|
|
|
|
if batch is not None:
|
Пекарня может простить остаток
Надо было отдать 5 500, взяли 5 000, полтысячи оставили. Долг закрыт,
но денег никто не отдавал.
У расчёта с пекарней появился вид: деньги или прощённый остаток. Свалить
их в одну кучу нельзя — тогда «Отдано пекарне» в статистике показывало бы
суммы, которых не платил. Поэтому «Отдано» — только живые деньги,
«Прощено» — отдельная колонка везде: в закупках, в сводке, в статистике
по периодам и по времени.
В прибыль прощённое идёт целиком: эти деньги предназначались пекарне,
а остались у тебя. «Осталось собрать» на ту же сумму уменьшается —
собирать под прощённый остаток уже не надо. Простить больше долга нельзя:
лишнее не засчитывается и, в отличие от переплаты деньгами, никуда
не переливается.
В диалоге платежа галочка «Остаток простили — закрыть партию»: вводишь
сколько отдал, остаток уходит отдельной записью, партия закрывается сама,
потому что её долг становится нулём.
Миграция схемы 3→4 помечает всё записанное раньше как живые деньги.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 06:44:19 +07:00
|
|
|
|
layout.addWidget(self.write_off)
|
|
|
|
|
|
|
|
|
|
|
|
self.hint = w.label("", "dim")
|
|
|
|
|
|
self.hint.setWordWrap(True)
|
|
|
|
|
|
layout.addWidget(self.hint)
|
2026-08-22 21:29:53 +07:00
|
|
|
|
|
|
|
|
|
|
layout.addWidget(
|
|
|
|
|
|
w.row(None, w.button("Отмена", on_click=self.reject), w.button("Записать", "primary", self.accept))
|
|
|
|
|
|
)
|
Пекарня может простить остаток
Надо было отдать 5 500, взяли 5 000, полтысячи оставили. Долг закрыт,
но денег никто не отдавал.
У расчёта с пекарней появился вид: деньги или прощённый остаток. Свалить
их в одну кучу нельзя — тогда «Отдано пекарне» в статистике показывало бы
суммы, которых не платил. Поэтому «Отдано» — только живые деньги,
«Прощено» — отдельная колонка везде: в закупках, в сводке, в статистике
по периодам и по времени.
В прибыль прощённое идёт целиком: эти деньги предназначались пекарне,
а остались у тебя. «Осталось собрать» на ту же сумму уменьшается —
собирать под прощённый остаток уже не надо. Простить больше долга нельзя:
лишнее не засчитывается и, в отличие от переплаты деньгами, никуда
не переливается.
В диалоге платежа галочка «Остаток простили — закрыть партию»: вводишь
сколько отдал, остаток уходит отдельной записью, партия закрывается сама,
потому что её долг становится нулём.
Миграция схемы 3→4 помечает всё записанное раньше как живые деньги.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 06:44:19 +07:00
|
|
|
|
self._update_hint()
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def remainder(self) -> Decimal:
|
|
|
|
|
|
"""Сколько останется непогашенным после этого платежа."""
|
|
|
|
|
|
return max(m.ZERO, m.money(self.owed - self.amount.value_decimal()))
|
|
|
|
|
|
|
|
|
|
|
|
def wants_write_off(self) -> bool:
|
|
|
|
|
|
return (
|
|
|
|
|
|
self.batch is not None
|
|
|
|
|
|
and self.write_off.isChecked()
|
|
|
|
|
|
and self.remainder > 0
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _update_hint(self) -> None:
|
|
|
|
|
|
currency = self.ctx.currency
|
|
|
|
|
|
if self.batch is None:
|
|
|
|
|
|
self.hint.setText("Без выбранной партии платёж закроет самые старые долги.")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
left = self.remainder
|
|
|
|
|
|
if left <= 0:
|
|
|
|
|
|
self.hint.setText(f"Партия №{self.batch.number} закроется полностью.")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
if self.write_off.isChecked():
|
|
|
|
|
|
self.hint.setText(
|
|
|
|
|
|
f"Останется {m.fmt_money(left, currency)} — спишем как скидку пекарни. "
|
|
|
|
|
|
"Партия закроется, а эти деньги попадут в прибыль и в статистику."
|
|
|
|
|
|
)
|
|
|
|
|
|
else:
|
|
|
|
|
|
self.hint.setText(
|
|
|
|
|
|
f"По партии №{self.batch.number} останется долг "
|
|
|
|
|
|
f"{m.fmt_money(left, currency)}."
|
|
|
|
|
|
)
|
2026-08-22 21:29:53 +07:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BatchesPage(QWidget):
|
|
|
|
|
|
def __init__(self, ctx, parent=None):
|
|
|
|
|
|
super().__init__(parent)
|
|
|
|
|
|
self.ctx = ctx
|
|
|
|
|
|
|
|
|
|
|
|
layout = QVBoxLayout(self)
|
|
|
|
|
|
layout.setContentsMargins(20, 18, 20, 18)
|
|
|
|
|
|
layout.setSpacing(12)
|
|
|
|
|
|
|
|
|
|
|
|
layout.addWidget(w.heading("Закупки"))
|
|
|
|
|
|
layout.addWidget(
|
|
|
|
|
|
w.row(
|
|
|
|
|
|
w.button("+ Закупка", "primary", self.create),
|
|
|
|
|
|
w.button("Изменить", on_click=self.edit),
|
|
|
|
|
|
w.button("Платёж пекарне", on_click=self.pay),
|
|
|
|
|
|
w.button("Закрыть / открыть", on_click=self.toggle_closed),
|
|
|
|
|
|
w.button("Удалить", "danger", self.delete),
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
splitter = QSplitter(Qt.Vertical)
|
|
|
|
|
|
|
|
|
|
|
|
self.table = w.table(
|
Пекарня может простить остаток
Надо было отдать 5 500, взяли 5 000, полтысячи оставили. Долг закрыт,
но денег никто не отдавал.
У расчёта с пекарней появился вид: деньги или прощённый остаток. Свалить
их в одну кучу нельзя — тогда «Отдано пекарне» в статистике показывало бы
суммы, которых не платил. Поэтому «Отдано» — только живые деньги,
«Прощено» — отдельная колонка везде: в закупках, в сводке, в статистике
по периодам и по времени.
В прибыль прощённое идёт целиком: эти деньги предназначались пекарне,
а остались у тебя. «Осталось собрать» на ту же сумму уменьшается —
собирать под прощённый остаток уже не надо. Простить больше долга нельзя:
лишнее не засчитывается и, в отличие от переплаты деньгами, никуда
не переливается.
В диалоге платежа галочка «Остаток простили — закрыть партию»: вводишь
сколько отдал, остаток уходит отдельной записью, партия закрывается сама,
потому что её долг становится нулём.
Миграция схемы 3→4 помечает всё записанное раньше как живые деньги.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 06:44:19 +07:00
|
|
|
|
["№", "Дата", "Покрыть до", "Осталось", "Долг", "Отдано",
|
|
|
|
|
|
"Прощено", "Собрано", "Покрытие", "Статус"],
|
|
|
|
|
|
stretch_column=9,
|
2026-08-22 21:29:53 +07:00
|
|
|
|
)
|
|
|
|
|
|
self.table.itemSelectionChanged.connect(self._show_details)
|
|
|
|
|
|
self.table.doubleClicked.connect(self.edit)
|
|
|
|
|
|
splitter.addWidget(self.table)
|
|
|
|
|
|
|
|
|
|
|
|
self.details = self._build_details()
|
|
|
|
|
|
splitter.addWidget(self.details)
|
|
|
|
|
|
splitter.setSizes([420, 260])
|
|
|
|
|
|
layout.addWidget(splitter, 1)
|
|
|
|
|
|
|
|
|
|
|
|
def _build_details(self) -> QWidget:
|
|
|
|
|
|
holder = QWidget()
|
|
|
|
|
|
layout = QVBoxLayout(holder)
|
|
|
|
|
|
layout.setContentsMargins(0, 8, 0, 0)
|
|
|
|
|
|
layout.setSpacing(8)
|
|
|
|
|
|
|
|
|
|
|
|
self.details_title = w.label("Выбери партию", "h2")
|
|
|
|
|
|
layout.addWidget(self.details_title)
|
|
|
|
|
|
|
|
|
|
|
|
self.progress = QProgressBar()
|
|
|
|
|
|
self.progress.setRange(0, 100)
|
|
|
|
|
|
layout.addWidget(self.progress)
|
|
|
|
|
|
|
|
|
|
|
|
self.details_line = w.label("", "dim")
|
|
|
|
|
|
self.details_line.setWordWrap(True)
|
|
|
|
|
|
layout.addWidget(self.details_line)
|
|
|
|
|
|
|
|
|
|
|
|
self.contents = w.table(["Товар", "Взято", "Осталось", "Себестоимость", "Сумма"])
|
|
|
|
|
|
layout.addWidget(self.contents, 1)
|
|
|
|
|
|
return holder
|
|
|
|
|
|
|
|
|
|
|
|
# --- отрисовка ---
|
|
|
|
|
|
|
|
|
|
|
|
def refresh(self) -> None:
|
|
|
|
|
|
currency = self.ctx.currency
|
|
|
|
|
|
rows, keys = [], []
|
|
|
|
|
|
|
|
|
|
|
|
# Свежие партии сверху: они и есть текущая работа.
|
|
|
|
|
|
for report in sorted(
|
|
|
|
|
|
self.ctx.report.batches, key=lambda r: (r.batch.date, r.batch.number), reverse=True
|
|
|
|
|
|
):
|
|
|
|
|
|
batch = report.batch
|
|
|
|
|
|
color = theme.STATUS_COLORS.get(report.status, "")
|
|
|
|
|
|
days = (
|
|
|
|
|
|
"—"
|
|
|
|
|
|
if report.status == ledger.STATUS_SETTLED
|
|
|
|
|
|
else f"{report.days_left} дн."
|
|
|
|
|
|
if report.days_left >= 0
|
|
|
|
|
|
else f"просрочено на {-report.days_left} дн."
|
|
|
|
|
|
)
|
|
|
|
|
|
rows.append(
|
|
|
|
|
|
[
|
|
|
|
|
|
w.sortable_num_item(str(batch.number), batch.number),
|
Фильтры и сортировка на экране продаж
Список продаж растёт быстрее всех остальных, а отбор был один — по типу.
Теперь панель: поиск, период, тип, человек, состояние оплаты. Всё
складывается, «за прошлый месяц Васе в долг» набирается тремя щелчками.
Правила отбора вынесены в app/salesfilter.py и живут без Qt: «прошлый
месяц» в январе уезжает в прошлый год, а поиск по двум словам ищет их по
отдельности — такое проверяется тестом, а не глазами по скриншоту.
Под таблицей — итоги ровно по показанному. Половина смысла фильтров
в этой строке: отобрал по человеку и сразу видишь, сколько он взял
и сколько за ним осталось.
Заодно починена сортировка по датам на продажах и закупках.
Отформатированная дата — обычная строка, и по алфавиту она сортируется
по дню месяца: 01.12.2025 оказывалась раньше 02.01.2020. Выглядело
рабочим, просто выдавало бессмыслицу.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 22:58:52 +07:00
|
|
|
|
w.date_item(batch.date),
|
|
|
|
|
|
w.date_item(batch.due_date),
|
2026-08-22 21:29:53 +07:00
|
|
|
|
w.text_item(days, color),
|
|
|
|
|
|
w.sortable_num_item(m.fmt_money(report.cost_total, currency), report.cost_total),
|
|
|
|
|
|
w.sortable_num_item(m.fmt_money(report.paid_to_bakery, currency), report.paid_to_bakery),
|
Пекарня может простить остаток
Надо было отдать 5 500, взяли 5 000, полтысячи оставили. Долг закрыт,
но денег никто не отдавал.
У расчёта с пекарней появился вид: деньги или прощённый остаток. Свалить
их в одну кучу нельзя — тогда «Отдано пекарне» в статистике показывало бы
суммы, которых не платил. Поэтому «Отдано» — только живые деньги,
«Прощено» — отдельная колонка везде: в закупках, в сводке, в статистике
по периодам и по времени.
В прибыль прощённое идёт целиком: эти деньги предназначались пекарне,
а остались у тебя. «Осталось собрать» на ту же сумму уменьшается —
собирать под прощённый остаток уже не надо. Простить больше долга нельзя:
лишнее не засчитывается и, в отличие от переплаты деньгами, никуда
не переливается.
В диалоге платежа галочка «Остаток простили — закрыть партию»: вводишь
сколько отдал, остаток уходит отдельной записью, партия закрывается сама,
потому что её долг становится нулём.
Миграция схемы 3→4 помечает всё записанное раньше как живые деньги.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 06:44:19 +07:00
|
|
|
|
w.sortable_num_item(
|
|
|
|
|
|
m.fmt_money(report.discount, currency) if report.discount else "",
|
|
|
|
|
|
report.discount,
|
|
|
|
|
|
theme.OK if report.discount else "",
|
|
|
|
|
|
),
|
2026-08-22 21:29:53 +07:00
|
|
|
|
w.sortable_num_item(m.fmt_money(report.cash_collected, currency), report.cash_collected),
|
|
|
|
|
|
w.sortable_num_item(f"{report.coverage_pct}%", report.coverage_pct, color),
|
|
|
|
|
|
w.text_item(ledger.STATUS_LABELS.get(report.status, ""), color),
|
|
|
|
|
|
]
|
|
|
|
|
|
)
|
|
|
|
|
|
keys.append(batch.id)
|
|
|
|
|
|
|
|
|
|
|
|
w.fill(self.table, rows, keys)
|
|
|
|
|
|
self._show_details()
|
|
|
|
|
|
|
|
|
|
|
|
def _show_details(self) -> None:
|
|
|
|
|
|
report = self._selected_report(quiet=True)
|
|
|
|
|
|
if report is None:
|
|
|
|
|
|
self.details_title.setText("Выбери партию")
|
|
|
|
|
|
self.details_line.setText("")
|
|
|
|
|
|
self.progress.setValue(0)
|
|
|
|
|
|
self.contents.setRowCount(0)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
currency = self.ctx.currency
|
|
|
|
|
|
color = theme.STATUS_COLORS.get(report.status, theme.ACCENT)
|
|
|
|
|
|
|
|
|
|
|
|
self.details_title.setText(f"Партия {report.label}")
|
|
|
|
|
|
self.progress.setValue(int(min(100, max(0, report.coverage_pct))))
|
|
|
|
|
|
self.progress.setFormat(f"собрано {report.coverage_pct}% себестоимости")
|
|
|
|
|
|
self.progress.setStyleSheet(f"QProgressBar::chunk {{ background: {color}; }}")
|
|
|
|
|
|
|
|
|
|
|
|
parts = [
|
|
|
|
|
|
f"Долг пекарне: {m.fmt_money(report.remaining_to_bakery, currency)} "
|
|
|
|
|
|
f"из {m.fmt_money(report.cost_total, currency)}",
|
|
|
|
|
|
f"собрано деньгами {m.fmt_money(report.cash_collected, currency)}",
|
|
|
|
|
|
f"осталось собрать {m.fmt_money(report.still_to_collect, currency)}",
|
|
|
|
|
|
]
|
Пекарня может простить остаток
Надо было отдать 5 500, взяли 5 000, полтысячи оставили. Долг закрыт,
но денег никто не отдавал.
У расчёта с пекарней появился вид: деньги или прощённый остаток. Свалить
их в одну кучу нельзя — тогда «Отдано пекарне» в статистике показывало бы
суммы, которых не платил. Поэтому «Отдано» — только живые деньги,
«Прощено» — отдельная колонка везде: в закупках, в сводке, в статистике
по периодам и по времени.
В прибыль прощённое идёт целиком: эти деньги предназначались пекарне,
а остались у тебя. «Осталось собрать» на ту же сумму уменьшается —
собирать под прощённый остаток уже не надо. Простить больше долга нельзя:
лишнее не засчитывается и, в отличие от переплаты деньгами, никуда
не переливается.
В диалоге платежа галочка «Остаток простили — закрыть партию»: вводишь
сколько отдал, остаток уходит отдельной записью, партия закрывается сама,
потому что её долг становится нулём.
Миграция схемы 3→4 помечает всё записанное раньше как живые деньги.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 06:44:19 +07:00
|
|
|
|
if report.discount > 0:
|
|
|
|
|
|
parts.append(f"пекарня простила {m.fmt_money(report.discount, currency)}")
|
2026-08-22 21:29:53 +07:00
|
|
|
|
if report.consumed_cost > 0:
|
|
|
|
|
|
parts.append(f"съедено и подарено на {m.fmt_money(report.consumed_cost, currency)}")
|
|
|
|
|
|
self.details_line.setText(" • ".join(parts))
|
|
|
|
|
|
|
|
|
|
|
|
doc = self.ctx.vault.doc
|
|
|
|
|
|
rows = []
|
|
|
|
|
|
for line in report.batch.lines:
|
|
|
|
|
|
left = report.stock.get(line.product_id, m.ZERO)
|
|
|
|
|
|
rows.append(
|
|
|
|
|
|
[
|
|
|
|
|
|
w.text_item(doc.product_name(line.product_id)),
|
|
|
|
|
|
w.sortable_num_item(m.fmt_qty(line.qty), line.qty),
|
|
|
|
|
|
w.sortable_num_item(
|
|
|
|
|
|
m.fmt_qty(left), left, theme.MUTED if left <= 0 else ""
|
|
|
|
|
|
),
|
|
|
|
|
|
w.sortable_num_item(m.fmt_money(line.unit_cost, currency), line.unit_cost),
|
|
|
|
|
|
w.sortable_num_item(m.fmt_money(line.total, currency), line.total),
|
|
|
|
|
|
]
|
|
|
|
|
|
)
|
|
|
|
|
|
w.fill(self.contents, rows)
|
|
|
|
|
|
|
|
|
|
|
|
# --- действия ---
|
|
|
|
|
|
|
|
|
|
|
|
def _selected_report(self, quiet: bool = False):
|
|
|
|
|
|
batch_id = w.selected_key(self.table)
|
|
|
|
|
|
report = self.ctx.report.batch_report(batch_id) if batch_id else None
|
|
|
|
|
|
if report is None and not quiet:
|
|
|
|
|
|
QMessageBox.information(self, "Не выбрано", "Выбери партию в списке.")
|
|
|
|
|
|
return report
|
|
|
|
|
|
|
|
|
|
|
|
def create(self) -> None:
|
|
|
|
|
|
if not [p for p in self.ctx.vault.doc.products if not p.archived]:
|
|
|
|
|
|
QMessageBox.information(
|
|
|
|
|
|
self, "Нет товаров", "Сначала заведи булки на вкладке «Товары»."
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
dialog = BatchDialog(self.ctx, parent=self)
|
|
|
|
|
|
if dialog.exec() != QDialog.Accepted:
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
journal.create_batch(
|
|
|
|
|
|
self.ctx.vault,
|
|
|
|
|
|
dialog.date.get_date(),
|
|
|
|
|
|
dialog.due.get_date(),
|
|
|
|
|
|
dialog.lines.lines("unit_cost"),
|
|
|
|
|
|
dialog.note.text(),
|
|
|
|
|
|
)
|
|
|
|
|
|
except journal.ValidationError as exc:
|
|
|
|
|
|
QMessageBox.warning(self, "Не получилось", str(exc))
|
|
|
|
|
|
return
|
|
|
|
|
|
self.ctx.changed()
|
|
|
|
|
|
|
|
|
|
|
|
def edit(self) -> None:
|
|
|
|
|
|
report = self._selected_report()
|
|
|
|
|
|
if report is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
dialog = BatchDialog(self.ctx, report.batch, parent=self)
|
|
|
|
|
|
if dialog.exec() != QDialog.Accepted:
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
journal.update_batch(
|
|
|
|
|
|
self.ctx.vault,
|
|
|
|
|
|
report.batch.id,
|
|
|
|
|
|
dialog.date.get_date(),
|
|
|
|
|
|
dialog.due.get_date(),
|
|
|
|
|
|
dialog.lines.lines("unit_cost"),
|
|
|
|
|
|
dialog.note.text(),
|
|
|
|
|
|
)
|
|
|
|
|
|
except journal.ValidationError as exc:
|
|
|
|
|
|
QMessageBox.warning(self, "Не получилось", str(exc))
|
|
|
|
|
|
return
|
|
|
|
|
|
self.ctx.changed()
|
|
|
|
|
|
|
|
|
|
|
|
def pay(self) -> None:
|
|
|
|
|
|
report = self._selected_report(quiet=True)
|
|
|
|
|
|
batch = report.batch if report else None
|
|
|
|
|
|
dialog = PaymentDialog(
|
|
|
|
|
|
self.ctx, batch, report.remaining_to_bakery if report else None, parent=self
|
|
|
|
|
|
)
|
|
|
|
|
|
if dialog.exec() != QDialog.Accepted:
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
Пекарня может простить остаток
Надо было отдать 5 500, взяли 5 000, полтысячи оставили. Долг закрыт,
но денег никто не отдавал.
У расчёта с пекарней появился вид: деньги или прощённый остаток. Свалить
их в одну кучу нельзя — тогда «Отдано пекарне» в статистике показывало бы
суммы, которых не платил. Поэтому «Отдано» — только живые деньги,
«Прощено» — отдельная колонка везде: в закупках, в сводке, в статистике
по периодам и по времени.
В прибыль прощённое идёт целиком: эти деньги предназначались пекарне,
а остались у тебя. «Осталось собрать» на ту же сумму уменьшается —
собирать под прощённый остаток уже не надо. Простить больше долга нельзя:
лишнее не засчитывается и, в отличие от переплаты деньгами, никуда
не переливается.
В диалоге платежа галочка «Остаток простили — закрыть партию»: вводишь
сколько отдал, остаток уходит отдельной записью, партия закрывается сама,
потому что её долг становится нулём.
Миграция схемы 3→4 помечает всё записанное раньше как живые деньги.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 06:44:19 +07:00
|
|
|
|
if dialog.amount.value_decimal() > 0:
|
|
|
|
|
|
journal.add_bakery_payment(
|
|
|
|
|
|
self.ctx.vault,
|
|
|
|
|
|
dialog.amount.value_decimal(),
|
|
|
|
|
|
dialog.date.get_date(),
|
|
|
|
|
|
batch.id if batch else None,
|
|
|
|
|
|
dialog.note.text(),
|
|
|
|
|
|
)
|
|
|
|
|
|
if dialog.wants_write_off():
|
|
|
|
|
|
journal.write_off_batch(
|
|
|
|
|
|
self.ctx.vault,
|
|
|
|
|
|
batch.id,
|
|
|
|
|
|
dialog.remainder,
|
|
|
|
|
|
dialog.date.get_date(),
|
|
|
|
|
|
dialog.note.text() or "остаток простили",
|
|
|
|
|
|
)
|
2026-08-22 21:29:53 +07:00
|
|
|
|
except journal.ValidationError as exc:
|
|
|
|
|
|
QMessageBox.warning(self, "Не получилось", str(exc))
|
|
|
|
|
|
return
|
|
|
|
|
|
self.ctx.changed()
|
|
|
|
|
|
|
|
|
|
|
|
def toggle_closed(self) -> None:
|
|
|
|
|
|
report = self._selected_report()
|
|
|
|
|
|
if report is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
journal.set_batch_closed(self.ctx.vault, report.batch.id, not report.batch.closed)
|
|
|
|
|
|
self.ctx.changed()
|
|
|
|
|
|
|
|
|
|
|
|
def delete(self) -> None:
|
|
|
|
|
|
report = self._selected_report()
|
|
|
|
|
|
if report is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
confirm = QMessageBox.question(
|
|
|
|
|
|
self,
|
|
|
|
|
|
"Удалить партию?",
|
|
|
|
|
|
f"Удалить партию {report.label}?\n\n"
|
|
|
|
|
|
"Платежи пекарне останутся в учёте — они просто потеряют привязку.",
|
|
|
|
|
|
)
|
|
|
|
|
|
if confirm != QMessageBox.Yes:
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
journal.delete_batch(self.ctx.vault, report.batch.id)
|
|
|
|
|
|
except journal.ValidationError as exc:
|
|
|
|
|
|
QMessageBox.warning(self, "Не получилось", str(exc))
|
|
|
|
|
|
return
|
|
|
|
|
|
self.ctx.changed()
|