Compare commits

..

No commits in common. "master" and "db-reconnect" have entirely different histories.

74 changed files with 289 additions and 3812 deletions

1
.gitignore vendored
View file

@ -1,2 +1 @@
.*.sw?
*~

View file

@ -1,2 +0,0 @@
barcodes*.svg
barcode-generator.txt

View file

@ -1,51 +0,0 @@
#!/usr/bin/python
#
# requires zint binary from zint package
#
from subprocess import Popen, PIPE
import sys
svghead = """<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" height="1052.3622" width="744.09448" version="1.1" id="svg2" inkscape:version="0.47 r22583" sodipodi:docname="barcodes.svg">
"""
svgfoot = """</svg>
"""
width = 5
scalex = 0.8
scaley = 0.8
p = 0
i = 0
j = 0
f = None
lines = sys.stdin.readlines()
for idx in xrange(len(lines)):
items = lines[idx].strip().split(';')
if idx % 30 == 0:
if f and not f.closed:
f.write(svgfoot)
f.close()
f = open('barcodes' + str(p) + '.svg','w')
p += 1
i = 0
j = 0
f.write(svghead)
elem = Popen(('./zint','--directsvg','--notext', '-d', items[1]), stdout = PIPE).communicate()[0].split('\n')
elem = elem[8:-2]
elem[0] = elem[0].replace('id="barcode"', 'transform="matrix(%f,0,0,%f,%f,%f)"' % (scalex, scaley, 50+i*140 , 180+j*140) )
elem.insert(-1, ' <text x="39.50" y="69.00" text-anchor="middle" font-family="Helvetica" font-size="14.0" fill="#000000" >%s</text>' % items[0])
f.write('\n'.join(elem)+'\n\n')
i += 1
if i >= width:
i = 0
j += 1
if not f.closed:
f.write(svgfoot)
f.close()

View file

@ -1,9 +0,0 @@
on brmbar:
select distinct barcode from barcodes b, transactions t, accounts a where t.responsible=a.id and time>'2015-01-01' and b.account=a.id order by barcode asc;
run this locally and paste output of previous command:
while read tmp; do echo "$tmp;$tmp";done|grep -v overflow|python2 ./barcode-generator.py
print resulting SVG files

2
brmbar3/.gitignore vendored
View file

@ -1,3 +1 @@
__pycache__
*.log
brmbar/*.pyc

View file

@ -1,85 +0,0 @@
BrmBar v3 Installation
======================
This is woefully incomplete; if you are deploying BrmBar, at this
point you will likely need to learn at least something aobut its
structure and internals. Some code modifications might even be
required too. Patches enhancing user configurability are welcome!
Maybe some things are missing. Ask the developers if you get in trouble,
e.g. in #brmlab on FreeNode.
Hardware Requirements
---------------------
* Display. Current UI is optimized for 4:3, 1024x768 display.
* Touchscreen. In emergency, you can use a mouse too, but it's
clumsy and scrolling is not too intuitive.
* Barcode reader. We want the kind that will behave as a HID device
and on scanning a barcode, it will send a CR-terminated scanned string.
* Physical keyboard stashed in vicinity will help. It is possible
to enter text (inventory names, receipt reasons) on the touchscreen,
but it's a bit frustrating.
* You will want to print a sheet of barcodes with names of all user
accounts; these will be then used by people to buy stuff using their
accounts - first scan barcode of the item, then scan your barcode,
voila. Scanning your barcode directly can bring the user to a screen
where they can see their credit and charge it too. See also USAGE.
Software Requirements
---------------------
* Developed and tested on Debian, but should work on other systems too.
* Python 3.
* QT4 with Python bindings:
* QT4 with the "Declarative" module, e.g. libqt4-declarative package.
* The PySide Qt4 bindings, e.g. python3-pyside.qtdeclarative package.
* Installing the qtcreator program may be helpful for QML testing
and development.
* PostgreSQL with Python pindings:
* The database server itself, e.g. postgresql package.
* PsyCoPg2, e.g. python3-psycopg2 package.
Ubuntu packages installation instructions
-----------------------------------------
* sudo apt-get install postgresql libqt4-declarative python3 python3-pyside.qtdeclarative python3-psycopg2 python3-pyqt4
Software Setup
--------------
* Create psql user and `brmbar` database.
brmuser@host:~> su postgres
postgres@host:/home/user> createuser -D brmuser
postgres@host:/home/user> su brmuser
brmuser@host:~> createdb brmbar
* The SQL schema in file `SQL` contains the required SQL tables,
but also INSERTs that add some rows essential for proper operation;
base currency and two base accounts. You *will* want to tweak the
currency name; default is `Kč` (the Czech crown), replace it with
your currency symbol. Then do `git grep 'Kč'` and replace all other
occurences of `Kč` in brmbar source with your currency name.
* Load the SQL schema stored in file `SQL` in the database.
brmuser@host:~/brmbar/brmbar3> psql brmbar
psql (9.1.8)
Type "help" for help.
brmbar=# \i SQL
* You should be able to fire up the GUI now and start entering data.
If you want to make sure all works as expected, execute the SQL
statements in file `SQL.test` (revisit for currency names too) which
will populate the database with a bit of sample data for testing.
* Regarding adding users at this point and for other usage instructions,
refer to the USAGE file.
TODO: Mention the actual commands to execute.
Troubleshooting
---------------
Assuming that you run brmbar from a terminal, if something gets
stuck, you can switch to the terminal by Alt-TAB, then kill brmbar
by the Ctrl-\ shortcut (sends SIGQUIT) and restart it.

View file

@ -1,64 +0,0 @@
How to "reset" the database - drop all history and keep only accounts with non-zero balance.
Legend:
> - SQL commands
$ - shell commands
Run the (full) inventory.
Get number of the first inventory TX.
> select id from account_balances where id in (select id from accounts where currency not in (select distinct currency from
transaction_nicesplits where transaction >= NUMBER_HERE and currency != 1 and memo like '%Inventory fix%') and acctype = 'inventory') and crbalance != 0 \g 'vynulovat'
$ ./brmbar-cli.py inventory `cat vynulovat | while read x; do echo $x 0; done`
Backup the database
$ pg_dump brmbar > backup.sql
Dump "> SELECT * FROM account_balances;" to file N.
Dump inventory to file nastavit FIXME.
Drop all transactions:
> delete from transaction_splits;
> delete from transactions;
Restore inventory:
$ cat nastavit | while read acc p amt; do ./brmbar-cli.py inventory $acc `echo $amt | grep -oE "^[0-9-]+"`; done
Restore cash balance:
$ cat N | grep debt | tr -s " " |cut -d \| -f 2,4 | while read acc p amt; do ./brmbar-cli.py changecredit $acc `echo $amt | grep -oE "^[0-9-]+"`; done
Delete zero-balance accounts:
> delete from accounts where accounts.id not in (select id from account_balances);
Delete orphaned barcodes:
> delete from barcodes where barcodes.account not in (select id from account_balances);
Delete orphaned currencies and exchange rates:
> CREATE OR REPLACE VIEW "a_tmp" AS
SELECT ts.account AS id, accounts.name, accounts.acctype, accounts.currency AS fff, (- sum(CASE WHEN (ts.side = 'credit'::transaction_split_side) THEN (- ts.amount) ELSE ts.amount END)) AS crbalance FROM (transaction_splits ts LEFT JOIN accounts ON ((accounts.id = ts.account))) GROUP BY ts.account, accounts.name, accounts.id, accounts.acctype ORDER BY (- sum(CASE WHEN (ts.side = 'credit'::transaction_split_side) THEN (- ts.amount) ELSE ts.amount END));
> delete from exchange_rates where source not in (select fff from a_tmp);
> delete from currencies where id not in (select fff from a_tmp);
> DROP VIEW "a_tmp";
Drop obsolete exchange rates:
> delete from exchange_rates where
valid_since <> (SELECT max(valid_since)
FROM exchange_rates e
WHERE e.target = exchange_rates.target and e.source = exchange_rates.source)
Restore system accounts:
> INSERT INTO "accounts" ("name", "currency", "acctype", "active")
VALUES ('BrmBar Profits', '1', 'income', '1');
> INSERT INTO "accounts" ("name", "currency", "acctype", "active")
VALUES ('BrmBar Excess', '1', 'income', '1');
> INSERT INTO "accounts" ("name", "currency", "acctype", "active")
VALUES ('BrmBar Deficit', '1', 'expense', '1');
> INSERT INTO "accounts" ("name", "currency", "acctype", "active")
VALUES ('BrmBar Cash', '1', 'cash', '1');
Restart brmbar.

View file

@ -1,81 +0,0 @@
BrmBar v3
=========
BrmBar is a management system for running a tiny hackerspace shop
with self-service usage based on trust and support for user accounts
and inventory tracking.
BrmBar offers a touchscreen-based user interface, identifies items
and users by barcodes scanned by a barcode reader, should run on any
decent Linux machine and stores its data in PostgreSQL database.
Features
--------
* Very simple user interface (using big touchscreen buttons and
barcode reader) that should enable even non-technical users to
do basic shopping with little to no training.
* Users may have their accounts they can load with money by
depositing larger sum of money in advance, then charging their
account when buying stuff. Of course, paying direct for cash
is also supported.
* Inventory and cash accounts are tracked so that you can make sure
there is no Club Mate mysteriously disappearing or if the amount
of cash in the cash box is not less than expected by the system.
* You can enter receipts for duct tapes and other necessities to be
financed by cash surplus generated by brmbar.
* Simple management operations (depositing and withdrawing money
from user accounts, entering receipts, stocking in new inventory)
can be also performed in the user interface even by non-technical
users with basic training.
* The database is based on the classical accounting paradigm.
This means no information is needlessly lost, you could even
make a GNUCash export and your accounting geeks will feel warm
and fuzzy.
* Multiple user interfaces available (and possible). The primary
user interface is based on QtQuick (Qt4 QML QtDeclarative).
User Interfaces
---------------
These UIs are provided:
* **brmbar-gui-qt4**: The default touchscreen-based UI. The Python side
provides an adapter object whose methods can be executed by the QML
code; ad-hoc directionary objects are used to exchange complex data
like account information.
* **brmbar-tui**: A trivial text-based "shell" UI that mimics a historic
interface used in the Brmlab hackerspace in the past. It supports only
selling items, querying item price and user account balance and
depositing money for the user accounts.
* **brmbar-cli**: A command-line interface intended for use in scripts
and remote usage when fixing problems. It is also meant to provide
advanced functionality like inventory revision that is too tedious
to implement in the Qt4 GUI and only the brmbar admins are expected
to do these tasks.
* **brmbar-web**: A simple read-only web interface to the stock list.
TODO
----
* The user interface needs some improvements, mainly regarding
scrolling in large lists.
* The brmbar-cli.py admin script for advanced/remote management
operations is largely unfinished. In the meantime, you need to use
SQL statements, sorry. Or finish it yourself. :-)
* It is common to have two stashes of cash, one in a cash box
in the shop, another in a vault (sometimes called "overflow")
where extra cash is stored. The brmbar model supports this,
but UI support needs to be added.
* Bitcoin support, somehow...
Some more TODO items may be listed in the GitHub issue tracker;
missing brmbar-gui-qt4 features are listed in the `TODO` file.
Other Resources
---------------
See the INSTALL file for setup instructions and USAGE file for
basic usage instructions. The doc/architecture file describes
the brmbar object model and briefly explains the brmbar Python
package.

View file

@ -1,11 +1,10 @@
CREATE SEQUENCE currencies_id_seq START WITH 2 INCREMENT BY 1;
CREATE SEQUENCE currencies_id_seq START WITH 1 INCREMENT BY 1;
CREATE TABLE currencies (
id INTEGER PRIMARY KEY NOT NULL DEFAULT NEXTVAL('currencies_id_seq'::regclass),
name VARCHAR(128) NOT NULL,
UNIQUE(name)
);
-- Some code depends on the primary physical currency to have id 1.
INSERT INTO currencies (id, name) VALUES (1, 'Kč');
INSERT INTO currencies (name) VALUES ('Kč');
CREATE TYPE exchange_rate_direction AS ENUM ('source_to_target', 'target_to_source');
CREATE TABLE exchange_rates (
@ -37,14 +36,10 @@ CREATE TABLE accounts (
currency INTEGER NOT NULL,
FOREIGN KEY (currency) REFERENCES currencies (id),
acctype account_type NOT NULL,
active BOOLEAN NOT NULL DEFAULT TRUE
acctype account_type NOT NULL
);
INSERT INTO accounts (name, currency, acctype) VALUES ('BrmBar Cash', (SELECT id FROM currencies WHERE name='Kč'), 'cash');
INSERT INTO accounts (name, currency, acctype) VALUES ('BrmBar Profits', (SELECT id FROM currencies WHERE name='Kč'), 'income');
INSERT INTO accounts (name, currency, acctype) VALUES ('BrmBar Excess', (SELECT id FROM currencies WHERE name='Kč'), 'income');
INSERT INTO accounts (name, currency, acctype) VALUES ('BrmBar Deficit', (SELECT id FROM currencies WHERE name='Kč'), 'expense');
CREATE SEQUENCE barcodes_id_seq START WITH 1 INCREMENT BY 1;
@ -54,9 +49,6 @@ CREATE TABLE barcodes (
account INTEGER NOT NULL,
FOREIGN KEY (account) REFERENCES accounts (id)
);
-- Barcode for cash
-- XXX Silently assume there is only one.
INSERT INTO barcodes (barcode, account) VALUES ('_cash_', (SELECT id FROM accounts WHERE acctype = 'cash'));
CREATE SEQUENCE transactions_id_seq START WITH 1 INCREMENT BY 1;
@ -98,30 +90,3 @@ CREATE VIEW account_balances AS
LEFT JOIN accounts ON accounts.id = ts.account
GROUP BY ts.account, accounts.name, accounts.acctype
ORDER BY crbalance ASC;
-- Transaction splits in a form that's nicer to query during manual inspection
CREATE VIEW transaction_nicesplits AS
SELECT ts.id AS id, ts.transaction AS transaction, ts.account AS account,
(CASE WHEN ts.side = 'credit' THEN -ts.amount ELSE ts.amount END) AS amount,
a.currency AS currency, ts.memo AS memo
FROM transaction_splits AS ts LEFT JOIN accounts AS a ON a.id = ts.account
ORDER BY ts.id;
-- List transactions with summary information regarding their cash element.
CREATE VIEW transaction_cashsums AS
SELECT t.id AS id, t.time AS time, SUM(credit_cash) AS cash_credit, SUM(debit_cash) AS cash_debit, a.name AS responsible, t.description AS description
FROM transactions AS t
LEFT JOIN (SELECT cts.amount AS credit_cash, cts.transaction AS cts_t
FROM transaction_nicesplits AS cts
LEFT JOIN accounts AS a ON a.id = cts.account OR a.id = cts.account
WHERE a.currency = (SELECT currency FROM accounts WHERE name = 'BrmBar Cash')
AND a.acctype IN ('cash', 'debt')
AND cts.amount < 0) credit ON cts_t = t.id
LEFT JOIN (SELECT dts.amount AS debit_cash, dts.transaction AS dts_t
FROM transaction_nicesplits AS dts
LEFT JOIN accounts AS a ON a.id = dts.account OR a.id = dts.account
WHERE a.currency = (SELECT currency FROM accounts WHERE name = 'BrmBar Cash')
AND a.acctype IN ('cash', 'debt')
AND dts.amount > 0) debit ON dts_t = t.id
LEFT JOIN accounts AS a ON a.id = t.responsible
GROUP BY t.id, a.name ORDER BY t.id DESC;

View file

@ -1,57 +0,0 @@
CREATE OR REPLACE FUNCTION accounts_id_seq_value()
RETURNS bigint
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
result bigint;
BEGIN
SELECT last_value FROM accounts_id_seq
INTO result;
RETURN result;
END;
$$;
CREATE OR REPLACE FUNCTION transactions_id_seq_value()
RETURNS bigint
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
result bigint;
BEGIN
SELECT last_value FROM transactions_id_seq
INTO result;
RETURN result;
END;
$$;
CREATE OR REPLACE FUNCTION transaction_splits_id_seq_value()
RETURNS bigint
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
result bigint;
BEGIN
SELECT last_value FROM transaction_splits_id_seq
INTO result;
RETURN result;
END;
$$;
CREATE OR REPLACE FUNCTION currencies_id_seq_value()
RETURNS bigint
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
DECLARE
result bigint;
BEGIN
SELECT last_value FROM currencies_id_seq
INTO result;
RETURN result;
END;
$$;

View file

@ -1,59 +0,0 @@
--RESET search_path;
SELECT pg_catalog.set_config('search_path', '', false);
-- intoduce implementation schema
CREATE SCHEMA IF NOT EXISTS brmbar_implementation;
-- version table (with initialization)
CREATE TABLE IF NOT EXISTS brmbar_implementation.brmbar_schema (
ver INTEGER NOT NULL
);
DO $$
DECLARE v INTEGER;
BEGIN
SELECT ver FROM brmbar_implementation.brmbar_schema INTO v;
IF v IS NULL THEN
INSERT INTO brmbar_implementation.brmbar_schema (ver) VALUES (1);
END IF;
END;
$$;
CREATE OR REPLACE FUNCTION brmbar_implementation.has_exact_schema_version(
IN i_ver INTEGER NOT NULL
) RETURNS INTEGER
VOLATILE NOT LEAKPROOF LANGUAGE plpgsql AS $$
DECLARE
v_ver INTEGER;
BEGIN
SELECT ver INTO STRICT v_ver FROM brmbar_implementation.brmbar_schema;
IF v_ver IS NULL or v_ver <> i_ver THEN
RAISE EXCEPTION 'Invalid brmbar schema version';
END IF;
RETURN v_ver;
/*
EXCEPTION
WHEN NO_DATA_FOUND THEN
RAISE EXCEPTION 'PID % not found';
WHEN TOO_MANY_ROWS THEN
RAISE EXCEPTION 'PID % not unique';
*/
END;
$$;
CREATE OR REPLACE FUNCTION brmbar_implementation.upgrade_schema_version_to(
IN i_ver INTEGER NOT NULL
) RETURNS INTEGER
VOLATILE NOT LEAKPROOF LANGUAGE plpgsql AS $$
DECLARE
v_ver INTEGER;
BEGIN
SELECT brmbar_implementation.has_exact_schema_version(i_ver) INTO v_ver;
IF v_ver + 1 = i_ver THEN
UPDATE brmbar_implementation.brmbar_schema SET ver = i_ver;
ELSE
RAISE EXCEPTION 'Invalid brmbar schema version';
END IF;
RETURN i_ver;
END;
$$;
-- vim: set ft=plsql :

View file

@ -1,61 +0,0 @@
--RESET search_path
SELECT pg_catalog.set_config('search_path', '', false);
--- upgrade schema
DO $upgrade_block$
DECLARE
current_ver INTEGER;
BEGIN
-- confirm that we are upgrading from version 1
SELECT brmbar_implementation.has_exact_schema_version(1) INTO current_ver;
IF current_ver <> 1 THEN
RAISE EXCEPTION 'BrmBar schema version % cannot be upgraded to version 2.', current_ver;
END IF;
-- structural changes
-- TRADING ACCOUNTS
--START TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- currency trading accounts - account type
ALTER TYPE public.account_type ADD VALUE IF NOT EXISTS 'trading';
-- constraint needed for foreign key in currencies table
ALTER TABLE public.accounts ADD CONSTRAINT accounts_id_acctype_key UNIQUE(id, acctype);
-- add columns to currencies to record the trading account associated with the currency
ALTER TABLE public.currencies
ADD COLUMN IF NOT EXISTS trading_account integer,
ADD COLUMN IF NOT EXISTS trading_account_type account_type GENERATED ALWAYS AS ('trading'::public.account_type) STORED;
-- make trading accounts (without making duplicates)
INSERT INTO public.accounts ("name", "currency", acctype)
SELECT
'Currency Trading Account: ' || c."name",
c.id,
'trading'::public.account_type
FROM public.currencies AS c
WHERE NOT EXISTS (
SELECT 1
FROM public.accounts a
WHERE a.currency = c.id AND a.acctype = 'trading'::public.account_type
);
-- record the trading account IDs in currencies table
UPDATE public.currencies AS c SET (trading_account) = (SELECT a.id FROM public.accounts AS a WHERE a.currency = c.id AND c.acctype = 'trading'::public.account_type);
-- foreign key to check the validity of currency trading account reference
ALTER TABLE public.currencies
ADD CONSTRAINT currencies_trading_fkey FOREIGN KEY (trading_account, trading_account_type)
REFERENCES xaccounts(id,acctype) DEFERRABLE INITIALLY DEFERRED;
--COMMIT AND CHAIN;
SELECT brmbar_implementation.upgrade_schema_version_to(2) INTO current_ver;
-- end of upgrade do block
end
$upgrade_block$;
-- vim: set ft=plsql :

View file

@ -1,5 +1,26 @@
This reprensents some generic features that would need to be implemented
in brmbar-gui-qt4 to have a fully fledged user interface.
+ Management view
+ Stock management link
+ User management link
+ Bilance overview (Cash, Profit, Credit total)
+ Item picker from list
+ User management
+ List of users
+ Withdrawal of user credit
+ Numerical manual entry support
+ Use for credit charge
+ Use for withdrawal
+ Restocking view (Stock management)
+ Item picker with edit button
+ Item editor (name, buy price, sale price, quantity)
+ Item-barcode assignment
+ Support for adding new items
+ Alphanumeric manual entry support
+ Use in item editor
+ Withdrawal for brmbar receipts
1. User responsible
2. Amount and description
** At this point it should be good enough to deploy **
. User management
- Add user

View file

@ -1,153 +0,0 @@
Quick Guide
-----------
* I want to buy for cash: I scan item's barcode, press **Pay by Cash** and pour
money into the cash box.
* I want to buy from credit: I scan item's barcode, then my barcode.
(If you don't have your barcode printed out, you can also type your
username on a physical keyboard.)
* I want to put money on credit: press **Charge**, I scan my barcode,
type some amount, press **Charge** and put money in the cash box.
Advanced Operations
-------------------
* I want to withdraw funds from my (positive) credit:
Press **Management**, choose **User Mgmt**, scan your barcode,
press the Withdraw Amount and type the amount. Then take the money
from the cash box.
* I want to stock in some inventory (that's been in brmbar before):
Press **Management**, **Stock Mgmt**, scan barcode of the item, edit
the purchase price (or also the selling price and label), press
**Restock** and enter the quantity of stocked in piece. Press **Save**.
Toss the bill (if possible with the current written date, to allow
pairing) to brmbar.
* I want to stock in some new inventory: Press **Management**, **Stock
Mgmt**, press **Add new item**, enter the name, purchase and selling price,
press **Create**. Then press **Restock**, enter the quantity stocked in.
Scan the item's barcode and press **Save**. Toss the bill in brmbar.
* I want to bill the brmbar with some small expenses like duct tape:
Press **Management** and **Receipt**. Press **Description** and write
a brief description of the bill. Press **Edit** near the **Money Amount**
and enter the amount. Scan *your* barcode. The operation is finished
by pressing **Create**. Toss bill (inscribed with the current date
to ease pairing) to brmbar.
General Notes
-------------
The system expects that we take money from the cash box right away.
If you don't want to (or there is e.g. not enough money), put money
on your credit account instead (see above). Please always do that
(never *I'll remember and I'll take money later*) so that there is
a record that the cash box and system records are not in sync and
there are no irregularities.
To enter text (or numbers too), you can use both the on-screen keyboard
and the physical keyboard nearby.
Administrative Usage
--------------------
* The most common administrative action you will need to do is adding
new user (also called debt or credit) accounts. The GUI support for
this is not implemented yet, but the `brmbar-cli.py` UI allows it:
./brmbar-cli.py adduser joehacker
Afterwards, print out a barcode saying "joehacker" and stick that
somewhere nearby; scanning that barcode will allow access to this
account (and so will typing "joehacker" on a physical keyboard).
* If your inventory stock count or cash box amount does not match
the in-system data, you will need to make a corrective transaction.
To fix cash amount to reality in which you counted 1234Kč, use
./brmbar-cli.py fixcash 1234
whereas to fix amount of a particular stock, use
./brmbar-cli.py inventory-interactive
then scan the item barcode and then enter the right amount.
* If you want to view recent transactions, run
psql brmbar
select * from transaction_cashsums;
* If you want to undo a transaction, get its id (using the select above)
and run
./brmbar-cli.py undo ID
* If you want to get overview of the financial situation, run
./brmbar-cli.py stats
The following items represent "material", "tangible" assets:
* Cash - how much should be in the money box
* Overflow - how much cash is stored in overflow credit accounts (pockets of admins)
* Inventory - how much worth (buy price) is the current inventory stock
I.e., cash plus overflow plus inventory is how much brmbar is worth
and cash plus overflow is how much brmbar can spend right now.
The following items represent "virtual" accounts which determine
the logical composition of the assets:
* Credit - sum of all credit accounts, i.e. money stored in brmbar by its users;
i.e. how much of the assets is users' money
* Profit - accumulated profit made by brmbar on buy/sell margins (but receipts
and inventory deficits are subtracted); i.e. how much of the assets is brmbar's
own money
* Fixups - sum of gains and losses accrued by inventory fixups, i.e. stemming
from differences between accounting and reality - positive is good, negative
is bad; this amount is added to profit on consolidation
The total worth of the material and virtual accounts should be equal.
Useful SQL queries
------------------
* Compute sum of sold stock:
select sum(amount) from transactions
left join transaction_splits on transaction_splits.transaction = transactions.id
where description like '% sale %' and side = 'debit';
* List of items not covered by inventory check:
select * from account_balances
where id not in (select account from transactions
left join transaction_splits on transaction_splits.transaction = transactions.id
where description like '% inventory %')
and acctype = 'inventory';
* List all cash transactions:
select time, transactions.id, description, responsible, amount from transactions
left join transaction_splits on transaction_splits.transaction = transactions.id
where transaction_splits.account = 1;
* List all inventory items ordered by their cummulative worth:
select foo.*, foo.rate * -foo.crbalance as worth from
(select account_balances.*,
(select exchange_rates.rate from exchange_rates, accounts
where exchange_rates.target = accounts.currency
and accounts.id = account_balances.id
order by exchange_rates.valid_since limit 1) as rate
from account_balances where account_balances.acctype = 'inventory')
as foo order by worth;

View file

@ -1,8 +0,0 @@
Accounts with multiple barcodes:
SELECT accounts.name,barcodes.account,barcodes.barcode
FROM "barcodes"
join accounts on accounts.id = barcodes.account
where barcodes.account in (select a from (select count(*) as c, account as a from barcodes group by account) as dt where c > 1)
ORDER BY "account" DESC

View file

@ -1,7 +0,0 @@
#!/bin/sh
case $1 in
alert) mplayer -really-quiet ~/trombone.wav & ;;
limit) mplayer -really-quiet ~/much.wav & ;;
charge) mplayer -really-quiet ~/charge.wav & ;;
esac

View file

@ -1,42 +0,0 @@
#! /usr/bin/env python3
import argparse
import brmbar
import math
from brmbar import Database
def main():
parser = argparse.ArgumentParser(usage = "File format: EAN amount total_price name, e.g. 4001242002377 6 167.40 Chio Tortillas")
parser.add_argument("filename")
args = parser.parse_args()
db = Database.Database("dbname=brmbar")
shop = brmbar.Shop.new_with_defaults(db)
currency = shop.currency
# ...
total = 0
with open(args.filename) as fin:
for line in fin:
split = line.split(" ")
ean, amount, price_total, name = split[0], int(split[1]), float(split[2]), " ".join(split[3:])
name = name.strip()
price_buy = price_total / amount
acct = brmbar.Account.load_by_barcode(db, ean)
if not acct:
print("Creating account for EAN {} '{}'".format(ean, name))
invcurr = brmbar.Currency.create(db, name)
acct = brmbar.Account.create(db, name, invcurr, "inventory")
acct.add_barcode(ean)
price_sell = max(math.ceil(price_buy * 1.15), price_buy)
acct.currency.update_sell_rate(currency, price_sell)
acct.currency.update_buy_rate(currency, price_buy)
cash = shop.buy_for_cash(acct, amount)
total += cash
print("Increased by {}, take {} from cashbox".format(amount, cash))
print("Total is {}".format(total))
if __name__ == "__main__":
main()

View file

@ -1,266 +1,61 @@
#!/usr/bin/python3
import sys
from brmbar import Database
import psycopg2
import brmbar
def help():
print("""BrmBar v3 (c) Petr Baudis <pasky@ucw.cz> 2012-2013
Usage: brmbar-cli.py COMMAND ARGS...
1. Commands pertaining the standard operation
showcredit USER
changecredit USER +-AMT
sellitem {USER|"cash"} ITEM +-AMT
You can use negative AMT to undo a sale.
restock ITEM AMT
userinfo USER
userlog USER TIMESTAMP
iteminfo ITEM
2. Management commands
listusers
List all user accounts in the system.
listitems
List all item accounts in the system.
stats
A set of various balances as shown in the Management
screen of the GUI.
adduser USER
Add user (debt) account with given username.
undo TRANSID
Commit a transaction that reverses all splits of a transaction with
a given id (to find out that id: select * from transaction_cashsums;)
3. Inventorization
inventory ITEM1 NEW_AMOUNT1 ITEM2 NEW_AMOUNT2
Inventory recounting (fixing the number of items)
inventory-interactive
Launches interactive mode for performing inventory with barcode reader
fixcash AMT
Fixes the cash and puts money difference into excess or deficit account
consolidate
Wraps up inventory + cash recounting, transferring the excess and
deficit accounts balance to the profits account and resetting them
USER and ITEM may be barcodes or account ids. AMT may be
both positive and negative amount (big difference to other
user interfaces; you can e.g. undo a sale!).
For users, you can use their name as USER as their username
is also the barcode. For items, use listitems command first
to find out the item id.
EXAMPLES:
Transfer 35Kc from pasky to sachy:
$ ./brmbar-cli.py changecredit pasky -35
$ ./brmbar-cli.py changecredit sachy +35
Buy one RaspberryPi for cash from commandline:
$ ./brmbar-cli.py listitems | grep -i raspberry
Raspberry Pi 2 1277 1.00 pcs
$ ./brmbar-cli.py sellitem cash 1277 1
""")
sys.exit(1)
def load_acct(inp):
acct = None
if inp.isdigit():
acct = brmbar.Account.load(db, id = inp)
if acct is None:
acct = brmbar.Account.load_by_barcode(db, inp)
if acct is None:
print("Cannot map account " + inp, file=sys.stderr)
exit(1)
return acct
def load_user(inp):
acct = load_acct(inp)
if acct.acctype != "debt":
print("Bad account " + inp + " type " + acct.acctype, file=sys.stderr)
exit(1)
return acct
def load_item(inp):
acct = load_acct(inp)
if acct.acctype != "inventory":
print("Bad account " + inp + " type " + acct.acctype, file=sys.stderr)
exit(1)
return acct
def load_item_by_barcode(inp):
acct = brmbar.Account.load_by_barcode(db, inp)
if acct.acctype != "inventory":
print("Bad EAN " + inp + " type " + acct.acctype, file=sys.stderr)
exit(1)
return acct
db = Database.Database("dbname=brmbar")
db = psycopg2.connect("dbname=brmbar")
shop = brmbar.Shop.new_with_defaults(db)
currency = shop.currency
if len(sys.argv) <= 1:
help()
active_inv_item = None
active_credit = None
for line in sys.stdin:
barcode = line.rstrip()
if sys.argv[1] == "showcredit":
acct = load_user(sys.argv[2])
print("{}: {}".format(acct.name, acct.negbalance_str()))
if barcode[0] == "$":
credits = {'$02': 20, '$05': 50, '$10': 100, '$20': 200, '$50': 500, '$1k': 1000}
credit = credits[barcode]
if credit is None:
print("Unknown barcode: " + barcode)
continue
print("CREDIT " + str(credit))
active_inv_item = None
active_credit = credit
continue
elif sys.argv[1] == "changecredit":
acct = load_user(sys.argv[2])
amt = int(sys.argv[3])
if amt > 0:
shop.add_credit(credit = amt, user = acct)
elif amt < 0:
shop.withdraw_credit(credit = -amt, user = acct)
print("{}: {}".format(acct.name, acct.negbalance_str()))
if barcode == "SCR":
print("SHOW CREDIT")
active_inv_item = None
active_credit = None
continue
elif sys.argv[1] == "sellitem":
if sys.argv[2] == "cash":
uacct = shop.cash
else:
uacct = load_user(sys.argv[2])
iacct = load_item(sys.argv[3])
amt = int(sys.argv[4])
if amt > 0:
if uacct == shop.cash:
shop.sell_for_cash(item = iacct, amount = amt)
acct = brmbar.Account.load_by_barcode(db, barcode)
if acct is None:
print("Unknown barcode: " + barcode)
continue
if acct.acctype == 'debt':
if active_inv_item is not None:
cost = shop.sell(item = active_inv_item, user = acct)
print("{} has bought {} for {} and now has {} balance".format(acct.name, active_inv_item.name, currency.str(cost), acct.negbalance_str()))
elif active_credit is not None:
shop.add_credit(credit = active_credit, user = acct)
print("{} has added {} credit and now has {} balance".format(acct.name, currency.str(active_credit), acct.negbalance_str()))
else:
shop.sell(item = iacct, user = uacct, amount = amt)
elif amt < 0:
shop.undo_sale(item = iacct, user = uacct, amount = -amt)
print("{}: {}".format(uacct.name, uacct.balance_str() if uacct == shop.cash else uacct.negbalance_str()))
print("{}: {}".format(iacct.name, iacct.balance_str()))
print("{} has {} balance".format(acct.name, acct.negbalance_str()))
active_inv_item = None
active_credit = None
elif sys.argv[1] == "userinfo":
acct = load_user(sys.argv[2])
print("{} (id {}): {}".format(acct.name, acct.id, acct.negbalance_str()))
elif acct.acctype == 'inventory':
buy, sell = acct.currency.rates(currency)
print("{} costs {} with {} in stock".format(acct.name, currency.str(sell), int(acct.balance())))
active_inv_item = acct
active_credit = None
res = db.execute_and_fetchall("SELECT barcode FROM barcodes WHERE account = %s", [acct.id])
print("Barcodes: " + ", ".join(map((lambda r: r[0]), res)))
elif sys.argv[1] == "userlog":
acct = load_user(sys.argv[2])
timestamp = sys.argv[3]
res = db.execute_and_fetchall("SELECT * FROM transaction_cashsums WHERE responsible=%s and time > TIMESTAMP %s ORDER BY time", [acct.name,timestamp])
for transaction in res:
print('\t'.join([str(f) for f in transaction]))
elif sys.argv[1] == "iteminfo":
acct = load_item(sys.argv[2])
print("{} (id {}): {} pcs".format(acct.name, acct.id, acct.balance()))
(buy, sell) = acct.currency.rates(currency)
print("Buy: " + currency.str(buy) + " Sell: " + currency.str(sell));
res = db.execute_and_fetchall("SELECT barcode FROM barcodes WHERE account = %s", [acct.id])
print("Barcodes: " + ", ".join(map((lambda r: r[0]), res)))
elif sys.argv[1] == "listusers":
for acct in shop.account_list("debt"):
print("{}\t{}\t{}".format(acct.name, acct.id, acct.negbalance_str()))
elif sys.argv[1] == "listitems":
for acct in shop.account_list("inventory"):
print("{}\t{}\t{} pcs".format(acct.name, acct.id, acct.balance()))
elif sys.argv[1] == "stats":
print("--- Material Assets ---")
print("Cash: {}".format(shop.cash.balance_str()))
print("Overflow: {}".format(shop.currency.str(shop.credit_balance(overflow='only'))))
print("Inventory: {}".format(shop.inventory_balance_str()))
print("--- Logical Accounts ---")
print("Credit: {}".format(shop.credit_negbalance_str(overflow='exclude')))
print("Profit: {}".format(shop.profits.balance_str()))
print("Fixups: {} (excess {}, deficit {})".format(
-shop.excess.balance() - shop.deficit.balance(),
shop.excess.negbalance_str(),
shop.deficit.balance_str()))
elif sys.argv[1] == "adduser":
acct = brmbar.Account.create(db, sys.argv[2], brmbar.Currency.load(db, id = 1), 'debt')
acct.add_barcode(sys.argv[2]) # will commit
print("{}: id {}".format(acct.name, acct.id));
elif sys.argv[1] == "undo":
newtid = shop.undo(int(sys.argv[2]))
print("Transaction %d undone by reverse transaction %d" % (int(sys.argv[2]), newtid))
elif sys.argv[1] == "inventory":
if (len(sys.argv) % 2 != 0 or len(sys.argv) < 4):
print ("Invalid number of parameters, count your parameters.")
else:
for i in range(2, len(sys.argv), 2):
iacct = load_item(sys.argv[i])
iamt = int(sys.argv[i+1])
print("Current state {} (id {}): {} pcs".format(iacct.name, iacct.id, iacct.balance()))
if shop.fix_inventory(item = iacct, amount = iamt):
print("New state {} (id {}): {} pcs".format(iacct.name, iacct.id, iacct.balance()))
else:
print ("No action needed amount is correct.")
elif sys.argv[1] == "inventory-interactive":
print("Inventory interactive mode. To exit interactive mode just enter empty barcode")
while True:
barcode = str(input("Enter barcode:"))
fuckyou = input("fuckyou")
if barcode == "":
break
iacct = brmbar.Account.load_by_barcode(db, barcode)
amount = str(input("What is the amount of {} in reality (expected: {} pcs):".format(iacct.name, iacct.balance())))
if amount == "":
break
elif int(amount) > 10000:
print("Ignoring too high amount {}, assuming barcode was mistakenly scanned instead".format(amount))
else:
iamt = int(amount)
print("Current state {} (id {}): {} pcs".format(iacct.name, iacct.id, iacct.balance()))
if shop.fix_inventory(item = iacct, amount = iamt):
print("New state {} (id {}): {} pcs".format(iacct.name, iacct.id, iacct.balance()))
else:
print("No action needed, amount is correct.")
print("End of processing. Bye")
elif sys.argv[1] == "fixcash" or sys.argv[1] == "changecash":
if (len(sys.argv) != 3):
print ("Invalid number of parameters, check your parameters.")
else:
print("Current Cash is : {}".format(shop.cash.balance_str()))
iamt = int(sys.argv[2])
if shop.fix_cash(amount = iamt):
print("New Cash is : {}".format(shop.cash.balance_str()))
else:
print ("No action needed amount is the same.")
elif sys.argv[1] == "consolidate":
if (len(sys.argv) != 2):
print ("Invalid number of parameters, check your parameters.")
else:
shop.consolidate()
elif sys.argv[1] in {"restock", "restock_ean"}:
if (len(sys.argv) != 4):
print ("Invalid number of parameters, check your parameters.")
else:
iacct = (load_item if sys.argv[1] == "restock" else load_item_by_barcode)(sys.argv[2])
oldbal = iacct.balance()
amt = int(sys.argv[3])
cash = shop.buy_for_cash(iacct, amt);
print("Old amount {}, increased by {}, take {} from cashbox".format(oldbal, amt, cash))
else:
help()
print("invalid account type {}".format(acct.acctype))
active_inv_item = None
active_credit = None

View file

@ -1,7 +1,6 @@
#!/usr/bin/python3
import sys
import subprocess
from PySide import QtCore, QtGui, QtDeclarative
@ -9,15 +8,6 @@ from brmbar import Database
import brmbar
# User credit balance limit; sale will fail when balance is below this limit.
LIMIT_BALANCE = -200
# When below this credit balance, an alert hook script (see below) is run.
ALERT_BALANCE = 0
# This script is executed when a user is buying things and their balance is
# below LIMIT_BALANCE (with argument "limit") or below ALERT_BALANCE
# (with argument "alert").
ALERT_SCRIPT = "./alert.sh"
class ShopAdapter(QtCore.QObject):
""" Interface between QML and the brmbar package """
@ -39,18 +29,6 @@ class ShopAdapter(QtCore.QObject):
map["price"] = str(sell)
return map
def acct_inventory_map2(self, acct):
buy, sell = 666, 666
map = acct.__dict__.copy()
map["balance"] = "{:.0f}".format(666)
map["buy_price"] = str(buy)
map["price"] = str(sell)
return map
def acct_cash_map(self, acct):
map = acct.__dict__.copy()
return map
def acct_map(self, acct):
if acct is None:
return None
@ -58,8 +36,6 @@ class ShopAdapter(QtCore.QObject):
return self.acct_debt_map(acct)
elif acct.acctype == "inventory":
return self.acct_inventory_map(acct)
elif acct.acctype == "cash":
return self.acct_cash_map(acct)
else:
return None
@ -88,18 +64,6 @@ class ShopAdapter(QtCore.QObject):
db.commit()
return acct
@QtCore.Slot('QVariant', 'QVariant', result='QVariant')
def canSellItem(self, itemid, userid):
user = brmbar.Account.load(db, id = userid)
if -user.balance() > ALERT_BALANCE:
return True
elif -user.balance() > LIMIT_BALANCE:
subprocess.call(["sh", ALERT_SCRIPT, "alert"])
return True
else:
subprocess.call(["sh", ALERT_SCRIPT, "limit"])
return False
@QtCore.Slot('QVariant', 'QVariant', result='QVariant')
def sellItem(self, itemid, userid):
user = brmbar.Account.load(db, id = userid)
@ -115,7 +79,6 @@ class ShopAdapter(QtCore.QObject):
@QtCore.Slot('QVariant', 'QVariant', result='QVariant')
def chargeCredit(self, credit, userid):
subprocess.call(["sh", ALERT_SCRIPT, "charge"])
user = brmbar.Account.load(db, id = userid)
shop.add_credit(credit = credit, user = user)
balance = user.negbalance_str()
@ -130,19 +93,6 @@ class ShopAdapter(QtCore.QObject):
db.commit()
return balance
@QtCore.Slot('QVariant', 'QVariant', 'QVariant', result='QVariant')
def newTransfer(self, uidfrom, uidto, amount):
ufrom = brmbar.Account.load(db, id=uidfrom)
uto = brmbar.Account.load(db, id=uidto)
shop.transfer_credit(ufrom, uto, amount = amount)
db.commit()
return currency.str(float(amount))
@QtCore.Slot('QVariant', result='QVariant')
def balance_user(self, userid):
user = brmbar.Account.load(db, id=userid)
return user.negbalance_str()
@QtCore.Slot(result='QVariant')
def balance_cash(self):
balance = shop.cash.balance_str()
@ -170,9 +120,9 @@ class ShopAdapter(QtCore.QObject):
db.commit()
return alist
@QtCore.Slot('QVariant', result='QVariant')
def itemList(self, query):
alist = [ self.acct_inventory_map2(a) for a in shop.account_list("inventory", like_str="%%"+query+"%%") ]
@QtCore.Slot(result='QVariant')
def itemList(self):
alist = [ self.acct_inventory_map(a) for a in shop.account_list("inventory") ]
db.commit()
return alist

View file

@ -1,3 +1,4 @@
// import QtQuick 1.0 // to target S60 5th Edition or Maemo 5
import QtQuick 1.1
Rectangle {
@ -9,7 +10,7 @@ Rectangle {
property string text: "Button"
property int fontSize: 0.768 * 60
property string btnColor: "#aaaaaa"
property variant btnColor: "#aaaaaa"
signal buttonClick
onButtonClick: { /* Supplied by component user. */ }

View file

@ -1,3 +1,4 @@
// import QtQuick 1.0 // to target S60 5th Edition or Maemo 5
import QtQuick 1.1
Rectangle {
@ -5,8 +6,8 @@ Rectangle {
width: 320
height: 65
property variant now: new Date()
property string textColor: "#000000"
property real textSize: 0.768 * 16
property variant textColor: "#000000"
property variant textSize: 0.768 * 16
Timer {
id: clockUpdater
interval: 1000 // update clock every second

View file

@ -4,6 +4,6 @@ BarKeyboard {
keys: "0123456789<qwertyuiop-asdfghjkl/+^zxcvbnm ,."
gridRows: 5
gridColumns: 11
buttonWidth: 90
buttonHeight: 80
buttonWidth: 80
buttonHeight: 70
}

View file

@ -5,8 +5,8 @@ Grid {
property string enteredText: ""
property int gridRows: 0
property int gridColumns: 0
property int buttonWidth: 80
property int buttonHeight: 80
property int buttonWidth: 70
property int buttonHeight: 70
property bool shift: false

View file

@ -10,9 +10,9 @@ Rectangle {
property int scrollbarWidth: 20
property string color: "white"
property real baseOpacityOff: 0.4
property real baseOpacityOn: 0.9
property variant color: "white"
property variant baseOpacityOff: 0.4
property variant baseOpacityOn: 0.9
radius: vertical ? width/2 : height/2

View file

@ -1,12 +1,13 @@
// import QtQuick 1.0 // to target S60 5th Edition or Maemo 5
import QtQuick 1.1
Item {
id: main_hint
width: 1150
width: 894
height: 80
property string hint_goal: ""
property string hint_action: ""
property variant hint_goal: ""
property variant hint_action: ""
Text {
id: text1

View file

@ -1,3 +1,4 @@
// import QtQuick 1.0 // to target S60 5th Edition or Maemo 5
import QtQuick 1.1
TextInput {

View file

@ -1,8 +1,10 @@
// import QtQuick 1.0 // to target S60 5th Edition or Maemo 5
import QtQuick 1.1
import QtQuick 1.0
Rectangle {
width: 1280
height: 1024
width: 1024
height: 768
color: "#000000"
Text {
id: title
@ -16,7 +18,7 @@ Rectangle {
}
BarClock {
id: clock
x: 456
x: 328
y: 35
color: "#000000"
textColor: "#217777"
@ -25,7 +27,7 @@ Rectangle {
Image {
id: image1
x: 944
x: 688
y: 41
height: 65
smooth: true
@ -36,7 +38,7 @@ Rectangle {
property alias status_text: status_text_id
Text {
id: status_text_id
x: 193
x: 65
y: 112
width: 894
color: "#ff4444"

View file

@ -1,12 +1,14 @@
// import QtQuick 1.0 // to target S60 5th Edition or Maemo 5
import QtQuick 1.1
import QtQuick 1.0
Item {
id: page
anchors.fill: parent
property string username: ""
property string userdbid: ""
property string amount: credit_pad.enteredText
property variant username: ""
property variant userdbid: ""
property variant amount: credit_pad.enteredText
Text {
id: item_name
@ -62,7 +64,7 @@ Item {
status_text.setStatus("Unknown barcode", "#ff4444")
return
}
if (acct.acctype === "debt") {
if (acct.acctype == "debt") {
username = acct.name
userdbid = acct.id
} else {
@ -77,7 +79,7 @@ Item {
BarButton {
id: charge_button
x: 65
y: 838
y: 582
width: 360
text: "Charge"
fontSize: 0.768 * 60
@ -89,8 +91,8 @@ Item {
BarButton {
id: cancel
x: 855
y: 838
x: 599
y: 582
width: 360
text: "Cancel"
onButtonClick: {
@ -100,17 +102,8 @@ Item {
}
function chargeCredit() {
var balance=0
if (!isNaN(amount)) {
if(amount>=0) {
balance = shop.chargeCredit(amount, userdbid)
status_text.setStatus("Charged "+amount+"! "+username+"'s credit is "+balance+".", "#ffff7c")
} else {
balance = shop.withdrawCredit((amount*(-1)), userdbid)
status_text.setStatus("Withdrawn "+amount+"! "+username+"'s credit is "+balance+".", "#ffff7c")
}
}
var balance = shop.chargeCredit(amount, userdbid)
status_text.setStatus("Charged! "+username+"'s credit is "+balance+".", "#ffff7c")
loadPage("MainPage")
}
}

View file

@ -1,14 +1,16 @@
// import QtQuick 1.0 // to target S60 5th Edition or Maemo 5
import QtQuick 1.1
import QtQuick 1.0
Item {
id: page
anchors.fill: parent
property string item_name: item_name_pad.enteredText
property string dbid: ""
property variant item_name: item_name_pad.enteredText
property variant dbid: ""
property variant info: ""
property string buy_price: item_buyprice_pad.enteredText
property string price: item_sellprice_pad.enteredText
property variant buy_price: item_buyprice_pad.enteredText
property variant price: item_sellprice_pad.enteredText
property string barcode: ""
state: "normal"
@ -26,7 +28,7 @@ Item {
/* TODO: Allow override. */
return
}
if (info.dbid === "") {
if (info.dbid == "") {
status_text.setStatus("Press [Create] first", "#ff4444")
return
}
@ -58,7 +60,7 @@ Item {
BarButton {
id: item_name_edit
x: 790
x: 534
y: 0
width: 240
height: 60
@ -113,7 +115,7 @@ Item {
BarButton {
id: item_buyprice_edit
x: 790
x: 534
y: 0
width: 240
height: 60
@ -168,7 +170,7 @@ Item {
BarButton {
id: item_sellprice_edit
x: 790
x: 534
y: 0
width: 240
height: 60
@ -223,7 +225,7 @@ Item {
BarButton {
id: item_balance_restock
x: 790
x: 534
y: 0
width: 240
height: 60
@ -304,46 +306,46 @@ Item {
BarButton {
id: save
x: 65
y: 838
y: 582
width: 360
text: dbid == "" ? "Create" : "Save"
onButtonClick: {
var xi = info;
xi["name"] = page.item_name;
xi["buy_price"] = page.buy_price;
xi["price"] = page.price;
info = xi
var xi = info;
xi["name"] = page.item_name;
xi["buy_price"] = page.buy_price;
xi["price"] = page.price;
info = xi
var res;
if (dbid == "") {
res = shop.newItem(info)
if (!res) {
status_text.setStatus("Please fill all values first.", "#ff4444")
return
}
} else {
res = shop.saveItem(dbid, info)
}
var res;
if (dbid == "") {
res = shop.newItem(info)
if (!res) {
status_text.setStatus("Please fill all values first.", "#ff4444")
return
}
} else {
res = shop.saveItem(dbid, info)
}
if (res.cost) {
status_text.setStatus((dbid == "" ? "Stocked!" : "Restocked!") + " Take " + res.cost + " from the money box.", "#ffff7c")
} else {
status_text.setStatus(dbid == "" ? "Item created" : "Changes saved", "#ffff7c")
}
if (res.cost) {
status_text.setStatus((dbid == "" ? "Stocked!" : "Restocked!") + " Take " + res.cost + " from the money box.", "#ffff7c")
} else {
status_text.setStatus(dbid == "" ? "Item created" : "Changes saved", "#ffff7c")
}
if (dbid == "") {
dbid = res.dbid
xi = info; xi["dbid"] = page.dbid; info = xi
} else {
loadPage("StockMgmt")
}
if (dbid == "") {
dbid = res.dbid
var xi = info; xi["dbid"] = page.dbid; info = xi
} else {
loadPage("StockMgmt")
}
}
}
BarButton {
id: cancel
x: 855
y: 838
x: 599
y: 582
width: 360
text: "Cancel"
onButtonClick: {

View file

@ -1,12 +1,14 @@
// import QtQuick 1.0 // to target S60 5th Edition or Maemo 5
import QtQuick 1.1
import QtQuick 1.0
Item {
id: page
anchors.fill: parent
property string name: ""
property string dbid: ""
property string price: ""
property variant name: ""
property variant dbid: ""
property variant price: ""
Text {
id: item_name
@ -23,7 +25,7 @@ Item {
Text {
id: text3
x: 867
x: 611
y: 156
height: 160
width: 348
@ -50,20 +52,12 @@ Item {
status_text.setStatus("Unknown barcode", "#ff4444")
return
}
if (acct.acctype !== "debt" && acct.acctype !== "cash") {
if (acct.acctype != "debt") {
loadPageByAcct(acct)
return
}
if (acct.acctype == "cash") { //Copied from BarButton.onButtonClick
shop.sellItemCash(dbid)
status_text.setStatus("Sold! Put " + price + " Kč in the money box.", "#ffff7c")
} else if (!shop.canSellItem(dbid, acct.id)) {
status_text.setStatus("NOT SOLD! "+acct.name+"'s credit is TOO LOW: "+shop.balance_user(acct.id), "#ff4444")
} else {
var balance = shop.sellItem(dbid, acct.id)
status_text.setStatus("Sold! "+acct.name+"'s credit is "+balance+".", "#ffff7c")
}
var balance = shop.sellItem(dbid, acct.id)
status_text.setStatus("Sold! "+acct.name+"'s credit is "+balance+".", "#ffff7c")
loadPage("MainPage")
}
}
@ -71,7 +65,7 @@ Item {
BarButton {
id: pay_cash
x: 65
y: 838
y: 582
width: 360
text: "Pay by cash"
fontSize: 0.768 * 60
@ -84,8 +78,8 @@ Item {
BarButton {
id: cancel
x: 855
y: 838
x: 599
y: 582
width: 360
text: "Cancel"
onButtonClick: {

View file

@ -1,4 +1,6 @@
// import QtQuick 1.0 // to target S60 5th Edition or Maemo 5
import QtQuick 1.1
import QtQuick 1.0
Item {
id: page
@ -25,7 +27,7 @@ Item {
BarButton {
x: 65
y: 838
y: 582
width: 360
text: "Charge"
onButtonClick: {
@ -33,31 +35,14 @@ Item {
}
}
BarButton {
x: 450
y: 838
width: 360
text: "Transfer"
onButtonClick: {
loadPage("Transfer")
}
}
BarButton {
id: management
x: 855
y: 838
x: 599
y: 582
width: 360
text: "Management"
onButtonClick: {
loadPage("Management")
}
}
BarButton {
x: 65
y: 438
width: 1150
text: "* Za uklid brmlabu vam nabijeme kredit. *"
}
}

View file

@ -1,4 +1,6 @@
// import QtQuick 1.0 // to target S60 5th Edition or Maemo 5
import QtQuick 1.1
import QtQuick 1.0
Item {
id: page
@ -63,7 +65,7 @@ Item {
Text {
id: credit_name
x: 791
x: 535
y: 156
width: 337
height: 160
@ -76,7 +78,7 @@ Item {
Text {
id: credit_amount
x: 961
x: 705
y: 156
height: 160
width: 254
@ -89,7 +91,7 @@ Item {
Text {
id: inv_name
x: 791
x: 535
y: 266
width: 337
height: 160
@ -102,7 +104,7 @@ Item {
Text {
id: inv_amount
x: 961
x: 705
y: 266
height: 160
width: 254
@ -116,7 +118,7 @@ Item {
BarButton {
id: stock_manager
x: 65
y: 686
y: 430
width: 360
text: "Stock Mgmt"
onButtonClick: {
@ -126,8 +128,8 @@ Item {
BarButton {
id: user_manager
x: 855
y: 686
x: 599
y: 430
width: 360
text: "User Mgmt"
onButtonClick: {
@ -138,7 +140,7 @@ Item {
BarButton {
id: select_item
x: 65
y: 838
y: 582
width: 360
text: "Receipt"
onButtonClick: {
@ -148,8 +150,8 @@ Item {
BarButton {
id: cancel
x: 855
y: 838
x: 599
y: 582
width: 360
text: "Main Screen"
onButtonClick: {

View file

@ -1,12 +1,14 @@
// import QtQuick 1.0 // to target S60 5th Edition or Maemo 5
import QtQuick 1.1
import QtQuick 1.0
Item {
id: page
anchors.fill: parent
property variant user
property string description: item_name_pad.enteredText
property string amount: amount_pad.enteredText
property variant description: item_name_pad.enteredText
property variant amount: amount_pad.enteredText
state: "normal"
@ -47,7 +49,7 @@ Item {
BarButton {
id: description_edit
x: 847
x: 591
y: 0
width: 300
height: 60
@ -102,7 +104,7 @@ Item {
BarButton {
id: amount_edit
x: 906
x: 650
y: 0
width: 240
height: 60
@ -148,7 +150,7 @@ Item {
BarButton {
id: save
x: 65
y: 838
y: 582
width: 360
text: "Create"
onButtonClick: {
@ -169,8 +171,8 @@ Item {
BarButton {
id: cancel
x: 855
y: 838
x: 599
y: 582
width: 360
text: "Cancel"
onButtonClick: {

View file

@ -1,4 +1,6 @@
// import QtQuick 1.0 // to target S60 5th Edition or Maemo 5
import QtQuick 1.1
import QtQuick 1.0
Item {
id: page
@ -6,8 +8,6 @@ Item {
property variant item_list_model
state: "normal"
BarcodeInput {
color: "#00ff00" /* just for debugging */
onAccepted: {
@ -29,8 +29,8 @@ Item {
id: item_list_container
x: 65
y: 166
width: 1155
height: 656
width: 899
height: 400
ListView {
id: item_list
@ -49,7 +49,7 @@ Item {
Text {
anchors.verticalCenter: parent.verticalCenter
x: 556
x: 300
width: 254
color: "#ffff7c"
text: modelData.price
@ -59,7 +59,7 @@ Item {
BarButton {
anchors.verticalCenter: parent.verticalCenter
x: 856
x: 600
width: 240
height: 68
text: "Edit"
@ -81,14 +81,12 @@ Item {
}
BarButton {
id: new_item
id: add_item
x: 65
y: 838
width: 281
height: 83
text: "New Item"
y: 582
width: 360
text: "Add Item"
fontSize: 0.768 * 60
visible: page.state == "normal"
onButtonClick: {
loadPage("ItemEdit", { dbid: "" })
}
@ -96,96 +94,16 @@ Item {
BarButton {
id: cancel
x: 855
y: 838
x: 599
y: 582
width: 360
text: "Main Screen"
onButtonClick: {
if (page.state == "search")
page.state = "normal"
else
loadPage("MainPage")
loadPage("MainPage")
}
}
BarButton {
id: search_button
x: 353
y: 838
text: "Search"
visible: page.state == "normal"
onButtonClick: { page.state = "search" }
}
BarKeyPad {
id: search_pad
x: 193
y: 554
opacity: 0
}
Text {
id: search_text
x: 65
y: 602
color: "#ffff7c"
text: search_pad.enteredText
visible: page.state == "search"
font.pixelSize: 0.768 * 46
opacity: 0
}
BarButton {
id: query_button
x: 353
y: 838
text: "Search"
visible: page.state == "search"
onButtonClick: {
page.item_list_model = shop.itemList(search_pad.enteredText)
item_list.model = page.item_list_model
}
}
states: [
State {
name: "normal"
},
State {
name: "search"
PropertyChanges {
target: item_list_container
x: 66
y: 166
width: 1155
height: 348
}
PropertyChanges {
target: search_pad
x: 83
y: 514
opacity: 1
}
PropertyChanges {
target: cancel
text: "Back"
}
PropertyChanges {
target: search_text
x: 65
y: 838
width: 528
height: 83
opacity: 1
}
}
]
Component.onCompleted: {
item_list_model = shop.itemList("")
item_list_model = shop.itemList()
}
}

View file

@ -1,174 +0,0 @@
import QtQuick 1.1
Item {
id: page
anchors.fill: parent
property variant userfrom: ""
property variant uidfrom: ""
property variant userto: ""
property variant uidto: ""
property string amount: amount_pad.enteredText
BarcodeInput {
color: "#00ff00" /* just for debugging */
focus: !(parent.userfrom != "" && parent.userto != "")
onAccepted: {
var acct = shop.barcodeInput(text)
text = ""
if (typeof(acct) == "undefined") {
status_text.setStatus("Unknown barcode", "#ff4444")
return
}
if (acct.acctype == "debt") {
if (userfrom == "") {
userfrom = acct.name
uidfrom = acct.id
} else {
userto = acct.name
uidto = acct.id
}
} else if (acct.acctype == "recharge") {
amount = acct.amount
} else {
status_text.setStatus("Unknown barcode", "#ff4444")
}
}
}
Item {
id: amount_row
visible: parent.userfrom != "" && parent.userto != ""
x: 65;
y: 166;
width: 890
height: 60
Text {
id: item_sellprice_label
x: 0
y: 0
height: 60
width: 200
color: "#ffffff"
text: "Money Amount:"
verticalAlignment: Text.AlignVCenter
font.pixelSize: 0.768 * 46
}
Text {
id: amount_input
x: 320
y: 0
height: 60
width: 269
color: "#ffff7c"
text: amount
horizontalAlignment: Text.AlignRight
verticalAlignment: Text.AlignVCenter
font.pixelSize: 0.768 * 122
}
}
BarNumPad {
id: amount_pad
x: 65
y: 239
visible: parent.userfrom != "" && parent.userto != ""
focus: parent.userfrom != "" && parent.userto != ""
Keys.onReturnPressed: { transfer.buttonClick() }
Keys.onEscapePressed: { cancel.buttonClick() }
}
BarTextHint {
id: barcode_row
x: 65
y: parent.userfrom == "" ? 314 : 414
hint_goal: (parent.userfrom == "" ? "Take money from:" : parent.userto == "" ? "Give money to:" : parent.amount == "" ? "Specify amount" : "")
hint_action: (parent.userfrom == "" || parent.userto == "" ? "Scan barcode now" : (parent.amount ? "" : "(or scan barcode now)"))
}
Text {
id: legend
visible: !(parent.userfrom != "" && parent.userto != "")
x: 65
y: 611
height: 154
width: 894
color: "#71cccc"
text: "This is for transfering credit between two brmbar users.\n May be used instead of *check next club-mate to me*."
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
font.pixelSize: 0.768 * 27
}
Text {
id: item_name
x: 422
y: 156
width: 537
height: 80
color: "#ffffff"
text: parent.userfrom ? parent.userfrom + " →" : "Money Transfer"
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignRight
verticalAlignment: Text.AlignVCenter
font.pixelSize: 0.768 * 60
}
Text {
id: item_name2
x: 422
y: 256
width: 537
height: 80
color: "#ffffff"
text: parent.userto ? "→ " + parent.userto : ""
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignRight
verticalAlignment: Text.AlignVCenter
font.pixelSize: 0.768 * 60
}
BarButton {
id: transfer
x: 65
y: 838
width: 360
text: "Transfer"
onButtonClick: {
if (userfrom == "") {
status_text.setStatus("Select FROM account.", "#ff4444")
return
}
if (userto == "") {
status_text.setStatus("Select TO account.", "#ff4444")
return
}
if (amount == "") {
status_text.setStatus("Enter amount.", "#ff4444")
return
}
var amount_str = shop.newTransfer(uidfrom, uidto, amount)
if (typeof(amount_str) == "undefined") {
status_text.setStatus("Transfer error.", "#ff4444")
return
}
status_text.setStatus("Transferred " + amount_str + " from " + userfrom + " to " + userto, "#ffff7c")
loadPage("MainPage")
}
}
BarButton {
id: cancel
x: 855
y: 838
width: 360
text: "Cancel"
onButtonClick: {
status_text.setStatus("Transfer cancelled", "#ff4444")
loadPage("MainPage")
}
}
}

View file

@ -1,12 +1,13 @@
// import QtQuick 1.0 // to target S60 5th Edition or Maemo 5
import QtQuick 1.1
Item {
id: page
anchors.fill: parent
property string name: ""
property string dbid: ""
property string negbalance: ""
property variant name: ""
property variant dbid: ""
property variant negbalance: ""
Text {
id: item_name
@ -42,7 +43,7 @@ Item {
status_text.setStatus("Unknown barcode", "#ff4444")
return
}
if (acct.acctype === "recharge") {
if (acct.acctype == "recharge") {
loadPage("ChargeCredit", { "username": name, "userdbid": dbid, "amount": acct.amount })
return
}
@ -54,7 +55,7 @@ Item {
BarButton {
id: charge_credit
x: 65
y: 838
y: 582
width: 360
text: "Charge"
fontSize: 0.768 * 60
@ -65,8 +66,8 @@ Item {
BarButton {
id: cancel
x: 855
y: 838
x: 599
y: 582
width: 360
text: "Main Screen"
onButtonClick: {

View file

@ -1,4 +1,6 @@
// import QtQuick 1.0 // to target S60 5th Edition or Maemo 5
import QtQuick 1.1
import QtQuick 1.0
Item {
id: page
@ -15,7 +17,7 @@ Item {
status_text.setStatus("Unknown barcode", "#ff4444")
return
}
if (acct.acctype !== "debt") {
if (acct.acctype != "debt") {
loadPageByAcct(acct)
return
}
@ -28,8 +30,8 @@ Item {
id: user_list_container
x: 65
y: 166
width: 1155
height: 656
width: 899
height: 400
ListView {
id: user_list
@ -48,7 +50,7 @@ Item {
Text {
anchors.verticalCenter: parent.verticalCenter
x: 556
x: 300
width: 254
color: "#ffff7c"
text: modelData.negbalance_str
@ -58,7 +60,7 @@ Item {
BarButton {
anchors.verticalCenter: parent.verticalCenter
x: 856
x: 600
width: 240
height: 68
text: "Withdraw"
@ -82,7 +84,7 @@ Item {
BarButton {
id: add_user
x: 65
y: 838
y: 582
width: 360
text: "Add User"
fontSize: 0.768 * 60
@ -91,8 +93,8 @@ Item {
BarButton {
id: cancel
x: 855
y: 838
x: 599
y: 582
width: 360
text: "Main Screen"
onButtonClick: {

View file

@ -1,12 +1,14 @@
// import QtQuick 1.0 // to target S60 5th Edition or Maemo 5
import QtQuick 1.1
import QtQuick 1.0
Item {
id: page
anchors.fill: parent
property string username: ""
property string userdbid: ""
property string amount: withdraw_pad.enteredText
property variant username: ""
property variant userdbid: ""
property variant amount: withdraw_pad.enteredText
Text {
id: item_name
@ -37,7 +39,7 @@ Item {
BarTextHint {
x: 65
y: 686
y: 430
hint_goal: (parent.username ? "" : parent.amount ? "Withdraw:" : "Withdraw amount?")
hint_action: (parent.username ? (parent.amount ? "" : "(or scan barcode now)") : "Scan barcode now")
}
@ -77,7 +79,7 @@ Item {
BarButton {
id: withdraw_button
x: 65
y: 838
y: 582
width: 360
text: "Withdraw"
fontSize: 0.768 * 60
@ -89,8 +91,8 @@ Item {
BarButton {
id: cancel
x: 855
y: 838
x: 599
y: 582
width: 360
text: "Cancel"
onButtonClick: {
@ -100,17 +102,8 @@ Item {
}
function withdrawCredit() {
var balance=0
if (!isNaN(amount)) {
amount=(amount*1)
if(amount>=0) {
balance = shop.withdrawCredit(amount, userdbid)
status_text.setStatus("Withdrawn "+amount+"! "+username+"'s credit is "+balance+".", "#ffff7c")
} else {
balance = shop.chargeCredit((amount*(-1)),userdbid)
status_text.setStatus("Charged "+amount+"! "+username+"'s credit is "+balance+".", "#ffff7c")
}
}
var balance = shop.withdrawCredit(amount, userdbid)
status_text.setStatus("Withdrawn! "+username+"'s credit is "+balance+".", "#ffff7c")
loadPage("MainPage")
}
}

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE QtCreatorProject>
<!-- Written by Qt Creator 2.5.0, 2013-01-31T18:05:42. -->
<!-- Written by Qt Creator 2.5.0, 2012-09-05T02:10:11. -->
<qtcreator>
<data>
<variable>ProjectExplorer.Project.ActiveTarget</variable>
@ -112,7 +112,7 @@
</data>
<data>
<variable>ProjectExplorer.Project.Updater.EnvironmentId</variable>
<value type="QString">{524378aa-09e0-4345-892b-1bd47313bcaf}</value>
<value type="QString">{a277f310-b549-4ad7-87ca-cd03f76f19ff}</value>
</data>
<data>
<variable>ProjectExplorer.Project.Updater.FileVersion</variable>

View file

@ -1,3 +1,4 @@
// import QtQuick 1.0 // to target S60 5th Edition or Maemo 5
import QtQuick 1.1
BasePage {
@ -16,11 +17,11 @@ BasePage {
}
function loadPageByAcct(acct) {
if (acct.acctype === "inventory") {
if (acct.acctype == "inventory") {
loadPage("ItemInfo", { name: acct["name"], dbid: acct["id"], price: acct["price"] })
} else if (acct.acctype === "debt") {
} else if (acct.acctype == "debt") {
loadPage("UserInfo", { name: acct["name"], dbid: acct["id"], negbalance: acct["negbalance"] })
} else if (acct.acctype === "recharge") {
} else if (acct.acctype == "recharge") {
loadPage("ChargeCredit", { amount: acct["amount"] })
}
}

View file

@ -1,62 +0,0 @@
#!/usr/bin/python3
import sys
from brmbar import Database
import brmbar
db = Database.Database("dbname=brmbar")
shop = brmbar.Shop.new_with_defaults(db)
currency = shop.currency
active_inv_item = None
active_credit = None
for line in sys.stdin:
barcode = line.rstrip()
if barcode[0] == "$":
credits = {'$02': 20, '$05': 50, '$10': 100, '$20': 200, '$50': 500, '$1k': 1000}
credit = credits[barcode]
if credit is None:
print("Unknown barcode: " + barcode)
continue
print("CREDIT " + str(credit))
active_inv_item = None
active_credit = credit
continue
if barcode == "SCR":
print("SHOW CREDIT")
active_inv_item = None
active_credit = None
continue
acct = brmbar.Account.load_by_barcode(db, barcode)
if acct is None:
print("Unknown barcode: " + barcode)
continue
if acct.acctype == 'debt':
if active_inv_item is not None:
cost = shop.sell(item = active_inv_item, user = acct)
print("{} has bought {} for {} and now has {} balance".format(acct.name, active_inv_item.name, currency.str(cost), acct.negbalance_str()))
elif active_credit is not None:
shop.add_credit(credit = active_credit, user = acct)
print("{} has added {} credit and now has {} balance".format(acct.name, currency.str(active_credit), acct.negbalance_str()))
else:
print("{} has {} balance".format(acct.name, acct.negbalance_str()))
active_inv_item = None
active_credit = None
elif acct.acctype == 'inventory':
buy, sell = acct.currency.rates(currency)
print("{} costs {} with {} in stock".format(acct.name, currency.str(sell), int(acct.balance())))
active_inv_item = acct
active_credit = None
else:
print("invalid account type {}".format(acct.acctype))
active_inv_item = None
active_credit = None

View file

@ -1,45 +0,0 @@
#!/usr/bin/python
import sys
from brmbar import Database
import brmbar
from flask import *
app = Flask(__name__)
#app.debug = True
@app.route('/stock/')
def stock(show_all=False):
# TODO: Use a fancy template.
# FIXME: XSS protection.
response = '<table border="1"><tr><th>Id</th><th>Item Name</th><th>Bal.</th></tr>'
for a in shop.account_list("inventory"):
style = ''
balance = a.balance()
if balance == 0:
if not show_all:
continue
style = 'color: grey; font-style: italic'
elif balance < 0:
style = 'color: red'
response += '<tr style="%s"><td>%d</td><td>%s</td><td>%d</td></tr>' % (style, a.id, a.name, balance)
response += '</table>'
if show_all:
response += '<p><a href=".">(hide out-of-stock items)</a></p>'
else:
response += '<p><a href="all">(show all items)</a></p>'
return response
@app.route('/stock/all')
def stockall():
return stock(show_all=True)
db = Database.Database("dbname=brmbar")
shop = brmbar.Shop.new_with_defaults(db)
currency = shop.currency
if __name__ == '__main__':
app.run(host='0.0.0.0')

View file

@ -40,22 +40,16 @@ class Account:
@classmethod
def create(cls, db, name, currency, acctype):
""" Constructor for new account """
# id = db.execute_and_fetch("INSERT INTO accounts (name, currency, acctype) VALUES (%s, %s, %s) RETURNING id", [name, currency.id, acctype])
id = db.execute_and_fetch("SELECT public.create_account(%s, %s, %s)", [name, currency.id, acctype])
# id = id[0]
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
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())
@ -74,11 +68,9 @@ class Account:
self.db.execute("INSERT INTO transaction_splits (transaction, side, account, amount, memo) VALUES (%s, %s, %s, %s, %s)", [transaction, side, self.id, amount, memo])
def add_barcode(self, barcode):
# self.db.execute("INSERT INTO barcodes (account, barcode) VALUES (%s, %s)", [self.id, barcode])
self.db.execute("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("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("UPDATE accounts SET name = %s WHERE id = %s", [name, self.id])
self.name = name

View file

@ -1,4 +1,3 @@
# vim: set fileencoding=utf8
class Currency:
""" Currency
@ -31,34 +30,15 @@ class Currency:
@classmethod
def create(cls, db, name):
""" Constructor for new currency """
# id = db.execute_and_fetch("INSERT INTO currencies (name) VALUES (%s) RETURNING id", [name])
id = db.execute_and_fetch("SELECT public.create_currency(%s)", [name])
# id = id[0]
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):
$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])
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])
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())
return (buy, sell)
def rates2(self, other):
# the original code for compare testing
res = self.db.execute_and_fetch("SELECT rate, rate_dir FROM exchange_rates WHERE target = %s AND source = %s AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1", [self.id, other.id])
if res is None:
raise NameError("Currency.rate(): Unknown conversion " + other.name() + " to " + self.name())
@ -73,7 +53,6 @@ class Currency:
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:
@ -89,10 +68,6 @@ class Currency:
return "{:.2f} {}".format(amount, self.name)
def update_sell_rate(self, target, rate):
# self.db.execute("INSERT INTO exchange_rates (source, target, rate, rate_dir) VALUES (%s, %s, %s, %s)", [self.id, target.id, rate, "source_to_target"])
self.db.execute("SELECT public.update_currency_sell_rate(%s, %s, %s)",
[self.id, target.id, rate])
self.db.execute("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("INSERT INTO exchange_rates (source, target, rate, rate_dir) VALUES (%s, %s, %s, %s)", [source.id, self.id, rate, "target_to_source"])
self.db.execute("SELECT public.update_currency_buy_rate(%s, %s, %s)",
[source.id, self.id, rate])
self.db.execute("INSERT INTO exchange_rates (source, target, rate, rate_dir) VALUES (%s, %s, %s, %s)", [source.id, self.id, rate, "target_to_source"])

View file

@ -36,12 +36,6 @@ class Database:
else:
cur.execute(query, attrs)
return cur
except psycopg2.DataError as error: # when biitr comes and enters '99999999999999999999' for amount
print("We have invalid input data (SQLi?): level %s (%s) @%s" % (
level, error, time.strftime("%Y%m%d %a %I:%m %p")
))
self.db_conn.rollback()
raise RuntimeError("Unsanitized data entered again... BOBBY TABLES")
except psycopg2.OperationalError as error:
print("Sleeping: level %s (%s) @%s" % (
level, error, time.strftime("%Y%m%d %a %I:%m %p")

View file

@ -7,141 +7,75 @@ class Shop:
Business logic so that only interaction is left in the hands
of the frontend scripts. """
def __init__(self, db, currency, profits, cash, excess, deficit):
def __init__(self, db, currency, profits, cash):
self.db = db
self.currency = currency # brmbar.Currency
self.profits = profits # income brmbar.Account for brmbar profit margins on items
self.cash = cash # our operational ("wallet") cash account
self.excess = excess # account from which is deducted cash during inventory item fixing (when system contains less items than is the reality)
self.deficit = deficit # account where is put cash during inventory item fixing (when system contains more items than is the reality)
@classmethod
def new_with_defaults(cls, db):
return cls(db,
currency = Currency.default(db),
profits = Account.load(db, name = "BrmBar Profits"),
cash = Account.load(db, name = "BrmBar Cash"),
excess = Account.load(db, name = "BrmBar Excess"),
deficit = Account.load(db, name = "BrmBar Deficit"))
cash = Account.load(db, name = "BrmBar Cash"))
def sell(self, item, user, amount = 1):
# Call the stored procedure for the sale
cost = self.db.execute_and_fetch(
"SELECT public.sell_item(%s, %s, %s, %s, %s)",
[item.id, amount, user.id, self.currency.id, f"BrmBar sale of {amount}x {item.name} to {user.name}"]
)[0]#[0]
self.db.commit()
return cost
# Sale: Currency conversion from item currency to shop currency
#(buy, sell) = item.currency.rates(self.currency)
#cost = amount * sell
#profit = amount * (sell - buy)
(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
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, f"BrmBar sale of {amount}x {item.name} for cash"]
)[0]#[0]
self.db.commit()
return cost
## Sale: Currency conversion from item currency to shop currency
#(buy, sell) = item.currency.rates(self.currency)
#cost = amount * sell
#profit = amount * (sell - buy)
#transaction = self._transaction(description = "BrmBar sale of {}x {} for cash".format(amount, item.name))
#item.credit(transaction, amount, "Cash")
#self.cash.debit(transaction, cost, item.name)
#self.profits.debit(transaction, profit, "Margin on " + item.name)
#self.db.commit()
#return cost
def undo_sale(self, item, user, amount = 1):
# Undo sale; rarely needed
#(buy, sell) = item.currency.rates(self.currency)
#cost = amount * sell
#profit = amount * (sell - buy)
#transaction = self._transaction(responsible = user, description = "BrmBar sale UNDO of {}x {} to {}".format(amount, item.name, user.name))
#item.debit(transaction, amount, user.name + " (sale undo)")
#user.credit(transaction, cost, item.name + " (sale undo)")
#self.profits.credit(transaction, profit, "Margin repaid on " + item.name)
# Call the stored procedure for undoing a sale
cost = self.db.execute_and_fetch(
"SELECT public.undo_sale_of_item(%s, %s, %s, %s)",
[item.id, amount, user.id, user.currency.id, f"BrmBar sale UNDO of {amount}x {item.name} to {user.name}"]
)[0]#[0]
# 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 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()
#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()
#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)",
[self.cash.id, credit, user.id, user.name]
)
self.db.commit()
#self.add_credit(amount, userto)
#self.withdraw_credit(amount, userfrom)
def buy_for_cash(self, item, amount = 1):
cost = self.db.execute_and_fetch(
"SELECT public.buy_for_cash(%s, %s, %s, %s, %s)",
[self.cash.id, item.id, amount, self.currency.id, item.name]
)[0]
# Buy: Currency conversion from item currency to shop currency
#(buy, sell) = item.currency.rates(self.currency)
#cost = amount * buy
(buy, sell) = item.currency.rates(self.currency)
cost = amount * buy
#transaction = self._transaction(description = "BrmBar stock replenishment of {}x {} for cash".format(amount, item.name))
#item.debit(transaction, amount, "Cash")
#self.cash.credit(transaction, cost, item.name)
transaction = self._transaction(description = "BrmBar stock replenishment of {}x {} for cash".format(amount, item.name))
item.debit(transaction, amount, "Cash")
self.cash.credit(transaction, cost, item.name)
self.db.commit()
return cost
def receipt_to_credit(self, user, credit, description):
#transaction = self._transaction(responsible = user, description = "Receipt: " + description)
#self.profits.credit(transaction, credit, user.name)
#user.credit(transaction, credit, "Credit from receipt: " + description)
self.db.execute_and_fetch(
"SELECT public.buy_for_cash(%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):
@ -150,7 +84,7 @@ class Shop:
transaction = transaction[0]
return transaction
def credit_balance(self, overflow=None):
def credit_balance(self):
# We assume all debt accounts share a currency
sumselect = """
SELECT SUM(ts.amount)
@ -158,17 +92,14 @@ class Shop:
LEFT JOIN transaction_splits AS ts ON a.id = ts.account
WHERE a.acctype = %s AND ts.side = %s
"""
if overflow is not None:
sumselect += ' AND a.name ' + ('NOT ' if overflow == 'exclude' else '') + ' LIKE \'%%-overflow\''
cur = self.db.execute_and_fetch(sumselect, ["debt", 'debit'])
debit = cur[0] or 0
credit = self.db.execute_and_fetch(sumselect, ["debt", 'credit'])
credit = credit[0] or 0
return debit - credit
def credit_negbalance_str(self, overflow=None):
return self.currency.str(-self.credit_balance(overflow=overflow))
def credit_negbalance_str(self):
return self.currency.str(-self.credit_balance())
# XXX causing extra heavy delay ( thousands of extra SQL queries ), disabled
def inventory_balance(self):
balance = 0
# Each inventory account has its own currency,
@ -181,116 +112,15 @@ class Shop:
# might have been bought for a different price! Therefore,
# we need to replace the command below with a complex SQL
# statement that will... ugh, accounting is hard!
b = inv.balance() * inv.currency.rates(self.currency)[0]
# if b != 0:
# print(str(b) + ',' + inv.name)
balance += b
balance += inv.currency.convert(inv.balance(), self.currency)
return balance
# XXX bypass hack
def inventory_balance_str(self):
# return self.currency.str(self.inventory_balance())
return "XXX"
return self.currency.str(self.inventory_balance())
def account_list(self, acctype, like_str="%%"):
def account_list(self, acctype):
"""list all accounts (people or items, as per acctype)"""
accts = []
cur = self.db.execute_and_fetchall("SELECT id FROM accounts WHERE acctype = %s AND name ILIKE %s ORDER BY name ASC", [acctype, like_str])
#FIXME: sanitize input like_str ^
cur = self.db.execute_and_fetchall("SELECT id FROM accounts WHERE acctype = %s ORDER BY name ASC", [acctype])
for inventory in cur:
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]
self.db.commit()
return rv
#amount_in_reality = amount
#amount_in_system = item.balance()
#(buy, sell) = item.currency.rates(self.currency)
#diff = abs(amount_in_reality - amount_in_system)
#buy_total = buy * diff
#if amount_in_reality > amount_in_system:
# transaction = self._transaction(description = "BrmBar inventory fix of {}pcs {} in system to {}pcs in reality".format(amount_in_system, item.name,amount_in_reality))
# item.debit(transaction, diff, "Inventory fix excess")
# self.excess.credit(transaction, buy_total, "Inventory fix excess " + item.name)
# self.db.commit()
# return True
#elif amount_in_reality < amount_in_system:
# transaction = self._transaction(description = "BrmBar inventory fix of {}pcs {} in system to {}pcs in reality".format(amount_in_system, item.name,amount_in_reality))
# item.credit(transaction, diff, "Inventory fix deficit")
# self.deficit.debit(transaction, buy_total, "Inventory fix deficit " + item.name)
# self.db.commit()
# return True
#else:
# transaction = self._transaction(description = "BrmBar inventory fix of {}pcs {} in system to {}pcs in reality".format(amount_in_system, item.name,amount_in_reality))
# item.debit(transaction, 0, "Inventory fix - amount was correct")
# item.credit(transaction, 0, "Inventory fix - amount was correct")
# self.db.commit()
# return False
def fix_cash(self, amount):
rv = self.db.execute_and_fetch(
"SELECT public.fix_cash(%s, %s, %s, %s)",
[self.excess.id, self.deficit.id, self.currency.id, amount]
)[0]
self.db.commit()
return rv
#amount_in_reality = amount
#amount_in_system = self.cash.balance()
#diff = abs(amount_in_reality - amount_in_system)
#if amount_in_reality > amount_in_system:
# transaction = self._transaction(description = "BrmBar cash inventory fix of {} in system to {} in reality".format(amount_in_system, amount_in_reality))
# self.cash.debit(transaction, diff, "Inventory fix excess")
# self.excess.credit(transaction, diff, "Inventory cash fix excess.")
# self.db.commit()
# return True
#elif amount_in_reality < amount_in_system:
# transaction = self._transaction(description = "BrmBar cash inventory fix of {} in system to {} in reality".format(amount_in_system, amount_in_reality))
# self.cash.credit(transaction, diff, "Inventory fix deficit")
# self.deficit.debit(transaction, diff, "Inventory fix deficit.")
# self.db.commit()
# return True
#else:
# return False
def consolidate(self):
msg = self.db.execute_and_fetch(
"SELECT public.make_consolidate_transaction(%s, %s, %s)",
[self.excess.id, self.deficit.id, self.profits.id]
)[0]
#transaction = self._transaction(description = "BrmBar inventory consolidation")
#excess_balance = self.excess.balance()
#if excess_balance != 0:
# print("Excess balance {} debited to profit".format(-excess_balance))
# self.excess.debit(transaction, -excess_balance, "Excess balance added to profit.")
# self.profits.debit(transaction, -excess_balance, "Excess balance added to profit.")
#deficit_balance = self.deficit.balance()
#if deficit_balance != 0:
# print("Deficit balance {} credited to profit".format(deficit_balance))
# self.deficit.credit(transaction, deficit_balance, "Deficit balance removed from profit.")
# self.profits.credit(transaction, deficit_balance, "Deficit balance removed from profit.")
if msg != None:
print(msg)
self.db.commit()
def undo(self, oldtid):
#description = self.db.execute_and_fetch("SELECT description FROM transactions WHERE id = %s", [oldtid])[0]
#description = 'undo %d (%s)' % (oldtid, description)
#transaction = self._transaction(description=description)
#for split in self.db.execute_and_fetchall("SELECT id, side, account, amount, memo FROM transaction_splits WHERE transaction = %s", [oldtid]):
# splitid, side, account, amount, memo = split
# memo = 'undo %d (%s)' % (splitid, memo)
# amount = -amount
# self.db.execute("INSERT INTO transaction_splits (transaction, side, account, amount, memo) VALUES (%s, %s, %s, %s, %s)", [transaction, side, account, amount, memo])
transaction = self.db.execute_and_fetch("SELECT public.undo_transaction(%s)",[oldtid])[0]
self.db.commit()
return transaction

View file

@ -1,15 +0,0 @@
# cleanup bounty
*/5 * * * * ~/brmbar/brmbar3/uklid-watchdog.sh
0 0 * * 1 ~/brmbar/brmbar3/uklid-refill.sh
# overall summary
5 4 * * * ~/brmbar/brmbar3/daily-summary.sh | mail -s "daily brmbar summary" yyy@yyy
# debt track
5 0 * * * ~/brmbar/brmbar3/dluhy.sh 2>/dev/null
# per-user summary
1 0 * * * /home/brmlab/brmbar/brmbar3/log.sh yyy yyy@yyy
# backup
6 * * * * echo "SELECT * FROM account_balances;" | psql brmbar | gzip -9 | ssh -Tp 110 -i /home/brmlab/.ssh/id_ecdsa jenda@coralmyn.hrach.eu
16 1 * * * pg_dump brmbar | gzip -9 | ssh -Tp 110 -i /home/brmlab/.ssh/id_ecdsa jenda@coralmyn.hrach.eu

View file

@ -1,11 +0,0 @@
#!/bin/sh
# Add a crontab entry like:
# 5 4 * * * ~/brmbar/brmbar3/daily-summary.sh | mail -s "daily brmbar summary" rada@brmlab.cz
cd ~/brmbar/brmbar3
./brmbar-cli.py stats
echo
echo "Time since last full inventory check: $(echo "select now()-time from transactions where description = 'BrmBar inventory consolidation' order by time desc limit 1;" | psql brmbar | tail -n +3 | head -n 1 | tr -s " ")"
echo
echo "Overflows: $(echo "SELECT name, -crbalance FROM account_balances WHERE name LIKE '%overflow%' AND crbalance != 0 ORDER BY name" | psql brmbar | tail -n +3 | grep '|' | tr -s " " | sed -e "s/ |/:/g" -e "s/$/;/" | tr -d "\n") TOTAL: $(echo "SELECT -SUM(crbalance) FROM account_balances WHERE name LIKE '%overflow%' AND crbalance != 0" | psql brmbar | tail -n +3 | head -n 1 | tr -s " ")"
echo
echo "Club Mate sold in last 24 hours: $(echo "select count(*) from transaction_cashsums where time > now() - '1 day'::INTERVAL and (description like '%Club Mate%' or description like '%granatove mate%')" | psql brmbar | tail -n +3 | head -n 1 | tr -s " ") bottles"

View file

@ -1,3 +0,0 @@
p1=`echo -n "brmbar - dluhy: "; echo "SELECT name, crbalance FROM account_balances WHERE acctype = 'debt' AND crbalance < -100 AND name NOT LIKE '%overflow%' AND name NOT LIKE 'sachyo' ORDER BY crbalance ASC" | psql brmbar | tail -n +3 | grep '|' | tr -s " " | sed -e "s/ |/:/g" -e "s/$/;/" | tr -d "\n"`
p2=`echo "SELECT sum(crbalance) FROM account_balances WHERE acctype = 'debt' AND crbalance < 0 AND name NOT LIKE '%overflow%' AND name NOT LIKE 'sachyo'" | psql brmbar | tail -n +3 | head -n 1 | tr -s " "`
echo "$p1 total$p2 Kc. https://www.elektro-obojky.cz/" | ssh -p 110 -i /home/brmlab/.ssh/id_rsa jenda@coralmyn.hrach.eu

View file

@ -1,106 +0,0 @@
BrmBar v3 - Architectural Overview
==================================
BrmBar v3 is written in Python, with the database stored in PostgreSQL
and the primary user interface modelled in QtQuick. All user interfaces
share a common *brmbar* package that provides few Python classes for
manipulation with the base objects.
Objects and Database Schema
---------------------------
### Account ###
The most essential brmbar object is an Account, which can track
balances of various kinds (described by *acctype* column) in the
classical accounting paradigm:
* **Cash**: A physical stash of cash. One cash account is created
by default, corresponding to the cash box where people put money
when buying stuff (or depositing money in their user accounts).
Often, that's the only cash account you need.
* **Debt**: Represents brmbar's debt to some person. These accounts
are actually the "user accounts" where people deposit money. When
a deposit of 100 is made, 100 is *subtracted* from the balance,
the balance is -100 and brmbar is in debt of 100 to the user.
When the user buys something for 200, 200 is *added* to the balance,
the balance is 100 and the user is in debt of 100 to the brmbar.
This is correct notation from accounting point of view, but somewhat
confusing for the users, so in the user interface (and crbalance
column of some views), this balance is *negated*!
* **Inventory**: Represents inventory items (e.g. Club Mate bottles).
The account balance represents the quantity of items.
* **Income**: Represents pure income of brmbar, i.e. the profit;
there is usually just a single account of this type where all the
profit (sell price of an item minus the buy price of an item)
is accumulated.
* **Expense**: This type is currently not used.
* **Starting balance** and **ending balance**: This may be used
in the future when transaction book needs to be compressed.
As you can see, the amount of cash, user accounts, inventory items
etc. are all represented as **Account** objects that are of various
**types**, are **named** and have a certain balance (calculated
from a transaction book). That balance is a number represented
in certain **currency**. It also has a set of **barcodes** associated.
### Currency, Exchange rate ###
Usually, all accounts that deal with cash (the cash, debt, income, ...
accounts) share a single currency that corresponds to the physical
currency locally in use (the default is `Kč`). However, inventory
items have balances corresponding to item quantities - to deal with
this correctly, each inventory item *has its own currency*; i.e.
`Club Mate` bottle is a currency associated with the `Club Mate`
account.
Currencies have defined (uni-directional) exchange rates. The exchange
rate of "Kč to Club Mate bottles" is the buy price of Club Mate, how
much you pay for one bottle of Club Mate from the cash box when you
are stocking in Club Mate. The exchange rate of "Club Mate bottle to Kč"
is the sell price of Club Mate, how much you pay for one bottle of Club
Mate to the cash box when you are buying it from brmbar (sell price
should be higher than buy price if you want to make a profit).
Exchange rate is valid since some defined time; historical exchange
rates are therefore kept and this allows to account for changing prices
of inventory items. (Unfortunately, at the time of writing this, the
profit calculation actually didn't make use of that yet.)
### Transactions, Transaction splits ###
A transaction book is used to determine current account balances and
stores all operations related to accounts - depositing or withdrawing
money, stocking in items, and most importantly buying stuff (either for
cash or from a debt account). A transaction happenned at some **time**
and was performed by certain **responsible** person.
The actual accounts involved in a transaction are specified by a list of
transaction splits that either put balance into the transaction (*credit*
side) or grab balance from it (*debit* side). For example, a typical
transaction representing a sale of Club Mate bottle to user "pasky"
would be split like this:
* *credit* of 1 Club Mate on Club Mate account with memo "pasky".
* *debit* of 35 Kč on "pasky" account with memo "Club Mate"
(indeed we _add_ 35Kč to the debt account for pasky buying
the Club Mate; if this seems weird, refer to the "debt" account
type description).
* *debit* of 5 Kč on income account Profits with memo "Margin
on Club Mate" (this represents the sale price - buy price delta,
i.e. the profit we made in brmbar by selling this Club Mate).
The brmbar Python Package
-------------------------
The **brmbar** package (in brmbar/ subdirectory) provides common brmbar
functionality for the various user interfaces:
* **Database**: Layer for performing SQL queries with some error handling.
* **Currency**: Class for querying and manipulating currency objects and
converting between them based on stored exchange rates.
* **Account**: Class for querying and manipulating the account objects
and their current balance.
* **Shop**: Class providing the "business logic" of all the actual user
operations: selling stuff, depositing and withdrawing moeny, adding
stock, retrieving list of accounts of given type, etc.

View file

@ -1,6 +0,0 @@
#!/bin/bash
p=`/home/brmlab/brmbar/brmbar3/brmbar-cli.py userlog "$1" yesterday`
if [ -n "$p" ]; then
echo "$p" | mail -s "brmbar report" "$2"
fi

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 INTO STRICT v_rate, rate_dir INTO STRICT v_rate_dir FROM public.exchange_rates WHERE target = i_item_id AND source = i_other_id AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1;
IF v_rate_dir = 'target_to_source'::public.exchange_rate_direction THEN
RETURN v_rate;
ELSE
RETURN 1/v_rate;
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN -1;
END;
$$
-- return negative number on rate not found
CREATE OR REPLACE FUNCTION public.find_sell_rate(
IN i_item_id public.accounts.id%TYPE;
IN i_other_id public.accounts.id%TYPE;
) RETURNS NUMERIC
LANGUAGE plpgsql
AS $$
DECLARE
v_rate public.exchange_rates.rate%TYPE;
v_rate_dir public.exchange_rates.rate_dir%TYPE;
BEGIN
SELECT rate INTO STRICT v_rate, rate_dir INTO STRICT v_rate_dir FROM public.exchange_rates WHERE target = i_other_id AND source = i_item_id AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1;
IF v_rate_dir = 'source_to_target'::public.exchange_rate_direction THEN
RETURN v_rate;
ELSE
RETURN 1/v_rate;
END IF;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN -1;
END;
$$
CREATE OR REPLACE FUNCTION public.create_transaction(
i_responsible_id public.accounts.id%TYPE,
i_description public.transactions.description%TYPE
) RETURNS public.transactions.id%TYPE AS $$
DECLARE
new_transaction_id public.transactions%TYPE;
BEGIN
-- Create a new transaction
INSERT INTO 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,143 +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%TYPE;
BEGIN
-- Create a new transaction
INSERT INTO public.transactions (responsible, description)
VALUES (i_responsible_id, i_description)
RETURNING id INTO new_transaction_id;
-- Return the new transaction ID
RETURN new_transaction_id;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION brmbar_privileged.sell_item_internal(
i_item_id public.accounts.id%TYPE,
i_amount INTEGER,
i_user_id public.accounts.id%TYPE,
i_target_currency_id public.currencies.id%TYPE,
i_other_memo TEXT,
i_description TEXT
) RETURNS NUMERIC
LANGUAGE plpgsql
AS $$
DECLARE
v_buy_rate NUMERIC;
v_sell_rate NUMERIC;
v_cost NUMERIC;
v_profit NUMERIC;
v_transaction_id public.transactions.id%TYPE;
BEGIN
-- Get the buy and sell rates from the stored functions
v_buy_rate := public.find_buy_rate(i_item_id, i_target_currency_id);
v_sell_rate := public.find_sell_rate(i_item_id, i_target_currency_id);
-- Calculate cost and profit
v_cost := i_amount * v_sell_rate;
v_profit := i_amount * (v_sell_rate - v_buy_rate);
-- Create a new transaction
v_transaction_id := brmbar_privileged.create_transaction(i_user_id, i_description);
-- the item (decrease stock)
INSERT INTO public.transaction_splits (transaction, side, account, amount, memo)
VALUES (i_transaction_id, 'credit', i_item_id, i_amount,
i_other_memo);
-- the user
INSERT INTO public.transaction_splits (transaction, side, account, amount, memo)
VALUES (i_transaction_id, 'debit', i_user_id, v_cost,
(SELECT "name" FROM public.accounts WHERE id = i_item_id));
-- the profit
INSERT INTO public.transaction_splits (transaction, side, account, amount, memo)
VALUES (i_transaction_id, 'debit', (SELECT account_id FROM accounts WHERE name = 'BrmBar Profits'), v_profit, (SELECT 'Margin on ' || "name" FROM public.accounts WHERE id = i_item_id));
-- Return the cost
RETURN v_cost;
END;
$$;
CREATE OR REPLACE FUNCTION public.sell_item(
i_item_id public.accounts.id%TYPE,
i_amount INTEGER,
i_user_id public.accounts.id%TYPE,
i_target_currency_id public.currencies.id%TYPE,
i_description TEXT
) RETURNS NUMERIC
LANGUAGE plpgsql
AS $$
BEGIN
RETURN brmbar_privileged.sell_item_internal(i_item_id,
i_amount,
i_other_id,
i_target_currency_id,
(SELECT "name" FROM public.accounts WHERE id = i_user_id),
i_description);
END;
$$;
CREATE OR REPLACE FUNCTION public.sell_item_for_cash(
i_item_id public.accounts.id%TYPE,
i_amount INTEGER,
i_user_id public.accounts.id%TYPE,
i_target_currency_id public.currencies.id%TYPE,
i_description TEXT
) RETURNS NUMERIC
LANGUAGE plpgsql
AS $$
BEGIN
RETURN brmbar_privileged.sell_item_internal(i_item_id,
i_amount,
i_other_id,
i_target_currency_id,
'Cash',
i_description);
END;
$$;
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%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,82 +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;
BEGIN
-- this could fail and it would generate exception in python
-- FIXME: convert v_buy_rate < 0 into python exception
v_buy_rate := public.find_buy_rate(i_item_id, i_target_currency_id);
-- this could fail and it would generate exception in python, even though it is not used
--v_sell_rate := public.find_sell_rate(i_item_id, i_target_currency_id);
-- Calculate cost and profit
v_cost := i_amount * v_buy_rate;
-- Create a new transaction
v_transaction_id := brmbar_privileged.create_transaction(NULL,
'BrmBar stock replenishment of ' || i_amount || 'x ' || i_item_name || ' for cash');
-- the item
INSERT INTO public.transaction_splits (transaction, side, account, amount, memo)
VALUES (i_transaction_id, 'debit', i_item_id, i_amount,
'Cash');
-- the cash
INSERT INTO public.transaction_splits (transaction, side, account, amount, memo)
VALUES (i_transaction_id, 'credit', i_cash_account_id, v_cost,
i_item_name);
-- Return the cost
RETURN v_cost;
END;
$$;
PERFORM brmbar_privileged.upgrade_schema_version_to(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 (i_transaction_id, 'credit', i_profits_id, i_amount, i_user_name);
-- the user
INSERT INTO public.transaction_splits (transaction, side, account, amount, memo)
VALUES (i_transaction_id, 'credit', i_user_id, i_amount, 'Credit from receipt: ' || i_description);
END;
$$;
PERFORM brmbar_privileged.upgrade_schema_version_to(16);
END IF;
END;
$upgrade_block$;
-- vim: set ft=plsql :

View file

@ -1,157 +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 INTO v_crsum,
COALESCE(SUM(CASE WHEN side='debit' THEN amount ELSE 0 END),0) dbsum into v_dbsum
FROM public.transaction_splits ts WHERE ts.account=4
RETURN v_dbsum - v_crsum;
END; $$;
CREATE OR REPLACE FUNCTION brmbar_privileged.fix_account_balance(
IN i_account_id public.acounts.id%TYPE,
IN i_account_currency_id public.currencies.id%TYPE,
IN i_excess_id public.acounts.id%TYPE,
IN i_deficit_id public.acounts.id%TYPE,
IN i_shop_currency_id public.currencies.id%TYPE,
IN i_amount_in_reality NUMERIC
) RETURNS BOOLEAN
VOLATILE NOT LEAKPROOF LANGUAGE plpgsql AS $fn$
DECLARE
v_amount_in_system NUMERIC;
v_buy_rate NUMERIC;
v_currency_id public.currencies.id%TYPE;
v_diff NUMERIC;
v_buy_total NUMERIC;
v_ntrn_id public.transactions.id%TYPE;
v_transaction_memo TEXT;
v_item_name TEXT;
v_excess_memo TEXT;
v_deficit_memo TEXT;
v_old_trn public.transactions%ROWTYPE;
v_old_split public.transaction_splits%ROWTYPE;
BEGIN
v_amount_in_system := public.compute_account_balance(i_account_id);
IF i_account_currency_id <> i_shop_currency_id THEN
v_buy_rate := public.find_buy_rate(i_item_id, i_shop_currency_id);
ELSE
v_buy_rate := 1;
END IF;
v_diff := ABS(i_amount_in_reality - v_amount_in_system);
v_buy_total := v_buy_rate * v_diff;
-- compute memo strings
IF i_item_id = 1 THEN -- cash account recognized by magic id
-- fixing cash
v_transaction_memo :=
'BrmBar cash inventory fix of ' || v_amount_in_system
|| ' in system to ' || i_amount_in_reality || ' in reality';
v_excess_memo := 'Inventory cash fix excess.';
v_deficit_memo := 'Inventory fix deficit.';
ELSE
-- fixing other account
SELECT "name" INTO v_item_name FROM public.accounts WHERE id = i_account_id;
v_transaction_memo :=
'BrmBar inventory fix of ' || v_amount_in_system || 'pcs '
|| v_item_name
|| ' in system to ' || i_amount_in_reality || 'pcs in reality';
v_excess_memo := 'Inventory fix excess ' || v_item_name;
v_deficit_memo := 'Inventory fix deficit ' || v_item_name;
END IF;
-- create transaction based on the relation between counting and accounting
IF i_amount_in_reality > v_amount_in_system THEN
v_ntrn_id := brmbar_privileged.create_transaction(NULL, v_transaction_memo);
INSERT INTO transaction_splits ("transaction", "side", "account", "amount", "memo")
VALUES (v_ntrn_id, 'debit', i_item_id, v_diff, 'Inventory fix excess');
INSERT INTO transaction_splits ("transaction", "side", "account", "amount", "memo")
VALUES (v_ntrn_id, 'credit', i_excess_id, v_buy_total, v_excess_memo);
RETURN TRUE;
ELSIF i_amount_in_reality < v_amount_in_system THEN
v_ntrn_id := brmbar_privileged.create_transaction(NULL, v_transaction_memo);
INSERT INTO transaction_splits ("transaction", "side", "account", "amount", "memo")
VALUES (v_ntrn_id, 'credit', i_item_id, v_diff, 'Inventory fix deficit');
INSERT INTO transaction_splits ("transaction", "side", "account", "amount", "memo")
VALUES (v_ntrn_id, 'debit', i_deficit_id, v_buy_total, v_deficit_memo);
RETURN TRUE;
ELSIF i_account_id <> 1 THEN -- cash account recognized by magic id
-- record that everything is going on swimmingly only for noncash accounts (WTF)
v_ntrn_id := brmbar_privileged.create_transaction(NULL, v_transaction_memo);
INSERT INTO transaction_splits ("transaction", "side", "account", "amount", "memo")
VALUES (v_ntrn_id, 'debit', i_item_id, 0, 'Inventory fix - amount was correct');
INSERT INTO transaction_splits ("transaction", "side", "account", "amount", "memo")
VALUES (v_ntrn_id, 'credit', i_item_id, 0, 'Inventory fix - amount was correct');
RETURN FALSE;
END IF;
RETURN FALSE;
END;
$fn$;
CREATE OR REPLACE FUNCTION public.fix_inventory(
IN i_account_id public.acounts.id%TYPE,
IN i_account_currency_id public.currencies.id%TYPE,
IN i_excess_id public.acounts.id%TYPE,
IN i_deficit_id public.acounts.id%TYPE,
IN i_shop_currency_id public.currencies.id%TYPE,
IN i_amount_in_reality NUMERIC
) RETURNS BOOLEAN
VOLATILE NOT LEAKPROOF LANGUAGE plpgsql AS $fn$
BEGIN
RETURN brmbar_privileged.fix_account_balance(
i_account_id,
i_account_currency_id,
i_excess_id,
i_deficit_id,
i_shop_currency_id,
i_amount_in_reality
);
END;
$fn$;
PERFORM brmbar_privileged.upgrade_schema_version_to(17);
END IF;
END;
$upgrade_block$;
-- vim: set ft=plsql :

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.acounts.id%TYPE,
IN i_deficit_id public.acounts.id%TYPE,
IN i_shop_currency_id public.currencies.id%TYPE,
IN i_amount_in_reality NUMERIC
) RETURNS BOOLEAN
VOLATILE NOT LEAKPROOF LANGUAGE plpgsql AS $fn$
BEGIN
RETURN brmbar_privileged.fix_account_balance(
1,
1,
i_excess_id,
i_deficit_id,
i_shop_currency_id,
i_amount_in_reality
);
END;
$fn$;
PERFORM brmbar_privileged.upgrade_schema_version_to(18);
END IF;
END;
$upgrade_block$;
-- vim: set ft=plsql :

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 '||o_id||' ('||v_old_trn.description||')') RETURNING id into v_ntrn_id;
FOR v_old_split IN
SELECT * FROM transaction_splits WHERE "transaction" = i_id
LOOP
INSERT INTO transaction_splits ("transaction", "side", "account", "amount", "memo")
VALUES (v_ntrn_id, v_old_split.side, v_old_split.account, -v_old_split.amount,
'undo ' || v_old_split.id || ' (' || v_old_split.memo || ')' );
END LOOP;
RETURN v_ntrn_id;
END;
$fn$;
PERFORM brmbar_privileged.upgrade_schema_version_to(20);
END IF;
END;
$upgrade_block$;
-- vim: set ft=plsql :

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

View file

@ -1,6 +0,0 @@
#!/bin/bash
if [ `./brmbar-cli.py iteminfo uklid|grep -o '[0-9]*.[0-9]* pcs'|cut -d '.' -f 1` -eq 0 ]; then
BOUNTY=`./brmbar-cli.py restock uklid 1 | grep -o 'take -[0-9]*'|grep -o '[0-9]*'`
echo "Brmlab cleanup bounty for ${BOUNTY}CZK!!!"|ssh jenda@fry.hrach.eu
fi

View file

@ -1,18 +0,0 @@
#!/bin/bash
LASTIDF=/home/brmlab/uklid.last
LASTID=`cat $LASTIDF 2>/dev/null || echo 0`
RES=`psql brmbar -Atq -c "select id,description from transactions where id>$LASTID and description like 'BrmBar sale of 1x uklid%' LIMIT 1;"`
if [ ! -z "$RES" ]; then
LASTID=`echo "$RES"|cut -d '|' -f 1`
echo $LASTID > $LASTIDF
WINNER=`echo "$RES"|grep -o 'to [^ ]*'|cut -d ' ' -f 2`
if [ -z "$WINNER" ]; then
WINNER="anonymous hunter"
fi
echo "Brmlab cleanup bounty was claimed by $WINNER! Thanks!"|ssh -p 110 jenda@coralmyn.hrach.eu
fi