From 15bb03e5feb8511b36d47d977e56fb95a716076c Mon Sep 17 00:00:00 2001 From: niekt0 Date: Sat, 31 Jan 2015 02:39:07 +0100 Subject: [PATCH 001/139] Added caching of credit balance to accounts and replacing view account_balances. --- brmbar3/SQL | 17 ++++++++++------- brmbar3/brmbar/Account.py | 11 ++++++----- brmbar3/brmbar/Shop.py | 14 +++++--------- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/brmbar3/SQL b/brmbar3/SQL index 7e640a6..6fec1d2 100644 --- a/brmbar3/SQL +++ b/brmbar3/SQL @@ -39,7 +39,8 @@ CREATE TABLE accounts ( acctype account_type NOT NULL, - active BOOLEAN NOT NULL DEFAULT TRUE + active BOOLEAN NOT NULL DEFAULT TRUE, + crbalance DECIMAL(12,2) NOT NULL ); INSERT INTO accounts (name, currency, acctype) VALUES ('BrmBar Cash', (SELECT id FROM currencies WHERE name='Kč'), 'cash'); INSERT INTO accounts (name, currency, acctype) VALUES ('BrmBar Profits', (SELECT id FROM currencies WHERE name='Kč'), 'income'); @@ -92,12 +93,14 @@ CREATE TABLE transaction_splits ( -- Note that currency information is currently not supplied; inventory items -- have balances in stock amounts. CREATE VIEW account_balances AS - SELECT ts.account AS id, accounts.name AS name, accounts.acctype AS acctype, - -SUM(CASE WHEN ts.side = 'credit' THEN -ts.amount ELSE ts.amount END) AS crbalance - FROM transaction_splits AS ts - LEFT JOIN accounts ON accounts.id = ts.account - GROUP BY ts.account, accounts.name, accounts.acctype - ORDER BY crbalance ASC; + SELECT id, name, acctype, crbalance FROM accounts ORDER BY crbalance ASC; + +-- SELECT ts.account AS id, accounts.name AS name, accounts.acctype AS acctype, +-- -SUM(CASE WHEN ts.side = 'credit' THEN -ts.amount ELSE ts.amount END) AS crbalance +-- FROM transaction_splits AS ts +-- LEFT JOIN accounts ON accounts.id = ts.account +-- GROUP BY ts.account, accounts.name, accounts.acctype +-- ORDER BY crbalance ASC; -- Transaction splits in a form that's nicer to query during manual inspection CREATE VIEW transaction_nicesplits AS diff --git a/brmbar3/brmbar/Account.py b/brmbar3/brmbar/Account.py index 29dd79e..df5dcd6 100644 --- a/brmbar3/brmbar/Account.py +++ b/brmbar3/brmbar/Account.py @@ -45,11 +45,9 @@ class Account: return cls(db, name = name, id = id, currency = currency, acctype = acctype) def balance(self): - debit = self.db.execute_and_fetch("SELECT SUM(amount) FROM transaction_splits WHERE account = %s AND side = %s", [self.id, 'debit']) - debit = debit[0] or 0 - credit = self.db.execute_and_fetch("SELECT SUM(amount) FROM transaction_splits WHERE account = %s AND side = %s", [self.id, 'credit']) - credit = credit[0] or 0 - return debit - credit + crbalance = self.db.execute_and_fetch("SELECT crbalance FROM accounts WHERE account = %s", [self.id]) + crbalance = crbalance[0] or 0 + return crbalance def balance_str(self): return self.currency.str(self.balance()) @@ -63,10 +61,13 @@ class Account: def credit(self, transaction, amount, memo): return self._transaction_split(transaction, 'credit', amount, memo) +# XXX atomicita def _transaction_split(self, transaction, side, amount, memo): """ Common part of credit() and debit(). """ self.db.execute("INSERT INTO transaction_splits (transaction, side, account, amount, memo) VALUES (%s, %s, %s, %s, %s)", [transaction, side, self.id, amount, memo]) + self.db.execute("UPDATE accounts set crbalance = crbalance + (CASE WHEN %s = 'credit' THEN -amount ELSE amount END)", [side]) + def add_barcode(self, barcode): self.db.execute("INSERT INTO barcodes (account, barcode) VALUES (%s, %s)", [self.id, barcode]) self.db.commit() diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index bf74fef..1adcdbf 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -105,16 +105,12 @@ class Shop: def credit_balance(self): # We assume all debt accounts share a currency sumselect = """ - SELECT SUM(ts.amount) - FROM accounts AS a - LEFT JOIN transaction_splits AS ts ON a.id = ts.account - WHERE a.acctype = %s AND ts.side = %s + SELECT SUM(crbalance) + FROM accounts WHERE acctype = %s AND """ - cur = self.db.execute_and_fetch(sumselect, ["debt", 'debit']) - debit = cur[0] or 0 - credit = self.db.execute_and_fetch(sumselect, ["debt", 'credit']) - credit = credit[0] or 0 - return debit - credit + cur = self.db.execute_and_fetch(sumselect, ["debt"]) + cur = cur[0] or 0 + return cur def credit_negbalance_str(self): return self.currency.str(-self.credit_balance()) From 0d2bb2eed70a760bfd8baa1504201f3cad87a6b4 Mon Sep 17 00:00:00 2001 From: niekt0 Date: Sat, 31 Jan 2015 03:38:08 +0100 Subject: [PATCH 002/139] sign fix (credit vs. debit) --- brmbar3/brmbar/Account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brmbar3/brmbar/Account.py b/brmbar3/brmbar/Account.py index df5dcd6..b22c041 100644 --- a/brmbar3/brmbar/Account.py +++ b/brmbar3/brmbar/Account.py @@ -66,7 +66,7 @@ class Account: """ Common part of credit() and debit(). """ self.db.execute("INSERT INTO transaction_splits (transaction, side, account, amount, memo) VALUES (%s, %s, %s, %s, %s)", [transaction, side, self.id, amount, memo]) - self.db.execute("UPDATE accounts set crbalance = crbalance + (CASE WHEN %s = 'credit' THEN -amount ELSE amount END)", [side]) + self.db.execute("UPDATE accounts set crbalance = crbalance - (CASE WHEN %s = 'credit' THEN -amount ELSE amount END)", [side]) def add_barcode(self, barcode): self.db.execute("INSERT INTO barcodes (account, barcode) VALUES (%s, %s)", [self.id, barcode]) From f922edf04237b1e5d34d958831a19488edae881c Mon Sep 17 00:00:00 2001 From: niekt0 Date: Sat, 31 Jan 2015 03:45:50 +0100 Subject: [PATCH 003/139] tab vs. spaces unification (python:( --- brmbar3/brmbar/Account.py | 2 +- brmbar3/brmbar/Shop.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/brmbar3/brmbar/Account.py b/brmbar3/brmbar/Account.py index b22c041..d474e6c 100644 --- a/brmbar3/brmbar/Account.py +++ b/brmbar3/brmbar/Account.py @@ -66,7 +66,7 @@ class Account: """ Common part of credit() and debit(). """ self.db.execute("INSERT INTO transaction_splits (transaction, side, account, amount, memo) VALUES (%s, %s, %s, %s, %s)", [transaction, side, self.id, amount, memo]) - self.db.execute("UPDATE accounts set crbalance = crbalance - (CASE WHEN %s = 'credit' THEN -amount ELSE amount END)", [side]) + self.db.execute("UPDATE accounts set crbalance = crbalance - (CASE WHEN %s = 'credit' THEN -amount ELSE amount END)", [side]) def add_barcode(self, barcode): self.db.execute("INSERT INTO barcodes (account, barcode) VALUES (%s, %s)", [self.id, barcode]) diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index 1adcdbf..a170fcd 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -135,7 +135,7 @@ class Shop: """list all accounts (people or items, as per acctype)""" accts = [] cur = self.db.execute_and_fetchall("SELECT id FROM accounts WHERE acctype = %s AND name ILIKE %s ORDER BY name ASC", [acctype, like_str]) - #FIXME: sanitize input like_str ^ + #FIXME: sanitize input like_str ^ for inventory in cur: accts += [ Account.load(self.db, id = inventory[0]) ] return accts From 96bf23662e9f4f5bd10e63235470d7200cef5bc3 Mon Sep 17 00:00:00 2001 From: niekt0 Date: Sat, 31 Jan 2015 04:11:28 +0100 Subject: [PATCH 004/139] fixes from runtime --- brmbar3/brmbar/Account.py | 4 ++-- brmbar3/brmbar/Shop.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/brmbar3/brmbar/Account.py b/brmbar3/brmbar/Account.py index d474e6c..3186dcc 100644 --- a/brmbar3/brmbar/Account.py +++ b/brmbar3/brmbar/Account.py @@ -45,7 +45,7 @@ class Account: return cls(db, name = name, id = id, currency = currency, acctype = acctype) def balance(self): - crbalance = self.db.execute_and_fetch("SELECT crbalance FROM accounts WHERE account = %s", [self.id]) + crbalance = self.db.execute_and_fetch("SELECT crbalance FROM accounts WHERE id = %s", [self.id]) crbalance = crbalance[0] or 0 return crbalance @@ -66,7 +66,7 @@ class Account: """ Common part of credit() and debit(). """ self.db.execute("INSERT INTO transaction_splits (transaction, side, account, amount, memo) VALUES (%s, %s, %s, %s, %s)", [transaction, side, self.id, amount, memo]) - self.db.execute("UPDATE accounts set crbalance = crbalance - (CASE WHEN %s = 'credit' THEN -amount ELSE amount END)", [side]) + self.db.execute("UPDATE accounts set crbalance = crbalance + (CASE WHEN %s = 'credit' THEN -amount ELSE amount END)", [side]) def add_barcode(self, barcode): self.db.execute("INSERT INTO barcodes (account, barcode) VALUES (%s, %s)", [self.id, barcode]) diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index a170fcd..b5c61dc 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -106,7 +106,7 @@ class Shop: # We assume all debt accounts share a currency sumselect = """ SELECT SUM(crbalance) - FROM accounts WHERE acctype = %s AND + FROM accounts WHERE acctype = %s """ cur = self.db.execute_and_fetch(sumselect, ["debt"]) cur = cur[0] or 0 From 5bc7b0e004fd3bff8ea5f3cd25f48e1ac783be26 Mon Sep 17 00:00:00 2001 From: niekt0 Date: Sat, 31 Jan 2015 04:24:05 +0100 Subject: [PATCH 005/139] runtime sql error fixed --- brmbar3/brmbar/Account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brmbar3/brmbar/Account.py b/brmbar3/brmbar/Account.py index 3186dcc..0c6448f 100644 --- a/brmbar3/brmbar/Account.py +++ b/brmbar3/brmbar/Account.py @@ -66,7 +66,7 @@ class Account: """ Common part of credit() and debit(). """ self.db.execute("INSERT INTO transaction_splits (transaction, side, account, amount, memo) VALUES (%s, %s, %s, %s, %s)", [transaction, side, self.id, amount, memo]) - self.db.execute("UPDATE accounts set crbalance = crbalance + (CASE WHEN %s = 'credit' THEN -amount ELSE amount END)", [side]) + self.db.execute("UPDATE accounts set crbalance = crbalance + (CASE WHEN %s = 'credit' THEN -%s ELSE %s END)", [side,amount,amount]) def add_barcode(self, barcode): self.db.execute("INSERT INTO barcodes (account, barcode) VALUES (%s, %s)", [self.id, barcode]) From 5de7bf7458d06b9db8bdcf0a0492e0404826e8ef Mon Sep 17 00:00:00 2001 From: niekt0 Date: Sat, 31 Jan 2015 21:09:52 +0100 Subject: [PATCH 006/139] Disabled default and inefective computation of the stock sumary --- brmbar3/brmbar/Shop.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index bf74fef..1c4ffb0 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -118,6 +118,7 @@ class Shop: def credit_negbalance_str(self): return self.currency.str(-self.credit_balance()) +# XXX causing extra heavy delay ( thousands of extra SQL queries ), disabled def inventory_balance(self): balance = 0 # Each inventory account has its own currency, @@ -132,8 +133,11 @@ class Shop: # statement that will... ugh, accounting is hard! balance += inv.balance() * inv.currency.rates(self.currency)[0] return balance + +# XXX bypass hack def inventory_balance_str(self): - return self.currency.str(self.inventory_balance()) + return "XXX" +# return self.currency.str(self.inventory_balance()) def account_list(self, acctype, like_str="%%"): """list all accounts (people or items, as per acctype)""" From 84881dece382c26446054a77d190d1fe5e0f9b68 Mon Sep 17 00:00:00 2001 From: Mrkva Date: Mon, 11 May 2015 02:04:11 +0200 Subject: [PATCH 007/139] * merge barcode-generator to master * quick howto on how current barcodes are generated --- barcode-generator/.gitignore | 2 + barcode-generator/barcode-generator.py | 51 ++++++++++++++++++++++++++ barcode-generator/howto.txt | 9 +++++ 3 files changed, 62 insertions(+) create mode 100644 barcode-generator/.gitignore create mode 100755 barcode-generator/barcode-generator.py create mode 100644 barcode-generator/howto.txt diff --git a/barcode-generator/.gitignore b/barcode-generator/.gitignore new file mode 100644 index 0000000..e6936f1 --- /dev/null +++ b/barcode-generator/.gitignore @@ -0,0 +1,2 @@ +barcodes*.svg +barcode-generator.txt diff --git a/barcode-generator/barcode-generator.py b/barcode-generator/barcode-generator.py new file mode 100755 index 0000000..f2d30a3 --- /dev/null +++ b/barcode-generator/barcode-generator.py @@ -0,0 +1,51 @@ +#!/usr/bin/python +# +# requires zint binary from zint package +# + +from subprocess import Popen, PIPE +import sys + +svghead = """ + + +""" + +svgfoot = """ +""" + +width = 5 +scalex = 0.8 +scaley = 0.8 + +p = 0 +i = 0 +j = 0 +f = None + +lines = sys.stdin.readlines() + +for idx in xrange(len(lines)): + items = lines[idx].strip().split(';') + if idx % 30 == 0: + if f and not f.closed: + f.write(svgfoot) + f.close() + f = open('barcodes' + str(p) + '.svg','w') + p += 1 + i = 0 + j = 0 + f.write(svghead) + elem = Popen(('./zint','--directsvg','--notext', '-d', items[1]), stdout = PIPE).communicate()[0].split('\n') + elem = elem[8:-2] + elem[0] = elem[0].replace('id="barcode"', 'transform="matrix(%f,0,0,%f,%f,%f)"' % (scalex, scaley, 50+i*140 , 180+j*140) ) + elem.insert(-1, ' %s' % items[0]) + f.write('\n'.join(elem)+'\n\n') + i += 1 + if i >= width: + i = 0 + j += 1 + +if not f.closed: + f.write(svgfoot) + f.close() diff --git a/barcode-generator/howto.txt b/barcode-generator/howto.txt new file mode 100644 index 0000000..4dfba63 --- /dev/null +++ b/barcode-generator/howto.txt @@ -0,0 +1,9 @@ +on brmbar: +select distinct barcode from barcodes b, transactions t, accounts a where t.responsible=a.id and time>'2015-01-01' and b.account=a.id order by barcode asc; + +run this locally and paste output of previous command: + +while read tmp; do echo "$tmp;$tmp";done|grep -v overflow|python2 ./barcode-generator.py + +print resulting SVG files + From 1c0e51a246065934177a33d297079ee23e48a74b Mon Sep 17 00:00:00 2001 From: Petr Baudis Date: Thu, 4 Jun 2015 22:57:20 +0200 Subject: [PATCH 008/139] brmbar-qt4: Add LIMIT_BALANCE(=-200) as a lower limit of user credit --- brmbar3/brmbar-gui-qt4.py | 13 +++++++++++++ brmbar3/brmbar-gui-qt4/ItemInfo.qml | 2 ++ 2 files changed, 15 insertions(+) diff --git a/brmbar3/brmbar-gui-qt4.py b/brmbar3/brmbar-gui-qt4.py index 23e6d37..429aeb6 100755 --- a/brmbar3/brmbar-gui-qt4.py +++ b/brmbar3/brmbar-gui-qt4.py @@ -8,6 +8,9 @@ from brmbar import Database import brmbar +# User credit balance limit; sale will fail when balance is below this limit. +LIMIT_BALANCE = -200 + class ShopAdapter(QtCore.QObject): """ Interface between QML and the brmbar package """ @@ -70,6 +73,11 @@ class ShopAdapter(QtCore.QObject): db.commit() return acct + @QtCore.Slot('QVariant', 'QVariant', result='QVariant') + def canSellItem(self, itemid, userid): + user = brmbar.Account.load(db, id = userid) + return -user.balance() > LIMIT_BALANCE + @QtCore.Slot('QVariant', 'QVariant', result='QVariant') def sellItem(self, itemid, userid): user = brmbar.Account.load(db, id = userid) @@ -99,6 +107,11 @@ class ShopAdapter(QtCore.QObject): db.commit() return balance + @QtCore.Slot('QVariant', result='QVariant') + def balance_user(self, userid): + user = brmbar.Account.load(db, id=userid) + return user.negbalance_str() + @QtCore.Slot(result='QVariant') def balance_cash(self): balance = shop.cash.balance_str() diff --git a/brmbar3/brmbar-gui-qt4/ItemInfo.qml b/brmbar3/brmbar-gui-qt4/ItemInfo.qml index a39e322..11e959d 100644 --- a/brmbar3/brmbar-gui-qt4/ItemInfo.qml +++ b/brmbar3/brmbar-gui-qt4/ItemInfo.qml @@ -58,6 +58,8 @@ Item { if (acct.acctype == "cash") { //Copied from BarButton.onButtonClick shop.sellItemCash(dbid) status_text.setStatus("Sold! Put " + price + " Kč in the money box.", "#ffff7c") + } else if (!shop.canSellItem(dbid, acct.id)) { + status_text.setStatus("NOT SOLD! "+acct.name+"'s credit is TOO LOW: "+shop.balance_user(acct.id), "#ff4444") } else { var balance = shop.sellItem(dbid, acct.id) status_text.setStatus("Sold! "+acct.name+"'s credit is "+balance+".", "#ffff7c") From 6cc95ab6808760f57a7716abccf8c71b52b36e9e Mon Sep 17 00:00:00 2001 From: Petr Baudis Date: Thu, 4 Jun 2015 23:06:57 +0200 Subject: [PATCH 009/139] brmbar-qt4: Add ALERT_SCRIPT support to call external hook on low balance --- brmbar3/brmbar-gui-qt4.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/brmbar3/brmbar-gui-qt4.py b/brmbar3/brmbar-gui-qt4.py index 429aeb6..a351bee 100755 --- a/brmbar3/brmbar-gui-qt4.py +++ b/brmbar3/brmbar-gui-qt4.py @@ -1,6 +1,7 @@ #!/usr/bin/python3 import sys +import subprocess from PySide import QtCore, QtGui, QtDeclarative @@ -10,6 +11,12 @@ import brmbar # User credit balance limit; sale will fail when balance is below this limit. LIMIT_BALANCE = -200 +# When below this credit balance, an alert hook script (see below) is run. +ALERT_BALANCE = 0 +# This script is executed when a user is buying things and their balance is +# below LIMIT_BALANCE (with argument "limit") or below ALERT_BALANCE +# (with argument "alert"). +ALERT_SCRIPT = "./alert.sh" class ShopAdapter(QtCore.QObject): @@ -76,7 +83,14 @@ class ShopAdapter(QtCore.QObject): @QtCore.Slot('QVariant', 'QVariant', result='QVariant') def canSellItem(self, itemid, userid): user = brmbar.Account.load(db, id = userid) - return -user.balance() > LIMIT_BALANCE + if -user.balance() > ALERT_BALANCE: + return True + elif -user.balance() > LIMIT_BALANCE: + subprocess.call(["sh", ALERT_SCRIPT, "alert"]) + return True + else: + subprocess.call(["sh", ALERT_SCRIPT, "limit"]) + return False @QtCore.Slot('QVariant', 'QVariant', result='QVariant') def sellItem(self, itemid, userid): From 934873c2fc73fc9cff1bfd5201f0e69f8ec7ebb9 Mon Sep 17 00:00:00 2001 From: Mrkva Date: Wed, 1 Jul 2015 01:06:59 +0200 Subject: [PATCH 010/139] Old uncommited changes, restock from cli, "cleanup bounty" feature --- brmbar3/alert.sh | 6 ++++++ brmbar3/brmbar-cli.py | 11 ++++++++++- brmbar3/brmbar-gui-qt4/MainPage.qml | 10 ++++++++++ brmbar3/brmbar-web.py | 2 +- brmbar3/uklid-refill.sh | 6 ++++++ brmbar3/uklid-watchdog.sh | 18 ++++++++++++++++++ 6 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 brmbar3/alert.sh create mode 100644 brmbar3/uklid-refill.sh create mode 100644 brmbar3/uklid-watchdog.sh diff --git a/brmbar3/alert.sh b/brmbar3/alert.sh new file mode 100644 index 0000000..af20fe3 --- /dev/null +++ b/brmbar3/alert.sh @@ -0,0 +1,6 @@ +#!/bin/sh +if [ "$1" = "alert" ]; then + mplayer ~/trombone.wav & +else + mplayer ~/much.wav & +fi diff --git a/brmbar3/brmbar-cli.py b/brmbar3/brmbar-cli.py index d91a24b..cb4cac9 100755 --- a/brmbar3/brmbar-cli.py +++ b/brmbar3/brmbar-cli.py @@ -17,6 +17,7 @@ Usage: brmbar-cli.py COMMAND ARGS... changecredit USER +-AMT sellitem USER ITEM +-AMT You can use negative AMT to undo a sale. + restock ITEM AMT userinfo USER iteminfo ITEM @@ -199,7 +200,15 @@ elif sys.argv[1] == "consolidate": print ("Invalid number of parameters, check your parameters.") else: shop.consolidate() - +elif sys.argv[1] == "restock": + if (len(sys.argv) != 4): + print ("Invalid number of parameters, check your parameters.") + else: + iacct = load_item(sys.argv[2]) + amt = int(sys.argv[3]) + cash = shop.buy_for_cash(iacct, amt); + print("Old amount {}, increased by {}, take {} from cashbox".format(iacct.balance(), amt, cash)) + else: help() diff --git a/brmbar3/brmbar-gui-qt4/MainPage.qml b/brmbar3/brmbar-gui-qt4/MainPage.qml index 8155943..effc9ad 100644 --- a/brmbar3/brmbar-gui-qt4/MainPage.qml +++ b/brmbar3/brmbar-gui-qt4/MainPage.qml @@ -43,4 +43,14 @@ Item { loadPage("Management") } } + + BarButton { + x: 65 + y: 438 + width: 1150 + text: "* Mroze a Termixy najdes v lednici *" + } + + + } diff --git a/brmbar3/brmbar-web.py b/brmbar3/brmbar-web.py index 0ec70dc..5d84378 100755 --- a/brmbar3/brmbar-web.py +++ b/brmbar3/brmbar-web.py @@ -1,4 +1,4 @@ -#!/usr/bin/python3 +#!/usr/bin/python import sys diff --git a/brmbar3/uklid-refill.sh b/brmbar3/uklid-refill.sh new file mode 100644 index 0000000..898a081 --- /dev/null +++ b/brmbar3/uklid-refill.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +if [ `./brmbar-cli.py iteminfo uklid|grep -o '[0-9]*.[0-9]* pcs'|cut -d '.' -f 1` -eq 0 ]; then + BOUNTY=`./brmbar-cli.py restock uklid 1 | grep -o 'take -[0-9]*'|grep -o '[0-9]*'` + echo "Brmlab cleanup bounty for ${BOUNTY}CZK!!!"|ssh jenda@fry.hrach.eu +fi diff --git a/brmbar3/uklid-watchdog.sh b/brmbar3/uklid-watchdog.sh new file mode 100644 index 0000000..c0fd55b --- /dev/null +++ b/brmbar3/uklid-watchdog.sh @@ -0,0 +1,18 @@ +#!/bin/bash +LASTIDF=/home/brmlab/uklid.last + +LASTID=`cat $LASTIDF 2>/dev/null || echo 0` + + +RES=`psql brmbar -Atq -c "select id,description from transactions where id>$LASTID and description like 'BrmBar sale of 1x uklid%' LIMIT 1;"` +if [ ! -z "$RES" ]; then + LASTID=`echo "$RES"|cut -d '|' -f 1` + echo $LASTID > $LASTIDF + + WINNER=`echo "$RES"|grep -o 'to [^ ]*'|cut -d ' ' -f 2` + if [ -z "$WINNER" ]; then + WINNER="anonymous hunter" + fi + echo "Brmlab cleanup bounty was claimed by $WINNER! Thanks!"|ssh jenda@fry.hrach.eu +fi + From fe80200e49a4af871709393057d388366fe422f1 Mon Sep 17 00:00:00 2001 From: TrimenZ Date: Wed, 14 Oct 2015 22:39:36 +0200 Subject: [PATCH 011/139] alert.sh Add charge sound. --- brmbar3/alert.sh | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/brmbar3/alert.sh b/brmbar3/alert.sh index af20fe3..0b643bc 100644 --- a/brmbar3/alert.sh +++ b/brmbar3/alert.sh @@ -1,6 +1,7 @@ #!/bin/sh -if [ "$1" = "alert" ]; then - mplayer ~/trombone.wav & -else - mplayer ~/much.wav & -fi + +case $1 in +alert) mplayer ~/trombone.wav & ;; +limit) mplayer ~/much.wav & ;; +charge) mplayer ~/charge.wav & ;; +esac From 906a3b62ed46e8ca6821ac15eb7dd460c3e28dae Mon Sep 17 00:00:00 2001 From: TrimenZ Date: Wed, 14 Oct 2015 22:44:12 +0200 Subject: [PATCH 012/139] brmbar-qt4: Add charge sound .. --- brmbar3/brmbar-gui-qt4.py | 1 + 1 file changed, 1 insertion(+) diff --git a/brmbar3/brmbar-gui-qt4.py b/brmbar3/brmbar-gui-qt4.py index a351bee..5b63552 100755 --- a/brmbar3/brmbar-gui-qt4.py +++ b/brmbar3/brmbar-gui-qt4.py @@ -107,6 +107,7 @@ class ShopAdapter(QtCore.QObject): @QtCore.Slot('QVariant', 'QVariant', result='QVariant') def chargeCredit(self, credit, userid): + subprocess.call(["sh", ALERT_SCRIPT, "charge"]) user = brmbar.Account.load(db, id = userid) shop.add_credit(credit = credit, user = user) balance = user.negbalance_str() From a162e544c1a74e1d8963d7c144b37874699c355e Mon Sep 17 00:00:00 2001 From: brmbar Date: Sat, 31 Oct 2015 21:16:01 +0100 Subject: [PATCH 013/139] alert.sh: +x --- brmbar3/alert.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 brmbar3/alert.sh diff --git a/brmbar3/alert.sh b/brmbar3/alert.sh old mode 100644 new mode 100755 From 5c84bd6a8fcd8c38478f2aa08f0cf97feb53b373 Mon Sep 17 00:00:00 2001 From: brmbar Date: Sat, 31 Oct 2015 22:02:12 +0100 Subject: [PATCH 014/139] GUI: Support for money transfer (by sachy + pasky) --- brmbar3/brmbar-gui-qt4/Transfer.qml | 174 ++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 brmbar3/brmbar-gui-qt4/Transfer.qml diff --git a/brmbar3/brmbar-gui-qt4/Transfer.qml b/brmbar3/brmbar-gui-qt4/Transfer.qml new file mode 100644 index 0000000..d6a3a4e --- /dev/null +++ b/brmbar3/brmbar-gui-qt4/Transfer.qml @@ -0,0 +1,174 @@ +import QtQuick 1.1 + +Item { + id: page + anchors.fill: parent + + property variant userfrom: "" + property variant uidfrom: "" + property variant userto: "" + property variant uidto: "" + property string amount: amount_pad.enteredText + + BarcodeInput { + color: "#00ff00" /* just for debugging */ + focus: !(parent.userfrom != "" && parent.userto != "") + onAccepted: { + var acct = shop.barcodeInput(text) + text = "" + if (typeof(acct) == "undefined") { + status_text.setStatus("Unknown barcode", "#ff4444") + return + } + if (acct.acctype == "debt") { + if (userfrom == "") { + userfrom = acct.name + uidfrom = acct.id + } else { + userto = acct.name + uidto = acct.id + } + } else if (acct.acctype == "recharge") { + amount = acct.amount + } else { + status_text.setStatus("Unknown barcode", "#ff4444") + } + } + } + + Item { + id: amount_row + visible: parent.userfrom != "" && parent.userto != "" + x: 65; + y: 166; + width: 890 + height: 60 + + Text { + id: item_sellprice_label + x: 0 + y: 0 + height: 60 + width: 200 + color: "#ffffff" + text: "Money Amount:" + verticalAlignment: Text.AlignVCenter + font.pixelSize: 0.768 * 46 + } + + Text { + id: amount_input + x: 320 + y: 0 + height: 60 + width: 269 + color: "#ffff7c" + text: amount + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + font.pixelSize: 0.768 * 122 + } + } + + BarNumPad { + id: amount_pad + x: 65 + y: 239 + visible: parent.userfrom != "" && parent.userto != "" + focus: parent.userfrom != "" && parent.userto != "" + Keys.onReturnPressed: { transfer.buttonClick() } + Keys.onEscapePressed: { cancel.buttonClick() } + } + + BarTextHint { + id: barcode_row + x: 65 + y: parent.userfrom == "" ? 314 : 414 + hint_goal: (parent.userfrom == "" ? "Take money from:" : parent.userto == "" ? "Give money to:" : parent.amount == "" ? "Specify amount" : "") + hint_action: (parent.userfrom == "" || parent.userto == "" ? "Scan barcode now" : (parent.amount ? "" : "(or scan barcode now)")) + } + + Text { + id: legend + visible: !(parent.userfrom != "" && parent.userto != "") + x: 65 + y: 611 + height: 154 + width: 894 + color: "#71cccc" + text: "This is for transfering credit between two brmbar users.\n May be used instead of *check next club-mate to me*." + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignHCenter + font.pixelSize: 0.768 * 27 + } + + Text { + id: item_name + x: 422 + y: 156 + width: 537 + height: 80 + color: "#ffffff" + text: parent.userfrom ? parent.userfrom + " →" : "Money Transfer" + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + font.pixelSize: 0.768 * 60 + } + + Text { + id: item_name2 + x: 422 + y: 256 + width: 537 + height: 80 + color: "#ffffff" + text: parent.userto ? "→ " + parent.userto : "" + wrapMode: Text.WordWrap + horizontalAlignment: Text.AlignRight + verticalAlignment: Text.AlignVCenter + font.pixelSize: 0.768 * 60 + } + + BarButton { + id: transfer + x: 65 + y: 838 + width: 360 + text: "Transfer" + onButtonClick: { + if (userfrom == "") { + status_text.setStatus("Select FROM account.", "#ff4444") + return + } + if (userto == "") { + status_text.setStatus("Select TO account.", "#ff4444") + return + } + if (amount == "") { + status_text.setStatus("Enter amount.", "#ff4444") + return + } + var amount_str = shop.newTransfer(uidfrom, uidto, amount) + if (typeof(amount_str) == "undefined") { + status_text.setStatus("Transfer error.", "#ff4444") + return + } + + status_text.setStatus("Transferred " + amount_str + " from " + userfrom + " to " + userto, "#ffff7c") + loadPage("MainPage") + } + } + + BarButton { + id: cancel + x: 855 + y: 838 + width: 360 + text: "Cancel" + onButtonClick: { + status_text.setStatus("Transfer cancelled", "#ff4444") + loadPage("MainPage") + } + } +} From 099d775102bff5a46ccfbdf0333f7d84d7b99447 Mon Sep 17 00:00:00 2001 From: Petr Baudis Date: Sun, 13 Dec 2015 16:10:03 +0100 Subject: [PATCH 015/139] Add missing collateral changes for Transfer --- brmbar3/brmbar-gui-qt4.py | 8 ++++++++ brmbar3/brmbar-gui-qt4/MainPage.qml | 13 ++++++++++--- brmbar3/brmbar/Shop.py | 4 ++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/brmbar3/brmbar-gui-qt4.py b/brmbar3/brmbar-gui-qt4.py index 5b63552..a41fd2c 100755 --- a/brmbar3/brmbar-gui-qt4.py +++ b/brmbar3/brmbar-gui-qt4.py @@ -122,6 +122,14 @@ class ShopAdapter(QtCore.QObject): db.commit() return balance + @QtCore.Slot('QVariant', 'QVariant', 'QVariant', result='QVariant') + def newTransfer(self, uidfrom, uidto, amount): + ufrom = brmbar.Account.load(db, id=uidfrom) + uto = brmbar.Account.load(db, id=uidto) + shop.transfer_credit(ufrom, uto, amount = amount) + db.commit() + return currency.str(float(amount)) + @QtCore.Slot('QVariant', result='QVariant') def balance_user(self, userid): user = brmbar.Account.load(db, id=userid) diff --git a/brmbar3/brmbar-gui-qt4/MainPage.qml b/brmbar3/brmbar-gui-qt4/MainPage.qml index effc9ad..6e555eb 100644 --- a/brmbar3/brmbar-gui-qt4/MainPage.qml +++ b/brmbar3/brmbar-gui-qt4/MainPage.qml @@ -33,6 +33,16 @@ Item { } } + BarButton { + x: 450 + y: 838 + width: 360 + text: "Transfer" + onButtonClick: { + loadPage("Transfer") + } + } + BarButton { id: management x: 855 @@ -50,7 +60,4 @@ Item { width: 1150 text: "* Mroze a Termixy najdes v lednici *" } - - - } diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index 1c4ffb0..55c26ba 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -78,6 +78,10 @@ class Shop: user.debit(transaction, credit, "Credit withdrawal") self.db.commit() + def transfer_credit(self, userfrom, userto, amount): + self.add_credit(amount, userto) + self.withdraw_credit(amount, userfrom) + def buy_for_cash(self, item, amount = 1): # Buy: Currency conversion from item currency to shop currency (buy, sell) = item.currency.rates(self.currency) From 76bb2cac98a3ff33307f97e169d85d24b01420d4 Mon Sep 17 00:00:00 2001 From: Petr Baudis Date: Sun, 13 Dec 2015 16:10:34 +0100 Subject: [PATCH 016/139] brmbar-cli help: Nicer inventorization split --- brmbar3/brmbar-cli.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/brmbar3/brmbar-cli.py b/brmbar3/brmbar-cli.py index cb4cac9..a2d6f6a 100755 --- a/brmbar3/brmbar-cli.py +++ b/brmbar3/brmbar-cli.py @@ -31,15 +31,18 @@ Usage: brmbar-cli.py COMMAND ARGS... screen of the GUI. adduser USER Add user (debt) account with given username. - inventory ITEM1 NEW_AMOUNT1 ITEM2 NEW_AMOUNT2 - Inventory recounting (fixing the number of items) - inventory-interactive - Launches interactive mode for performing inventory with barcode reader - changecash AMT - Fixes the cash and puts money difference into excess or deficit account - consolidate - Wraps up inventory + cash recounting, transferring the excess and - deficit accounts balance to the profits account and resetting them + +3. Inventorization + + inventory ITEM1 NEW_AMOUNT1 ITEM2 NEW_AMOUNT2 + Inventory recounting (fixing the number of items) + inventory-interactive + Launches interactive mode for performing inventory with barcode reader + changecash AMT + Fixes the cash and puts money difference into excess or deficit account + consolidate + Wraps up inventory + cash recounting, transferring the excess and + deficit accounts balance to the profits account and resetting them USER and ITEM may be barcodes or account ids. AMT may be both positive and negative amount (big difference to other From feec2c9ac42de3d202ec64802f4df264456d27cc Mon Sep 17 00:00:00 2001 From: brmbar Date: Sun, 13 Dec 2015 16:36:21 +0100 Subject: [PATCH 017/139] brmbar-cli restock: Print correct old balance --- brmbar3/brmbar-cli.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/brmbar3/brmbar-cli.py b/brmbar3/brmbar-cli.py index a2d6f6a..ca99a73 100755 --- a/brmbar3/brmbar-cli.py +++ b/brmbar3/brmbar-cli.py @@ -208,9 +208,10 @@ elif sys.argv[1] == "restock": print ("Invalid number of parameters, check your parameters.") else: iacct = load_item(sys.argv[2]) + oldbal = iacct.balance() amt = int(sys.argv[3]) cash = shop.buy_for_cash(iacct, amt); - print("Old amount {}, increased by {}, take {} from cashbox".format(iacct.balance(), amt, cash)) + print("Old amount {}, increased by {}, take {} from cashbox".format(oldbal, amt, cash)) else: From 3ea61f01d7b18de9c0b385f4458ad51c502e6b9f Mon Sep 17 00:00:00 2001 From: brmbar Date: Sun, 13 Dec 2015 16:36:50 +0100 Subject: [PATCH 018/139] brmbar-cli undo: New feature --- brmbar3/brmbar-cli.py | 7 +++++++ brmbar3/brmbar/Shop.py | 15 +++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/brmbar3/brmbar-cli.py b/brmbar3/brmbar-cli.py index ca99a73..28fbe87 100755 --- a/brmbar3/brmbar-cli.py +++ b/brmbar3/brmbar-cli.py @@ -31,6 +31,9 @@ Usage: brmbar-cli.py COMMAND ARGS... screen of the GUI. adduser USER Add user (debt) account with given username. + undo TRANSID + Commit a transaction that reverses all splits of a transaction with + a given id (to find out that id: select * from transaction_cashsums;) 3. Inventorization @@ -152,6 +155,10 @@ elif sys.argv[1] == "adduser": acct.add_barcode(sys.argv[2]) # will commit print("{}: id {}".format(acct.name, acct.id)); +elif sys.argv[1] == "undo": + newtid = shop.undo(int(sys.argv[2])) + print("Transaction %d undone by reverse transaction %d" % (int(sys.argv[2]), newtid)) + elif sys.argv[1] == "inventory": if (len(sys.argv) % 2 != 0 or len(sys.argv) < 4): print ("Invalid number of parameters, count your parameters.") diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index 55c26ba..40b7872 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -177,6 +177,7 @@ class Shop: item.credit(transaction, 0, "Inventory fix - amount was correct") self.db.commit() return False + def fix_cash(self, amount): amount_in_reality = amount amount_in_system = self.cash.balance() @@ -196,6 +197,7 @@ class Shop: return True else: return False + def consolidate(self): transaction = self._transaction(description = "BrmBar inventory consolidation") @@ -210,3 +212,16 @@ class Shop: self.deficit.credit(transaction, deficit_balance, "Deficit balance removed from profit.") self.profits.credit(transaction, deficit_balance, "Deficit balance removed from profit.") self.db.commit() + + def undo(self, oldtid): + description = self.db.execute_and_fetch("SELECT description FROM transactions WHERE id = %s", [oldtid])[0] + description = 'undo %d (%s)' % (oldtid, description) + + transaction = self._transaction(description=description) + for split in self.db.execute_and_fetchall("SELECT id, side, account, amount, memo FROM transaction_splits WHERE transaction = %s", [oldtid]): + splitid, side, account, amount, memo = split + memo = 'undo %d (%s)' % (splitid, memo) + amount = -amount + self.db.execute("INSERT INTO transaction_splits (transaction, side, account, amount, memo) VALUES (%s, %s, %s, %s, %s)", [transaction, side, account, amount, memo]) + self.db.commit() + return transaction From 7a8e4ef794c2d65bee139d54fb1f7cffbf226155 Mon Sep 17 00:00:00 2001 From: Pavel Ruzicka Date: Tue, 15 Dec 2015 02:54:46 +0100 Subject: [PATCH 019/139] mplayer -really-quiet --- brmbar3/alert.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/brmbar3/alert.sh b/brmbar3/alert.sh index 0b643bc..05496e6 100755 --- a/brmbar3/alert.sh +++ b/brmbar3/alert.sh @@ -1,7 +1,7 @@ #!/bin/sh case $1 in -alert) mplayer ~/trombone.wav & ;; -limit) mplayer ~/much.wav & ;; -charge) mplayer ~/charge.wav & ;; +alert) mplayer -really-quiet ~/trombone.wav & ;; +limit) mplayer -really-quiet ~/much.wav & ;; +charge) mplayer -really-quiet ~/charge.wav & ;; esac From edc99f1ff9ed2f9c95b35b9080c544c5ac8193de Mon Sep 17 00:00:00 2001 From: brmbar Date: Mon, 4 Jan 2016 20:35:43 +0100 Subject: [PATCH 020/139] brmbar-cli.py docs: All commands are implemented --- brmbar3/brmbar-cli.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/brmbar3/brmbar-cli.py b/brmbar3/brmbar-cli.py index 28fbe87..427c4dd 100755 --- a/brmbar3/brmbar-cli.py +++ b/brmbar3/brmbar-cli.py @@ -53,9 +53,7 @@ user interfaces; you can e.g. undo a sale!). For users, you can use their name as USER as their username is also the barcode. For items, use listitems command first -to find out the item id. - -Commands prefixed with ! are not implemented yet.""") +to find out the item id.""") sys.exit(1) From 6b67dd372e92d82176178dd6a49103bdadbf1b87 Mon Sep 17 00:00:00 2001 From: brmbar Date: Mon, 4 Jan 2016 20:39:51 +0100 Subject: [PATCH 021/139] brmbar-cli.py inventory-interactive: Try to detect mistaken barcode scan --- brmbar3/brmbar-cli.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/brmbar3/brmbar-cli.py b/brmbar3/brmbar-cli.py index 427c4dd..1e63adf 100755 --- a/brmbar3/brmbar-cli.py +++ b/brmbar3/brmbar-cli.py @@ -182,16 +182,18 @@ elif sys.argv[1] == "inventory-interactive": break else: iacct = brmbar.Account.load_by_barcode(db, barcode) - amount = str(input("What is the amount of {} in reality current is {}:".format(iacct.name, iacct.balance()))) + amount = str(input("What is the amount of {} in reality (expected: {} pcs):".format(iacct.name, iacct.balance()))) if amount == "": break + elif int(amount) > 10000: + print("Ignoring too high amount {}, assuming barcode was mistakenly scanned instead".format(amount)) else: iamt = int(amount) print("Current state {} (id {}): {} pcs".format(iacct.name, iacct.id, iacct.balance())) if shop.fix_inventory(item = iacct, amount = iamt): print("New state {} (id {}): {} pcs".format(iacct.name, iacct.id, iacct.balance())) else: - print ("No action needed amount is correct.") + print("No action needed, amount is correct.") print("End of processing. Bye") elif sys.argv[1] == "changecash": if (len(sys.argv) != 3): From 2bbb46d4eab6f74188d1860e8a4334dc46fc5d0f Mon Sep 17 00:00:00 2001 From: brmbar Date: Mon, 4 Jan 2016 20:41:19 +0100 Subject: [PATCH 022/139] brmbar-cli.py inventory-interactive: Tidy up code --- brmbar3/brmbar-cli.py | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/brmbar3/brmbar-cli.py b/brmbar3/brmbar-cli.py index 1e63adf..0b4fc42 100755 --- a/brmbar3/brmbar-cli.py +++ b/brmbar3/brmbar-cli.py @@ -174,27 +174,26 @@ elif sys.argv[1] == "inventory": elif sys.argv[1] == "inventory-interactive": print("Inventory interactive mode. To exit interactive mode just enter empty barcode") - keep_entering = True - while keep_entering: + while True: barcode = str(input("Enter barcode:")) fuckyou = input("fuckyou") if barcode == "": break + iacct = brmbar.Account.load_by_barcode(db, barcode) + amount = str(input("What is the amount of {} in reality (expected: {} pcs):".format(iacct.name, iacct.balance()))) + if amount == "": + break + elif int(amount) > 10000: + print("Ignoring too high amount {}, assuming barcode was mistakenly scanned instead".format(amount)) else: - iacct = brmbar.Account.load_by_barcode(db, barcode) - amount = str(input("What is the amount of {} in reality (expected: {} pcs):".format(iacct.name, iacct.balance()))) - if amount == "": - break - elif int(amount) > 10000: - print("Ignoring too high amount {}, assuming barcode was mistakenly scanned instead".format(amount)) + iamt = int(amount) + print("Current state {} (id {}): {} pcs".format(iacct.name, iacct.id, iacct.balance())) + if shop.fix_inventory(item = iacct, amount = iamt): + print("New state {} (id {}): {} pcs".format(iacct.name, iacct.id, iacct.balance())) else: - iamt = int(amount) - print("Current state {} (id {}): {} pcs".format(iacct.name, iacct.id, iacct.balance())) - if shop.fix_inventory(item = iacct, amount = iamt): - print("New state {} (id {}): {} pcs".format(iacct.name, iacct.id, iacct.balance())) - else: - print("No action needed, amount is correct.") + print("No action needed, amount is correct.") print("End of processing. Bye") + elif sys.argv[1] == "changecash": if (len(sys.argv) != 3): print ("Invalid number of parameters, check your parameters.") @@ -205,11 +204,13 @@ elif sys.argv[1] == "changecash": print("New Cash is : {}".format(shop.cash.balance_str())) else: print ("No action needed amount is the same.") + elif sys.argv[1] == "consolidate": if (len(sys.argv) != 2): print ("Invalid number of parameters, check your parameters.") else: shop.consolidate() + elif sys.argv[1] == "restock": if (len(sys.argv) != 4): print ("Invalid number of parameters, check your parameters.") From 9e95724556a511f30f12fa01bc4ce8de99f7cab4 Mon Sep 17 00:00:00 2001 From: brmbar Date: Mon, 4 Jan 2016 20:49:08 +0100 Subject: [PATCH 023/139] brmbar-cli.py sellitem: Allow selling for cash --- brmbar3/brmbar-cli.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/brmbar3/brmbar-cli.py b/brmbar3/brmbar-cli.py index 0b4fc42..86332e2 100755 --- a/brmbar3/brmbar-cli.py +++ b/brmbar3/brmbar-cli.py @@ -15,7 +15,7 @@ Usage: brmbar-cli.py COMMAND ARGS... 1. Commands pertaining the standard operation showcredit USER changecredit USER +-AMT - sellitem USER ITEM +-AMT + sellitem {USER|"cash"} ITEM +-AMT You can use negative AMT to undo a sale. restock ITEM AMT userinfo USER @@ -105,14 +105,20 @@ elif sys.argv[1] == "changecredit": print("{}: {}".format(acct.name, acct.negbalance_str())) elif sys.argv[1] == "sellitem": - uacct = load_user(sys.argv[2]) + if sys.argv[2] == "cash": + uacct = shop.cash + else: + uacct = load_user(sys.argv[2]) iacct = load_item(sys.argv[3]) amt = int(sys.argv[4]) if amt > 0: - shop.sell(item = iacct, user = uacct, amount = amt) + if uacct == shop.cash: + shop.sell_for_cash(item = iacct, amount = amt) + else: + shop.sell(item = iacct, user = uacct, amount = amt) elif amt < 0: shop.undo_sale(item = iacct, user = uacct, amount = -amt) - print("{}: {}".format(uacct.name, uacct.negbalance_str())) + print("{}: {}".format(uacct.name, uacct.balance_str() if uacct == shop.cash else uacct.negbalance_str())) print("{}: {}".format(iacct.name, iacct.balance_str())) elif sys.argv[1] == "userinfo": From 4494dafbb889c62987b434be8abefa06b7a32b5b Mon Sep 17 00:00:00 2001 From: brmbar Date: Mon, 4 Jan 2016 21:04:35 +0100 Subject: [PATCH 024/139] Shop.inventory_balance(): Tidy up, commented out code for easy per-item balance dumps --- brmbar3/brmbar/Shop.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index 40b7872..4822bdd 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -135,13 +135,16 @@ class Shop: # might have been bought for a different price! Therefore, # we need to replace the command below with a complex SQL # statement that will... ugh, accounting is hard! - balance += inv.balance() * inv.currency.rates(self.currency)[0] + b = inv.balance() * inv.currency.rates(self.currency)[0] + # if b != 0: + # print(str(b) + ',' + inv.name) + balance += b return balance # XXX bypass hack def inventory_balance_str(self): - return "XXX" -# return self.currency.str(self.inventory_balance()) + # return self.currency.str(self.inventory_balance()) + return "XXX" def account_list(self, acctype, like_str="%%"): """list all accounts (people or items, as per acctype)""" From e3a4fe880d3221d863685bdcb2ef3ded75949364 Mon Sep 17 00:00:00 2001 From: brmbar Date: Mon, 4 Jan 2016 21:04:49 +0100 Subject: [PATCH 025/139] brmbar-cli.py docs: Two examples --- brmbar3/brmbar-cli.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/brmbar3/brmbar-cli.py b/brmbar3/brmbar-cli.py index 86332e2..be38c5f 100755 --- a/brmbar3/brmbar-cli.py +++ b/brmbar3/brmbar-cli.py @@ -53,7 +53,21 @@ user interfaces; you can e.g. undo a sale!). For users, you can use their name as USER as their username is also the barcode. For items, use listitems command first -to find out the item id.""") +to find out the item id. + +EXAMPLES: + +Transfer 35Kc from pasky to sachy: + + $ ./brmbar-cli.py changecredit pasky -35 + $ ./brmbar-cli.py changecredit sachy +35 + +Buy one RaspberryPi for cash from commandline: + + $ ./brmbar-cli.py listitems | grep -i raspberry + Raspberry Pi 2 1277 1.00 pcs + $ ./brmbar-cli.py sellitem cash 1277 1 +""") sys.exit(1) From 0c349f6c6abb8b047134d93d95bd018ccd423ad4 Mon Sep 17 00:00:00 2001 From: Petr Baudis Date: Tue, 5 Jan 2016 22:00:03 +0100 Subject: [PATCH 026/139] USAGE: A few new scenarios for Administrative Usage --- brmbar3/USAGE.md | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/brmbar3/USAGE.md b/brmbar3/USAGE.md index 5821232..5f31c5e 100644 --- a/brmbar3/USAGE.md +++ b/brmbar3/USAGE.md @@ -68,12 +68,26 @@ Administrative Usage account (and so will typing "joehacker" on a physical keyboard). * If your inventory stock count or cash box amount does not match -the in-system data, you will need to make a corrective transaction. -In the future, brmbar-cli.py will support this, but there is no -implementation yet; it's not entirely clear yet what is the proper -way to do this from the accounting standpoint. In the meantime, you -can use SQL INSERTs to manually create a transaction with appropriate -transaction splits (see doc/architecture for details on splits). + the in-system data, you will need to make a corrective transaction. + To fix cash amount to reality in which you counted 1234Kč, use + + ./brmbar-cli.py fixcash 1234 + + whereas to fix amount of a particular stock, use + + ./brmbar-cli.py inventory-interactive + + then scan the item barcode and then enter the right amount. + +* If you want to view recent transactions, run + + psql brmbar + select * from transaction_cashsums; + +* If you want to undo a transaction, get its id (using the select above) + and run + + ./brmbar-cli.py undo ID Useful SQL queries From 11eaecfccf23b3d3c6a9fc362d383ede0aaaea91 Mon Sep 17 00:00:00 2001 From: Petr Baudis Date: Wed, 6 Jan 2016 02:00:56 +0100 Subject: [PATCH 027/139] brmbar-cli changecash -> fixcash (changecash remains an alias) --- brmbar3/brmbar-cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/brmbar3/brmbar-cli.py b/brmbar3/brmbar-cli.py index be38c5f..d12081c 100755 --- a/brmbar3/brmbar-cli.py +++ b/brmbar3/brmbar-cli.py @@ -41,7 +41,7 @@ Usage: brmbar-cli.py COMMAND ARGS... Inventory recounting (fixing the number of items) inventory-interactive Launches interactive mode for performing inventory with barcode reader - changecash AMT + fixcash AMT Fixes the cash and puts money difference into excess or deficit account consolidate Wraps up inventory + cash recounting, transferring the excess and @@ -214,7 +214,7 @@ elif sys.argv[1] == "inventory-interactive": print("No action needed, amount is correct.") print("End of processing. Bye") -elif sys.argv[1] == "changecash": +elif sys.argv[1] == "fixcash" or sys.argv[1] == "changecash": if (len(sys.argv) != 3): print ("Invalid number of parameters, check your parameters.") else: From 9a700885912f163714ddd2809d0533d815b8f2c5 Mon Sep 17 00:00:00 2001 From: Petr Baudis Date: Wed, 6 Jan 2016 05:02:24 +0100 Subject: [PATCH 028/139] USAGE: Fix pre within lists --- brmbar3/USAGE.md | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/brmbar3/USAGE.md b/brmbar3/USAGE.md index 5f31c5e..fd02e0e 100644 --- a/brmbar3/USAGE.md +++ b/brmbar3/USAGE.md @@ -95,32 +95,32 @@ Useful SQL queries * Compute sum of sold stock: - select sum(amount) from transactions - left join transaction_splits on transaction_splits.transaction = transactions.id - where description like '% sale %' and side = 'debit'; + select sum(amount) from transactions + left join transaction_splits on transaction_splits.transaction = transactions.id + where description like '% sale %' and side = 'debit'; * List of items not covered by inventory check: - select * from account_balances - where id not in (select account from transactions - left join transaction_splits on transaction_splits.transaction = transactions.id - where description like '% inventory %') - and acctype = 'inventory'; + select * from account_balances + where id not in (select account from transactions + left join transaction_splits on transaction_splits.transaction = transactions.id + where description like '% inventory %') + and acctype = 'inventory'; * List all cash transactions: - select time, transactions.id, description, responsible, amount from transactions - left join transaction_splits on transaction_splits.transaction = transactions.id - where transaction_splits.account = 1; + select time, transactions.id, description, responsible, amount from transactions + left join transaction_splits on transaction_splits.transaction = transactions.id + where transaction_splits.account = 1; * List all inventory items ordered by their cummulative worth: - select foo.*, foo.rate * -foo.crbalance as worth from - (select account_balances.*, - (select exchange_rates.rate from exchange_rates, accounts - where exchange_rates.target = accounts.currency - and accounts.id = account_balances.id - order by exchange_rates.valid_since limit 1) as rate - from account_balances where account_balances.acctype = 'inventory') - as foo order by worth; + select foo.*, foo.rate * -foo.crbalance as worth from + (select account_balances.*, + (select exchange_rates.rate from exchange_rates, accounts + where exchange_rates.target = accounts.currency + and accounts.id = account_balances.id + order by exchange_rates.valid_since limit 1) as rate + from account_balances where account_balances.acctype = 'inventory') + as foo order by worth; From 2bfd1796d01cd008dc386b20ef45dc58ca1e3756 Mon Sep 17 00:00:00 2001 From: brmbar Date: Thu, 7 Jan 2016 05:06:37 +0100 Subject: [PATCH 029/139] brmbar-cli stats: Separate credit and overflow --- brmbar3/brmbar-cli.py | 5 +++-- brmbar3/brmbar/Shop.py | 8 +++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/brmbar3/brmbar-cli.py b/brmbar3/brmbar-cli.py index d12081c..9ad8e7c 100755 --- a/brmbar3/brmbar-cli.py +++ b/brmbar3/brmbar-cli.py @@ -162,9 +162,10 @@ elif sys.argv[1] == "listitems": elif sys.argv[1] == "stats": print("Cash: {}".format(shop.cash.balance_str())) - print("Profit: {}".format(shop.profits.balance_str())) - print("Credit: {}".format(shop.credit_negbalance_str())) + print("Overflow: {}".format(shop.currency.str(shop.credit_balance(overflow='only')))) print("Inventory: {}".format(shop.inventory_balance_str())) + print("Credit: {}".format(shop.credit_negbalance_str(overflow='exclude'))) + print("Profit: {}".format(shop.profits.balance_str())) print("Excess: {}".format(shop.excess.negbalance_str())) print("Deficit: {}".format(shop.deficit.balance_str())) diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index 4822bdd..6027084 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -106,7 +106,7 @@ class Shop: transaction = transaction[0] return transaction - def credit_balance(self): + def credit_balance(self, overflow=None): # We assume all debt accounts share a currency sumselect = """ SELECT SUM(ts.amount) @@ -114,13 +114,15 @@ class Shop: LEFT JOIN transaction_splits AS ts ON a.id = ts.account WHERE a.acctype = %s AND ts.side = %s """ + if overflow is not None: + sumselect += ' AND a.name ' + ('NOT ' if overflow == 'exclude' else '') + ' LIKE \'%%-overflow\'' cur = self.db.execute_and_fetch(sumselect, ["debt", 'debit']) debit = cur[0] or 0 credit = self.db.execute_and_fetch(sumselect, ["debt", 'credit']) credit = credit[0] or 0 return debit - credit - def credit_negbalance_str(self): - return self.currency.str(-self.credit_balance()) + def credit_negbalance_str(self, overflow=None): + return self.currency.str(-self.credit_balance(overflow=overflow)) # XXX causing extra heavy delay ( thousands of extra SQL queries ), disabled def inventory_balance(self): From c5becfcabee3575c0a7df469d7a854689de18311 Mon Sep 17 00:00:00 2001 From: brmbar Date: Thu, 7 Jan 2016 05:20:50 +0100 Subject: [PATCH 030/139] brmbar-cli stats: sum excess and deficit to Fixups --- brmbar3/brmbar-cli.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/brmbar3/brmbar-cli.py b/brmbar3/brmbar-cli.py index 9ad8e7c..bb49daa 100755 --- a/brmbar3/brmbar-cli.py +++ b/brmbar3/brmbar-cli.py @@ -166,8 +166,10 @@ elif sys.argv[1] == "stats": print("Inventory: {}".format(shop.inventory_balance_str())) print("Credit: {}".format(shop.credit_negbalance_str(overflow='exclude'))) print("Profit: {}".format(shop.profits.balance_str())) - print("Excess: {}".format(shop.excess.negbalance_str())) - print("Deficit: {}".format(shop.deficit.balance_str())) + print("Fixups: {} (excess {}, deficit {})".format( + -shop.excess.balance() - shop.deficit.balance(), + shop.excess.negbalance_str(), + shop.deficit.balance_str())) elif sys.argv[1] == "adduser": acct = brmbar.Account.create(db, sys.argv[2], brmbar.Currency.load(db, id = 1), 'debt') From c419c91a40e21777ddcd8dcf395fa8471a8c2518 Mon Sep 17 00:00:00 2001 From: brmbar Date: Thu, 7 Jan 2016 05:26:13 +0100 Subject: [PATCH 031/139] brmbar-cli stats: separate material, logical accounts --- brmbar3/brmbar-cli.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/brmbar3/brmbar-cli.py b/brmbar3/brmbar-cli.py index bb49daa..e967f0a 100755 --- a/brmbar3/brmbar-cli.py +++ b/brmbar3/brmbar-cli.py @@ -161,9 +161,11 @@ elif sys.argv[1] == "listitems": print("{}\t{}\t{} pcs".format(acct.name, acct.id, acct.balance())) elif sys.argv[1] == "stats": + print("--- Material Assets ---") print("Cash: {}".format(shop.cash.balance_str())) print("Overflow: {}".format(shop.currency.str(shop.credit_balance(overflow='only')))) print("Inventory: {}".format(shop.inventory_balance_str())) + print("--- Logical Accounts ---") print("Credit: {}".format(shop.credit_negbalance_str(overflow='exclude'))) print("Profit: {}".format(shop.profits.balance_str())) print("Fixups: {} (excess {}, deficit {})".format( From 0e306912d7c0a83eab63c8622846e13130f2e339 Mon Sep 17 00:00:00 2001 From: Petr Baudis Date: Thu, 7 Jan 2016 05:26:40 +0100 Subject: [PATCH 032/139] USAGE: Document brmbar-cli stats --- brmbar3/USAGE.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/brmbar3/USAGE.md b/brmbar3/USAGE.md index fd02e0e..d749a3a 100644 --- a/brmbar3/USAGE.md +++ b/brmbar3/USAGE.md @@ -89,6 +89,33 @@ Administrative Usage ./brmbar-cli.py undo ID +* If you want to get overview of the financial situation, run + + ./brmbar-cli.py stats + + The following items represent "material", "tangible" assets: + + * Cash - how much should be in the money box + * Overflow - how much cash is stored in overflow credit accounts (pockets of admins) + * Inventory - how much worth (buy price) is the current inventory stock + + I.e., cash plus overflow plus inventory is how much brmbar is worth + and cash plus overflow is how much brmbar can spend right now. + + The following items represent "virtual" accounts which determine + the logical composition of the assets: + + * Credit - sum of all credit accounts, i.e. money stored in brmbar by its users; + i.e. how much of the assets is users' money + * Profit - accumulated profit made by brmbar on buy/sell margins (but receipts + and inventory deficits are subtracted); i.e. how much of the assets is brmbar's + own money + * Fixups - sum of gains and losses accrued by inventory fixups, i.e. stemming + from differences between accounting and reality - positive is good, negative + is bad; this amount is added to profit on consolidation + + The total worth of the material and virtual accounts should be equal. + Useful SQL queries ------------------ From a9e72d736cef4770bd12a924bca5aa9d254074fd Mon Sep 17 00:00:00 2001 From: brmbar Date: Sun, 10 Jan 2016 21:03:59 +0100 Subject: [PATCH 033/139] brmbar-cli userlog --- brmbar3/SQL | 5 ++--- brmbar3/brmbar-cli.py | 9 +++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/brmbar3/SQL b/brmbar3/SQL index 7e640a6..b25908f 100644 --- a/brmbar3/SQL +++ b/brmbar3/SQL @@ -107,8 +107,7 @@ CREATE VIEW transaction_nicesplits AS FROM transaction_splits AS ts LEFT JOIN accounts AS a ON a.id = ts.account ORDER BY ts.id; --- List transactions with summary information regarding their cash element --- (except in case of transfers between cash and debt accounts, which will cancel out). +-- List transactions with summary information regarding their cash element. CREATE VIEW transaction_cashsums AS SELECT t.id AS id, t.time AS time, SUM(credit_cash) AS cash_credit, SUM(debit_cash) AS cash_debit, t.description AS description FROM transactions AS t @@ -124,4 +123,4 @@ CREATE VIEW transaction_cashsums AS WHERE a.currency = (SELECT currency FROM accounts WHERE name = 'BrmBar Cash') AND a.acctype IN ('cash', 'debt') AND dts.amount > 0) debit ON dts_t = t.id - GROUP BY t.id ORDER BY t.id; + GROUP BY t.id ORDER BY t.id DESC; diff --git a/brmbar3/brmbar-cli.py b/brmbar3/brmbar-cli.py index e967f0a..4ad26ab 100755 --- a/brmbar3/brmbar-cli.py +++ b/brmbar3/brmbar-cli.py @@ -19,6 +19,7 @@ Usage: brmbar-cli.py COMMAND ARGS... You can use negative AMT to undo a sale. restock ITEM AMT userinfo USER + userlog USER TIMESTAMP iteminfo ITEM 2. Management commands @@ -142,6 +143,14 @@ elif sys.argv[1] == "userinfo": res = db.execute_and_fetchall("SELECT barcode FROM barcodes WHERE account = %s", [acct.id]) print("Barcodes: " + ", ".join(map((lambda r: r[0]), res))) +elif sys.argv[1] == "userlog": + acct = load_user(sys.argv[2]) + timestamp = sys.argv[3] + + res = db.execute_and_fetchall("SELECT transactions.time,transactions.description FROM transactions INNER JOIN accounts ON accounts.id=transactions.responsible WHERE accounts.name=%s and time > TIMESTAMP %s ORDER BY time", [acct.name,timestamp]) + for transaction in res: + print("{}\t{}\t".format(transaction[0],transaction[1])) + elif sys.argv[1] == "iteminfo": acct = load_item(sys.argv[2]) print("{} (id {}): {} pcs".format(acct.name, acct.id, acct.balance())) From 8f6776f3d0fda4ab2e36b84f038c109006b7378d Mon Sep 17 00:00:00 2001 From: Ruzicka Pavel Date: Sun, 10 Jan 2016 21:07:47 +0100 Subject: [PATCH 034/139] brmbar-cli userlog --- brmbar3/brmbar-cli.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/brmbar3/brmbar-cli.py b/brmbar3/brmbar-cli.py index e967f0a..4ad26ab 100755 --- a/brmbar3/brmbar-cli.py +++ b/brmbar3/brmbar-cli.py @@ -19,6 +19,7 @@ Usage: brmbar-cli.py COMMAND ARGS... You can use negative AMT to undo a sale. restock ITEM AMT userinfo USER + userlog USER TIMESTAMP iteminfo ITEM 2. Management commands @@ -142,6 +143,14 @@ elif sys.argv[1] == "userinfo": res = db.execute_and_fetchall("SELECT barcode FROM barcodes WHERE account = %s", [acct.id]) print("Barcodes: " + ", ".join(map((lambda r: r[0]), res))) +elif sys.argv[1] == "userlog": + acct = load_user(sys.argv[2]) + timestamp = sys.argv[3] + + res = db.execute_and_fetchall("SELECT transactions.time,transactions.description FROM transactions INNER JOIN accounts ON accounts.id=transactions.responsible WHERE accounts.name=%s and time > TIMESTAMP %s ORDER BY time", [acct.name,timestamp]) + for transaction in res: + print("{}\t{}\t".format(transaction[0],transaction[1])) + elif sys.argv[1] == "iteminfo": acct = load_item(sys.argv[2]) print("{} (id {}): {} pcs".format(acct.name, acct.id, acct.balance())) From 466b92e7c20ed13f72d0d5bf1390c60dc32ac586 Mon Sep 17 00:00:00 2001 From: brmbar Date: Fri, 19 Aug 2016 04:26:40 +0200 Subject: [PATCH 035/139] uncommitted changes from other people... --- brmbar3/SQL | 5 +++-- brmbar3/brmbar-cli.py | 4 ++-- brmbar3/brmbar-gui-qt4/MainPage.qml | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/brmbar3/SQL b/brmbar3/SQL index b25908f..a61dba8 100644 --- a/brmbar3/SQL +++ b/brmbar3/SQL @@ -109,7 +109,7 @@ CREATE VIEW transaction_nicesplits AS -- List transactions with summary information regarding their cash element. CREATE VIEW transaction_cashsums AS - SELECT t.id AS id, t.time AS time, SUM(credit_cash) AS cash_credit, SUM(debit_cash) AS cash_debit, t.description AS description + SELECT t.id AS id, t.time AS time, SUM(credit_cash) AS cash_credit, SUM(debit_cash) AS cash_debit, a.name AS responsible, t.description AS description FROM transactions AS t LEFT JOIN (SELECT cts.amount AS credit_cash, cts.transaction AS cts_t FROM transaction_nicesplits AS cts @@ -123,4 +123,5 @@ CREATE VIEW transaction_cashsums AS WHERE a.currency = (SELECT currency FROM accounts WHERE name = 'BrmBar Cash') AND a.acctype IN ('cash', 'debt') AND dts.amount > 0) debit ON dts_t = t.id - GROUP BY t.id ORDER BY t.id DESC; + LEFT JOIN accounts AS a ON a.id = t.responsible + GROUP BY t.id, a.name ORDER BY t.id DESC; diff --git a/brmbar3/brmbar-cli.py b/brmbar3/brmbar-cli.py index 4ad26ab..b641e07 100755 --- a/brmbar3/brmbar-cli.py +++ b/brmbar3/brmbar-cli.py @@ -147,9 +147,9 @@ elif sys.argv[1] == "userlog": acct = load_user(sys.argv[2]) timestamp = sys.argv[3] - res = db.execute_and_fetchall("SELECT transactions.time,transactions.description FROM transactions INNER JOIN accounts ON accounts.id=transactions.responsible WHERE accounts.name=%s and time > TIMESTAMP %s ORDER BY time", [acct.name,timestamp]) + res = db.execute_and_fetchall("SELECT * FROM transaction_cashsums WHERE responsible=%s and time > TIMESTAMP %s ORDER BY time", [acct.name,timestamp]) for transaction in res: - print("{}\t{}\t".format(transaction[0],transaction[1])) + print('\t'.join([str(f) for f in transaction])) elif sys.argv[1] == "iteminfo": acct = load_item(sys.argv[2]) diff --git a/brmbar3/brmbar-gui-qt4/MainPage.qml b/brmbar3/brmbar-gui-qt4/MainPage.qml index 6e555eb..d11fd0f 100644 --- a/brmbar3/brmbar-gui-qt4/MainPage.qml +++ b/brmbar3/brmbar-gui-qt4/MainPage.qml @@ -58,6 +58,6 @@ Item { x: 65 y: 438 width: 1150 - text: "* Mroze a Termixy najdes v lednici *" + text: "* Za uklid brmlabu vam nabijeme kredit. *" } } From 1f3f1cdd8f9343dab6105514994c6185a2e81bc0 Mon Sep 17 00:00:00 2001 From: brmbar Date: Fri, 19 Aug 2016 04:37:04 +0200 Subject: [PATCH 036/139] disable exchange rate and balance in StockMgmt because of unbearable slowdown --- brmbar3/brmbar-gui-qt4.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/brmbar3/brmbar-gui-qt4.py b/brmbar3/brmbar-gui-qt4.py index a41fd2c..9ca11d0 100755 --- a/brmbar3/brmbar-gui-qt4.py +++ b/brmbar3/brmbar-gui-qt4.py @@ -39,6 +39,14 @@ class ShopAdapter(QtCore.QObject): map["price"] = str(sell) return map + def acct_inventory_map2(self, acct): + buy, sell = 666, 666 + map = acct.__dict__.copy() + map["balance"] = "{:.0f}".format(666) + map["buy_price"] = str(buy) + map["price"] = str(sell) + return map + def acct_cash_map(self, acct): map = acct.__dict__.copy() return map @@ -164,7 +172,7 @@ class ShopAdapter(QtCore.QObject): @QtCore.Slot('QVariant', result='QVariant') def itemList(self, query): - alist = [ self.acct_inventory_map(a) for a in shop.account_list("inventory", like_str="%%"+query+"%%") ] + alist = [ self.acct_inventory_map2(a) for a in shop.account_list("inventory", like_str="%%"+query+"%%") ] db.commit() return alist From e2e6632df085123e61e84d619ef94bc382923d02 Mon Sep 17 00:00:00 2001 From: brmbar Date: Mon, 2 Apr 2018 01:40:37 +0200 Subject: [PATCH 037/139] sample crontab file --- brmbar3/crontab | 15 +++++++++++++++ brmbar3/dluhy.sh | 3 +++ brmbar3/log.sh | 6 ++++++ 3 files changed, 24 insertions(+) create mode 100644 brmbar3/crontab create mode 100755 brmbar3/dluhy.sh create mode 100755 brmbar3/log.sh diff --git a/brmbar3/crontab b/brmbar3/crontab new file mode 100644 index 0000000..2e66748 --- /dev/null +++ b/brmbar3/crontab @@ -0,0 +1,15 @@ +# cleanup bounty +*/5 * * * * ~/brmbar/brmbar3/uklid-watchdog.sh +0 0 * * 1 ~/brmbar/brmbar3/uklid-refill.sh +# overall summary +5 4 * * * ~/brmbar/brmbar3/daily-summary.sh | mail -s "daily brmbar summary" yyy@yyy +# debt track +5 0 * * * ~/brmbar/brmbar3/dluhy.sh 2>/dev/null + +# per-user summary +1 0 * * * /home/brmlab/brmbar/brmbar3/log.sh yyy yyy@yyy + +# backup +6 * * * * echo "SELECT * FROM account_balances;" | psql brmbar | gzip -9 | ssh -Tp 110 -i /home/brmlab/.ssh/id_ecdsa jenda@coralmyn.hrach.eu +16 1 * * * pg_dump brmbar | gzip -9 | ssh -Tp 110 -i /home/brmlab/.ssh/id_ecdsa jenda@coralmyn.hrach.eu + diff --git a/brmbar3/dluhy.sh b/brmbar3/dluhy.sh new file mode 100755 index 0000000..5ec9adc --- /dev/null +++ b/brmbar3/dluhy.sh @@ -0,0 +1,3 @@ +p1=`echo -n "brmbar - dluhy: "; echo "SELECT name, crbalance FROM account_balances WHERE acctype = 'debt' AND crbalance < -100 AND name NOT LIKE '%overflow%' AND name NOT LIKE 'sachyo' ORDER BY crbalance ASC" | psql brmbar | tail -n +3 | grep '|' | tr -s " " | sed -e "s/ |/:/g" -e "s/$/;/" | tr -d "\n"` +p2=`echo "SELECT sum(crbalance) FROM account_balances WHERE acctype = 'debt' AND crbalance < 0 AND name NOT LIKE '%overflow%' AND name NOT LIKE 'sachyo'" | psql brmbar | tail -n +3 | head -n 1 | tr -s " "` +echo "$p1 total$p2 Kc" | ssh -p 110 -i /home/brmlab/.ssh/id_rsa jenda@coralmyn.hrach.eu diff --git a/brmbar3/log.sh b/brmbar3/log.sh new file mode 100755 index 0000000..b1afbdb --- /dev/null +++ b/brmbar3/log.sh @@ -0,0 +1,6 @@ +#!/bin/bash +p=`/home/brmlab/brmbar/brmbar3/brmbar-cli.py userlog "$1" yesterday` + +if [ -n "$p" ]; then + echo "$p" | mail -s "brmbar report" "$2" +fi From 6f3fc6767acd2fe9f776918f7989d3ed399af58a Mon Sep 17 00:00:00 2001 From: brmbar Date: Mon, 2 Apr 2018 01:40:46 +0200 Subject: [PATCH 038/139] db purge howto --- brmbar3/PURGE.txt | 64 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 brmbar3/PURGE.txt diff --git a/brmbar3/PURGE.txt b/brmbar3/PURGE.txt new file mode 100644 index 0000000..c5e5b30 --- /dev/null +++ b/brmbar3/PURGE.txt @@ -0,0 +1,64 @@ +How to "reset" the database - drop all history and keep only accounts with non-zero balance. + +Legend: +> - SQL commands +$ - shell commands + +Run the (full) inventory. + +Get number of the first inventory TX. + +> select id from account_balances where id in (select id from accounts where currency not in (select distinct currency from + transaction_nicesplits where transaction >= NUMBER_HERE and currency != 1 and memo like '%Inventory fix%') and acctype = 'inventory') and crbalance != 0 \g 'vynulovat' +$ ./brmbar-cli.py inventory `cat vynulovat | while read x; do echo $x 0; done` + +Backup the database +$ pg_dump brmbar > backup.sql + +Dump "> SELECT * FROM account_balances;" to file N. + +Dump inventory to file nastavit FIXME. + +Drop all transactions: +> delete from transaction_splits; +> delete from transactions; + +Restore inventory: +$ cat nastavit | while read acc p amt; do ./brmbar-cli.py inventory $acc `echo $amt | grep -oE "^[0-9-]+"`; done + +Restore cash balance: +$ cat N | grep debt | tr -s " " |cut -d \| -f 2,4 | while read acc p amt; do ./brmbar-cli.py changecredit $acc `echo $amt | grep -oE "^[0-9-]+"`; done + +Delete zero-balance accounts: +> delete from accounts where accounts.id not in (select id from account_balances); + +Delete orphaned barcodes: +> delete from barcodes where barcodes.account not in (select id from account_balances); + +Delete orphaned currencies and exchange rates: +> CREATE OR REPLACE VIEW "a_tmp" AS +SELECT ts.account AS id, accounts.name, accounts.acctype, accounts.currency AS fff, (- sum(CASE WHEN (ts.side = 'credit'::transaction_split_side) THEN (- ts.amount) ELSE ts.amount END)) AS crbalance FROM (transaction_splits ts LEFT JOIN accounts ON ((accounts.id = ts.account))) GROUP BY ts.account, accounts.name, accounts.id, accounts.acctype ORDER BY (- sum(CASE WHEN (ts.side = 'credit'::transaction_split_side) THEN (- ts.amount) ELSE ts.amount END)); + +> delete from exchange_rates where source not in (select fff from a_tmp); +> delete from currencies where id not in (select fff from a_tmp); + +> DROP VIEW "a_tmp"; + +Drop obsolete exchange rates: + +> delete from exchange_rates where + valid_since <> (SELECT max(valid_since) + FROM exchange_rates e + WHERE e.target = exchange_rates.target and e.source = exchange_rates.source) + +Restore system accounts: +> INSERT INTO "accounts" ("name", "currency", "acctype", "active") + VALUES ('BrmBar Profits', '1', 'income', '1'); +> INSERT INTO "accounts" ("name", "currency", "acctype", "active") + VALUES ('BrmBar Excess', '1', 'income', '1'); +> INSERT INTO "accounts" ("name", "currency", "acctype", "active") + VALUES ('BrmBar Deficit', '1', 'expense', '1'); +> INSERT INTO "accounts" ("name", "currency", "acctype", "active") + VALUES ('BrmBar Cash', '1', 'cash', '1'); + +Restart brmbar. From f061cc7f7b2a6413b154486fdb40f726beea2c23 Mon Sep 17 00:00:00 2001 From: brmbar Date: Mon, 2 Apr 2018 01:41:02 +0200 Subject: [PATCH 039/139] gitignore --- brmbar3/.gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/brmbar3/.gitignore b/brmbar3/.gitignore index bee8a64..4a5b2ab 100644 --- a/brmbar3/.gitignore +++ b/brmbar3/.gitignore @@ -1 +1,3 @@ __pycache__ +*.log +brmbar/*.pyc From a1c37cb69523b89664aec03ba19cc01857caf6c5 Mon Sep 17 00:00:00 2001 From: brmbar Date: Mon, 2 Apr 2018 01:41:24 +0200 Subject: [PATCH 040/139] brmbar-cli: restock by EAN (for automated restocking) --- brmbar3/brmbar-cli.py | 12 +++++++++--- brmbar3/uklid-watchdog.sh | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/brmbar3/brmbar-cli.py b/brmbar3/brmbar-cli.py index b641e07..cc5c6e3 100755 --- a/brmbar3/brmbar-cli.py +++ b/brmbar3/brmbar-cli.py @@ -97,6 +97,12 @@ def load_item(inp): exit(1) return acct +def load_item_by_barcode(inp): + acct = brmbar.Account.load_by_barcode(db, inp) + if acct.acctype != "inventory": + print("Bad EAN " + inp + " type " + acct.acctype, file=sys.stderr) + exit(1) + return acct db = Database.Database("dbname=brmbar") shop = brmbar.Shop.new_with_defaults(db) @@ -245,16 +251,16 @@ elif sys.argv[1] == "consolidate": else: shop.consolidate() -elif sys.argv[1] == "restock": +elif sys.argv[1] in {"restock", "restock_ean"}: if (len(sys.argv) != 4): print ("Invalid number of parameters, check your parameters.") else: - iacct = load_item(sys.argv[2]) + iacct = (load_item if sys.argv[1] == "restock" else load_item_by_barcode)(sys.argv[2]) oldbal = iacct.balance() amt = int(sys.argv[3]) cash = shop.buy_for_cash(iacct, amt); print("Old amount {}, increased by {}, take {} from cashbox".format(oldbal, amt, cash)) - + else: help() diff --git a/brmbar3/uklid-watchdog.sh b/brmbar3/uklid-watchdog.sh index c0fd55b..7f86870 100644 --- a/brmbar3/uklid-watchdog.sh +++ b/brmbar3/uklid-watchdog.sh @@ -13,6 +13,6 @@ if [ ! -z "$RES" ]; then if [ -z "$WINNER" ]; then WINNER="anonymous hunter" fi - echo "Brmlab cleanup bounty was claimed by $WINNER! Thanks!"|ssh jenda@fry.hrach.eu + echo "Brmlab cleanup bounty was claimed by $WINNER! Thanks!"|ssh -p 110 jenda@coralmyn.hrach.eu fi From 6749b2c97ae8cd7e351196a16ba6e22793b9dc3c Mon Sep 17 00:00:00 2001 From: brmbar Date: Mon, 2 Apr 2018 01:46:01 +0200 Subject: [PATCH 041/139] autostock.py --- brmbar3/autostock.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100755 brmbar3/autostock.py diff --git a/brmbar3/autostock.py b/brmbar3/autostock.py new file mode 100755 index 0000000..0e8afac --- /dev/null +++ b/brmbar3/autostock.py @@ -0,0 +1,42 @@ +#! /usr/bin/env python3 + +import argparse +import brmbar +import math +from brmbar import Database + +def main(): + parser = argparse.ArgumentParser(usage = "File format: EAN amount total_price name, e.g. 4001242002377 6 167.40 Chio Tortillas") + parser.add_argument("filename") + args = parser.parse_args() + + db = Database.Database("dbname=brmbar") + shop = brmbar.Shop.new_with_defaults(db) + currency = shop.currency + + # ... + total = 0 + with open(args.filename) as fin: + for line in fin: + split = line.split(" ") + ean, amount, price_total, name = split[0], int(split[1]), float(split[2]), " ".join(split[3:]) + name = name.strip() + + price_buy = price_total / amount + acct = brmbar.Account.load_by_barcode(db, ean) + if not acct: + print("Creating account for EAN {} '{}'".format(ean, name)) + invcurr = brmbar.Currency.create(db, name) + acct = brmbar.Account.create(db, name, invcurr, "inventory") + acct.add_barcode(ean) + price_sell = max(math.ceil(price_buy * 1.15), price_buy) + acct.currency.update_sell_rate(currency, price_sell) + acct.currency.update_buy_rate(currency, price_buy) + cash = shop.buy_for_cash(acct, amount) + total += cash + print("Increased by {}, take {} from cashbox".format(amount, cash)) + print("Total is {}".format(total)) + +if __name__ == "__main__": + main() + From dbd3835dfb33e017b842552d9d81799dd5b645ce Mon Sep 17 00:00:00 2001 From: niekt0 Date: Tue, 30 Jan 2024 20:18:07 +0100 Subject: [PATCH 042/139] fixed NaN issue in withdraw/charge --- brmbar3/brmbar-gui-qt4/ChargeCredit.qml | 13 +++++++++++-- brmbar3/brmbar-gui-qt4/Withdraw.qml | 13 +++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/brmbar3/brmbar-gui-qt4/ChargeCredit.qml b/brmbar3/brmbar-gui-qt4/ChargeCredit.qml index 8eda557..0ff8dc8 100644 --- a/brmbar3/brmbar-gui-qt4/ChargeCredit.qml +++ b/brmbar3/brmbar-gui-qt4/ChargeCredit.qml @@ -100,8 +100,17 @@ Item { } function chargeCredit() { - var balance = shop.chargeCredit(amount, userdbid) - status_text.setStatus("Charged! "+username+"'s credit is "+balance+".", "#ffff7c") + var balance=0 + if (!isNaN(amount)) { + if(amount>=0) { + balance = shop.chargeCredit(amount, userdbid) + status_text.setStatus("Charged "+amount+"! "+username+"'s credit is "+balance+".", "#ffff7c") + } else { + balance = shop.withdrawCredit((amount*(-1)), userdbid) + status_text.setStatus("Withdrawn "+amount+"! "+username+"'s credit is "+balance+".", "#ffff7c") + } + } loadPage("MainPage") + } } diff --git a/brmbar3/brmbar-gui-qt4/Withdraw.qml b/brmbar3/brmbar-gui-qt4/Withdraw.qml index a9e7700..b37b670 100644 --- a/brmbar3/brmbar-gui-qt4/Withdraw.qml +++ b/brmbar3/brmbar-gui-qt4/Withdraw.qml @@ -100,8 +100,17 @@ Item { } function withdrawCredit() { - var balance = shop.withdrawCredit(amount, userdbid) - status_text.setStatus("Withdrawn! "+username+"'s credit is "+balance+".", "#ffff7c") + var balance=0 + if (!isNaN(amount)) { + amount=(amount*1) + if(amount>=0) { + balance = shop.withdrawCredit(amount, userdbid) + status_text.setStatus("Withdrawn "+amount+"! "+username+"'s credit is "+balance+".", "#ffff7c") + } else { + balance = shop.chargeCredit((amount*(-1)),userdbid) + status_text.setStatus("Charged "+amount+"! "+username+"'s credit is "+balance+".", "#ffff7c") + } + } loadPage("MainPage") } } From e04d614e159dacbe443e22813893bcd5ee10b479 Mon Sep 17 00:00:00 2001 From: TMA Date: Sun, 30 Mar 2025 20:55:00 +0200 Subject: [PATCH 043/139] uncommitted changes from other people... again --- .gitignore | 1 + brmbar3/USEFUL.txt | 8 ++++++++ brmbar3/brmbar-gui-qt4/ChargeCredit.qml | 13 +++++++++++-- brmbar3/brmbar-gui-qt4/Withdraw.qml | 13 +++++++++++-- brmbar3/dluhy.sh | 2 +- 5 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 brmbar3/USEFUL.txt diff --git a/.gitignore b/.gitignore index 3268211..9eccd17 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ .*.sw? +*~ diff --git a/brmbar3/USEFUL.txt b/brmbar3/USEFUL.txt new file mode 100644 index 0000000..11c0071 --- /dev/null +++ b/brmbar3/USEFUL.txt @@ -0,0 +1,8 @@ +Accounts with multiple barcodes: + +SELECT accounts.name,barcodes.account,barcodes.barcode +FROM "barcodes" +join accounts on accounts.id = barcodes.account +where barcodes.account in (select a from (select count(*) as c, account as a from barcodes group by account) as dt where c > 1) +ORDER BY "account" DESC + diff --git a/brmbar3/brmbar-gui-qt4/ChargeCredit.qml b/brmbar3/brmbar-gui-qt4/ChargeCredit.qml index 8eda557..cdcc006 100644 --- a/brmbar3/brmbar-gui-qt4/ChargeCredit.qml +++ b/brmbar3/brmbar-gui-qt4/ChargeCredit.qml @@ -100,8 +100,17 @@ Item { } function chargeCredit() { - var balance = shop.chargeCredit(amount, userdbid) - status_text.setStatus("Charged! "+username+"'s credit is "+balance+".", "#ffff7c") + var balance=0 + if (!isNaN(amount)) { + if(amount>=0) { + balance = shop.chargeCredit(amount, userdbid) + status_text.setStatus("Charged "+amount+"! "+username+"'s credit is "+balance+".", "#ffff7c") + } else { + balance = shop.withdrawCredit((amount*(-1)), userdbid) + status_text.setStatus("Withdrawn "+amount+"! "+username+"'s credit is "+balance+".", "#ffff7c") + } + } loadPage("MainPage") + } } diff --git a/brmbar3/brmbar-gui-qt4/Withdraw.qml b/brmbar3/brmbar-gui-qt4/Withdraw.qml index a9e7700..2d8c49b 100644 --- a/brmbar3/brmbar-gui-qt4/Withdraw.qml +++ b/brmbar3/brmbar-gui-qt4/Withdraw.qml @@ -100,8 +100,17 @@ Item { } function withdrawCredit() { - var balance = shop.withdrawCredit(amount, userdbid) - status_text.setStatus("Withdrawn! "+username+"'s credit is "+balance+".", "#ffff7c") + var balance=0 + if (!isNaN(amount)) { + amount=(amount*1) + if(amount>=0) { + balance = shop.withdrawCredit(amount, userdbid) + status_text.setStatus("Withdrawn "+amount+"! "+username+"'s credit is "+balance+".", "#ffff7c") + } else { + balance = shop.chargeCredit((amount*(-1)),userdbid) + status_text.setStatus("Charged "+amount+"! "+username+"'s credit is "+balance+".", "#ffff7c") + } + } loadPage("MainPage") } } diff --git a/brmbar3/dluhy.sh b/brmbar3/dluhy.sh index 5ec9adc..61c2a46 100755 --- a/brmbar3/dluhy.sh +++ b/brmbar3/dluhy.sh @@ -1,3 +1,3 @@ p1=`echo -n "brmbar - dluhy: "; echo "SELECT name, crbalance FROM account_balances WHERE acctype = 'debt' AND crbalance < -100 AND name NOT LIKE '%overflow%' AND name NOT LIKE 'sachyo' ORDER BY crbalance ASC" | psql brmbar | tail -n +3 | grep '|' | tr -s " " | sed -e "s/ |/:/g" -e "s/$/;/" | tr -d "\n"` p2=`echo "SELECT sum(crbalance) FROM account_balances WHERE acctype = 'debt' AND crbalance < 0 AND name NOT LIKE '%overflow%' AND name NOT LIKE 'sachyo'" | psql brmbar | tail -n +3 | head -n 1 | tr -s " "` -echo "$p1 total$p2 Kc" | ssh -p 110 -i /home/brmlab/.ssh/id_rsa jenda@coralmyn.hrach.eu +echo "$p1 total$p2 Kc. https://www.elektro-obojky.cz/" | ssh -p 110 -i /home/brmlab/.ssh/id_rsa jenda@coralmyn.hrach.eu From 2f601a0b1ac9750e19373f267d36129870273b07 Mon Sep 17 00:00:00 2001 From: TMA Date: Fri, 11 Apr 2025 13:39:07 +0200 Subject: [PATCH 044/139] functions for querying sequence values for read only access --- brmbar3/SQL-for-RO-access.sql | 57 +++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 brmbar3/SQL-for-RO-access.sql diff --git a/brmbar3/SQL-for-RO-access.sql b/brmbar3/SQL-for-RO-access.sql new file mode 100644 index 0000000..b8c7291 --- /dev/null +++ b/brmbar3/SQL-for-RO-access.sql @@ -0,0 +1,57 @@ +CREATE OR REPLACE FUNCTION accounts_id_seq_value() +RETURNS bigint +LANGUAGE plpgsql +SECURITY DEFINER +AS $$ +DECLARE + result bigint; +BEGIN + SELECT last_value FROM accounts_id_seq + INTO result; + RETURN result; +END; +$$; + +CREATE OR REPLACE FUNCTION transactions_id_seq_value() +RETURNS bigint +LANGUAGE plpgsql +SECURITY DEFINER +AS $$ +DECLARE + result bigint; +BEGIN + SELECT last_value FROM transactions_id_seq + INTO result; + RETURN result; +END; +$$; + +CREATE OR REPLACE FUNCTION transaction_splits_id_seq_value() +RETURNS bigint +LANGUAGE plpgsql +SECURITY DEFINER +AS $$ +DECLARE + result bigint; +BEGIN + SELECT last_value FROM transaction_splits_id_seq + INTO result; + RETURN result; +END; +$$; + +CREATE OR REPLACE FUNCTION currencies_id_seq_value() +RETURNS bigint +LANGUAGE plpgsql +SECURITY DEFINER +AS $$ +DECLARE + result bigint; +BEGIN + SELECT last_value FROM currencies_id_seq + INTO result; + RETURN result; +END; +$$; + + From 3000731ac7b4bbcba045276a0df93acbb02b9af8 Mon Sep 17 00:00:00 2001 From: TMA Date: Fri, 11 Apr 2025 20:56:17 +0200 Subject: [PATCH 045/139] SQL schema v002 script part 1 --- brmbar3/SQL | 4 +- brmbar3/SQL-schema-v002.sql | 80 +++++++++++++++++++++++++++++++++++++ brmbar3/brmbar/Shop.py | 17 ++++---- 3 files changed, 91 insertions(+), 10 deletions(-) create mode 100644 brmbar3/SQL-schema-v002.sql diff --git a/brmbar3/SQL b/brmbar3/SQL index a61dba8..04667f4 100644 --- a/brmbar3/SQL +++ b/brmbar3/SQL @@ -1,11 +1,11 @@ -CREATE SEQUENCE currencies_id_seq START WITH 1 INCREMENT BY 1; +CREATE SEQUENCE currencies_id_seq START WITH 2 INCREMENT BY 1; CREATE TABLE currencies ( id INTEGER PRIMARY KEY NOT NULL DEFAULT NEXTVAL('currencies_id_seq'::regclass), name VARCHAR(128) NOT NULL, UNIQUE(name) ); -- Some code depends on the primary physical currency to have id 1. -INSERT INTO currencies (name) VALUES ('Kč'); +INSERT INTO currencies (id, name) VALUES (1, 'Kč'); CREATE TYPE exchange_rate_direction AS ENUM ('source_to_target', 'target_to_source'); CREATE TABLE exchange_rates ( diff --git a/brmbar3/SQL-schema-v002.sql b/brmbar3/SQL-schema-v002.sql new file mode 100644 index 0000000..afe6c80 --- /dev/null +++ b/brmbar3/SQL-schema-v002.sql @@ -0,0 +1,80 @@ +-- intoduce implementation schema +CREATE SCHEMA IF NOT EXISTS brmbar_implementation; +-- version table (with initialization) +CREATE TABLE IF NOT EXISTS brmbar_implementation.brmbar_schema ( + ver INTEGER NOT NULL +); +DO $$ +DECLARE v INTEGER; +BEGIN + SELECT ver FROM brmbar_implementation.brmbar_schema INTO v; + IF v IS NULL THEN + INSERT INTO brmbar_implementation.brmbar_schema (ver) VALUES (1); + END IF; +END; +$$; + + +--- upgrade schema +DO $$ +DECLARE +current_ver INTEGER; +BEGIN + +SELECT ver FROM brmbar_implementation.brmbar_schema INTO current_ver; +IF current_ver <> 1 THEN + RAISE EXCEPTION 'BrmBar schema version % cannot be upgraded to version 2.', current_ver; +END IF; + +ALTER TYPE public.account_type ADD VALUE 'trading'; + +--drop function if exists public.create_currency; +CREATE OR REPLACE PROCEDURE public.create_currency( + IN i_name public.currencies.name%TYPE, + OUT o_id public.currencies.id%TYPE) +LANGUAGE plpgsql SECURITY DEFINER AS $fn$ +BEGIN + INSERT INTO currencies ("name") VALUES (i_name) RETURNING o_id; +END +$fn$; + +CREATE OR REPLACE PROCEDURE public.undo_transaction( + IN i_id public.transactions.id%TYPE, + OUT o_id public.transactions.id%TYPE) +LANGUAGE plpgsql SECURITY DEFINER AS $fn$ +DECLARE + v_ntrn_id public.transactions.id%TYPE; + v_old_trn public.transactions%ROWTYPE; + v_old_split public.transaction_splits%ROWTYPE; +BEGIN + SELECT * INTO v_old_trn FROM public.transactions WHERE id = i_id; + INSERT INTO transactions ("description") VALUES ('undo '||o_id||' ('||v_old_trn.description||')') RETURNING id into v_ntrn_id; + FOR v_old_split IN + SELECT * FROM transaction_splits WHERE "transaction" = i_id + LOOP + INSERT INTO transaction_splits ("transaction", "side", "account", "amount", "memo") + VALUES (v_ntrn_id, v_old_split.side, v_old_split.account, -v_old_split.amount, + 'undo ' || v_old_split.id || ' (' || v_old_split.memo || ')' ); + END LOOP; + o_id := v_ntrn_id; +END +$fn$; + +-- this is for the case that psycopg2 cannot do output arguments +CREATE OR REPLACE FUNCTION public.undo_transaction_fn( + IN i_id public.transactions.id%TYPE) +RETURNS public.transactions.id%TYPE +VOLATILE NOT LEAKPROOF LANGUAGE plpgsql AS $fn$ +DECLARE + v_ntrn_id public.transactions.id%TYPE; +BEGIN + CALL public.undo_transaction(i_id, v_ntrn_id); + RETURN v_ntrn_id; +END +$fn$; + + +-- end of upgrade do block +end +$$; + diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index 6027084..5049fcc 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -219,14 +219,15 @@ class Shop: self.db.commit() def undo(self, oldtid): - description = self.db.execute_and_fetch("SELECT description FROM transactions WHERE id = %s", [oldtid])[0] - description = 'undo %d (%s)' % (oldtid, description) + #description = self.db.execute_and_fetch("SELECT description FROM transactions WHERE id = %s", [oldtid])[0] + #description = 'undo %d (%s)' % (oldtid, description) - transaction = self._transaction(description=description) - for split in self.db.execute_and_fetchall("SELECT id, side, account, amount, memo FROM transaction_splits WHERE transaction = %s", [oldtid]): - splitid, side, account, amount, memo = split - memo = 'undo %d (%s)' % (splitid, memo) - amount = -amount - self.db.execute("INSERT INTO transaction_splits (transaction, side, account, amount, memo) VALUES (%s, %s, %s, %s, %s)", [transaction, side, account, amount, memo]) + #transaction = self._transaction(description=description) + #for split in self.db.execute_and_fetchall("SELECT id, side, account, amount, memo FROM transaction_splits WHERE transaction = %s", [oldtid]): + # splitid, side, account, amount, memo = split + # memo = 'undo %d (%s)' % (splitid, memo) + # amount = -amount + # self.db.execute("INSERT INTO transaction_splits (transaction, side, account, amount, memo) VALUES (%s, %s, %s, %s, %s)", [transaction, side, account, amount, memo]) + transaction = self.db.execute_and_fetch("CALL undo_transaction(%s)",[oldtid])[0] self.db.commit() return transaction From f6ec2be215bbfbdf99156507b5e53fc0a185c5ac Mon Sep 17 00:00:00 2001 From: TMA Date: Sat, 19 Apr 2025 22:44:01 +0200 Subject: [PATCH 046/139] schema v1 setup script --- brmbar3/SQL-schema-v001.sql | 59 +++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 brmbar3/SQL-schema-v001.sql diff --git a/brmbar3/SQL-schema-v001.sql b/brmbar3/SQL-schema-v001.sql new file mode 100644 index 0000000..44b9693 --- /dev/null +++ b/brmbar3/SQL-schema-v001.sql @@ -0,0 +1,59 @@ +--RESET search_path; +SELECT pg_catalog.set_config('search_path', '', false); +-- intoduce implementation schema +CREATE SCHEMA IF NOT EXISTS brmbar_implementation; +-- version table (with initialization) +CREATE TABLE IF NOT EXISTS brmbar_implementation.brmbar_schema ( + ver INTEGER NOT NULL +); +DO $$ +DECLARE v INTEGER; +BEGIN + SELECT ver FROM brmbar_implementation.brmbar_schema INTO v; + IF v IS NULL THEN + INSERT INTO brmbar_implementation.brmbar_schema (ver) VALUES (1); + END IF; +END; +$$; + +CREATE OR REPLACE FUNCTION brmbar_implementation.has_exact_schema_version( + IN i_ver INTEGER NOT NULL +) RETURNS INTEGER +VOLATILE NOT LEAKPROOF LANGUAGE plpgsql AS $$ +DECLARE + v_ver INTEGER; +BEGIN + SELECT ver INTO STRICT v_ver FROM brmbar_implementation.brmbar_schema; + IF v_ver IS NULL or v_ver <> i_ver THEN + RAISE EXCEPTION 'Invalid brmbar schema version'; + END IF; + RETURN v_ver; +/* +EXCEPTION + WHEN NO_DATA_FOUND THEN + RAISE EXCEPTION 'PID % not found'; + WHEN TOO_MANY_ROWS THEN + RAISE EXCEPTION 'PID % not unique'; +*/ +END; +$$; + +CREATE OR REPLACE FUNCTION brmbar_implementation.upgrade_schema_version_to( + IN i_ver INTEGER NOT NULL +) RETURNS INTEGER +VOLATILE NOT LEAKPROOF LANGUAGE plpgsql AS $$ +DECLARE + v_ver INTEGER; +BEGIN + SELECT brmbar_implementation.has_exact_schema_version(i_ver) INTO v_ver; + IF v_ver + 1 = i_ver THEN + UPDATE brmbar_implementation.brmbar_schema SET ver = i_ver; + ELSE + RAISE EXCEPTION 'Invalid brmbar schema version'; + END IF; + RETURN i_ver; +END; +$$; + +-- vim: set ft=plsql : + From 900264469a9f925749246477b7803b052b91ae07 Mon Sep 17 00:00:00 2001 From: TMA Date: Sun, 20 Apr 2025 10:13:31 +0200 Subject: [PATCH 047/139] schema v2 setup script --- brmbar3/SQL-schema-v002.sql | 105 +++++++++++++++--------------------- 1 file changed, 43 insertions(+), 62 deletions(-) diff --git a/brmbar3/SQL-schema-v002.sql b/brmbar3/SQL-schema-v002.sql index afe6c80..bfb63fa 100644 --- a/brmbar3/SQL-schema-v002.sql +++ b/brmbar3/SQL-schema-v002.sql @@ -1,80 +1,61 @@ --- intoduce implementation schema -CREATE SCHEMA IF NOT EXISTS brmbar_implementation; --- version table (with initialization) -CREATE TABLE IF NOT EXISTS brmbar_implementation.brmbar_schema ( - ver INTEGER NOT NULL -); -DO $$ -DECLARE v INTEGER; -BEGIN - SELECT ver FROM brmbar_implementation.brmbar_schema INTO v; - IF v IS NULL THEN - INSERT INTO brmbar_implementation.brmbar_schema (ver) VALUES (1); - END IF; -END; -$$; - +--RESET search_path +SELECT pg_catalog.set_config('search_path', '', false); --- upgrade schema -DO $$ +DO $upgrade_block$ DECLARE current_ver INTEGER; BEGIN -SELECT ver FROM brmbar_implementation.brmbar_schema INTO current_ver; +-- confirm that we are upgrading from version 1 +SELECT brmbar_implementation.has_exact_schema_version(1) INTO current_ver; IF current_ver <> 1 THEN RAISE EXCEPTION 'BrmBar schema version % cannot be upgraded to version 2.', current_ver; END IF; -ALTER TYPE public.account_type ADD VALUE 'trading'; +-- structural changes ---drop function if exists public.create_currency; -CREATE OR REPLACE PROCEDURE public.create_currency( - IN i_name public.currencies.name%TYPE, - OUT o_id public.currencies.id%TYPE) -LANGUAGE plpgsql SECURITY DEFINER AS $fn$ -BEGIN - INSERT INTO currencies ("name") VALUES (i_name) RETURNING o_id; -END -$fn$; +-- TRADING ACCOUNTS +--START TRANSACTION ISOLATION LEVEL SERIALIZABLE; -CREATE OR REPLACE PROCEDURE public.undo_transaction( - IN i_id public.transactions.id%TYPE, - OUT o_id public.transactions.id%TYPE) -LANGUAGE plpgsql SECURITY DEFINER AS $fn$ -DECLARE - v_ntrn_id public.transactions.id%TYPE; - v_old_trn public.transactions%ROWTYPE; - v_old_split public.transaction_splits%ROWTYPE; -BEGIN - SELECT * INTO v_old_trn FROM public.transactions WHERE id = i_id; - INSERT INTO transactions ("description") VALUES ('undo '||o_id||' ('||v_old_trn.description||')') RETURNING id into v_ntrn_id; - FOR v_old_split IN - SELECT * FROM transaction_splits WHERE "transaction" = i_id - LOOP - INSERT INTO transaction_splits ("transaction", "side", "account", "amount", "memo") - VALUES (v_ntrn_id, v_old_split.side, v_old_split.account, -v_old_split.amount, - 'undo ' || v_old_split.id || ' (' || v_old_split.memo || ')' ); - END LOOP; - o_id := v_ntrn_id; -END -$fn$; +-- currency trading accounts - account type +ALTER TYPE public.account_type ADD VALUE IF NOT EXISTS 'trading'; --- this is for the case that psycopg2 cannot do output arguments -CREATE OR REPLACE FUNCTION public.undo_transaction_fn( - IN i_id public.transactions.id%TYPE) -RETURNS public.transactions.id%TYPE -VOLATILE NOT LEAKPROOF LANGUAGE plpgsql AS $fn$ -DECLARE - v_ntrn_id public.transactions.id%TYPE; -BEGIN - CALL public.undo_transaction(i_id, v_ntrn_id); - RETURN v_ntrn_id; -END -$fn$; +-- constraint needed for foreign key in currencies table +ALTER TABLE public.accounts ADD CONSTRAINT accounts_id_acctype_key UNIQUE(id, acctype); + +-- add columns to currencies to record the trading account associated with the currency +ALTER TABLE public.currencies + ADD COLUMN IF NOT EXISTS trading_account integer, + ADD COLUMN IF NOT EXISTS trading_account_type account_type GENERATED ALWAYS AS ('trading'::public.account_type) STORED; + +-- make trading accounts (without making duplicates) +INSERT INTO public.accounts ("name", "currency", acctype) +SELECT + 'Currency Trading Account: ' || c."name", + c.id, + 'trading'::public.account_type +FROM public.currencies AS c +WHERE NOT EXISTS ( + SELECT 1 + FROM public.accounts a + WHERE a.currency = c.id AND a.acctype = 'trading'::public.account_type +); +-- record the trading account IDs in currencies table +UPDATE public.currencies AS c SET (trading_account) = (SELECT a.id FROM public.accounts AS a WHERE a.currency = c.id AND c.acctype = 'trading'::public.account_type); + +-- foreign key to check the validity of currency trading account reference +ALTER TABLE public.currencies + ADD CONSTRAINT currencies_trading_fkey FOREIGN KEY (trading_account, trading_account_type) + REFERENCES xaccounts(id,acctype) DEFERRABLE INITIALLY DEFERRED; + +--COMMIT AND CHAIN; + +SELECT brmbar_implementation.upgrade_schema_version_to(2) INTO current_ver; -- end of upgrade do block end -$$; +$upgrade_block$; +-- vim: set ft=plsql : From c04f934340929607a88251c66522e66f2ae94a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Sun, 20 Apr 2025 17:01:05 +0200 Subject: [PATCH 048/139] #1: add initial SQL schema as the database currently in use --- brmbar3/schema/0001-init.sql | 313 +++++++++++++++++++++++++++++++++++ 1 file changed, 313 insertions(+) create mode 100644 brmbar3/schema/0001-init.sql diff --git a/brmbar3/schema/0001-init.sql b/brmbar3/schema/0001-init.sql new file mode 100644 index 0000000..52afd7e --- /dev/null +++ b/brmbar3/schema/0001-init.sql @@ -0,0 +1,313 @@ +-- +-- 0000-init.sql +-- +-- Initial SQL schema construction as of 2025-04-20 (or so) +-- +-- ISC License +-- +-- Copyright 2023-2025 Brmlab, z.s. +-- Dominik Pantůček +-- +-- Permission to use, copy, modify, and/or distribute this software +-- for any purpose with or without fee is hereby granted, provided +-- that the above copyright notice and this permission notice appear +-- in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +-- + +-- To require fully-qualified names +SELECT pg_catalog.set_config('search_path', '', false); + +-- Privileged schema with protected data +CREATE SCHEMA IF NOT EXISTS brmbar_privileged; + +-- Initial versioning +CREATE TABLE IF NOT EXISTS brmbar_privileged.brmbar_schema( + ver INTEGER NOT NULL +); + +-- ---------------------------------------------------------------- +-- Legacy Schema Initialization +-- ---------------------------------------------------------------- + +DO $$ +DECLARE v INTEGER; +BEGIN + SELECT ver FROM brmbar_privileged.brmbar_schema INTO v; + IF v IS NULL THEN + -- -------------------------------- + -- Legacy Types + + SELECT COUNT(*) INTO v + FROM pg_catalog.pg_type typ + INNER JOIN pg_catalog.pg_namespace nsp + ON nsp.oid = typ.typnamespace + WHERE nsp.nspname = 'public' + AND typ.typname='exchange_rate_direction'; + IF v=0 THEN + RAISE NOTICE 'Creating type exchange_rate_direction'; + CREATE TYPE public.exchange_rate_direction + AS ENUM ('source_to_target', 'target_to_source'); + ELSE + RAISE NOTICE 'Type exchange_rate_direction already exists'; + END IF; + + SELECT COUNT(*) INTO v + FROM pg_catalog.pg_type typ + INNER JOIN pg_catalog.pg_namespace nsp + ON nsp.oid = typ.typnamespace + WHERE nsp.nspname = 'public' + AND typ.typname='account_type'; + IF v=0 THEN + RAISE NOTICE 'Creating type account_type'; + CREATE TYPE public.account_type + AS ENUM ('cash', 'debt', 'inventory', 'income', 'expense', + 'starting_balance', 'ending_balance'); + ELSE + RAISE NOTICE 'Type account_type already exists'; + END IF; + + SELECT COUNT(*) INTO v + FROM pg_catalog.pg_type typ + INNER JOIN pg_catalog.pg_namespace nsp + ON nsp.oid = typ.typnamespace + WHERE nsp.nspname = 'public' + AND typ.typname='transaction_split_side'; + IF v=0 THEN + RAISE NOTICE 'Creating type transaction_split_side'; + CREATE TYPE public.transaction_split_side + AS ENUM ('credit', 'debit'); + ELSE + RAISE NOTICE 'Type transaction_split_side already exists'; + END IF; + + -- -------------------------------- + -- Currencies sequence, table and potential initial data + + CREATE SEQUENCE IF NOT EXISTS public.currencies_id_seq + START WITH 2 INCREMENT BY 1; + CREATE TABLE IF NOT EXISTS public.currencies ( + id INTEGER PRIMARY KEY NOT NULL DEFAULT NEXTVAL('public.currencies_id_seq'::regclass), + name VARCHAR(128) NOT NULL, + UNIQUE(name) + ); + INSERT INTO public.currencies (id, name) VALUES (1, 'Kč') + ON CONFLICT DO NOTHING; + + -- -------------------------------- + -- Exchange rates table - no initial data required + + CREATE TABLE IF NOT EXISTS public.exchange_rates ( + valid_since TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL, + + target INTEGER NOT NULL, + FOREIGN KEY (target) REFERENCES public.currencies (id), + + source INTEGER NOT NULL, + FOREIGN KEY (source) REFERENCES public.currencies (id), + + rate DECIMAL(12,2) NOT NULL, + rate_dir public.exchange_rate_direction NOT NULL + ); + + -- -------------------------------- + -- Accounts sequence and table and 4 initial accounts + + CREATE SEQUENCE IF NOT EXISTS public.accounts_id_seq + START WITH 2 INCREMENT BY 1; + CREATE TABLE IF NOT EXISTS public.accounts ( + id INTEGER PRIMARY KEY NOT NULL DEFAULT NEXTVAL('public.accounts_id_seq'::regclass), + + name VARCHAR(128) NOT NULL, + UNIQUE (name), + + currency INTEGER NOT NULL, + FOREIGN KEY (currency) REFERENCES public.currencies (id), + + acctype public.account_type NOT NULL, + + active BOOLEAN NOT NULL DEFAULT TRUE + ); + INSERT INTO public.accounts (id, name, currency, acctype) + VALUES (1, 'BrmBar Cash', (SELECT id FROM public.currencies WHERE name='Kč'), 'cash') + ON CONFLICT DO NOTHING; + INSERT INTO public.accounts (name, currency, acctype) + VALUES ('BrmBar Profits', (SELECT id FROM public.currencies WHERE name='Kč'), 'income') + ON CONFLICT DO NOTHING; + INSERT INTO public.accounts (name, currency, acctype) + VALUES ('BrmBar Excess', (SELECT id FROM public.currencies WHERE name='Kč'), 'income') + ON CONFLICT DO NOTHING; + INSERT INTO public.accounts (name, currency, acctype) + VALUES ('BrmBar Deficit', (SELECT id FROM public.currencies WHERE name='Kč'), 'expense') + ON CONFLICT DO NOTHING; + + -- -------------------------------- + -- Barcodes + + CREATE TABLE IF NOT EXISTS public.barcodes ( + barcode VARCHAR(128) PRIMARY KEY NOT NULL, + + account INTEGER NOT NULL, + FOREIGN KEY (account) REFERENCES public.accounts (id) + ); + INSERT INTO public.barcodes (barcode, account) + VALUES ('_cash_', (SELECT id FROM public.accounts WHERE acctype = 'cash')) + ON CONFLICT DO NOTHING; + + -- -------------------------------- + -- Transactions + + CREATE SEQUENCE IF NOT EXISTS public.transactions_id_seq + START WITH 1 INCREMENT BY 1; + CREATE TABLE IF NOT EXISTS public.transactions ( + id INTEGER PRIMARY KEY NOT NULL DEFAULT NEXTVAL('public.transactions_id_seq'::regclass), + time TIMESTAMP DEFAULT NOW() NOT NULL, + + responsible INTEGER, + FOREIGN KEY (responsible) REFERENCES public.accounts (id), + + description TEXT + ); + + -- -------------------------------- + -- Transaction splits + + CREATE SEQUENCE IF NOT EXISTS public.transaction_splits_id_seq + START WITH 1 INCREMENT BY 1; + CREATE TABLE IF NOT EXISTS public.transaction_splits ( + id INTEGER PRIMARY KEY NOT NULL DEFAULT NEXTVAL('public.transaction_splits_id_seq'::regclass), + + transaction INTEGER NOT NULL, + FOREIGN KEY (transaction) REFERENCES public.transactions (id), + + side public.transaction_split_side NOT NULL, + + account INTEGER NOT NULL, + FOREIGN KEY (account) REFERENCES public.accounts (id), + amount DECIMAL(12,2) NOT NULL, + + memo TEXT + ); + + -- -------------------------------- + -- Account balances view + + CREATE OR REPLACE VIEW public.account_balances AS + SELECT ts.account AS id, + accounts.name, + accounts.acctype, + - sum( + CASE + WHEN ts.side = 'credit'::public.transaction_split_side THEN - ts.amount + ELSE ts.amount + END) AS crbalance + FROM public.transaction_splits ts + LEFT JOIN public.accounts ON accounts.id = ts.account + GROUP BY ts.account, accounts.name, accounts.acctype + ORDER BY (- sum( + CASE + WHEN ts.side = 'credit'::public.transaction_split_side THEN - ts.amount + ELSE ts.amount + END)); + + -- -------------------------------- + -- Transaction nice splits view + + CREATE OR REPLACE VIEW public.transaction_nicesplits AS + SELECT ts.id, + ts.transaction, + ts.account, + CASE + WHEN ts.side = 'credit'::public.transaction_split_side THEN - ts.amount + ELSE ts.amount + END AS amount, + a.currency, + ts.memo + FROM public.transaction_splits ts + LEFT JOIN public.accounts a ON a.id = ts.account + ORDER BY ts.id; + + -- -------------------------------- + -- Transaction cash sums view + + CREATE OR REPLACE VIEW public.transaction_cashsums AS + SELECT t.id, + t."time", + sum(credit.credit_cash) AS cash_credit, + sum(debit.debit_cash) AS cash_debit, + a.name AS responsible, + t.description + FROM public.transactions t + LEFT JOIN ( SELECT cts.amount AS credit_cash, + cts.transaction AS cts_t + FROM public.transaction_nicesplits cts + LEFT JOIN public.accounts a_1 ON a_1.id = cts.account OR a_1.id = cts.account + WHERE a_1.currency = (( SELECT accounts.currency + FROM public.accounts + WHERE accounts.name::text = 'BrmBar Cash'::text)) + AND (a_1.acctype = ANY (ARRAY['cash'::public.account_type, 'debt'::public.account_type])) + AND cts.amount < 0::numeric) credit ON credit.cts_t = t.id + LEFT JOIN ( SELECT dts.amount AS debit_cash, + dts.transaction AS dts_t + FROM public.transaction_nicesplits dts + LEFT JOIN public.accounts a_1 ON a_1.id = dts.account OR a_1.id = dts.account + WHERE a_1.currency = (( SELECT accounts.currency + FROM public.accounts + WHERE accounts.name::text = 'BrmBar Cash'::text)) + AND (a_1.acctype = ANY (ARRAY['cash'::public.account_type, 'debt'::public.account_type])) + AND dts.amount > 0::numeric) debit ON debit.dts_t = t.id + LEFT JOIN public.accounts a ON a.id = t.responsible + GROUP BY t.id, a.name + ORDER BY t.id DESC; + + -- -------------------------------- + -- Function to check schema version (used in migrations) + + CREATE OR REPLACE FUNCTION brmbar_privileged.has_exact_schema_version( + IN i_ver INTEGER + ) RETURNS INTEGER + VOLATILE NOT LEAKPROOF LANGUAGE plpgsql AS $x$ + DECLARE + v_ver INTEGER; + BEGIN + SELECT ver INTO v_ver FROM brmbar_privileged.brmbar_schema; + IF v_ver is NULL THEN + RETURN false; + ELSE + RETURN v_ver = i_ver; + END IF; + END; + $x$; + + -- -------------------------------- + -- + + CREATE OR REPLACE FUNCTION brmbar_privileged.upgrade_schema_version_to( + IN i_ver INTEGER + ) RETURNS INTEGER + VOLATILE NOT LEAKPROOF LANGUAGE plpgsql AS $x$ + DECLARE + v_ver INTEGER; + BEGIN + SELECT ver FROM brmbar_privileged.brmbar_schema INTO v_ver; + IF v_ver=(i_ver-1) THEN + UPDATE brmbar_privileged.brmbar_schema SET ver = i_ver; + ELSE + RAISE EXCEPTION 'Invalid brmbar schema version transition (% -> %)', v_ver, i_ver; + END IF; + END; + $x$; + + -- Initialize version 1 + INSERT INTO brmbar_privileged.brmbar_schema(ver) VALUES(1); + END IF; +END; +$$; From 111a8c9b63138e405f4073fd38787f9e7267d03c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Sun, 20 Apr 2025 17:11:49 +0200 Subject: [PATCH 049/139] #2: add trading account type type and fix schema migrations machinery --- brmbar3/schema/0001-init.sql | 6 ++-- brmbar3/schema/0002-trading-accounts.sql | 37 ++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 brmbar3/schema/0002-trading-accounts.sql diff --git a/brmbar3/schema/0001-init.sql b/brmbar3/schema/0001-init.sql index 52afd7e..8e7f8db 100644 --- a/brmbar3/schema/0001-init.sql +++ b/brmbar3/schema/0001-init.sql @@ -1,5 +1,5 @@ -- --- 0000-init.sql +-- 0001-init.sql -- -- Initial SQL schema construction as of 2025-04-20 (or so) -- @@ -273,7 +273,7 @@ BEGIN CREATE OR REPLACE FUNCTION brmbar_privileged.has_exact_schema_version( IN i_ver INTEGER - ) RETURNS INTEGER + ) RETURNS BOOLEAN VOLATILE NOT LEAKPROOF LANGUAGE plpgsql AS $x$ DECLARE v_ver INTEGER; @@ -292,7 +292,7 @@ BEGIN CREATE OR REPLACE FUNCTION brmbar_privileged.upgrade_schema_version_to( IN i_ver INTEGER - ) RETURNS INTEGER + ) RETURNS VOID VOLATILE NOT LEAKPROOF LANGUAGE plpgsql AS $x$ DECLARE v_ver INTEGER; diff --git a/brmbar3/schema/0002-trading-accounts.sql b/brmbar3/schema/0002-trading-accounts.sql new file mode 100644 index 0000000..08397cd --- /dev/null +++ b/brmbar3/schema/0002-trading-accounts.sql @@ -0,0 +1,37 @@ +-- +-- 0002-trading-accounts.sql +-- +-- #2 - add trading accounts to account type type +-- +-- ISC License +-- +-- Copyright 2023-2025 Brmlab, z.s. +-- Dominik Pantůček +-- +-- Permission to use, copy, modify, and/or distribute this software +-- for any purpose with or without fee is hereby granted, provided +-- that the above copyright notice and this permission notice appear +-- in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +-- + +DO $upgrade_block$ +BEGIN + +IF brmbar_privileged.has_exact_schema_version(1) THEN + + ALTER TYPE public.account_type ADD VALUE 'trading'; + + PERFORM brmbar_privileged.upgrade_schema_version_to(2); +END IF; + +END; +$upgrade_block$; From c5d1fc3402e9aafbf6de1fd1ae0a7c9d905b4cda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Sun, 20 Apr 2025 17:42:12 +0200 Subject: [PATCH 050/139] #3: create account in SQL not in Python. --- brmbar3/brmbar/Account.py | 3 +- brmbar3/schema/0002-trading-accounts.sql | 3 ++ brmbar3/schema/0003-new-account.sql | 52 ++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 brmbar3/schema/0003-new-account.sql diff --git a/brmbar3/brmbar/Account.py b/brmbar3/brmbar/Account.py index 29dd79e..fca6d82 100644 --- a/brmbar3/brmbar/Account.py +++ b/brmbar3/brmbar/Account.py @@ -40,7 +40,8 @@ class Account: @classmethod def create(cls, db, name, currency, acctype): """ Constructor for new account """ - id = db.execute_and_fetch("INSERT INTO accounts (name, currency, acctype) VALUES (%s, %s, %s) RETURNING id", [name, currency.id, acctype]) + # id = db.execute_and_fetch("INSERT INTO accounts (name, currency, acctype) VALUES (%s, %s, %s) RETURNING id", [name, currency.id, acctype]) + id = db.execute_and_fetch("SELECT public.create_account(%s, %s, %s)", [name, currency.id, acctype]) id = id[0] return cls(db, name = name, id = id, currency = currency, acctype = acctype) diff --git a/brmbar3/schema/0002-trading-accounts.sql b/brmbar3/schema/0002-trading-accounts.sql index 08397cd..021df3d 100644 --- a/brmbar3/schema/0002-trading-accounts.sql +++ b/brmbar3/schema/0002-trading-accounts.sql @@ -23,6 +23,9 @@ -- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. -- +-- Require fully-qualified names +SELECT pg_catalog.set_config('search_path', '', false); + DO $upgrade_block$ BEGIN diff --git a/brmbar3/schema/0003-new-account.sql b/brmbar3/schema/0003-new-account.sql new file mode 100644 index 0000000..9ac02b5 --- /dev/null +++ b/brmbar3/schema/0003-new-account.sql @@ -0,0 +1,52 @@ +-- +-- 0003-new-account.sql +-- +-- #3 - stored procedure for creating new account +-- +-- ISC License +-- +-- Copyright 2023-2025 Brmlab, z.s. +-- Dominik Pantůček +-- +-- Permission to use, copy, modify, and/or distribute this software +-- for any purpose with or without fee is hereby granted, provided +-- that the above copyright notice and this permission notice appear +-- in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +-- + +-- Require fully-qualified names +SELECT pg_catalog.set_config('search_path', '', false); + +DO $upgrade_block$ +BEGIN + +IF brmbar_privileged.has_exact_schema_version(2) THEN + + CREATE OR REPLACE FUNCTION public.create_account( + IN i_name public.accounts.name%TYPE, + IN i_currency public.accounts.currency%TYPE, + IN i_acctype public.accounts.acctype%TYPE + ) RETURNS INTEGER LANGUAGE plpgsql AS $$ + DECLARE + r_id INTEGER; + BEGIN + INSERT INTO public.accounts (name, currency, acctype) + VALUES (i_name, i_currency, i_acctype) RETURNING id INTO r_id; + RETURN r_id; + END + $$; + + PERFORM brmbar_privileged.upgrade_schema_version_to(3); +END IF; + +END; +$upgrade_block$; From 58ab1d00be2914c9c215c67d81ba02ad86ee78f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Sun, 20 Apr 2025 17:55:18 +0200 Subject: [PATCH 051/139] #4: add account to barcode stored procedure. --- brmbar3/brmbar/Account.py | 3 +- brmbar3/schema/0004-add-account-barcode.sql | 50 +++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 brmbar3/schema/0004-add-account-barcode.sql diff --git a/brmbar3/brmbar/Account.py b/brmbar3/brmbar/Account.py index fca6d82..8615dfd 100644 --- a/brmbar3/brmbar/Account.py +++ b/brmbar3/brmbar/Account.py @@ -69,7 +69,8 @@ class Account: self.db.execute("INSERT INTO transaction_splits (transaction, side, account, amount, memo) VALUES (%s, %s, %s, %s, %s)", [transaction, side, self.id, amount, memo]) def add_barcode(self, barcode): - self.db.execute("INSERT INTO barcodes (account, barcode) VALUES (%s, %s)", [self.id, barcode]) + # self.db.execute("INSERT INTO barcodes (account, barcode) VALUES (%s, %s)", [self.id, barcode]) + self.db.execute("SELECT public.add_barcode_to_account(%s, %s)", [self.id, barcode]) self.db.commit() def rename(self, name): diff --git a/brmbar3/schema/0004-add-account-barcode.sql b/brmbar3/schema/0004-add-account-barcode.sql new file mode 100644 index 0000000..dbdba9a --- /dev/null +++ b/brmbar3/schema/0004-add-account-barcode.sql @@ -0,0 +1,50 @@ +-- +-- 0004-add-account-barcode.sql +-- +-- #4 - stored procedure for adding barcode to account +-- +-- ISC License +-- +-- Copyright 2023-2025 Brmlab, z.s. +-- Dominik Pantůček +-- +-- Permission to use, copy, modify, and/or distribute this software +-- for any purpose with or without fee is hereby granted, provided +-- that the above copyright notice and this permission notice appear +-- in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +-- + +-- Require fully-qualified names +SELECT pg_catalog.set_config('search_path', '', false); + +DO $upgrade_block$ +BEGIN + +IF brmbar_privileged.has_exact_schema_version(3) THEN + + CREATE OR REPLACE FUNCTION public.add_barcode_to_account( + IN i_account public.barcodes.account%TYPE, + IN i_barcode public.barcodes.barcode%TYPE + ) RETURNS VOID LANGUAGE plpgsql AS $$ + DECLARE + r_id INTEGER; + BEGIN + INSERT INTO public.barcodes (account, barcode) + VALUES (i_account, i_barcode); + END + $$; + + PERFORM brmbar_privileged.upgrade_schema_version_to(4); +END IF; + +END; +$upgrade_block$; From 8f42145bee0bb6e265d7a1f7e63816c6666d5999 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Sun, 20 Apr 2025 18:00:40 +0200 Subject: [PATCH 052/139] #5: rename account stored function --- brmbar3/brmbar/Account.py | 3 +- brmbar3/schema/0005-rename-account.sql | 51 ++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 brmbar3/schema/0005-rename-account.sql diff --git a/brmbar3/brmbar/Account.py b/brmbar3/brmbar/Account.py index 8615dfd..3e3a24d 100644 --- a/brmbar3/brmbar/Account.py +++ b/brmbar3/brmbar/Account.py @@ -74,5 +74,6 @@ class Account: self.db.commit() def rename(self, name): - self.db.execute("UPDATE accounts SET name = %s WHERE id = %s", [name, self.id]) + # self.db.execute("UPDATE accounts SET name = %s WHERE id = %s", [name, self.id]) + self.db.execute("SELECT public.rename_account(%s, %s)", [self.id, name]) self.name = name diff --git a/brmbar3/schema/0005-rename-account.sql b/brmbar3/schema/0005-rename-account.sql new file mode 100644 index 0000000..a957029 --- /dev/null +++ b/brmbar3/schema/0005-rename-account.sql @@ -0,0 +1,51 @@ +-- +-- 0005-rename-account.sql +-- +-- #5 - stored procedure for renaming account +-- +-- ISC License +-- +-- Copyright 2023-2025 Brmlab, z.s. +-- Dominik Pantůček +-- +-- Permission to use, copy, modify, and/or distribute this software +-- for any purpose with or without fee is hereby granted, provided +-- that the above copyright notice and this permission notice appear +-- in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +-- + +-- Require fully-qualified names +SELECT pg_catalog.set_config('search_path', '', false); + +DO $upgrade_block$ +BEGIN + +IF brmbar_privileged.has_exact_schema_version(4) THEN + + CREATE OR REPLACE FUNCTION public.rename_account( + IN i_account public.accounts.id%TYPE, + IN i_name public.accounts.name%TYPE + ) RETURNS VOID LANGUAGE plpgsql AS $$ + DECLARE + r_id INTEGER; + BEGIN + UPDATE public.accounts + SET name = i_name + WHERE id = i_account; + END + $$; + + PERFORM brmbar_privileged.upgrade_schema_version_to(5); +END IF; + +END; +$upgrade_block$; From 9235607d4cae9f9a262a1aa85b02d4b6c8ca3311 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Sun, 20 Apr 2025 19:15:23 +0200 Subject: [PATCH 053/139] #6: new currency stored function --- brmbar3/brmbar/Account.py | 2 +- brmbar3/brmbar/Currency.py | 5 +-- brmbar3/schema/0006-new-currency.sql | 50 ++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 brmbar3/schema/0006-new-currency.sql diff --git a/brmbar3/brmbar/Account.py b/brmbar3/brmbar/Account.py index 3e3a24d..40e86be 100644 --- a/brmbar3/brmbar/Account.py +++ b/brmbar3/brmbar/Account.py @@ -42,7 +42,7 @@ class Account: """ Constructor for new account """ # id = db.execute_and_fetch("INSERT INTO accounts (name, currency, acctype) VALUES (%s, %s, %s) RETURNING id", [name, currency.id, acctype]) id = db.execute_and_fetch("SELECT public.create_account(%s, %s, %s)", [name, currency.id, acctype]) - id = id[0] + # id = id[0] return cls(db, name = name, id = id, currency = currency, acctype = acctype) def balance(self): diff --git a/brmbar3/brmbar/Currency.py b/brmbar3/brmbar/Currency.py index d2216ca..a03a893 100644 --- a/brmbar3/brmbar/Currency.py +++ b/brmbar3/brmbar/Currency.py @@ -31,8 +31,9 @@ class Currency: @classmethod def create(cls, db, name): """ Constructor for new currency """ - id = db.execute_and_fetch("INSERT INTO currencies (name) VALUES (%s) RETURNING id", [name]) - id = id[0] + # id = db.execute_and_fetch("INSERT INTO currencies (name) VALUES (%s) RETURNING id", [name]) + id = db.execute_and_fetch("SELECT public.create_currency(%s)", [name]) + # id = id[0] return cls(db, name = name, id = id) def rates(self, other): diff --git a/brmbar3/schema/0006-new-currency.sql b/brmbar3/schema/0006-new-currency.sql new file mode 100644 index 0000000..0ed9a94 --- /dev/null +++ b/brmbar3/schema/0006-new-currency.sql @@ -0,0 +1,50 @@ +-- +-- 0006-new-currency.sql +-- +-- #6 - stored procedure for creating new currency +-- +-- ISC License +-- +-- Copyright 2023-2025 Brmlab, z.s. +-- Dominik Pantůček +-- +-- Permission to use, copy, modify, and/or distribute this software +-- for any purpose with or without fee is hereby granted, provided +-- that the above copyright notice and this permission notice appear +-- in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +-- + +-- Require fully-qualified names +SELECT pg_catalog.set_config('search_path', '', false); + +DO $upgrade_block$ +BEGIN + +IF brmbar_privileged.has_exact_schema_version(5) THEN + + CREATE OR REPLACE FUNCTION public.create_currency( + IN i_name public.currencies.name%TYPE + ) RETURNS INTEGER LANGUAGE plpgsql AS $$ + DECLARE + r_id INTEGER; + BEGIN + INSERT INTO public.currencies (name) + VALUES (i_name) RETURNING id INTO r_id; + RETURN r_id; + END + $$; + + PERFORM brmbar_privileged.upgrade_schema_version_to(6); +END IF; + +END; +$upgrade_block$; From 66870bbc8c943b16919a5659cb229d24c502d2f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Sun, 20 Apr 2025 19:22:19 +0200 Subject: [PATCH 054/139] #7: migrate to stored function for updating currency sell rate. --- brmbar3/brmbar/Currency.py | 4 +- .../schema/0007-update-currency-sell-rate.sql | 49 +++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 brmbar3/schema/0007-update-currency-sell-rate.sql diff --git a/brmbar3/brmbar/Currency.py b/brmbar3/brmbar/Currency.py index a03a893..e204ac4 100644 --- a/brmbar3/brmbar/Currency.py +++ b/brmbar3/brmbar/Currency.py @@ -70,6 +70,8 @@ class Currency: return "{:.2f} {}".format(amount, self.name) def update_sell_rate(self, target, rate): - self.db.execute("INSERT INTO exchange_rates (source, target, rate, rate_dir) VALUES (%s, %s, %s, %s)", [self.id, target.id, rate, "source_to_target"]) + # self.db.execute("INSERT INTO exchange_rates (source, target, rate, rate_dir) VALUES (%s, %s, %s, %s)", [self.id, target.id, rate, "source_to_target"]) + self.db.execute("SELECT update_currency_sell_rate(%s, %s, %s)", + [self.id, target.id, rate]) def update_buy_rate(self, source, rate): self.db.execute("INSERT INTO exchange_rates (source, target, rate, rate_dir) VALUES (%s, %s, %s, %s)", [source.id, self.id, rate, "target_to_source"]) diff --git a/brmbar3/schema/0007-update-currency-sell-rate.sql b/brmbar3/schema/0007-update-currency-sell-rate.sql new file mode 100644 index 0000000..f627897 --- /dev/null +++ b/brmbar3/schema/0007-update-currency-sell-rate.sql @@ -0,0 +1,49 @@ +-- +-- 0007-update-currency-sell-rate.sql +-- +-- #7 - stored procedure for updating sell rate +-- +-- ISC License +-- +-- Copyright 2023-2025 Brmlab, z.s. +-- Dominik Pantůček +-- +-- Permission to use, copy, modify, and/or distribute this software +-- for any purpose with or without fee is hereby granted, provided +-- that the above copyright notice and this permission notice appear +-- in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +-- + +-- Require fully-qualified names +SELECT pg_catalog.set_config('search_path', '', false); + +DO $upgrade_block$ +BEGIN + +IF brmbar_privileged.has_exact_schema_version(6) THEN + + CREATE OR REPLACE FUNCTION public.update_currency_sell_rate( + IN i_currency public.exchange_rates.source%TYPE, + IN i_target public.exchange_rates.target%TYPE, + IN i_rate public.exchange_rates.rate%TYPE + ) RETURNS VOID LANGUAGE plpgsql AS $$ + BEGIN + INSERT INTO public.exchange_rates(source, target, rate, rate_dir) + VALUES (i_currency, i_target, i_rate, 'source_to_target'); + END + $$; + + PERFORM brmbar_privileged.upgrade_schema_version_to(7); +END IF; + +END; +$upgrade_block$; From aded7a5769570f63c4e62e88e762c65ada4d77ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Sun, 20 Apr 2025 19:27:02 +0200 Subject: [PATCH 055/139] #8: stored function to update currency buy rate --- brmbar3/brmbar/Currency.py | 6 ++- .../schema/0008-update-currency-buy-rate.sql | 49 +++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 brmbar3/schema/0008-update-currency-buy-rate.sql diff --git a/brmbar3/brmbar/Currency.py b/brmbar3/brmbar/Currency.py index e204ac4..294d9f4 100644 --- a/brmbar3/brmbar/Currency.py +++ b/brmbar3/brmbar/Currency.py @@ -71,7 +71,9 @@ class Currency: def update_sell_rate(self, target, rate): # self.db.execute("INSERT INTO exchange_rates (source, target, rate, rate_dir) VALUES (%s, %s, %s, %s)", [self.id, target.id, rate, "source_to_target"]) - self.db.execute("SELECT update_currency_sell_rate(%s, %s, %s)", + self.db.execute("SELECT public.update_currency_sell_rate(%s, %s, %s)", [self.id, target.id, rate]) def update_buy_rate(self, source, rate): - self.db.execute("INSERT INTO exchange_rates (source, target, rate, rate_dir) VALUES (%s, %s, %s, %s)", [source.id, self.id, rate, "target_to_source"]) + # self.db.execute("INSERT INTO exchange_rates (source, target, rate, rate_dir) VALUES (%s, %s, %s, %s)", [source.id, self.id, rate, "target_to_source"]) + self.db.execute("SELECT public.update_currency_buy_rate(%s, %s, %s)", + [source.id, self.id, rate]) diff --git a/brmbar3/schema/0008-update-currency-buy-rate.sql b/brmbar3/schema/0008-update-currency-buy-rate.sql new file mode 100644 index 0000000..cbab11b --- /dev/null +++ b/brmbar3/schema/0008-update-currency-buy-rate.sql @@ -0,0 +1,49 @@ +-- +-- 0008-update-currency-buy-rate.sql +-- +-- #8 - stored procedure for updating buy rate +-- +-- ISC License +-- +-- Copyright 2023-2025 Brmlab, z.s. +-- Dominik Pantůček +-- +-- Permission to use, copy, modify, and/or distribute this software +-- for any purpose with or without fee is hereby granted, provided +-- that the above copyright notice and this permission notice appear +-- in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +-- + +-- Require fully-qualified names +SELECT pg_catalog.set_config('search_path', '', false); + +DO $upgrade_block$ +BEGIN + +IF brmbar_privileged.has_exact_schema_version(7) THEN + + CREATE OR REPLACE FUNCTION public.update_currency_buy_rate( + IN i_currency public.exchange_rates.target%TYPE, + IN i_source public.exchange_rates.source%TYPE, + IN i_rate public.exchange_rates.rate%TYPE + ) RETURNS VOID LANGUAGE plpgsql AS $$ + BEGIN + INSERT INTO public.exchange_rates(source, target, rate, rate_dir) + VALUES (i_source, i_currency, i_rate, 'target_to_source'); + END + $$; + + PERFORM brmbar_privileged.upgrade_schema_version_to(8); +END IF; + +END; +$upgrade_block$; From cef95c43136a642f660bd1f3f59176d6fe5fd705 Mon Sep 17 00:00:00 2001 From: TMA Date: Sun, 20 Apr 2025 23:43:52 +0200 Subject: [PATCH 056/139] #9: stored function for sell transaction --- brmbar3/brmbar/Currency.py | 18 ++-- brmbar3/brmbar/Shop.py | 25 +++-- brmbar3/schema/0009-shop-sell.sql | 149 ++++++++++++++++++++++++++++++ 3 files changed, 175 insertions(+), 17 deletions(-) create mode 100644 brmbar3/schema/0009-shop-sell.sql diff --git a/brmbar3/brmbar/Currency.py b/brmbar3/brmbar/Currency.py index 294d9f4..7c56984 100644 --- a/brmbar3/brmbar/Currency.py +++ b/brmbar3/brmbar/Currency.py @@ -40,18 +40,20 @@ class Currency: """ Return tuple ($buy, $sell) of rates of $self in relation to $other (brmbar.Currency): $buy is the price of $self in means of $other when buying it (into brmbar) $sell is the price of $self in means of $other when selling it (from brmbar) """ - - res = self.db.execute_and_fetch("SELECT rate, rate_dir FROM exchange_rates WHERE target = %s AND source = %s AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1", [self.id, other.id]) + # buy rate + res = self.db.execute_and_fetch("SELECT public.find_buy_rate(%s, %s)",[self.id, other.id]) if res is None: + raise NameError("Something fishy in find_buy_rate."); + buy = res[0] + if buy < 0: raise NameError("Currency.rate(): Unknown conversion " + other.name() + " to " + self.name()) - buy_rate, buy_rate_dir = res - buy = buy_rate if buy_rate_dir == "target_to_source" else 1/buy_rate - - res = self.db.execute_and_fetch("SELECT rate, rate_dir FROM exchange_rates WHERE target = %s AND source = %s AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1", [other.id, self.id]) + # sell rate + res = self.db.execute_and_fetch("SELECT public.find_sell_rate(%s, %s)",[self.id, other.id]) if res is None: + raise NameError("Something fishy in find_sell_rate."); + sell = res[0] + if sell < 0: raise NameError("Currency.rate(): Unknown conversion " + self.name() + " to " + other.name()) - sell_rate, sell_rate_dir = res - sell = sell_rate if sell_rate_dir == "source_to_target" else 1/sell_rate return (buy, sell) diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index 5049fcc..f005d7b 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -25,18 +25,25 @@ class Shop: deficit = Account.load(db, name = "BrmBar Deficit")) def sell(self, item, user, amount = 1): - # Sale: Currency conversion from item currency to shop currency - (buy, sell) = item.currency.rates(self.currency) - cost = amount * sell - profit = amount * (sell - buy) + # Call the stored procedure for the sale + cost = self.db.execute_and_fetch( + "SELECT public.sell_item(%s, %s, %s, %s, %s)", + [item.id, amount, user.id, self.currency.id, f"BrmBar sale of {amount}x {item.name} to {user.name}"] + )[0]#[0] - transaction = self._transaction(responsible = user, description = "BrmBar sale of {}x {} to {}".format(amount, item.name, user.name)) - item.credit(transaction, amount, user.name) - user.debit(transaction, cost, item.name) # debit (increase) on a _debt_ account - self.profits.debit(transaction, profit, "Margin on " + item.name) self.db.commit() - return cost + # Sale: Currency conversion from item currency to shop currency + #(buy, sell) = item.currency.rates(self.currency) + #cost = amount * sell + #profit = amount * (sell - buy) + + #transaction = self._transaction(responsible = user, description = "BrmBar sale of {}x {} to {}".format(amount, item.name, user.name)) + #item.credit(transaction, amount, user.name) + #user.debit(transaction, cost, item.name) # debit (increase) on a _debt_ account + #self.profits.debit(transaction, profit, "Margin on " + item.name) + #self.db.commit() + #return cost def sell_for_cash(self, item, amount = 1): # Sale: Currency conversion from item currency to shop currency diff --git a/brmbar3/schema/0009-shop-sell.sql b/brmbar3/schema/0009-shop-sell.sql new file mode 100644 index 0000000..64d4353 --- /dev/null +++ b/brmbar3/schema/0009-shop-sell.sql @@ -0,0 +1,149 @@ +-- +-- 0009-shop-sell.sql +-- +-- #9 - stored function for sell transaction +-- +-- ISC License +-- +-- Copyright 2023-2025 Brmlab, z.s. +-- TMA +-- +-- Permission to use, copy, modify, and/or distribute this software +-- for any purpose with or without fee is hereby granted, provided +-- that the above copyright notice and this permission notice appear +-- in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +-- + +-- To require fully-qualified names +SELECT pg_catalog.set_config('search_path', '', false); + +DO $upgrade_block$ +BEGIN + +IF brmbar_privileged.has_exact_schema_version(8) THEN + +-- return negative number on rate not found +CREATE OR REPLACE FUNCTION public.find_buy_rate( + IN i_item_id public.accounts.id%TYPE; + IN i_other_id public.accounts.id%TYPE; +) RETURNS NUMERIC +LANGUAGE plpgsql +AS $$ +DECLARE + v_rate public.exchange_rates.rate%TYPE; + v_rate_dir public.exchange_rates.rate_dir%TYPE; +BEGIN + SELECT rate INTO STRICT v_rate, rate_dir INTO STRICT v_rate_dir FROM public.exchange_rates WHERE target = i_item_id AND source = i_other_id AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1; + IF v_rate_dir = 'target_to_source'::public.exchange_rate_direction THEN + RETURN v_rate; + ELSE + RETURN 1/v_rate; + END IF; +EXCEPTION + WHEN NO_DATA_FOUND THEN + RETURN -1; +END; +$$ + + +-- return negative number on rate not found +CREATE OR REPLACE FUNCTION public.find_sell_rate( + IN i_item_id public.accounts.id%TYPE; + IN i_other_id public.accounts.id%TYPE; +) RETURNS NUMERIC +LANGUAGE plpgsql +AS $$ +DECLARE + v_rate public.exchange_rates.rate%TYPE; + v_rate_dir public.exchange_rates.rate_dir%TYPE; +BEGIN + SELECT rate INTO STRICT v_rate, rate_dir INTO STRICT v_rate_dir FROM public.exchange_rates WHERE target = i_other_id AND source = i_item_id AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1; + IF v_rate_dir = 'source_to_target'::public.exchange_rate_direction THEN + RETURN v_rate; + ELSE + RETURN 1/v_rate; + END IF; +EXCEPTION + WHEN NO_DATA_FOUND THEN + RETURN -1; +END; +$$ + +CREATE OR REPLACE FUNCTION public.create_transaction( + i_responsible_id public.accounts.id%TYPE, + i_description public.transactions.description%TYPE +) RETURNS public.transactions.id%TYPE AS $$ +DECLARE + new_transaction_id public.transactions%TYPE; +BEGIN + -- Create a new transaction + INSERT INTO transactions (responsible, description) + VALUES (i_responsible_id, i_description) + RETURNING id INTO new_transaction_id; + -- Return the new transaction ID + RETURN new_transaction_id; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION public.sell_item( + i_item_id public.accounts.id%TYPE, + i_amount INTEGER, + i_user_id public.accounts.id%TYPE, + i_target_currency_id public.currencies.id%TYPE, + i_description TEXT +) RETURNS NUMERIC +LANGUAGE plpgsql +AS $$ +DECLARE + v_buy_rate NUMERIC; + v_sell_rate NUMERIC; + v_cost NUMERIC; + v_profit NUMERIC; + v_transaction_id public.transactions.id%TYPE; +BEGIN + -- Get the buy and sell rates from the stored functions + v_buy_rate := find_buy_rate(i_item_id, i_target_currency_id); + v_sell_rate := find_sell_rate(i_item_id, i_target_currency_id); + + -- Calculate cost and profit + v_cost := i_amount * v_sell_rate; + v_profit := i_amount * (v_sell_rate - v_buy_rate); + + -- Create a new transaction + v_transaction_id := create_transaction(i_user_id, i_description); + + -- the item (decrease stock) + INSERT INTO transaction_splits (transaction, side, account, amount, memo) + VALUES (i_transaction_id, 'credit', i_item_id, i_amount, + (SELECT "name" FROM public.accounts WHERE id = i_user_id)); + + -- the user + INSERT INTO transaction_splits (transaction, side, account, amount, memo) + VALUES (i_transaction_id, 'debit', i_user_id, v_cost, + (SELECT "name" FROM public.accounts WHERE id = i_item_id)); + + -- the profit + INSERT INTO transaction_splits (transaction, side, account, amount, memo) + VALUES (i_transaction_id, 'debit', (SELECT account_id FROM accounts WHERE name = 'BrmBar Profits'), v_profit, (SELECT 'Margin on ' || "name" FROM public.accounts WHERE id = i_item_id)); + + -- Return the cost + RETURN v_cost; +END; +$$; + +PERFORM brmbar_privileged.upgrade_schema_version_to(9); +END IF; + +END; +$upgrade_block$; + +-- vim: set ft=plsql : From 4c7012c517b3933468a844c78ded98b0c83e9f5f Mon Sep 17 00:00:00 2001 From: TMA Date: Mon, 21 Apr 2025 12:22:22 +0200 Subject: [PATCH 057/139] #10: stored function for cash sell transaction --- brmbar3/brmbar/Shop.py | 25 ++-- brmbar3/schema/0010-shop-sell-for-cash.sql | 141 +++++++++++++++++++++ 2 files changed, 157 insertions(+), 9 deletions(-) create mode 100644 brmbar3/schema/0010-shop-sell-for-cash.sql diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index f005d7b..0e9b3b2 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -46,18 +46,25 @@ class Shop: #return cost def sell_for_cash(self, item, amount = 1): - # Sale: Currency conversion from item currency to shop currency - (buy, sell) = item.currency.rates(self.currency) - cost = amount * sell - profit = amount * (sell - buy) + cost = self.db.execute_and_fetch( + "SELECT public.sell_item_for_cash(%s, %s, %s, %s, %s)", + [item.id, amount, user.id, self.currency.id, f"BrmBar sale of {amount}x {item.name} for cash"] + )[0]#[0] - transaction = self._transaction(description = "BrmBar sale of {}x {} for cash".format(amount, item.name)) - item.credit(transaction, amount, "Cash") - self.cash.debit(transaction, cost, item.name) - self.profits.debit(transaction, profit, "Margin on " + item.name) self.db.commit() - return cost + ## Sale: Currency conversion from item currency to shop currency + #(buy, sell) = item.currency.rates(self.currency) + #cost = amount * sell + #profit = amount * (sell - buy) + + #transaction = self._transaction(description = "BrmBar sale of {}x {} for cash".format(amount, item.name)) + #item.credit(transaction, amount, "Cash") + #self.cash.debit(transaction, cost, item.name) + #self.profits.debit(transaction, profit, "Margin on " + item.name) + #self.db.commit() + + #return cost def undo_sale(self, item, user, amount = 1): # Undo sale; rarely needed diff --git a/brmbar3/schema/0010-shop-sell-for-cash.sql b/brmbar3/schema/0010-shop-sell-for-cash.sql new file mode 100644 index 0000000..03dabda --- /dev/null +++ b/brmbar3/schema/0010-shop-sell-for-cash.sql @@ -0,0 +1,141 @@ +-- +-- 0009-shop-sell.sql +-- +-- #10 - stored function for cash sell transaction +-- +-- ISC License +-- +-- Copyright 2023-2025 Brmlab, z.s. +-- TMA +-- +-- Permission to use, copy, modify, and/or distribute this software +-- for any purpose with or without fee is hereby granted, provided +-- that the above copyright notice and this permission notice appear +-- in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +-- + +-- To require fully-qualified names +SELECT pg_catalog.set_config('search_path', '', false); + +DO $upgrade_block$ +BEGIN + +IF brmbar_privileged.has_exact_schema_version(9) THEN + +CREATE OR REPLACE FUNCTION brmbar_privileged.create_transaction( + i_responsible_id public.accounts.id%TYPE, + i_description public.transactions.description%TYPE +) RETURNS public.transactions.id%TYPE AS $$ +DECLARE + new_transaction_id public.transactions%TYPE; +BEGIN + -- Create a new transaction + INSERT INTO public.transactions (responsible, description) + VALUES (i_responsible_id, i_description) + RETURNING id INTO new_transaction_id; + -- Return the new transaction ID + RETURN new_transaction_id; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION brmbar_privileged.sell_item_internal( + i_item_id public.accounts.id%TYPE, + i_amount INTEGER, + i_user_id public.accounts.id%TYPE, + i_target_currency_id public.currencies.id%TYPE, + i_other_memo TEXT, + i_description TEXT +) RETURNS NUMERIC +LANGUAGE plpgsql +AS $$ +DECLARE + v_buy_rate NUMERIC; + v_sell_rate NUMERIC; + v_cost NUMERIC; + v_profit NUMERIC; + v_transaction_id public.transactions.id%TYPE; +BEGIN + -- Get the buy and sell rates from the stored functions + v_buy_rate := public.find_buy_rate(i_item_id, i_target_currency_id); + v_sell_rate := public.find_sell_rate(i_item_id, i_target_currency_id); + + -- Calculate cost and profit + v_cost := i_amount * v_sell_rate; + v_profit := i_amount * (v_sell_rate - v_buy_rate); + + -- Create a new transaction + v_transaction_id := brmbar_privileged.create_transaction(i_user_id, i_description); + + -- the item (decrease stock) + INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) + VALUES (i_transaction_id, 'credit', i_item_id, i_amount, + i_other_memo); + + -- the user + INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) + VALUES (i_transaction_id, 'debit', i_user_id, v_cost, + (SELECT "name" FROM public.accounts WHERE id = i_item_id)); + + -- the profit + INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) + VALUES (i_transaction_id, 'debit', (SELECT account_id FROM accounts WHERE name = 'BrmBar Profits'), v_profit, (SELECT 'Margin on ' || "name" FROM public.accounts WHERE id = i_item_id)); + + -- Return the cost + RETURN v_cost; +END; +$$; + +CREATE OR REPLACE FUNCTION public.sell_item( + i_item_id public.accounts.id%TYPE, + i_amount INTEGER, + i_user_id public.accounts.id%TYPE, + i_target_currency_id public.currencies.id%TYPE, + i_description TEXT +) RETURNS NUMERIC +LANGUAGE plpgsql +AS $$ +BEGIN + RETURN brmbar_privileged.sell_item_internal(i_item_id, + i_amount, + i_other_id, + i_target_currency_id, + (SELECT "name" FROM public.accounts WHERE id = i_user_id), + i_description); +END; +$$; + +CREATE OR REPLACE FUNCTION public.sell_item_for_cash( + i_item_id public.accounts.id%TYPE, + i_amount INTEGER, + i_user_id public.accounts.id%TYPE, + i_target_currency_id public.currencies.id%TYPE, + i_description TEXT +) RETURNS NUMERIC +LANGUAGE plpgsql +AS $$ +BEGIN + RETURN brmbar_privileged.sell_item_internal(i_item_id, + i_amount, + i_other_id, + i_target_currency_id, + 'Cash', + i_description); +END; +$$; + +PERFORM brmbar_privileged.upgrade_schema_version_to(10); +END IF; + +END; +$upgrade_block$; + +-- vim: set ft=plsql : From deb3faa71707811dafa84303cca39f7bda482fba Mon Sep 17 00:00:00 2001 From: TMA Date: Mon, 21 Apr 2025 12:27:29 +0200 Subject: [PATCH 058/139] minor fixes in schemata 0009 and 0010 --- brmbar3/schema/0009-shop-sell.sql | 14 +++++++------- brmbar3/schema/0010-shop-sell-for-cash.sql | 2 ++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/brmbar3/schema/0009-shop-sell.sql b/brmbar3/schema/0009-shop-sell.sql index 64d4353..811a5f5 100644 --- a/brmbar3/schema/0009-shop-sell.sql +++ b/brmbar3/schema/0009-shop-sell.sql @@ -86,7 +86,7 @@ DECLARE new_transaction_id public.transactions%TYPE; BEGIN -- Create a new transaction - INSERT INTO transactions (responsible, description) + INSERT INTO public.transactions (responsible, description) VALUES (i_responsible_id, i_description) RETURNING id INTO new_transaction_id; -- Return the new transaction ID @@ -111,28 +111,28 @@ DECLARE v_transaction_id public.transactions.id%TYPE; BEGIN -- Get the buy and sell rates from the stored functions - v_buy_rate := find_buy_rate(i_item_id, i_target_currency_id); - v_sell_rate := find_sell_rate(i_item_id, i_target_currency_id); + v_buy_rate := public.find_buy_rate(i_item_id, i_target_currency_id); + v_sell_rate := public.find_sell_rate(i_item_id, i_target_currency_id); -- Calculate cost and profit v_cost := i_amount * v_sell_rate; v_profit := i_amount * (v_sell_rate - v_buy_rate); -- Create a new transaction - v_transaction_id := create_transaction(i_user_id, i_description); + v_transaction_id := public.create_transaction(i_user_id, i_description); -- the item (decrease stock) - INSERT INTO transaction_splits (transaction, side, account, amount, memo) + INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) VALUES (i_transaction_id, 'credit', i_item_id, i_amount, (SELECT "name" FROM public.accounts WHERE id = i_user_id)); -- the user - INSERT INTO transaction_splits (transaction, side, account, amount, memo) + INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) VALUES (i_transaction_id, 'debit', i_user_id, v_cost, (SELECT "name" FROM public.accounts WHERE id = i_item_id)); -- the profit - INSERT INTO transaction_splits (transaction, side, account, amount, memo) + INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) VALUES (i_transaction_id, 'debit', (SELECT account_id FROM accounts WHERE name = 'BrmBar Profits'), v_profit, (SELECT 'Margin on ' || "name" FROM public.accounts WHERE id = i_item_id)); -- Return the cost diff --git a/brmbar3/schema/0010-shop-sell-for-cash.sql b/brmbar3/schema/0010-shop-sell-for-cash.sql index 03dabda..ea30445 100644 --- a/brmbar3/schema/0010-shop-sell-for-cash.sql +++ b/brmbar3/schema/0010-shop-sell-for-cash.sql @@ -132,6 +132,8 @@ BEGIN END; $$; +DROP FUNCTION public.create_transaction; + PERFORM brmbar_privileged.upgrade_schema_version_to(10); END IF; From 028d4d98dbe7dfcdd5352d9cc885be9038a73828 Mon Sep 17 00:00:00 2001 From: TMA Date: Mon, 21 Apr 2025 17:06:21 +0200 Subject: [PATCH 059/139] #11: stored function for sale undo transaction --- brmbar3/brmbar/Shop.py | 20 +++-- brmbar3/schema/0011-shop-undo-sale.sql | 102 +++++++++++++++++++++++++ 2 files changed, 115 insertions(+), 7 deletions(-) create mode 100644 brmbar3/schema/0011-shop-undo-sale.sql diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index 0e9b3b2..eb57d1a 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -68,14 +68,20 @@ class Shop: def undo_sale(self, item, user, amount = 1): # Undo sale; rarely needed - (buy, sell) = item.currency.rates(self.currency) - cost = amount * sell - profit = amount * (sell - buy) + #(buy, sell) = item.currency.rates(self.currency) + #cost = amount * sell + #profit = amount * (sell - buy) + + #transaction = self._transaction(responsible = user, description = "BrmBar sale UNDO of {}x {} to {}".format(amount, item.name, user.name)) + #item.debit(transaction, amount, user.name + " (sale undo)") + #user.credit(transaction, cost, item.name + " (sale undo)") + #self.profits.credit(transaction, profit, "Margin repaid on " + item.name) + # Call the stored procedure for undoing a sale + cost = self.db.execute_and_fetch( + "SELECT public.undo_sale_of_item(%s, %s, %s, %s)", + [item.id, amount, user.id, user.currency.id, f"BrmBar sale UNDO of {amount}x {item.name} to {user.name}"] + )[0]#[0] - transaction = self._transaction(responsible = user, description = "BrmBar sale UNDO of {}x {} to {}".format(amount, item.name, user.name)) - item.debit(transaction, amount, user.name + " (sale undo)") - user.credit(transaction, cost, item.name + " (sale undo)") - self.profits.credit(transaction, profit, "Margin repaid on " + item.name) self.db.commit() return cost diff --git a/brmbar3/schema/0011-shop-undo-sale.sql b/brmbar3/schema/0011-shop-undo-sale.sql new file mode 100644 index 0000000..67d47cc --- /dev/null +++ b/brmbar3/schema/0011-shop-undo-sale.sql @@ -0,0 +1,102 @@ +-- +-- 0011-shop-undo-sale.sql +-- +-- #11 - stored function for sale undo transaction +-- +-- ISC License +-- +-- Copyright 2023-2025 Brmlab, z.s. +-- TMA +-- +-- Permission to use, copy, modify, and/or distribute this software +-- for any purpose with or without fee is hereby granted, provided +-- that the above copyright notice and this permission notice appear +-- in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +-- + +-- To require fully-qualified names +SELECT pg_catalog.set_config('search_path', '', false); + +DO $upgrade_block$ +BEGIN + +IF brmbar_privileged.has_exact_schema_version(10) THEN + +CREATE OR REPLACE FUNCTION brmbar_privileged.create_transaction( + i_responsible_id public.accounts.id%TYPE, + i_description public.transactions.description%TYPE +) RETURNS public.transactions.id%TYPE AS $$ +DECLARE + new_transaction_id public.transactions%TYPE; +BEGIN + -- Create a new transaction + INSERT INTO public.transactions (responsible, description) + VALUES (i_responsible_id, i_description) + RETURNING id INTO new_transaction_id; + -- Return the new transaction ID + RETURN new_transaction_id; +END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION public.undo_sale_of_item( + i_item_id public.accounts.id%TYPE, + i_amount INTEGER, + i_user_id public.accounts.id%TYPE, + i_target_currency_id public.currencies.id%TYPE, + i_description TEXT +) RETURNS NUMERIC +LANGUAGE plpgsql +AS $$ +DECLARE + v_buy_rate NUMERIC; + v_sell_rate NUMERIC; + v_cost NUMERIC; + v_profit NUMERIC; + v_transaction_id public.transactions.id%TYPE; +BEGIN + -- Get the buy and sell rates from the stored functions + v_buy_rate := public.find_buy_rate(i_item_id, i_target_currency_id); + v_sell_rate := public.find_sell_rate(i_item_id, i_target_currency_id); + + -- Calculate cost and profit + v_cost := i_amount * v_sell_rate; + v_profit := i_amount * (v_sell_rate - v_buy_rate); + + -- Create a new transaction + v_transaction_id := brmbar_privileged.create_transaction(i_user_id, i_description); + + -- the item (decrease stock) + INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) + VALUES (i_transaction_id, 'debit', i_item_id, i_amount, + (SELECT "name" || ' (sale undo)' FROM public.accounts WHERE id = i_user_id)); + + -- the user + INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) + VALUES (i_transaction_id, 'credit', i_user_id, v_cost, + (SELECT "name" || ' (sale undo)' FROM public.accounts WHERE id = i_item_id)); + + -- the profit + INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) + VALUES (i_transaction_id, 'credit', (SELECT account_id FROM accounts WHERE name = 'BrmBar Profits'), v_profit, (SELECT 'Margin repaid on ' || "name" FROM public.accounts WHERE id = i_item_id)); + + -- Return the cost + RETURN v_cost; +END; +$$; + +PERFORM brmbar_privileged.upgrade_schema_version_to(11); +END IF; + +END; +$upgrade_block$; + +-- vim: set ft=plsql : From 7ed7417492464d1e7a2e7168b4a42a84812a8ca5 Mon Sep 17 00:00:00 2001 From: TMA Date: Mon, 21 Apr 2025 17:06:42 +0200 Subject: [PATCH 060/139] schema 0010: fix header id --- brmbar3/schema/0010-shop-sell-for-cash.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brmbar3/schema/0010-shop-sell-for-cash.sql b/brmbar3/schema/0010-shop-sell-for-cash.sql index ea30445..23ad131 100644 --- a/brmbar3/schema/0010-shop-sell-for-cash.sql +++ b/brmbar3/schema/0010-shop-sell-for-cash.sql @@ -1,5 +1,5 @@ -- --- 0009-shop-sell.sql +-- 0010-shop-sell-for-cash.sql -- -- #10 - stored function for cash sell transaction -- From f0cd8361d1fd1f5831e70e42d47db9c53d87c098 Mon Sep 17 00:00:00 2001 From: TMA Date: Mon, 21 Apr 2025 17:23:24 +0200 Subject: [PATCH 061/139] #12 - stored function for cash deposit transactions --- brmbar3/brmbar/Shop.py | 13 ++- brmbar3/schema/0012-shop-add-credit.sql | 110 ++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 brmbar3/schema/0012-shop-add-credit.sql diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index eb57d1a..0f672c2 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -87,10 +87,17 @@ class Shop: return cost def add_credit(self, credit, user): - transaction = self._transaction(responsible = user, description = "BrmBar credit replenishment for " + user.name) - self.cash.debit(transaction, credit, user.name) - user.credit(transaction, credit, "Credit replenishment") + trn = self.db.execute_and_fetch( + "SELECT public.add_credit(%s, %s, %s, %s)", + [self.cash.id, credit, user.id, user.name] + )[0] self.db.commit() + return trn # unused + + #transaction = self._transaction(responsible = user, description = "BrmBar credit replenishment for " + user.name) + #self.cash.debit(transaction, credit, user.name) + #user.credit(transaction, credit, "Credit replenishment") + #self.db.commit() def withdraw_credit(self, credit, user): transaction = self._transaction(responsible = user, description = "BrmBar credit withdrawal for " + user.name) diff --git a/brmbar3/schema/0012-shop-add-credit.sql b/brmbar3/schema/0012-shop-add-credit.sql new file mode 100644 index 0000000..48f2da6 --- /dev/null +++ b/brmbar3/schema/0012-shop-add-credit.sql @@ -0,0 +1,110 @@ +-- +-- 0012-shop-add-credit.sql +-- +-- #12 - stored function for cash deposit transactions +-- +-- ISC License +-- +-- Copyright 2023-2025 Brmlab, z.s. +-- TMA +-- +-- Permission to use, copy, modify, and/or distribute this software +-- for any purpose with or without fee is hereby granted, provided +-- that the above copyright notice and this permission notice appear +-- in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +-- + +-- To require fully-qualified names +SELECT pg_catalog.set_config('search_path', '', false); + +DO $upgrade_block$ +BEGIN + +IF brmbar_privileged.has_exact_schema_version(11) THEN + +CREATE OR REPLACE FUNCTION public.add_credit( + i_cash_account_id public.accounts.id%TYPE, + i_credit NUMERIC, + i_user_id public.accounts.id%TYPE, + i_user_name TEXT +) RETURNS VOID +LANGUAGE plpgsql +AS $$ +DECLARE + v_transaction_id public.transactions.id%TYPE; +BEGIN + -- Create a new transaction + v_transaction_id := brmbar_privileged.create_transaction(i_user_id, 'BrmBar credit replenishment for ' || i_user_name); + -- Debit cash (credit replenishment) + INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) + VALUES (v_transaction_id, 'debit', i_cash_account_id, i_credit, i_user_name); + -- Credit the user + INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) + VALUES (v_transaction_id, 'credit', i_user_id, i_credit, 'Credit replenishment'); +END; +$$; + + + +CREATE OR REPLACE FUNCTION public.undo_sale_of_item( + i_item_id public.accounts.id%TYPE, + i_amount INTEGER, + i_user_id public.accounts.id%TYPE, + i_target_currency_id public.currencies.id%TYPE, + i_description TEXT +) RETURNS NUMERIC +LANGUAGE plpgsql +AS $$ +DECLARE + v_buy_rate NUMERIC; + v_sell_rate NUMERIC; + v_cost NUMERIC; + v_profit NUMERIC; + v_transaction_id public.transactions.id%TYPE; +BEGIN + -- Get the buy and sell rates from the stored functions + v_buy_rate := public.find_buy_rate(i_item_id, i_target_currency_id); + v_sell_rate := public.find_sell_rate(i_item_id, i_target_currency_id); + + -- Calculate cost and profit + v_cost := i_amount * v_sell_rate; + v_profit := i_amount * (v_sell_rate - v_buy_rate); + + -- Create a new transaction + v_transaction_id := brmbar_privileged.create_transaction(i_user_id, i_description); + + -- the item (decrease stock) + INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) + VALUES (i_transaction_id, 'debit', i_item_id, i_amount, + (SELECT "name" || ' (sale undo)' FROM public.accounts WHERE id = i_user_id)); + + -- the user + INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) + VALUES (i_transaction_id, 'credit', i_user_id, v_cost, + (SELECT "name" || ' (sale undo)' FROM public.accounts WHERE id = i_item_id)); + + -- the profit + INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) + VALUES (i_transaction_id, 'credit', (SELECT account_id FROM accounts WHERE name = 'BrmBar Profits'), v_profit, (SELECT 'Margin repaid on ' || "name" FROM public.accounts WHERE id = i_item_id)); + + -- Return the cost + RETURN v_cost; +END; +$$; + +PERFORM brmbar_privileged.upgrade_schema_version_to(12); +END IF; + +END; +$upgrade_block$; + +-- vim: set ft=plsql : From 7b11a8c954c71f52d64f0b0c180ffbe7f1372b19 Mon Sep 17 00:00:00 2001 From: TMA Date: Mon, 21 Apr 2025 17:30:57 +0200 Subject: [PATCH 062/139] #12 - stored function for cash deposit transactions - remove duplicate fn --- brmbar3/schema/0012-shop-add-credit.sql | 46 ------------------------- 1 file changed, 46 deletions(-) diff --git a/brmbar3/schema/0012-shop-add-credit.sql b/brmbar3/schema/0012-shop-add-credit.sql index 48f2da6..00318ae 100644 --- a/brmbar3/schema/0012-shop-add-credit.sql +++ b/brmbar3/schema/0012-shop-add-credit.sql @@ -55,52 +55,6 @@ $$; -CREATE OR REPLACE FUNCTION public.undo_sale_of_item( - i_item_id public.accounts.id%TYPE, - i_amount INTEGER, - i_user_id public.accounts.id%TYPE, - i_target_currency_id public.currencies.id%TYPE, - i_description TEXT -) RETURNS NUMERIC -LANGUAGE plpgsql -AS $$ -DECLARE - v_buy_rate NUMERIC; - v_sell_rate NUMERIC; - v_cost NUMERIC; - v_profit NUMERIC; - v_transaction_id public.transactions.id%TYPE; -BEGIN - -- Get the buy and sell rates from the stored functions - v_buy_rate := public.find_buy_rate(i_item_id, i_target_currency_id); - v_sell_rate := public.find_sell_rate(i_item_id, i_target_currency_id); - - -- Calculate cost and profit - v_cost := i_amount * v_sell_rate; - v_profit := i_amount * (v_sell_rate - v_buy_rate); - - -- Create a new transaction - v_transaction_id := brmbar_privileged.create_transaction(i_user_id, i_description); - - -- the item (decrease stock) - INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) - VALUES (i_transaction_id, 'debit', i_item_id, i_amount, - (SELECT "name" || ' (sale undo)' FROM public.accounts WHERE id = i_user_id)); - - -- the user - INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) - VALUES (i_transaction_id, 'credit', i_user_id, v_cost, - (SELECT "name" || ' (sale undo)' FROM public.accounts WHERE id = i_item_id)); - - -- the profit - INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) - VALUES (i_transaction_id, 'credit', (SELECT account_id FROM accounts WHERE name = 'BrmBar Profits'), v_profit, (SELECT 'Margin repaid on ' || "name" FROM public.accounts WHERE id = i_item_id)); - - -- Return the cost - RETURN v_cost; -END; -$$; - PERFORM brmbar_privileged.upgrade_schema_version_to(12); END IF; From e15f1646ce460c288a17067905bd95889ad32e8a Mon Sep 17 00:00:00 2001 From: TMA Date: Mon, 21 Apr 2025 17:35:03 +0200 Subject: [PATCH 063/139] #13: stored function for cash withdrawal transactions --- brmbar3/brmbar/Shop.py | 12 +++- brmbar3/schema/0013-shop-withdraw-credit.sql | 64 ++++++++++++++++++++ 2 files changed, 73 insertions(+), 3 deletions(-) create mode 100644 brmbar3/schema/0013-shop-withdraw-credit.sql diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index 0f672c2..85251de 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -100,10 +100,16 @@ class Shop: #self.db.commit() def withdraw_credit(self, credit, user): - transaction = self._transaction(responsible = user, description = "BrmBar credit withdrawal for " + user.name) - self.cash.credit(transaction, credit, user.name) - user.debit(transaction, credit, "Credit withdrawal") + trn = self.db.execute_and_fetch( + "SELECT public.withdraw_credit(%s, %s, %s, %s)", + [self.cash.id, credit, user.id, user.name] + )[0] self.db.commit() + return trn # unused + #transaction = self._transaction(responsible = user, description = "BrmBar credit withdrawal for " + user.name) + #self.cash.credit(transaction, credit, user.name) + #user.debit(transaction, credit, "Credit withdrawal") + #self.db.commit() def transfer_credit(self, userfrom, userto, amount): self.add_credit(amount, userto) diff --git a/brmbar3/schema/0013-shop-withdraw-credit.sql b/brmbar3/schema/0013-shop-withdraw-credit.sql new file mode 100644 index 0000000..d379186 --- /dev/null +++ b/brmbar3/schema/0013-shop-withdraw-credit.sql @@ -0,0 +1,64 @@ +-- +-- 0013-shop-withdraw-credit.sql +-- +-- #13 - stored function for cash withdrawal transactions +-- +-- ISC License +-- +-- Copyright 2023-2025 Brmlab, z.s. +-- TMA +-- +-- Permission to use, copy, modify, and/or distribute this software +-- for any purpose with or without fee is hereby granted, provided +-- that the above copyright notice and this permission notice appear +-- in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +-- + +-- To require fully-qualified names +SELECT pg_catalog.set_config('search_path', '', false); + +DO $upgrade_block$ +BEGIN + +IF brmbar_privileged.has_exact_schema_version(12) THEN + +CREATE OR REPLACE FUNCTION public.withdraw_credit( + i_cash_account_id public.accounts.id%TYPE, + i_credit NUMERIC, + i_user_id public.accounts.id%TYPE, + i_user_name TEXT +) RETURNS VOID +LANGUAGE plpgsql +AS $$ +DECLARE + v_transaction_id public.transactions.id%TYPE; +BEGIN + -- Create a new transaction + v_transaction_id := brmbar_privileged.create_transaction(i_user_id, 'BrmBar credit withdrawal for ' || i_user_name); + -- Debit cash (credit replenishment) + INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) + VALUES (v_transaction_id, 'credit', i_cash_account_id, i_credit, i_user_name); + -- Credit the user + INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) + VALUES (v_transaction_id, 'debit', i_user_id, i_credit, 'Credit withdrawal'); +END; +$$; + + + +PERFORM brmbar_privileged.upgrade_schema_version_to(13); +END IF; + +END; +$upgrade_block$; + +-- vim: set ft=plsql : From ad832fc71ba7a0dfcc45ee6f2d6f851c8c453481 Mon Sep 17 00:00:00 2001 From: TMA Date: Mon, 21 Apr 2025 17:45:40 +0200 Subject: [PATCH 064/139] #14: stored function for "credit" transfer transactions --- brmbar3/brmbar/Shop.py | 19 ++++--- brmbar3/schema/0014-shop-transfer-credit.sql | 58 ++++++++++++++++++++ 2 files changed, 69 insertions(+), 8 deletions(-) create mode 100644 brmbar3/schema/0014-shop-transfer-credit.sql diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index 85251de..df29c18 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -87,12 +87,11 @@ class Shop: return cost def add_credit(self, credit, user): - trn = self.db.execute_and_fetch( + self.db.execute_and_fetch( "SELECT public.add_credit(%s, %s, %s, %s)", [self.cash.id, credit, user.id, user.name] - )[0] + ) self.db.commit() - return trn # unused #transaction = self._transaction(responsible = user, description = "BrmBar credit replenishment for " + user.name) #self.cash.debit(transaction, credit, user.name) @@ -100,20 +99,24 @@ class Shop: #self.db.commit() def withdraw_credit(self, credit, user): - trn = self.db.execute_and_fetch( + self.db.execute_and_fetch( "SELECT public.withdraw_credit(%s, %s, %s, %s)", [self.cash.id, credit, user.id, user.name] - )[0] + ) self.db.commit() - return trn # unused #transaction = self._transaction(responsible = user, description = "BrmBar credit withdrawal for " + user.name) #self.cash.credit(transaction, credit, user.name) #user.debit(transaction, credit, "Credit withdrawal") #self.db.commit() def transfer_credit(self, userfrom, userto, amount): - self.add_credit(amount, userto) - self.withdraw_credit(amount, userfrom) + self.db.execute_and_fetch( + "SELECT public.transfer_credit(%s, %s, %s, %s)", + [self.cash.id, credit, user.id, user.name] + ) + self.db.commit() + #self.add_credit(amount, userto) + #self.withdraw_credit(amount, userfrom) def buy_for_cash(self, item, amount = 1): # Buy: Currency conversion from item currency to shop currency diff --git a/brmbar3/schema/0014-shop-transfer-credit.sql b/brmbar3/schema/0014-shop-transfer-credit.sql new file mode 100644 index 0000000..48dc644 --- /dev/null +++ b/brmbar3/schema/0014-shop-transfer-credit.sql @@ -0,0 +1,58 @@ +-- +-- 0014-shop-transfer-credit.sql +-- +-- #14 - stored function for "credit" transfer transactions +-- +-- ISC License +-- +-- Copyright 2023-2025 Brmlab, z.s. +-- TMA +-- +-- Permission to use, copy, modify, and/or distribute this software +-- for any purpose with or without fee is hereby granted, provided +-- that the above copyright notice and this permission notice appear +-- in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +-- + +-- To require fully-qualified names +SELECT pg_catalog.set_config('search_path', '', false); + +DO $upgrade_block$ +BEGIN + +IF brmbar_privileged.has_exact_schema_version(12) THEN + +CREATE OR REPLACE FUNCTION public.transfer_credit( + i_cash_account_id public.accounts.id%TYPE, + i_credit NUMERIC, + i_userfrom_id public.accounts.id%TYPE, + i_userfrom_name TEXT, + i_userto_id public.accounts.id%TYPE, + i_userto_name TEXT +) RETURNS VOID +LANGUAGE plpgsql +AS $$ +BEGIN + PERFORM public.add_credit(i_cash_account_id, i_credit, i_userto_id, i_userto_name); + PERFORM public.withdraw_credit(i_cash_account_id, i_credit, i_userfrom_id, i_userfrom_name); +END; +$$; + + + +PERFORM brmbar_privileged.upgrade_schema_version_to(13); +END IF; + +END; +$upgrade_block$; + +-- vim: set ft=plsql : From 629b35655dd6fd97f506fc5598ab34a4d399589d Mon Sep 17 00:00:00 2001 From: TMA Date: Mon, 21 Apr 2025 18:23:11 +0200 Subject: [PATCH 065/139] #15: stored function for cash-based stock replenishment transaction --- brmbar3/brmbar/Shop.py | 14 ++-- brmbar3/schema/0015-shop-buy-for-cash.sql | 82 +++++++++++++++++++++++ 2 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 brmbar3/schema/0015-shop-buy-for-cash.sql diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index df29c18..f669c75 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -119,13 +119,17 @@ class Shop: #self.withdraw_credit(amount, userfrom) def buy_for_cash(self, item, amount = 1): + cost = self.db.execute_and_fetch( + "SELECT public.buy_for_cash(%s, %s, %s, %s, %s)", + [self.cash.id, item.id, amount, self.currency.id, item.name] + )[0] # Buy: Currency conversion from item currency to shop currency - (buy, sell) = item.currency.rates(self.currency) - cost = amount * buy + #(buy, sell) = item.currency.rates(self.currency) + #cost = amount * buy - transaction = self._transaction(description = "BrmBar stock replenishment of {}x {} for cash".format(amount, item.name)) - item.debit(transaction, amount, "Cash") - self.cash.credit(transaction, cost, item.name) + #transaction = self._transaction(description = "BrmBar stock replenishment of {}x {} for cash".format(amount, item.name)) + #item.debit(transaction, amount, "Cash") + #self.cash.credit(transaction, cost, item.name) self.db.commit() return cost diff --git a/brmbar3/schema/0015-shop-buy-for-cash.sql b/brmbar3/schema/0015-shop-buy-for-cash.sql new file mode 100644 index 0000000..9736d2e --- /dev/null +++ b/brmbar3/schema/0015-shop-buy-for-cash.sql @@ -0,0 +1,82 @@ +-- +-- 0015-shop-buy-for-cash.sql +-- +-- #15 - stored function for cash-based stock replenishment transaction +-- +-- ISC License +-- +-- Copyright 2023-2025 Brmlab, z.s. +-- TMA +-- +-- Permission to use, copy, modify, and/or distribute this software +-- for any purpose with or without fee is hereby granted, provided +-- that the above copyright notice and this permission notice appear +-- in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +-- + +-- To require fully-qualified names +SELECT pg_catalog.set_config('search_path', '', false); + +DO $upgrade_block$ +BEGIN + +IF brmbar_privileged.has_exact_schema_version(9) THEN + +CREATE OR REPLACE FUNCTION public.buy_for_cash( + i_cash_account_id public.accounts.id%TYPE, + i_item_id public.accounts.id%TYPE, + i_amount INTEGER, + i_target_currency_id public.currencies.id%TYPE, + i_item_name TEXT +) RETURNS NUMERIC +LANGUAGE plpgsql +AS $$ +DECLARE + v_buy_rate NUMERIC; + v_cost NUMERIC; + v_transaction_id public.transactions.id%TYPE; +BEGIN + -- this could fail and it would generate exception in python + -- FIXME: convert v_buy_rate < 0 into python exception + v_buy_rate := public.find_buy_rate(i_item_id, i_target_currency_id); + -- this could fail and it would generate exception in python, even though it is not used + --v_sell_rate := public.find_sell_rate(i_item_id, i_target_currency_id); + + -- Calculate cost and profit + v_cost := i_amount * v_buy_rate; + + -- Create a new transaction + v_transaction_id := brmbar_privileged.create_transaction(NULL, + 'BrmBar stock replenishment of ' || i_amount || 'x ' || i_item_name || ' for cash'); + + -- the item + INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) + VALUES (i_transaction_id, 'debit', i_item_id, i_amount, + 'Cash'); + + -- the cash + INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) + VALUES (i_transaction_id, 'credit', i_cash_account_id, v_cost, + i_item_name); + + -- Return the cost + RETURN v_cost; +END; +$$; + +PERFORM brmbar_privileged.upgrade_schema_version_to(10); +END IF; + +END; +$upgrade_block$; + +-- vim: set ft=plsql : From 1df7db93e71dd00db7e556f724879e3c1cc4528b Mon Sep 17 00:00:00 2001 From: TMA Date: Mon, 21 Apr 2025 18:36:50 +0200 Subject: [PATCH 066/139] #16: stored function for receipt reimbursement transaction --- brmbar3/brmbar/Shop.py | 10 ++- .../schema/0016-shop-receipt-to-credit.sql | 64 +++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 brmbar3/schema/0016-shop-receipt-to-credit.sql diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index f669c75..1bdd4c2 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -135,9 +135,13 @@ class Shop: return cost def receipt_to_credit(self, user, credit, description): - transaction = self._transaction(responsible = user, description = "Receipt: " + description) - self.profits.credit(transaction, credit, user.name) - user.credit(transaction, credit, "Credit from receipt: " + description) + #transaction = self._transaction(responsible = user, description = "Receipt: " + description) + #self.profits.credit(transaction, credit, user.name) + #user.credit(transaction, credit, "Credit from receipt: " + description) + self.db.execute_and_fetch( + "SELECT public.buy_for_cash(%s, %s, %s, %s, %s)", + [self.profits.id, user.id, user.name, credit, description] + )[0] self.db.commit() def _transaction(self, responsible = None, description = None): diff --git a/brmbar3/schema/0016-shop-receipt-to-credit.sql b/brmbar3/schema/0016-shop-receipt-to-credit.sql new file mode 100644 index 0000000..1b91e20 --- /dev/null +++ b/brmbar3/schema/0016-shop-receipt-to-credit.sql @@ -0,0 +1,64 @@ +-- +-- 0016-shop-buy-for-cash.sql +-- +-- #16 - stored function for receipt reimbursement transaction +-- +-- ISC License +-- +-- Copyright 2023-2025 Brmlab, z.s. +-- TMA +-- +-- Permission to use, copy, modify, and/or distribute this software +-- for any purpose with or without fee is hereby granted, provided +-- that the above copyright notice and this permission notice appear +-- in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +-- + +-- To require fully-qualified names +SELECT pg_catalog.set_config('search_path', '', false); + +DO $upgrade_block$ +BEGIN + +IF brmbar_privileged.has_exact_schema_version(9) THEN + +CREATE OR REPLACE FUNCTION public.receipt_reimbursement( + i_profits_id public.accounts.id%TYPE, + i_user_id public.accounts.id%TYPE, + i_user_name public.accounts.name%TYPE, + i_amount NUMERIC, + i_description TEXT +) RETURNS VOID +LANGUAGE plpgsql +AS $$ +DECLARE + v_transaction_id public.transactions.id%TYPE; +BEGIN + -- Create a new transaction + v_transaction_id := brmbar_privileged.create_transaction(i_user_id, + 'Receipt: ' || i_description); + -- the "profit" + INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) + VALUES (i_transaction_id, 'credit', i_profits_id, i_amount, i_user_name); + -- the user + INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) + VALUES (i_transaction_id, 'credit', i_user_id, i_amount, 'Credit from receipt: ' || i_description); +END; +$$; + +PERFORM brmbar_privileged.upgrade_schema_version_to(10); +END IF; + +END; +$upgrade_block$; + +-- vim: set ft=plsql : From aad79dafa6a75ec60e1d6b129f0099489d22c796 Mon Sep 17 00:00:00 2001 From: TMA Date: Mon, 21 Apr 2025 18:45:00 +0200 Subject: [PATCH 067/139] fix bugs in schema version checking --- brmbar3/schema/0014-shop-transfer-credit.sql | 4 ++-- brmbar3/schema/0015-shop-buy-for-cash.sql | 4 ++-- brmbar3/schema/0016-shop-receipt-to-credit.sql | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/brmbar3/schema/0014-shop-transfer-credit.sql b/brmbar3/schema/0014-shop-transfer-credit.sql index 48dc644..7e13c03 100644 --- a/brmbar3/schema/0014-shop-transfer-credit.sql +++ b/brmbar3/schema/0014-shop-transfer-credit.sql @@ -29,7 +29,7 @@ SELECT pg_catalog.set_config('search_path', '', false); DO $upgrade_block$ BEGIN -IF brmbar_privileged.has_exact_schema_version(12) THEN +IF brmbar_privileged.has_exact_schema_version(13) THEN CREATE OR REPLACE FUNCTION public.transfer_credit( i_cash_account_id public.accounts.id%TYPE, @@ -49,7 +49,7 @@ $$; -PERFORM brmbar_privileged.upgrade_schema_version_to(13); +PERFORM brmbar_privileged.upgrade_schema_version_to(14); END IF; END; diff --git a/brmbar3/schema/0015-shop-buy-for-cash.sql b/brmbar3/schema/0015-shop-buy-for-cash.sql index 9736d2e..5cf12bb 100644 --- a/brmbar3/schema/0015-shop-buy-for-cash.sql +++ b/brmbar3/schema/0015-shop-buy-for-cash.sql @@ -29,7 +29,7 @@ SELECT pg_catalog.set_config('search_path', '', false); DO $upgrade_block$ BEGIN -IF brmbar_privileged.has_exact_schema_version(9) THEN +IF brmbar_privileged.has_exact_schema_version(14) THEN CREATE OR REPLACE FUNCTION public.buy_for_cash( i_cash_account_id public.accounts.id%TYPE, @@ -73,7 +73,7 @@ BEGIN END; $$; -PERFORM brmbar_privileged.upgrade_schema_version_to(10); +PERFORM brmbar_privileged.upgrade_schema_version_to(15); END IF; END; diff --git a/brmbar3/schema/0016-shop-receipt-to-credit.sql b/brmbar3/schema/0016-shop-receipt-to-credit.sql index 1b91e20..21af8b1 100644 --- a/brmbar3/schema/0016-shop-receipt-to-credit.sql +++ b/brmbar3/schema/0016-shop-receipt-to-credit.sql @@ -29,7 +29,7 @@ SELECT pg_catalog.set_config('search_path', '', false); DO $upgrade_block$ BEGIN -IF brmbar_privileged.has_exact_schema_version(9) THEN +IF brmbar_privileged.has_exact_schema_version(15) THEN CREATE OR REPLACE FUNCTION public.receipt_reimbursement( i_profits_id public.accounts.id%TYPE, @@ -55,7 +55,7 @@ BEGIN END; $$; -PERFORM brmbar_privileged.upgrade_schema_version_to(10); +PERFORM brmbar_privileged.upgrade_schema_version_to(16); END IF; END; From 96027ca66ae65c14287b6ead3ac2a3ef49eb9791 Mon Sep 17 00:00:00 2001 From: TMA Date: Mon, 21 Apr 2025 19:49:07 +0200 Subject: [PATCH 068/139] #19: stored function for "consolidation" transaction --- brmbar3/brmbar/Account.py | 15 ++-- brmbar3/brmbar/Shop.py | 29 ++++--- brmbar3/schema/0019-shop-consolidate.sql | 97 ++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 17 deletions(-) create mode 100644 brmbar3/schema/0019-shop-consolidate.sql diff --git a/brmbar3/brmbar/Account.py b/brmbar3/brmbar/Account.py index 40e86be..215df11 100644 --- a/brmbar3/brmbar/Account.py +++ b/brmbar3/brmbar/Account.py @@ -46,11 +46,16 @@ class Account: return cls(db, name = name, id = id, currency = currency, acctype = acctype) def balance(self): - debit = self.db.execute_and_fetch("SELECT SUM(amount) FROM transaction_splits WHERE account = %s AND side = %s", [self.id, 'debit']) - debit = debit[0] or 0 - credit = self.db.execute_and_fetch("SELECT SUM(amount) FROM transaction_splits WHERE account = %s AND side = %s", [self.id, 'credit']) - credit = credit[0] or 0 - return debit - credit + bal = self.db.execute_and_fetch( + "SELECT public.compute_account_balance(%s)", + [self.id] + )[0] + return bal + #debit = self.db.execute_and_fetch("SELECT SUM(amount) FROM transaction_splits WHERE account = %s AND side = %s", [self.id, 'debit']) + #debit = debit[0] or 0 + #credit = self.db.execute_and_fetch("SELECT SUM(amount) FROM transaction_splits WHERE account = %s AND side = %s", [self.id, 'credit']) + #credit = credit[0] or 0 + #return debit - credit def balance_str(self): return self.currency.str(self.balance()) diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index 1bdd4c2..e0084f5 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -248,18 +248,23 @@ class Shop: return False def consolidate(self): - transaction = self._transaction(description = "BrmBar inventory consolidation") - - excess_balance = self.excess.balance() - if excess_balance != 0: - print("Excess balance {} debited to profit".format(-excess_balance)) - self.excess.debit(transaction, -excess_balance, "Excess balance added to profit.") - self.profits.debit(transaction, -excess_balance, "Excess balance added to profit.") - deficit_balance = self.deficit.balance() - if deficit_balance != 0: - print("Deficit balance {} credited to profit".format(deficit_balance)) - self.deficit.credit(transaction, deficit_balance, "Deficit balance removed from profit.") - self.profits.credit(transaction, deficit_balance, "Deficit balance removed from profit.") + msg = self.db.execute_and_fetch( + "SELECT public.make_consolidate_transaction(%s, %s, %s)", + [self.excess.id, self.deficit.id, self.profits.id] + )[0] + #transaction = self._transaction(description = "BrmBar inventory consolidation") + #excess_balance = self.excess.balance() + #if excess_balance != 0: + # print("Excess balance {} debited to profit".format(-excess_balance)) + # self.excess.debit(transaction, -excess_balance, "Excess balance added to profit.") + # self.profits.debit(transaction, -excess_balance, "Excess balance added to profit.") + #deficit_balance = self.deficit.balance() + #if deficit_balance != 0: + # print("Deficit balance {} credited to profit".format(deficit_balance)) + # self.deficit.credit(transaction, deficit_balance, "Deficit balance removed from profit.") + # self.profits.credit(transaction, deficit_balance, "Deficit balance removed from profit.") + if msg != None: + print(msg) self.db.commit() def undo(self, oldtid): diff --git a/brmbar3/schema/0019-shop-consolidate.sql b/brmbar3/schema/0019-shop-consolidate.sql new file mode 100644 index 0000000..29e767b --- /dev/null +++ b/brmbar3/schema/0019-shop-consolidate.sql @@ -0,0 +1,97 @@ +-- +-- 0019-shop-buy-for-cash.sql +-- +-- #19 - stored function for "consolidation" transaction +-- +-- ISC License +-- +-- Copyright 2023-2025 Brmlab, z.s. +-- TMA +-- +-- Permission to use, copy, modify, and/or distribute this software +-- for any purpose with or without fee is hereby granted, provided +-- that the above copyright notice and this permission notice appear +-- in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +-- + +-- To require fully-qualified names +SELECT pg_catalog.set_config('search_path', '', false); + +DO $upgrade_block$ +BEGIN + +IF brmbar_privileged.has_exact_schema_version(18) THEN + +CREATE OR REPLACE FUNCTION public.compute_account_balance( + i_account_id public.accounts.id%TYPE +) RETURNS NUMERIC +LANGUAGE plpgsql +AS $$ +DECLARE + v_crsum NUMERIC; + v_dbsum NUMERIC; +BEGIN + SELECT COALESCE(SUM(CASE WHEN side='credit' THEN amount ELSE 0 END),0) crsum INTO v_crsum, + COALESCE(SUM(CASE WHEN side='debit' THEN amount ELSE 0 END),0) dbsum into v_dbsum + FROM public.transaction_splits ts WHERE ts.account=4 + RETURN v_dbsum - v_crsum; +END; $$; + +CREATE OR REPLACE FUNCTION public.make_consolidate_transaction( + i_excess_id public.accounts.id%TYPE, + i_deficit_id public.accounts.id%TYPE, + i_profits_id public.accounts.id%TYPE +) RETURNS TEXT +LANGUAGE plpgsql +AS $$ +DECLARE + v_transaction_id public.transactions.id%TYPE; + v_excess_balance NUMERIC; + v_deficit_balance NUMERIC; + v_ret TEXT; +BEGIN + v_ret := NULL; + -- Create a new transaction + v_transaction_id := brmbar_privileged.create_transaction(NULL, + 'BrmBar inventory consolidation'); + v_excess_balance := public.compute_account_balance(i_excess_id); + v_deficit_balance := public.compute_account_balance(i_deficit_id); + IF v_excess_balance <> 0 THEN + v_ret := 'Excess balance ' || -v_excess_balance || ' debited to profit'; + INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) + VALUES (i_transaction_id, 'debit', i_excess_id, -v_excess_balance, + 'Excess balance added to profit.'); + INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) + VALUES (i_transaction_id, 'debit', i_profits_id, -v_excess_balance, + 'Excess balance added to profit.'); + END IF; + IF v_deficit_balance <> 0 THEN + v_ret := COALESCE(v_ret, ''); + v_ret := v_ret || 'Deficit balance ' || v_deficit_balance || ' credited to profit'; + INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) + VALUES (i_transaction_id, 'credit', i_deficit_id, v_deficit_balance, + 'Deficit balance removed from profit.'); + INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) + VALUES (i_transaction_id, 'credit', i_profits_id, v_deficit_balance, + 'Deficit balance removed from profit.'); + END IF; + RETURN v_ret; +END; +$$; + +PERFORM brmbar_privileged.upgrade_schema_version_to(19); +END IF; + +END; +$upgrade_block$; + +-- vim: set ft=plsql : From c8892f825cf34956c2b7593790136fce45d7078b Mon Sep 17 00:00:00 2001 From: TMA Date: Mon, 21 Apr 2025 19:56:15 +0200 Subject: [PATCH 069/139] cosmetic --- brmbar3/schema/0019-shop-consolidate.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brmbar3/schema/0019-shop-consolidate.sql b/brmbar3/schema/0019-shop-consolidate.sql index 29e767b..1290cf4 100644 --- a/brmbar3/schema/0019-shop-consolidate.sql +++ b/brmbar3/schema/0019-shop-consolidate.sql @@ -1,5 +1,5 @@ -- --- 0019-shop-buy-for-cash.sql +-- 0019-shop-consolidate.sql -- -- #19 - stored function for "consolidation" transaction -- From 69c405f7151a5cc048261745af316546e42367e5 Mon Sep 17 00:00:00 2001 From: TMA Date: Mon, 21 Apr 2025 19:56:39 +0200 Subject: [PATCH 070/139] #20: stored function for undo transaction --- brmbar3/brmbar/Shop.py | 2 +- brmbar3/schema/0020-shop-undo.sql | 64 +++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 brmbar3/schema/0020-shop-undo.sql diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index e0084f5..c8f4a36 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -277,6 +277,6 @@ class Shop: # memo = 'undo %d (%s)' % (splitid, memo) # amount = -amount # self.db.execute("INSERT INTO transaction_splits (transaction, side, account, amount, memo) VALUES (%s, %s, %s, %s, %s)", [transaction, side, account, amount, memo]) - transaction = self.db.execute_and_fetch("CALL undo_transaction(%s)",[oldtid])[0] + transaction = self.db.execute_and_fetch("SELECT public.undo_transaction(%s)",[oldtid])[0] self.db.commit() return transaction diff --git a/brmbar3/schema/0020-shop-undo.sql b/brmbar3/schema/0020-shop-undo.sql new file mode 100644 index 0000000..a279580 --- /dev/null +++ b/brmbar3/schema/0020-shop-undo.sql @@ -0,0 +1,64 @@ +-- +-- 0020-shop-undo.sql +-- +-- #20 - stored function for undo transaction +-- +-- ISC License +-- +-- Copyright 2023-2025 Brmlab, z.s. +-- TMA +-- +-- Permission to use, copy, modify, and/or distribute this software +-- for any purpose with or without fee is hereby granted, provided +-- that the above copyright notice and this permission notice appear +-- in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +-- + +-- To require fully-qualified names +SELECT pg_catalog.set_config('search_path', '', false); + +DO $upgrade_block$ +BEGIN + +IF brmbar_privileged.has_exact_schema_version(19) THEN + +CREATE OR REPLACE FUNCTION public.undo_transaction( + IN i_id public.transactions.id%TYPE) +RETURNS public.transactions.id%TYPE +VOLATILE NOT LEAKPROOF LANGUAGE plpgsql AS $fn$ +DECLARE + v_ntrn_id public.transactions.id%TYPE; + v_old_trn public.transactions%ROWTYPE; + v_old_split public.transaction_splits%ROWTYPE; +BEGIN + SELECT * INTO v_old_trn FROM public.transactions WHERE id = i_id; + INSERT INTO transactions ("description") VALUES ('undo '||o_id||' ('||v_old_trn.description||')') RETURNING id into v_ntrn_id; + FOR v_old_split IN + SELECT * FROM transaction_splits WHERE "transaction" = i_id + LOOP + INSERT INTO transaction_splits ("transaction", "side", "account", "amount", "memo") + VALUES (v_ntrn_id, v_old_split.side, v_old_split.account, -v_old_split.amount, + 'undo ' || v_old_split.id || ' (' || v_old_split.memo || ')' ); + END LOOP; + RETURN v_ntrn_id; +END; +$fn$; + + + +PERFORM brmbar_privileged.upgrade_schema_version_to(20); +END IF; + +END; +$upgrade_block$; + +-- vim: set ft=plsql : From c21b394b429c449f32995228623ead792b4e1953 Mon Sep 17 00:00:00 2001 From: TMA Date: Mon, 21 Apr 2025 21:14:18 +0200 Subject: [PATCH 071/139] #17: stored function for "fixing" inventory transaction --- brmbar3/brmbar/Shop.py | 53 ++++--- brmbar3/schema/0017-shop-fix-inventory.sql | 157 +++++++++++++++++++++ brmbar3/schema/0019-shop-consolidate.sql | 15 -- 3 files changed, 187 insertions(+), 38 deletions(-) create mode 100644 brmbar3/schema/0017-shop-fix-inventory.sql diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index c8f4a36..2d96fce 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -202,30 +202,37 @@ class Shop: return accts def fix_inventory(self, item, amount): - amount_in_reality = amount - amount_in_system = item.balance() - (buy, sell) = item.currency.rates(self.currency) + rv = self.db.execute_and_fetch( + "SELECT public.fix_inventory(%s, %s, %s, %s, %s, %s)", + [item.id, item.currency.id, self.excess.id, self.deficit.id, self.currency.id, amount] + )[0] - diff = abs(amount_in_reality - amount_in_system) - buy_total = buy * diff - if amount_in_reality > amount_in_system: - transaction = self._transaction(description = "BrmBar inventory fix of {}pcs {} in system to {}pcs in reality".format(amount_in_system, item.name,amount_in_reality)) - item.debit(transaction, diff, "Inventory fix excess") - self.excess.credit(transaction, buy_total, "Inventory fix excess " + item.name) - self.db.commit() - return True - elif amount_in_reality < amount_in_system: - transaction = self._transaction(description = "BrmBar inventory fix of {}pcs {} in system to {}pcs in reality".format(amount_in_system, item.name,amount_in_reality)) - item.credit(transaction, diff, "Inventory fix deficit") - self.deficit.debit(transaction, buy_total, "Inventory fix deficit " + item.name) - self.db.commit() - return True - else: - transaction = self._transaction(description = "BrmBar inventory fix of {}pcs {} in system to {}pcs in reality".format(amount_in_system, item.name,amount_in_reality)) - item.debit(transaction, 0, "Inventory fix - amount was correct") - item.credit(transaction, 0, "Inventory fix - amount was correct") - self.db.commit() - return False + self.db.commit() + return rv + #amount_in_reality = amount + #amount_in_system = item.balance() + #(buy, sell) = item.currency.rates(self.currency) + + #diff = abs(amount_in_reality - amount_in_system) + #buy_total = buy * diff + #if amount_in_reality > amount_in_system: + # transaction = self._transaction(description = "BrmBar inventory fix of {}pcs {} in system to {}pcs in reality".format(amount_in_system, item.name,amount_in_reality)) + # item.debit(transaction, diff, "Inventory fix excess") + # self.excess.credit(transaction, buy_total, "Inventory fix excess " + item.name) + # self.db.commit() + # return True + #elif amount_in_reality < amount_in_system: + # transaction = self._transaction(description = "BrmBar inventory fix of {}pcs {} in system to {}pcs in reality".format(amount_in_system, item.name,amount_in_reality)) + # item.credit(transaction, diff, "Inventory fix deficit") + # self.deficit.debit(transaction, buy_total, "Inventory fix deficit " + item.name) + # self.db.commit() + # return True + #else: + # transaction = self._transaction(description = "BrmBar inventory fix of {}pcs {} in system to {}pcs in reality".format(amount_in_system, item.name,amount_in_reality)) + # item.debit(transaction, 0, "Inventory fix - amount was correct") + # item.credit(transaction, 0, "Inventory fix - amount was correct") + # self.db.commit() + # return False def fix_cash(self, amount): amount_in_reality = amount diff --git a/brmbar3/schema/0017-shop-fix-inventory.sql b/brmbar3/schema/0017-shop-fix-inventory.sql new file mode 100644 index 0000000..3bacd8d --- /dev/null +++ b/brmbar3/schema/0017-shop-fix-inventory.sql @@ -0,0 +1,157 @@ +-- +-- 0017-shop-fix-inventory.sql +-- +-- #17 - stored function for "fixing" inventory transaction +-- +-- ISC License +-- +-- Copyright 2023-2025 Brmlab, z.s. +-- TMA +-- +-- Permission to use, copy, modify, and/or distribute this software +-- for any purpose with or without fee is hereby granted, provided +-- that the above copyright notice and this permission notice appear +-- in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +-- + +-- To require fully-qualified names +SELECT pg_catalog.set_config('search_path', '', false); + +DO $upgrade_block$ +BEGIN + +IF brmbar_privileged.has_exact_schema_version(16) THEN + +CREATE OR REPLACE FUNCTION public.compute_account_balance( + i_account_id public.accounts.id%TYPE +) RETURNS NUMERIC +LANGUAGE plpgsql +AS $$ +DECLARE + v_crsum NUMERIC; + v_dbsum NUMERIC; +BEGIN + SELECT COALESCE(SUM(CASE WHEN side='credit' THEN amount ELSE 0 END),0) crsum INTO v_crsum, + COALESCE(SUM(CASE WHEN side='debit' THEN amount ELSE 0 END),0) dbsum into v_dbsum + FROM public.transaction_splits ts WHERE ts.account=4 + RETURN v_dbsum - v_crsum; +END; $$; + +CREATE OR REPLACE FUNCTION brmbar_privileged.fix_account_balance( + IN i_account_id public.acounts.id%TYPE, + IN i_account_currency_id public.currencies.id%TYPE, + IN i_excess_id public.acounts.id%TYPE, + IN i_deficit_id public.acounts.id%TYPE, + IN i_shop_currency_id public.currencies.id%TYPE, + IN i_amount_in_reality NUMERIC +) RETURNS BOOLEAN +VOLATILE NOT LEAKPROOF LANGUAGE plpgsql AS $fn$ +DECLARE + v_amount_in_system NUMERIC; + v_buy_rate NUMERIC; + v_currency_id public.currencies.id%TYPE; + v_diff NUMERIC; + v_buy_total NUMERIC; + v_ntrn_id public.transactions.id%TYPE; + v_transaction_memo TEXT; + v_item_name TEXT; + v_excess_memo TEXT; + v_deficit_memo TEXT; + + v_old_trn public.transactions%ROWTYPE; + v_old_split public.transaction_splits%ROWTYPE; +BEGIN + v_amount_in_system := public.compute_account_balance(i_account_id); + IF i_account_currency_id <> i_shop_currency_id THEN + v_buy_rate := public.find_buy_rate(i_item_id, i_shop_currency_id); + ELSE + v_buy_rate := 1; + END IF; + + v_diff := ABS(i_amount_in_reality - v_amount_in_system); + v_buy_total := v_buy_rate * v_diff; + -- compute memo strings + IF i_item_id = 1 THEN -- cash account recognized by magic id + -- fixing cash + v_transaction_memo := + 'BrmBar cash inventory fix of ' || v_amount_in_system + || ' in system to ' || i_amount_in_reality || ' in reality'; + v_excess_memo := 'Inventory cash fix excess.'; + v_deficit_memo := 'Inventory fix deficit.'; + ELSE + -- fixing other account + SELECT "name" INTO v_item_name FROM public.accounts WHERE id = i_account_id; + v_transaction_memo := + 'BrmBar inventory fix of ' || v_amount_in_system || 'pcs ' + || v_item_name + || ' in system to ' || i_amount_in_reality || 'pcs in reality'; + v_excess_memo := 'Inventory fix excess ' || v_item_name; + v_deficit_memo := 'Inventory fix deficit ' || v_item_name; + END IF; + -- create transaction based on the relation between counting and accounting + IF i_amount_in_reality > v_amount_in_system THEN + v_ntrn_id := brmbar_privileged.create_transaction(NULL, v_transaction_memo); + INSERT INTO transaction_splits ("transaction", "side", "account", "amount", "memo") + VALUES (v_ntrn_id, 'debit', i_item_id, v_diff, 'Inventory fix excess'); + INSERT INTO transaction_splits ("transaction", "side", "account", "amount", "memo") + VALUES (v_ntrn_id, 'credit', i_excess_id, v_buy_total, v_excess_memo); + RETURN TRUE; + ELSIF i_amount_in_reality < v_amount_in_system THEN + v_ntrn_id := brmbar_privileged.create_transaction(NULL, v_transaction_memo); + INSERT INTO transaction_splits ("transaction", "side", "account", "amount", "memo") + VALUES (v_ntrn_id, 'credit', i_item_id, v_diff, 'Inventory fix deficit'); + INSERT INTO transaction_splits ("transaction", "side", "account", "amount", "memo") + VALUES (v_ntrn_id, 'debit', i_deficit_id, v_buy_total, v_deficit_memo); + RETURN TRUE; + ELSIF i_account_id <> 1 THEN -- cash account recognized by magic id + -- record that everything is going on swimmingly only for noncash accounts (WTF) + v_ntrn_id := brmbar_privileged.create_transaction(NULL, v_transaction_memo); + INSERT INTO transaction_splits ("transaction", "side", "account", "amount", "memo") + VALUES (v_ntrn_id, 'debit', i_item_id, 0, 'Inventory fix - amount was correct'); + INSERT INTO transaction_splits ("transaction", "side", "account", "amount", "memo") + VALUES (v_ntrn_id, 'credit', i_item_id, 0, 'Inventory fix - amount was correct'); + RETURN FALSE; + END IF; + RETURN FALSE; +END; +$fn$; + + +CREATE OR REPLACE FUNCTION public.fix_inventory( + IN i_account_id public.acounts.id%TYPE, + IN i_account_currency_id public.currencies.id%TYPE, + IN i_excess_id public.acounts.id%TYPE, + IN i_deficit_id public.acounts.id%TYPE, + IN i_shop_currency_id public.currencies.id%TYPE, + IN i_amount_in_reality NUMERIC +) RETURNS BOOLEAN +VOLATILE NOT LEAKPROOF LANGUAGE plpgsql AS $fn$ +BEGIN + RETURN brmbar_privileged.fix_account_balance( + i_account_id, + i_account_currency_id, + i_excess_id, + i_deficit_id, + i_shop_currency_id, + i_amount_in_reality + ); +END; +$fn$; + + +PERFORM brmbar_privileged.upgrade_schema_version_to(17); +END IF; + +END; +$upgrade_block$; + +-- vim: set ft=plsql : diff --git a/brmbar3/schema/0019-shop-consolidate.sql b/brmbar3/schema/0019-shop-consolidate.sql index 1290cf4..db06685 100644 --- a/brmbar3/schema/0019-shop-consolidate.sql +++ b/brmbar3/schema/0019-shop-consolidate.sql @@ -31,21 +31,6 @@ BEGIN IF brmbar_privileged.has_exact_schema_version(18) THEN -CREATE OR REPLACE FUNCTION public.compute_account_balance( - i_account_id public.accounts.id%TYPE -) RETURNS NUMERIC -LANGUAGE plpgsql -AS $$ -DECLARE - v_crsum NUMERIC; - v_dbsum NUMERIC; -BEGIN - SELECT COALESCE(SUM(CASE WHEN side='credit' THEN amount ELSE 0 END),0) crsum INTO v_crsum, - COALESCE(SUM(CASE WHEN side='debit' THEN amount ELSE 0 END),0) dbsum into v_dbsum - FROM public.transaction_splits ts WHERE ts.account=4 - RETURN v_dbsum - v_crsum; -END; $$; - CREATE OR REPLACE FUNCTION public.make_consolidate_transaction( i_excess_id public.accounts.id%TYPE, i_deficit_id public.accounts.id%TYPE, From f0058aad682da46f051486f0eeb4618f9e9b7b00 Mon Sep 17 00:00:00 2001 From: TMA Date: Mon, 21 Apr 2025 21:14:34 +0200 Subject: [PATCH 072/139] #18: stored function for "fixing cash" transaction --- brmbar3/brmbar/Shop.py | 41 ++++++++++-------- brmbar3/schema/0018-shop-fix-cash.sql | 60 +++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 17 deletions(-) create mode 100644 brmbar3/schema/0018-shop-fix-cash.sql diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index 2d96fce..e7dbc6c 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -235,24 +235,31 @@ class Shop: # return False def fix_cash(self, amount): - amount_in_reality = amount - amount_in_system = self.cash.balance() + rv = self.db.execute_and_fetch( + "SELECT public.fix_cash(%s, %s, %s, %s)", + [self.excess.id, self.deficit.id, self.currency.id, amount] + )[0] - diff = abs(amount_in_reality - amount_in_system) - if amount_in_reality > amount_in_system: - transaction = self._transaction(description = "BrmBar cash inventory fix of {} in system to {} in reality".format(amount_in_system, amount_in_reality)) - self.cash.debit(transaction, diff, "Inventory fix excess") - self.excess.credit(transaction, diff, "Inventory cash fix excess.") - self.db.commit() - return True - elif amount_in_reality < amount_in_system: - transaction = self._transaction(description = "BrmBar cash inventory fix of {} in system to {} in reality".format(amount_in_system, amount_in_reality)) - self.cash.credit(transaction, diff, "Inventory fix deficit") - self.deficit.debit(transaction, diff, "Inventory fix deficit.") - self.db.commit() - return True - else: - return False + self.db.commit() + return rv + #amount_in_reality = amount + #amount_in_system = self.cash.balance() + + #diff = abs(amount_in_reality - amount_in_system) + #if amount_in_reality > amount_in_system: + # transaction = self._transaction(description = "BrmBar cash inventory fix of {} in system to {} in reality".format(amount_in_system, amount_in_reality)) + # self.cash.debit(transaction, diff, "Inventory fix excess") + # self.excess.credit(transaction, diff, "Inventory cash fix excess.") + # self.db.commit() + # return True + #elif amount_in_reality < amount_in_system: + # transaction = self._transaction(description = "BrmBar cash inventory fix of {} in system to {} in reality".format(amount_in_system, amount_in_reality)) + # self.cash.credit(transaction, diff, "Inventory fix deficit") + # self.deficit.debit(transaction, diff, "Inventory fix deficit.") + # self.db.commit() + # return True + #else: + # return False def consolidate(self): msg = self.db.execute_and_fetch( diff --git a/brmbar3/schema/0018-shop-fix-cash.sql b/brmbar3/schema/0018-shop-fix-cash.sql new file mode 100644 index 0000000..2af5d72 --- /dev/null +++ b/brmbar3/schema/0018-shop-fix-cash.sql @@ -0,0 +1,60 @@ +-- +-- 0018-shop-fix-cash.sql +-- +-- #18 - stored function for "fixing cash" transaction +-- +-- ISC License +-- +-- Copyright 2023-2025 Brmlab, z.s. +-- TMA +-- +-- Permission to use, copy, modify, and/or distribute this software +-- for any purpose with or without fee is hereby granted, provided +-- that the above copyright notice and this permission notice appear +-- in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +-- + +-- To require fully-qualified names +SELECT pg_catalog.set_config('search_path', '', false); + +DO $upgrade_block$ +BEGIN + +IF brmbar_privileged.has_exact_schema_version(17) THEN + +CREATE OR REPLACE FUNCTION public.fix_cash( + IN i_excess_id public.acounts.id%TYPE, + IN i_deficit_id public.acounts.id%TYPE, + IN i_shop_currency_id public.currencies.id%TYPE, + IN i_amount_in_reality NUMERIC +) RETURNS BOOLEAN +VOLATILE NOT LEAKPROOF LANGUAGE plpgsql AS $fn$ +BEGIN + RETURN brmbar_privileged.fix_account_balance( + 1, + 1, + i_excess_id, + i_deficit_id, + i_shop_currency_id, + i_amount_in_reality + ); +END; +$fn$; + + +PERFORM brmbar_privileged.upgrade_schema_version_to(18); +END IF; + +END; +$upgrade_block$; + +-- vim: set ft=plsql : From 0bff27da29bd51e613b65b8b0a7c6992a1917105 Mon Sep 17 00:00:00 2001 From: TMA Date: Tue, 22 Apr 2025 00:00:38 +0200 Subject: [PATCH 073/139] test reimplementation of brmbar.Currency.rates method --- brmbar3/brmbar/Currency.py | 17 ++++++++ brmbar3/test--currency-rates.py | 73 +++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 brmbar3/test--currency-rates.py diff --git a/brmbar3/brmbar/Currency.py b/brmbar3/brmbar/Currency.py index 7c56984..29da4a3 100644 --- a/brmbar3/brmbar/Currency.py +++ b/brmbar3/brmbar/Currency.py @@ -57,6 +57,23 @@ class Currency: return (buy, sell) + def rates2(self, other): + # the original code for compare testing + res = self.db.execute_and_fetch("SELECT rate, rate_dir FROM exchange_rates WHERE target = %s AND source = %s AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1", [self.id, other.id]) + if res is None: + raise NameError("Currency.rate(): Unknown conversion " + other.name() + " to " + self.name()) + buy_rate, buy_rate_dir = res + buy = buy_rate if buy_rate_dir == "target_to_source" else 1/buy_rate + + res = self.db.execute_and_fetch("SELECT rate, rate_dir FROM exchange_rates WHERE target = %s AND source = %s AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1", [other.id, self.id]) + if res is None: + raise NameError("Currency.rate(): Unknown conversion " + self.name() + " to " + other.name()) + sell_rate, sell_rate_dir = res + sell = sell_rate if sell_rate_dir == "source_to_target" else 1/sell_rate + + return (buy, sell) + + def convert(self, amount, target): res = self.db.execute_and_fetch("SELECT rate, rate_dir FROM exchange_rates WHERE target = %s AND source = %s AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1", [target.id, self.id]) if res is None: diff --git a/brmbar3/test--currency-rates.py b/brmbar3/test--currency-rates.py new file mode 100644 index 0000000..9ef93bb --- /dev/null +++ b/brmbar3/test--currency-rates.py @@ -0,0 +1,73 @@ +#!/usr/bin/python3 + +import sys +import subprocess + +#from brmbar import Database +#from brmbar import Currency + +from contextlib import closing +import psycopg2 +from brmbar.Database import Database +from brmbar.Currency import Currency +import math + +#import brmbar + + + +def approx_equal(a, b, tol=1e-6): + """Check if two (buy, sell) rate tuples are approximately equal.""" + return ( + isinstance(a, tuple) and isinstance(b, tuple) and + math.isclose(a[0], b[0], abs_tol=tol) and + math.isclose(a[1], b[1], abs_tol=tol) + ) + +def compare_exceptions(e1, e2): + """Compare exception types and messages.""" + return type(e1) == type(e2) and str(e1) == str(e2) + +def main(): + db = Database("dbname=brmbar") + + # Get all currencies + with closing(db.db_conn.cursor()) as cur: + cur.execute("SELECT id, name FROM currencies") + currencies = cur.fetchall() + + # Build Currency objects + currency_objs = [Currency(db, id, name) for id, name in currencies] + + # Test all currency pairs + for c1 in currency_objs: + for c2 in currency_objs: + #if c1.id == c2.id: + # continue + + try: + rates1 = c1.rates(c2) + exc1 = None + except (RuntimeError, NameError) as e1: + rates1 = None + exc1 = e1 + + try: + rates2 = c1.rates2(c2) + exc2 = None + except (RuntimeError, NameError) as e2: + rates2 = None + exc2 = e2 + + if exc1 or exc2: + if not compare_exceptions(exc1, exc2): + print(f"[EXCEPTION DIFFERENCE] {c1.name} -> {c2.name}") + print(f" rates() exception: {type(exc1).__name__}: {exc1}") + print(f" rates2() exception: {type(exc2).__name__}: {exc2}") + elif not approx_equal(rates1, rates2): + print(f"[VALUE DIFFERENCE] {c1.name} -> {c2.name}") + print(f" rates(): {rates1}") + print(f" rates2(): {rates2}") + +if __name__ == "__main__": + main() From a8a124835c8fd8655a23974fd98797c0632e9c80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Thu, 10 Jul 2025 16:00:28 +0200 Subject: [PATCH 074/139] Schema 0009: fix find_buy_rate arguments syntax --- brmbar3/schema/0009-shop-sell.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/brmbar3/schema/0009-shop-sell.sql b/brmbar3/schema/0009-shop-sell.sql index 811a5f5..7f411dc 100644 --- a/brmbar3/schema/0009-shop-sell.sql +++ b/brmbar3/schema/0009-shop-sell.sql @@ -33,8 +33,8 @@ IF brmbar_privileged.has_exact_schema_version(8) THEN -- return negative number on rate not found CREATE OR REPLACE FUNCTION public.find_buy_rate( - IN i_item_id public.accounts.id%TYPE; - IN i_other_id public.accounts.id%TYPE; + IN i_item_id public.accounts.id%TYPE, + IN i_other_id public.accounts.id%TYPE ) RETURNS NUMERIC LANGUAGE plpgsql AS $$ From 8d2c9cb20f245476c73d6c5d3e229107013843f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Thu, 10 Jul 2025 16:05:19 +0200 Subject: [PATCH 075/139] Schema 0009: fix DDL statements ends, fix argument delimiters in second procedure as well --- brmbar3/schema/0009-shop-sell.sql | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/brmbar3/schema/0009-shop-sell.sql b/brmbar3/schema/0009-shop-sell.sql index 7f411dc..4be346c 100644 --- a/brmbar3/schema/0009-shop-sell.sql +++ b/brmbar3/schema/0009-shop-sell.sql @@ -52,13 +52,13 @@ EXCEPTION WHEN NO_DATA_FOUND THEN RETURN -1; END; -$$ +$$; -- return negative number on rate not found CREATE OR REPLACE FUNCTION public.find_sell_rate( - IN i_item_id public.accounts.id%TYPE; - IN i_other_id public.accounts.id%TYPE; + IN i_item_id public.accounts.id%TYPE, + IN i_other_id public.accounts.id%TYPE ) RETURNS NUMERIC LANGUAGE plpgsql AS $$ @@ -76,7 +76,7 @@ EXCEPTION WHEN NO_DATA_FOUND THEN RETURN -1; END; -$$ +$$; CREATE OR REPLACE FUNCTION public.create_transaction( i_responsible_id public.accounts.id%TYPE, From 903cc8f4d70f6c3fbf42bd1035913d3a156ccfa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Thu, 10 Jul 2025 16:15:19 +0200 Subject: [PATCH 076/139] Schema 0009: fix select into --- brmbar3/schema/0009-shop-sell.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/brmbar3/schema/0009-shop-sell.sql b/brmbar3/schema/0009-shop-sell.sql index 4be346c..da3af48 100644 --- a/brmbar3/schema/0009-shop-sell.sql +++ b/brmbar3/schema/0009-shop-sell.sql @@ -42,7 +42,7 @@ DECLARE v_rate public.exchange_rates.rate%TYPE; v_rate_dir public.exchange_rates.rate_dir%TYPE; BEGIN - SELECT rate INTO STRICT v_rate, rate_dir INTO STRICT v_rate_dir FROM public.exchange_rates WHERE target = i_item_id AND source = i_other_id AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1; + SELECT rate, rate_dir INTO STRICT v_rate, v_rate_dir FROM public.exchange_rates WHERE target = i_item_id AND source = i_other_id AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1; IF v_rate_dir = 'target_to_source'::public.exchange_rate_direction THEN RETURN v_rate; ELSE @@ -66,7 +66,7 @@ DECLARE v_rate public.exchange_rates.rate%TYPE; v_rate_dir public.exchange_rates.rate_dir%TYPE; BEGIN - SELECT rate INTO STRICT v_rate, rate_dir INTO STRICT v_rate_dir FROM public.exchange_rates WHERE target = i_other_id AND source = i_item_id AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1; + SELECT rate, rate_dir INTO STRICT v_rate, v_rate_dir FROM public.exchange_rates WHERE target = i_other_id AND source = i_item_id AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1; IF v_rate_dir = 'source_to_target'::public.exchange_rate_direction THEN RETURN v_rate; ELSE From ae16ebc51f4b4966ff9f2c1261741bab4956d4a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Thu, 10 Jul 2025 16:17:34 +0200 Subject: [PATCH 077/139] Schema 0009: fix type inference for id --- brmbar3/schema/0009-shop-sell.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brmbar3/schema/0009-shop-sell.sql b/brmbar3/schema/0009-shop-sell.sql index da3af48..6468bdb 100644 --- a/brmbar3/schema/0009-shop-sell.sql +++ b/brmbar3/schema/0009-shop-sell.sql @@ -83,7 +83,7 @@ CREATE OR REPLACE FUNCTION public.create_transaction( i_description public.transactions.description%TYPE ) RETURNS public.transactions.id%TYPE AS $$ DECLARE - new_transaction_id public.transactions%TYPE; + new_transaction_id public.transactions.id%TYPE; BEGIN -- Create a new transaction INSERT INTO public.transactions (responsible, description) From fba614de785167f7e081d66037a2fa5b8deff320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Thu, 10 Jul 2025 16:18:39 +0200 Subject: [PATCH 078/139] Schema 0010: fix type inference for id --- brmbar3/schema/0010-shop-sell-for-cash.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brmbar3/schema/0010-shop-sell-for-cash.sql b/brmbar3/schema/0010-shop-sell-for-cash.sql index 23ad131..de93d62 100644 --- a/brmbar3/schema/0010-shop-sell-for-cash.sql +++ b/brmbar3/schema/0010-shop-sell-for-cash.sql @@ -36,7 +36,7 @@ CREATE OR REPLACE FUNCTION brmbar_privileged.create_transaction( i_description public.transactions.description%TYPE ) RETURNS public.transactions.id%TYPE AS $$ DECLARE - new_transaction_id public.transactions%TYPE; + new_transaction_id public.transactions.id%TYPE; BEGIN -- Create a new transaction INSERT INTO public.transactions (responsible, description) From 3b0cb6472d341463abc15daf34947240327feb7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Thu, 10 Jul 2025 16:19:25 +0200 Subject: [PATCH 079/139] Schema 0011: fix type inference for id --- brmbar3/schema/0011-shop-undo-sale.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brmbar3/schema/0011-shop-undo-sale.sql b/brmbar3/schema/0011-shop-undo-sale.sql index 67d47cc..4427070 100644 --- a/brmbar3/schema/0011-shop-undo-sale.sql +++ b/brmbar3/schema/0011-shop-undo-sale.sql @@ -36,7 +36,7 @@ CREATE OR REPLACE FUNCTION brmbar_privileged.create_transaction( i_description public.transactions.description%TYPE ) RETURNS public.transactions.id%TYPE AS $$ DECLARE - new_transaction_id public.transactions%TYPE; + new_transaction_id public.transactions.id%TYPE; BEGIN -- Create a new transaction INSERT INTO public.transactions (responsible, description) From f7f137821b00f3efb468d96555b255b689f983ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Thu, 10 Jul 2025 16:21:22 +0200 Subject: [PATCH 080/139] Schema 0017: fix into syntax --- brmbar3/schema/0017-shop-fix-inventory.sql | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/brmbar3/schema/0017-shop-fix-inventory.sql b/brmbar3/schema/0017-shop-fix-inventory.sql index 3bacd8d..64c1843 100644 --- a/brmbar3/schema/0017-shop-fix-inventory.sql +++ b/brmbar3/schema/0017-shop-fix-inventory.sql @@ -40,8 +40,10 @@ DECLARE v_crsum NUMERIC; v_dbsum NUMERIC; BEGIN - SELECT COALESCE(SUM(CASE WHEN side='credit' THEN amount ELSE 0 END),0) crsum INTO v_crsum, - COALESCE(SUM(CASE WHEN side='debit' THEN amount ELSE 0 END),0) dbsum into v_dbsum + SELECT + COALESCE(SUM(CASE WHEN side='credit' THEN amount ELSE 0 END),0) crsum, + COALESCE(SUM(CASE WHEN side='debit' THEN amount ELSE 0 END),0) dbsum + INTO v_crsum, v_dbsum FROM public.transaction_splits ts WHERE ts.account=4 RETURN v_dbsum - v_crsum; END; $$; From 1eff329496501087e80bda028bb2cb603cecc627 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Thu, 10 Jul 2025 16:22:13 +0200 Subject: [PATCH 081/139] Schema 0017: fix semicolon before return --- brmbar3/schema/0017-shop-fix-inventory.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brmbar3/schema/0017-shop-fix-inventory.sql b/brmbar3/schema/0017-shop-fix-inventory.sql index 64c1843..fafad73 100644 --- a/brmbar3/schema/0017-shop-fix-inventory.sql +++ b/brmbar3/schema/0017-shop-fix-inventory.sql @@ -44,7 +44,7 @@ BEGIN COALESCE(SUM(CASE WHEN side='credit' THEN amount ELSE 0 END),0) crsum, COALESCE(SUM(CASE WHEN side='debit' THEN amount ELSE 0 END),0) dbsum INTO v_crsum, v_dbsum - FROM public.transaction_splits ts WHERE ts.account=4 + FROM public.transaction_splits ts WHERE ts.account=4; RETURN v_dbsum - v_crsum; END; $$; From 5e348b2463ca273a742f74aa9f6bee33c17405dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Thu, 10 Jul 2025 16:23:25 +0200 Subject: [PATCH 082/139] Schema 0017: fix ac-c-ounts --- brmbar3/schema/0017-shop-fix-inventory.sql | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/brmbar3/schema/0017-shop-fix-inventory.sql b/brmbar3/schema/0017-shop-fix-inventory.sql index fafad73..7c04aa4 100644 --- a/brmbar3/schema/0017-shop-fix-inventory.sql +++ b/brmbar3/schema/0017-shop-fix-inventory.sql @@ -49,10 +49,10 @@ BEGIN END; $$; CREATE OR REPLACE FUNCTION brmbar_privileged.fix_account_balance( - IN i_account_id public.acounts.id%TYPE, + IN i_account_id public.accounts.id%TYPE, IN i_account_currency_id public.currencies.id%TYPE, - IN i_excess_id public.acounts.id%TYPE, - IN i_deficit_id public.acounts.id%TYPE, + IN i_excess_id public.accounts.id%TYPE, + IN i_deficit_id public.accounts.id%TYPE, IN i_shop_currency_id public.currencies.id%TYPE, IN i_amount_in_reality NUMERIC ) RETURNS BOOLEAN @@ -129,10 +129,10 @@ $fn$; CREATE OR REPLACE FUNCTION public.fix_inventory( - IN i_account_id public.acounts.id%TYPE, + IN i_account_id public.accounts.id%TYPE, IN i_account_currency_id public.currencies.id%TYPE, - IN i_excess_id public.acounts.id%TYPE, - IN i_deficit_id public.acounts.id%TYPE, + IN i_excess_id public.accounts.id%TYPE, + IN i_deficit_id public.accounts.id%TYPE, IN i_shop_currency_id public.currencies.id%TYPE, IN i_amount_in_reality NUMERIC ) RETURNS BOOLEAN From 597bca87b5de7f019a7251600753a89d1ac46a0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Thu, 10 Jul 2025 16:24:31 +0200 Subject: [PATCH 083/139] Schema 0018: fix ac-c-ounts --- brmbar3/schema/0018-shop-fix-cash.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/brmbar3/schema/0018-shop-fix-cash.sql b/brmbar3/schema/0018-shop-fix-cash.sql index 2af5d72..5ced17d 100644 --- a/brmbar3/schema/0018-shop-fix-cash.sql +++ b/brmbar3/schema/0018-shop-fix-cash.sql @@ -32,8 +32,8 @@ BEGIN IF brmbar_privileged.has_exact_schema_version(17) THEN CREATE OR REPLACE FUNCTION public.fix_cash( - IN i_excess_id public.acounts.id%TYPE, - IN i_deficit_id public.acounts.id%TYPE, + IN i_excess_id public.accounts.id%TYPE, + IN i_deficit_id public.accounts.id%TYPE, IN i_shop_currency_id public.currencies.id%TYPE, IN i_amount_in_reality NUMERIC ) RETURNS BOOLEAN From cd9b4484c86a0be062a68fce8e92ebde13bff5cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Thu, 10 Jul 2025 16:54:45 +0200 Subject: [PATCH 084/139] Shop.py: python <3.6 compatibility --- brmbar3/brmbar/Shop.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index e7dbc6c..dd762c1 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -28,7 +28,8 @@ class Shop: # Call the stored procedure for the sale cost = self.db.execute_and_fetch( "SELECT public.sell_item(%s, %s, %s, %s, %s)", - [item.id, amount, user.id, self.currency.id, f"BrmBar sale of {amount}x {item.name} to {user.name}"] + [item.id, amount, user.id, self.currency.id, + "BrmBar sale of {0}x {1} to {2}".format(amount, item.name, user.name)] )[0]#[0] self.db.commit() @@ -48,7 +49,8 @@ class Shop: def sell_for_cash(self, item, amount = 1): cost = self.db.execute_and_fetch( "SELECT public.sell_item_for_cash(%s, %s, %s, %s, %s)", - [item.id, amount, user.id, self.currency.id, f"BrmBar sale of {amount}x {item.name} for cash"] + [item.id, amount, user.id, self.currency.id, + "BrmBar sale of {0}x {1} for cash".format(amount, item.name)] )[0]#[0] self.db.commit() @@ -79,7 +81,7 @@ class Shop: # Call the stored procedure for undoing a sale cost = self.db.execute_and_fetch( "SELECT public.undo_sale_of_item(%s, %s, %s, %s)", - [item.id, amount, user.id, user.currency.id, f"BrmBar sale UNDO of {amount}x {item.name} to {user.name}"] + [item.id, amount, user.id, user.currency.id, "BrmBar sale UNDO of {0}x {1} to {2}".format(amount, item.name, user.name)] )[0]#[0] self.db.commit() From 3a4aaa74cecd847a27ecbb4302ab63ffc6c3204b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Sat, 12 Jul 2025 15:15:21 +0200 Subject: [PATCH 085/139] 0017: use proper account id argument - not a constant --- brmbar3/schema/0017-shop-fix-inventory.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brmbar3/schema/0017-shop-fix-inventory.sql b/brmbar3/schema/0017-shop-fix-inventory.sql index 7c04aa4..a011892 100644 --- a/brmbar3/schema/0017-shop-fix-inventory.sql +++ b/brmbar3/schema/0017-shop-fix-inventory.sql @@ -44,7 +44,7 @@ BEGIN COALESCE(SUM(CASE WHEN side='credit' THEN amount ELSE 0 END),0) crsum, COALESCE(SUM(CASE WHEN side='debit' THEN amount ELSE 0 END),0) dbsum INTO v_crsum, v_dbsum - FROM public.transaction_splits ts WHERE ts.account=4; + FROM public.transaction_splits ts WHERE ts.account=i_account_id; RETURN v_dbsum - v_crsum; END; $$; From 5355eca6f5c7c063d0dfc2bb07934726c3ac5c6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Sat, 12 Jul 2025 15:55:06 +0200 Subject: [PATCH 086/139] gui: pass database configuration as command-line options --- brmbar3/brmbar-gui-qt4.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/brmbar3/brmbar-gui-qt4.py b/brmbar3/brmbar-gui-qt4.py index 9ca11d0..fca7f03 100755 --- a/brmbar3/brmbar-gui-qt4.py +++ b/brmbar3/brmbar-gui-qt4.py @@ -9,6 +9,8 @@ from brmbar import Database import brmbar +import argparse + # User credit balance limit; sale will fail when balance is below this limit. LIMIT_BALANCE = -200 # When below this credit balance, an alert hook script (see below) is run. @@ -224,6 +226,23 @@ class ShopAdapter(QtCore.QObject): db.commit() return balance +parser = argparse.ArgumentParser() +parser.add_argument("--dbname", help="Database name", type=str) +parser.add_argument("--dbuser", help="Database user", type=str) +parser.add_argument("--dbhost", help="Database host", type=str) +parser.add_argument("--dbpass", help="Database user password", type=str) +args = parser.parse_args() +argdbname = args.dbname +argdbuser = args.dbuser +argdbhost = args.dbhost +argdbpass = args.dbpass + +db = Database.Database( + "dbname={0} user={1} host={2} password={3}".format( + argdbname,argdbuser,argdbhost,argdbpass + ) +) + db = Database.Database("dbname=brmbar") shop = brmbar.Shop.new_with_defaults(db) currency = shop.currency From 15dfd0be0445aaf5b2f0f56b35b28659b33d7de5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Sat, 12 Jul 2025 15:57:41 +0200 Subject: [PATCH 087/139] gui: remove old db initialization --- brmbar3/brmbar-gui-qt4.py | 1 - 1 file changed, 1 deletion(-) diff --git a/brmbar3/brmbar-gui-qt4.py b/brmbar3/brmbar-gui-qt4.py index fca7f03..f6027dc 100755 --- a/brmbar3/brmbar-gui-qt4.py +++ b/brmbar3/brmbar-gui-qt4.py @@ -243,7 +243,6 @@ db = Database.Database( ) ) -db = Database.Database("dbname=brmbar") shop = brmbar.Shop.new_with_defaults(db) currency = shop.currency db.commit() From 3ab537f7d9cfed7a3f1c9628aed7dc6aa70349c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Sat, 12 Jul 2025 17:15:22 +0200 Subject: [PATCH 088/139] Try logging. --- brmbar3/brmbar-gui-qt4.py | 11 +++++++++++ brmbar3/brmbar/Account.py | 3 +++ 2 files changed, 14 insertions(+) diff --git a/brmbar3/brmbar-gui-qt4.py b/brmbar3/brmbar-gui-qt4.py index f6027dc..75e77c4 100755 --- a/brmbar3/brmbar-gui-qt4.py +++ b/brmbar3/brmbar-gui-qt4.py @@ -11,6 +11,17 @@ import brmbar import argparse +import logging + +root = logging.getLogger() +root.setLevel(logging.DEBUG) + +handler = logging.StreamHandler(sys.stdout) +handler.setLevel(logging.DEBUG) +formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') +handler.setFormatter(formatter) +root.addHandler(handler) + # User credit balance limit; sale will fail when balance is below this limit. LIMIT_BALANCE = -200 # When below this credit balance, an alert hook script (see below) is run. diff --git a/brmbar3/brmbar/Account.py b/brmbar3/brmbar/Account.py index 215df11..5344301 100644 --- a/brmbar3/brmbar/Account.py +++ b/brmbar3/brmbar/Account.py @@ -1,4 +1,6 @@ from .Currency import Currency +import logging +logger = logging.getLogger(__name__) class Account: """ BrmBar Account @@ -14,6 +16,7 @@ class Account: @classmethod def load_by_barcode(cls, db, barcode): + logger.debug("load_by_barcode: %s", barcode) res = db.execute_and_fetch("SELECT account FROM barcodes WHERE barcode = %s", [barcode]) if res is None: return None From 93f9b336b6a446ce4c53b35eff46aa28b77d657c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Sat, 12 Jul 2025 17:25:27 +0200 Subject: [PATCH 089/139] Shop.py: logging to sell --- brmbar3/brmbar/Shop.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index dd762c1..2c878b2 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -1,6 +1,8 @@ import brmbar from .Currency import Currency from .Account import Account +import logging +logger = logging.getLogger(__name__) class Shop: """ BrmBar Shop @@ -26,6 +28,8 @@ class Shop: def sell(self, item, user, amount = 1): # Call the stored procedure for the sale + logger.debug("sell: item.id=%s amount=%s user.id=%s self.currency.id=%s", + item.id, amount, user.id, self.currency.id) cost = self.db.execute_and_fetch( "SELECT public.sell_item(%s, %s, %s, %s, %s)", [item.id, amount, user.id, self.currency.id, From f9230ed5bfbe2130632034ec7dc6e7b8f4b42e94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Sat, 12 Jul 2025 17:29:08 +0200 Subject: [PATCH 090/139] gui: try logging acct in acct_map --- brmbar3/brmbar-gui-qt4.py | 1 + brmbar3/brmbar/Account.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/brmbar3/brmbar-gui-qt4.py b/brmbar3/brmbar-gui-qt4.py index 75e77c4..dea0807 100755 --- a/brmbar3/brmbar-gui-qt4.py +++ b/brmbar3/brmbar-gui-qt4.py @@ -65,6 +65,7 @@ class ShopAdapter(QtCore.QObject): return map def acct_map(self, acct): + logger.debug("acct_map: acct=%s", acct) if acct is None: return None if acct.acctype == 'debt': diff --git a/brmbar3/brmbar/Account.py b/brmbar3/brmbar/Account.py index 5344301..ef7a494 100644 --- a/brmbar3/brmbar/Account.py +++ b/brmbar3/brmbar/Account.py @@ -16,7 +16,7 @@ class Account: @classmethod def load_by_barcode(cls, db, barcode): - logger.debug("load_by_barcode: %s", barcode) + logger.debug("load_by_barcode: '%s'", barcode) res = db.execute_and_fetch("SELECT account FROM barcodes WHERE barcode = %s", [barcode]) if res is None: return None From c3abceac5d57e5bc4fba3e0e66bcb480dac427f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Sat, 12 Jul 2025 17:33:28 +0200 Subject: [PATCH 091/139] gui: try logging acct in acct_map better --- brmbar3/brmbar-gui-qt4.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/brmbar3/brmbar-gui-qt4.py b/brmbar3/brmbar-gui-qt4.py index dea0807..8f06882 100755 --- a/brmbar3/brmbar-gui-qt4.py +++ b/brmbar3/brmbar-gui-qt4.py @@ -65,9 +65,10 @@ class ShopAdapter(QtCore.QObject): return map def acct_map(self, acct): - logger.debug("acct_map: acct=%s", acct) if acct is None: + logger.debug("acct_map: acct is None") return None + logger.debug("acct_map: acct.acctype=%s", acct.acctype) if acct.acctype == 'debt': return self.acct_debt_map(acct) elif acct.acctype == "inventory": From 829bab66b400c8f9b47727bf87b78d54f1257131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Sat, 12 Jul 2025 17:35:38 +0200 Subject: [PATCH 092/139] gui: fine-grained logging --- brmbar3/brmbar-gui-qt4.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/brmbar3/brmbar-gui-qt4.py b/brmbar3/brmbar-gui-qt4.py index 8f06882..4bb9e48 100755 --- a/brmbar3/brmbar-gui-qt4.py +++ b/brmbar3/brmbar-gui-qt4.py @@ -87,12 +87,14 @@ class ShopAdapter(QtCore.QObject): Therefore, we construct a map that we can pass around easily. We return None on unrecognized barcode. """ barcode = str(barcode) + logger.debug("barcodeInput: barcode='%s'", barcode) if barcode and barcode[0] == "$": credits = {'$02': 20, '$05': 50, '$10': 100, '$20': 200, '$50': 500, '$1k': 1000} credit = credits[barcode] if credit is None: return None return { "acctype": "recharge", "amount": str(credit)+".00" } + logger.debug("barcodeInput: before load_by_barcode") acct = self.acct_map(brmbar.Account.load_by_barcode(db, barcode)) db.commit() return acct From 0a8e2b727b918302e8fc242c02b488f7fb55b6fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Sat, 12 Jul 2025 17:37:17 +0200 Subject: [PATCH 093/139] gui: logger --- brmbar3/brmbar-gui-qt4.py | 1 + 1 file changed, 1 insertion(+) diff --git a/brmbar3/brmbar-gui-qt4.py b/brmbar3/brmbar-gui-qt4.py index 4bb9e48..62e4c4a 100755 --- a/brmbar3/brmbar-gui-qt4.py +++ b/brmbar3/brmbar-gui-qt4.py @@ -21,6 +21,7 @@ handler.setLevel(logging.DEBUG) formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') handler.setFormatter(formatter) root.addHandler(handler) +logger = logging.getLogger(__name__) # User credit balance limit; sale will fail when balance is below this limit. LIMIT_BALANCE = -200 From 343df879fb5e95e0c2465c0435d5b1aaedcfafb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Sat, 12 Jul 2025 17:40:16 +0200 Subject: [PATCH 094/139] Shop: sell log res --- brmbar3/brmbar/Shop.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index 2c878b2..c6f7c1c 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -30,12 +30,13 @@ class Shop: # Call the stored procedure for the sale logger.debug("sell: item.id=%s amount=%s user.id=%s self.currency.id=%s", item.id, amount, user.id, self.currency.id) - cost = self.db.execute_and_fetch( + res = self.db.execute_and_fetch( "SELECT public.sell_item(%s, %s, %s, %s, %s)", [item.id, amount, user.id, self.currency.id, "BrmBar sale of {0}x {1} to {2}".format(amount, item.name, user.name)] - )[0]#[0] - + )#[0] + logger.debug("sell: res=%s", res) + cost = res[0] self.db.commit() return cost # Sale: Currency conversion from item currency to shop currency From 557d36cac313e8700e257f740d5e99104cb23f64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Sat, 12 Jul 2025 17:42:32 +0200 Subject: [PATCH 095/139] Shop: not array --- brmbar3/brmbar/Shop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index c6f7c1c..7a64274 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -35,7 +35,7 @@ class Shop: [item.id, amount, user.id, self.currency.id, "BrmBar sale of {0}x {1} to {2}".format(amount, item.name, user.name)] )#[0] - logger.debug("sell: res=%s", res) + logger.debug("sell: res[0]=%s", res[0]) cost = res[0] self.db.commit() return cost From 38838692c63f50bf68ecba7f89caa60385100f1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Sat, 12 Jul 2025 17:45:06 +0200 Subject: [PATCH 096/139] Database: other exceptions --- brmbar3/brmbar/Database.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/brmbar3/brmbar/Database.py b/brmbar3/brmbar/Database.py index d0202d4..6b985eb 100644 --- a/brmbar3/brmbar/Database.py +++ b/brmbar3/brmbar/Database.py @@ -3,6 +3,8 @@ import psycopg2 from contextlib import closing import time +import logging +logger = logging.getLogger(__name__) class Database: """self-reconnecting database object""" @@ -55,6 +57,8 @@ class Database: time.sleep(1) cur = self.db_conn.cursor() #how ugly is this? return self._execute(cur, query, attrs, level+1) + except Exception as ex: + logger.debug("_execute exception: %s", ex) def commit(self): """passes commit to db""" From ee2b9452998e592400189986ed2d22ec761bac1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Sat, 12 Jul 2025 17:53:33 +0200 Subject: [PATCH 097/139] 0010: fix other to user --- brmbar3/schema/0010-shop-sell-for-cash.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/brmbar3/schema/0010-shop-sell-for-cash.sql b/brmbar3/schema/0010-shop-sell-for-cash.sql index de93d62..4881b27 100644 --- a/brmbar3/schema/0010-shop-sell-for-cash.sql +++ b/brmbar3/schema/0010-shop-sell-for-cash.sql @@ -106,7 +106,7 @@ AS $$ BEGIN RETURN brmbar_privileged.sell_item_internal(i_item_id, i_amount, - i_other_id, + i_user_id, i_target_currency_id, (SELECT "name" FROM public.accounts WHERE id = i_user_id), i_description); @@ -125,7 +125,7 @@ AS $$ BEGIN RETURN brmbar_privileged.sell_item_internal(i_item_id, i_amount, - i_other_id, + i_user_id, i_target_currency_id, 'Cash', i_description); From 81366fd2bbeb4585d1da269e8b413a1a572c2477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Sat, 12 Jul 2025 18:04:56 +0200 Subject: [PATCH 098/139] 0010: fix v_transaction_id --- brmbar3/schema/0010-shop-sell-for-cash.sql | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/brmbar3/schema/0010-shop-sell-for-cash.sql b/brmbar3/schema/0010-shop-sell-for-cash.sql index 4881b27..c29b0f8 100644 --- a/brmbar3/schema/0010-shop-sell-for-cash.sql +++ b/brmbar3/schema/0010-shop-sell-for-cash.sql @@ -77,17 +77,17 @@ BEGIN -- the item (decrease stock) INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) - VALUES (i_transaction_id, 'credit', i_item_id, i_amount, + VALUES (v_transaction_id, 'credit', i_item_id, i_amount, i_other_memo); -- the user INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) - VALUES (i_transaction_id, 'debit', i_user_id, v_cost, + VALUES (v_transaction_id, 'debit', i_user_id, v_cost, (SELECT "name" FROM public.accounts WHERE id = i_item_id)); -- the profit INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) - VALUES (i_transaction_id, 'debit', (SELECT account_id FROM accounts WHERE name = 'BrmBar Profits'), v_profit, (SELECT 'Margin on ' || "name" FROM public.accounts WHERE id = i_item_id)); + VALUES (v_transaction_id, 'debit', (SELECT account_id FROM accounts WHERE name = 'BrmBar Profits'), v_profit, (SELECT 'Margin on ' || "name" FROM public.accounts WHERE id = i_item_id)); -- Return the cost RETURN v_cost; From 8ee51a23d9165620f1b5b0400bab4e4e6773d021 Mon Sep 17 00:00:00 2001 From: TMA Date: Sat, 12 Jul 2025 18:25:24 +0200 Subject: [PATCH 099/139] account_id -> id --- brmbar3/schema/0010-shop-sell-for-cash.sql | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/brmbar3/schema/0010-shop-sell-for-cash.sql b/brmbar3/schema/0010-shop-sell-for-cash.sql index c29b0f8..12e0b9d 100644 --- a/brmbar3/schema/0010-shop-sell-for-cash.sql +++ b/brmbar3/schema/0010-shop-sell-for-cash.sql @@ -87,7 +87,11 @@ BEGIN -- the profit INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) - VALUES (v_transaction_id, 'debit', (SELECT account_id FROM accounts WHERE name = 'BrmBar Profits'), v_profit, (SELECT 'Margin on ' || "name" FROM public.accounts WHERE id = i_item_id)); + VALUES (v_transaction_id, + 'debit', + (SELECT id FROM public.accounts WHERE name = 'BrmBar Profits'), + v_profit, + (SELECT 'Margin on ' || "name" FROM public.accounts WHERE id = i_item_id)); -- Return the cost RETURN v_cost; From 9d8827ccbdbb9a7ef4cfe3e761bc706bd97faba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Sat, 12 Jul 2025 18:52:21 +0200 Subject: [PATCH 100/139] 0010: lookup item currency id --- brmbar3/schema/0010-shop-sell-for-cash.sql | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/brmbar3/schema/0010-shop-sell-for-cash.sql b/brmbar3/schema/0010-shop-sell-for-cash.sql index 12e0b9d..86fcf9c 100644 --- a/brmbar3/schema/0010-shop-sell-for-cash.sql +++ b/brmbar3/schema/0010-shop-sell-for-cash.sql @@ -63,10 +63,17 @@ DECLARE v_cost NUMERIC; v_profit NUMERIC; v_transaction_id public.transactions.id%TYPE; + v_item_currency_id public.accounts.currency%TYPE; BEGIN + -- Get item's currency + SELECT currency + INTO v_item_currency_id + FROM public.accounts + WHERE id=i_item_id; + -- Get the buy and sell rates from the stored functions - v_buy_rate := public.find_buy_rate(i_item_id, i_target_currency_id); - v_sell_rate := public.find_sell_rate(i_item_id, i_target_currency_id); + v_buy_rate := public.find_buy_rate(v_item_currency_id, i_target_currency_id); + v_sell_rate := public.find_sell_rate(v_item_currency_id, i_target_currency_id); -- Calculate cost and profit v_cost := i_amount * v_sell_rate; From afb5476c2d4f3a6bff1c26c5295a2ba819310219 Mon Sep 17 00:00:00 2001 From: TMA Date: Sat, 12 Jul 2025 19:11:25 +0200 Subject: [PATCH 101/139] o_id -> i_id --- brmbar3/schema/0020-shop-undo.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brmbar3/schema/0020-shop-undo.sql b/brmbar3/schema/0020-shop-undo.sql index a279580..d2d12b3 100644 --- a/brmbar3/schema/0020-shop-undo.sql +++ b/brmbar3/schema/0020-shop-undo.sql @@ -41,7 +41,7 @@ DECLARE v_old_split public.transaction_splits%ROWTYPE; BEGIN SELECT * INTO v_old_trn FROM public.transactions WHERE id = i_id; - INSERT INTO transactions ("description") VALUES ('undo '||o_id||' ('||v_old_trn.description||')') RETURNING id into v_ntrn_id; + INSERT INTO transactions ("description") VALUES ('undo '||i_id||' ('||v_old_trn.description||')') RETURNING id into v_ntrn_id; FOR v_old_split IN SELECT * FROM transaction_splits WHERE "transaction" = i_id LOOP From ff68817129d677ae372942a909f70692d735131f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Thu, 17 Jul 2025 15:17:18 +0200 Subject: [PATCH 102/139] Moar transfer logging. --- brmbar3/brmbar-gui-qt4.py | 7 ++++++- brmbar3/brmbar/Database.py | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/brmbar3/brmbar-gui-qt4.py b/brmbar3/brmbar-gui-qt4.py index 62e4c4a..cb8fa46 100755 --- a/brmbar3/brmbar-gui-qt4.py +++ b/brmbar3/brmbar-gui-qt4.py @@ -150,11 +150,16 @@ class ShopAdapter(QtCore.QObject): @QtCore.Slot('QVariant', 'QVariant', 'QVariant', result='QVariant') def newTransfer(self, uidfrom, uidto, amount): + logger.debug("newTransfer %s %s %s", uidfrom, uidto, amount) ufrom = brmbar.Account.load(db, id=uidfrom) + logger.debug(" ufrom = %s", ufrom) uto = brmbar.Account.load(db, id=uidto) + logger.debug(" uto = %s", uto) shop.transfer_credit(ufrom, uto, amount = amount) db.commit() - return currency.str(float(amount)) + csfa = currency.str(float(amount)) + logger.debug(" csfa = '%s'", csfa) + return csfa @QtCore.Slot('QVariant', result='QVariant') def balance_user(self, userid): diff --git a/brmbar3/brmbar/Database.py b/brmbar3/brmbar/Database.py index 6b985eb..7cf3a9b 100644 --- a/brmbar3/brmbar/Database.py +++ b/brmbar3/brmbar/Database.py @@ -39,13 +39,13 @@ class Database: cur.execute(query, attrs) return cur except psycopg2.DataError as error: # when biitr comes and enters '99999999999999999999' for amount - print("We have invalid input data (SQLi?): level %s (%s) @%s" % ( + logger.debug("We have invalid input data (SQLi?): level %s (%s) @%s" % ( level, error, time.strftime("%Y%m%d %a %I:%m %p") )) self.db_conn.rollback() raise RuntimeError("Unsanitized data entered again... BOBBY TABLES") except psycopg2.OperationalError as error: - print("Sleeping: level %s (%s) @%s" % ( + logger.debug("Sleeping: level %s (%s) @%s" % ( level, error, time.strftime("%Y%m%d %a %I:%m %p") )) #TODO: emit message "db conn failed, reconnecting From 9f63a6760ee55596b8470c32b1033d08ffa110d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Thu, 17 Jul 2025 15:24:18 +0200 Subject: [PATCH 103/139] Fix Shop.py credit -> amount --- brmbar3/brmbar/Shop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index 7a64274..aa582b8 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -119,7 +119,7 @@ class Shop: def transfer_credit(self, userfrom, userto, amount): self.db.execute_and_fetch( "SELECT public.transfer_credit(%s, %s, %s, %s)", - [self.cash.id, credit, user.id, user.name] + [self.cash.id, amount, user.id, user.name] ) self.db.commit() #self.add_credit(amount, userto) From 6f945c3a0fed166c57020538fd9eede9d0620b94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Thu, 17 Jul 2025 15:27:57 +0200 Subject: [PATCH 104/139] Fix Shop.py transfer with more arguments. --- brmbar3/brmbar/Shop.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index aa582b8..03153d8 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -118,8 +118,8 @@ class Shop: def transfer_credit(self, userfrom, userto, amount): self.db.execute_and_fetch( - "SELECT public.transfer_credit(%s, %s, %s, %s)", - [self.cash.id, amount, user.id, user.name] + "SELECT public.transfer_credit(%s, %s, %s, %s, %s, %s)", + [self.cash.id, amount, userfrom.id, userfrom.name, userto.id, userto.name] ) self.db.commit() #self.add_credit(amount, userto) From f8a265f1d28df451e6c92dda72957849ed8c2ec6 Mon Sep 17 00:00:00 2001 From: TMA Date: Thu, 17 Jul 2025 16:01:27 +0200 Subject: [PATCH 105/139] 0021: constraints on currency columns in database: numbers only --- .../0021-constraints-on-numeric-columns.sql | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 brmbar3/schema/0021-constraints-on-numeric-columns.sql diff --git a/brmbar3/schema/0021-constraints-on-numeric-columns.sql b/brmbar3/schema/0021-constraints-on-numeric-columns.sql new file mode 100644 index 0000000..524f08a --- /dev/null +++ b/brmbar3/schema/0021-constraints-on-numeric-columns.sql @@ -0,0 +1,48 @@ +-- +-- 0021-constraints-on-numeric-columns.sql +-- +-- #21 - stored function for adding constraints to numeric columns +-- +-- ISC License +-- +-- Copyright 2023-2025 Brmlab, z.s. +-- TMA +-- +-- Permission to use, copy, modify, and/or distribute this software +-- for any purpose with or without fee is hereby granted, provided +-- that the above copyright notice and this permission notice appear +-- in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +-- + +-- To require fully-qualified names +SELECT pg_catalog.set_config('search_path', '', false); + +DO $upgrade_block$ +BEGIN + +IF brmbar_privileged.has_exact_schema_version(20) THEN + +ALTER TABLE public.transaction_splits ADD CONSTRAINT amount_check + CHECK (amount NOT IN ('Infinity'::numeric, '-Infinity'::numeric, 'NaN'::numeric)); + +ALTER TABLE public.exchange_rates ADD CONSTRAINT rate_check + CHECK (rate NOT IN ('Infinity'::numeric, '-Infinity'::numeric, 'NaN'::numeric)); + + +PERFORM brmbar_privileged.upgrade_schema_version_to(21); +END IF; + +END; +$upgrade_block$; + +-- vim: set ft=plsql : + From 5d658a7406d33a263e76af15f7a6e43cb56f0406 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Thu, 17 Jul 2025 16:23:26 +0200 Subject: [PATCH 106/139] Rollback on generic database exceptions. --- brmbar3/brmbar/Database.py | 1 + 1 file changed, 1 insertion(+) diff --git a/brmbar3/brmbar/Database.py b/brmbar3/brmbar/Database.py index 7cf3a9b..ba0b24b 100644 --- a/brmbar3/brmbar/Database.py +++ b/brmbar3/brmbar/Database.py @@ -59,6 +59,7 @@ class Database: return self._execute(cur, query, attrs, level+1) except Exception as ex: logger.debug("_execute exception: %s", ex) + self.db_conn.rollback() def commit(self): """passes commit to db""" From e31165d668e28f067bdefa6fc9dc27c2ab5b7565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Thu, 17 Jul 2025 16:47:23 +0200 Subject: [PATCH 107/139] Change to receipt_reimbursement. --- brmbar3/brmbar/Shop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index 03153d8..c219be1 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -146,7 +146,7 @@ class Shop: #self.profits.credit(transaction, credit, user.name) #user.credit(transaction, credit, "Credit from receipt: " + description) self.db.execute_and_fetch( - "SELECT public.buy_for_cash(%s, %s, %s, %s, %s)", + "SELECT public.receipt_reimbursement(%s, %s, %s, %s, %s)", [self.profits.id, user.id, user.name, credit, description] )[0] self.db.commit() From 95e55aef23dc385995b53b2eac9efe3afd456554 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Thu, 17 Jul 2025 16:50:02 +0200 Subject: [PATCH 108/139] Fix i_transaction_id as v_transaction_id. --- brmbar3/schema/0016-shop-receipt-to-credit.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/brmbar3/schema/0016-shop-receipt-to-credit.sql b/brmbar3/schema/0016-shop-receipt-to-credit.sql index 21af8b1..db77f7a 100644 --- a/brmbar3/schema/0016-shop-receipt-to-credit.sql +++ b/brmbar3/schema/0016-shop-receipt-to-credit.sql @@ -48,10 +48,10 @@ BEGIN 'Receipt: ' || i_description); -- the "profit" INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) - VALUES (i_transaction_id, 'credit', i_profits_id, i_amount, i_user_name); + VALUES (v_transaction_id, 'credit', i_profits_id, i_amount, i_user_name); -- the user INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) - VALUES (i_transaction_id, 'credit', i_user_id, i_amount, 'Credit from receipt: ' || i_description); + VALUES (v_transaction_id, 'credit', i_user_id, i_amount, 'Credit from receipt: ' || i_description); END; $$; From be0b50fedd088c2db3f3c0d711c4b61aaced058e Mon Sep 17 00:00:00 2001 From: TMA Date: Thu, 17 Jul 2025 16:57:42 +0200 Subject: [PATCH 109/139] Fix i_transaction_id as v_transaction_id elsewhere. --- brmbar3/schema/0015-shop-buy-for-cash.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/brmbar3/schema/0015-shop-buy-for-cash.sql b/brmbar3/schema/0015-shop-buy-for-cash.sql index 5cf12bb..90cb883 100644 --- a/brmbar3/schema/0015-shop-buy-for-cash.sql +++ b/brmbar3/schema/0015-shop-buy-for-cash.sql @@ -60,12 +60,12 @@ BEGIN -- the item INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) - VALUES (i_transaction_id, 'debit', i_item_id, i_amount, + VALUES (v_transaction_id, 'debit', i_item_id, i_amount, 'Cash'); -- the cash INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) - VALUES (i_transaction_id, 'credit', i_cash_account_id, v_cost, + VALUES (v_transaction_id, 'credit', i_cash_account_id, v_cost, i_item_name); -- Return the cost From f962586e3acbd6bf2fd2c9b488d9504ff05b7924 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Thu, 17 Jul 2025 17:16:31 +0200 Subject: [PATCH 110/139] Ensure amount in buy_for_cash is integer. --- brmbar3/brmbar/Shop.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index c219be1..fcc54be 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -126,9 +126,12 @@ class Shop: #self.withdraw_credit(amount, userfrom) def buy_for_cash(self, item, amount = 1): + iamount = int(amount) + famount = float(iamount) + assert famount == amount, "amount is not integer value %s".format(amount) cost = self.db.execute_and_fetch( "SELECT public.buy_for_cash(%s, %s, %s, %s, %s)", - [self.cash.id, item.id, amount, self.currency.id, item.name] + [self.cash.id, item.id, iamount, self.currency.id, item.name] )[0] # Buy: Currency conversion from item currency to shop currency #(buy, sell) = item.currency.rates(self.currency) From 76a484ea5e15d07d8bf74d0dde394784bdf4d30d Mon Sep 17 00:00:00 2001 From: TMA Date: Thu, 17 Jul 2025 17:30:09 +0200 Subject: [PATCH 111/139] 0015: find_buy_rate takes currency ID --- brmbar3/schema/0015-shop-buy-for-cash.sql | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/brmbar3/schema/0015-shop-buy-for-cash.sql b/brmbar3/schema/0015-shop-buy-for-cash.sql index 90cb883..aa66260 100644 --- a/brmbar3/schema/0015-shop-buy-for-cash.sql +++ b/brmbar3/schema/0015-shop-buy-for-cash.sql @@ -44,12 +44,15 @@ DECLARE v_buy_rate NUMERIC; v_cost NUMERIC; v_transaction_id public.transactions.id%TYPE; + v_item_currency_id public.accounts.currency%TYPE; BEGIN - -- this could fail and it would generate exception in python - -- FIXME: convert v_buy_rate < 0 into python exception - v_buy_rate := public.find_buy_rate(i_item_id, i_target_currency_id); - -- this could fail and it would generate exception in python, even though it is not used - --v_sell_rate := public.find_sell_rate(i_item_id, i_target_currency_id); + -- Get item's currency + SELECT currency + INTO STRICT v_item_currency_id + FROM public.accounts + WHERE id=i_item_id; + -- Get the buy rates from the stored functions + v_buy_rate := public.find_buy_rate(v_item_currency_id, i_target_currency_id); -- Calculate cost and profit v_cost := i_amount * v_buy_rate; From 01491deec3fcb8fbac3fcd6feaabb9ea91e40574 Mon Sep 17 00:00:00 2001 From: TMA Date: Thu, 17 Jul 2025 17:48:36 +0200 Subject: [PATCH 112/139] remove Account._transaction_split and dependents --- brmbar3/brmbar/Account.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/brmbar3/brmbar/Account.py b/brmbar3/brmbar/Account.py index ef7a494..a849279 100644 --- a/brmbar3/brmbar/Account.py +++ b/brmbar3/brmbar/Account.py @@ -66,15 +66,15 @@ class Account: def negbalance_str(self): return self.currency.str(-self.balance()) - def debit(self, transaction, amount, memo): - return self._transaction_split(transaction, 'debit', amount, memo) + #def debit(self, transaction, amount, memo): + # return self._transaction_split(transaction, 'debit', amount, memo) - def credit(self, transaction, amount, memo): - return self._transaction_split(transaction, 'credit', amount, memo) + #def credit(self, transaction, amount, memo): + # return self._transaction_split(transaction, 'credit', amount, memo) - def _transaction_split(self, transaction, side, amount, memo): - """ Common part of credit() and debit(). """ - self.db.execute("INSERT INTO transaction_splits (transaction, side, account, amount, memo) VALUES (%s, %s, %s, %s, %s)", [transaction, side, self.id, amount, memo]) + #def _transaction_split(self, transaction, side, amount, memo): + # """ Common part of credit() and debit(). """ + # self.db.execute("INSERT INTO transaction_splits (transaction, side, account, amount, memo) VALUES (%s, %s, %s, %s, %s)", [transaction, side, self.id, amount, memo]) def add_barcode(self, barcode): # self.db.execute("INSERT INTO barcodes (account, barcode) VALUES (%s, %s)", [self.id, barcode]) From a8e4b3216d800fc8bde367f4f7009dd0ad531b78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Mon, 21 Jul 2025 15:02:09 +0200 Subject: [PATCH 113/139] Old scripts: mark them deprecated and make them exit with status 1. --- brmbar3/autostock.py | 3 +++ brmbar3/brmbar-cli.py | 2 ++ brmbar3/brmbar-tui.py | 3 +++ brmbar3/brmbar-web.py | 3 +++ 4 files changed, 11 insertions(+) diff --git a/brmbar3/autostock.py b/brmbar3/autostock.py index 0e8afac..23d1e8d 100755 --- a/brmbar3/autostock.py +++ b/brmbar3/autostock.py @@ -4,6 +4,7 @@ import argparse import brmbar import math from brmbar import Database +import sys def main(): parser = argparse.ArgumentParser(usage = "File format: EAN amount total_price name, e.g. 4001242002377 6 167.40 Chio Tortillas") @@ -38,5 +39,7 @@ def main(): print("Total is {}".format(total)) if __name__ == "__main__": + print("!!! THIS PROGRAM NO LONGER WORKS !!!") + sys.exit(1) main() diff --git a/brmbar3/brmbar-cli.py b/brmbar3/brmbar-cli.py index cc5c6e3..a0545ab 100755 --- a/brmbar3/brmbar-cli.py +++ b/brmbar3/brmbar-cli.py @@ -6,6 +6,8 @@ from brmbar import Database import brmbar +print("!!! THIS PROGRAM NO LONGER WORKS !!!") +sys.exit(1) def help(): print("""BrmBar v3 (c) Petr Baudis 2012-2013 diff --git a/brmbar3/brmbar-tui.py b/brmbar3/brmbar-tui.py index 00bdf44..a2dc10b 100755 --- a/brmbar3/brmbar-tui.py +++ b/brmbar3/brmbar-tui.py @@ -6,6 +6,9 @@ from brmbar import Database import brmbar +print("!!! THIS PROGRAM NO LONGER WORKS !!!") +sys.exit(1) + db = Database.Database("dbname=brmbar") shop = brmbar.Shop.new_with_defaults(db) currency = shop.currency diff --git a/brmbar3/brmbar-web.py b/brmbar3/brmbar-web.py index 5d84378..522e9ed 100755 --- a/brmbar3/brmbar-web.py +++ b/brmbar3/brmbar-web.py @@ -6,6 +6,9 @@ from brmbar import Database import brmbar +print("!!! THIS PROGRAM NO LONGER WORKS !!!") +sys.exit(1) + from flask import * app = Flask(__name__) #app.debug = True From dde8bd764d8c8ce8cd21c6f76745a864eaa8c777 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Mon, 21 Jul 2025 15:42:40 +0200 Subject: [PATCH 114/139] Use new shop init interface and black all brmbar/*.py. --- brmbar3/brmbar/Account.py | 69 ++++----- brmbar3/brmbar/Currency.py | 122 +++++++++------ brmbar3/brmbar/Database.py | 37 +++-- brmbar3/brmbar/Shop.py | 293 ++++++++++++++++--------------------- 4 files changed, 253 insertions(+), 268 deletions(-) diff --git a/brmbar3/brmbar/Account.py b/brmbar3/brmbar/Account.py index a849279..3a25170 100644 --- a/brmbar3/brmbar/Account.py +++ b/brmbar3/brmbar/Account.py @@ -1,12 +1,15 @@ from .Currency import Currency import logging + logger = logging.getLogger(__name__) + class Account: - """ BrmBar Account + """BrmBar Account Both users and items are accounts. So is the money box, etc. Each account has a currency.""" + def __init__(self, db, id, name, currency, acctype): self.db = db self.id = id @@ -17,48 +20,38 @@ class Account: @classmethod def load_by_barcode(cls, db, barcode): logger.debug("load_by_barcode: '%s'", barcode) - res = db.execute_and_fetch("SELECT account FROM barcodes WHERE barcode = %s", [barcode]) + res = db.execute_and_fetch( + "SELECT account FROM barcodes WHERE barcode = %s", [barcode] + ) if res is None: return None id = res[0] - return cls.load(db, id = id) + return cls.load(db, id=id) @classmethod - def load(cls, db, id = None, name = None): - """ Constructor for existing account """ - if id is not None: - name = db.execute_and_fetch("SELECT name FROM accounts WHERE id = %s", [id]) - name = name[0] - elif name is not None: - id = db.execute_and_fetch("SELECT id FROM accounts WHERE name = %s", [name]) - id = id[0] - else: - raise NameError("Account.load(): Specify either id or name") - - currid, acctype = db.execute_and_fetch("SELECT currency, acctype FROM accounts WHERE id = %s", [id]) - currency = Currency.load(db, id = currid) - - return cls(db, name = name, id = id, currency = currency, acctype = acctype) + def load(cls, db, id=None): + """Constructor for existing account""" + if id is None: + raise NameError("Account.load(): Specify id") + name, currid, acctype = db.execute_and_fetch( + "SELECT name, currency, acctype FROM accounts WHERE id = %s", [id] + ) + currency = Currency.load(db, id=currid) + return cls(db, name=name, id=id, currency=currency, acctype=acctype) @classmethod def create(cls, db, name, currency, acctype): - """ Constructor for new account """ - # id = db.execute_and_fetch("INSERT INTO accounts (name, currency, acctype) VALUES (%s, %s, %s) RETURNING id", [name, currency.id, acctype]) - id = db.execute_and_fetch("SELECT public.create_account(%s, %s, %s)", [name, currency.id, acctype]) - # id = id[0] - return cls(db, name = name, id = id, currency = currency, acctype = acctype) + """Constructor for new account""" + id = db.execute_and_fetch( + "SELECT public.create_account(%s, %s, %s)", [name, currency.id, acctype] + ) + return cls(db, name=name, id=id, currency=currency, acctype=acctype) def balance(self): bal = self.db.execute_and_fetch( - "SELECT public.compute_account_balance(%s)", - [self.id] + "SELECT public.compute_account_balance(%s)", [self.id] )[0] return bal - #debit = self.db.execute_and_fetch("SELECT SUM(amount) FROM transaction_splits WHERE account = %s AND side = %s", [self.id, 'debit']) - #debit = debit[0] or 0 - #credit = self.db.execute_and_fetch("SELECT SUM(amount) FROM transaction_splits WHERE account = %s AND side = %s", [self.id, 'credit']) - #credit = credit[0] or 0 - #return debit - credit def balance_str(self): return self.currency.str(self.balance()) @@ -66,22 +59,12 @@ class Account: def negbalance_str(self): return self.currency.str(-self.balance()) - #def debit(self, transaction, amount, memo): - # return self._transaction_split(transaction, 'debit', amount, memo) - - #def credit(self, transaction, amount, memo): - # return self._transaction_split(transaction, 'credit', amount, memo) - - #def _transaction_split(self, transaction, side, amount, memo): - # """ Common part of credit() and debit(). """ - # self.db.execute("INSERT INTO transaction_splits (transaction, side, account, amount, memo) VALUES (%s, %s, %s, %s, %s)", [transaction, side, self.id, amount, memo]) - def add_barcode(self, barcode): - # self.db.execute("INSERT INTO barcodes (account, barcode) VALUES (%s, %s)", [self.id, barcode]) - self.db.execute("SELECT public.add_barcode_to_account(%s, %s)", [self.id, barcode]) + self.db.execute( + "SELECT public.add_barcode_to_account(%s, %s)", [self.id, barcode] + ) self.db.commit() def rename(self, name): - # self.db.execute("UPDATE accounts SET name = %s WHERE id = %s", [name, self.id]) self.db.execute("SELECT public.rename_account(%s, %s)", [self.id, name]) self.name = name diff --git a/brmbar3/brmbar/Currency.py b/brmbar3/brmbar/Currency.py index 29da4a3..3033287 100644 --- a/brmbar3/brmbar/Currency.py +++ b/brmbar3/brmbar/Currency.py @@ -1,10 +1,12 @@ # vim: set fileencoding=utf8 + class Currency: - """ Currency - + """Currency + Each account has a currency (1 Kč, 1 Club Maté, ...), pairs of - currencies have (asymmetric) exchange rates. """ + currencies have (asymmetric) exchange rates.""" + def __init__(self, db, id, name): self.db = db self.id = id @@ -12,72 +14,103 @@ class Currency: @classmethod def default(cls, db): - """ Default wallet currency """ - return cls.load(db, name = "Kč") + """Default wallet currency""" + return cls.load(db, name="Kč") @classmethod - def load(cls, db, id = None, name = None): - """ Constructor for existing currency """ - if id is not None: - name = db.execute_and_fetch("SELECT name FROM currencies WHERE id = %s", [id]) - name = name[0] - elif name is not None: - id = db.execute_and_fetch("SELECT id FROM currencies WHERE name = %s", [name]) - id = id[0] - else: - raise NameError("Currency.load(): Specify either id or name") - return cls(db, name = name, id = id) + def load(cls, db, id=None): + """Constructor for existing currency""" + if id is None: + raise NameError("Currency.load(): Specify id") + name = db.execute_and_fetch("SELECT name FROM currencies WHERE id = %s", [id]) + name = name[0] + return cls(db, id=id, name=name) @classmethod def create(cls, db, name): - """ Constructor for new currency """ - # id = db.execute_and_fetch("INSERT INTO currencies (name) VALUES (%s) RETURNING id", [name]) + """Constructor for new currency""" id = db.execute_and_fetch("SELECT public.create_currency(%s)", [name]) - # id = id[0] - return cls(db, name = name, id = id) + return cls(db, id=id, name=name) def rates(self, other): - """ Return tuple ($buy, $sell) of rates of $self in relation to $other (brmbar.Currency): + """Return tuple ($buy, $sell) of rates of $self in relation to $other (brmbar.Currency): $buy is the price of $self in means of $other when buying it (into brmbar) - $sell is the price of $self in means of $other when selling it (from brmbar) """ + $sell is the price of $self in means of $other when selling it (from brmbar)""" # buy rate - res = self.db.execute_and_fetch("SELECT public.find_buy_rate(%s, %s)",[self.id, other.id]) + res = self.db.execute_and_fetch( + "SELECT public.find_buy_rate(%s, %s)", [self.id, other.id] + ) if res is None: - raise NameError("Something fishy in find_buy_rate."); + raise NameError("Something fishy in find_buy_rate.") buy = res[0] if buy < 0: - raise NameError("Currency.rate(): Unknown conversion " + other.name() + " to " + self.name()) + raise NameError( + "Currency.rate(): Unknown conversion " + + other.name() + + " to " + + self.name() + ) # sell rate - res = self.db.execute_and_fetch("SELECT public.find_sell_rate(%s, %s)",[self.id, other.id]) + res = self.db.execute_and_fetch( + "SELECT public.find_sell_rate(%s, %s)", [self.id, other.id] + ) if res is None: - raise NameError("Something fishy in find_sell_rate."); + raise NameError("Something fishy in find_sell_rate.") sell = res[0] if sell < 0: - raise NameError("Currency.rate(): Unknown conversion " + self.name() + " to " + other.name()) + raise NameError( + "Currency.rate(): Unknown conversion " + + self.name() + + " to " + + other.name() + ) return (buy, sell) def rates2(self, other): # the original code for compare testing - res = self.db.execute_and_fetch("SELECT rate, rate_dir FROM exchange_rates WHERE target = %s AND source = %s AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1", [self.id, other.id]) + res = self.db.execute_and_fetch( + "SELECT rate, rate_dir FROM exchange_rates WHERE target = %s AND source = %s AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1", + [self.id, other.id], + ) if res is None: - raise NameError("Currency.rate(): Unknown conversion " + other.name() + " to " + self.name()) + raise NameError( + "Currency.rate(): Unknown conversion " + + other.name() + + " to " + + self.name() + ) buy_rate, buy_rate_dir = res - buy = buy_rate if buy_rate_dir == "target_to_source" else 1/buy_rate + buy = buy_rate if buy_rate_dir == "target_to_source" else 1 / buy_rate - res = self.db.execute_and_fetch("SELECT rate, rate_dir FROM exchange_rates WHERE target = %s AND source = %s AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1", [other.id, self.id]) + res = self.db.execute_and_fetch( + "SELECT rate, rate_dir FROM exchange_rates WHERE target = %s AND source = %s AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1", + [other.id, self.id], + ) if res is None: - raise NameError("Currency.rate(): Unknown conversion " + self.name() + " to " + other.name()) + raise NameError( + "Currency.rate(): Unknown conversion " + + self.name() + + " to " + + other.name() + ) sell_rate, sell_rate_dir = res - sell = sell_rate if sell_rate_dir == "source_to_target" else 1/sell_rate + sell = sell_rate if sell_rate_dir == "source_to_target" else 1 / sell_rate return (buy, sell) - def convert(self, amount, target): - res = self.db.execute_and_fetch("SELECT rate, rate_dir FROM exchange_rates WHERE target = %s AND source = %s AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1", [target.id, self.id]) + res = self.db.execute_and_fetch( + "SELECT rate, rate_dir FROM exchange_rates WHERE target = %s AND source = %s AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1", + [target.id, self.id], + ) if res is None: - raise NameError("Currency.convert(): Unknown conversion " + self.name() + " to " + target.name()) + raise NameError( + "Currency.convert(): Unknown conversion " + + self.name() + + " to " + + target.name() + ) rate, rate_dir = res if rate_dir == "source_to_target": resamount = amount * rate @@ -89,10 +122,13 @@ class Currency: return "{:.2f} {}".format(amount, self.name) def update_sell_rate(self, target, rate): - # self.db.execute("INSERT INTO exchange_rates (source, target, rate, rate_dir) VALUES (%s, %s, %s, %s)", [self.id, target.id, rate, "source_to_target"]) - self.db.execute("SELECT public.update_currency_sell_rate(%s, %s, %s)", - [self.id, target.id, rate]) + self.db.execute( + "SELECT public.update_currency_sell_rate(%s, %s, %s)", + [self.id, target.id, rate], + ) + def update_buy_rate(self, source, rate): - # self.db.execute("INSERT INTO exchange_rates (source, target, rate, rate_dir) VALUES (%s, %s, %s, %s)", [source.id, self.id, rate, "target_to_source"]) - self.db.execute("SELECT public.update_currency_buy_rate(%s, %s, %s)", - [source.id, self.id, rate]) + self.db.execute( + "SELECT public.update_currency_buy_rate(%s, %s, %s)", + [source.id, self.id, rate], + ) diff --git a/brmbar3/brmbar/Database.py b/brmbar3/brmbar/Database.py index ba0b24b..b70687c 100644 --- a/brmbar3/brmbar/Database.py +++ b/brmbar3/brmbar/Database.py @@ -4,26 +4,29 @@ import psycopg2 from contextlib import closing import time import logging + logger = logging.getLogger(__name__) + class Database: """self-reconnecting database object""" + def __init__(self, dsn): self.db_conn = psycopg2.connect(dsn) self.dsn = dsn - def execute(self, query, attrs = None): + def execute(self, query, attrs=None): """execute a query and return one result""" with closing(self.db_conn.cursor()) as cur: cur = self._execute(cur, query, attrs) - def execute_and_fetch(self, query, attrs = None): + def execute_and_fetch(self, query, attrs=None): """execute a query and return one result""" with closing(self.db_conn.cursor()) as cur: cur = self._execute(cur, query, attrs) return cur.fetchone() - def execute_and_fetchall(self, query, attrs = None): + def execute_and_fetchall(self, query, attrs=None): """execute a query and return all results""" with closing(self.db_conn.cursor()) as cur: cur = self._execute(cur, query, attrs) @@ -38,25 +41,29 @@ class Database: else: cur.execute(query, attrs) return cur - except psycopg2.DataError as error: # when biitr comes and enters '99999999999999999999' for amount - logger.debug("We have invalid input data (SQLi?): level %s (%s) @%s" % ( - level, error, time.strftime("%Y%m%d %a %I:%m %p") - )) + except ( + psycopg2.DataError + ) as error: # when biitr comes and enters '99999999999999999999' for amount + logger.debug( + "We have invalid input data (SQLi?): level %s (%s) @%s" + % (level, error, time.strftime("%Y%m%d %a %I:%m %p")) + ) self.db_conn.rollback() raise RuntimeError("Unsanitized data entered again... BOBBY TABLES") except psycopg2.OperationalError as error: - logger.debug("Sleeping: level %s (%s) @%s" % ( - level, error, time.strftime("%Y%m%d %a %I:%m %p") - )) - #TODO: emit message "db conn failed, reconnecting - time.sleep(2 ** level) + logger.debug( + "Sleeping: level %s (%s) @%s" + % (level, error, time.strftime("%Y%m%d %a %I:%m %p")) + ) + # TODO: emit message "db conn failed, reconnecting + time.sleep(2**level) try: self.db_conn = psycopg2.connect(self.dsn) except psycopg2.OperationalError: - #TODO: emit message "psql not running to interface + # TODO: emit message "psql not running to interface time.sleep(1) - cur = self.db_conn.cursor() #how ugly is this? - return self._execute(cur, query, attrs, level+1) + cur = self.db_conn.cursor() # how ugly is this? + return self._execute(cur, query, attrs, level + 1) except Exception as ex: logger.debug("_execute exception: %s", ex) self.db_conn.rollback() diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index fcc54be..4c858ab 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -2,161 +2,163 @@ import brmbar from .Currency import Currency from .Account import Account import logging + logger = logging.getLogger(__name__) + class Shop: - """ BrmBar Shop + """BrmBar Shop Business logic so that only interaction is left in the hands - of the frontend scripts. """ - def __init__(self, db, currency, profits, cash, excess, deficit): + of the frontend scripts.""" + + def __init__(self, db, currency_id, profits_id, cash_id, excess_id, deficit_id): + # Keep db as-is self.db = db - self.currency = currency # brmbar.Currency - self.profits = profits # income brmbar.Account for brmbar profit margins on items - self.cash = cash # our operational ("wallet") cash account - self.excess = excess # account from which is deducted cash during inventory item fixing (when system contains less items than is the reality) - self.deficit = deficit # account where is put cash during inventory item fixing (when system contains more items than is the reality) + + # Store all ids + self.currency_id = currency_id + self.profits_id = profits_id + self.cash_id = cash_id + self.excess_id = excess_id + self.deficit_id = deficit_id + + # Create objects where needed for legacy code + + # brmbar.Currency + self.currency = Currency.load(self.db, id=self.currency_id) + + # income brmbar.Account for brmbar profit margins on items + self.profits = Account.load(db, id=self.profits_id) + + # our operational ("wallet") cash account + self.cash = Account.load(db, id=self.cash_id) + + # account from which is deducted cash during inventory item + # fixing (when system contains less items than is the + # reality) + self.excess = Account.load(db, id=self.excess_id) + + # account where is put cash during inventory item fixing (when + # system contains more items than is the reality) + self.deficit = Account.load(db, id=self.deficit_id) @classmethod def new_with_defaults(cls, db): - return cls(db, - currency = Currency.default(db), - profits = Account.load(db, name = "BrmBar Profits"), - cash = Account.load(db, name = "BrmBar Cash"), - excess = Account.load(db, name = "BrmBar Excess"), - deficit = Account.load(db, name = "BrmBar Deficit")) + # shop_class_initialization_data + currency_id, profits_id, cash_id, excess_id, deficit_id = db.execute_and_fetch( + "select currency_id, profits_id, cash_id, excess_id, deficit_id from public.shop_class_initialization_data()" + ) + return cls( + db, + currency_id=currency_id, + profits=profits_id, + cash=cash_id, + excess=excess_id, + deficit=deficit_id, + ) - def sell(self, item, user, amount = 1): + def sell(self, item, user, amount=1): # Call the stored procedure for the sale - logger.debug("sell: item.id=%s amount=%s user.id=%s self.currency.id=%s", - item.id, amount, user.id, self.currency.id) + logger.debug( + "sell: item.id=%s amount=%s user.id=%s self.currency.id=%s", + item.id, + amount, + user.id, + self.currency.id, + ) res = self.db.execute_and_fetch( "SELECT public.sell_item(%s, %s, %s, %s, %s)", - [item.id, amount, user.id, self.currency.id, - "BrmBar sale of {0}x {1} to {2}".format(amount, item.name, user.name)] - )#[0] + [ + item.id, + amount, + user.id, + self.currency.id, + "BrmBar sale of {0}x {1} to {2}".format(amount, item.name, user.name), + ], + ) logger.debug("sell: res[0]=%s", res[0]) cost = res[0] self.db.commit() return cost - # Sale: Currency conversion from item currency to shop currency - #(buy, sell) = item.currency.rates(self.currency) - #cost = amount * sell - #profit = amount * (sell - buy) - #transaction = self._transaction(responsible = user, description = "BrmBar sale of {}x {} to {}".format(amount, item.name, user.name)) - #item.credit(transaction, amount, user.name) - #user.debit(transaction, cost, item.name) # debit (increase) on a _debt_ account - #self.profits.debit(transaction, profit, "Margin on " + item.name) - #self.db.commit() - #return cost - - def sell_for_cash(self, item, amount = 1): + def sell_for_cash(self, item, amount=1): cost = self.db.execute_and_fetch( "SELECT public.sell_item_for_cash(%s, %s, %s, %s, %s)", - [item.id, amount, user.id, self.currency.id, - "BrmBar sale of {0}x {1} for cash".format(amount, item.name)] - )[0]#[0] + [ + item.id, + amount, + user.id, + self.currency.id, + "BrmBar sale of {0}x {1} for cash".format(amount, item.name), + ], + )[0] self.db.commit() return cost - ## Sale: Currency conversion from item currency to shop currency - #(buy, sell) = item.currency.rates(self.currency) - #cost = amount * sell - #profit = amount * (sell - buy) - #transaction = self._transaction(description = "BrmBar sale of {}x {} for cash".format(amount, item.name)) - #item.credit(transaction, amount, "Cash") - #self.cash.debit(transaction, cost, item.name) - #self.profits.debit(transaction, profit, "Margin on " + item.name) - #self.db.commit() - - #return cost - - def undo_sale(self, item, user, amount = 1): - # Undo sale; rarely needed - #(buy, sell) = item.currency.rates(self.currency) - #cost = amount * sell - #profit = amount * (sell - buy) - - #transaction = self._transaction(responsible = user, description = "BrmBar sale UNDO of {}x {} to {}".format(amount, item.name, user.name)) - #item.debit(transaction, amount, user.name + " (sale undo)") - #user.credit(transaction, cost, item.name + " (sale undo)") - #self.profits.credit(transaction, profit, "Margin repaid on " + item.name) + def undo_sale(self, item, user, amount=1): # Call the stored procedure for undoing a sale cost = self.db.execute_and_fetch( "SELECT public.undo_sale_of_item(%s, %s, %s, %s)", - [item.id, amount, user.id, user.currency.id, "BrmBar sale UNDO of {0}x {1} to {2}".format(amount, item.name, user.name)] - )[0]#[0] - + [ + item.id, + amount, + user.id, + user.currency.id, + "BrmBar sale UNDO of {0}x {1} to {2}".format( + amount, item.name, user.name + ), + ], + )[0] self.db.commit() - return cost def add_credit(self, credit, user): self.db.execute_and_fetch( "SELECT public.add_credit(%s, %s, %s, %s)", - [self.cash.id, credit, user.id, user.name] + [self.cash.id, credit, user.id, user.name], ) self.db.commit() - #transaction = self._transaction(responsible = user, description = "BrmBar credit replenishment for " + user.name) - #self.cash.debit(transaction, credit, user.name) - #user.credit(transaction, credit, "Credit replenishment") - #self.db.commit() - def withdraw_credit(self, credit, user): self.db.execute_and_fetch( "SELECT public.withdraw_credit(%s, %s, %s, %s)", - [self.cash.id, credit, user.id, user.name] + [self.cash.id, credit, user.id, user.name], ) self.db.commit() - #transaction = self._transaction(responsible = user, description = "BrmBar credit withdrawal for " + user.name) - #self.cash.credit(transaction, credit, user.name) - #user.debit(transaction, credit, "Credit withdrawal") - #self.db.commit() def transfer_credit(self, userfrom, userto, amount): self.db.execute_and_fetch( "SELECT public.transfer_credit(%s, %s, %s, %s, %s, %s)", - [self.cash.id, amount, userfrom.id, userfrom.name, userto.id, userto.name] + [self.cash.id, amount, userfrom.id, userfrom.name, userto.id, userto.name], ) self.db.commit() - #self.add_credit(amount, userto) - #self.withdraw_credit(amount, userfrom) - def buy_for_cash(self, item, amount = 1): + def buy_for_cash(self, item, amount=1): iamount = int(amount) famount = float(iamount) assert famount == amount, "amount is not integer value %s".format(amount) cost = self.db.execute_and_fetch( "SELECT public.buy_for_cash(%s, %s, %s, %s, %s)", - [self.cash.id, item.id, iamount, self.currency.id, item.name] + [self.cash.id, item.id, iamount, self.currency.id, item.name], )[0] - # Buy: Currency conversion from item currency to shop currency - #(buy, sell) = item.currency.rates(self.currency) - #cost = amount * buy - - #transaction = self._transaction(description = "BrmBar stock replenishment of {}x {} for cash".format(amount, item.name)) - #item.debit(transaction, amount, "Cash") - #self.cash.credit(transaction, cost, item.name) self.db.commit() return cost def receipt_to_credit(self, user, credit, description): - #transaction = self._transaction(responsible = user, description = "Receipt: " + description) - #self.profits.credit(transaction, credit, user.name) - #user.credit(transaction, credit, "Credit from receipt: " + description) self.db.execute_and_fetch( "SELECT public.receipt_reimbursement(%s, %s, %s, %s, %s)", - [self.profits.id, user.id, user.name, credit, description] + [self.profits.id, user.id, user.name, credit, description], )[0] self.db.commit() - def _transaction(self, responsible = None, description = None): - transaction = self.db.execute_and_fetch("INSERT INTO transactions (responsible, description) VALUES (%s, %s) RETURNING id", - [responsible.id if responsible else None, description]) + def _transaction(self, responsible=None, description=None): + transaction = self.db.execute_and_fetch( + "INSERT INTO transactions (responsible, description) VALUES (%s, %s) RETURNING id", + [responsible.id if responsible else None, description], + ) transaction = transaction[0] return transaction @@ -169,24 +171,31 @@ class Shop: WHERE a.acctype = %s AND ts.side = %s """ if overflow is not None: - sumselect += ' AND a.name ' + ('NOT ' if overflow == 'exclude' else '') + ' LIKE \'%%-overflow\'' - cur = self.db.execute_and_fetch(sumselect, ["debt", 'debit']) + sumselect += ( + " AND a.name " + + ("NOT " if overflow == "exclude" else "") + + " LIKE '%%-overflow'" + ) + cur = self.db.execute_and_fetch(sumselect, ["debt", "debit"]) debit = cur[0] or 0 - credit = self.db.execute_and_fetch(sumselect, ["debt", 'credit']) + credit = self.db.execute_and_fetch(sumselect, ["debt", "credit"]) credit = credit[0] or 0 return debit - credit + def credit_negbalance_str(self, overflow=None): return self.currency.str(-self.credit_balance(overflow=overflow)) -# XXX causing extra heavy delay ( thousands of extra SQL queries ), disabled + # XXX causing extra heavy delay ( thousands of extra SQL queries ), disabled def inventory_balance(self): balance = 0 # Each inventory account has its own currency, # so we just do this ugly iteration - cur = self.db.execute_and_fetchall("SELECT id FROM accounts WHERE acctype = %s", ["inventory"]) + cur = self.db.execute_and_fetchall( + "SELECT id FROM accounts WHERE acctype = %s", ["inventory"] + ) for inventory in cur: invid = inventory[0] - inv = Account.load(self.db, id = invid) + inv = Account.load(self.db, id=invid) # FIXME: This is not correct as each instance of inventory # might have been bought for a different price! Therefore, # we need to replace the command below with a complex SQL @@ -197,7 +206,7 @@ class Shop: balance += b return balance -# XXX bypass hack + # XXX bypass hack def inventory_balance_str(self): # return self.currency.str(self.inventory_balance()) return "XXX" @@ -205,102 +214,52 @@ class Shop: def account_list(self, acctype, like_str="%%"): """list all accounts (people or items, as per acctype)""" accts = [] - cur = self.db.execute_and_fetchall("SELECT id FROM accounts WHERE acctype = %s AND name ILIKE %s ORDER BY name ASC", [acctype, like_str]) - #FIXME: sanitize input like_str ^ + cur = self.db.execute_and_fetchall( + "SELECT id FROM accounts WHERE acctype = %s AND name ILIKE %s ORDER BY name ASC", + [acctype, like_str], + ) + # FIXME: sanitize input like_str ^ for inventory in cur: - accts += [ Account.load(self.db, id = inventory[0]) ] + accts += [Account.load(self.db, id=inventory[0])] return accts def fix_inventory(self, item, amount): rv = self.db.execute_and_fetch( "SELECT public.fix_inventory(%s, %s, %s, %s, %s, %s)", - [item.id, item.currency.id, self.excess.id, self.deficit.id, self.currency.id, amount] + [ + item.id, + item.currency.id, + self.excess.id, + self.deficit.id, + self.currency.id, + amount, + ], )[0] self.db.commit() return rv - #amount_in_reality = amount - #amount_in_system = item.balance() - #(buy, sell) = item.currency.rates(self.currency) - - #diff = abs(amount_in_reality - amount_in_system) - #buy_total = buy * diff - #if amount_in_reality > amount_in_system: - # transaction = self._transaction(description = "BrmBar inventory fix of {}pcs {} in system to {}pcs in reality".format(amount_in_system, item.name,amount_in_reality)) - # item.debit(transaction, diff, "Inventory fix excess") - # self.excess.credit(transaction, buy_total, "Inventory fix excess " + item.name) - # self.db.commit() - # return True - #elif amount_in_reality < amount_in_system: - # transaction = self._transaction(description = "BrmBar inventory fix of {}pcs {} in system to {}pcs in reality".format(amount_in_system, item.name,amount_in_reality)) - # item.credit(transaction, diff, "Inventory fix deficit") - # self.deficit.debit(transaction, buy_total, "Inventory fix deficit " + item.name) - # self.db.commit() - # return True - #else: - # transaction = self._transaction(description = "BrmBar inventory fix of {}pcs {} in system to {}pcs in reality".format(amount_in_system, item.name,amount_in_reality)) - # item.debit(transaction, 0, "Inventory fix - amount was correct") - # item.credit(transaction, 0, "Inventory fix - amount was correct") - # self.db.commit() - # return False def fix_cash(self, amount): rv = self.db.execute_and_fetch( "SELECT public.fix_cash(%s, %s, %s, %s)", - [self.excess.id, self.deficit.id, self.currency.id, amount] + [self.excess.id, self.deficit.id, self.currency.id, amount], )[0] self.db.commit() return rv - #amount_in_reality = amount - #amount_in_system = self.cash.balance() - - #diff = abs(amount_in_reality - amount_in_system) - #if amount_in_reality > amount_in_system: - # transaction = self._transaction(description = "BrmBar cash inventory fix of {} in system to {} in reality".format(amount_in_system, amount_in_reality)) - # self.cash.debit(transaction, diff, "Inventory fix excess") - # self.excess.credit(transaction, diff, "Inventory cash fix excess.") - # self.db.commit() - # return True - #elif amount_in_reality < amount_in_system: - # transaction = self._transaction(description = "BrmBar cash inventory fix of {} in system to {} in reality".format(amount_in_system, amount_in_reality)) - # self.cash.credit(transaction, diff, "Inventory fix deficit") - # self.deficit.debit(transaction, diff, "Inventory fix deficit.") - # self.db.commit() - # return True - #else: - # return False def consolidate(self): msg = self.db.execute_and_fetch( "SELECT public.make_consolidate_transaction(%s, %s, %s)", - [self.excess.id, self.deficit.id, self.profits.id] + [self.excess.id, self.deficit.id, self.profits.id], )[0] - #transaction = self._transaction(description = "BrmBar inventory consolidation") - #excess_balance = self.excess.balance() - #if excess_balance != 0: - # print("Excess balance {} debited to profit".format(-excess_balance)) - # self.excess.debit(transaction, -excess_balance, "Excess balance added to profit.") - # self.profits.debit(transaction, -excess_balance, "Excess balance added to profit.") - #deficit_balance = self.deficit.balance() - #if deficit_balance != 0: - # print("Deficit balance {} credited to profit".format(deficit_balance)) - # self.deficit.credit(transaction, deficit_balance, "Deficit balance removed from profit.") - # self.profits.credit(transaction, deficit_balance, "Deficit balance removed from profit.") if msg != None: print(msg) self.db.commit() def undo(self, oldtid): - #description = self.db.execute_and_fetch("SELECT description FROM transactions WHERE id = %s", [oldtid])[0] - #description = 'undo %d (%s)' % (oldtid, description) - - #transaction = self._transaction(description=description) - #for split in self.db.execute_and_fetchall("SELECT id, side, account, amount, memo FROM transaction_splits WHERE transaction = %s", [oldtid]): - # splitid, side, account, amount, memo = split - # memo = 'undo %d (%s)' % (splitid, memo) - # amount = -amount - # self.db.execute("INSERT INTO transaction_splits (transaction, side, account, amount, memo) VALUES (%s, %s, %s, %s, %s)", [transaction, side, account, amount, memo]) - transaction = self.db.execute_and_fetch("SELECT public.undo_transaction(%s)",[oldtid])[0] + transaction = self.db.execute_and_fetch( + "SELECT public.undo_transaction(%s)", [oldtid] + )[0] self.db.commit() return transaction From 04f98147fdd0d8cce59c5a46f46c8c3c1fe5882d Mon Sep 17 00:00:00 2001 From: TMA Date: Mon, 21 Jul 2025 15:42:53 +0200 Subject: [PATCH 115/139] 0022: shop initialization stored procedure --- brmbar3/schema/0022-shop-init.sql | 85 +++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 brmbar3/schema/0022-shop-init.sql diff --git a/brmbar3/schema/0022-shop-init.sql b/brmbar3/schema/0022-shop-init.sql new file mode 100644 index 0000000..9f50eda --- /dev/null +++ b/brmbar3/schema/0022-shop-init.sql @@ -0,0 +1,85 @@ +-- +-- 0022-shop-init.sql +-- +-- #22 - stored function for initializing Shop.py +-- +-- ISC License +-- +-- Copyright 2023-2025 Brmlab, z.s. +-- TMA +-- +-- Permission to use, copy, modify, and/or distribute this software +-- for any purpose with or without fee is hereby granted, provided +-- that the above copyright notice and this permission notice appear +-- in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +-- + +-- To require fully-qualified names +SELECT pg_catalog.set_config('search_path', '', false); + +DO $upgrade_block$ +DECLARE + v INTEGER; +BEGIN + +IF brmbar_privileged.has_exact_schema_version(21) THEN + + +SELECT COUNT(1) INTO v + FROM pg_catalog.pg_type typ + INNER JOIN pg_catalog.pg_namespace nsp + ON nsp.oid = typ.typnamespace + WHERE nsp.nspname = 'brmbar_privileged' + AND typ.typname='shop_class_initialization_data_type'; + +IF v>0 THEN + RAISE NOTICE 'Changing type shop_class_initialization_data_type'; + DROP TYPE brmbar_privileged.shop_class_initialization_data_type; +ELSE + RAISE NOTICE 'Creating type shop_class_initialization_data_type'; +END IF; + +CREATE TYPE brmbar_privileged.shop_class_initialization_data_type +AS ( + currency_id INTEGER, --public.currencies.id%TYPE, + profits_id INTEGER, --public.accounts.id%TYPE, + cash_id INTEGER, --public.accounts.id%TYPE, + excess_id INTEGER, --public.accounts.id%TYPE, + deficit_id INTEGER --public.accounts.id%TYPE +); + +CREATE OR REPLACE FUNCTION public.shop_class_initialization_data() +RETURNS brmbar_privileged.shop_class_initialization_data_type +LANGUAGE plpgsql +AS +$$ +DECLARE + rv brmbar_privileged.shop_class_initialization_data_type; +BEGIN + rv.currency_id := 1; + SELECT id INTO rv.profits_id FROM public.accounts WHERE name = 'BrmBar Profits'; + SELECT id INTO rv.cash_id FROM public.accounts WHERE name = 'BrmBar Cash'; + SELECT id INTO rv.excess_id FROM public.accounts WHERE name = 'BrmBar Excess'; + SELECT id INTO rv.deficit_id FROM public.accounts WHERE name = 'BrmBar Deficit'; + RETURN rv; +END; +$$; + + +PERFORM brmbar_privileged.upgrade_schema_version_to(22); +END IF; + +END; +$upgrade_block$; + +-- vim: set ft=plsql : + From 3933514e86bcbc6303dd739d40a90530d06e6127 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Mon, 21 Jul 2025 15:46:26 +0200 Subject: [PATCH 116/139] Fix Shop keyword arguments. --- brmbar3/brmbar/Shop.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index 4c858ab..4100592 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -52,10 +52,10 @@ class Shop: return cls( db, currency_id=currency_id, - profits=profits_id, - cash=cash_id, - excess=excess_id, - deficit=deficit_id, + profits_id=profits_id, + cash_id=cash_id, + excess_id=excess_id, + deficit_id=deficit_id, ) def sell(self, item, user, amount=1): From 4f9611727bb4ff98ce802d542989d848cfc7803e Mon Sep 17 00:00:00 2001 From: TMA Date: Mon, 21 Jul 2025 16:34:28 +0200 Subject: [PATCH 117/139] 0022: drop: add cascade --- brmbar3/schema/0022-shop-init.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brmbar3/schema/0022-shop-init.sql b/brmbar3/schema/0022-shop-init.sql index 9f50eda..55fe098 100644 --- a/brmbar3/schema/0022-shop-init.sql +++ b/brmbar3/schema/0022-shop-init.sql @@ -43,7 +43,7 @@ SELECT COUNT(1) INTO v IF v>0 THEN RAISE NOTICE 'Changing type shop_class_initialization_data_type'; - DROP TYPE brmbar_privileged.shop_class_initialization_data_type; + DROP TYPE brmbar_privileged.shop_class_initialization_data_type CASCADE; ELSE RAISE NOTICE 'Creating type shop_class_initialization_data_type'; END IF; From 2e328e4fb37be7ac324b936979e7410722c57d9c Mon Sep 17 00:00:00 2001 From: TMA Date: Mon, 21 Jul 2025 16:49:29 +0200 Subject: [PATCH 118/139] 0023: stored function for total inventory balance --- brmbar3/schema/0023-inventory-balance.sql | 94 +++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 brmbar3/schema/0023-inventory-balance.sql diff --git a/brmbar3/schema/0023-inventory-balance.sql b/brmbar3/schema/0023-inventory-balance.sql new file mode 100644 index 0000000..0feb637 --- /dev/null +++ b/brmbar3/schema/0023-inventory-balance.sql @@ -0,0 +1,94 @@ +-- +-- 0023-inventory-balance.sql +-- +-- #23 - stored function for total inventory balance +-- +-- ISC License +-- +-- Copyright 2023-2025 Brmlab, z.s. +-- TMA +-- +-- Permission to use, copy, modify, and/or distribute this software +-- for any purpose with or without fee is hereby granted, provided +-- that the above copyright notice and this permission notice appear +-- in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +-- + +-- To require fully-qualified names +SELECT pg_catalog.set_config('search_path', '', false); + +DO $upgrade_block$ +BEGIN + +IF brmbar_privileged.has_exact_schema_version(22) THEN + +CREATE OR REPLACE VIEW brmbar_privileged.debit_balances AS +SELECT + ts.account AS debit_account, + SUM(ts.amount) AS debit_sum +FROM public.transaction_splits ts +WHERE (ts.side = 'debit'::public.transaction_split_side) +GROUP BY ts.account; + +CREATE OR REPLACE VIEW brmbar_privileged.credit_balances AS +SELECT + ts.account AS credit_account, + SUM(ts.amount) AS credit_sum +FROM public.transaction_splits ts +WHERE (ts.side = 'credit'::public.transaction_split_side) +GROUP BY ts.account; + +/* + CASE + WHEN (ts.side = 'credit'::public.transaction_split_side) THEN (- ts.amount) + ELSE ts.amount + END AS amount, + a.currency, + ts.memo + FROM (public.transaction_splits ts + LEFT JOIN public.accounts a ON ((a.id = ts.account))) + ORDER BY ts.id; +*/ + +CREATE OR REPLACE FUNCTION public.inventory_balance() +RETURNS DECIMAL(12,2) +VOLATILE NOT LEAKPROOF LANGUAGE plpgsql AS $fn$ +DECLARE + rv DECIMAL(12,2); +BEGIN + WITH inventory_balances AS ( + SELECT COALESCE(credit_sum, 0) * public.find_buy_rate(a.currency, 1) as credit_sum, + COALESCE(debit_sum, 0) * public.find_buy_rate(a.currency, 1) as debit_sum, + COALESCE(credit_account, debit_account) as cd_account + FROM brmbar_privileged.credit_balances cb + FULL OUTER JOIN brmbar_privileged.debit_balances db + ON (debit_account = credit_account) + LEFT JOIN public.accounts a + ON (a.id = COALESCE(credit_account, debit_account)) + WHERE a.acctype = 'inventory'::public.account_type + ) + SELECT SUM(debit_sum) - SUM(credit_sum) INTO rv + FROM inventory_balances; + + RETURN rv; +END; +$fn$; + + + +PERFORM brmbar_privileged.upgrade_schema_version_to(23); +END IF; + +END; +$upgrade_block$; + +-- vim: set ft=plsql : From 39fe8d97fdab56ab8e0ec3c6253bc9c10a40d4f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Mon, 21 Jul 2025 16:56:50 +0200 Subject: [PATCH 119/139] Use inventory_balance. --- brmbar3/brmbar/Shop.py | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index 4100592..4c32973 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -185,31 +185,11 @@ class Shop: def credit_negbalance_str(self, overflow=None): return self.currency.str(-self.credit_balance(overflow=overflow)) - # XXX causing extra heavy delay ( thousands of extra SQL queries ), disabled def inventory_balance(self): - balance = 0 - # Each inventory account has its own currency, - # so we just do this ugly iteration - cur = self.db.execute_and_fetchall( - "SELECT id FROM accounts WHERE acctype = %s", ["inventory"] - ) - for inventory in cur: - invid = inventory[0] - inv = Account.load(self.db, id=invid) - # FIXME: This is not correct as each instance of inventory - # might have been bought for a different price! Therefore, - # we need to replace the command below with a complex SQL - # statement that will... ugh, accounting is hard! - b = inv.balance() * inv.currency.rates(self.currency)[0] - # if b != 0: - # print(str(b) + ',' + inv.name) - balance += b - return balance + return self.db.execute_and_fetch("SELECT public.inventory_balance()") - # XXX bypass hack def inventory_balance_str(self): - # return self.currency.str(self.inventory_balance()) - return "XXX" + return self.currency.str(self.inventory_balance()) def account_list(self, acctype, like_str="%%"): """list all accounts (people or items, as per acctype)""" From 7a301379f01eb18091b955fc492c7a4773f0cb30 Mon Sep 17 00:00:00 2001 From: TMA Date: Mon, 21 Jul 2025 17:01:35 +0200 Subject: [PATCH 120/139] 0023: security definer --- brmbar3/schema/0023-inventory-balance.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brmbar3/schema/0023-inventory-balance.sql b/brmbar3/schema/0023-inventory-balance.sql index 0feb637..2072812 100644 --- a/brmbar3/schema/0023-inventory-balance.sql +++ b/brmbar3/schema/0023-inventory-balance.sql @@ -61,7 +61,7 @@ GROUP BY ts.account; CREATE OR REPLACE FUNCTION public.inventory_balance() RETURNS DECIMAL(12,2) -VOLATILE NOT LEAKPROOF LANGUAGE plpgsql AS $fn$ +VOLATILE NOT LEAKPROOF LANGUAGE plpgsql SECURITY DEFINER AS $fn$ DECLARE rv DECIMAL(12,2); BEGIN From a1ec2bdb6b1596185473f7586c31a1bb3d720b34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Mon, 21 Jul 2025 17:06:13 +0200 Subject: [PATCH 121/139] Debug raw inventory balance result. --- brmbar3/brmbar/Shop.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index 4c32973..887984a 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -186,7 +186,9 @@ class Shop: return self.currency.str(-self.credit_balance(overflow=overflow)) def inventory_balance(self): - return self.db.execute_and_fetch("SELECT public.inventory_balance()") + res = self.db.execute_and_fetch("SELECT public.inventory_balance()") + logger.debug("inventory_balance res = %s", res) + return res def inventory_balance_str(self): return self.currency.str(self.inventory_balance()) From 8487202b8f6d2fc50df3c9cffc2b89034b217049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Mon, 21 Jul 2025 17:07:50 +0200 Subject: [PATCH 122/139] Record to columns. --- brmbar3/brmbar/Shop.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index 887984a..b32f5bb 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -186,8 +186,9 @@ class Shop: return self.currency.str(-self.credit_balance(overflow=overflow)) def inventory_balance(self): - res = self.db.execute_and_fetch("SELECT public.inventory_balance()") - logger.debug("inventory_balance res = %s", res) + resa = self.db.execute_and_fetch("SELECT * FROM public.inventory_balance()") + res = resa[0] + logger.debug("inventory_balance resa = %s", resa) return res def inventory_balance_str(self): From 70c46bd95093e7217bbc4da16d30bd189c54006f Mon Sep 17 00:00:00 2001 From: TMA Date: Sat, 26 Jul 2025 15:15:03 +0200 Subject: [PATCH 123/139] log all sql statements --- brmbar3/brmbar/Database.py | 1 + 1 file changed, 1 insertion(+) diff --git a/brmbar3/brmbar/Database.py b/brmbar3/brmbar/Database.py index b70687c..5b844dd 100644 --- a/brmbar3/brmbar/Database.py +++ b/brmbar3/brmbar/Database.py @@ -35,6 +35,7 @@ class Database: def _execute(self, cur, query, attrs, level=1): """execute a query, and in case of OperationalError (db restart) reconnect to database. Recurses with increasig pause between tries""" + logger.debug("SQL: (%s) @%s" % (query, time.strftime("%Y%m%d %a %I:%m %p"))) try: if attrs is None: cur.execute(query) From c04d4a1f8e6add5beb3236bc37c88b4310b14e60 Mon Sep 17 00:00:00 2001 From: TMA Date: Sat, 26 Jul 2025 15:50:10 +0200 Subject: [PATCH 124/139] 0024: propagate errors in find_*_rate --- brmbar3/schema/0024-find-rates-fix.sql | 89 ++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 brmbar3/schema/0024-find-rates-fix.sql diff --git a/brmbar3/schema/0024-find-rates-fix.sql b/brmbar3/schema/0024-find-rates-fix.sql new file mode 100644 index 0000000..8dae7d8 --- /dev/null +++ b/brmbar3/schema/0024-find-rates-fix.sql @@ -0,0 +1,89 @@ +-- +-- 0024-find-rates-fix.sql +-- +-- #23 - fix stored functions find_buy_rate and find_sell_rate +-- +-- ISC License +-- +-- Copyright 2023-2025 Brmlab, z.s. +-- TMA +-- +-- Permission to use, copy, modify, and/or distribute this software +-- for any purpose with or without fee is hereby granted, provided +-- that the above copyright notice and this permission notice appear +-- in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +-- + +-- To require fully-qualified names +SELECT pg_catalog.set_config('search_path', '', false); + +DO $upgrade_block$ +BEGIN + +IF brmbar_privileged.has_exact_schema_version(23) THEN + +CREATE OR REPLACE FUNCTION public.find_buy_rate( + IN i_item_currency_id public.accounts.id%TYPE, + IN i_other_currency_id public.accounts.id%TYPE +) RETURNS NUMERIC +LANGUAGE plpgsql +AS $$ +DECLARE + v_rate public.exchange_rates.rate%TYPE; + v_rate_dir public.exchange_rates.rate_dir%TYPE; +BEGIN + SELECT rate, rate_dir INTO STRICT v_rate, v_rate_dir FROM public.exchange_rates WHERE target = i_item_currency_id AND source = i_other_currency_id AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1; + IF v_rate_dir = 'target_to_source'::public.exchange_rate_direction THEN + RETURN v_rate; + ELSE + RETURN 1/v_rate; + END IF; + /* propagate error +EXCEPTION + WHEN NO_DATA_FOUND THEN + RETURN -1; + */ +END; +$$; + +-- return negative number on rate not found +CREATE OR REPLACE FUNCTION public.find_sell_rate( + IN i_item_currency_id public.accounts.id%TYPE, + IN i_other_currency_id public.accounts.id%TYPE +) RETURNS NUMERIC +LANGUAGE plpgsql +AS $$ +DECLARE + v_rate public.exchange_rates.rate%TYPE; + v_rate_dir public.exchange_rates.rate_dir%TYPE; +BEGIN + SELECT rate, rate_dir INTO STRICT v_rate, v_rate_dir FROM public.exchange_rates WHERE target = i_other_currency_id AND source = i_item_currency_id AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1; + IF v_rate_dir = 'source_to_target'::public.exchange_rate_direction THEN + RETURN v_rate; + ELSE + RETURN 1/v_rate; + END IF; + /* propagate error +EXCEPTION + WHEN NO_DATA_FOUND THEN + RETURN -1; + */ +END; +$$; + +PERFORM brmbar_privileged.upgrade_schema_version_to(24); +END IF; + +END; +$upgrade_block$; + +-- vim: set ft=plsql : From c17aa9966695955830846bb84db697f1f166a7ec Mon Sep 17 00:00:00 2001 From: TMA Date: Sat, 26 Jul 2025 16:02:47 +0200 Subject: [PATCH 125/139] 0024: drop old versions before installing new ones --- brmbar3/schema/0024-find-rates-fix.sql | 3 +++ 1 file changed, 3 insertions(+) diff --git a/brmbar3/schema/0024-find-rates-fix.sql b/brmbar3/schema/0024-find-rates-fix.sql index 8dae7d8..a196873 100644 --- a/brmbar3/schema/0024-find-rates-fix.sql +++ b/brmbar3/schema/0024-find-rates-fix.sql @@ -31,6 +31,9 @@ BEGIN IF brmbar_privileged.has_exact_schema_version(23) THEN +DROP FUNCTION IF EXISTS public.find_buy_rate(integer,integer); +DROP FUNCTION IF EXISTS public.find_sell_rate(integer,integer); + CREATE OR REPLACE FUNCTION public.find_buy_rate( IN i_item_currency_id public.accounts.id%TYPE, IN i_other_currency_id public.accounts.id%TYPE From 9afef5bce272f41dc00e4b1d33cc70bd7cdac6ed Mon Sep 17 00:00:00 2001 From: TMA Date: Sat, 26 Jul 2025 16:57:30 +0200 Subject: [PATCH 126/139] maybe make something faster --- brmbar3/brmbar/Shop.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index b32f5bb..a2ef1d1 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -198,12 +198,13 @@ class Shop: """list all accounts (people or items, as per acctype)""" accts = [] cur = self.db.execute_and_fetchall( - "SELECT id FROM accounts WHERE acctype = %s AND name ILIKE %s ORDER BY name ASC", + "SELECT a.id, a.name aname, a.currency, a.acctype, c.name cname FROM accounts a JOIN currencies c ON c.id=a.currency WHERE a.acctype = %s AND a.name ILIKE %s ORDER BY a.name ASC", [acctype, like_str], ) # FIXME: sanitize input like_str ^ for inventory in cur: - accts += [Account.load(self.db, id=inventory[0])] + curr = Currency(db=self.db, id=inventory[2], name=inventory[4]); + accts += [Account(self.db, id=inventory[0], name=inventory[1], currency=curr, acctype=inventory[3])] return accts def fix_inventory(self, item, amount): From 54c687edf3a5b64c11053a690eeed1f8875136d6 Mon Sep 17 00:00:00 2001 From: TMA Date: Sat, 26 Jul 2025 17:12:50 +0200 Subject: [PATCH 127/139] disable slowdown in management for now --- brmbar3/brmbar-gui-qt4.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/brmbar3/brmbar-gui-qt4.py b/brmbar3/brmbar-gui-qt4.py index cb8fa46..4eb668c 100755 --- a/brmbar3/brmbar-gui-qt4.py +++ b/brmbar3/brmbar-gui-qt4.py @@ -168,21 +168,25 @@ class ShopAdapter(QtCore.QObject): @QtCore.Slot(result='QVariant') def balance_cash(self): + return "N/A" balance = shop.cash.balance_str() db.commit() return balance @QtCore.Slot(result='QVariant') def balance_profit(self): + return "N/A" balance = shop.profits.balance_str() db.commit() return balance @QtCore.Slot(result='QVariant') def balance_inventory(self): + return "N/A" balance = shop.inventory_balance_str() db.commit() return balance @QtCore.Slot(result='QVariant') def balance_credit(self): + return "N/A" balance = shop.credit_negbalance_str() db.commit() return balance From 1d9a2e9facb5b35a996015feb10ee864f95b3f3a Mon Sep 17 00:00:00 2001 From: TMA Date: Sat, 26 Jul 2025 18:26:59 +0200 Subject: [PATCH 128/139] Fix buy rate updating --- brmbar3/brmbar/Currency.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brmbar3/brmbar/Currency.py b/brmbar3/brmbar/Currency.py index 3033287..61d6a75 100644 --- a/brmbar3/brmbar/Currency.py +++ b/brmbar3/brmbar/Currency.py @@ -130,5 +130,5 @@ class Currency: def update_buy_rate(self, source, rate): self.db.execute( "SELECT public.update_currency_buy_rate(%s, %s, %s)", - [source.id, self.id, rate], + [self.id, source.id, rate], ) From 2a19bd291f6bf1417290b7185d9a19ae8e9253ab Mon Sep 17 00:00:00 2001 From: TMA Date: Thu, 21 Aug 2025 16:46:33 +0200 Subject: [PATCH 129/139] typo fix --- brmbar3/schema/0024-find-rates-fix.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brmbar3/schema/0024-find-rates-fix.sql b/brmbar3/schema/0024-find-rates-fix.sql index a196873..70bab06 100644 --- a/brmbar3/schema/0024-find-rates-fix.sql +++ b/brmbar3/schema/0024-find-rates-fix.sql @@ -1,7 +1,7 @@ -- -- 0024-find-rates-fix.sql -- --- #23 - fix stored functions find_buy_rate and find_sell_rate +-- #24 - fix stored functions find_buy_rate and find_sell_rate -- -- ISC License -- From 38a4a3def1372eb88dc34278b9375a0d5167250b Mon Sep 17 00:00:00 2001 From: TMA Date: Thu, 21 Aug 2025 16:47:45 +0200 Subject: [PATCH 130/139] 0025: account class loader procedure --- brmbar3/schema/0025-load-account.sql | 109 +++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 brmbar3/schema/0025-load-account.sql diff --git a/brmbar3/schema/0025-load-account.sql b/brmbar3/schema/0025-load-account.sql new file mode 100644 index 0000000..046328c --- /dev/null +++ b/brmbar3/schema/0025-load-account.sql @@ -0,0 +1,109 @@ +-- +-- 0025-load-account.sql +-- +-- #25 - stored procedures for account loading +-- +-- ISC License +-- +-- Copyright 2023-2025 Brmlab, z.s. +-- TMA +-- +-- Permission to use, copy, modify, and/or distribute this software +-- for any purpose with or without fee is hereby granted, provided +-- that the above copyright notice and this permission notice appear +-- in all copies. +-- +-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +-- + +-- To require fully-qualified names +SELECT pg_catalog.set_config('search_path', '', false); + +DO $upgrade_block$ +BEGIN + +IF brmbar_privileged.has_exact_schema_version(24) THEN + +SELECT COUNT(1) INTO v + FROM pg_catalog.pg_type typ + INNER JOIN pg_catalog.pg_namespace nsp + ON nsp.oid = typ.typnamespace + WHERE nsp.nspname = 'brmbar_privileged' + AND typ.typname='account_class_initialization_data_type'; + +IF v>0 THEN + RAISE NOTICE 'Changing type account_class_initialization_data_type'; + DROP TYPE brmbar_privileged.account_class_initialization_data_type CASCADE; +ELSE + RAISE NOTICE 'Creating type account_class_initialization_data_type'; +END IF; + +SELECT COUNT(1) INTO v + FROM pg_catalog.pg_type typ + INNER JOIN pg_catalog.pg_namespace nsp + ON nsp.oid = typ.typnamespace + WHERE nsp.nspname = 'brmbar_privileged' + AND typ.typname='account_load_type'; + +IF v>0 THEN + RAISE NOTICE 'Changing type account_load_type'; + DROP TYPE brmbar_privileged.account_load_type CASCADE; +ELSE + RAISE NOTICE 'Creating type account_load_type'; +END IF; + +CREATE TYPE brmbar_privileged.account_load_type + AS ENUM ('by_id', 'by_barcode'); + +CREATE TYPE brmbar_privileged.account_class_initialization_data_type +AS ( + account_id INTEGER, --public.accounts.id%TYPE, + account_name TEXT, --public.accounts.id%TYPE, + account_acctype brmbar_privileged.accounts_type, + currency_id INTEGER, --public.currencies.id%TYPE, + currency_name TEXT +); + +CREATE OR REPLACE FUNCTION public.account_class_initialization_data( + load_by brmbar_privileged.account_load_type, + i_id INTEGER, + i_barcode TEXT) +RETURNS brmbar_privileged.account_class_initialization_data_type +LANGUAGE plpgsql +AS +$$ +DECLARE + rv brmbar_privileged.account_class_initialization_data_type; +BEGIN + IF load_by = 'by_id' THEN + SELECT a.id, a.name, a.acctype, c.id, c.name + INTO STRICT rv.account_id, rv.account_name, rv.account_acctype, rv.currency_id, rv.currency_name + FROM public.accounts a JOIN public.currencies c ON a.currency = c.id + WHERE a.id = i_id; + ELSE -- by_barcode + SELECT a.id, a.name, a.acctype, c.id, c.name + INTO STRICT rv.account_id, rv.account_name, rv.account_acctype, rv.currency_id, rv.currency_name + FROM public.accounts a JOIN public.currencies c ON a.currency = c.id JOIN public.barcodes b ON b.account = a.id + WHERE b.barcode = i_barcode; + END IF; + + RETURN rv; +END; +$$; + + + +PERFORM brmbar_privileged.upgrade_schema_version_to(25); +END IF; + +END; +$upgrade_block$; + +-- vim: set ft=plsql : From 89fb60bab51ab2510d070f3eb150c338b1908924 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Thu, 21 Aug 2025 16:49:44 +0200 Subject: [PATCH 131/139] Use account loading stored procedure for Account construction. --- brmbar3/brmbar/Account.py | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/brmbar3/brmbar/Account.py b/brmbar3/brmbar/Account.py index 3a25170..f1310da 100644 --- a/brmbar3/brmbar/Account.py +++ b/brmbar3/brmbar/Account.py @@ -20,24 +20,22 @@ class Account: @classmethod def load_by_barcode(cls, db, barcode): logger.debug("load_by_barcode: '%s'", barcode) - res = db.execute_and_fetch( - "SELECT account FROM barcodes WHERE barcode = %s", [barcode] - ) - if res is None: - return None - id = res[0] - return cls.load(db, id=id) + account_id, account_name, account_acctype, currency_id, currency_name = db_execute_and_fetch( + "SELECT account_id, account_name, account_acctype, currency_id, currency_name + FROM public.account_class_initialization_data('by_barcode', NULL, %s)", + [barcode]) + currency = Currency(db, currency_id, currency_name) + return cls(db, account_id, account_name, currency, account_acctype) @classmethod def load(cls, db, id=None): """Constructor for existing account""" - if id is None: - raise NameError("Account.load(): Specify id") - name, currid, acctype = db.execute_and_fetch( - "SELECT name, currency, acctype FROM accounts WHERE id = %s", [id] - ) - currency = Currency.load(db, id=currid) - return cls(db, name=name, id=id, currency=currency, acctype=acctype) + account_id, account_name, account_acctype, currency_id, currency_name = db_execute_and_fetch( + "SELECT account_id, account_name, account_acctype, currency_id, currency_name + FROM public.account_class_initialization_data('by_id', %s, NULL)", + [id]) + currency = Currency(db, currency_id, currency_name) + return cls(db, account_id, account_name, currency, account_acctype) @classmethod def create(cls, db, name, currency, acctype): From c2ddab9ce824f677319c17d7bc581ca47a678cf9 Mon Sep 17 00:00:00 2001 From: TMA Date: Thu, 21 Aug 2025 16:54:56 +0200 Subject: [PATCH 132/139] 0025: declare v --- brmbar3/schema/0025-load-account.sql | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/brmbar3/schema/0025-load-account.sql b/brmbar3/schema/0025-load-account.sql index 046328c..6a83fec 100644 --- a/brmbar3/schema/0025-load-account.sql +++ b/brmbar3/schema/0025-load-account.sql @@ -27,6 +27,8 @@ SELECT pg_catalog.set_config('search_path', '', false); DO $upgrade_block$ +DECLARE + v INTEGER; BEGIN IF brmbar_privileged.has_exact_schema_version(24) THEN @@ -93,7 +95,7 @@ BEGIN FROM public.accounts a JOIN public.currencies c ON a.currency = c.id JOIN public.barcodes b ON b.account = a.id WHERE b.barcode = i_barcode; END IF; - + -- RETURN rv; END; $$; From 1e324c092060c82ed8637d8ef60b0a0c4b42025f Mon Sep 17 00:00:00 2001 From: TMA Date: Thu, 21 Aug 2025 16:58:59 +0200 Subject: [PATCH 133/139] 0025: it's public.account_type --- brmbar3/schema/0025-load-account.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brmbar3/schema/0025-load-account.sql b/brmbar3/schema/0025-load-account.sql index 6a83fec..efc2a8d 100644 --- a/brmbar3/schema/0025-load-account.sql +++ b/brmbar3/schema/0025-load-account.sql @@ -68,7 +68,7 @@ CREATE TYPE brmbar_privileged.account_class_initialization_data_type AS ( account_id INTEGER, --public.accounts.id%TYPE, account_name TEXT, --public.accounts.id%TYPE, - account_acctype brmbar_privileged.accounts_type, + account_acctype public.accounts_type, currency_id INTEGER, --public.currencies.id%TYPE, currency_name TEXT ); From 57f36dd2f502934e5c0349dc7f066ed2bef0ae7f Mon Sep 17 00:00:00 2001 From: TMA Date: Thu, 21 Aug 2025 17:01:57 +0200 Subject: [PATCH 134/139] 0025: it's public.account_type, FRFR --- brmbar3/schema/0025-load-account.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brmbar3/schema/0025-load-account.sql b/brmbar3/schema/0025-load-account.sql index efc2a8d..3076e0b 100644 --- a/brmbar3/schema/0025-load-account.sql +++ b/brmbar3/schema/0025-load-account.sql @@ -68,7 +68,7 @@ CREATE TYPE brmbar_privileged.account_class_initialization_data_type AS ( account_id INTEGER, --public.accounts.id%TYPE, account_name TEXT, --public.accounts.id%TYPE, - account_acctype public.accounts_type, + account_acctype public.account_type, currency_id INTEGER, --public.currencies.id%TYPE, currency_name TEXT ); From dd8c93e967436affdb9fbc1f52fbad7c4eae9874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Thu, 21 Aug 2025 17:10:03 +0200 Subject: [PATCH 135/139] Fix string literal. --- brmbar3/brmbar/Account.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/brmbar3/brmbar/Account.py b/brmbar3/brmbar/Account.py index f1310da..b5b69a4 100644 --- a/brmbar3/brmbar/Account.py +++ b/brmbar3/brmbar/Account.py @@ -21,8 +21,7 @@ class Account: def load_by_barcode(cls, db, barcode): logger.debug("load_by_barcode: '%s'", barcode) account_id, account_name, account_acctype, currency_id, currency_name = db_execute_and_fetch( - "SELECT account_id, account_name, account_acctype, currency_id, currency_name - FROM public.account_class_initialization_data('by_barcode', NULL, %s)", + "SELECT account_id, account_name, account_acctype, currency_id, currency_name FROM public.account_class_initialization_data('by_barcode', NULL, %s)", [barcode]) currency = Currency(db, currency_id, currency_name) return cls(db, account_id, account_name, currency, account_acctype) @@ -31,8 +30,7 @@ class Account: def load(cls, db, id=None): """Constructor for existing account""" account_id, account_name, account_acctype, currency_id, currency_name = db_execute_and_fetch( - "SELECT account_id, account_name, account_acctype, currency_id, currency_name - FROM public.account_class_initialization_data('by_id', %s, NULL)", + "SELECT account_id, account_name, account_acctype, currency_id, currency_name FROM public.account_class_initialization_data('by_id', %s, NULL)", [id]) currency = Currency(db, currency_id, currency_name) return cls(db, account_id, account_name, currency, account_acctype) From e604e0000b0bcb188ba96078096a70697deb2951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Thu, 21 Aug 2025 17:12:14 +0200 Subject: [PATCH 136/139] Fix db. --- brmbar3/brmbar/Account.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/brmbar3/brmbar/Account.py b/brmbar3/brmbar/Account.py index b5b69a4..2499d05 100644 --- a/brmbar3/brmbar/Account.py +++ b/brmbar3/brmbar/Account.py @@ -20,7 +20,7 @@ class Account: @classmethod def load_by_barcode(cls, db, barcode): logger.debug("load_by_barcode: '%s'", barcode) - account_id, account_name, account_acctype, currency_id, currency_name = db_execute_and_fetch( + account_id, account_name, account_acctype, currency_id, currency_name = db.execute_and_fetch( "SELECT account_id, account_name, account_acctype, currency_id, currency_name FROM public.account_class_initialization_data('by_barcode', NULL, %s)", [barcode]) currency = Currency(db, currency_id, currency_name) @@ -29,7 +29,7 @@ class Account: @classmethod def load(cls, db, id=None): """Constructor for existing account""" - account_id, account_name, account_acctype, currency_id, currency_name = db_execute_and_fetch( + account_id, account_name, account_acctype, currency_id, currency_name = db.execute_and_fetch( "SELECT account_id, account_name, account_acctype, currency_id, currency_name FROM public.account_class_initialization_data('by_id', %s, NULL)", [id]) currency = Currency(db, currency_id, currency_name) From aee3a1d24f1ae93b24fc2b110d28916684396bf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Thu, 21 Aug 2025 17:12:22 +0200 Subject: [PATCH 137/139] Fix db 2. --- brmbar3/brmbar/Account.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/brmbar3/brmbar/Account.py b/brmbar3/brmbar/Account.py index 2499d05..e54ea8d 100644 --- a/brmbar3/brmbar/Account.py +++ b/brmbar3/brmbar/Account.py @@ -20,7 +20,7 @@ class Account: @classmethod def load_by_barcode(cls, db, barcode): logger.debug("load_by_barcode: '%s'", barcode) - account_id, account_name, account_acctype, currency_id, currency_name = db.execute_and_fetch( + account_id, account_name, account_acctype, currency_id, currency_name = self.db.execute_and_fetch( "SELECT account_id, account_name, account_acctype, currency_id, currency_name FROM public.account_class_initialization_data('by_barcode', NULL, %s)", [barcode]) currency = Currency(db, currency_id, currency_name) @@ -29,7 +29,7 @@ class Account: @classmethod def load(cls, db, id=None): """Constructor for existing account""" - account_id, account_name, account_acctype, currency_id, currency_name = db.execute_and_fetch( + account_id, account_name, account_acctype, currency_id, currency_name = self.db.execute_and_fetch( "SELECT account_id, account_name, account_acctype, currency_id, currency_name FROM public.account_class_initialization_data('by_id', %s, NULL)", [id]) currency = Currency(db, currency_id, currency_name) From 92deae67753fa935fb367ea3d459424cab485f2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Thu, 21 Aug 2025 17:13:30 +0200 Subject: [PATCH 138/139] Fix db 3. --- brmbar3/brmbar/Account.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/brmbar3/brmbar/Account.py b/brmbar3/brmbar/Account.py index e54ea8d..2499d05 100644 --- a/brmbar3/brmbar/Account.py +++ b/brmbar3/brmbar/Account.py @@ -20,7 +20,7 @@ class Account: @classmethod def load_by_barcode(cls, db, barcode): logger.debug("load_by_barcode: '%s'", barcode) - account_id, account_name, account_acctype, currency_id, currency_name = self.db.execute_and_fetch( + account_id, account_name, account_acctype, currency_id, currency_name = db.execute_and_fetch( "SELECT account_id, account_name, account_acctype, currency_id, currency_name FROM public.account_class_initialization_data('by_barcode', NULL, %s)", [barcode]) currency = Currency(db, currency_id, currency_name) @@ -29,7 +29,7 @@ class Account: @classmethod def load(cls, db, id=None): """Constructor for existing account""" - account_id, account_name, account_acctype, currency_id, currency_name = self.db.execute_and_fetch( + account_id, account_name, account_acctype, currency_id, currency_name = db.execute_and_fetch( "SELECT account_id, account_name, account_acctype, currency_id, currency_name FROM public.account_class_initialization_data('by_id', %s, NULL)", [id]) currency = Currency(db, currency_id, currency_name) From f6128ecc4a9d2f487843cbc70e074188b981b380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20Pant=C5=AF=C4=8Dek?= Date: Thu, 28 Aug 2025 16:58:33 +0200 Subject: [PATCH 139/139] Currency.py: remove unused methods - rates2, convert --- brmbar3/brmbar/Currency.py | 51 -------------------------------------- 1 file changed, 51 deletions(-) diff --git a/brmbar3/brmbar/Currency.py b/brmbar3/brmbar/Currency.py index 61d6a75..fba3059 100644 --- a/brmbar3/brmbar/Currency.py +++ b/brmbar3/brmbar/Currency.py @@ -67,57 +67,6 @@ class Currency: return (buy, sell) - def rates2(self, other): - # the original code for compare testing - res = self.db.execute_and_fetch( - "SELECT rate, rate_dir FROM exchange_rates WHERE target = %s AND source = %s AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1", - [self.id, other.id], - ) - if res is None: - raise NameError( - "Currency.rate(): Unknown conversion " - + other.name() - + " to " - + self.name() - ) - buy_rate, buy_rate_dir = res - buy = buy_rate if buy_rate_dir == "target_to_source" else 1 / buy_rate - - res = self.db.execute_and_fetch( - "SELECT rate, rate_dir FROM exchange_rates WHERE target = %s AND source = %s AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1", - [other.id, self.id], - ) - if res is None: - raise NameError( - "Currency.rate(): Unknown conversion " - + self.name() - + " to " - + other.name() - ) - sell_rate, sell_rate_dir = res - sell = sell_rate if sell_rate_dir == "source_to_target" else 1 / sell_rate - - return (buy, sell) - - def convert(self, amount, target): - res = self.db.execute_and_fetch( - "SELECT rate, rate_dir FROM exchange_rates WHERE target = %s AND source = %s AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1", - [target.id, self.id], - ) - if res is None: - raise NameError( - "Currency.convert(): Unknown conversion " - + self.name() - + " to " - + target.name() - ) - rate, rate_dir = res - if rate_dir == "source_to_target": - resamount = amount * rate - else: - resamount = amount / rate - return resamount - def str(self, amount): return "{:.2f} {}".format(amount, self.name)