food-records/app/ui/login.py
Claude c8b9f5048b Учёт закупки и перепродажи булок
Десктопное приложение на PySide6. Вся база — один JSON-документ,
зашифрованный AES-256-GCM под паролем (ключ через scrypt), лежит
в data/vault.fmdb и раз в час уезжает в этот репозиторий.

Основное:
- партии с дедлайном возврата себестоимости пекарне, FIFO-разнос продаж
  по партиям и прогресс покрытия к сроку;
- типы выбытия: розница, другу по себестоимости, съел сам, подарок,
  списание — съеденное вычитается из прибыли, за него платить всё равно;
- долги контрагентов с частичными оплатами;
- номенклатура с историей изменения цен;
- журнал изменений внутри базы: git хранит непрозрачные снимки,
  поэтому настоящая история ведётся здесь.

Синхронизация коммитит только путь базы и схлопывает часовые пуши
в один коммит на день через amend + force-with-lease.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 17:29:53 +03:00

160 lines
5.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Разблокировка существующей базы или создание новой."""
from __future__ import annotations
from pathlib import Path
from PySide6.QtCore import Qt
from PySide6.QtWidgets import (
QCheckBox,
QDialog,
QFormLayout,
QLabel,
QLineEdit,
QMessageBox,
QVBoxLayout,
)
from .. import crypto, journal
from ..storage import Vault
from . import widgets as w
MIN_PASSWORD_LENGTH = 4
class LoginDialog(QDialog):
"""Диалог входа. После accept() в self.vault лежит открытая база."""
def __init__(self, vault_path: Path, parent=None):
super().__init__(parent)
self.vault_path = Path(vault_path)
self.is_new = not self.vault_path.exists()
self.vault: Vault | None = None
self.setWindowTitle("Булочная")
self.setMinimumWidth(440)
layout = QVBoxLayout(self)
layout.setContentsMargins(24, 20, 24, 20)
layout.setSpacing(12)
layout.addWidget(w.heading("Новая база" if self.is_new else "Булочная"))
layout.addWidget(
w.label(
"Придумай пароль. Им шифруется весь учёт — восстановить его "
"невозможно, так что запиши в надёжном месте."
if self.is_new
else str(self.vault_path),
"dim",
)
)
layout.itemAt(layout.count() - 1).widget().setWordWrap(True)
form = QFormLayout()
form.setSpacing(8)
self.password = QLineEdit()
self.password.setEchoMode(QLineEdit.Password)
self.password.returnPressed.connect(self.accept)
form.addRow("Пароль", self.password)
if self.is_new:
self.confirm = QLineEdit()
self.confirm.setEchoMode(QLineEdit.Password)
self.confirm.returnPressed.connect(self.accept)
form.addRow("Ещё раз", self.confirm)
self.remote = QLineEdit()
self.remote.setPlaceholderText("https://gt.ser.gay/kizya/food-market.git")
form.addRow("Репозиторий", self.remote)
self.token = QLineEdit()
self.token.setEchoMode(QLineEdit.Password)
self.token.setPlaceholderText("необязательно, можно задать позже")
form.addRow("Токен GitLab", self.token)
self.demo = QCheckBox("Завести три булки для примера")
self.demo.setChecked(True)
form.addRow("", self.demo)
layout.addLayout(form)
show = QCheckBox("Показать пароль")
show.toggled.connect(self._toggle_echo)
layout.addWidget(show)
self.error = QLabel("")
self.error.setWordWrap(True)
self.error.setStyleSheet("color: #e0605e;")
layout.addWidget(self.error)
layout.addWidget(
w.row(
None,
w.button("Отмена", on_click=self.reject),
w.button("Создать" if self.is_new else "Открыть", "primary", self.accept),
)
)
self.password.setFocus()
def _toggle_echo(self, visible: bool) -> None:
mode = QLineEdit.Normal if visible else QLineEdit.Password
self.password.setEchoMode(mode)
if self.is_new:
self.confirm.setEchoMode(mode)
def accept(self) -> None:
self.error.setText("")
password = self.password.text()
if self.is_new:
if len(password) < MIN_PASSWORD_LENGTH:
self.error.setText(f"Пароль короче {MIN_PASSWORD_LENGTH} символов.")
return
if password != self.confirm.text():
self.error.setText("Пароли не совпадают.")
return
self._create(password)
else:
self._open(password)
def _create(self, password: str) -> None:
try:
vault = Vault.create(self.vault_path, password)
except OSError as exc:
self.error.setText(f"Не удалось создать файл: {exc}")
return
git_settings = {}
if self.remote.text().strip():
git_settings["remote_url"] = self.remote.text().strip()
if self.token.text():
git_settings["token"] = self.token.text()
if git_settings:
journal.update_settings(vault, git=git_settings)
if self.demo.isChecked():
journal.create_product(vault, "Булка с повидлом", cost_price=30, retail_price=50)
journal.create_product(vault, "Булка с маком", cost_price=20, retail_price=35)
journal.create_product(vault, "Булка с корицей", cost_price=25, retail_price=40)
vault.save()
self.vault = vault
super().accept()
def _open(self, password: str) -> None:
try:
self.vault = Vault.open(self.vault_path, password)
except crypto.WrongPassword:
self.error.setText("Неверный пароль.")
self.password.selectAll()
self.password.setFocus()
return
except crypto.VaultError as exc:
QMessageBox.critical(self, "Не удалось открыть базу", str(exc))
return
except OSError as exc:
QMessageBox.critical(self, "Не удалось прочитать файл", str(exc))
return
super().accept()