diff --git a/brmbar3/brmbar/Account.py b/brmbar3/brmbar/Account.py index e9107eb..a1de8e2 100644 --- a/brmbar3/brmbar/Account.py +++ b/brmbar3/brmbar/Account.py @@ -4,87 +4,87 @@ import psycopg2 from contextlib import closing class Account: - """ BrmBar 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 + 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_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") + @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) + 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) + 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 (%s, %s, %s) RETURNING id", [name, currency.id, acctype]) - id = cur.fetchone()[0] - 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 (%s, %s, %s) RETURNING id", [name, currency.id, 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(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 balance_str(self): + return self.currency.str(self.balance()) - def negbalance_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 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 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]) + 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]) - def add_barcode(self, barcode): - with closing(self.db.cursor()) as cur: - cur.execute("INSERT INTO barcodes (account, barcode) VALUES (%s, %s)", [self.id, barcode]) - self.db.commit() + def add_barcode(self, barcode): + with closing(self.db.cursor()) as cur: + cur.execute("INSERT INTO barcodes (account, barcode) VALUES (%s, %s)", [self.id, barcode]) + self.db.commit() - def rename(self, name): - with closing(self.db.cursor()) as cur: - cur.execute("UPDATE accounts SET name = %s WHERE id = %s", [name, self.id]) - self.name = name + def rename(self, name): + with closing(self.db.cursor()) as cur: + cur.execute("UPDATE accounts SET name = %s WHERE id = %s", [name, self.id]) + self.name = name diff --git a/brmbar3/brmbar/Currency.py b/brmbar3/brmbar/Currency.py index 8aeaf61..ba818e4 100644 --- a/brmbar3/brmbar/Currency.py +++ b/brmbar3/brmbar/Currency.py @@ -2,84 +2,84 @@ 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 + """ 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 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 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 (%s) RETURNING id", [name]) - id = cur.fetchone()[0] - 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 (%s) RETURNING id", [name]) + id = cur.fetchone()[0] + return cls(db, name = name, id = id) - def rates(self, other): - """ Return tuple ($buy, $sell) of rates of $self in relation to $other (brmbar.Currency): - $buy is the price of $self in means of $other when buying it (into brmbar) - $sell is the price of $self in means of $other when selling it (from brmbar) """ - with closing(self.db.cursor()) as cur: + 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 AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1", [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 AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1", [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 AND valid_since <= NOW() ORDER BY valid_since DESC LIMIT 1", [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 + 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() + 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) + 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 DESC LIMIT 1", [target.id, self.id]) - res = cur.fetchone() - if res is None: - raise NameError("Currency.convert(): Unknown conversion " + self.name() + " to " + other.name()) - rate, rate_dir = res - if rate_dir == "source_to_target": - resamount = amount * rate - else: - resamount = amount / rate - return resamount + 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 DESC LIMIT 1", [target.id, self.id]) + res = cur.fetchone() + if res is None: + raise NameError("Currency.convert(): Unknown conversion " + self.name() + " to " + other.name()) + rate, rate_dir = res + if rate_dir == "source_to_target": + resamount = amount * rate + else: + resamount = amount / rate + return resamount - def str(self, amount): - return "{:.2f} {}".format(amount, self.name) + def str(self, amount): + return "{:.2f} {}".format(amount, self.name) - def update_sell_rate(self, target, rate): - 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"]) - def update_buy_rate(self, source, rate): - 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"]) + def update_sell_rate(self, target, rate): + 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"]) + def update_buy_rate(self, source, rate): + 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"]) diff --git a/brmbar3/brmbar/Shop.py b/brmbar3/brmbar/Shop.py index 211d20f..d689445 100644 --- a/brmbar3/brmbar/Shop.py +++ b/brmbar3/brmbar/Shop.py @@ -6,127 +6,127 @@ import psycopg2 from contextlib import closing class Shop: - """ BrmBar 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 + 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")) + @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) + 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() + 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 + return cost - def sell_for_cash(self, item, 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) + def sell_for_cash(self, item, 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(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() + transaction = self._transaction(description = "BrmBar sale of {}x {} for cash".format(amount, item.name)) + item.credit(transaction, amount, "Cash") + self.cash.debit(transaction, cost, item.name) + self.profits.debit(transaction, profit, "Margin on " + item.name) + self.db.commit() - return cost + 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 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 withdraw_credit(self, credit, user): - transaction = self._transaction(responsible = user, description = "BrmBar credit withdrawal for " + user.name) - self.cash.credit(transaction, credit, user.name) - user.debit(transaction, credit, "Credit withdrawal") - self.db.commit() + def withdraw_credit(self, credit, user): + transaction = self._transaction(responsible = user, description = "BrmBar credit withdrawal for " + user.name) + self.cash.credit(transaction, credit, user.name) + user.debit(transaction, credit, "Credit withdrawal") + self.db.commit() - def buy_for_cash(self, item, amount = 1): - # Buy: Currency conversion from item currency to shop currency - (buy, sell) = item.currency.rates(self.currency) - cost = amount * buy + def buy_for_cash(self, item, amount = 1): + # Buy: Currency conversion from item currency to shop currency + (buy, sell) = item.currency.rates(self.currency) + cost = amount * buy - transaction = self._transaction(description = "BrmBar stock replenishment of {}x {} for cash".format(amount, item.name)) - item.debit(transaction, amount, "Cash") - self.cash.credit(transaction, cost, item.name) - self.db.commit() + transaction = self._transaction(description = "BrmBar stock replenishment of {}x {} for cash".format(amount, item.name)) + item.debit(transaction, amount, "Cash") + self.cash.credit(transaction, cost, item.name) + self.db.commit() - return cost + return cost - def receipt_to_credit(self, user, credit, description): - transaction = self._transaction(responsible = user, description = "Receipt: " + description) - self.profits.credit(transaction, credit, user.name) - user.credit(transaction, credit, "Credit from receipt: " + description) - self.db.commit() + def receipt_to_credit(self, user, credit, description): + transaction = self._transaction(responsible = user, description = "Receipt: " + description) + self.profits.credit(transaction, credit, user.name) + user.credit(transaction, credit, "Credit from receipt: " + description) + self.db.commit() - def _transaction(self, responsible = None, description = None): - with closing(self.db.cursor()) as cur: - cur.execute("INSERT INTO transactions (responsible, description) VALUES (%s, %s) RETURNING id", - [responsible.id if responsible else None, description]) - transaction = cur.fetchone()[0] - return transaction + def _transaction(self, responsible = None, description = None): + with closing(self.db.cursor()) as cur: + cur.execute("INSERT INTO transactions (responsible, description) VALUES (%s, %s) RETURNING id", + [responsible.id if responsible else None, description]) + transaction = cur.fetchone()[0] + return transaction - def credit_balance(self): - with closing(self.db.cursor()) as cur: - # We assume all debt accounts share a currency - sumselect = """ - SELECT SUM(ts.amount) - FROM accounts AS a - LEFT JOIN transaction_splits AS ts ON a.id = ts.account - WHERE a.acctype = %s AND ts.side = %s - """ - cur.execute(sumselect, ["debt", 'debit']) - debit = cur.fetchone()[0] or 0 - cur.execute(sumselect, ["debt", 'credit']) - credit = cur.fetchone()[0] or 0 - return debit - credit - def credit_negbalance_str(self): - return self.currency.str(-self.credit_balance()) + def credit_balance(self): + with closing(self.db.cursor()) as cur: + # We assume all debt accounts share a currency + sumselect = """ + SELECT SUM(ts.amount) + FROM accounts AS a + LEFT JOIN transaction_splits AS ts ON a.id = ts.account + WHERE a.acctype = %s AND ts.side = %s + """ + cur.execute(sumselect, ["debt", 'debit']) + debit = cur.fetchone()[0] or 0 + cur.execute(sumselect, ["debt", 'credit']) + credit = cur.fetchone()[0] or 0 + return debit - credit + def credit_negbalance_str(self): + return self.currency.str(-self.credit_balance()) - def inventory_balance(self): - balance = 0 - with closing(self.db.cursor()) as cur: - # Each inventory account has its own currency, - # so we just do this ugly iteration - cur.execute("SELECT id FROM accounts WHERE acctype = %s", ["inventory"]) - 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.currency.convert(inv.balance(), self.currency) - return balance - def inventory_balance_str(self): - return self.currency.str(self.inventory_balance()) + def inventory_balance(self): + balance = 0 + with closing(self.db.cursor()) as cur: + # Each inventory account has its own currency, + # so we just do this ugly iteration + cur.execute("SELECT id FROM accounts WHERE acctype = %s", ["inventory"]) + 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.currency.convert(inv.balance(), self.currency) + return balance + def inventory_balance_str(self): + return self.currency.str(self.inventory_balance()) - def account_list(self, acctype): - accts = [] - with closing(self.db.cursor()) as cur: - cur.execute("SELECT id FROM accounts WHERE acctype = %s ORDER BY name ASC", [acctype]) - for inventory in cur: - accts += [ Account.load(self.db, id = inventory[0]) ] - return accts + def account_list(self, acctype): + accts = [] + with closing(self.db.cursor()) as cur: + cur.execute("SELECT id FROM accounts WHERE acctype = %s ORDER BY name ASC", [acctype]) + for inventory in cur: + accts += [ Account.load(self.db, id = inventory[0]) ] + return accts