From 3a81c9fec25bf64a11e3ca2773f4f32c50284ce5 Mon Sep 17 00:00:00 2001 From: Petr Baudis Date: Wed, 5 Sep 2012 01:31:05 +0200 Subject: [PATCH] Rewrite brmbar v3 in Python because Perl has lousy Qt bindings Complete 1:1 reimplementation of existing Perl codebase. --- brmbar3/.gitignore | 1 + brmbar3/brmbar-cli.py | 62 +++++++++++++++++++++++++++++ brmbar3/brmbar/Account.py | 80 ++++++++++++++++++++++++++++++++++++++ brmbar3/brmbar/Currency.py | 77 ++++++++++++++++++++++++++++++++++++ brmbar3/brmbar/Shop.py | 50 ++++++++++++++++++++++++ brmbar3/brmbar/__init__.py | 3 ++ 6 files changed, 273 insertions(+) create mode 100644 brmbar3/.gitignore create mode 100755 brmbar3/brmbar-cli.py create mode 100644 brmbar3/brmbar/Account.py create mode 100644 brmbar3/brmbar/Currency.py create mode 100644 brmbar3/brmbar/Shop.py create mode 100644 brmbar3/brmbar/__init__.py diff --git a/brmbar3/.gitignore b/brmbar3/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/brmbar3/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/brmbar3/brmbar-cli.py b/brmbar3/brmbar-cli.py new file mode 100755 index 0000000..cb6cb20 --- /dev/null +++ b/brmbar3/brmbar-cli.py @@ -0,0 +1,62 @@ +#!/usr/bin/python3 + +import sys +import psycopg2 + +import brmbar + +db = psycopg2.connect("dbname=brmbar") +shop = brmbar.Shop.new_with_defaults(db) +currency = shop.currency + +active_inv_item = None +active_credit = None + +for line in sys.stdin: + barcode = line.rstrip() + + # TODO $neco + if barcode[0] == "$": + credits = {'$02': 20, '$05': 50, '$10': 100, '$20': 200, '$50': 500, '$1k': 1000} + credit = credits[barcode] + if credit is None: + print("Unknown barcode: " + barcode) + continue + print("CREDIT " + str(credit)) + active_inv_item = None + active_credit = credit + continue; + + if barcode == "SCR": + print("SHOW CREDIT") + active_inv_item = None + active_credit = None + continue + + acct = brmbar.Account.load_by_barcode(db, barcode) + if acct is None: + print("Unknown barcode: " + barcode) + continue + + if acct.acctype == 'debt': + if active_inv_item is not None: + cost = shop.sell(item = active_inv_item, user = acct) + print("{} has bought {} for {} and now has {} balance".format(acct.name, active_inv_item.name, currency.str(cost), acct.negbalance_str())) + elif active_credit is not None: + shop.add_credit(credit = active_credit, user = acct) + print("{} has added {} credit and now has {} balance".format(acct.name, currency.str(active_credit), acct.negbalance_str())) + else: + print("{} has {} balance".format(acct.name, acct.negbalance_str())) + active_inv_item = None + active_credit = None + + elif acct.acctype == 'inventory': + buy, sell = acct.currency.rates(currency) + print("{} costs {} with {} in stock".format(acct.name, currency.str(sell), int(acct.balance()))) + active_inv_item = acct + active_credit = None + + else: + print("invalid account type {}".format(acct.acctype)) + active_inv_item = None + active_credit = None diff --git a/brmbar3/brmbar/Account.py b/brmbar3/brmbar/Account.py new file mode 100644 index 0000000..005708e --- /dev/null +++ b/brmbar3/brmbar/Account.py @@ -0,0 +1,80 @@ +from .Currency import Currency + +import psycopg2 +from contextlib import closing + +class Account: + """ BrmBar Account + + Both users and items are accounts. So is the money box, etc. + Each account has a currency.""" + def __init__(self, db, id, name, currency, acctype): + self.db = db + self.id = id + self.name = name + self.currency = currency + self.acctype = acctype + + @classmethod + def load_by_barcode(cls, db, barcode): + with closing(db.cursor()) as cur: + cur.execute("SELECT account FROM barcodes WHERE barcode = %s", [barcode]) + res = cur.fetchone() + if res is None: + return None + id = res[0] + return cls.load(db, id = id) + + @classmethod + def load(cls, db, id = None, name = None): + """ Constructor for existing account """ + if id is not None: + with closing(db.cursor()) as cur: + cur.execute("SELECT name FROM accounts WHERE id = %s", [id]) + name = cur.fetchone()[0] + elif name is not None: + with closing(db.cursor()) as cur: + cur.execute("SELECT id FROM accounts WHERE name = %s", [name]) + id = cur.fetchone()[0] + else: + raise NameError("Account.load(): Specify either id or name") + + with closing(db.cursor()) as cur: + cur.execute("SELECT currency, acctype FROM accounts WHERE id = %s", [id]) + currid, acctype = cur.fetchone() + currency = Currency.load(db, id = currid) + + return cls(db, name = name, id = id, currency = currency, acctype = acctype) + + @classmethod + def create(cls, db, name, currency, acctype): + """ Constructor for new account """ + with closing(db.cursor()) as cur: + cur.execute("INSERT INTO accounts (name, currency, acctype) VALUES (?, ?, ?) RETURNING id", [name, currency, acctype]) + id = cur.fetchone()[0] + return cls(db, name = name, id = id, currency = currency, acctype = acctype) + + def balance(self): + with closing(self.db.cursor()) as cur: + cur.execute("SELECT SUM(amount) FROM transaction_splits WHERE account = %s AND side = %s", [self.id, 'debit']) + debit = cur.fetchone()[0] or 0 + cur.execute("SELECT SUM(amount) FROM transaction_splits WHERE account = %s AND side = %s", [self.id, 'credit']) + credit = cur.fetchone()[0] or 0 + return debit - credit + + def balance_str(self): + return self.currency.str(self.balance()) + + def negbalance_str(self): + return self.currency.str(-self.balance()) + + def debit(self, transaction, amount, memo): + return self._transaction_split(transaction, 'debit', amount, memo) + + def credit(self, transaction, amount, memo): + return self._transaction_split(transaction, 'credit', amount, memo) + + def _transaction_split(self, transaction, side, amount, memo): + """ Common part of credit() and debit(). """ + with closing(self.db.cursor()) as cur: + cur.execute("INSERT INTO transaction_splits (transaction, side, account, amount, memo) VALUES (%s, %s, %s, %s, %s)", [transaction, side, self.id, amount, memo]) diff --git a/brmbar3/brmbar/Currency.py b/brmbar3/brmbar/Currency.py new file mode 100644 index 0000000..46a7623 --- /dev/null +++ b/brmbar3/brmbar/Currency.py @@ -0,0 +1,77 @@ +import psycopg2 +from contextlib import closing + +class Currency: + """ Currency + + Each account has a currency (1 Kč, 1 Club Maté, ...), pairs of + currencies have (asymmetric) exchange rates. """ + def __init__(self, db, id, name): + self.db = db + self.id = id + self.name = name + + @classmethod + def default(cls, db): + """ Default wallet currency """ + return cls.load(db, name = "Kč") + + @classmethod + def load(cls, db, id = None, name = None): + """ Constructor for existing currency """ + if id is not None: + with closing(db.cursor()) as cur: + cur.execute("SELECT name FROM currencies WHERE id = %s", [id]) + name = cur.fetchone()[0] + elif name is not None: + with closing(db.cursor()) as cur: + cur.execute("SELECT id FROM currencies WHERE name = %s", [name]) + id = cur.fetchone()[0] + else: + raise NameError("Currency.load(): Specify either id or name") + return cls(db, name = name, id = id) + + @classmethod + def create(cls, db, name): + """ Constructor for new currency """ + with closing(db.cursor()) as cur: + cur.execute("INSERT INTO currencies (name) VALUES (?) RETURNING id", [name]) + id = cur.fetchone()[0] + return cls.new(db, name = name, id = id) + + def rates(self, other): + """ Return tuple ($buy, $sell) of rates of $self in relation to $other (brmbar.Currency): + $buy is the price of $self in means of $other when buying it (into brmbar) + $sell is the price of $self in means of $other when selling it (from brmbar) """ + with closing(self.db.cursor()) as cur: + + cur.execute("SELECT rate, rate_dir FROM exchange_rates WHERE target = %s AND source = %s", [self.id, other.id]) + res = cur.fetchone() + if res is None: + raise NameError("Currency.rate(): Unknown conversion " + other.name() + " to " + self.name()) + buy_rate, buy_rate_dir = res + buy = buy_rate if buy_rate_dir == "target_to_source" else 1/buy_rate + + cur.execute("SELECT rate, rate_dir FROM exchange_rates WHERE target = %s AND source = %s", [other.id, self.id]) + res = cur.fetchone() + if res is None: + raise NameError("Currency.rate(): Unknown conversion " + self.name() + " to " + other.name()) + sell_rate, sell_rate_dir = res + sell = sell_rate if sell_rate_dir == "source_to_target" else 1/sell_rate + + return (buy, sell) + + def convert(self, amount, target): + with closing(self.db.cursor()) as cur: + cur.execute("SELECT rate, rate_dir FROM exchange_rates WHERE target = %s AND source = %s AND valid_since <= NOW() ORDER BY valid_since ASC LIMIT 1", [target.id, self.id]) + res = cur.fetchone() + if res is None: + raise NameError("Currency.convert(): Unknown conversion " + self.name() + " to " + other.name()) + if rate_dir == "source_to_target": + resamount = amount * rate + else: + resamount = amount / rate + return resamount + + def str(self, amount): + return str(amount) + " " + self.name diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py new file mode 100644 index 0000000..422e118 --- /dev/null +++ b/brmbar3/brmbar/Shop.py @@ -0,0 +1,50 @@ +import brmbar +from .Currency import Currency +from .Account import Account + +import psycopg2 +from contextlib import closing + +class Shop: + """ BrmBar Shop + + Business logic so that only interaction is left in the hands + of the frontend scripts. """ + def __init__(self, db, currency, profits, cash): + self.db = db + self.currency = currency # brmbar.Currency + self.profits = profits # income brmbar.Account for brmbar profit margins on items + self.cash = cash # our operational ("wallet") cash account + + @classmethod + def new_with_defaults(cls, db): + return cls(db, + currency = Currency.default(db), + profits = Account.load(db, name = "BrmBar Profits"), + cash = Account.load(db, name = "BrmBar Cash")) + + def sell(self, item, user, amount = 1): + # Sale: Currency conversion from item currency to shop currency + (buy, sell) = item.currency.rates(self.currency) + cost = amount * sell + profit = amount * (sell - buy) + + transaction = self._transaction(responsible = user, description = "BrmBar sale of {}x {} to {}".format(amount, item.name, user.name)) + item.credit(transaction, amount, user.name) + user.debit(transaction, cost, item.name) # debit (increase) on a _debt_ account + self.profits.debit(transaction, profit, "Margin on " + item.name) + self.db.commit() + + return cost + + def add_credit(self, credit, user): + transaction = self._transaction(responsible = user, description = "BrmBar credit replenishment for " + user.name) + self.cash.debit(transaction, credit, user.name) + user.credit(transaction, credit, "Credit replenishment") + self.db.commit() + + def _transaction(self, responsible, description): + with closing(self.db.cursor()) as cur: + cur.execute("INSERT INTO transactions (responsible, description) VALUES (%s, %s) RETURNING id", [responsible.id, description]) + transaction = cur.fetchone()[0] + return transaction diff --git a/brmbar3/brmbar/__init__.py b/brmbar3/brmbar/__init__.py new file mode 100644 index 0000000..0a8d0d8 --- /dev/null +++ b/brmbar3/brmbar/__init__.py @@ -0,0 +1,3 @@ +from .Account import Account +from .Currency import Currency +from .Shop import Shop