Compare commits

..

No commits in common. "master" and "sql-refactor" have entirely different histories.

35 changed files with 260 additions and 2604 deletions

View file

@ -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()

View file

@ -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 <pasky@ucw.cz> 2012-2013

View file

@ -9,20 +9,6 @@ from brmbar import Database
import brmbar
import argparse
import logging
root = logging.getLogger()
root.setLevel(logging.DEBUG)
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
root.addHandler(handler)
logger = logging.getLogger(__name__)
# User credit balance limit; sale will fail when balance is below this limit.
LIMIT_BALANCE = -200
# When below this credit balance, an alert hook script (see below) is run.
@ -67,9 +53,7 @@ class ShopAdapter(QtCore.QObject):
def acct_map(self, acct):
if acct is None:
logger.debug("acct_map: acct is None")
return None
logger.debug("acct_map: acct.acctype=%s", acct.acctype)
if acct.acctype == 'debt':
return self.acct_debt_map(acct)
elif acct.acctype == "inventory":
@ -88,14 +72,12 @@ class ShopAdapter(QtCore.QObject):
Therefore, we construct a map that we can pass around easily.
We return None on unrecognized barcode. """
barcode = str(barcode)
logger.debug("barcodeInput: barcode='%s'", barcode)
if barcode and barcode[0] == "$":
credits = {'$02': 20, '$05': 50, '$10': 100, '$20': 200, '$50': 500, '$1k': 1000}
credit = credits[barcode]
if credit is None:
return None
return { "acctype": "recharge", "amount": str(credit)+".00" }
logger.debug("barcodeInput: before load_by_barcode")
acct = self.acct_map(brmbar.Account.load_by_barcode(db, barcode))
db.commit()
return acct
@ -150,16 +132,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):
@ -168,25 +145,21 @@ class ShopAdapter(QtCore.QObject):
@QtCore.Slot(result='QVariant')
def balance_cash(self):
return "N/A"
balance = shop.cash.balance_str()
db.commit()
return balance
@QtCore.Slot(result='QVariant')
def balance_profit(self):
return "N/A"
balance = shop.profits.balance_str()
db.commit()
return balance
@QtCore.Slot(result='QVariant')
def balance_inventory(self):
return "N/A"
balance = shop.inventory_balance_str()
db.commit()
return balance
@QtCore.Slot(result='QVariant')
def balance_credit(self):
return "N/A"
balance = shop.credit_negbalance_str()
db.commit()
return balance
@ -251,23 +224,7 @@ class ShopAdapter(QtCore.QObject):
db.commit()
return balance
parser = argparse.ArgumentParser()
parser.add_argument("--dbname", help="Database name", type=str)
parser.add_argument("--dbuser", help="Database user", type=str)
parser.add_argument("--dbhost", help="Database host", type=str)
parser.add_argument("--dbpass", help="Database user password", type=str)
args = parser.parse_args()
argdbname = args.dbname
argdbuser = args.dbuser
argdbhost = args.dbhost
argdbpass = args.dbpass
db = Database.Database(
"dbname={0} user={1} host={2} password={3}".format(
argdbname,argdbuser,argdbhost,argdbpass
)
)
db = Database.Database("dbname=brmbar")
shop = brmbar.Shop.new_with_defaults(db)
currency = shop.currency
db.commit()

View file

@ -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

View file

@ -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

View file

@ -1,15 +1,10 @@
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
@ -19,35 +14,42 @@ class Account:
@classmethod
def load_by_barcode(cls, db, barcode):
logger.debug("load_by_barcode: '%s'", barcode)
account_id, account_name, account_acctype, currency_id, currency_name = db.execute_and_fetch(
"SELECT account_id, account_name, account_acctype, currency_id, currency_name FROM public.account_class_initialization_data('by_barcode', NULL, %s)",
[barcode])
currency = Currency(db, currency_id, currency_name)
return cls(db, account_id, account_name, currency, account_acctype)
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)
@classmethod
def load(cls, db, id=None):
"""Constructor for existing account"""
account_id, account_name, account_acctype, currency_id, currency_name = db.execute_and_fetch(
"SELECT account_id, account_name, account_acctype, currency_id, currency_name FROM public.account_class_initialization_data('by_id', %s, NULL)",
[id])
currency = Currency(db, currency_id, currency_name)
return cls(db, account_id, account_name, currency, account_acctype)
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 = 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]
)[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())
@ -55,12 +57,20 @@ 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.commit()
def rename(self, name):
self.db.execute("SELECT public.rename_account(%s, %s)", [self.id, name])
self.db.execute("UPDATE accounts SET name = %s WHERE id = %s", [name, self.id])
self.name = name

View file

@ -1,12 +1,10 @@
# vim: set fileencoding=utf8
class Currency:
"""Currency
""" Currency
Each account has a currency (1 , 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,70 +12,63 @@ class Currency:
@classmethod
def default(cls, db):
"""Default wallet currency"""
return cls.load(db, name="")
""" Default wallet currency """
return cls.load(db, name = "")
@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"""
id = db.execute_and_fetch("SELECT public.create_currency(%s)", [name])
return cls(db, id=id, name=name)
""" Constructor for new currency """
id = db.execute_and_fetch("INSERT INTO currencies (name) VALUES (%s) RETURNING id", [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)"""
# buy rate
res = self.db.execute_and_fetch(
"SELECT public.find_buy_rate(%s, %s)", [self.id, other.id]
)
$sell is the price of $self in means of $other when selling it (from brmbar) """
res = self.db.execute_and_fetch("SELECT rate, rate_dir FROM exchange_rates WHERE target = %s AND source = %s AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1", [self.id, other.id])
if res is None:
raise NameError("Something fishy in find_buy_rate.")
buy = res[0]
if buy < 0:
raise NameError(
"Currency.rate(): Unknown conversion "
+ other.name()
+ " to "
+ self.name()
)
# sell rate
res = self.db.execute_and_fetch(
"SELECT public.find_sell_rate(%s, %s)", [self.id, other.id]
)
raise NameError("Currency.rate(): Unknown conversion " + other.name() + " to " + self.name())
buy_rate, buy_rate_dir = res
buy = buy_rate if buy_rate_dir == "target_to_source" else 1/buy_rate
res = self.db.execute_and_fetch("SELECT rate, rate_dir FROM exchange_rates WHERE target = %s AND source = %s AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1", [other.id, self.id])
if res is None:
raise NameError("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())
sell_rate, sell_rate_dir = res
sell = sell_rate if sell_rate_dir == "source_to_target" else 1/sell_rate
return (buy, sell)
def convert(self, amount, target):
res = self.db.execute_and_fetch("SELECT rate, rate_dir FROM exchange_rates WHERE target = %s AND source = %s AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1", [target.id, self.id])
if res is None:
raise NameError("Currency.convert(): Unknown conversion " + self.name() + " to " + target.name())
rate, rate_dir = res
if rate_dir == "source_to_target":
resamount = amount * rate
else:
resamount = amount / rate
return resamount
def str(self, amount):
return "{:.2f} {}".format(amount, self.name)
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"])
def update_buy_rate(self, source, rate):
self.db.execute(
"SELECT public.update_currency_buy_rate(%s, %s, %s)",
[self.id, source.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"])

View file

@ -3,30 +3,25 @@
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)
@ -35,39 +30,31 @@ class Database:
def _execute(self, cur, query, attrs, level=1):
"""execute a query, and in case of OperationalError (db restart)
reconnect to database. Recurses with increasig pause between tries"""
logger.debug("SQL: (%s) @%s" % (query, time.strftime("%Y%m%d %a %I:%m %p")))
try:
if attrs is None:
cur.execute(query)
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)
except Exception as ex:
logger.debug("_execute exception: %s", ex)
self.db_conn.rollback()
cur = self.db_conn.cursor() #how ugly is this?
return self._execute(cur, query, attrs, level+1)
def commit(self):
"""passes commit to db"""

View file

@ -1,164 +1,108 @@
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):
# 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,
)
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),
],
)
logger.debug("sell: res[0]=%s", res[0])
cost = res[0]
def sell(self, item, user, amount = 1):
# Sale: Currency conversion from item currency to shop currency
(buy, sell) = item.currency.rates(self.currency)
cost = amount * sell
profit = amount * (sell - buy)
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]
def sell_for_cash(self, item, amount = 1):
# Sale: Currency conversion from item currency to shop currency
(buy, sell) = item.currency.rates(self.currency)
cost = amount * sell
profit = amount * (sell - buy)
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):
# 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]
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)
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],
)
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],
)
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.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)
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],
)[0]
def buy_for_cash(self, item, amount = 1):
# 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):
self.db.execute_and_fetch(
"SELECT public.receipt_reimbursement(%s, %s, %s, %s, %s)",
[self.profits.id, user.id, user.name, credit, description],
)[0]
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.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,79 +115,119 @@ 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 a.id, a.name aname, a.currency, a.acctype, c.name cname FROM accounts a JOIN currencies c ON c.id=a.currency WHERE a.acctype = %s AND a.name ILIKE %s ORDER BY a.name ASC",
[acctype, like_str],
)
# FIXME: sanitize input like_str ^
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:
curr = Currency(db=self.db, id=inventory[2], name=inventory[4]);
accts += [Account(self.db, id=inventory[0], name=inventory[1], currency=curr, acctype=inventory[3])]
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,
],
)[0]
amount_in_reality = amount
amount_in_system = item.balance()
(buy, sell) = item.currency.rates(self.currency)
self.db.commit()
return rv
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],
)[0]
amount_in_reality = amount
amount_in_system = self.cash.balance()
self.db.commit()
return rv
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],
)[0]
if msg != None:
print(msg)
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.")
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("CALL undo_transaction(%s)",[oldtid])[0]
self.db.commit()
return transaction

View file

@ -1,313 +0,0 @@
--
-- 0001-init.sql
--
-- Initial SQL schema construction as of 2025-04-20 (or so)
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- Dominik Pantůček <dominik.pantucek@trustica.cz>
--
-- Permission to use, copy, modify, and/or distribute this software
-- for any purpose with or without fee is hereby granted, provided
-- that the above copyright notice and this permission notice appear
-- in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--
-- To require fully-qualified names
SELECT pg_catalog.set_config('search_path', '', false);
-- Privileged schema with protected data
CREATE SCHEMA IF NOT EXISTS brmbar_privileged;
-- Initial versioning
CREATE TABLE IF NOT EXISTS brmbar_privileged.brmbar_schema(
ver INTEGER NOT NULL
);
-- ----------------------------------------------------------------
-- Legacy Schema Initialization
-- ----------------------------------------------------------------
DO $$
DECLARE v INTEGER;
BEGIN
SELECT ver FROM brmbar_privileged.brmbar_schema INTO v;
IF v IS NULL THEN
-- --------------------------------
-- Legacy Types
SELECT COUNT(*) INTO v
FROM pg_catalog.pg_type typ
INNER JOIN pg_catalog.pg_namespace nsp
ON nsp.oid = typ.typnamespace
WHERE nsp.nspname = 'public'
AND typ.typname='exchange_rate_direction';
IF v=0 THEN
RAISE NOTICE 'Creating type exchange_rate_direction';
CREATE TYPE public.exchange_rate_direction
AS ENUM ('source_to_target', 'target_to_source');
ELSE
RAISE NOTICE 'Type exchange_rate_direction already exists';
END IF;
SELECT COUNT(*) INTO v
FROM pg_catalog.pg_type typ
INNER JOIN pg_catalog.pg_namespace nsp
ON nsp.oid = typ.typnamespace
WHERE nsp.nspname = 'public'
AND typ.typname='account_type';
IF v=0 THEN
RAISE NOTICE 'Creating type account_type';
CREATE TYPE public.account_type
AS ENUM ('cash', 'debt', 'inventory', 'income', 'expense',
'starting_balance', 'ending_balance');
ELSE
RAISE NOTICE 'Type account_type already exists';
END IF;
SELECT COUNT(*) INTO v
FROM pg_catalog.pg_type typ
INNER JOIN pg_catalog.pg_namespace nsp
ON nsp.oid = typ.typnamespace
WHERE nsp.nspname = 'public'
AND typ.typname='transaction_split_side';
IF v=0 THEN
RAISE NOTICE 'Creating type transaction_split_side';
CREATE TYPE public.transaction_split_side
AS ENUM ('credit', 'debit');
ELSE
RAISE NOTICE 'Type transaction_split_side already exists';
END IF;
-- --------------------------------
-- Currencies sequence, table and potential initial data
CREATE SEQUENCE IF NOT EXISTS public.currencies_id_seq
START WITH 2 INCREMENT BY 1;
CREATE TABLE IF NOT EXISTS public.currencies (
id INTEGER PRIMARY KEY NOT NULL DEFAULT NEXTVAL('public.currencies_id_seq'::regclass),
name VARCHAR(128) NOT NULL,
UNIQUE(name)
);
INSERT INTO public.currencies (id, name) VALUES (1, '')
ON CONFLICT DO NOTHING;
-- --------------------------------
-- Exchange rates table - no initial data required
CREATE TABLE IF NOT EXISTS public.exchange_rates (
valid_since TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL,
target INTEGER NOT NULL,
FOREIGN KEY (target) REFERENCES public.currencies (id),
source INTEGER NOT NULL,
FOREIGN KEY (source) REFERENCES public.currencies (id),
rate DECIMAL(12,2) NOT NULL,
rate_dir public.exchange_rate_direction NOT NULL
);
-- --------------------------------
-- Accounts sequence and table and 4 initial accounts
CREATE SEQUENCE IF NOT EXISTS public.accounts_id_seq
START WITH 2 INCREMENT BY 1;
CREATE TABLE IF NOT EXISTS public.accounts (
id INTEGER PRIMARY KEY NOT NULL DEFAULT NEXTVAL('public.accounts_id_seq'::regclass),
name VARCHAR(128) NOT NULL,
UNIQUE (name),
currency INTEGER NOT NULL,
FOREIGN KEY (currency) REFERENCES public.currencies (id),
acctype public.account_type NOT NULL,
active BOOLEAN NOT NULL DEFAULT TRUE
);
INSERT INTO public.accounts (id, name, currency, acctype)
VALUES (1, 'BrmBar Cash', (SELECT id FROM public.currencies WHERE name=''), 'cash')
ON CONFLICT DO NOTHING;
INSERT INTO public.accounts (name, currency, acctype)
VALUES ('BrmBar Profits', (SELECT id FROM public.currencies WHERE name=''), 'income')
ON CONFLICT DO NOTHING;
INSERT INTO public.accounts (name, currency, acctype)
VALUES ('BrmBar Excess', (SELECT id FROM public.currencies WHERE name=''), 'income')
ON CONFLICT DO NOTHING;
INSERT INTO public.accounts (name, currency, acctype)
VALUES ('BrmBar Deficit', (SELECT id FROM public.currencies WHERE name=''), 'expense')
ON CONFLICT DO NOTHING;
-- --------------------------------
-- Barcodes
CREATE TABLE IF NOT EXISTS public.barcodes (
barcode VARCHAR(128) PRIMARY KEY NOT NULL,
account INTEGER NOT NULL,
FOREIGN KEY (account) REFERENCES public.accounts (id)
);
INSERT INTO public.barcodes (barcode, account)
VALUES ('_cash_', (SELECT id FROM public.accounts WHERE acctype = 'cash'))
ON CONFLICT DO NOTHING;
-- --------------------------------
-- Transactions
CREATE SEQUENCE IF NOT EXISTS public.transactions_id_seq
START WITH 1 INCREMENT BY 1;
CREATE TABLE IF NOT EXISTS public.transactions (
id INTEGER PRIMARY KEY NOT NULL DEFAULT NEXTVAL('public.transactions_id_seq'::regclass),
time TIMESTAMP DEFAULT NOW() NOT NULL,
responsible INTEGER,
FOREIGN KEY (responsible) REFERENCES public.accounts (id),
description TEXT
);
-- --------------------------------
-- Transaction splits
CREATE SEQUENCE IF NOT EXISTS public.transaction_splits_id_seq
START WITH 1 INCREMENT BY 1;
CREATE TABLE IF NOT EXISTS public.transaction_splits (
id INTEGER PRIMARY KEY NOT NULL DEFAULT NEXTVAL('public.transaction_splits_id_seq'::regclass),
transaction INTEGER NOT NULL,
FOREIGN KEY (transaction) REFERENCES public.transactions (id),
side public.transaction_split_side NOT NULL,
account INTEGER NOT NULL,
FOREIGN KEY (account) REFERENCES public.accounts (id),
amount DECIMAL(12,2) NOT NULL,
memo TEXT
);
-- --------------------------------
-- Account balances view
CREATE OR REPLACE VIEW public.account_balances AS
SELECT ts.account AS id,
accounts.name,
accounts.acctype,
- sum(
CASE
WHEN ts.side = 'credit'::public.transaction_split_side THEN - ts.amount
ELSE ts.amount
END) AS crbalance
FROM public.transaction_splits ts
LEFT JOIN public.accounts ON accounts.id = ts.account
GROUP BY ts.account, accounts.name, accounts.acctype
ORDER BY (- sum(
CASE
WHEN ts.side = 'credit'::public.transaction_split_side THEN - ts.amount
ELSE ts.amount
END));
-- --------------------------------
-- Transaction nice splits view
CREATE OR REPLACE VIEW public.transaction_nicesplits AS
SELECT ts.id,
ts.transaction,
ts.account,
CASE
WHEN ts.side = 'credit'::public.transaction_split_side THEN - ts.amount
ELSE ts.amount
END AS amount,
a.currency,
ts.memo
FROM public.transaction_splits ts
LEFT JOIN public.accounts a ON a.id = ts.account
ORDER BY ts.id;
-- --------------------------------
-- Transaction cash sums view
CREATE OR REPLACE VIEW public.transaction_cashsums AS
SELECT t.id,
t."time",
sum(credit.credit_cash) AS cash_credit,
sum(debit.debit_cash) AS cash_debit,
a.name AS responsible,
t.description
FROM public.transactions t
LEFT JOIN ( SELECT cts.amount AS credit_cash,
cts.transaction AS cts_t
FROM public.transaction_nicesplits cts
LEFT JOIN public.accounts a_1 ON a_1.id = cts.account OR a_1.id = cts.account
WHERE a_1.currency = (( SELECT accounts.currency
FROM public.accounts
WHERE accounts.name::text = 'BrmBar Cash'::text))
AND (a_1.acctype = ANY (ARRAY['cash'::public.account_type, 'debt'::public.account_type]))
AND cts.amount < 0::numeric) credit ON credit.cts_t = t.id
LEFT JOIN ( SELECT dts.amount AS debit_cash,
dts.transaction AS dts_t
FROM public.transaction_nicesplits dts
LEFT JOIN public.accounts a_1 ON a_1.id = dts.account OR a_1.id = dts.account
WHERE a_1.currency = (( SELECT accounts.currency
FROM public.accounts
WHERE accounts.name::text = 'BrmBar Cash'::text))
AND (a_1.acctype = ANY (ARRAY['cash'::public.account_type, 'debt'::public.account_type]))
AND dts.amount > 0::numeric) debit ON debit.dts_t = t.id
LEFT JOIN public.accounts a ON a.id = t.responsible
GROUP BY t.id, a.name
ORDER BY t.id DESC;
-- --------------------------------
-- Function to check schema version (used in migrations)
CREATE OR REPLACE FUNCTION brmbar_privileged.has_exact_schema_version(
IN i_ver INTEGER
) RETURNS BOOLEAN
VOLATILE NOT LEAKPROOF LANGUAGE plpgsql AS $x$
DECLARE
v_ver INTEGER;
BEGIN
SELECT ver INTO v_ver FROM brmbar_privileged.brmbar_schema;
IF v_ver is NULL THEN
RETURN false;
ELSE
RETURN v_ver = i_ver;
END IF;
END;
$x$;
-- --------------------------------
--
CREATE OR REPLACE FUNCTION brmbar_privileged.upgrade_schema_version_to(
IN i_ver INTEGER
) RETURNS VOID
VOLATILE NOT LEAKPROOF LANGUAGE plpgsql AS $x$
DECLARE
v_ver INTEGER;
BEGIN
SELECT ver FROM brmbar_privileged.brmbar_schema INTO v_ver;
IF v_ver=(i_ver-1) THEN
UPDATE brmbar_privileged.brmbar_schema SET ver = i_ver;
ELSE
RAISE EXCEPTION 'Invalid brmbar schema version transition (% -> %)', v_ver, i_ver;
END IF;
END;
$x$;
-- Initialize version 1
INSERT INTO brmbar_privileged.brmbar_schema(ver) VALUES(1);
END IF;
END;
$$;

View file

@ -1,40 +0,0 @@
--
-- 0002-trading-accounts.sql
--
-- #2 - add trading accounts to account type type
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- Dominik Pantůček <dominik.pantucek@trustica.cz>
--
-- Permission to use, copy, modify, and/or distribute this software
-- for any purpose with or without fee is hereby granted, provided
-- that the above copyright notice and this permission notice appear
-- in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--
-- Require fully-qualified names
SELECT pg_catalog.set_config('search_path', '', false);
DO $upgrade_block$
BEGIN
IF brmbar_privileged.has_exact_schema_version(1) THEN
ALTER TYPE public.account_type ADD VALUE 'trading';
PERFORM brmbar_privileged.upgrade_schema_version_to(2);
END IF;
END;
$upgrade_block$;

View file

@ -1,52 +0,0 @@
--
-- 0003-new-account.sql
--
-- #3 - stored procedure for creating new account
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- Dominik Pantůček <dominik.pantucek@trustica.cz>
--
-- Permission to use, copy, modify, and/or distribute this software
-- for any purpose with or without fee is hereby granted, provided
-- that the above copyright notice and this permission notice appear
-- in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--
-- Require fully-qualified names
SELECT pg_catalog.set_config('search_path', '', false);
DO $upgrade_block$
BEGIN
IF brmbar_privileged.has_exact_schema_version(2) THEN
CREATE OR REPLACE FUNCTION public.create_account(
IN i_name public.accounts.name%TYPE,
IN i_currency public.accounts.currency%TYPE,
IN i_acctype public.accounts.acctype%TYPE
) RETURNS INTEGER LANGUAGE plpgsql AS $$
DECLARE
r_id INTEGER;
BEGIN
INSERT INTO public.accounts (name, currency, acctype)
VALUES (i_name, i_currency, i_acctype) RETURNING id INTO r_id;
RETURN r_id;
END
$$;
PERFORM brmbar_privileged.upgrade_schema_version_to(3);
END IF;
END;
$upgrade_block$;

View file

@ -1,50 +0,0 @@
--
-- 0004-add-account-barcode.sql
--
-- #4 - stored procedure for adding barcode to account
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- Dominik Pantůček <dominik.pantucek@trustica.cz>
--
-- Permission to use, copy, modify, and/or distribute this software
-- for any purpose with or without fee is hereby granted, provided
-- that the above copyright notice and this permission notice appear
-- in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--
-- Require fully-qualified names
SELECT pg_catalog.set_config('search_path', '', false);
DO $upgrade_block$
BEGIN
IF brmbar_privileged.has_exact_schema_version(3) THEN
CREATE OR REPLACE FUNCTION public.add_barcode_to_account(
IN i_account public.barcodes.account%TYPE,
IN i_barcode public.barcodes.barcode%TYPE
) RETURNS VOID LANGUAGE plpgsql AS $$
DECLARE
r_id INTEGER;
BEGIN
INSERT INTO public.barcodes (account, barcode)
VALUES (i_account, i_barcode);
END
$$;
PERFORM brmbar_privileged.upgrade_schema_version_to(4);
END IF;
END;
$upgrade_block$;

View file

@ -1,51 +0,0 @@
--
-- 0005-rename-account.sql
--
-- #5 - stored procedure for renaming account
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- Dominik Pantůček <dominik.pantucek@trustica.cz>
--
-- Permission to use, copy, modify, and/or distribute this software
-- for any purpose with or without fee is hereby granted, provided
-- that the above copyright notice and this permission notice appear
-- in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--
-- Require fully-qualified names
SELECT pg_catalog.set_config('search_path', '', false);
DO $upgrade_block$
BEGIN
IF brmbar_privileged.has_exact_schema_version(4) THEN
CREATE OR REPLACE FUNCTION public.rename_account(
IN i_account public.accounts.id%TYPE,
IN i_name public.accounts.name%TYPE
) RETURNS VOID LANGUAGE plpgsql AS $$
DECLARE
r_id INTEGER;
BEGIN
UPDATE public.accounts
SET name = i_name
WHERE id = i_account;
END
$$;
PERFORM brmbar_privileged.upgrade_schema_version_to(5);
END IF;
END;
$upgrade_block$;

View file

@ -1,50 +0,0 @@
--
-- 0006-new-currency.sql
--
-- #6 - stored procedure for creating new currency
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- Dominik Pantůček <dominik.pantucek@trustica.cz>
--
-- Permission to use, copy, modify, and/or distribute this software
-- for any purpose with or without fee is hereby granted, provided
-- that the above copyright notice and this permission notice appear
-- in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--
-- Require fully-qualified names
SELECT pg_catalog.set_config('search_path', '', false);
DO $upgrade_block$
BEGIN
IF brmbar_privileged.has_exact_schema_version(5) THEN
CREATE OR REPLACE FUNCTION public.create_currency(
IN i_name public.currencies.name%TYPE
) RETURNS INTEGER LANGUAGE plpgsql AS $$
DECLARE
r_id INTEGER;
BEGIN
INSERT INTO public.currencies (name)
VALUES (i_name) RETURNING id INTO r_id;
RETURN r_id;
END
$$;
PERFORM brmbar_privileged.upgrade_schema_version_to(6);
END IF;
END;
$upgrade_block$;

View file

@ -1,49 +0,0 @@
--
-- 0007-update-currency-sell-rate.sql
--
-- #7 - stored procedure for updating sell rate
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- Dominik Pantůček <dominik.pantucek@trustica.cz>
--
-- Permission to use, copy, modify, and/or distribute this software
-- for any purpose with or without fee is hereby granted, provided
-- that the above copyright notice and this permission notice appear
-- in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--
-- Require fully-qualified names
SELECT pg_catalog.set_config('search_path', '', false);
DO $upgrade_block$
BEGIN
IF brmbar_privileged.has_exact_schema_version(6) THEN
CREATE OR REPLACE FUNCTION public.update_currency_sell_rate(
IN i_currency public.exchange_rates.source%TYPE,
IN i_target public.exchange_rates.target%TYPE,
IN i_rate public.exchange_rates.rate%TYPE
) RETURNS VOID LANGUAGE plpgsql AS $$
BEGIN
INSERT INTO public.exchange_rates(source, target, rate, rate_dir)
VALUES (i_currency, i_target, i_rate, 'source_to_target');
END
$$;
PERFORM brmbar_privileged.upgrade_schema_version_to(7);
END IF;
END;
$upgrade_block$;

View file

@ -1,49 +0,0 @@
--
-- 0008-update-currency-buy-rate.sql
--
-- #8 - stored procedure for updating buy rate
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- Dominik Pantůček <dominik.pantucek@trustica.cz>
--
-- Permission to use, copy, modify, and/or distribute this software
-- for any purpose with or without fee is hereby granted, provided
-- that the above copyright notice and this permission notice appear
-- in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--
-- Require fully-qualified names
SELECT pg_catalog.set_config('search_path', '', false);
DO $upgrade_block$
BEGIN
IF brmbar_privileged.has_exact_schema_version(7) THEN
CREATE OR REPLACE FUNCTION public.update_currency_buy_rate(
IN i_currency public.exchange_rates.target%TYPE,
IN i_source public.exchange_rates.source%TYPE,
IN i_rate public.exchange_rates.rate%TYPE
) RETURNS VOID LANGUAGE plpgsql AS $$
BEGIN
INSERT INTO public.exchange_rates(source, target, rate, rate_dir)
VALUES (i_source, i_currency, i_rate, 'target_to_source');
END
$$;
PERFORM brmbar_privileged.upgrade_schema_version_to(8);
END IF;
END;
$upgrade_block$;

View file

@ -1,149 +0,0 @@
--
-- 0009-shop-sell.sql
--
-- #9 - stored function for sell transaction
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- TMA <tma+hs@jikos.cz>
--
-- Permission to use, copy, modify, and/or distribute this software
-- for any purpose with or without fee is hereby granted, provided
-- that the above copyright notice and this permission notice appear
-- in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--
-- To require fully-qualified names
SELECT pg_catalog.set_config('search_path', '', false);
DO $upgrade_block$
BEGIN
IF brmbar_privileged.has_exact_schema_version(8) THEN
-- return negative number on rate not found
CREATE OR REPLACE FUNCTION public.find_buy_rate(
IN i_item_id public.accounts.id%TYPE,
IN i_other_id public.accounts.id%TYPE
) RETURNS NUMERIC
LANGUAGE plpgsql
AS $$
DECLARE
v_rate public.exchange_rates.rate%TYPE;
v_rate_dir public.exchange_rates.rate_dir%TYPE;
BEGIN
SELECT rate, rate_dir INTO STRICT v_rate, v_rate_dir FROM public.exchange_rates WHERE target = i_item_id AND source = i_other_id AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1;
IF v_rate_dir = 'target_to_source'::public.exchange_rate_direction THEN
RETURN v_rate;
ELSE
RETURN 1/v_rate;
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN -1;
END;
$$;
-- return negative number on rate not found
CREATE OR REPLACE FUNCTION public.find_sell_rate(
IN i_item_id public.accounts.id%TYPE,
IN i_other_id public.accounts.id%TYPE
) RETURNS NUMERIC
LANGUAGE plpgsql
AS $$
DECLARE
v_rate public.exchange_rates.rate%TYPE;
v_rate_dir public.exchange_rates.rate_dir%TYPE;
BEGIN
SELECT rate, rate_dir INTO STRICT v_rate, v_rate_dir FROM public.exchange_rates WHERE target = i_other_id AND source = i_item_id AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1;
IF v_rate_dir = 'source_to_target'::public.exchange_rate_direction THEN
RETURN v_rate;
ELSE
RETURN 1/v_rate;
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN -1;
END;
$$;
CREATE OR REPLACE FUNCTION public.create_transaction(
i_responsible_id public.accounts.id%TYPE,
i_description public.transactions.description%TYPE
) RETURNS public.transactions.id%TYPE AS $$
DECLARE
new_transaction_id public.transactions.id%TYPE;
BEGIN
-- Create a new transaction
INSERT INTO public.transactions (responsible, description)
VALUES (i_responsible_id, i_description)
RETURNING id INTO new_transaction_id;
-- Return the new transaction ID
RETURN new_transaction_id;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION public.sell_item(
i_item_id public.accounts.id%TYPE,
i_amount INTEGER,
i_user_id public.accounts.id%TYPE,
i_target_currency_id public.currencies.id%TYPE,
i_description TEXT
) RETURNS NUMERIC
LANGUAGE plpgsql
AS $$
DECLARE
v_buy_rate NUMERIC;
v_sell_rate NUMERIC;
v_cost NUMERIC;
v_profit NUMERIC;
v_transaction_id public.transactions.id%TYPE;
BEGIN
-- Get the buy and sell rates from the stored functions
v_buy_rate := public.find_buy_rate(i_item_id, i_target_currency_id);
v_sell_rate := public.find_sell_rate(i_item_id, i_target_currency_id);
-- Calculate cost and profit
v_cost := i_amount * v_sell_rate;
v_profit := i_amount * (v_sell_rate - v_buy_rate);
-- Create a new transaction
v_transaction_id := public.create_transaction(i_user_id, i_description);
-- the item (decrease stock)
INSERT INTO public.transaction_splits (transaction, side, account, amount, memo)
VALUES (i_transaction_id, 'credit', i_item_id, i_amount,
(SELECT "name" FROM public.accounts WHERE id = i_user_id));
-- the user
INSERT INTO public.transaction_splits (transaction, side, account, amount, memo)
VALUES (i_transaction_id, 'debit', i_user_id, v_cost,
(SELECT "name" FROM public.accounts WHERE id = i_item_id));
-- the profit
INSERT INTO public.transaction_splits (transaction, side, account, amount, memo)
VALUES (i_transaction_id, 'debit', (SELECT account_id FROM accounts WHERE name = 'BrmBar Profits'), v_profit, (SELECT 'Margin on ' || "name" FROM public.accounts WHERE id = i_item_id));
-- Return the cost
RETURN v_cost;
END;
$$;
PERFORM brmbar_privileged.upgrade_schema_version_to(9);
END IF;
END;
$upgrade_block$;
-- vim: set ft=plsql :

View file

@ -1,154 +0,0 @@
--
-- 0010-shop-sell-for-cash.sql
--
-- #10 - stored function for cash sell transaction
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- TMA <tma+hs@jikos.cz>
--
-- Permission to use, copy, modify, and/or distribute this software
-- for any purpose with or without fee is hereby granted, provided
-- that the above copyright notice and this permission notice appear
-- in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--
-- To require fully-qualified names
SELECT pg_catalog.set_config('search_path', '', false);
DO $upgrade_block$
BEGIN
IF brmbar_privileged.has_exact_schema_version(9) THEN
CREATE OR REPLACE FUNCTION brmbar_privileged.create_transaction(
i_responsible_id public.accounts.id%TYPE,
i_description public.transactions.description%TYPE
) RETURNS public.transactions.id%TYPE AS $$
DECLARE
new_transaction_id public.transactions.id%TYPE;
BEGIN
-- Create a new transaction
INSERT INTO public.transactions (responsible, description)
VALUES (i_responsible_id, i_description)
RETURNING id INTO new_transaction_id;
-- Return the new transaction ID
RETURN new_transaction_id;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION brmbar_privileged.sell_item_internal(
i_item_id public.accounts.id%TYPE,
i_amount INTEGER,
i_user_id public.accounts.id%TYPE,
i_target_currency_id public.currencies.id%TYPE,
i_other_memo TEXT,
i_description TEXT
) RETURNS NUMERIC
LANGUAGE plpgsql
AS $$
DECLARE
v_buy_rate NUMERIC;
v_sell_rate NUMERIC;
v_cost NUMERIC;
v_profit NUMERIC;
v_transaction_id public.transactions.id%TYPE;
v_item_currency_id public.accounts.currency%TYPE;
BEGIN
-- Get item's currency
SELECT currency
INTO v_item_currency_id
FROM public.accounts
WHERE id=i_item_id;
-- Get the buy and sell rates from the stored functions
v_buy_rate := public.find_buy_rate(v_item_currency_id, i_target_currency_id);
v_sell_rate := public.find_sell_rate(v_item_currency_id, i_target_currency_id);
-- Calculate cost and profit
v_cost := i_amount * v_sell_rate;
v_profit := i_amount * (v_sell_rate - v_buy_rate);
-- Create a new transaction
v_transaction_id := brmbar_privileged.create_transaction(i_user_id, i_description);
-- the item (decrease stock)
INSERT INTO public.transaction_splits (transaction, side, account, amount, memo)
VALUES (v_transaction_id, 'credit', i_item_id, i_amount,
i_other_memo);
-- the user
INSERT INTO public.transaction_splits (transaction, side, account, amount, memo)
VALUES (v_transaction_id, 'debit', i_user_id, v_cost,
(SELECT "name" FROM public.accounts WHERE id = i_item_id));
-- the profit
INSERT INTO public.transaction_splits (transaction, side, account, amount, memo)
VALUES (v_transaction_id,
'debit',
(SELECT id FROM public.accounts WHERE name = 'BrmBar Profits'),
v_profit,
(SELECT 'Margin on ' || "name" FROM public.accounts WHERE id = i_item_id));
-- Return the cost
RETURN v_cost;
END;
$$;
CREATE OR REPLACE FUNCTION public.sell_item(
i_item_id public.accounts.id%TYPE,
i_amount INTEGER,
i_user_id public.accounts.id%TYPE,
i_target_currency_id public.currencies.id%TYPE,
i_description TEXT
) RETURNS NUMERIC
LANGUAGE plpgsql
AS $$
BEGIN
RETURN brmbar_privileged.sell_item_internal(i_item_id,
i_amount,
i_user_id,
i_target_currency_id,
(SELECT "name" FROM public.accounts WHERE id = i_user_id),
i_description);
END;
$$;
CREATE OR REPLACE FUNCTION public.sell_item_for_cash(
i_item_id public.accounts.id%TYPE,
i_amount INTEGER,
i_user_id public.accounts.id%TYPE,
i_target_currency_id public.currencies.id%TYPE,
i_description TEXT
) RETURNS NUMERIC
LANGUAGE plpgsql
AS $$
BEGIN
RETURN brmbar_privileged.sell_item_internal(i_item_id,
i_amount,
i_user_id,
i_target_currency_id,
'Cash',
i_description);
END;
$$;
DROP FUNCTION public.create_transaction;
PERFORM brmbar_privileged.upgrade_schema_version_to(10);
END IF;
END;
$upgrade_block$;
-- vim: set ft=plsql :

View file

@ -1,102 +0,0 @@
--
-- 0011-shop-undo-sale.sql
--
-- #11 - stored function for sale undo transaction
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- TMA <tma+hs@jikos.cz>
--
-- Permission to use, copy, modify, and/or distribute this software
-- for any purpose with or without fee is hereby granted, provided
-- that the above copyright notice and this permission notice appear
-- in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--
-- To require fully-qualified names
SELECT pg_catalog.set_config('search_path', '', false);
DO $upgrade_block$
BEGIN
IF brmbar_privileged.has_exact_schema_version(10) THEN
CREATE OR REPLACE FUNCTION brmbar_privileged.create_transaction(
i_responsible_id public.accounts.id%TYPE,
i_description public.transactions.description%TYPE
) RETURNS public.transactions.id%TYPE AS $$
DECLARE
new_transaction_id public.transactions.id%TYPE;
BEGIN
-- Create a new transaction
INSERT INTO public.transactions (responsible, description)
VALUES (i_responsible_id, i_description)
RETURNING id INTO new_transaction_id;
-- Return the new transaction ID
RETURN new_transaction_id;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION public.undo_sale_of_item(
i_item_id public.accounts.id%TYPE,
i_amount INTEGER,
i_user_id public.accounts.id%TYPE,
i_target_currency_id public.currencies.id%TYPE,
i_description TEXT
) RETURNS NUMERIC
LANGUAGE plpgsql
AS $$
DECLARE
v_buy_rate NUMERIC;
v_sell_rate NUMERIC;
v_cost NUMERIC;
v_profit NUMERIC;
v_transaction_id public.transactions.id%TYPE;
BEGIN
-- Get the buy and sell rates from the stored functions
v_buy_rate := public.find_buy_rate(i_item_id, i_target_currency_id);
v_sell_rate := public.find_sell_rate(i_item_id, i_target_currency_id);
-- Calculate cost and profit
v_cost := i_amount * v_sell_rate;
v_profit := i_amount * (v_sell_rate - v_buy_rate);
-- Create a new transaction
v_transaction_id := brmbar_privileged.create_transaction(i_user_id, i_description);
-- the item (decrease stock)
INSERT INTO public.transaction_splits (transaction, side, account, amount, memo)
VALUES (i_transaction_id, 'debit', i_item_id, i_amount,
(SELECT "name" || ' (sale undo)' FROM public.accounts WHERE id = i_user_id));
-- the user
INSERT INTO public.transaction_splits (transaction, side, account, amount, memo)
VALUES (i_transaction_id, 'credit', i_user_id, v_cost,
(SELECT "name" || ' (sale undo)' FROM public.accounts WHERE id = i_item_id));
-- the profit
INSERT INTO public.transaction_splits (transaction, side, account, amount, memo)
VALUES (i_transaction_id, 'credit', (SELECT account_id FROM accounts WHERE name = 'BrmBar Profits'), v_profit, (SELECT 'Margin repaid on ' || "name" FROM public.accounts WHERE id = i_item_id));
-- Return the cost
RETURN v_cost;
END;
$$;
PERFORM brmbar_privileged.upgrade_schema_version_to(11);
END IF;
END;
$upgrade_block$;
-- vim: set ft=plsql :

View file

@ -1,64 +0,0 @@
--
-- 0012-shop-add-credit.sql
--
-- #12 - stored function for cash deposit transactions
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- TMA <tma+hs@jikos.cz>
--
-- Permission to use, copy, modify, and/or distribute this software
-- for any purpose with or without fee is hereby granted, provided
-- that the above copyright notice and this permission notice appear
-- in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--
-- To require fully-qualified names
SELECT pg_catalog.set_config('search_path', '', false);
DO $upgrade_block$
BEGIN
IF brmbar_privileged.has_exact_schema_version(11) THEN
CREATE OR REPLACE FUNCTION public.add_credit(
i_cash_account_id public.accounts.id%TYPE,
i_credit NUMERIC,
i_user_id public.accounts.id%TYPE,
i_user_name TEXT
) RETURNS VOID
LANGUAGE plpgsql
AS $$
DECLARE
v_transaction_id public.transactions.id%TYPE;
BEGIN
-- Create a new transaction
v_transaction_id := brmbar_privileged.create_transaction(i_user_id, 'BrmBar credit replenishment for ' || i_user_name);
-- Debit cash (credit replenishment)
INSERT INTO public.transaction_splits (transaction, side, account, amount, memo)
VALUES (v_transaction_id, 'debit', i_cash_account_id, i_credit, i_user_name);
-- Credit the user
INSERT INTO public.transaction_splits (transaction, side, account, amount, memo)
VALUES (v_transaction_id, 'credit', i_user_id, i_credit, 'Credit replenishment');
END;
$$;
PERFORM brmbar_privileged.upgrade_schema_version_to(12);
END IF;
END;
$upgrade_block$;
-- vim: set ft=plsql :

View file

@ -1,64 +0,0 @@
--
-- 0013-shop-withdraw-credit.sql
--
-- #13 - stored function for cash withdrawal transactions
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- TMA <tma+hs@jikos.cz>
--
-- Permission to use, copy, modify, and/or distribute this software
-- for any purpose with or without fee is hereby granted, provided
-- that the above copyright notice and this permission notice appear
-- in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--
-- To require fully-qualified names
SELECT pg_catalog.set_config('search_path', '', false);
DO $upgrade_block$
BEGIN
IF brmbar_privileged.has_exact_schema_version(12) THEN
CREATE OR REPLACE FUNCTION public.withdraw_credit(
i_cash_account_id public.accounts.id%TYPE,
i_credit NUMERIC,
i_user_id public.accounts.id%TYPE,
i_user_name TEXT
) RETURNS VOID
LANGUAGE plpgsql
AS $$
DECLARE
v_transaction_id public.transactions.id%TYPE;
BEGIN
-- Create a new transaction
v_transaction_id := brmbar_privileged.create_transaction(i_user_id, 'BrmBar credit withdrawal for ' || i_user_name);
-- Debit cash (credit replenishment)
INSERT INTO public.transaction_splits (transaction, side, account, amount, memo)
VALUES (v_transaction_id, 'credit', i_cash_account_id, i_credit, i_user_name);
-- Credit the user
INSERT INTO public.transaction_splits (transaction, side, account, amount, memo)
VALUES (v_transaction_id, 'debit', i_user_id, i_credit, 'Credit withdrawal');
END;
$$;
PERFORM brmbar_privileged.upgrade_schema_version_to(13);
END IF;
END;
$upgrade_block$;
-- vim: set ft=plsql :

View file

@ -1,58 +0,0 @@
--
-- 0014-shop-transfer-credit.sql
--
-- #14 - stored function for "credit" transfer transactions
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- TMA <tma+hs@jikos.cz>
--
-- 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(13) THEN
CREATE OR REPLACE FUNCTION public.transfer_credit(
i_cash_account_id public.accounts.id%TYPE,
i_credit NUMERIC,
i_userfrom_id public.accounts.id%TYPE,
i_userfrom_name TEXT,
i_userto_id public.accounts.id%TYPE,
i_userto_name TEXT
) RETURNS VOID
LANGUAGE plpgsql
AS $$
BEGIN
PERFORM public.add_credit(i_cash_account_id, i_credit, i_userto_id, i_userto_name);
PERFORM public.withdraw_credit(i_cash_account_id, i_credit, i_userfrom_id, i_userfrom_name);
END;
$$;
PERFORM brmbar_privileged.upgrade_schema_version_to(14);
END IF;
END;
$upgrade_block$;
-- vim: set ft=plsql :

View file

@ -1,85 +0,0 @@
--
-- 0015-shop-buy-for-cash.sql
--
-- #15 - stored function for cash-based stock replenishment transaction
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- TMA <tma+hs@jikos.cz>
--
-- 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(14) THEN
CREATE OR REPLACE FUNCTION public.buy_for_cash(
i_cash_account_id public.accounts.id%TYPE,
i_item_id public.accounts.id%TYPE,
i_amount INTEGER,
i_target_currency_id public.currencies.id%TYPE,
i_item_name TEXT
) RETURNS NUMERIC
LANGUAGE plpgsql
AS $$
DECLARE
v_buy_rate NUMERIC;
v_cost NUMERIC;
v_transaction_id public.transactions.id%TYPE;
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);
-- Calculate cost and profit
v_cost := i_amount * v_buy_rate;
-- Create a new transaction
v_transaction_id := brmbar_privileged.create_transaction(NULL,
'BrmBar stock replenishment of ' || i_amount || 'x ' || i_item_name || ' for cash');
-- the item
INSERT INTO public.transaction_splits (transaction, side, account, amount, memo)
VALUES (v_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,
i_item_name);
-- Return the cost
RETURN v_cost;
END;
$$;
PERFORM brmbar_privileged.upgrade_schema_version_to(15);
END IF;
END;
$upgrade_block$;
-- vim: set ft=plsql :

View file

@ -1,64 +0,0 @@
--
-- 0016-shop-buy-for-cash.sql
--
-- #16 - stored function for receipt reimbursement transaction
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- TMA <tma+hs@jikos.cz>
--
-- 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(15) THEN
CREATE OR REPLACE FUNCTION public.receipt_reimbursement(
i_profits_id public.accounts.id%TYPE,
i_user_id public.accounts.id%TYPE,
i_user_name public.accounts.name%TYPE,
i_amount NUMERIC,
i_description TEXT
) RETURNS VOID
LANGUAGE plpgsql
AS $$
DECLARE
v_transaction_id public.transactions.id%TYPE;
BEGIN
-- Create a new transaction
v_transaction_id := brmbar_privileged.create_transaction(i_user_id,
'Receipt: ' || i_description);
-- the "profit"
INSERT INTO public.transaction_splits (transaction, side, account, amount, memo)
VALUES (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 (v_transaction_id, 'credit', i_user_id, i_amount, 'Credit from receipt: ' || i_description);
END;
$$;
PERFORM brmbar_privileged.upgrade_schema_version_to(16);
END IF;
END;
$upgrade_block$;
-- vim: set ft=plsql :

View file

@ -1,159 +0,0 @@
--
-- 0017-shop-fix-inventory.sql
--
-- #17 - stored function for "fixing" inventory transaction
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- TMA <tma+hs@jikos.cz>
--
-- Permission to use, copy, modify, and/or distribute this software
-- for any purpose with or without fee is hereby granted, provided
-- that the above copyright notice and this permission notice appear
-- in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--
-- To require fully-qualified names
SELECT pg_catalog.set_config('search_path', '', false);
DO $upgrade_block$
BEGIN
IF brmbar_privileged.has_exact_schema_version(16) THEN
CREATE OR REPLACE FUNCTION public.compute_account_balance(
i_account_id public.accounts.id%TYPE
) RETURNS NUMERIC
LANGUAGE plpgsql
AS $$
DECLARE
v_crsum NUMERIC;
v_dbsum NUMERIC;
BEGIN
SELECT
COALESCE(SUM(CASE WHEN side='credit' THEN amount ELSE 0 END),0) crsum,
COALESCE(SUM(CASE WHEN side='debit' THEN amount ELSE 0 END),0) dbsum
INTO v_crsum, v_dbsum
FROM public.transaction_splits ts WHERE ts.account=i_account_id;
RETURN v_dbsum - v_crsum;
END; $$;
CREATE OR REPLACE FUNCTION brmbar_privileged.fix_account_balance(
IN i_account_id public.accounts.id%TYPE,
IN i_account_currency_id public.currencies.id%TYPE,
IN i_excess_id public.accounts.id%TYPE,
IN i_deficit_id public.accounts.id%TYPE,
IN i_shop_currency_id public.currencies.id%TYPE,
IN i_amount_in_reality NUMERIC
) RETURNS BOOLEAN
VOLATILE NOT LEAKPROOF LANGUAGE plpgsql AS $fn$
DECLARE
v_amount_in_system NUMERIC;
v_buy_rate NUMERIC;
v_currency_id public.currencies.id%TYPE;
v_diff NUMERIC;
v_buy_total NUMERIC;
v_ntrn_id public.transactions.id%TYPE;
v_transaction_memo TEXT;
v_item_name TEXT;
v_excess_memo TEXT;
v_deficit_memo TEXT;
v_old_trn public.transactions%ROWTYPE;
v_old_split public.transaction_splits%ROWTYPE;
BEGIN
v_amount_in_system := public.compute_account_balance(i_account_id);
IF i_account_currency_id <> i_shop_currency_id THEN
v_buy_rate := public.find_buy_rate(i_item_id, i_shop_currency_id);
ELSE
v_buy_rate := 1;
END IF;
v_diff := ABS(i_amount_in_reality - v_amount_in_system);
v_buy_total := v_buy_rate * v_diff;
-- compute memo strings
IF i_item_id = 1 THEN -- cash account recognized by magic id
-- fixing cash
v_transaction_memo :=
'BrmBar cash inventory fix of ' || v_amount_in_system
|| ' in system to ' || i_amount_in_reality || ' in reality';
v_excess_memo := 'Inventory cash fix excess.';
v_deficit_memo := 'Inventory fix deficit.';
ELSE
-- fixing other account
SELECT "name" INTO v_item_name FROM public.accounts WHERE id = i_account_id;
v_transaction_memo :=
'BrmBar inventory fix of ' || v_amount_in_system || 'pcs '
|| v_item_name
|| ' in system to ' || i_amount_in_reality || 'pcs in reality';
v_excess_memo := 'Inventory fix excess ' || v_item_name;
v_deficit_memo := 'Inventory fix deficit ' || v_item_name;
END IF;
-- create transaction based on the relation between counting and accounting
IF i_amount_in_reality > v_amount_in_system THEN
v_ntrn_id := brmbar_privileged.create_transaction(NULL, v_transaction_memo);
INSERT INTO transaction_splits ("transaction", "side", "account", "amount", "memo")
VALUES (v_ntrn_id, 'debit', i_item_id, v_diff, 'Inventory fix excess');
INSERT INTO transaction_splits ("transaction", "side", "account", "amount", "memo")
VALUES (v_ntrn_id, 'credit', i_excess_id, v_buy_total, v_excess_memo);
RETURN TRUE;
ELSIF i_amount_in_reality < v_amount_in_system THEN
v_ntrn_id := brmbar_privileged.create_transaction(NULL, v_transaction_memo);
INSERT INTO transaction_splits ("transaction", "side", "account", "amount", "memo")
VALUES (v_ntrn_id, 'credit', i_item_id, v_diff, 'Inventory fix deficit');
INSERT INTO transaction_splits ("transaction", "side", "account", "amount", "memo")
VALUES (v_ntrn_id, 'debit', i_deficit_id, v_buy_total, v_deficit_memo);
RETURN TRUE;
ELSIF i_account_id <> 1 THEN -- cash account recognized by magic id
-- record that everything is going on swimmingly only for noncash accounts (WTF)
v_ntrn_id := brmbar_privileged.create_transaction(NULL, v_transaction_memo);
INSERT INTO transaction_splits ("transaction", "side", "account", "amount", "memo")
VALUES (v_ntrn_id, 'debit', i_item_id, 0, 'Inventory fix - amount was correct');
INSERT INTO transaction_splits ("transaction", "side", "account", "amount", "memo")
VALUES (v_ntrn_id, 'credit', i_item_id, 0, 'Inventory fix - amount was correct');
RETURN FALSE;
END IF;
RETURN FALSE;
END;
$fn$;
CREATE OR REPLACE FUNCTION public.fix_inventory(
IN i_account_id public.accounts.id%TYPE,
IN i_account_currency_id public.currencies.id%TYPE,
IN i_excess_id public.accounts.id%TYPE,
IN i_deficit_id public.accounts.id%TYPE,
IN i_shop_currency_id public.currencies.id%TYPE,
IN i_amount_in_reality NUMERIC
) RETURNS BOOLEAN
VOLATILE NOT LEAKPROOF LANGUAGE plpgsql AS $fn$
BEGIN
RETURN brmbar_privileged.fix_account_balance(
i_account_id,
i_account_currency_id,
i_excess_id,
i_deficit_id,
i_shop_currency_id,
i_amount_in_reality
);
END;
$fn$;
PERFORM brmbar_privileged.upgrade_schema_version_to(17);
END IF;
END;
$upgrade_block$;
-- vim: set ft=plsql :

View file

@ -1,60 +0,0 @@
--
-- 0018-shop-fix-cash.sql
--
-- #18 - stored function for "fixing cash" transaction
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- TMA <tma+hs@jikos.cz>
--
-- Permission to use, copy, modify, and/or distribute this software
-- for any purpose with or without fee is hereby granted, provided
-- that the above copyright notice and this permission notice appear
-- in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--
-- To require fully-qualified names
SELECT pg_catalog.set_config('search_path', '', false);
DO $upgrade_block$
BEGIN
IF brmbar_privileged.has_exact_schema_version(17) THEN
CREATE OR REPLACE FUNCTION public.fix_cash(
IN i_excess_id public.accounts.id%TYPE,
IN i_deficit_id public.accounts.id%TYPE,
IN i_shop_currency_id public.currencies.id%TYPE,
IN i_amount_in_reality NUMERIC
) RETURNS BOOLEAN
VOLATILE NOT LEAKPROOF LANGUAGE plpgsql AS $fn$
BEGIN
RETURN brmbar_privileged.fix_account_balance(
1,
1,
i_excess_id,
i_deficit_id,
i_shop_currency_id,
i_amount_in_reality
);
END;
$fn$;
PERFORM brmbar_privileged.upgrade_schema_version_to(18);
END IF;
END;
$upgrade_block$;
-- vim: set ft=plsql :

View file

@ -1,82 +0,0 @@
--
-- 0019-shop-consolidate.sql
--
-- #19 - stored function for "consolidation" transaction
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- TMA <tma+hs@jikos.cz>
--
-- Permission to use, copy, modify, and/or distribute this software
-- for any purpose with or without fee is hereby granted, provided
-- that the above copyright notice and this permission notice appear
-- in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--
-- To require fully-qualified names
SELECT pg_catalog.set_config('search_path', '', false);
DO $upgrade_block$
BEGIN
IF brmbar_privileged.has_exact_schema_version(18) THEN
CREATE OR REPLACE FUNCTION public.make_consolidate_transaction(
i_excess_id public.accounts.id%TYPE,
i_deficit_id public.accounts.id%TYPE,
i_profits_id public.accounts.id%TYPE
) RETURNS TEXT
LANGUAGE plpgsql
AS $$
DECLARE
v_transaction_id public.transactions.id%TYPE;
v_excess_balance NUMERIC;
v_deficit_balance NUMERIC;
v_ret TEXT;
BEGIN
v_ret := NULL;
-- Create a new transaction
v_transaction_id := brmbar_privileged.create_transaction(NULL,
'BrmBar inventory consolidation');
v_excess_balance := public.compute_account_balance(i_excess_id);
v_deficit_balance := public.compute_account_balance(i_deficit_id);
IF v_excess_balance <> 0 THEN
v_ret := 'Excess balance ' || -v_excess_balance || ' debited to profit';
INSERT INTO public.transaction_splits (transaction, side, account, amount, memo)
VALUES (i_transaction_id, 'debit', i_excess_id, -v_excess_balance,
'Excess balance added to profit.');
INSERT INTO public.transaction_splits (transaction, side, account, amount, memo)
VALUES (i_transaction_id, 'debit', i_profits_id, -v_excess_balance,
'Excess balance added to profit.');
END IF;
IF v_deficit_balance <> 0 THEN
v_ret := COALESCE(v_ret, '');
v_ret := v_ret || 'Deficit balance ' || v_deficit_balance || ' credited to profit';
INSERT INTO public.transaction_splits (transaction, side, account, amount, memo)
VALUES (i_transaction_id, 'credit', i_deficit_id, v_deficit_balance,
'Deficit balance removed from profit.');
INSERT INTO public.transaction_splits (transaction, side, account, amount, memo)
VALUES (i_transaction_id, 'credit', i_profits_id, v_deficit_balance,
'Deficit balance removed from profit.');
END IF;
RETURN v_ret;
END;
$$;
PERFORM brmbar_privileged.upgrade_schema_version_to(19);
END IF;
END;
$upgrade_block$;
-- vim: set ft=plsql :

View file

@ -1,64 +0,0 @@
--
-- 0020-shop-undo.sql
--
-- #20 - stored function for undo transaction
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- TMA <tma+hs@jikos.cz>
--
-- Permission to use, copy, modify, and/or distribute this software
-- for any purpose with or without fee is hereby granted, provided
-- that the above copyright notice and this permission notice appear
-- in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--
-- To require fully-qualified names
SELECT pg_catalog.set_config('search_path', '', false);
DO $upgrade_block$
BEGIN
IF brmbar_privileged.has_exact_schema_version(19) THEN
CREATE OR REPLACE FUNCTION public.undo_transaction(
IN i_id public.transactions.id%TYPE)
RETURNS public.transactions.id%TYPE
VOLATILE NOT LEAKPROOF LANGUAGE plpgsql AS $fn$
DECLARE
v_ntrn_id public.transactions.id%TYPE;
v_old_trn public.transactions%ROWTYPE;
v_old_split public.transaction_splits%ROWTYPE;
BEGIN
SELECT * INTO v_old_trn FROM public.transactions WHERE id = i_id;
INSERT INTO transactions ("description") VALUES ('undo '||i_id||' ('||v_old_trn.description||')') RETURNING id into v_ntrn_id;
FOR v_old_split IN
SELECT * FROM transaction_splits WHERE "transaction" = i_id
LOOP
INSERT INTO transaction_splits ("transaction", "side", "account", "amount", "memo")
VALUES (v_ntrn_id, v_old_split.side, v_old_split.account, -v_old_split.amount,
'undo ' || v_old_split.id || ' (' || v_old_split.memo || ')' );
END LOOP;
RETURN v_ntrn_id;
END;
$fn$;
PERFORM brmbar_privileged.upgrade_schema_version_to(20);
END IF;
END;
$upgrade_block$;
-- vim: set ft=plsql :

View file

@ -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 <tma+hs@jikos.cz>
--
-- 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 :

View file

@ -1,85 +0,0 @@
--
-- 0022-shop-init.sql
--
-- #22 - stored function for initializing Shop.py
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- TMA <tma+hs@jikos.cz>
--
-- 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 :

View file

@ -1,94 +0,0 @@
--
-- 0023-inventory-balance.sql
--
-- #23 - stored function for total inventory balance
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- TMA <tma+hs@jikos.cz>
--
-- 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 :

View file

@ -1,92 +0,0 @@
--
-- 0024-find-rates-fix.sql
--
-- #24 - fix stored functions find_buy_rate and find_sell_rate
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- TMA <tma+hs@jikos.cz>
--
-- Permission to use, copy, modify, and/or distribute this software
-- for any purpose with or without fee is hereby granted, provided
-- that the above copyright notice and this permission notice appear
-- in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--
-- To require fully-qualified names
SELECT pg_catalog.set_config('search_path', '', false);
DO $upgrade_block$
BEGIN
IF brmbar_privileged.has_exact_schema_version(23) THEN
DROP FUNCTION IF EXISTS public.find_buy_rate(integer,integer);
DROP FUNCTION IF EXISTS public.find_sell_rate(integer,integer);
CREATE OR REPLACE FUNCTION public.find_buy_rate(
IN i_item_currency_id public.accounts.id%TYPE,
IN i_other_currency_id public.accounts.id%TYPE
) RETURNS NUMERIC
LANGUAGE plpgsql
AS $$
DECLARE
v_rate public.exchange_rates.rate%TYPE;
v_rate_dir public.exchange_rates.rate_dir%TYPE;
BEGIN
SELECT rate, rate_dir INTO STRICT v_rate, v_rate_dir FROM public.exchange_rates WHERE target = i_item_currency_id AND source = i_other_currency_id AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1;
IF v_rate_dir = 'target_to_source'::public.exchange_rate_direction THEN
RETURN v_rate;
ELSE
RETURN 1/v_rate;
END IF;
/* propagate error
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN -1;
*/
END;
$$;
-- return negative number on rate not found
CREATE OR REPLACE FUNCTION public.find_sell_rate(
IN i_item_currency_id public.accounts.id%TYPE,
IN i_other_currency_id public.accounts.id%TYPE
) RETURNS NUMERIC
LANGUAGE plpgsql
AS $$
DECLARE
v_rate public.exchange_rates.rate%TYPE;
v_rate_dir public.exchange_rates.rate_dir%TYPE;
BEGIN
SELECT rate, rate_dir INTO STRICT v_rate, v_rate_dir FROM public.exchange_rates WHERE target = i_other_currency_id AND source = i_item_currency_id AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1;
IF v_rate_dir = 'source_to_target'::public.exchange_rate_direction THEN
RETURN v_rate;
ELSE
RETURN 1/v_rate;
END IF;
/* propagate error
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN -1;
*/
END;
$$;
PERFORM brmbar_privileged.upgrade_schema_version_to(24);
END IF;
END;
$upgrade_block$;
-- vim: set ft=plsql :

View file

@ -1,111 +0,0 @@
--
-- 0025-load-account.sql
--
-- #25 - stored procedures for account loading
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- TMA <tma+hs@jikos.cz>
--
-- 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(24) THEN
SELECT COUNT(1) INTO v
FROM pg_catalog.pg_type typ
INNER JOIN pg_catalog.pg_namespace nsp
ON nsp.oid = typ.typnamespace
WHERE nsp.nspname = 'brmbar_privileged'
AND typ.typname='account_class_initialization_data_type';
IF v>0 THEN
RAISE NOTICE 'Changing type account_class_initialization_data_type';
DROP TYPE brmbar_privileged.account_class_initialization_data_type CASCADE;
ELSE
RAISE NOTICE 'Creating type account_class_initialization_data_type';
END IF;
SELECT COUNT(1) INTO v
FROM pg_catalog.pg_type typ
INNER JOIN pg_catalog.pg_namespace nsp
ON nsp.oid = typ.typnamespace
WHERE nsp.nspname = 'brmbar_privileged'
AND typ.typname='account_load_type';
IF v>0 THEN
RAISE NOTICE 'Changing type account_load_type';
DROP TYPE brmbar_privileged.account_load_type CASCADE;
ELSE
RAISE NOTICE 'Creating type account_load_type';
END IF;
CREATE TYPE brmbar_privileged.account_load_type
AS ENUM ('by_id', 'by_barcode');
CREATE TYPE brmbar_privileged.account_class_initialization_data_type
AS (
account_id INTEGER, --public.accounts.id%TYPE,
account_name TEXT, --public.accounts.id%TYPE,
account_acctype public.account_type,
currency_id INTEGER, --public.currencies.id%TYPE,
currency_name TEXT
);
CREATE OR REPLACE FUNCTION public.account_class_initialization_data(
load_by brmbar_privileged.account_load_type,
i_id INTEGER,
i_barcode TEXT)
RETURNS brmbar_privileged.account_class_initialization_data_type
LANGUAGE plpgsql
AS
$$
DECLARE
rv brmbar_privileged.account_class_initialization_data_type;
BEGIN
IF load_by = 'by_id' THEN
SELECT a.id, a.name, a.acctype, c.id, c.name
INTO STRICT rv.account_id, rv.account_name, rv.account_acctype, rv.currency_id, rv.currency_name
FROM public.accounts a JOIN public.currencies c ON a.currency = c.id
WHERE a.id = i_id;
ELSE -- by_barcode
SELECT a.id, a.name, a.acctype, c.id, c.name
INTO STRICT rv.account_id, rv.account_name, rv.account_acctype, rv.currency_id, rv.currency_name
FROM public.accounts a JOIN public.currencies c ON a.currency = c.id JOIN public.barcodes b ON b.account = a.id
WHERE b.barcode = i_barcode;
END IF;
--
RETURN rv;
END;
$$;
PERFORM brmbar_privileged.upgrade_schema_version_to(25);
END IF;
END;
$upgrade_block$;
-- vim: set ft=plsql :

View file

@ -1,73 +0,0 @@
#!/usr/bin/python3
import sys
import subprocess
#from brmbar import Database
#from brmbar import Currency
from contextlib import closing
import psycopg2
from brmbar.Database import Database
from brmbar.Currency import Currency
import math
#import brmbar
def approx_equal(a, b, tol=1e-6):
"""Check if two (buy, sell) rate tuples are approximately equal."""
return (
isinstance(a, tuple) and isinstance(b, tuple) and
math.isclose(a[0], b[0], abs_tol=tol) and
math.isclose(a[1], b[1], abs_tol=tol)
)
def compare_exceptions(e1, e2):
"""Compare exception types and messages."""
return type(e1) == type(e2) and str(e1) == str(e2)
def main():
db = Database("dbname=brmbar")
# Get all currencies
with closing(db.db_conn.cursor()) as cur:
cur.execute("SELECT id, name FROM currencies")
currencies = cur.fetchall()
# Build Currency objects
currency_objs = [Currency(db, id, name) for id, name in currencies]
# Test all currency pairs
for c1 in currency_objs:
for c2 in currency_objs:
#if c1.id == c2.id:
# continue
try:
rates1 = c1.rates(c2)
exc1 = None
except (RuntimeError, NameError) as e1:
rates1 = None
exc1 = e1
try:
rates2 = c1.rates2(c2)
exc2 = None
except (RuntimeError, NameError) as e2:
rates2 = None
exc2 = e2
if exc1 or exc2:
if not compare_exceptions(exc1, exc2):
print(f"[EXCEPTION DIFFERENCE] {c1.name} -> {c2.name}")
print(f" rates() exception: {type(exc1).__name__}: {exc1}")
print(f" rates2() exception: {type(exc2).__name__}: {exc2}")
elif not approx_equal(rates1, rates2):
print(f"[VALUE DIFFERENCE] {c1.name} -> {c2.name}")
print(f" rates(): {rates1}")
print(f" rates2(): {rates2}")
if __name__ == "__main__":
main()