mirror of
https://github.com/brmlab/brmbar.git
synced 2025-06-07 21:04:00 +02:00
Rewrite brmbar v3 in Python because Perl has lousy Qt bindings
Complete 1:1 reimplementation of existing Perl codebase.
This commit is contained in:
parent
945ee705c3
commit
3a81c9fec2
6 changed files with 273 additions and 0 deletions
1
brmbar3/.gitignore
vendored
Normal file
1
brmbar3/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
__pycache__
|
62
brmbar3/brmbar-cli.py
Executable file
62
brmbar3/brmbar-cli.py
Executable file
|
@ -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
|
80
brmbar3/brmbar/Account.py
Normal file
80
brmbar3/brmbar/Account.py
Normal file
|
@ -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])
|
77
brmbar3/brmbar/Currency.py
Normal file
77
brmbar3/brmbar/Currency.py
Normal file
|
@ -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
|
50
brmbar3/brmbar/Shop.py
Normal file
50
brmbar3/brmbar/Shop.py
Normal file
|
@ -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
|
3
brmbar3/brmbar/__init__.py
Normal file
3
brmbar3/brmbar/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from .Account import Account
|
||||
from .Currency import Currency
|
||||
from .Shop import Shop
|
Loading…
Add table
Add a link
Reference in a new issue