diff --git a/brmbar3/autostock.py b/brmbar3/autostock.py index 23d1e8d..0e8afac 100755 --- a/brmbar3/autostock.py +++ b/brmbar3/autostock.py @@ -4,7 +4,6 @@ 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") @@ -39,7 +38,5 @@ 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 a0545ab..cc5c6e3 100755 --- a/brmbar3/brmbar-cli.py +++ b/brmbar3/brmbar-cli.py @@ -6,8 +6,6 @@ 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-gui-qt4.py b/brmbar3/brmbar-gui-qt4.py index cb8fa46..62e4c4a 100755 --- a/brmbar3/brmbar-gui-qt4.py +++ b/brmbar3/brmbar-gui-qt4.py @@ -150,16 +150,11 @@ 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() - csfa = currency.str(float(amount)) - logger.debug(" csfa = '%s'", csfa) - return csfa + return currency.str(float(amount)) @QtCore.Slot('QVariant', result='QVariant') def balance_user(self, userid): diff --git a/brmbar3/brmbar-tui.py b/brmbar3/brmbar-tui.py index a2dc10b..00bdf44 100755 --- a/brmbar3/brmbar-tui.py +++ b/brmbar3/brmbar-tui.py @@ -6,9 +6,6 @@ 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 522e9ed..5d84378 100755 --- a/brmbar3/brmbar-web.py +++ b/brmbar3/brmbar-web.py @@ -6,9 +6,6 @@ from brmbar import Database import brmbar -print("!!! THIS PROGRAM NO LONGER WORKS !!!") -sys.exit(1) - from flask import * app = Flask(__name__) #app.debug = True diff --git a/brmbar3/brmbar/Account.py b/brmbar3/brmbar/Account.py index 3a25170..ef7a494 100644 --- a/brmbar3/brmbar/Account.py +++ b/brmbar3/brmbar/Account.py @@ -1,15 +1,12 @@ 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 @@ -20,38 +17,48 @@ 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): - """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) + 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) @classmethod def create(cls, db, name, currency, 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) + """ 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) 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()) @@ -59,12 +66,22 @@ 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( - "SELECT public.add_barcode_to_account(%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): + # 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 3033287..29da4a3 100644 --- a/brmbar3/brmbar/Currency.py +++ b/brmbar3/brmbar/Currency.py @@ -1,12 +1,10 @@ # 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 @@ -14,103 +12,72 @@ 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): - """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) + 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) @classmethod def create(cls, db, name): - """Constructor for new currency""" + """ Constructor for new currency """ + # 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]) - return cls(db, id=id, name=name) + # id = id[0] + return cls(db, name = name, id = id) 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 @@ -122,13 +89,10 @@ class Currency: return "{:.2f} {}".format(amount, self.name) def update_sell_rate(self, target, rate): - self.db.execute( - "SELECT public.update_currency_sell_rate(%s, %s, %s)", - [self.id, target.id, 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]) 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.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/brmbar/Database.py b/brmbar3/brmbar/Database.py index b70687c..6b985eb 100644 --- a/brmbar3/brmbar/Database.py +++ b/brmbar3/brmbar/Database.py @@ -4,29 +4,26 @@ 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) @@ -41,32 +38,27 @@ 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 + print("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) + print("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() def commit(self): """passes commit to db""" diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index b32f5bb..7a64274 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -2,163 +2,158 @@ 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_id, profits_id, cash_id, excess_id, deficit_id): - # Keep db as-is + of the frontend scripts. """ + def __init__(self, db, currency, profits, cash, excess, deficit): self.db = db - - # 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) + 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) @classmethod def new_with_defaults(cls, db): - # 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_id=profits_id, - cash_id=cash_id, - excess_id=excess_id, - deficit_id=deficit_id, - ) + 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")) - 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), - ], - ) + [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[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) - def sell_for_cash(self, item, amount=1): + #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): 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] + [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() 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) - def undo_sale(self, item, user, amount=1): + #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) # 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] + [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() + 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], + "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): - iamount = int(amount) - famount = float(iamount) - assert famount == amount, "amount is not integer value %s".format(amount) + 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, iamount, self.currency.id, item.name], + [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 + + #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], + "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): - 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 @@ -171,78 +166,138 @@ 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 def inventory_balance(self): - resa = self.db.execute_and_fetch("SELECT * FROM public.inventory_balance()") - res = resa[0] - logger.debug("inventory_balance resa = %s", resa) - return res + 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 +# XXX bypass hack def inventory_balance_str(self): - 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)""" 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): - transaction = self.db.execute_and_fetch( - "SELECT public.undo_transaction(%s)", [oldtid] - )[0] + #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] self.db.commit() return transaction diff --git a/brmbar3/schema/0015-shop-buy-for-cash.sql b/brmbar3/schema/0015-shop-buy-for-cash.sql index aa66260..5cf12bb 100644 --- a/brmbar3/schema/0015-shop-buy-for-cash.sql +++ b/brmbar3/schema/0015-shop-buy-for-cash.sql @@ -44,15 +44,12 @@ DECLARE v_buy_rate NUMERIC; v_cost NUMERIC; v_transaction_id public.transactions.id%TYPE; - v_item_currency_id public.accounts.currency%TYPE; BEGIN - -- 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); + -- 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; @@ -63,12 +60,12 @@ BEGIN -- the item INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) - VALUES (v_transaction_id, 'debit', i_item_id, i_amount, + VALUES (i_transaction_id, 'debit', i_item_id, i_amount, 'Cash'); -- the cash INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) - VALUES (v_transaction_id, 'credit', i_cash_account_id, v_cost, + VALUES (i_transaction_id, 'credit', i_cash_account_id, v_cost, i_item_name); -- Return the cost diff --git a/brmbar3/schema/0016-shop-receipt-to-credit.sql b/brmbar3/schema/0016-shop-receipt-to-credit.sql index db77f7a..21af8b1 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 (v_transaction_id, 'credit', i_profits_id, i_amount, i_user_name); + 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 (v_transaction_id, 'credit', i_user_id, i_amount, 'Credit from receipt: ' || i_description); + VALUES (i_transaction_id, 'credit', i_user_id, i_amount, 'Credit from receipt: ' || i_description); END; $$; diff --git a/brmbar3/schema/0021-constraints-on-numeric-columns.sql b/brmbar3/schema/0021-constraints-on-numeric-columns.sql deleted file mode 100644 index 524f08a..0000000 --- a/brmbar3/schema/0021-constraints-on-numeric-columns.sql +++ /dev/null @@ -1,48 +0,0 @@ --- --- 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 : - diff --git a/brmbar3/schema/0022-shop-init.sql b/brmbar3/schema/0022-shop-init.sql deleted file mode 100644 index 55fe098..0000000 --- a/brmbar3/schema/0022-shop-init.sql +++ /dev/null @@ -1,85 +0,0 @@ --- --- 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 CASCADE; -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 : - diff --git a/brmbar3/schema/0023-inventory-balance.sql b/brmbar3/schema/0023-inventory-balance.sql deleted file mode 100644 index 2072812..0000000 --- a/brmbar3/schema/0023-inventory-balance.sql +++ /dev/null @@ -1,94 +0,0 @@ --- --- 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 SECURITY DEFINER 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 :