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 01/21] 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 02/21] 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 03/21] 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 04/21] 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 05/21] 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 06/21] 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 07/21] 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 08/21] 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 09/21] 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 10/21] 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 11/21] 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 12/21] 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 13/21] 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 14/21] 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 15/21] 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 16/21] 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 17/21] 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 18/21] 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 19/21] 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 20/21] 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 21/21] 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):