Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 8 additions & 14 deletions plugins/POS.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,28 +199,22 @@ def printdeclaratie(self, args):
self.open()
self.slowwrite(BON)

def makepartybon(self, started_amount, current_amount, settled_amount, started_at):
def makepartybon(self, settlement):
BON = PRINTER + LARGE + CENTER + LOGO
BON += NORMAL + b"Party mode afrekening\n\n"
BON += RIGHT + b"%s - Arnhem\n" % time.strftime("%Y-%m-%d %H:%M:%S").encode()
BON += LEFT + b"\n"
BON += b"Gestart: %s\n" % started_at.encode()
BON += b"Begonnen met: %8.2f\n" % started_amount
BON += b"Over: %8.2f\n" % current_amount
BON += b"Afgerekend: %8.2f\n" % settled_amount
BON += b"Gestart: %s\n" % settlement["started_at"].encode()
BON += b"Begonnen met: %8.2f\n" % settlement["started_amount"]
BON += b"Bijgestort: %8.2f\n" % settlement["credited_amount"]
BON += b"Over: %8.2f\n" % settlement["current_amount"]
BON += b"Afgerekend: %8.2f\n" % settlement["settled_amount"]
BON += LEFT + FEED + CUT
return BON

def printparty(self, started_amount, current_amount, settled_amount, started_at):
def printparty(self, settlement):
self.open()
self.slowwrite(
self.makepartybon(
started_amount,
current_amount,
settled_amount,
started_at,
)
)
self.slowwrite(self.makepartybon(settlement))

def hook_post_checkout(self, user):
self.loadbons()
Expand Down
86 changes: 81 additions & 5 deletions plugins/git.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
# -*- coding: utf-8 -*-
import fcntl
import logging
import os
import subprocess
import threading
import time

logger = logging.getLogger(__name__)

DATA_REPO = "data"
KASSA_GIT_LOCK = os.path.join(DATA_REPO, ".kassa-git.lock")
GIT_INDEX_LOCK = os.path.join(DATA_REPO, ".git", "index.lock")
STALE_INDEX_LOCK_SECONDS = 60


class git:
Expand All @@ -10,13 +21,78 @@ def __init__(self, SID, master):
self.master = master
self.SID = SID

def _run_git(self, args):
return subprocess.run(
["git", *args],
cwd=DATA_REPO,
check=False,
capture_output=True,
text=True,
)

def _remove_stale_index_lock(self):
try:
age = time.time() - os.path.getmtime(GIT_INDEX_LOCK)
except FileNotFoundError:
return False

if age < STALE_INDEX_LOCK_SECONDS:
logger.warning(
"data_git_index_lock_present sid=%s age_seconds=%.1f",
self.SID,
age,
)
return False

try:
os.unlink(GIT_INDEX_LOCK)
except FileNotFoundError:
return False
except OSError:
logger.exception("data_git_index_lock_remove_failed sid=%s", self.SID)
return False

logger.warning(
"data_git_index_lock_removed sid=%s age_seconds=%.1f",
self.SID,
age,
)
return True

def _commit(self):
add_result = self._run_git(["add", "-A", "."])
commit_result = self._run_git(["commit", "-m", str(self.master.transID), "."])
combined_output = (add_result.stderr or "") + (commit_result.stderr or "")

if "index.lock" in combined_output and self._remove_stale_index_lock():
add_result = self._run_git(["add", "-A", "."])
commit_result = self._run_git(
["commit", "-m", str(self.master.transID), "."]
)

if add_result.returncode != 0 or commit_result.returncode != 0:
message = "\n".join(
part
for part in (
add_result.stderr.strip(),
commit_result.stderr.strip(),
commit_result.stdout.strip(),
)
if part
)
if "nothing to commit" not in message:
logger.warning(
"data_git_commit_failed sid=%s error=%s", self.SID, message
)

def background(self):
with self.lock:
subprocess.run(
["git", "commit", "-m", str(self.master.transID), "."],
cwd="data",
check=False,
)
with open(KASSA_GIT_LOCK, "w", encoding="utf-8") as lock_file:
fcntl.flock(lock_file, fcntl.LOCK_EX)
try:
self._commit()
finally:
fcntl.flock(lock_file, fcntl.LOCK_UN)

def hook_post_checkout(self, _text):
threading.Thread(target=self.background).start()
Expand Down
67 changes: 58 additions & 9 deletions plugins/party.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import json
import logging
import os
import re
import tempfile
import time

logger = logging.getLogger(__name__)

PARTY_USER = "party"
STATE_FILE = "data/revbank.party"
LOG_FILE = "data/revbank.log"
GAIN_RE = re.compile(r"^(\S+) BALANCE\s+\d+\s+(\S+) had .* gained \+([0-9.]+),")


def _atomic_write(path, data):
Expand All @@ -31,13 +34,15 @@ def _atomic_write(path, data):
class party:
active = False
started_amount = 0.0
credited_amount = 0.0
started_at = ""

def __init__(self, SID, master):
self.master = master
self.SID = SID
self.active = False
self.started_amount = 0.0
self.credited_amount = 0.0
self.started_at = ""

def help(self):
Expand All @@ -54,13 +59,18 @@ def member_override(self):
def loadstate(self):
self.active = False
self.started_amount = 0.0
self.credited_amount = 0.0
self.started_at = ""
try:
with open(STATE_FILE, encoding="utf-8") as f:
data = json.load(f)
self.active = bool(data.get("active", False))
self.started_amount = float(data.get("started_amount", 0.0))
self.started_at = str(data.get("started_at", ""))
if "credited_amount" in data:
self.credited_amount = float(data.get("credited_amount", 0.0))
elif self.active:
self.credited_amount = self._credited_amount_from_log()
except FileNotFoundError:
return
except (OSError, json.JSONDecodeError, TypeError, ValueError) as exc:
Expand All @@ -72,10 +82,31 @@ def writestate(self):
{
"active": self.active,
"started_amount": self.started_amount,
"credited_amount": self.credited_amount,
"started_at": self.started_at,
},
)

def _credited_amount_from_log(self):
credited_amount = 0.0
try:
with open(LOG_FILE, encoding="utf-8") as f:
for line in f:
match = GAIN_RE.match(line)
if not match:
continue
timestamp, user, value = match.groups()
if user == PARTY_USER and timestamp >= self.started_at:
credited_amount += float(value)
except FileNotFoundError:
return 0.0
except (OSError, ValueError) as exc:
logger.warning(
"party_credit_log_load_failed sid=%s error=%s", self.SID, exc
)
return 0.0
return round(credited_amount, 2)

def _ensure_party_account(self):
accounts = self.master.accounts
if PARTY_USER not in accounts.accounts:
Expand All @@ -102,6 +133,7 @@ def _enable(self):
return True
self.active = True
self.started_amount = current_amount
self.credited_amount = 0.0
self.started_at = time.strftime("%Y-%m-%d_%H:%M:%S")
self.writestate()
self._publish_members()
Expand All @@ -119,14 +151,18 @@ def _disable(self):
return True
self.master.accounts.readaccounts()
current_amount = self._ensure_party_account()
settled_amount = round(self.started_amount - current_amount, 2)
settled_amount = round(
self.started_amount + self.credited_amount - current_amount, 2
)
settlement = {
"started_amount": self.started_amount,
"credited_amount": self.credited_amount,
"current_amount": current_amount,
"settled_amount": settled_amount,
"started_at": self.started_at,
}
try:
self.master.POS.printparty(
self.started_amount,
current_amount,
settled_amount,
self.started_at,
)
self.master.POS.printparty(settlement)
except Exception as exc: # pylint: disable=broad-exception-caught
logger.exception("party_receipt_print_failed sid=%s", self.SID)
self.master.send_message(
Expand All @@ -142,11 +178,24 @@ def _disable(self):
self.master.send_message(
True,
"message",
"Party mode off; started EUR %.2f, left EUR %.2f, settled EUR %.2f"
% (self.started_amount, current_amount, settled_amount),
"Party mode off; started EUR %.2f, credited EUR %.2f, "
"left EUR %.2f, settled EUR %.2f"
% (
self.started_amount,
self.credited_amount,
current_amount,
settled_amount,
),
)
return True

def hook_balance(self, args):
usr, had, has, _transID = args
if not self.active or usr != PARTY_USER or has <= had:
return
self.credited_amount = round(self.credited_amount + (has - had), 2)
self.writestate()

def input(self, text):
if text == "partymodeon":
return self._enable()
Expand Down
27 changes: 20 additions & 7 deletions tests/plugins/test_POS.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,17 +199,22 @@ def test_printdeclaratie(self):
self.POS.slowwrite.assert_called()

def test_makepartybon(self):
bon = self.POS.makepartybon(
started_amount=100.0,
current_amount=72.5,
settled_amount=27.5,
started_at="2026-05-28_20:00:00",
)
settlement = {
"started_amount": 100.0,
"current_amount": 72.5,
"settled_amount": 27.5,
"started_at": "2026-05-28_20:00:00",
"credited_amount": 25.0,
}

bon = self.POS.makepartybon(settlement)

assert b"Party mode afrekening" in bon
assert b"Gestart: 2026-05-28_20:00:00" in bon
assert b"Begonnen met:" in bon
assert b" 100.00" in bon
assert b"Bijgestort:" in bon
assert b" 25.00" in bon
assert b"Over:" in bon
assert b" 72.50" in bon
assert b"Afgerekend:" in bon
Expand All @@ -219,7 +224,15 @@ def test_printparty(self):
with patch.object(self.POS, "open") as mock_open_pos, patch.object(
self.POS, "slowwrite"
) as mock_slowwrite:
self.POS.printparty(100.0, 72.5, 27.5, "2026-05-28_20:00:00")
self.POS.printparty(
{
"started_amount": 100.0,
"current_amount": 72.5,
"settled_amount": 27.5,
"started_at": "2026-05-28_20:00:00",
"credited_amount": 25.0,
}
)

mock_open_pos.assert_called_once()
mock_slowwrite.assert_called_once()
Expand Down
Loading
Loading