forked from brmlab/brmbar-github
Use new shop init interface and black all brmbar/*.py.
This commit is contained in:
parent
a8e4b3216d
commit
dde8bd764d
4 changed files with 253 additions and 268 deletions
|
@ -1,12 +1,15 @@
|
||||||
from .Currency import Currency
|
from .Currency import Currency
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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
|
||||||
|
@ -17,48 +20,38 @@ class Account:
|
||||||
@classmethod
|
@classmethod
|
||||||
def load_by_barcode(cls, db, barcode):
|
def load_by_barcode(cls, db, barcode):
|
||||||
logger.debug("load_by_barcode: '%s'", barcode)
|
logger.debug("load_by_barcode: '%s'", barcode)
|
||||||
res = db.execute_and_fetch("SELECT account FROM barcodes WHERE barcode = %s", [barcode])
|
res = db.execute_and_fetch(
|
||||||
|
"SELECT account FROM barcodes WHERE barcode = %s", [barcode]
|
||||||
|
)
|
||||||
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):
|
||||||
""" Constructor for existing account """
|
"""Constructor for existing account"""
|
||||||
if id is not None:
|
if id is None:
|
||||||
name = db.execute_and_fetch("SELECT name FROM accounts WHERE id = %s", [id])
|
raise NameError("Account.load(): Specify id")
|
||||||
name = name[0]
|
name, currid, acctype = db.execute_and_fetch(
|
||||||
elif name is not None:
|
"SELECT name, currency, acctype FROM accounts WHERE id = %s", [id]
|
||||||
id = db.execute_and_fetch("SELECT id FROM accounts WHERE name = %s", [name])
|
)
|
||||||
id = id[0]
|
currency = Currency.load(db, id=currid)
|
||||||
else:
|
return cls(db, name=name, id=id, currency=currency, acctype=acctype)
|
||||||
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 = db.execute_and_fetch("SELECT public.create_account(%s, %s, %s)", [name, currency.id, acctype])
|
"SELECT public.create_account(%s, %s, %s)", [name, currency.id, acctype]
|
||||||
# id = id[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):
|
||||||
bal = self.db.execute_and_fetch(
|
bal = self.db.execute_and_fetch(
|
||||||
"SELECT public.compute_account_balance(%s)",
|
"SELECT public.compute_account_balance(%s)", [self.id]
|
||||||
[self.id]
|
|
||||||
)[0]
|
)[0]
|
||||||
return bal
|
return bal
|
||||||
#debit = self.db.execute_and_fetch("SELECT SUM(amount) FROM transaction_splits WHERE account = %s AND side = %s", [self.id, 'debit'])
|
|
||||||
#debit = debit[0] or 0
|
|
||||||
#credit = self.db.execute_and_fetch("SELECT SUM(amount) FROM transaction_splits WHERE account = %s AND side = %s", [self.id, 'credit'])
|
|
||||||
#credit = credit[0] or 0
|
|
||||||
#return debit - credit
|
|
||||||
|
|
||||||
def balance_str(self):
|
def balance_str(self):
|
||||||
return self.currency.str(self.balance())
|
return self.currency.str(self.balance())
|
||||||
|
@ -66,22 +59,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(
|
||||||
self.db.execute("SELECT public.add_barcode_to_account(%s, %s)", [self.id, barcode])
|
"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.db.execute("SELECT public.rename_account(%s, %s)", [self.id, name])
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
# vim: set fileencoding=utf8
|
# vim: set fileencoding=utf8
|
||||||
|
|
||||||
|
|
||||||
class Currency:
|
class Currency:
|
||||||
""" Currency
|
"""Currency
|
||||||
|
|
||||||
Each account has a currency (1 Kč, 1 Club Maté, ...), pairs of
|
Each account has a currency (1 Kč, 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
|
||||||
|
@ -12,72 +14,103 @@ class Currency:
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def default(cls, db):
|
def default(cls, db):
|
||||||
""" Default wallet currency """
|
"""Default wallet currency"""
|
||||||
return cls.load(db, name = "Kč")
|
return cls.load(db, name="Kč")
|
||||||
|
|
||||||
@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:
|
||||||
name = db.execute_and_fetch("SELECT name FROM currencies WHERE id = %s", [id])
|
raise NameError("Currency.load(): Specify id")
|
||||||
name = name[0]
|
name = db.execute_and_fetch("SELECT name FROM currencies WHERE id = %s", [id])
|
||||||
elif name is not None:
|
name = name[0]
|
||||||
id = db.execute_and_fetch("SELECT id FROM currencies WHERE name = %s", [name])
|
return cls(db, id=id, name=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 = 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
|
# buy rate
|
||||||
res = self.db.execute_and_fetch("SELECT public.find_buy_rate(%s, %s)",[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("Something fishy in find_buy_rate.");
|
raise NameError("Something fishy in find_buy_rate.")
|
||||||
buy = res[0]
|
buy = res[0]
|
||||||
if buy < 0:
|
if buy < 0:
|
||||||
raise NameError("Currency.rate(): Unknown conversion " + other.name() + " to " + self.name())
|
raise NameError(
|
||||||
|
"Currency.rate(): Unknown conversion "
|
||||||
|
+ other.name()
|
||||||
|
+ " to "
|
||||||
|
+ self.name()
|
||||||
|
)
|
||||||
# sell rate
|
# sell rate
|
||||||
res = self.db.execute_and_fetch("SELECT public.find_sell_rate(%s, %s)",[self.id, other.id])
|
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("Something fishy in find_sell_rate.");
|
raise NameError("Something fishy in find_sell_rate.")
|
||||||
sell = res[0]
|
sell = res[0]
|
||||||
if sell < 0:
|
if sell < 0:
|
||||||
raise NameError("Currency.rate(): Unknown conversion " + self.name() + " to " + other.name())
|
raise NameError(
|
||||||
|
"Currency.rate(): Unknown conversion "
|
||||||
|
+ self.name()
|
||||||
|
+ " to "
|
||||||
|
+ other.name()
|
||||||
|
)
|
||||||
|
|
||||||
return (buy, sell)
|
return (buy, sell)
|
||||||
|
|
||||||
def rates2(self, other):
|
def rates2(self, other):
|
||||||
# the original code for compare testing
|
# the original code for compare testing
|
||||||
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 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],
|
||||||
|
)
|
||||||
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
|
||||||
|
|
||||||
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])
|
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],
|
||||||
|
)
|
||||||
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):
|
||||||
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])
|
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:
|
if res is None:
|
||||||
raise NameError("Currency.convert(): Unknown conversion " + self.name() + " to " + target.name())
|
raise NameError(
|
||||||
|
"Currency.convert(): Unknown conversion "
|
||||||
|
+ self.name()
|
||||||
|
+ " to "
|
||||||
|
+ target.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
|
||||||
|
@ -89,10 +122,13 @@ class Currency:
|
||||||
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(
|
||||||
self.db.execute("SELECT public.update_currency_sell_rate(%s, %s, %s)",
|
"SELECT public.update_currency_sell_rate(%s, %s, %s)",
|
||||||
[self.id, target.id, rate])
|
[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(
|
||||||
self.db.execute("SELECT public.update_currency_buy_rate(%s, %s, %s)",
|
"SELECT public.update_currency_buy_rate(%s, %s, %s)",
|
||||||
[source.id, self.id, rate])
|
[source.id, self.id, rate],
|
||||||
|
)
|
||||||
|
|
|
@ -4,26 +4,29 @@ import psycopg2
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
import time
|
import time
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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
|
||||||
|
|
||||||
def execute(self, query, attrs = None):
|
def execute(self, query, attrs=None):
|
||||||
"""execute a query and return one result"""
|
"""execute a query and return one result"""
|
||||||
with closing(self.db_conn.cursor()) as cur:
|
with closing(self.db_conn.cursor()) as cur:
|
||||||
cur = self._execute(cur, query, attrs)
|
cur = self._execute(cur, query, attrs)
|
||||||
|
|
||||||
def execute_and_fetch(self, query, attrs = None):
|
def execute_and_fetch(self, query, attrs=None):
|
||||||
"""execute a query and return one result"""
|
"""execute a query and return one result"""
|
||||||
with closing(self.db_conn.cursor()) as cur:
|
with closing(self.db_conn.cursor()) as cur:
|
||||||
cur = self._execute(cur, query, attrs)
|
cur = self._execute(cur, query, attrs)
|
||||||
return cur.fetchone()
|
return cur.fetchone()
|
||||||
|
|
||||||
def execute_and_fetchall(self, query, attrs = None):
|
def execute_and_fetchall(self, query, attrs=None):
|
||||||
"""execute a query and return all results"""
|
"""execute a query and return all results"""
|
||||||
with closing(self.db_conn.cursor()) as cur:
|
with closing(self.db_conn.cursor()) as cur:
|
||||||
cur = self._execute(cur, query, attrs)
|
cur = self._execute(cur, query, attrs)
|
||||||
|
@ -38,25 +41,29 @@ class Database:
|
||||||
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 (
|
||||||
logger.debug("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:
|
||||||
logger.debug("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
|
)
|
||||||
time.sleep(2 ** level)
|
# TODO: emit message "db conn failed, reconnecting
|
||||||
|
time.sleep(2**level)
|
||||||
try:
|
try:
|
||||||
self.db_conn = psycopg2.connect(self.dsn)
|
self.db_conn = psycopg2.connect(self.dsn)
|
||||||
except psycopg2.OperationalError:
|
except psycopg2.OperationalError:
|
||||||
#TODO: emit message "psql not running to interface
|
# TODO: emit message "psql not running to interface
|
||||||
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:
|
except Exception as ex:
|
||||||
logger.debug("_execute exception: %s", ex)
|
logger.debug("_execute exception: %s", ex)
|
||||||
self.db_conn.rollback()
|
self.db_conn.rollback()
|
||||||
|
|
|
@ -2,161 +2,163 @@ import brmbar
|
||||||
from .Currency import Currency
|
from .Currency import Currency
|
||||||
from .Account import Account
|
from .Account import Account
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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=profits_id,
|
||||||
|
cash=cash_id,
|
||||||
|
excess=excess_id,
|
||||||
|
deficit=deficit_id,
|
||||||
|
)
|
||||||
|
|
||||||
def sell(self, item, user, amount = 1):
|
def sell(self, item, user, amount=1):
|
||||||
# Call the stored procedure for the sale
|
# Call the stored procedure for the sale
|
||||||
logger.debug("sell: item.id=%s amount=%s user.id=%s self.currency.id=%s",
|
logger.debug(
|
||||||
item.id, amount, user.id, self.currency.id)
|
"sell: item.id=%s amount=%s user.id=%s self.currency.id=%s",
|
||||||
|
item.id,
|
||||||
|
amount,
|
||||||
|
user.id,
|
||||||
|
self.currency.id,
|
||||||
|
)
|
||||||
res = self.db.execute_and_fetch(
|
res = self.db.execute_and_fetch(
|
||||||
"SELECT public.sell_item(%s, %s, %s, %s, %s)",
|
"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)]
|
item.id,
|
||||||
)#[0]
|
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])
|
logger.debug("sell: res[0]=%s", res[0])
|
||||||
cost = res[0]
|
cost = res[0]
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
return cost
|
return cost
|
||||||
# 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))
|
def sell_for_cash(self, item, amount=1):
|
||||||
#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 sell_for_cash(self, item, amount = 1):
|
|
||||||
cost = self.db.execute_and_fetch(
|
cost = self.db.execute_and_fetch(
|
||||||
"SELECT public.sell_item_for_cash(%s, %s, %s, %s, %s)",
|
"SELECT public.sell_item_for_cash(%s, %s, %s, %s, %s)",
|
||||||
[item.id, amount, user.id, self.currency.id,
|
[
|
||||||
"BrmBar sale of {0}x {1} for cash".format(amount, item.name)]
|
item.id,
|
||||||
)[0]#[0]
|
amount,
|
||||||
|
user.id,
|
||||||
|
self.currency.id,
|
||||||
|
"BrmBar sale of {0}x {1} for cash".format(amount, item.name),
|
||||||
|
],
|
||||||
|
)[0]
|
||||||
|
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
return cost
|
return cost
|
||||||
## 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))
|
def undo_sale(self, item, user, amount=1):
|
||||||
#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
|
|
||||||
|
|
||||||
def undo_sale(self, item, user, amount = 1):
|
|
||||||
# Undo sale; rarely needed
|
|
||||||
#(buy, sell) = item.currency.rates(self.currency)
|
|
||||||
#cost = amount * sell
|
|
||||||
#profit = amount * (sell - buy)
|
|
||||||
|
|
||||||
#transaction = self._transaction(responsible = user, description = "BrmBar sale UNDO of {}x {} to {}".format(amount, item.name, user.name))
|
|
||||||
#item.debit(transaction, amount, user.name + " (sale undo)")
|
|
||||||
#user.credit(transaction, cost, item.name + " (sale undo)")
|
|
||||||
#self.profits.credit(transaction, profit, "Margin repaid on " + item.name)
|
|
||||||
# Call the stored procedure for undoing a sale
|
# Call the stored procedure for undoing a sale
|
||||||
cost = self.db.execute_and_fetch(
|
cost = self.db.execute_and_fetch(
|
||||||
"SELECT public.undo_sale_of_item(%s, %s, %s, %s)",
|
"SELECT public.undo_sale_of_item(%s, %s, %s, %s)",
|
||||||
[item.id, amount, user.id, user.currency.id, "BrmBar sale UNDO of {0}x {1} to {2}".format(amount, item.name, user.name)]
|
[
|
||||||
)[0]#[0]
|
item.id,
|
||||||
|
amount,
|
||||||
|
user.id,
|
||||||
|
user.currency.id,
|
||||||
|
"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):
|
||||||
self.db.execute_and_fetch(
|
self.db.execute_and_fetch(
|
||||||
"SELECT public.add_credit(%s, %s, %s, %s)",
|
"SELECT public.add_credit(%s, %s, %s, %s)",
|
||||||
[self.cash.id, credit, user.id, user.name]
|
[self.cash.id, credit, user.id, user.name],
|
||||||
)
|
)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
|
|
||||||
#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):
|
def withdraw_credit(self, credit, user):
|
||||||
self.db.execute_and_fetch(
|
self.db.execute_and_fetch(
|
||||||
"SELECT public.withdraw_credit(%s, %s, %s, %s)",
|
"SELECT public.withdraw_credit(%s, %s, %s, %s)",
|
||||||
[self.cash.id, credit, user.id, user.name]
|
[self.cash.id, credit, user.id, user.name],
|
||||||
)
|
)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
#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 transfer_credit(self, userfrom, userto, amount):
|
def transfer_credit(self, userfrom, userto, amount):
|
||||||
self.db.execute_and_fetch(
|
self.db.execute_and_fetch(
|
||||||
"SELECT public.transfer_credit(%s, %s, %s, %s, %s, %s)",
|
"SELECT public.transfer_credit(%s, %s, %s, %s, %s, %s)",
|
||||||
[self.cash.id, amount, userfrom.id, userfrom.name, userto.id, userto.name]
|
[self.cash.id, amount, userfrom.id, userfrom.name, userto.id, userto.name],
|
||||||
)
|
)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
#self.add_credit(amount, userto)
|
|
||||||
#self.withdraw_credit(amount, userfrom)
|
|
||||||
|
|
||||||
def buy_for_cash(self, item, amount = 1):
|
def buy_for_cash(self, item, amount=1):
|
||||||
iamount = int(amount)
|
iamount = int(amount)
|
||||||
famount = float(iamount)
|
famount = float(iamount)
|
||||||
assert famount == amount, "amount is not integer value %s".format(amount)
|
assert famount == amount, "amount is not integer value %s".format(amount)
|
||||||
cost = self.db.execute_and_fetch(
|
cost = self.db.execute_and_fetch(
|
||||||
"SELECT public.buy_for_cash(%s, %s, %s, %s, %s)",
|
"SELECT public.buy_for_cash(%s, %s, %s, %s, %s)",
|
||||||
[self.cash.id, item.id, iamount, self.currency.id, item.name]
|
[self.cash.id, item.id, iamount, self.currency.id, item.name],
|
||||||
)[0]
|
)[0]
|
||||||
# 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()
|
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.profits.credit(transaction, credit, user.name)
|
|
||||||
#user.credit(transaction, credit, "Credit from receipt: " + description)
|
|
||||||
self.db.execute_and_fetch(
|
self.db.execute_and_fetch(
|
||||||
"SELECT public.receipt_reimbursement(%s, %s, %s, %s, %s)",
|
"SELECT public.receipt_reimbursement(%s, %s, %s, %s, %s)",
|
||||||
[self.profits.id, user.id, user.name, credit, description]
|
[self.profits.id, user.id, user.name, credit, description],
|
||||||
)[0]
|
)[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
|
||||||
|
|
||||||
|
@ -169,24 +171,31 @@ class Shop:
|
||||||
WHERE a.acctype = %s AND ts.side = %s
|
WHERE a.acctype = %s AND ts.side = %s
|
||||||
"""
|
"""
|
||||||
if overflow is not None:
|
if overflow is not None:
|
||||||
sumselect += ' AND a.name ' + ('NOT ' if overflow == 'exclude' else '') + ' LIKE \'%%-overflow\''
|
sumselect += (
|
||||||
cur = self.db.execute_and_fetch(sumselect, ["debt", 'debit'])
|
" 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, overflow=None):
|
def credit_negbalance_str(self, overflow=None):
|
||||||
return self.currency.str(-self.credit_balance(overflow=overflow))
|
return self.currency.str(-self.credit_balance(overflow=overflow))
|
||||||
|
|
||||||
# XXX causing extra heavy delay ( thousands of extra SQL queries ), disabled
|
# XXX causing extra heavy delay ( thousands of extra SQL queries ), disabled
|
||||||
def inventory_balance(self):
|
def inventory_balance(self):
|
||||||
balance = 0
|
balance = 0
|
||||||
# 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 = self.db.execute_and_fetchall("SELECT id FROM accounts WHERE acctype = %s", ["inventory"])
|
cur = self.db.execute_and_fetchall(
|
||||||
|
"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
|
||||||
|
@ -197,7 +206,7 @@ class Shop:
|
||||||
balance += b
|
balance += b
|
||||||
return balance
|
return balance
|
||||||
|
|
||||||
# XXX bypass hack
|
# XXX bypass hack
|
||||||
def inventory_balance_str(self):
|
def inventory_balance_str(self):
|
||||||
# return self.currency.str(self.inventory_balance())
|
# return self.currency.str(self.inventory_balance())
|
||||||
return "XXX"
|
return "XXX"
|
||||||
|
@ -205,102 +214,52 @@ class Shop:
|
||||||
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(
|
||||||
#FIXME: sanitize input like_str ^
|
"SELECT id FROM accounts WHERE acctype = %s AND name ILIKE %s ORDER BY name ASC",
|
||||||
|
[acctype, like_str],
|
||||||
|
)
|
||||||
|
# FIXME: sanitize input like_str ^
|
||||||
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
|
||||||
|
|
||||||
def fix_inventory(self, item, amount):
|
def fix_inventory(self, item, amount):
|
||||||
rv = self.db.execute_and_fetch(
|
rv = self.db.execute_and_fetch(
|
||||||
"SELECT public.fix_inventory(%s, %s, %s, %s, %s, %s)",
|
"SELECT public.fix_inventory(%s, %s, %s, %s, %s, %s)",
|
||||||
[item.id, item.currency.id, self.excess.id, self.deficit.id, self.currency.id, amount]
|
[
|
||||||
|
item.id,
|
||||||
|
item.currency.id,
|
||||||
|
self.excess.id,
|
||||||
|
self.deficit.id,
|
||||||
|
self.currency.id,
|
||||||
|
amount,
|
||||||
|
],
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
return rv
|
return rv
|
||||||
#amount_in_reality = amount
|
|
||||||
#amount_in_system = item.balance()
|
|
||||||
#(buy, sell) = item.currency.rates(self.currency)
|
|
||||||
|
|
||||||
#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()
|
|
||||||
# return True
|
|
||||||
#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):
|
||||||
rv = self.db.execute_and_fetch(
|
rv = self.db.execute_and_fetch(
|
||||||
"SELECT public.fix_cash(%s, %s, %s, %s)",
|
"SELECT public.fix_cash(%s, %s, %s, %s)",
|
||||||
[self.excess.id, self.deficit.id, self.currency.id, amount]
|
[self.excess.id, self.deficit.id, self.currency.id, amount],
|
||||||
)[0]
|
)[0]
|
||||||
|
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
return rv
|
return rv
|
||||||
#amount_in_reality = amount
|
|
||||||
#amount_in_system = self.cash.balance()
|
|
||||||
|
|
||||||
#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()
|
|
||||||
# return True
|
|
||||||
#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):
|
||||||
msg = self.db.execute_and_fetch(
|
msg = self.db.execute_and_fetch(
|
||||||
"SELECT public.make_consolidate_transaction(%s, %s, %s)",
|
"SELECT public.make_consolidate_transaction(%s, %s, %s)",
|
||||||
[self.excess.id, self.deficit.id, self.profits.id]
|
[self.excess.id, self.deficit.id, self.profits.id],
|
||||||
)[0]
|
)[0]
|
||||||
#transaction = self._transaction(description = "BrmBar inventory consolidation")
|
|
||||||
#excess_balance = self.excess.balance()
|
|
||||||
#if excess_balance != 0:
|
|
||||||
# print("Excess balance {} debited to profit".format(-excess_balance))
|
|
||||||
# self.excess.debit(transaction, -excess_balance, "Excess balance added to profit.")
|
|
||||||
# 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.")
|
|
||||||
if msg != None:
|
if msg != None:
|
||||||
print(msg)
|
print(msg)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
|
|
||||||
def undo(self, oldtid):
|
def undo(self, oldtid):
|
||||||
#description = self.db.execute_and_fetch("SELECT description FROM transactions WHERE id = %s", [oldtid])[0]
|
transaction = self.db.execute_and_fetch(
|
||||||
#description = 'undo %d (%s)' % (oldtid, description)
|
"SELECT public.undo_transaction(%s)", [oldtid]
|
||||||
|
)[0]
|
||||||
#transaction = self._transaction(description=description)
|
|
||||||
#for split in self.db.execute_and_fetchall("SELECT id, side, account, amount, memo FROM transaction_splits WHERE transaction = %s", [oldtid]):
|
|
||||||
# splitid, side, account, amount, memo = split
|
|
||||||
# memo = 'undo %d (%s)' % (splitid, memo)
|
|
||||||
# amount = -amount
|
|
||||||
# self.db.execute("INSERT INTO transaction_splits (transaction, side, account, amount, memo) VALUES (%s, %s, %s, %s, %s)", [transaction, side, account, amount, memo])
|
|
||||||
transaction = self.db.execute_and_fetch("SELECT public.undo_transaction(%s)",[oldtid])[0]
|
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
return transaction
|
return transaction
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue