This commit is contained in:
Václav 'Ax' Hůla 2012-10-26 21:41:23 +02:00
parent 1c050d7da5
commit ec94f1d034
3 changed files with 247 additions and 247 deletions

View file

@ -4,87 +4,87 @@ import psycopg2
from contextlib import closing from contextlib import closing
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
self.name = name self.name = name
self.currency = currency self.currency = currency
self.acctype = acctype self.acctype = acctype
@classmethod @classmethod
def load_by_barcode(cls, db, barcode): def load_by_barcode(cls, db, barcode):
with closing(db.cursor()) as cur: with closing(db.cursor()) as cur:
cur.execute("SELECT account FROM barcodes WHERE barcode = %s", [barcode]) cur.execute("SELECT account FROM barcodes WHERE barcode = %s", [barcode])
res = cur.fetchone() res = cur.fetchone()
if res is None: if res is None:
return None return None
id = res[0] id = res[0]
return cls.load(db, id = id) return cls.load(db, id = id)
@classmethod @classmethod
def load(cls, db, id = None, name = None): def load(cls, db, id = None, name = None):
""" Constructor for existing account """ """ Constructor for existing account """
if id is not None: if id is not None:
with closing(db.cursor()) as cur: with closing(db.cursor()) as cur:
cur.execute("SELECT name FROM accounts WHERE id = %s", [id]) cur.execute("SELECT name FROM accounts WHERE id = %s", [id])
name = cur.fetchone()[0] name = cur.fetchone()[0]
elif name is not None: elif name is not None:
with closing(db.cursor()) as cur: with closing(db.cursor()) as cur:
cur.execute("SELECT id FROM accounts WHERE name = %s", [name]) cur.execute("SELECT id FROM accounts WHERE name = %s", [name])
id = cur.fetchone()[0] id = cur.fetchone()[0]
else: else:
raise NameError("Account.load(): Specify either id or name") raise NameError("Account.load(): Specify either id or name")
with closing(db.cursor()) as cur: with closing(db.cursor()) as cur:
cur.execute("SELECT currency, acctype FROM accounts WHERE id = %s", [id]) cur.execute("SELECT currency, acctype FROM accounts WHERE id = %s", [id])
currid, acctype = cur.fetchone() currid, acctype = cur.fetchone()
currency = Currency.load(db, id = currid) currency = Currency.load(db, id = currid)
return cls(db, name = name, id = id, currency = currency, acctype = acctype) 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 """
with closing(db.cursor()) as cur: with closing(db.cursor()) as cur:
cur.execute("INSERT INTO accounts (name, currency, acctype) VALUES (%s, %s, %s) RETURNING id", [name, currency.id, acctype]) cur.execute("INSERT INTO accounts (name, currency, acctype) VALUES (%s, %s, %s) RETURNING id", [name, currency.id, acctype])
id = cur.fetchone()[0] id = cur.fetchone()[0]
return cls(db, name = name, id = id, currency = currency, acctype = acctype) return cls(db, name = name, id = id, currency = currency, acctype = acctype)
def balance(self): def balance(self):
with closing(self.db.cursor()) as cur: with closing(self.db.cursor()) as cur:
cur.execute("SELECT SUM(amount) FROM transaction_splits WHERE account = %s AND side = %s", [self.id, 'debit']) cur.execute("SELECT SUM(amount) FROM transaction_splits WHERE account = %s AND side = %s", [self.id, 'debit'])
debit = cur.fetchone()[0] or 0 debit = cur.fetchone()[0] or 0
cur.execute("SELECT SUM(amount) FROM transaction_splits WHERE account = %s AND side = %s", [self.id, 'credit']) cur.execute("SELECT SUM(amount) FROM transaction_splits WHERE account = %s AND side = %s", [self.id, 'credit'])
credit = cur.fetchone()[0] or 0 credit = cur.fetchone()[0] or 0
return debit - credit return debit - credit
def balance_str(self): def balance_str(self):
return self.currency.str(self.balance()) return self.currency.str(self.balance())
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): def debit(self, transaction, amount, memo):
return self._transaction_split(transaction, 'debit', amount, memo) return self._transaction_split(transaction, 'debit', amount, memo)
def credit(self, transaction, amount, memo): def credit(self, transaction, amount, memo):
return self._transaction_split(transaction, 'credit', amount, memo) return self._transaction_split(transaction, 'credit', amount, memo)
def _transaction_split(self, transaction, side, amount, memo): def _transaction_split(self, transaction, side, amount, memo):
""" Common part of credit() and debit(). """ """ Common part of credit() and debit(). """
with closing(self.db.cursor()) as cur: 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]) cur.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):
with closing(self.db.cursor()) as cur: with closing(self.db.cursor()) as cur:
cur.execute("INSERT INTO barcodes (account, barcode) VALUES (%s, %s)", [self.id, barcode]) cur.execute("INSERT INTO barcodes (account, barcode) VALUES (%s, %s)", [self.id, barcode])
self.db.commit() self.db.commit()
def rename(self, name): def rename(self, name):
with closing(self.db.cursor()) as cur: with closing(self.db.cursor()) as cur:
cur.execute("UPDATE accounts SET name = %s WHERE id = %s", [name, self.id]) cur.execute("UPDATE accounts SET name = %s WHERE id = %s", [name, self.id])
self.name = name self.name = name

View file

@ -2,84 +2,84 @@ import psycopg2
from contextlib import closing from contextlib import closing
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
self.name = name self.name = name
@classmethod @classmethod
def default(cls, db): def default(cls, db):
""" Default wallet currency """ """ Default wallet currency """
return cls.load(db, name = "") return cls.load(db, name = "")
@classmethod @classmethod
def load(cls, db, id = None, name = None): def load(cls, db, id = None, name = None):
""" Constructor for existing currency """ """ Constructor for existing currency """
if id is not None: if id is not None:
with closing(db.cursor()) as cur: with closing(db.cursor()) as cur:
cur.execute("SELECT name FROM currencies WHERE id = %s", [id]) cur.execute("SELECT name FROM currencies WHERE id = %s", [id])
name = cur.fetchone()[0] name = cur.fetchone()[0]
elif name is not None: elif name is not None:
with closing(db.cursor()) as cur: with closing(db.cursor()) as cur:
cur.execute("SELECT id FROM currencies WHERE name = %s", [name]) cur.execute("SELECT id FROM currencies WHERE name = %s", [name])
id = cur.fetchone()[0] id = cur.fetchone()[0]
else: else:
raise NameError("Currency.load(): Specify either id or name") raise NameError("Currency.load(): Specify either id or name")
return cls(db, name = name, id = id) 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 """
with closing(db.cursor()) as cur: with closing(db.cursor()) as cur:
cur.execute("INSERT INTO currencies (name) VALUES (%s) RETURNING id", [name]) cur.execute("INSERT INTO currencies (name) VALUES (%s) RETURNING id", [name])
id = cur.fetchone()[0] id = cur.fetchone()[0]
return cls(db, name = name, id = id) 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) """
with closing(self.db.cursor()) as cur: 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 DESC LIMIT 1", [self.id, other.id]) cur.execute("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 = cur.fetchone() res = cur.fetchone()
if res is None: if res is None:
raise NameError("Currency.rate(): Unknown conversion " + other.name() + " to " + self.name()) raise NameError("Currency.rate(): Unknown conversion " + other.name() + " to " + self.name())
buy_rate, buy_rate_dir = res buy_rate, buy_rate_dir = res
buy = buy_rate if buy_rate_dir == "target_to_source" else 1/buy_rate buy = buy_rate if buy_rate_dir == "target_to_source" else 1/buy_rate
cur.execute("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]) cur.execute("SELECT rate, rate_dir FROM exchange_rates WHERE target = %s AND source = %s AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1", [other.id, self.id])
res = cur.fetchone() res = cur.fetchone()
if res is None: if res is None:
raise NameError("Currency.rate(): Unknown conversion " + self.name() + " to " + other.name()) raise NameError("Currency.rate(): Unknown conversion " + self.name() + " to " + other.name())
sell_rate, sell_rate_dir = res sell_rate, sell_rate_dir = res
sell = sell_rate if sell_rate_dir == "source_to_target" else 1/sell_rate sell = sell_rate if sell_rate_dir == "source_to_target" else 1/sell_rate
return (buy, sell) return (buy, sell)
def convert(self, amount, target): def convert(self, amount, target):
with closing(self.db.cursor()) as cur: 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 DESC LIMIT 1", [target.id, self.id]) cur.execute("SELECT rate, rate_dir FROM exchange_rates WHERE target = %s AND source = %s AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1", [target.id, self.id])
res = cur.fetchone() res = cur.fetchone()
if res is None: if res is None:
raise NameError("Currency.convert(): Unknown conversion " + self.name() + " to " + other.name()) raise NameError("Currency.convert(): Unknown conversion " + self.name() + " to " + other.name())
rate, rate_dir = res rate, rate_dir = res
if rate_dir == "source_to_target": if rate_dir == "source_to_target":
resamount = amount * rate resamount = amount * rate
else: else:
resamount = amount / rate resamount = amount / rate
return resamount 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):
with closing(self.db.cursor()) as cur: with closing(self.db.cursor()) as cur:
cur.execute("INSERT INTO exchange_rates (source, target, rate, rate_dir) VALUES (%s, %s, %s, %s)", [self.id, target.id, rate, "source_to_target"]) cur.execute("INSERT INTO exchange_rates (source, target, rate, rate_dir) VALUES (%s, %s, %s, %s)", [self.id, target.id, rate, "source_to_target"])
def update_buy_rate(self, source, rate): def update_buy_rate(self, source, rate):
with closing(self.db.cursor()) as cur: with closing(self.db.cursor()) as cur:
cur.execute("INSERT INTO exchange_rates (source, target, rate, rate_dir) VALUES (%s, %s, %s, %s)", [source.id, self.id, rate, "target_to_source"]) cur.execute("INSERT INTO exchange_rates (source, target, rate, rate_dir) VALUES (%s, %s, %s, %s)", [source.id, self.id, rate, "target_to_source"])

View file

@ -6,127 +6,127 @@ import psycopg2
from contextlib import closing from contextlib import closing
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): def __init__(self, db, currency, profits, cash):
self.db = db self.db = db
self.currency = currency # brmbar.Currency self.currency = currency # brmbar.Currency
self.profits = profits # income brmbar.Account for brmbar profit margins on items self.profits = profits # income brmbar.Account for brmbar profit margins on items
self.cash = cash # our operational ("wallet") cash account self.cash = cash # our operational ("wallet") cash account
@classmethod @classmethod
def new_with_defaults(cls, db): def new_with_defaults(cls, db):
return cls(db, return cls(db,
currency = Currency.default(db), currency = Currency.default(db),
profits = Account.load(db, name = "BrmBar Profits"), profits = Account.load(db, name = "BrmBar Profits"),
cash = Account.load(db, name = "BrmBar Cash")) cash = Account.load(db, name = "BrmBar Cash"))
def sell(self, item, user, amount = 1): def sell(self, item, user, amount = 1):
# Sale: Currency conversion from item currency to shop currency # Sale: Currency conversion from item currency to shop currency
(buy, sell) = item.currency.rates(self.currency) (buy, sell) = item.currency.rates(self.currency)
cost = amount * sell cost = amount * sell
profit = amount * (sell - buy) profit = amount * (sell - buy)
transaction = self._transaction(responsible = user, description = "BrmBar sale of {}x {} to {}".format(amount, item.name, user.name)) transaction = self._transaction(responsible = user, description = "BrmBar sale of {}x {} to {}".format(amount, item.name, user.name))
item.credit(transaction, amount, user.name) item.credit(transaction, amount, user.name)
user.debit(transaction, cost, item.name) # debit (increase) on a _debt_ account user.debit(transaction, cost, item.name) # debit (increase) on a _debt_ account
self.profits.debit(transaction, profit, "Margin on " + item.name) self.profits.debit(transaction, profit, "Margin on " + item.name)
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 # Sale: Currency conversion from item currency to shop currency
(buy, sell) = item.currency.rates(self.currency) (buy, sell) = item.currency.rates(self.currency)
cost = amount * sell cost = amount * sell
profit = amount * (sell - buy) profit = amount * (sell - buy)
transaction = self._transaction(description = "BrmBar sale of {}x {} for cash".format(amount, item.name)) transaction = self._transaction(description = "BrmBar sale of {}x {} for cash".format(amount, item.name))
item.credit(transaction, amount, "Cash") item.credit(transaction, amount, "Cash")
self.cash.debit(transaction, cost, item.name) self.cash.debit(transaction, cost, item.name)
self.profits.debit(transaction, profit, "Margin on " + item.name) self.profits.debit(transaction, profit, "Margin on " + item.name)
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) transaction = self._transaction(responsible = user, description = "BrmBar credit replenishment for " + user.name)
self.cash.debit(transaction, credit, user.name) self.cash.debit(transaction, credit, user.name)
user.credit(transaction, credit, "Credit replenishment") user.credit(transaction, credit, "Credit replenishment")
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) transaction = self._transaction(responsible = user, description = "BrmBar credit withdrawal for " + user.name)
self.cash.credit(transaction, credit, user.name) self.cash.credit(transaction, credit, user.name)
user.debit(transaction, credit, "Credit withdrawal") user.debit(transaction, credit, "Credit withdrawal")
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 # Buy: Currency conversion from item currency to shop currency
(buy, sell) = item.currency.rates(self.currency) (buy, sell) = item.currency.rates(self.currency)
cost = amount * buy cost = amount * buy
transaction = self._transaction(description = "BrmBar stock replenishment of {}x {} for cash".format(amount, item.name)) transaction = self._transaction(description = "BrmBar stock replenishment of {}x {} for cash".format(amount, item.name))
item.debit(transaction, amount, "Cash") item.debit(transaction, amount, "Cash")
self.cash.credit(transaction, cost, item.name) self.cash.credit(transaction, cost, item.name)
self.db.commit() self.db.commit()
return cost return cost
def receipt_to_credit(self, user, credit, description): def receipt_to_credit(self, user, credit, description):
transaction = self._transaction(responsible = user, description = "Receipt: " + description) transaction = self._transaction(responsible = user, description = "Receipt: " + description)
self.profits.credit(transaction, credit, user.name) self.profits.credit(transaction, credit, user.name)
user.credit(transaction, credit, "Credit from receipt: " + description) user.credit(transaction, credit, "Credit from receipt: " + description)
self.db.commit() self.db.commit()
def _transaction(self, responsible = None, description = None): def _transaction(self, responsible = None, description = None):
with closing(self.db.cursor()) as cur: with closing(self.db.cursor()) as cur:
cur.execute("INSERT INTO transactions (responsible, description) VALUES (%s, %s) RETURNING id", cur.execute("INSERT INTO transactions (responsible, description) VALUES (%s, %s) RETURNING id",
[responsible.id if responsible else None, description]) [responsible.id if responsible else None, description])
transaction = cur.fetchone()[0] transaction = cur.fetchone()[0]
return transaction return transaction
def credit_balance(self): def credit_balance(self):
with closing(self.db.cursor()) as cur: with closing(self.db.cursor()) as cur:
# 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)
FROM accounts AS a FROM accounts AS a
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.execute(sumselect, ["debt", 'debit']) cur.execute(sumselect, ["debt", 'debit'])
debit = cur.fetchone()[0] or 0 debit = cur.fetchone()[0] or 0
cur.execute(sumselect, ["debt", 'credit']) cur.execute(sumselect, ["debt", 'credit'])
credit = cur.fetchone()[0] or 0 credit = cur.fetchone()[0] or 0
return debit - credit return debit - credit
def credit_negbalance_str(self): def credit_negbalance_str(self):
return self.currency.str(-self.credit_balance()) return self.currency.str(-self.credit_balance())
def inventory_balance(self): def inventory_balance(self):
balance = 0 balance = 0
with closing(self.db.cursor()) as cur: with closing(self.db.cursor()) as cur:
# Each inventory account has its own currency, # Each inventory account has its own currency,
# so we just do this ugly iteration # so we just do this ugly iteration
cur.execute("SELECT id FROM accounts WHERE acctype = %s", ["inventory"]) cur.execute("SELECT id FROM accounts WHERE acctype = %s", ["inventory"])
for inventory in cur: for inventory in cur:
invid = inventory[0] invid = inventory[0]
inv = Account.load(self.db, id = invid) inv = Account.load(self.db, id = invid)
# FIXME: This is not correct as each instance of inventory # FIXME: This is not correct as each instance of inventory
# might have been bought for a different price! Therefore, # might have been bought for a different price! Therefore,
# we need to replace the command below with a complex SQL # we need to replace the command below with a complex SQL
# statement that will... ugh, accounting is hard! # statement that will... ugh, accounting is hard!
balance += inv.currency.convert(inv.balance(), self.currency) balance += inv.currency.convert(inv.balance(), self.currency)
return balance 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): def account_list(self, acctype):
accts = [] accts = []
with closing(self.db.cursor()) as cur: with closing(self.db.cursor()) as cur:
cur.execute("SELECT id FROM accounts WHERE acctype = %s ORDER BY name ASC", [acctype]) cur.execute("SELECT id FROM accounts WHERE acctype = %s ORDER BY name ASC", [acctype])
for inventory in cur: for inventory in cur:
accts += [ Account.load(self.db, id = inventory[0]) ] accts += [ Account.load(self.db, id = inventory[0]) ]
return accts return accts