Merge branch 'master' into deployed

This commit is contained in:
Dominik Pantůček 2025-07-21 17:19:06 +02:00
commit 114e67c445
14 changed files with 514 additions and 296 deletions

View file

@ -4,6 +4,7 @@ import argparse
import brmbar import brmbar
import math import math
from brmbar import Database from brmbar import Database
import sys
def main(): def main():
parser = argparse.ArgumentParser(usage = "File format: EAN amount total_price name, e.g. 4001242002377 6 167.40 Chio Tortillas") parser = argparse.ArgumentParser(usage = "File format: EAN amount total_price name, e.g. 4001242002377 6 167.40 Chio Tortillas")
@ -38,5 +39,7 @@ def main():
print("Total is {}".format(total)) print("Total is {}".format(total))
if __name__ == "__main__": if __name__ == "__main__":
print("!!! THIS PROGRAM NO LONGER WORKS !!!")
sys.exit(1)
main() main()

View file

@ -6,6 +6,8 @@ from brmbar import Database
import brmbar import brmbar
print("!!! THIS PROGRAM NO LONGER WORKS !!!")
sys.exit(1)
def help(): def help():
print("""BrmBar v3 (c) Petr Baudis <pasky@ucw.cz> 2012-2013 print("""BrmBar v3 (c) Petr Baudis <pasky@ucw.cz> 2012-2013

View file

@ -150,11 +150,16 @@ class ShopAdapter(QtCore.QObject):
@QtCore.Slot('QVariant', 'QVariant', 'QVariant', result='QVariant') @QtCore.Slot('QVariant', 'QVariant', 'QVariant', result='QVariant')
def newTransfer(self, uidfrom, uidto, amount): def newTransfer(self, uidfrom, uidto, amount):
logger.debug("newTransfer %s %s %s", uidfrom, uidto, amount)
ufrom = brmbar.Account.load(db, id=uidfrom) ufrom = brmbar.Account.load(db, id=uidfrom)
logger.debug(" ufrom = %s", ufrom)
uto = brmbar.Account.load(db, id=uidto) uto = brmbar.Account.load(db, id=uidto)
logger.debug(" uto = %s", uto)
shop.transfer_credit(ufrom, uto, amount = amount) shop.transfer_credit(ufrom, uto, amount = amount)
db.commit() db.commit()
return currency.str(float(amount)) csfa = currency.str(float(amount))
logger.debug(" csfa = '%s'", csfa)
return csfa
@QtCore.Slot('QVariant', result='QVariant') @QtCore.Slot('QVariant', result='QVariant')
def balance_user(self, userid): def balance_user(self, userid):

View file

@ -6,6 +6,9 @@ from brmbar import Database
import brmbar import brmbar
print("!!! THIS PROGRAM NO LONGER WORKS !!!")
sys.exit(1)
db = Database.Database("dbname=brmbar") db = Database.Database("dbname=brmbar")
shop = brmbar.Shop.new_with_defaults(db) shop = brmbar.Shop.new_with_defaults(db)
currency = shop.currency currency = shop.currency

View file

@ -6,6 +6,9 @@ from brmbar import Database
import brmbar import brmbar
print("!!! THIS PROGRAM NO LONGER WORKS !!!")
sys.exit(1)
from flask import * from flask import *
app = Flask(__name__) app = Flask(__name__)
#app.debug = True #app.debug = True

View file

@ -1,12 +1,15 @@
from .Currency import Currency from .Currency import Currency
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Account: class Account:
""" BrmBar Account """BrmBar Account
Both users and items are accounts. So is the money box, etc. Both users and items are accounts. So is the money box, etc.
Each account has a currency.""" Each account has a currency."""
def __init__(self, db, id, name, currency, acctype): def __init__(self, db, id, name, currency, acctype):
self.db = db self.db = db
self.id = id self.id = id
@ -17,48 +20,38 @@ class Account:
@classmethod @classmethod
def load_by_barcode(cls, db, barcode): def load_by_barcode(cls, db, barcode):
logger.debug("load_by_barcode: '%s'", barcode) logger.debug("load_by_barcode: '%s'", barcode)
res = db.execute_and_fetch("SELECT account FROM barcodes WHERE barcode = %s", [barcode]) res = db.execute_and_fetch(
"SELECT account FROM barcodes WHERE barcode = %s", [barcode]
)
if res is None: if res is None:
return None return None
id = res[0] id = res[0]
return cls.load(db, id = id) return cls.load(db, id=id)
@classmethod @classmethod
def load(cls, db, id = None, name = None): def load(cls, db, id=None):
""" Constructor for existing account """ """Constructor for existing account"""
if id is not None: if id is None:
name = db.execute_and_fetch("SELECT name FROM accounts WHERE id = %s", [id]) raise NameError("Account.load(): Specify id")
name = name[0] name, currid, acctype = db.execute_and_fetch(
elif name is not None: "SELECT name, currency, acctype FROM accounts WHERE id = %s", [id]
id = db.execute_and_fetch("SELECT id FROM accounts WHERE name = %s", [name]) )
id = id[0] currency = Currency.load(db, id=currid)
else: return cls(db, name=name, id=id, currency=currency, acctype=acctype)
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 @classmethod
def create(cls, db, name, currency, acctype): def create(cls, db, name, currency, acctype):
""" Constructor for new account """ """Constructor for new account"""
# id = db.execute_and_fetch("INSERT INTO accounts (name, currency, acctype) VALUES (%s, %s, %s) RETURNING id", [name, currency.id, acctype]) id = db.execute_and_fetch(
id = db.execute_and_fetch("SELECT public.create_account(%s, %s, %s)", [name, currency.id, acctype]) "SELECT public.create_account(%s, %s, %s)", [name, currency.id, acctype]
# id = id[0] )
return cls(db, name = name, id = id, currency = currency, acctype = acctype) return cls(db, name=name, id=id, currency=currency, acctype=acctype)
def balance(self): def balance(self):
bal = self.db.execute_and_fetch( bal = self.db.execute_and_fetch(
"SELECT public.compute_account_balance(%s)", "SELECT public.compute_account_balance(%s)", [self.id]
[self.id]
)[0] )[0]
return bal 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): def balance_str(self):
return self.currency.str(self.balance()) return self.currency.str(self.balance())
@ -66,22 +59,12 @@ class Account:
def negbalance_str(self): def negbalance_str(self):
return self.currency.str(-self.balance()) 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): def add_barcode(self, barcode):
# self.db.execute("INSERT INTO barcodes (account, barcode) VALUES (%s, %s)", [self.id, barcode]) self.db.execute(
self.db.execute("SELECT public.add_barcode_to_account(%s, %s)", [self.id, barcode]) "SELECT public.add_barcode_to_account(%s, %s)", [self.id, barcode]
)
self.db.commit() self.db.commit()
def rename(self, name): def rename(self, name):
# self.db.execute("UPDATE accounts SET name = %s WHERE id = %s", [name, self.id])
self.db.execute("SELECT public.rename_account(%s, %s)", [self.id, name]) self.db.execute("SELECT public.rename_account(%s, %s)", [self.id, name])
self.name = name self.name = name

View file

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

View file

@ -4,26 +4,29 @@ import psycopg2
from contextlib import closing from contextlib import closing
import time import time
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Database: class Database:
"""self-reconnecting database object""" """self-reconnecting database object"""
def __init__(self, dsn): def __init__(self, dsn):
self.db_conn = psycopg2.connect(dsn) self.db_conn = psycopg2.connect(dsn)
self.dsn = dsn self.dsn = dsn
def execute(self, query, attrs = None): def execute(self, query, attrs=None):
"""execute a query and return one result""" """execute a query and return one result"""
with closing(self.db_conn.cursor()) as cur: with closing(self.db_conn.cursor()) as cur:
cur = self._execute(cur, query, attrs) 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""" """execute a query and return one result"""
with closing(self.db_conn.cursor()) as cur: with closing(self.db_conn.cursor()) as cur:
cur = self._execute(cur, query, attrs) cur = self._execute(cur, query, attrs)
return cur.fetchone() 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""" """execute a query and return all results"""
with closing(self.db_conn.cursor()) as cur: with closing(self.db_conn.cursor()) as cur:
cur = self._execute(cur, query, attrs) cur = self._execute(cur, query, attrs)
@ -38,27 +41,32 @@ class Database:
else: else:
cur.execute(query, attrs) cur.execute(query, attrs)
return cur return cur
except psycopg2.DataError as error: # when biitr comes and enters '99999999999999999999' for amount except (
print("We have invalid input data (SQLi?): level %s (%s) @%s" % ( psycopg2.DataError
level, error, time.strftime("%Y%m%d %a %I:%m %p") ) as error: # when biitr comes and enters '99999999999999999999' for amount
)) logger.debug(
"We have invalid input data (SQLi?): level %s (%s) @%s"
% (level, error, time.strftime("%Y%m%d %a %I:%m %p"))
)
self.db_conn.rollback() self.db_conn.rollback()
raise RuntimeError("Unsanitized data entered again... BOBBY TABLES") raise RuntimeError("Unsanitized data entered again... BOBBY TABLES")
except psycopg2.OperationalError as error: except psycopg2.OperationalError as error:
print("Sleeping: level %s (%s) @%s" % ( logger.debug(
level, error, time.strftime("%Y%m%d %a %I:%m %p") "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) # TODO: emit message "db conn failed, reconnecting
time.sleep(2**level)
try: try:
self.db_conn = psycopg2.connect(self.dsn) self.db_conn = psycopg2.connect(self.dsn)
except psycopg2.OperationalError: except psycopg2.OperationalError:
#TODO: emit message "psql not running to interface # TODO: emit message "psql not running to interface
time.sleep(1) time.sleep(1)
cur = self.db_conn.cursor() #how ugly is this? cur = self.db_conn.cursor() # how ugly is this?
return self._execute(cur, query, attrs, level+1) return self._execute(cur, query, attrs, level + 1)
except Exception as ex: except Exception as ex:
logger.debug("_execute exception: %s", ex) logger.debug("_execute exception: %s", ex)
self.db_conn.rollback()
def commit(self): def commit(self):
"""passes commit to db""" """passes commit to db"""

View file

@ -2,158 +2,163 @@ import brmbar
from .Currency import Currency from .Currency import Currency
from .Account import Account from .Account import Account
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Shop: class Shop:
""" BrmBar Shop """BrmBar Shop
Business logic so that only interaction is left in the hands Business logic so that only interaction is left in the hands
of the frontend scripts. """ of the frontend scripts."""
def __init__(self, db, currency, profits, cash, excess, deficit):
def __init__(self, db, currency_id, profits_id, cash_id, excess_id, deficit_id):
# Keep db as-is
self.db = db self.db = db
self.currency = currency # brmbar.Currency
self.profits = profits # income brmbar.Account for brmbar profit margins on items # Store all ids
self.cash = cash # our operational ("wallet") cash account self.currency_id = currency_id
self.excess = excess # account from which is deducted cash during inventory item fixing (when system contains less items than is the reality) self.profits_id = profits_id
self.deficit = deficit # account where is put cash during inventory item fixing (when system contains more items than is the reality) self.cash_id = cash_id
self.excess_id = excess_id
self.deficit_id = deficit_id
# Create objects where needed for legacy code
# brmbar.Currency
self.currency = Currency.load(self.db, id=self.currency_id)
# income brmbar.Account for brmbar profit margins on items
self.profits = Account.load(db, id=self.profits_id)
# our operational ("wallet") cash account
self.cash = Account.load(db, id=self.cash_id)
# account from which is deducted cash during inventory item
# fixing (when system contains less items than is the
# reality)
self.excess = Account.load(db, id=self.excess_id)
# account where is put cash during inventory item fixing (when
# system contains more items than is the reality)
self.deficit = Account.load(db, id=self.deficit_id)
@classmethod @classmethod
def new_with_defaults(cls, db): def new_with_defaults(cls, db):
return cls(db, # shop_class_initialization_data
currency = Currency.default(db), currency_id, profits_id, cash_id, excess_id, deficit_id = db.execute_and_fetch(
profits = Account.load(db, name = "BrmBar Profits"), "select currency_id, profits_id, cash_id, excess_id, deficit_id from public.shop_class_initialization_data()"
cash = Account.load(db, name = "BrmBar Cash"), )
excess = Account.load(db, name = "BrmBar Excess"), return cls(
deficit = Account.load(db, name = "BrmBar Deficit")) db,
currency_id=currency_id,
profits_id=profits_id,
cash_id=cash_id,
excess_id=excess_id,
deficit_id=deficit_id,
)
def sell(self, item, user, amount = 1): def sell(self, item, user, amount=1):
# Call the stored procedure for the sale # Call the stored procedure for the sale
logger.debug("sell: item.id=%s amount=%s user.id=%s self.currency.id=%s", logger.debug(
item.id, amount, user.id, self.currency.id) "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( res = self.db.execute_and_fetch(
"SELECT public.sell_item(%s, %s, %s, %s, %s)", "SELECT public.sell_item(%s, %s, %s, %s, %s)",
[item.id, amount, user.id, self.currency.id, [
"BrmBar sale of {0}x {1} to {2}".format(amount, item.name, user.name)] item.id,
)#[0] 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]) logger.debug("sell: res[0]=%s", res[0])
cost = res[0] cost = res[0]
self.db.commit() self.db.commit()
return cost return cost
# Sale: Currency conversion from item currency to shop currency
#(buy, sell) = item.currency.rates(self.currency)
#cost = amount * sell
#profit = amount * (sell - buy)
#transaction = self._transaction(responsible = user, description = "BrmBar sale of {}x {} to {}".format(amount, item.name, user.name)) def sell_for_cash(self, item, amount=1):
#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( cost = self.db.execute_and_fetch(
"SELECT public.sell_item_for_cash(%s, %s, %s, %s, %s)", "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)] item.id,
)[0]#[0] amount,
user.id,
self.currency.id,
"BrmBar sale of {0}x {1} for cash".format(amount, item.name),
],
)[0]
self.db.commit() self.db.commit()
return cost return cost
## Sale: Currency conversion from item currency to shop currency
#(buy, sell) = item.currency.rates(self.currency)
#cost = amount * sell
#profit = amount * (sell - buy)
#transaction = self._transaction(description = "BrmBar sale of {}x {} for cash".format(amount, item.name)) def undo_sale(self, item, user, amount=1):
#item.credit(transaction, amount, "Cash")
#self.cash.debit(transaction, cost, item.name)
#self.profits.debit(transaction, profit, "Margin on " + item.name)
#self.db.commit()
#return cost
def undo_sale(self, item, user, amount = 1):
# Undo sale; rarely needed
#(buy, sell) = item.currency.rates(self.currency)
#cost = amount * sell
#profit = amount * (sell - buy)
#transaction = self._transaction(responsible = user, description = "BrmBar sale UNDO of {}x {} to {}".format(amount, item.name, user.name))
#item.debit(transaction, amount, user.name + " (sale undo)")
#user.credit(transaction, cost, item.name + " (sale undo)")
#self.profits.credit(transaction, profit, "Margin repaid on " + item.name)
# Call the stored procedure for undoing a sale # Call the stored procedure for undoing a sale
cost = self.db.execute_and_fetch( cost = self.db.execute_and_fetch(
"SELECT public.undo_sale_of_item(%s, %s, %s, %s)", "SELECT public.undo_sale_of_item(%s, %s, %s, %s)",
[item.id, amount, user.id, user.currency.id, "BrmBar sale UNDO of {0}x {1} to {2}".format(amount, item.name, user.name)] [
)[0]#[0] item.id,
amount,
user.id,
user.currency.id,
"BrmBar sale UNDO of {0}x {1} to {2}".format(
amount, item.name, user.name
),
],
)[0]
self.db.commit() self.db.commit()
return cost return cost
def add_credit(self, credit, user): def add_credit(self, credit, user):
self.db.execute_and_fetch( self.db.execute_and_fetch(
"SELECT public.add_credit(%s, %s, %s, %s)", "SELECT public.add_credit(%s, %s, %s, %s)",
[self.cash.id, credit, user.id, user.name] [self.cash.id, credit, user.id, user.name],
) )
self.db.commit() self.db.commit()
#transaction = self._transaction(responsible = user, description = "BrmBar credit replenishment for " + user.name)
#self.cash.debit(transaction, credit, user.name)
#user.credit(transaction, credit, "Credit replenishment")
#self.db.commit()
def withdraw_credit(self, credit, user): def withdraw_credit(self, credit, user):
self.db.execute_and_fetch( self.db.execute_and_fetch(
"SELECT public.withdraw_credit(%s, %s, %s, %s)", "SELECT public.withdraw_credit(%s, %s, %s, %s)",
[self.cash.id, credit, user.id, user.name] [self.cash.id, credit, user.id, user.name],
) )
self.db.commit() self.db.commit()
#transaction = self._transaction(responsible = user, description = "BrmBar credit withdrawal for " + user.name)
#self.cash.credit(transaction, credit, user.name)
#user.debit(transaction, credit, "Credit withdrawal")
#self.db.commit()
def transfer_credit(self, userfrom, userto, amount): def transfer_credit(self, userfrom, userto, amount):
self.db.execute_and_fetch( self.db.execute_and_fetch(
"SELECT public.transfer_credit(%s, %s, %s, %s)", "SELECT public.transfer_credit(%s, %s, %s, %s, %s, %s)",
[self.cash.id, credit, user.id, user.name] [self.cash.id, amount, userfrom.id, userfrom.name, userto.id, userto.name],
) )
self.db.commit() self.db.commit()
#self.add_credit(amount, userto)
#self.withdraw_credit(amount, userfrom)
def buy_for_cash(self, item, amount = 1): def buy_for_cash(self, item, amount=1):
iamount = int(amount)
famount = float(iamount)
assert famount == amount, "amount is not integer value %s".format(amount)
cost = self.db.execute_and_fetch( cost = self.db.execute_and_fetch(
"SELECT public.buy_for_cash(%s, %s, %s, %s, %s)", "SELECT public.buy_for_cash(%s, %s, %s, %s, %s)",
[self.cash.id, item.id, amount, self.currency.id, item.name] [self.cash.id, item.id, iamount, self.currency.id, item.name],
)[0] )[0]
# Buy: Currency conversion from item currency to shop currency
#(buy, sell) = item.currency.rates(self.currency)
#cost = amount * buy
#transaction = self._transaction(description = "BrmBar stock replenishment of {}x {} for cash".format(amount, item.name))
#item.debit(transaction, amount, "Cash")
#self.cash.credit(transaction, cost, item.name)
self.db.commit() self.db.commit()
return cost return cost
def receipt_to_credit(self, user, credit, description): def receipt_to_credit(self, user, credit, description):
#transaction = self._transaction(responsible = user, description = "Receipt: " + description)
#self.profits.credit(transaction, credit, user.name)
#user.credit(transaction, credit, "Credit from receipt: " + description)
self.db.execute_and_fetch( self.db.execute_and_fetch(
"SELECT public.buy_for_cash(%s, %s, %s, %s, %s)", "SELECT public.receipt_reimbursement(%s, %s, %s, %s, %s)",
[self.profits.id, user.id, user.name, credit, description] [self.profits.id, user.id, user.name, credit, description],
)[0] )[0]
self.db.commit() self.db.commit()
def _transaction(self, responsible = None, description = None): def _transaction(self, responsible=None, description=None):
transaction = self.db.execute_and_fetch("INSERT INTO transactions (responsible, description) VALUES (%s, %s) RETURNING id", transaction = self.db.execute_and_fetch(
[responsible.id if responsible else None, description]) "INSERT INTO transactions (responsible, description) VALUES (%s, %s) RETURNING id",
[responsible.id if responsible else None, description],
)
transaction = transaction[0] transaction = transaction[0]
return transaction return transaction
@ -166,138 +171,78 @@ class Shop:
WHERE a.acctype = %s AND ts.side = %s WHERE a.acctype = %s AND ts.side = %s
""" """
if overflow is not None: if overflow is not None:
sumselect += ' AND a.name ' + ('NOT ' if overflow == 'exclude' else '') + ' LIKE \'%%-overflow\'' sumselect += (
cur = self.db.execute_and_fetch(sumselect, ["debt", 'debit']) " AND a.name "
+ ("NOT " if overflow == "exclude" else "")
+ " LIKE '%%-overflow'"
)
cur = self.db.execute_and_fetch(sumselect, ["debt", "debit"])
debit = cur[0] or 0 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 credit = credit[0] or 0
return debit - credit return debit - credit
def credit_negbalance_str(self, overflow=None): def credit_negbalance_str(self, overflow=None):
return self.currency.str(-self.credit_balance(overflow=overflow)) return self.currency.str(-self.credit_balance(overflow=overflow))
# XXX causing extra heavy delay ( thousands of extra SQL queries ), disabled
def inventory_balance(self): def inventory_balance(self):
balance = 0 resa = self.db.execute_and_fetch("SELECT * FROM public.inventory_balance()")
# Each inventory account has its own currency, res = resa[0]
# so we just do this ugly iteration logger.debug("inventory_balance resa = %s", resa)
cur = self.db.execute_and_fetchall("SELECT id FROM accounts WHERE acctype = %s", ["inventory"]) return res
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): 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="%%"): def account_list(self, acctype, like_str="%%"):
"""list all accounts (people or items, as per acctype)""" """list all accounts (people or items, as per acctype)"""
accts = [] accts = []
cur = self.db.execute_and_fetchall("SELECT id FROM accounts WHERE acctype = %s AND name ILIKE %s ORDER BY name ASC", [acctype, like_str]) cur = self.db.execute_and_fetchall(
#FIXME: sanitize input like_str ^ "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: for inventory in cur:
accts += [ Account.load(self.db, id = inventory[0]) ] accts += [Account.load(self.db, id=inventory[0])]
return accts return accts
def fix_inventory(self, item, amount): def fix_inventory(self, item, amount):
rv = self.db.execute_and_fetch( rv = self.db.execute_and_fetch(
"SELECT public.fix_inventory(%s, %s, %s, %s, %s, %s)", "SELECT public.fix_inventory(%s, %s, %s, %s, %s, %s)",
[item.id, item.currency.id, self.excess.id, self.deficit.id, self.currency.id, amount] [
item.id,
item.currency.id,
self.excess.id,
self.deficit.id,
self.currency.id,
amount,
],
)[0] )[0]
self.db.commit() self.db.commit()
return rv return rv
#amount_in_reality = amount
#amount_in_system = item.balance()
#(buy, sell) = item.currency.rates(self.currency)
#diff = abs(amount_in_reality - amount_in_system)
#buy_total = buy * diff
#if amount_in_reality > amount_in_system:
# transaction = self._transaction(description = "BrmBar inventory fix of {}pcs {} in system to {}pcs in reality".format(amount_in_system, item.name,amount_in_reality))
# item.debit(transaction, diff, "Inventory fix excess")
# self.excess.credit(transaction, buy_total, "Inventory fix excess " + item.name)
# self.db.commit()
# return True
#elif amount_in_reality < amount_in_system:
# transaction = self._transaction(description = "BrmBar inventory fix of {}pcs {} in system to {}pcs in reality".format(amount_in_system, item.name,amount_in_reality))
# item.credit(transaction, diff, "Inventory fix deficit")
# self.deficit.debit(transaction, buy_total, "Inventory fix deficit " + item.name)
# self.db.commit()
# return True
#else:
# transaction = self._transaction(description = "BrmBar inventory fix of {}pcs {} in system to {}pcs in reality".format(amount_in_system, item.name,amount_in_reality))
# item.debit(transaction, 0, "Inventory fix - amount was correct")
# item.credit(transaction, 0, "Inventory fix - amount was correct")
# self.db.commit()
# return False
def fix_cash(self, amount): def fix_cash(self, amount):
rv = self.db.execute_and_fetch( rv = self.db.execute_and_fetch(
"SELECT public.fix_cash(%s, %s, %s, %s)", "SELECT public.fix_cash(%s, %s, %s, %s)",
[self.excess.id, self.deficit.id, self.currency.id, amount] [self.excess.id, self.deficit.id, self.currency.id, amount],
)[0] )[0]
self.db.commit() self.db.commit()
return rv return rv
#amount_in_reality = amount
#amount_in_system = self.cash.balance()
#diff = abs(amount_in_reality - amount_in_system)
#if amount_in_reality > amount_in_system:
# transaction = self._transaction(description = "BrmBar cash inventory fix of {} in system to {} in reality".format(amount_in_system, amount_in_reality))
# self.cash.debit(transaction, diff, "Inventory fix excess")
# self.excess.credit(transaction, diff, "Inventory cash fix excess.")
# self.db.commit()
# return True
#elif amount_in_reality < amount_in_system:
# transaction = self._transaction(description = "BrmBar cash inventory fix of {} in system to {} in reality".format(amount_in_system, amount_in_reality))
# self.cash.credit(transaction, diff, "Inventory fix deficit")
# self.deficit.debit(transaction, diff, "Inventory fix deficit.")
# self.db.commit()
# return True
#else:
# return False
def consolidate(self): def consolidate(self):
msg = self.db.execute_and_fetch( msg = self.db.execute_and_fetch(
"SELECT public.make_consolidate_transaction(%s, %s, %s)", "SELECT public.make_consolidate_transaction(%s, %s, %s)",
[self.excess.id, self.deficit.id, self.profits.id] [self.excess.id, self.deficit.id, self.profits.id],
)[0] )[0]
#transaction = self._transaction(description = "BrmBar inventory consolidation")
#excess_balance = self.excess.balance()
#if excess_balance != 0:
# print("Excess balance {} debited to profit".format(-excess_balance))
# self.excess.debit(transaction, -excess_balance, "Excess balance added to profit.")
# self.profits.debit(transaction, -excess_balance, "Excess balance added to profit.")
#deficit_balance = self.deficit.balance()
#if deficit_balance != 0:
# print("Deficit balance {} credited to profit".format(deficit_balance))
# self.deficit.credit(transaction, deficit_balance, "Deficit balance removed from profit.")
# self.profits.credit(transaction, deficit_balance, "Deficit balance removed from profit.")
if msg != None: if msg != None:
print(msg) print(msg)
self.db.commit() self.db.commit()
def undo(self, oldtid): def undo(self, oldtid):
#description = self.db.execute_and_fetch("SELECT description FROM transactions WHERE id = %s", [oldtid])[0] transaction = self.db.execute_and_fetch(
#description = 'undo %d (%s)' % (oldtid, description) "SELECT public.undo_transaction(%s)", [oldtid]
)[0]
#transaction = self._transaction(description=description)
#for split in self.db.execute_and_fetchall("SELECT id, side, account, amount, memo FROM transaction_splits WHERE transaction = %s", [oldtid]):
# splitid, side, account, amount, memo = split
# memo = 'undo %d (%s)' % (splitid, memo)
# amount = -amount
# self.db.execute("INSERT INTO transaction_splits (transaction, side, account, amount, memo) VALUES (%s, %s, %s, %s, %s)", [transaction, side, account, amount, memo])
transaction = self.db.execute_and_fetch("SELECT public.undo_transaction(%s)",[oldtid])[0]
self.db.commit() self.db.commit()
return transaction return transaction

View file

@ -44,12 +44,15 @@ DECLARE
v_buy_rate NUMERIC; v_buy_rate NUMERIC;
v_cost NUMERIC; v_cost NUMERIC;
v_transaction_id public.transactions.id%TYPE; v_transaction_id public.transactions.id%TYPE;
v_item_currency_id public.accounts.currency%TYPE;
BEGIN BEGIN
-- this could fail and it would generate exception in python -- Get item's currency
-- FIXME: convert v_buy_rate < 0 into python exception SELECT currency
v_buy_rate := public.find_buy_rate(i_item_id, i_target_currency_id); INTO STRICT v_item_currency_id
-- this could fail and it would generate exception in python, even though it is not used FROM public.accounts
--v_sell_rate := public.find_sell_rate(i_item_id, i_target_currency_id); 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 -- Calculate cost and profit
v_cost := i_amount * v_buy_rate; v_cost := i_amount * v_buy_rate;
@ -60,12 +63,12 @@ BEGIN
-- the item -- the item
INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) INSERT INTO public.transaction_splits (transaction, side, account, amount, memo)
VALUES (i_transaction_id, 'debit', i_item_id, i_amount, VALUES (v_transaction_id, 'debit', i_item_id, i_amount,
'Cash'); 'Cash');
-- the cash -- the cash
INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) INSERT INTO public.transaction_splits (transaction, side, account, amount, memo)
VALUES (i_transaction_id, 'credit', i_cash_account_id, v_cost, VALUES (v_transaction_id, 'credit', i_cash_account_id, v_cost,
i_item_name); i_item_name);
-- Return the cost -- Return the cost

View file

@ -48,10 +48,10 @@ BEGIN
'Receipt: ' || i_description); 'Receipt: ' || i_description);
-- the "profit" -- the "profit"
INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) INSERT INTO public.transaction_splits (transaction, side, account, amount, memo)
VALUES (i_transaction_id, 'credit', i_profits_id, i_amount, i_user_name); VALUES (v_transaction_id, 'credit', i_profits_id, i_amount, i_user_name);
-- the user -- the user
INSERT INTO public.transaction_splits (transaction, side, account, amount, memo) INSERT INTO public.transaction_splits (transaction, side, account, amount, memo)
VALUES (i_transaction_id, 'credit', i_user_id, i_amount, 'Credit from receipt: ' || i_description); VALUES (v_transaction_id, 'credit', i_user_id, i_amount, 'Credit from receipt: ' || i_description);
END; END;
$$; $$;

View file

@ -0,0 +1,48 @@
--
-- 0021-constraints-on-numeric-columns.sql
--
-- #21 - stored function for adding constraints to numeric columns
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- TMA <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

@ -0,0 +1,85 @@
--
-- 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

@ -0,0 +1,94 @@
--
-- 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 :