Compare commits

...
Sign in to create a new pull request.

139 commits

Author SHA1 Message Date
TMA
8e97f43a86 0026: maintain trading accounts for each currency 2025-12-11 21:41:18 +01:00
f6128ecc4a Currency.py: remove unused methods - rates2, convert 2025-08-28 16:58:33 +02:00
92deae6775 Fix db 3. 2025-08-21 17:13:30 +02:00
aee3a1d24f Fix db 2. 2025-08-21 17:12:22 +02:00
e604e0000b Fix db. 2025-08-21 17:12:14 +02:00
dd8c93e967 Fix string literal. 2025-08-21 17:10:38 +02:00
TMA
57f36dd2f5 0025: it's public.account_type, FRFR 2025-08-21 17:01:57 +02:00
TMA
1e324c0920 0025: it's public.account_type 2025-08-21 16:58:59 +02:00
TMA
c2ddab9ce8 0025: declare v 2025-08-21 16:54:56 +02:00
89fb60bab5 Use account loading stored procedure for Account construction. 2025-08-21 16:49:44 +02:00
TMA
38a4a3def1 0025: account class loader procedure 2025-08-21 16:47:45 +02:00
TMA
2a19bd291f typo fix 2025-08-21 16:46:33 +02:00
TMA
1d9a2e9fac Fix buy rate updating 2025-07-26 18:26:59 +02:00
TMA
54c687edf3 disable slowdown in management for now 2025-07-26 17:12:50 +02:00
TMA
9afef5bce2 maybe make something faster 2025-07-26 16:57:30 +02:00
TMA
c17aa99666 0024: drop old versions before installing new ones 2025-07-26 16:02:47 +02:00
TMA
c04d4a1f8e 0024: propagate errors in find_*_rate 2025-07-26 15:50:10 +02:00
TMA
70c46bd950 log all sql statements 2025-07-26 15:15:03 +02:00
8487202b8f Record to columns. 2025-07-21 17:07:50 +02:00
a1ec2bdb6b Debug raw inventory balance result. 2025-07-21 17:06:13 +02:00
TMA
7a301379f0 0023: security definer 2025-07-21 17:02:02 +02:00
39fe8d97fd Use inventory_balance. 2025-07-21 16:56:50 +02:00
TMA
2e328e4fb3 0023: stored function for total inventory balance 2025-07-21 16:49:29 +02:00
TMA
4f9611727b 0022: drop: add cascade 2025-07-21 16:34:28 +02:00
3933514e86 Fix Shop keyword arguments. 2025-07-21 15:46:26 +02:00
TMA
04f98147fd 0022: shop initialization stored procedure 2025-07-21 15:43:13 +02:00
dde8bd764d Use new shop init interface and black all brmbar/*.py. 2025-07-21 15:42:40 +02:00
a8e4b3216d Old scripts: mark them deprecated and make them exit with status 1. 2025-07-21 15:02:09 +02:00
TMA
01491deec3 remove Account._transaction_split and dependents 2025-07-17 17:48:36 +02:00
TMA
76a484ea5e 0015: find_buy_rate takes currency ID 2025-07-17 17:30:44 +02:00
f962586e3a Ensure amount in buy_for_cash is integer. 2025-07-17 17:16:52 +02:00
TMA
be0b50fedd Fix i_transaction_id as v_transaction_id elsewhere. 2025-07-17 16:57:42 +02:00
95e55aef23 Fix i_transaction_id as v_transaction_id. 2025-07-17 16:50:02 +02:00
e31165d668 Change to receipt_reimbursement. 2025-07-17 16:47:23 +02:00
5d658a7406 Rollback on generic database exceptions. 2025-07-17 16:23:26 +02:00
TMA
f8a265f1d2 0021: constraints on currency columns in database: numbers only 2025-07-17 16:02:50 +02:00
6f945c3a0f Fix Shop.py transfer with more arguments. 2025-07-17 15:27:57 +02:00
9f63a6760e Fix Shop.py credit -> amount 2025-07-17 15:24:18 +02:00
ff68817129 Moar transfer logging. 2025-07-17 15:17:18 +02:00
TMA
afb5476c2d o_id -> i_id 2025-07-12 19:12:52 +02:00
9d8827ccbd 0010: lookup item currency id 2025-07-12 18:52:21 +02:00
TMA
8ee51a23d9 account_id -> id 2025-07-12 18:25:24 +02:00
81366fd2bb 0010: fix v_transaction_id 2025-07-12 18:04:56 +02:00
ee2b945299 0010: fix other to user 2025-07-12 17:53:33 +02:00
38838692c6 Database: other exceptions 2025-07-12 17:45:06 +02:00
557d36cac3 Shop: not array 2025-07-12 17:42:32 +02:00
343df879fb Shop: sell log res 2025-07-12 17:40:16 +02:00
0a8e2b727b gui: logger 2025-07-12 17:37:17 +02:00
829bab66b4 gui: fine-grained logging 2025-07-12 17:35:38 +02:00
c3abceac5d gui: try logging acct in acct_map better 2025-07-12 17:33:28 +02:00
f9230ed5bf gui: try logging acct in acct_map 2025-07-12 17:29:08 +02:00
93f9b336b6 Shop.py: logging to sell 2025-07-12 17:25:27 +02:00
3ab537f7d9 Try logging. 2025-07-12 17:15:22 +02:00
15dfd0be04 gui: remove old db initialization 2025-07-12 15:57:41 +02:00
5355eca6f5 gui: pass database configuration as command-line options 2025-07-12 15:55:06 +02:00
3a4aaa74ce 0017: use proper account id argument - not a constant 2025-07-12 15:15:21 +02:00
cd9b4484c8 Shop.py: python <3.6 compatibility 2025-07-10 16:54:45 +02:00
597bca87b5 Schema 0018: fix ac-c-ounts 2025-07-10 16:24:31 +02:00
5e348b2463 Schema 0017: fix ac-c-ounts 2025-07-10 16:23:25 +02:00
1eff329496 Schema 0017: fix semicolon before return 2025-07-10 16:22:13 +02:00
f7f137821b Schema 0017: fix into syntax 2025-07-10 16:21:22 +02:00
3b0cb6472d Schema 0011: fix type inference for id 2025-07-10 16:19:25 +02:00
fba614de78 Schema 0010: fix type inference for id 2025-07-10 16:18:39 +02:00
ae16ebc51f Schema 0009: fix type inference for id 2025-07-10 16:17:34 +02:00
903cc8f4d7 Schema 0009: fix select into 2025-07-10 16:15:19 +02:00
8d2c9cb20f Schema 0009: fix DDL statements ends, fix argument delimiters in second procedure as well 2025-07-10 16:05:19 +02:00
a8a124835c Schema 0009: fix find_buy_rate arguments syntax 2025-07-10 16:00:28 +02:00
TMA
0bff27da29 test reimplementation of brmbar.Currency.rates method 2025-04-22 00:00:38 +02:00
TMA
f0058aad68 #18: stored function for "fixing cash" transaction 2025-04-21 21:14:34 +02:00
TMA
c21b394b42 #17: stored function for "fixing" inventory transaction 2025-04-21 21:14:18 +02:00
TMA
69c405f715 #20: stored function for undo transaction 2025-04-21 19:56:39 +02:00
TMA
c8892f825c cosmetic 2025-04-21 19:56:15 +02:00
TMA
96027ca66a #19: stored function for "consolidation" transaction 2025-04-21 19:49:07 +02:00
TMA
aad79dafa6 fix bugs in schema version checking 2025-04-21 18:45:00 +02:00
TMA
1df7db93e7 #16: stored function for receipt reimbursement transaction 2025-04-21 18:36:50 +02:00
TMA
629b35655d #15: stored function for cash-based stock replenishment transaction 2025-04-21 18:23:11 +02:00
TMA
ad832fc71b #14: stored function for "credit" transfer transactions 2025-04-21 17:45:40 +02:00
TMA
e15f1646ce #13: stored function for cash withdrawal transactions 2025-04-21 17:37:31 +02:00
TMA
7b11a8c954 #12 - stored function for cash deposit transactions - remove duplicate fn 2025-04-21 17:30:57 +02:00
TMA
f0cd8361d1 #12 - stored function for cash deposit transactions 2025-04-21 17:23:24 +02:00
TMA
7ed7417492 schema 0010: fix header id 2025-04-21 17:06:42 +02:00
TMA
028d4d98db #11: stored function for sale undo transaction 2025-04-21 17:06:21 +02:00
TMA
deb3faa717 minor fixes in schemata 0009 and 0010 2025-04-21 12:27:29 +02:00
TMA
4c7012c517 #10: stored function for cash sell transaction 2025-04-21 12:22:22 +02:00
TMA
cef95c4313 #9: stored function for sell transaction 2025-04-20 23:50:25 +02:00
aded7a5769 #8: stored function to update currency buy rate 2025-04-20 19:27:02 +02:00
66870bbc8c #7: migrate to stored function for updating currency sell rate. 2025-04-20 19:22:19 +02:00
9235607d4c #6: new currency stored function 2025-04-20 19:15:23 +02:00
8f42145bee #5: rename account stored function 2025-04-20 18:00:40 +02:00
58ab1d00be #4: add account to barcode stored procedure. 2025-04-20 17:55:18 +02:00
c5d1fc3402 #3: create account in SQL not in Python. 2025-04-20 17:42:12 +02:00
111a8c9b63 #2: add trading account type type and fix schema migrations machinery 2025-04-20 17:11:49 +02:00
c04f934340 #1: add initial SQL schema as the database currently in use 2025-04-20 17:01:05 +02:00
TMA
40f4abe37f Merge branch 'sql-refactor' 2025-04-20 14:51:32 +02:00
TMA
900264469a schema v2 setup script 2025-04-20 10:13:31 +02:00
TMA
f6ec2be215 schema v1 setup script 2025-04-20 10:12:30 +02:00
TMA
3000731ac7 SQL schema v002 script part 1 2025-04-11 20:56:17 +02:00
TMA
2f601a0b1a functions for querying sequence values for read only access 2025-04-11 13:39:07 +02:00
TMA
5f9292fd02 Merge code from deployed production version 2025-03-30 20:58:03 +02:00
TMA
e04d614e15 uncommitted changes from other people... again 2025-03-30 20:55:00 +02:00
niekt0
dbd3835dfb fixed NaN issue in withdraw/charge 2024-01-30 20:18:07 +01:00
brmbar
6749b2c97a autostock.py 2018-04-02 01:46:01 +02:00
brmbar
a1c37cb695 brmbar-cli: restock by EAN (for automated restocking) 2018-04-02 01:41:24 +02:00
brmbar
f061cc7f7b gitignore 2018-04-02 01:41:02 +02:00
brmbar
6f3fc6767a db purge howto 2018-04-02 01:40:46 +02:00
brmbar
e2e6632df0 sample crontab file 2018-04-02 01:40:37 +02:00
brmbar
1f3f1cdd8f disable exchange rate and balance in StockMgmt because of unbearable slowdown 2016-08-19 04:37:04 +02:00
brmbar
466b92e7c2 uncommitted changes from other people... 2016-08-19 04:26:40 +02:00
brmbar
6208ae6027 Merge branch 'master' of https://github.com/brmlab/brmbar 2016-01-10 21:13:58 +01:00
Ruzicka Pavel
8f6776f3d0 brmbar-cli userlog 2016-01-10 21:07:47 +01:00
brmbar
a9e72d736c brmbar-cli userlog 2016-01-10 21:03:59 +01:00
Petr Baudis
0e306912d7 USAGE: Document brmbar-cli stats 2016-01-07 05:26:40 +01:00
brmbar
c419c91a40 brmbar-cli stats: separate material, logical accounts 2016-01-07 05:26:13 +01:00
brmbar
c5becfcabe brmbar-cli stats: sum excess and deficit to Fixups 2016-01-07 05:20:50 +01:00
brmbar
2bfd1796d0 brmbar-cli stats: Separate credit and overflow 2016-01-07 05:06:37 +01:00
Petr Baudis
9a70088591 USAGE: Fix pre within lists 2016-01-06 05:02:24 +01:00
Petr Baudis
11eaecfccf brmbar-cli changecash -> fixcash (changecash remains an alias) 2016-01-06 02:00:56 +01:00
Petr Baudis
0c349f6c6a USAGE: A few new scenarios for Administrative Usage 2016-01-05 22:00:03 +01:00
brmbar
e3a4fe880d brmbar-cli.py docs: Two examples 2016-01-04 21:04:49 +01:00
brmbar
4494dafbb8 Shop.inventory_balance(): Tidy up, commented out code for easy per-item balance dumps 2016-01-04 21:04:35 +01:00
brmbar
9e95724556 brmbar-cli.py sellitem: Allow selling for cash 2016-01-04 20:49:08 +01:00
brmbar
2bbb46d4ea brmbar-cli.py inventory-interactive: Tidy up code 2016-01-04 20:41:19 +01:00
brmbar
6b67dd372e brmbar-cli.py inventory-interactive: Try to detect mistaken barcode scan 2016-01-04 20:39:51 +01:00
brmbar
edc99f1ff9 brmbar-cli.py docs: All commands are implemented 2016-01-04 20:35:43 +01:00
Pavel Ruzicka
7a8e4ef794 mplayer -really-quiet 2015-12-15 02:54:46 +01:00
brmbar
3ea61f01d7 brmbar-cli undo: New feature 2015-12-13 16:36:50 +01:00
brmbar
feec2c9ac4 brmbar-cli restock: Print correct old balance 2015-12-13 16:36:21 +01:00
Petr Baudis
76bb2cac98 brmbar-cli help: Nicer inventorization split 2015-12-13 16:10:34 +01:00
Petr Baudis
099d775102 Add missing collateral changes for Transfer 2015-12-13 16:10:03 +01:00
brmbar
5c84bd6a8f GUI: Support for money transfer (by sachy + pasky) 2015-10-31 22:02:12 +01:00
brmbar
a162e544c1 alert.sh: +x 2015-10-31 21:16:01 +01:00
TrimenZ
906a3b62ed brmbar-qt4: Add charge sound
..
2015-10-14 22:44:12 +02:00
TrimenZ
fe80200e49 alert.sh
Add charge sound.
2015-10-14 22:39:36 +02:00
Mrkva
48c583a81c Merge branch 'master' of github.com:brmlab/brmbar 2015-07-01 01:08:12 +02:00
Mrkva
934873c2fc Old uncommited changes, restock from cli, "cleanup bounty" feature 2015-07-01 01:06:59 +02:00
Petr Baudis
6cc95ab680 brmbar-qt4: Add ALERT_SCRIPT support to call external hook on low balance 2015-06-04 23:07:21 +02:00
Petr Baudis
1c0e51a246 brmbar-qt4: Add LIMIT_BALANCE(=-200) as a lower limit of user credit 2015-06-04 22:57:20 +02:00
Mrkva
84881dece3 * merge barcode-generator to master
* quick howto on how current barcodes are generated
2015-05-11 02:04:11 +02:00
niekt0
5de7bf7458 Disabled default and inefective computation of the stock sumary 2015-01-31 21:09:52 +01:00
59 changed files with 3539 additions and 312 deletions

1
.gitignore vendored
View file

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

2
barcode-generator/.gitignore vendored Normal file
View file

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

View file

@ -0,0 +1,51 @@
#!/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

@ -0,0 +1,9 @@
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 +1,3 @@
__pycache__ __pycache__
*.log
brmbar/*.pyc

64
brmbar3/PURGE.txt Normal file
View file

@ -0,0 +1,64 @@
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,11 +1,11 @@
CREATE SEQUENCE currencies_id_seq START WITH 1 INCREMENT BY 1; CREATE SEQUENCE currencies_id_seq START WITH 2 INCREMENT BY 1;
CREATE TABLE currencies ( CREATE TABLE currencies (
id INTEGER PRIMARY KEY NOT NULL DEFAULT NEXTVAL('currencies_id_seq'::regclass), id INTEGER PRIMARY KEY NOT NULL DEFAULT NEXTVAL('currencies_id_seq'::regclass),
name VARCHAR(128) NOT NULL, name VARCHAR(128) NOT NULL,
UNIQUE(name) UNIQUE(name)
); );
-- Some code depends on the primary physical currency to have id 1. -- Some code depends on the primary physical currency to have id 1.
INSERT INTO currencies (name) VALUES ('Kč'); INSERT INTO currencies (id, name) VALUES (1, 'Kč');
CREATE TYPE exchange_rate_direction AS ENUM ('source_to_target', 'target_to_source'); CREATE TYPE exchange_rate_direction AS ENUM ('source_to_target', 'target_to_source');
CREATE TABLE exchange_rates ( CREATE TABLE exchange_rates (
@ -107,10 +107,9 @@ CREATE VIEW transaction_nicesplits AS
FROM transaction_splits AS ts LEFT JOIN accounts AS a ON a.id = ts.account FROM transaction_splits AS ts LEFT JOIN accounts AS a ON a.id = ts.account
ORDER BY ts.id; ORDER BY ts.id;
-- List transactions with summary information regarding their cash element -- List transactions with summary information regarding their cash element.
-- (except in case of transfers between cash and debt accounts, which will cancel out).
CREATE VIEW transaction_cashsums AS 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, t.description AS description 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 FROM transactions AS t
LEFT JOIN (SELECT cts.amount AS credit_cash, cts.transaction AS cts_t LEFT JOIN (SELECT cts.amount AS credit_cash, cts.transaction AS cts_t
FROM transaction_nicesplits AS cts FROM transaction_nicesplits AS cts
@ -124,4 +123,5 @@ CREATE VIEW transaction_cashsums AS
WHERE a.currency = (SELECT currency FROM accounts WHERE name = 'BrmBar Cash') WHERE a.currency = (SELECT currency FROM accounts WHERE name = 'BrmBar Cash')
AND a.acctype IN ('cash', 'debt') AND a.acctype IN ('cash', 'debt')
AND dts.amount > 0) debit ON dts_t = t.id AND dts.amount > 0) debit ON dts_t = t.id
GROUP BY t.id ORDER BY 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

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

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

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

@ -69,11 +69,52 @@ Administrative Usage
* If your inventory stock count or cash box amount does not match * If your inventory stock count or cash box amount does not match
the in-system data, you will need to make a corrective transaction. the in-system data, you will need to make a corrective transaction.
In the future, brmbar-cli.py will support this, but there is no To fix cash amount to reality in which you counted 1234Kč, use
implementation yet; it's not entirely clear yet what is the proper
way to do this from the accounting standpoint. In the meantime, you ./brmbar-cli.py fixcash 1234
can use SQL INSERTs to manually create a transaction with appropriate
transaction splits (see doc/architecture for details on splits). 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 Useful SQL queries

8
brmbar3/USEFUL.txt Normal file
View file

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

7
brmbar3/alert.sh Executable file
View file

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

45
brmbar3/autostock.py Executable file
View file

@ -0,0 +1,45 @@
#! /usr/bin/env python3
import argparse
import brmbar
import math
from brmbar import Database
import sys
def main():
parser = argparse.ArgumentParser(usage = "File format: EAN amount total_price name, e.g. 4001242002377 6 167.40 Chio Tortillas")
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__":
print("!!! THIS PROGRAM NO LONGER WORKS !!!")
sys.exit(1)
main()

View file

@ -6,6 +6,8 @@ from brmbar import Database
import brmbar import brmbar
print("!!! THIS PROGRAM NO LONGER WORKS !!!")
sys.exit(1)
def help(): def help():
print("""BrmBar v3 (c) Petr Baudis <pasky@ucw.cz> 2012-2013 print("""BrmBar v3 (c) Petr Baudis <pasky@ucw.cz> 2012-2013
@ -15,9 +17,11 @@ Usage: brmbar-cli.py COMMAND ARGS...
1. Commands pertaining the standard operation 1. Commands pertaining the standard operation
showcredit USER showcredit USER
changecredit USER +-AMT changecredit USER +-AMT
sellitem USER ITEM +-AMT sellitem {USER|"cash"} ITEM +-AMT
You can use negative AMT to undo a sale. You can use negative AMT to undo a sale.
restock ITEM AMT
userinfo USER userinfo USER
userlog USER TIMESTAMP
iteminfo ITEM iteminfo ITEM
2. Management commands 2. Management commands
@ -30,11 +34,17 @@ Usage: brmbar-cli.py COMMAND ARGS...
screen of the GUI. screen of the GUI.
adduser USER adduser USER
Add user (debt) account with given username. 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 ITEM1 NEW_AMOUNT1 ITEM2 NEW_AMOUNT2
Inventory recounting (fixing the number of items) Inventory recounting (fixing the number of items)
inventory-interactive inventory-interactive
Launches interactive mode for performing inventory with barcode reader Launches interactive mode for performing inventory with barcode reader
changecash AMT fixcash AMT
Fixes the cash and puts money difference into excess or deficit account Fixes the cash and puts money difference into excess or deficit account
consolidate consolidate
Wraps up inventory + cash recounting, transferring the excess and Wraps up inventory + cash recounting, transferring the excess and
@ -48,7 +58,19 @@ For users, you can use their name as USER as their username
is also the barcode. For items, use listitems command first is also the barcode. For items, use listitems command first
to find out the item id. to find out the item id.
Commands prefixed with ! are not implemented yet.""") 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) sys.exit(1)
@ -77,6 +99,12 @@ def load_item(inp):
exit(1) exit(1)
return acct 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 = Database.Database("dbname=brmbar")
shop = brmbar.Shop.new_with_defaults(db) shop = brmbar.Shop.new_with_defaults(db)
@ -100,14 +128,20 @@ elif sys.argv[1] == "changecredit":
print("{}: {}".format(acct.name, acct.negbalance_str())) print("{}: {}".format(acct.name, acct.negbalance_str()))
elif sys.argv[1] == "sellitem": elif sys.argv[1] == "sellitem":
if sys.argv[2] == "cash":
uacct = shop.cash
else:
uacct = load_user(sys.argv[2]) uacct = load_user(sys.argv[2])
iacct = load_item(sys.argv[3]) iacct = load_item(sys.argv[3])
amt = int(sys.argv[4]) amt = int(sys.argv[4])
if amt > 0: if amt > 0:
if uacct == shop.cash:
shop.sell_for_cash(item = iacct, amount = amt)
else:
shop.sell(item = iacct, user = uacct, amount = amt) shop.sell(item = iacct, user = uacct, amount = amt)
elif amt < 0: elif amt < 0:
shop.undo_sale(item = iacct, user = uacct, amount = -amt) shop.undo_sale(item = iacct, user = uacct, amount = -amt)
print("{}: {}".format(uacct.name, uacct.negbalance_str())) print("{}: {}".format(uacct.name, uacct.balance_str() if uacct == shop.cash else uacct.negbalance_str()))
print("{}: {}".format(iacct.name, iacct.balance_str())) print("{}: {}".format(iacct.name, iacct.balance_str()))
elif sys.argv[1] == "userinfo": elif sys.argv[1] == "userinfo":
@ -117,6 +151,14 @@ elif sys.argv[1] == "userinfo":
res = db.execute_and_fetchall("SELECT barcode FROM barcodes WHERE account = %s", [acct.id]) res = db.execute_and_fetchall("SELECT barcode FROM barcodes WHERE account = %s", [acct.id])
print("Barcodes: " + ", ".join(map((lambda r: r[0]), res))) 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": elif sys.argv[1] == "iteminfo":
acct = load_item(sys.argv[2]) acct = load_item(sys.argv[2])
print("{} (id {}): {} pcs".format(acct.name, acct.id, acct.balance())) print("{} (id {}): {} pcs".format(acct.name, acct.id, acct.balance()))
@ -136,18 +178,27 @@ elif sys.argv[1] == "listitems":
print("{}\t{}\t{} pcs".format(acct.name, acct.id, acct.balance())) print("{}\t{}\t{} pcs".format(acct.name, acct.id, acct.balance()))
elif sys.argv[1] == "stats": elif sys.argv[1] == "stats":
print("--- Material Assets ---")
print("Cash: {}".format(shop.cash.balance_str())) print("Cash: {}".format(shop.cash.balance_str()))
print("Profit: {}".format(shop.profits.balance_str())) print("Overflow: {}".format(shop.currency.str(shop.credit_balance(overflow='only'))))
print("Credit: {}".format(shop.credit_negbalance_str()))
print("Inventory: {}".format(shop.inventory_balance_str())) print("Inventory: {}".format(shop.inventory_balance_str()))
print("Excess: {}".format(shop.excess.negbalance_str())) print("--- Logical Accounts ---")
print("Deficit: {}".format(shop.deficit.balance_str())) 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": elif sys.argv[1] == "adduser":
acct = brmbar.Account.create(db, sys.argv[2], brmbar.Currency.load(db, id = 1), 'debt') acct = brmbar.Account.create(db, sys.argv[2], brmbar.Currency.load(db, id = 1), 'debt')
acct.add_barcode(sys.argv[2]) # will commit acct.add_barcode(sys.argv[2]) # will commit
print("{}: id {}".format(acct.name, acct.id)); 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": elif sys.argv[1] == "inventory":
if (len(sys.argv) % 2 != 0 or len(sys.argv) < 4): if (len(sys.argv) % 2 != 0 or len(sys.argv) < 4):
print ("Invalid number of parameters, count your parameters.") print ("Invalid number of parameters, count your parameters.")
@ -165,26 +216,27 @@ elif sys.argv[1] == "inventory":
elif sys.argv[1] == "inventory-interactive": elif sys.argv[1] == "inventory-interactive":
print("Inventory interactive mode. To exit interactive mode just enter empty barcode") print("Inventory interactive mode. To exit interactive mode just enter empty barcode")
keep_entering = True while True:
while keep_entering:
barcode = str(input("Enter barcode:")) barcode = str(input("Enter barcode:"))
fuckyou = input("fuckyou") fuckyou = input("fuckyou")
if barcode == "": if barcode == "":
break break
else:
iacct = brmbar.Account.load_by_barcode(db, barcode) iacct = brmbar.Account.load_by_barcode(db, barcode)
amount = str(input("What is the amount of {} in reality current is {}:".format(iacct.name, iacct.balance()))) amount = str(input("What is the amount of {} in reality (expected: {} pcs):".format(iacct.name, iacct.balance())))
if amount == "": if amount == "":
break break
elif int(amount) > 10000:
print("Ignoring too high amount {}, assuming barcode was mistakenly scanned instead".format(amount))
else: else:
iamt = int(amount) iamt = int(amount)
print("Current state {} (id {}): {} pcs".format(iacct.name, iacct.id, iacct.balance())) print("Current state {} (id {}): {} pcs".format(iacct.name, iacct.id, iacct.balance()))
if shop.fix_inventory(item = iacct, amount = iamt): if shop.fix_inventory(item = iacct, amount = iamt):
print("New state {} (id {}): {} pcs".format(iacct.name, iacct.id, iacct.balance())) print("New state {} (id {}): {} pcs".format(iacct.name, iacct.id, iacct.balance()))
else: else:
print ("No action needed amount is correct.") print("No action needed, amount is correct.")
print("End of processing. Bye") print("End of processing. Bye")
elif sys.argv[1] == "changecash":
elif sys.argv[1] == "fixcash" or sys.argv[1] == "changecash":
if (len(sys.argv) != 3): if (len(sys.argv) != 3):
print ("Invalid number of parameters, check your parameters.") print ("Invalid number of parameters, check your parameters.")
else: else:
@ -194,12 +246,23 @@ elif sys.argv[1] == "changecash":
print("New Cash is : {}".format(shop.cash.balance_str())) print("New Cash is : {}".format(shop.cash.balance_str()))
else: else:
print ("No action needed amount is the same.") print ("No action needed amount is the same.")
elif sys.argv[1] == "consolidate": elif sys.argv[1] == "consolidate":
if (len(sys.argv) != 2): if (len(sys.argv) != 2):
print ("Invalid number of parameters, check your parameters.") print ("Invalid number of parameters, check your parameters.")
else: else:
shop.consolidate() 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: else:
help() help()

View file

@ -1,6 +1,7 @@
#!/usr/bin/python3 #!/usr/bin/python3
import sys import sys
import subprocess
from PySide import QtCore, QtGui, QtDeclarative from PySide import QtCore, QtGui, QtDeclarative
@ -8,6 +9,29 @@ from brmbar import Database
import brmbar import brmbar
import argparse
import logging
root = logging.getLogger()
root.setLevel(logging.DEBUG)
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
root.addHandler(handler)
logger = logging.getLogger(__name__)
# User credit balance limit; sale will fail when balance is below this limit.
LIMIT_BALANCE = -200
# When below this credit balance, an alert hook script (see below) is run.
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): class ShopAdapter(QtCore.QObject):
""" Interface between QML and the brmbar package """ """ Interface between QML and the brmbar package """
@ -29,13 +53,23 @@ class ShopAdapter(QtCore.QObject):
map["price"] = str(sell) map["price"] = str(sell)
return map 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): def acct_cash_map(self, acct):
map = acct.__dict__.copy() map = acct.__dict__.copy()
return map return map
def acct_map(self, acct): def acct_map(self, acct):
if acct is None: if acct is None:
logger.debug("acct_map: acct is None")
return None return None
logger.debug("acct_map: acct.acctype=%s", acct.acctype)
if acct.acctype == 'debt': if acct.acctype == 'debt':
return self.acct_debt_map(acct) return self.acct_debt_map(acct)
elif acct.acctype == "inventory": elif acct.acctype == "inventory":
@ -54,12 +88,14 @@ class ShopAdapter(QtCore.QObject):
Therefore, we construct a map that we can pass around easily. Therefore, we construct a map that we can pass around easily.
We return None on unrecognized barcode. """ We return None on unrecognized barcode. """
barcode = str(barcode) barcode = str(barcode)
logger.debug("barcodeInput: barcode='%s'", barcode)
if barcode and barcode[0] == "$": if barcode and barcode[0] == "$":
credits = {'$02': 20, '$05': 50, '$10': 100, '$20': 200, '$50': 500, '$1k': 1000} credits = {'$02': 20, '$05': 50, '$10': 100, '$20': 200, '$50': 500, '$1k': 1000}
credit = credits[barcode] credit = credits[barcode]
if credit is None: if credit is None:
return None return None
return { "acctype": "recharge", "amount": str(credit)+".00" } return { "acctype": "recharge", "amount": str(credit)+".00" }
logger.debug("barcodeInput: before load_by_barcode")
acct = self.acct_map(brmbar.Account.load_by_barcode(db, barcode)) acct = self.acct_map(brmbar.Account.load_by_barcode(db, barcode))
db.commit() db.commit()
return acct return acct
@ -70,6 +106,18 @@ class ShopAdapter(QtCore.QObject):
db.commit() db.commit()
return acct 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') @QtCore.Slot('QVariant', 'QVariant', result='QVariant')
def sellItem(self, itemid, userid): def sellItem(self, itemid, userid):
user = brmbar.Account.load(db, id = userid) user = brmbar.Account.load(db, id = userid)
@ -85,6 +133,7 @@ class ShopAdapter(QtCore.QObject):
@QtCore.Slot('QVariant', 'QVariant', result='QVariant') @QtCore.Slot('QVariant', 'QVariant', result='QVariant')
def chargeCredit(self, credit, userid): def chargeCredit(self, credit, userid):
subprocess.call(["sh", ALERT_SCRIPT, "charge"])
user = brmbar.Account.load(db, id = userid) user = brmbar.Account.load(db, id = userid)
shop.add_credit(credit = credit, user = user) shop.add_credit(credit = credit, user = user)
balance = user.negbalance_str() balance = user.negbalance_str()
@ -99,23 +148,45 @@ class ShopAdapter(QtCore.QObject):
db.commit() db.commit()
return balance return balance
@QtCore.Slot('QVariant', 'QVariant', 'QVariant', result='QVariant')
def newTransfer(self, uidfrom, uidto, amount):
logger.debug("newTransfer %s %s %s", uidfrom, uidto, amount)
ufrom = brmbar.Account.load(db, id=uidfrom)
logger.debug(" ufrom = %s", ufrom)
uto = brmbar.Account.load(db, id=uidto)
logger.debug(" uto = %s", uto)
shop.transfer_credit(ufrom, uto, amount = amount)
db.commit()
csfa = currency.str(float(amount))
logger.debug(" csfa = '%s'", csfa)
return csfa
@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') @QtCore.Slot(result='QVariant')
def balance_cash(self): def balance_cash(self):
return "N/A"
balance = shop.cash.balance_str() balance = shop.cash.balance_str()
db.commit() db.commit()
return balance return balance
@QtCore.Slot(result='QVariant') @QtCore.Slot(result='QVariant')
def balance_profit(self): def balance_profit(self):
return "N/A"
balance = shop.profits.balance_str() balance = shop.profits.balance_str()
db.commit() db.commit()
return balance return balance
@QtCore.Slot(result='QVariant') @QtCore.Slot(result='QVariant')
def balance_inventory(self): def balance_inventory(self):
return "N/A"
balance = shop.inventory_balance_str() balance = shop.inventory_balance_str()
db.commit() db.commit()
return balance return balance
@QtCore.Slot(result='QVariant') @QtCore.Slot(result='QVariant')
def balance_credit(self): def balance_credit(self):
return "N/A"
balance = shop.credit_negbalance_str() balance = shop.credit_negbalance_str()
db.commit() db.commit()
return balance return balance
@ -128,7 +199,7 @@ class ShopAdapter(QtCore.QObject):
@QtCore.Slot('QVariant', result='QVariant') @QtCore.Slot('QVariant', result='QVariant')
def itemList(self, query): def itemList(self, query):
alist = [ self.acct_inventory_map(a) for a in shop.account_list("inventory", like_str="%%"+query+"%%") ] alist = [ self.acct_inventory_map2(a) for a in shop.account_list("inventory", like_str="%%"+query+"%%") ]
db.commit() db.commit()
return alist return alist
@ -180,7 +251,23 @@ class ShopAdapter(QtCore.QObject):
db.commit() db.commit()
return balance return balance
db = Database.Database("dbname=brmbar") parser = argparse.ArgumentParser()
parser.add_argument("--dbname", help="Database name", type=str)
parser.add_argument("--dbuser", help="Database user", type=str)
parser.add_argument("--dbhost", help="Database host", type=str)
parser.add_argument("--dbpass", help="Database user password", type=str)
args = parser.parse_args()
argdbname = args.dbname
argdbuser = args.dbuser
argdbhost = args.dbhost
argdbpass = args.dbpass
db = Database.Database(
"dbname={0} user={1} host={2} password={3}".format(
argdbname,argdbuser,argdbhost,argdbpass
)
)
shop = brmbar.Shop.new_with_defaults(db) shop = brmbar.Shop.new_with_defaults(db)
currency = shop.currency currency = shop.currency
db.commit() db.commit()

View file

@ -100,8 +100,17 @@ Item {
} }
function chargeCredit() { function chargeCredit() {
var balance = shop.chargeCredit(amount, userdbid) var balance=0
status_text.setStatus("Charged! "+username+"'s credit is "+balance+".", "#ffff7c") 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")
}
}
loadPage("MainPage") loadPage("MainPage")
} }
} }

View file

@ -58,6 +58,8 @@ Item {
if (acct.acctype == "cash") { //Copied from BarButton.onButtonClick if (acct.acctype == "cash") { //Copied from BarButton.onButtonClick
shop.sellItemCash(dbid) shop.sellItemCash(dbid)
status_text.setStatus("Sold! Put " + price + " Kč in the money box.", "#ffff7c") 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 { } else {
var balance = shop.sellItem(dbid, acct.id) var balance = shop.sellItem(dbid, acct.id)
status_text.setStatus("Sold! "+acct.name+"'s credit is "+balance+".", "#ffff7c") status_text.setStatus("Sold! "+acct.name+"'s credit is "+balance+".", "#ffff7c")

View file

@ -33,6 +33,16 @@ Item {
} }
} }
BarButton {
x: 450
y: 838
width: 360
text: "Transfer"
onButtonClick: {
loadPage("Transfer")
}
}
BarButton { BarButton {
id: management id: management
x: 855 x: 855
@ -43,4 +53,11 @@ Item {
loadPage("Management") loadPage("Management")
} }
} }
BarButton {
x: 65
y: 438
width: 1150
text: "* Za uklid brmlabu vam nabijeme kredit. *"
}
} }

View file

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

@ -100,8 +100,17 @@ Item {
} }
function withdrawCredit() { function withdrawCredit() {
var balance = shop.withdrawCredit(amount, userdbid) var balance=0
status_text.setStatus("Withdrawn! "+username+"'s credit is "+balance+".", "#ffff7c") 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")
}
}
loadPage("MainPage") loadPage("MainPage")
} }
} }

View file

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

View file

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

View file

@ -1,10 +1,15 @@
from .Currency import Currency from .Currency import Currency
import logging
logger = logging.getLogger(__name__)
class Account: class Account:
"""BrmBar Account """BrmBar Account
Both users and items are accounts. So is the money box, etc. Both users and items are accounts. So is the money box, etc.
Each account has a currency.""" Each account has a currency."""
def __init__(self, db, id, name, currency, acctype): def __init__(self, db, id, name, currency, acctype):
self.db = db self.db = db
self.id = id self.id = id
@ -14,42 +19,35 @@ class Account:
@classmethod @classmethod
def load_by_barcode(cls, db, barcode): def load_by_barcode(cls, db, barcode):
res = db.execute_and_fetch("SELECT account FROM barcodes WHERE barcode = %s", [barcode]) logger.debug("load_by_barcode: '%s'", barcode)
if res is None: account_id, account_name, account_acctype, currency_id, currency_name = db.execute_and_fetch(
return None "SELECT account_id, account_name, account_acctype, currency_id, currency_name FROM public.account_class_initialization_data('by_barcode', NULL, %s)",
id = res[0] [barcode])
return cls.load(db, id = id) currency = Currency(db, currency_id, currency_name)
return cls(db, account_id, account_name, currency, account_acctype)
@classmethod @classmethod
def load(cls, db, id = None, name = None): def load(cls, db, id=None):
"""Constructor for existing account""" """Constructor for existing account"""
if id is not None: account_id, account_name, account_acctype, currency_id, currency_name = db.execute_and_fetch(
name = db.execute_and_fetch("SELECT name FROM accounts WHERE id = %s", [id]) "SELECT account_id, account_name, account_acctype, currency_id, currency_name FROM public.account_class_initialization_data('by_id', %s, NULL)",
name = name[0] [id])
elif name is not None: currency = Currency(db, currency_id, currency_name)
id = db.execute_and_fetch("SELECT id FROM accounts WHERE name = %s", [name]) return cls(db, account_id, account_name, currency, account_acctype)
id = id[0]
else:
raise NameError("Account.load(): Specify either id or name")
currid, acctype = db.execute_and_fetch("SELECT currency, acctype FROM accounts WHERE id = %s", [id])
currency = Currency.load(db, id = currid)
return cls(db, name = name, id = id, currency = currency, acctype = acctype)
@classmethod @classmethod
def create(cls, db, name, currency, acctype): def create(cls, db, name, currency, acctype):
"""Constructor for new account""" """Constructor for new account"""
id = db.execute_and_fetch("INSERT INTO accounts (name, currency, acctype) VALUES (%s, %s, %s) RETURNING id", [name, currency.id, acctype]) id = db.execute_and_fetch(
id = id[0] "SELECT public.create_account(%s, %s, %s)", [name, currency.id, acctype]
)
return cls(db, name=name, id=id, currency=currency, acctype=acctype) return cls(db, name=name, id=id, currency=currency, acctype=acctype)
def balance(self): def balance(self):
debit = self.db.execute_and_fetch("SELECT SUM(amount) FROM transaction_splits WHERE account = %s AND side = %s", [self.id, 'debit']) bal = self.db.execute_and_fetch(
debit = debit[0] or 0 "SELECT public.compute_account_balance(%s)", [self.id]
credit = self.db.execute_and_fetch("SELECT SUM(amount) FROM transaction_splits WHERE account = %s AND side = %s", [self.id, 'credit']) )[0]
credit = credit[0] or 0 return bal
return debit - credit
def balance_str(self): def balance_str(self):
return self.currency.str(self.balance()) return self.currency.str(self.balance())
@ -57,20 +55,12 @@ class Account:
def negbalance_str(self): def negbalance_str(self):
return self.currency.str(-self.balance()) return self.currency.str(-self.balance())
def debit(self, transaction, amount, memo):
return self._transaction_split(transaction, 'debit', amount, memo)
def credit(self, transaction, amount, memo):
return self._transaction_split(transaction, 'credit', amount, memo)
def _transaction_split(self, transaction, side, amount, memo):
""" Common part of credit() and debit(). """
self.db.execute("INSERT INTO transaction_splits (transaction, side, account, amount, memo) VALUES (%s, %s, %s, %s, %s)", [transaction, side, self.id, amount, memo])
def add_barcode(self, barcode): def add_barcode(self, barcode):
self.db.execute("INSERT INTO barcodes (account, barcode) VALUES (%s, %s)", [self.id, barcode]) self.db.execute(
"SELECT public.add_barcode_to_account(%s, %s)", [self.id, barcode]
)
self.db.commit() self.db.commit()
def rename(self, name): def rename(self, name):
self.db.execute("UPDATE accounts SET name = %s WHERE id = %s", [name, self.id]) self.db.execute("SELECT public.rename_account(%s, %s)", [self.id, name])
self.name = name self.name = name

View file

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

View file

@ -3,9 +3,14 @@
import psycopg2 import psycopg2
from contextlib import closing from contextlib import closing
import time import time
import logging
logger = logging.getLogger(__name__)
class Database: class Database:
"""self-reconnecting database object""" """self-reconnecting database object"""
def __init__(self, dsn): def __init__(self, dsn):
self.db_conn = psycopg2.connect(dsn) self.db_conn = psycopg2.connect(dsn)
self.dsn = dsn self.dsn = dsn
@ -30,22 +35,27 @@ class Database:
def _execute(self, cur, query, attrs, level=1): def _execute(self, cur, query, attrs, level=1):
"""execute a query, and in case of OperationalError (db restart) """execute a query, and in case of OperationalError (db restart)
reconnect to database. Recurses with increasig pause between tries""" reconnect to database. Recurses with increasig pause between tries"""
logger.debug("SQL: (%s) @%s" % (query, time.strftime("%Y%m%d %a %I:%m %p")))
try: try:
if attrs is None: if attrs is None:
cur.execute(query) cur.execute(query)
else: else:
cur.execute(query, attrs) cur.execute(query, attrs)
return cur return cur
except psycopg2.DataError as error: # when biitr comes and enters '99999999999999999999' for amount except (
print("We have invalid input data (SQLi?): level %s (%s) @%s" % ( psycopg2.DataError
level, error, time.strftime("%Y%m%d %a %I:%m %p") ) as error: # when biitr comes and enters '99999999999999999999' for amount
)) logger.debug(
"We have invalid input data (SQLi?): level %s (%s) @%s"
% (level, error, time.strftime("%Y%m%d %a %I:%m %p"))
)
self.db_conn.rollback() self.db_conn.rollback()
raise RuntimeError("Unsanitized data entered again... BOBBY TABLES") raise RuntimeError("Unsanitized data entered again... BOBBY TABLES")
except psycopg2.OperationalError as error: except psycopg2.OperationalError as error:
print("Sleeping: level %s (%s) @%s" % ( logger.debug(
level, error, time.strftime("%Y%m%d %a %I:%m %p") "Sleeping: level %s (%s) @%s"
)) % (level, error, time.strftime("%Y%m%d %a %I:%m %p"))
)
# TODO: emit message "db conn failed, reconnecting # TODO: emit message "db conn failed, reconnecting
time.sleep(2**level) time.sleep(2**level)
try: try:
@ -55,6 +65,9 @@ class Database:
time.sleep(1) time.sleep(1)
cur = self.db_conn.cursor() # how ugly is this? cur = self.db_conn.cursor() # how ugly is this?
return self._execute(cur, query, attrs, level + 1) return self._execute(cur, query, attrs, level + 1)
except Exception as ex:
logger.debug("_execute exception: %s", ex)
self.db_conn.rollback()
def commit(self): def commit(self):
"""passes commit to db""" """passes commit to db"""

View file

@ -1,108 +1,168 @@
import brmbar import brmbar
from .Currency import Currency from .Currency import Currency
from .Account import Account from .Account import Account
import logging
logger = logging.getLogger(__name__)
class Shop: class Shop:
"""BrmBar Shop """BrmBar Shop
Business logic so that only interaction is left in the hands Business logic so that only interaction is left in the hands
of the frontend scripts.""" of the frontend scripts."""
def __init__(self, db, currency, profits, cash, excess, deficit):
def __init__(self, db, currency_id, profits_id, cash_id, excess_id, deficit_id):
# Keep db as-is
self.db = db self.db = db
self.currency = currency # brmbar.Currency
self.profits = profits # income brmbar.Account for brmbar profit margins on items # Store all ids
self.cash = cash # our operational ("wallet") cash account self.currency_id = currency_id
self.excess = excess # account from which is deducted cash during inventory item fixing (when system contains less items than is the reality) self.profits_id = profits_id
self.deficit = deficit # account where is put cash during inventory item fixing (when system contains more items than is the reality) self.cash_id = cash_id
self.excess_id = excess_id
self.deficit_id = deficit_id
# Create objects where needed for legacy code
# brmbar.Currency
self.currency = Currency.load(self.db, id=self.currency_id)
# income brmbar.Account for brmbar profit margins on items
self.profits = Account.load(db, id=self.profits_id)
# our operational ("wallet") cash account
self.cash = Account.load(db, id=self.cash_id)
# account from which is deducted cash during inventory item
# fixing (when system contains less items than is the
# reality)
self.excess = Account.load(db, id=self.excess_id)
# account where is put cash during inventory item fixing (when
# system contains more items than is the reality)
self.deficit = Account.load(db, id=self.deficit_id)
@classmethod @classmethod
def new_with_defaults(cls, db): def new_with_defaults(cls, db):
return cls(db, # shop_class_initialization_data
currency = Currency.default(db), currency_id, profits_id, cash_id, excess_id, deficit_id = db.execute_and_fetch(
profits = Account.load(db, name = "BrmBar Profits"), "select currency_id, profits_id, cash_id, excess_id, deficit_id from public.shop_class_initialization_data()"
cash = Account.load(db, name = "BrmBar Cash"), )
excess = Account.load(db, name = "BrmBar Excess"), return cls(
deficit = Account.load(db, name = "BrmBar Deficit")) db,
currency_id=currency_id,
profits_id=profits_id,
cash_id=cash_id,
excess_id=excess_id,
deficit_id=deficit_id,
)
def sell(self, item, user, amount=1): def sell(self, item, user, amount=1):
# Sale: Currency conversion from item currency to shop currency # Call the stored procedure for the sale
(buy, sell) = item.currency.rates(self.currency) logger.debug(
cost = amount * sell "sell: item.id=%s amount=%s user.id=%s self.currency.id=%s",
profit = amount * (sell - buy) item.id,
amount,
transaction = self._transaction(responsible = user, description = "BrmBar sale of {}x {} to {}".format(amount, item.name, user.name)) user.id,
item.credit(transaction, amount, user.name) self.currency.id,
user.debit(transaction, cost, item.name) # debit (increase) on a _debt_ account )
self.profits.debit(transaction, profit, "Margin on " + item.name) res = self.db.execute_and_fetch(
"SELECT public.sell_item(%s, %s, %s, %s, %s)",
[
item.id,
amount,
user.id,
self.currency.id,
"BrmBar sale of {0}x {1} to {2}".format(amount, item.name, user.name),
],
)
logger.debug("sell: res[0]=%s", res[0])
cost = res[0]
self.db.commit() self.db.commit()
return cost return cost
def sell_for_cash(self, item, amount=1): def sell_for_cash(self, item, amount=1):
# Sale: Currency conversion from item currency to shop currency cost = self.db.execute_and_fetch(
(buy, sell) = item.currency.rates(self.currency) "SELECT public.sell_item_for_cash(%s, %s, %s, %s, %s)",
cost = amount * sell [
profit = amount * (sell - buy) item.id,
amount,
user.id,
self.currency.id,
"BrmBar sale of {0}x {1} for cash".format(amount, item.name),
],
)[0]
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() self.db.commit()
return cost return cost
def undo_sale(self, item, user, amount=1): def undo_sale(self, item, user, amount=1):
# Undo sale; rarely needed # Call the stored procedure for undoing a sale
(buy, sell) = item.currency.rates(self.currency) cost = self.db.execute_and_fetch(
cost = amount * sell "SELECT public.undo_sale_of_item(%s, %s, %s, %s)",
profit = amount * (sell - buy) [
item.id,
transaction = self._transaction(responsible = user, description = "BrmBar sale UNDO of {}x {} to {}".format(amount, item.name, user.name)) amount,
item.debit(transaction, amount, user.name + " (sale undo)") user.id,
user.credit(transaction, cost, item.name + " (sale undo)") user.currency.id,
self.profits.credit(transaction, profit, "Margin repaid on " + item.name) "BrmBar sale UNDO of {0}x {1} to {2}".format(
amount, item.name, user.name
),
],
)[0]
self.db.commit() self.db.commit()
return cost return cost
def add_credit(self, credit, user): def add_credit(self, credit, user):
transaction = self._transaction(responsible = user, description = "BrmBar credit replenishment for " + user.name) self.db.execute_and_fetch(
self.cash.debit(transaction, credit, user.name) "SELECT public.add_credit(%s, %s, %s, %s)",
user.credit(transaction, credit, "Credit replenishment") [self.cash.id, credit, user.id, user.name],
)
self.db.commit() self.db.commit()
def withdraw_credit(self, credit, user): def withdraw_credit(self, credit, user):
transaction = self._transaction(responsible = user, description = "BrmBar credit withdrawal for " + user.name) self.db.execute_and_fetch(
self.cash.credit(transaction, credit, user.name) "SELECT public.withdraw_credit(%s, %s, %s, %s)",
user.debit(transaction, credit, "Credit withdrawal") [self.cash.id, credit, user.id, user.name],
)
self.db.commit()
def transfer_credit(self, userfrom, userto, amount):
self.db.execute_and_fetch(
"SELECT public.transfer_credit(%s, %s, %s, %s, %s, %s)",
[self.cash.id, amount, userfrom.id, userfrom.name, userto.id, userto.name],
)
self.db.commit() self.db.commit()
def buy_for_cash(self, item, amount=1): def buy_for_cash(self, item, amount=1):
# Buy: Currency conversion from item currency to shop currency iamount = int(amount)
(buy, sell) = item.currency.rates(self.currency) famount = float(iamount)
cost = amount * buy assert famount == amount, "amount is not integer value %s".format(amount)
cost = self.db.execute_and_fetch(
transaction = self._transaction(description = "BrmBar stock replenishment of {}x {} for cash".format(amount, item.name)) "SELECT public.buy_for_cash(%s, %s, %s, %s, %s)",
item.debit(transaction, amount, "Cash") [self.cash.id, item.id, iamount, self.currency.id, item.name],
self.cash.credit(transaction, cost, item.name) )[0]
self.db.commit() self.db.commit()
return cost return cost
def receipt_to_credit(self, user, credit, description): def receipt_to_credit(self, user, credit, description):
transaction = self._transaction(responsible = user, description = "Receipt: " + description) self.db.execute_and_fetch(
self.profits.credit(transaction, credit, user.name) "SELECT public.receipt_reimbursement(%s, %s, %s, %s, %s)",
user.credit(transaction, credit, "Credit from receipt: " + description) [self.profits.id, user.id, user.name, credit, description],
)[0]
self.db.commit() self.db.commit()
def _transaction(self, responsible=None, description=None): def _transaction(self, responsible=None, description=None):
transaction = self.db.execute_and_fetch("INSERT INTO transactions (responsible, description) VALUES (%s, %s) RETURNING id", transaction = self.db.execute_and_fetch(
[responsible.id if responsible else None, description]) "INSERT INTO transactions (responsible, description) VALUES (%s, %s) RETURNING id",
[responsible.id if responsible else None, description],
)
transaction = transaction[0] transaction = transaction[0]
return transaction return transaction
def credit_balance(self): def credit_balance(self, overflow=None):
# We assume all debt accounts share a currency # We assume all debt accounts share a currency
sumselect = """ sumselect = """
SELECT SUM(ts.amount) SELECT SUM(ts.amount)
@ -110,95 +170,80 @@ class Shop:
LEFT JOIN transaction_splits AS ts ON a.id = ts.account LEFT JOIN transaction_splits AS ts ON a.id = ts.account
WHERE a.acctype = %s AND ts.side = %s WHERE a.acctype = %s AND ts.side = %s
""" """
cur = self.db.execute_and_fetch(sumselect, ["debt", 'debit']) 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 debit = cur[0] or 0
credit = self.db.execute_and_fetch(sumselect, ["debt", 'credit']) credit = self.db.execute_and_fetch(sumselect, ["debt", "credit"])
credit = credit[0] or 0 credit = credit[0] or 0
return debit - credit return debit - credit
def credit_negbalance_str(self):
return self.currency.str(-self.credit_balance()) def credit_negbalance_str(self, overflow=None):
return self.currency.str(-self.credit_balance(overflow=overflow))
def inventory_balance(self): def inventory_balance(self):
balance = 0 resa = self.db.execute_and_fetch("SELECT * FROM public.inventory_balance()")
# Each inventory account has its own currency, res = resa[0]
# so we just do this ugly iteration logger.debug("inventory_balance resa = %s", resa)
cur = self.db.execute_and_fetchall("SELECT id FROM accounts WHERE acctype = %s", ["inventory"]) return res
for inventory in cur:
invid = inventory[0]
inv = Account.load(self.db, id = invid)
# FIXME: This is not correct as each instance of inventory
# might have been bought for a different price! Therefore,
# we need to replace the command below with a complex SQL
# statement that will... ugh, accounting is hard!
balance += inv.balance() * inv.currency.rates(self.currency)[0]
return balance
def inventory_balance_str(self): def inventory_balance_str(self):
return self.currency.str(self.inventory_balance()) return self.currency.str(self.inventory_balance())
def account_list(self, acctype, like_str="%%"): def account_list(self, acctype, like_str="%%"):
"""list all accounts (people or items, as per acctype)""" """list all accounts (people or items, as per acctype)"""
accts = [] accts = []
cur = self.db.execute_and_fetchall("SELECT id FROM accounts WHERE acctype = %s AND name ILIKE %s ORDER BY name ASC", [acctype, like_str]) cur = self.db.execute_and_fetchall(
"SELECT a.id, a.name aname, a.currency, a.acctype, c.name cname FROM accounts a JOIN currencies c ON c.id=a.currency WHERE a.acctype = %s AND a.name ILIKE %s ORDER BY a.name ASC",
[acctype, like_str],
)
# FIXME: sanitize input like_str ^ # FIXME: sanitize input like_str ^
for inventory in cur: for inventory in cur:
accts += [ Account.load(self.db, id = inventory[0]) ] curr = Currency(db=self.db, id=inventory[2], name=inventory[4]);
accts += [Account(self.db, id=inventory[0], name=inventory[1], currency=curr, acctype=inventory[3])]
return accts return accts
def fix_inventory(self, item, amount): def fix_inventory(self, item, amount):
amount_in_reality = amount rv = self.db.execute_and_fetch(
amount_in_system = item.balance() "SELECT public.fix_inventory(%s, %s, %s, %s, %s, %s)",
(buy, sell) = item.currency.rates(self.currency) [
item.id,
item.currency.id,
self.excess.id,
self.deficit.id,
self.currency.id,
amount,
],
)[0]
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() self.db.commit()
return True return rv
elif amount_in_reality < amount_in_system:
transaction = self._transaction(description = "BrmBar inventory fix of {}pcs {} in system to {}pcs in reality".format(amount_in_system, item.name,amount_in_reality))
item.credit(transaction, diff, "Inventory fix deficit")
self.deficit.debit(transaction, buy_total, "Inventory fix deficit " + item.name)
self.db.commit()
return True
else:
transaction = self._transaction(description = "BrmBar inventory fix of {}pcs {} in system to {}pcs in reality".format(amount_in_system, item.name,amount_in_reality))
item.debit(transaction, 0, "Inventory fix - amount was correct")
item.credit(transaction, 0, "Inventory fix - amount was correct")
self.db.commit()
return False
def fix_cash(self, amount): def fix_cash(self, amount):
amount_in_reality = amount rv = self.db.execute_and_fetch(
amount_in_system = self.cash.balance() "SELECT public.fix_cash(%s, %s, %s, %s)",
[self.excess.id, self.deficit.id, self.currency.id, amount],
)[0]
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() self.db.commit()
return True return rv
elif amount_in_reality < amount_in_system:
transaction = self._transaction(description = "BrmBar cash inventory fix of {} in system to {} in reality".format(amount_in_system, amount_in_reality))
self.cash.credit(transaction, diff, "Inventory fix deficit")
self.deficit.debit(transaction, diff, "Inventory fix deficit.")
self.db.commit()
return True
else:
return False
def consolidate(self): def consolidate(self):
transaction = self._transaction(description = "BrmBar inventory consolidation") msg = self.db.execute_and_fetch(
"SELECT public.make_consolidate_transaction(%s, %s, %s)",
excess_balance = self.excess.balance() [self.excess.id, self.deficit.id, self.profits.id],
if excess_balance != 0: )[0]
print("Excess balance {} debited to profit".format(-excess_balance)) if msg != None:
self.excess.debit(transaction, -excess_balance, "Excess balance added to profit.") print(msg)
self.profits.debit(transaction, -excess_balance, "Excess balance added to profit.")
deficit_balance = self.deficit.balance()
if deficit_balance != 0:
print("Deficit balance {} credited to profit".format(deficit_balance))
self.deficit.credit(transaction, deficit_balance, "Deficit balance removed from profit.")
self.profits.credit(transaction, deficit_balance, "Deficit balance removed from profit.")
self.db.commit() self.db.commit()
def undo(self, oldtid):
transaction = self.db.execute_and_fetch(
"SELECT public.undo_transaction(%s)", [oldtid]
)[0]
self.db.commit()
return transaction

15
brmbar3/crontab Normal file
View file

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

3
brmbar3/dluhy.sh Executable file
View file

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

6
brmbar3/log.sh Executable file
View file

@ -0,0 +1,6 @@
#!/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

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

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

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

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

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

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

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

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

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

View file

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

View file

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

View file

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

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

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

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

View file

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

View file

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

View file

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

View file

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

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

View file

@ -0,0 +1,48 @@
--
-- 0021-constraints-on-numeric-columns.sql
--
-- #21 - stored function for adding constraints to numeric columns
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- TMA <tma+hs@jikos.cz>
--
-- Permission to use, copy, modify, and/or distribute this software
-- for any purpose with or without fee is hereby granted, provided
-- that the above copyright notice and this permission notice appear
-- in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--
-- To require fully-qualified names
SELECT pg_catalog.set_config('search_path', '', false);
DO $upgrade_block$
BEGIN
IF brmbar_privileged.has_exact_schema_version(20) THEN
ALTER TABLE public.transaction_splits ADD CONSTRAINT amount_check
CHECK (amount NOT IN ('Infinity'::numeric, '-Infinity'::numeric, 'NaN'::numeric));
ALTER TABLE public.exchange_rates ADD CONSTRAINT rate_check
CHECK (rate NOT IN ('Infinity'::numeric, '-Infinity'::numeric, 'NaN'::numeric));
PERFORM brmbar_privileged.upgrade_schema_version_to(21);
END IF;
END;
$upgrade_block$;
-- vim: set ft=plsql :

View file

@ -0,0 +1,85 @@
--
-- 0022-shop-init.sql
--
-- #22 - stored function for initializing Shop.py
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- TMA <tma+hs@jikos.cz>
--
-- Permission to use, copy, modify, and/or distribute this software
-- for any purpose with or without fee is hereby granted, provided
-- that the above copyright notice and this permission notice appear
-- in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--
-- To require fully-qualified names
SELECT pg_catalog.set_config('search_path', '', false);
DO $upgrade_block$
DECLARE
v INTEGER;
BEGIN
IF brmbar_privileged.has_exact_schema_version(21) THEN
SELECT COUNT(1) INTO v
FROM pg_catalog.pg_type typ
INNER JOIN pg_catalog.pg_namespace nsp
ON nsp.oid = typ.typnamespace
WHERE nsp.nspname = 'brmbar_privileged'
AND typ.typname='shop_class_initialization_data_type';
IF v>0 THEN
RAISE NOTICE 'Changing type shop_class_initialization_data_type';
DROP TYPE brmbar_privileged.shop_class_initialization_data_type CASCADE;
ELSE
RAISE NOTICE 'Creating type shop_class_initialization_data_type';
END IF;
CREATE TYPE brmbar_privileged.shop_class_initialization_data_type
AS (
currency_id INTEGER, --public.currencies.id%TYPE,
profits_id INTEGER, --public.accounts.id%TYPE,
cash_id INTEGER, --public.accounts.id%TYPE,
excess_id INTEGER, --public.accounts.id%TYPE,
deficit_id INTEGER --public.accounts.id%TYPE
);
CREATE OR REPLACE FUNCTION public.shop_class_initialization_data()
RETURNS brmbar_privileged.shop_class_initialization_data_type
LANGUAGE plpgsql
AS
$$
DECLARE
rv brmbar_privileged.shop_class_initialization_data_type;
BEGIN
rv.currency_id := 1;
SELECT id INTO rv.profits_id FROM public.accounts WHERE name = 'BrmBar Profits';
SELECT id INTO rv.cash_id FROM public.accounts WHERE name = 'BrmBar Cash';
SELECT id INTO rv.excess_id FROM public.accounts WHERE name = 'BrmBar Excess';
SELECT id INTO rv.deficit_id FROM public.accounts WHERE name = 'BrmBar Deficit';
RETURN rv;
END;
$$;
PERFORM brmbar_privileged.upgrade_schema_version_to(22);
END IF;
END;
$upgrade_block$;
-- vim: set ft=plsql :

View file

@ -0,0 +1,94 @@
--
-- 0023-inventory-balance.sql
--
-- #23 - stored function for total inventory balance
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- TMA <tma+hs@jikos.cz>
--
-- Permission to use, copy, modify, and/or distribute this software
-- for any purpose with or without fee is hereby granted, provided
-- that the above copyright notice and this permission notice appear
-- in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--
-- To require fully-qualified names
SELECT pg_catalog.set_config('search_path', '', false);
DO $upgrade_block$
BEGIN
IF brmbar_privileged.has_exact_schema_version(22) THEN
CREATE OR REPLACE VIEW brmbar_privileged.debit_balances AS
SELECT
ts.account AS debit_account,
SUM(ts.amount) AS debit_sum
FROM public.transaction_splits ts
WHERE (ts.side = 'debit'::public.transaction_split_side)
GROUP BY ts.account;
CREATE OR REPLACE VIEW brmbar_privileged.credit_balances AS
SELECT
ts.account AS credit_account,
SUM(ts.amount) AS credit_sum
FROM public.transaction_splits ts
WHERE (ts.side = 'credit'::public.transaction_split_side)
GROUP BY ts.account;
/*
CASE
WHEN (ts.side = 'credit'::public.transaction_split_side) THEN (- ts.amount)
ELSE ts.amount
END AS amount,
a.currency,
ts.memo
FROM (public.transaction_splits ts
LEFT JOIN public.accounts a ON ((a.id = ts.account)))
ORDER BY ts.id;
*/
CREATE OR REPLACE FUNCTION public.inventory_balance()
RETURNS DECIMAL(12,2)
VOLATILE NOT LEAKPROOF LANGUAGE plpgsql SECURITY DEFINER AS $fn$
DECLARE
rv DECIMAL(12,2);
BEGIN
WITH inventory_balances AS (
SELECT COALESCE(credit_sum, 0) * public.find_buy_rate(a.currency, 1) as credit_sum,
COALESCE(debit_sum, 0) * public.find_buy_rate(a.currency, 1) as debit_sum,
COALESCE(credit_account, debit_account) as cd_account
FROM brmbar_privileged.credit_balances cb
FULL OUTER JOIN brmbar_privileged.debit_balances db
ON (debit_account = credit_account)
LEFT JOIN public.accounts a
ON (a.id = COALESCE(credit_account, debit_account))
WHERE a.acctype = 'inventory'::public.account_type
)
SELECT SUM(debit_sum) - SUM(credit_sum) INTO rv
FROM inventory_balances;
RETURN rv;
END;
$fn$;
PERFORM brmbar_privileged.upgrade_schema_version_to(23);
END IF;
END;
$upgrade_block$;
-- vim: set ft=plsql :

View file

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

View file

@ -0,0 +1,111 @@
--
-- 0025-load-account.sql
--
-- #25 - stored procedures for account loading
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- TMA <tma+hs@jikos.cz>
--
-- Permission to use, copy, modify, and/or distribute this software
-- for any purpose with or without fee is hereby granted, provided
-- that the above copyright notice and this permission notice appear
-- in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--
-- To require fully-qualified names
SELECT pg_catalog.set_config('search_path', '', false);
DO $upgrade_block$
DECLARE
v INTEGER;
BEGIN
IF brmbar_privileged.has_exact_schema_version(24) THEN
SELECT COUNT(1) INTO v
FROM pg_catalog.pg_type typ
INNER JOIN pg_catalog.pg_namespace nsp
ON nsp.oid = typ.typnamespace
WHERE nsp.nspname = 'brmbar_privileged'
AND typ.typname='account_class_initialization_data_type';
IF v>0 THEN
RAISE NOTICE 'Changing type account_class_initialization_data_type';
DROP TYPE brmbar_privileged.account_class_initialization_data_type CASCADE;
ELSE
RAISE NOTICE 'Creating type account_class_initialization_data_type';
END IF;
SELECT COUNT(1) INTO v
FROM pg_catalog.pg_type typ
INNER JOIN pg_catalog.pg_namespace nsp
ON nsp.oid = typ.typnamespace
WHERE nsp.nspname = 'brmbar_privileged'
AND typ.typname='account_load_type';
IF v>0 THEN
RAISE NOTICE 'Changing type account_load_type';
DROP TYPE brmbar_privileged.account_load_type CASCADE;
ELSE
RAISE NOTICE 'Creating type account_load_type';
END IF;
CREATE TYPE brmbar_privileged.account_load_type
AS ENUM ('by_id', 'by_barcode');
CREATE TYPE brmbar_privileged.account_class_initialization_data_type
AS (
account_id INTEGER, --public.accounts.id%TYPE,
account_name TEXT, --public.accounts.id%TYPE,
account_acctype public.account_type,
currency_id INTEGER, --public.currencies.id%TYPE,
currency_name TEXT
);
CREATE OR REPLACE FUNCTION public.account_class_initialization_data(
load_by brmbar_privileged.account_load_type,
i_id INTEGER,
i_barcode TEXT)
RETURNS brmbar_privileged.account_class_initialization_data_type
LANGUAGE plpgsql
AS
$$
DECLARE
rv brmbar_privileged.account_class_initialization_data_type;
BEGIN
IF load_by = 'by_id' THEN
SELECT a.id, a.name, a.acctype, c.id, c.name
INTO STRICT rv.account_id, rv.account_name, rv.account_acctype, rv.currency_id, rv.currency_name
FROM public.accounts a JOIN public.currencies c ON a.currency = c.id
WHERE a.id = i_id;
ELSE -- by_barcode
SELECT a.id, a.name, a.acctype, c.id, c.name
INTO STRICT rv.account_id, rv.account_name, rv.account_acctype, rv.currency_id, rv.currency_name
FROM public.accounts a JOIN public.currencies c ON a.currency = c.id JOIN public.barcodes b ON b.account = a.id
WHERE b.barcode = i_barcode;
END IF;
--
RETURN rv;
END;
$$;
PERFORM brmbar_privileged.upgrade_schema_version_to(25);
END IF;
END;
$upgrade_block$;
-- vim: set ft=plsql :

View file

@ -0,0 +1,86 @@
--
-- 0026-trading-accounts-2.sql
--
-- #26 - maintain trading accounts for each currency
--
-- ISC License
--
-- Copyright 2023-2025 Brmlab, z.s.
-- TMA <tma+hs@jikos.cz>
--
-- Permission to use, copy, modify, and/or distribute this software
-- for any purpose with or without fee is hereby granted, provided
-- that the above copyright notice and this permission notice appear
-- in all copies.
--
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
-- WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
-- WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
-- AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
-- CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
-- OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-- NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
-- CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
--
-- To require fully-qualified names
SELECT pg_catalog.set_config('search_path', '', false);
DO $upgrade_block$
DECLARE
v INTEGER;
BEGIN
IF brmbar_privileged.has_exact_schema_version(25) THEN
-- enforce uniqueness of trading accounts
CREATE UNIQUE INDEX uniq_trading_currency
ON public.accounts (currency)
WHERE acctype = 'trading';
-- create trading accounts for existing currencies
DO $$
DECLARE
v_rec RECORD;
v_trading INTEGER;
BEGIN
-- Loop through all currencies
FOR v_rec IN SELECT id, name FROM public.currencies LOOP
SELECT public.create_account(
'Trading account: ' || v_rec.name,
v_rec.id,
'trading'
) INTO v_trading;
END LOOP;
END $$;
CREATE OR REPLACE FUNCTION public.create_currency(
IN i_name public.currencies.name%TYPE
) RETURNS INTEGER LANGUAGE plpgsql AS $$
DECLARE
r_id INTEGER;
v_trading INTEGER;
BEGIN
-- First the currency
INSERT INTO public.currencies (name)
VALUES (i_name) RETURNING id INTO r_id;
-- Then the 'trading' account
SELECT public.create_account(
'Trading account: ' || i_name,
r_id,
'trading'
) INTO v_trading;
RETURN r_id;
END
$$;
PERFORM brmbar_privileged.upgrade_schema_version_to(26);
END IF;
END;
$upgrade_block$;
-- vim: set ft=plsql sw=4 ts=4 et :

View file

@ -0,0 +1,73 @@
#!/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()

6
brmbar3/uklid-refill.sh Normal file
View file

@ -0,0 +1,6 @@
#!/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

18
brmbar3/uklid-watchdog.sh Normal file
View file

@ -0,0 +1,18 @@
#!/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