|
@@ -6,10 +6,112 @@
|
|
|
#
|
|
|
# THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
|
|
|
|
|
|
-import psycopg2
|
|
|
+try:
|
|
|
+ import psycopg2
|
|
|
+ MOCK = False
|
|
|
+except:
|
|
|
+ from faker import Faker
|
|
|
+ Faker.seed(4321)
|
|
|
+ fake = Faker()
|
|
|
+ MOCK = True
|
|
|
+ # first, import a similar Provider or use the default one
|
|
|
+ from faker.providers import BaseProvider
|
|
|
+
|
|
|
+ # create new provider class
|
|
|
+ class Products(BaseProvider):
|
|
|
+ PRODUCTS = {
|
|
|
+ 'Dark Chocolate': 'Chocolate',
|
|
|
+ 'Milk Chocolate': 'Chocolate',
|
|
|
+ 'Cooking Chocolate': 'Chocolate',
|
|
|
+ 'Cucumber': 'Veggies',
|
|
|
+ 'Bananas': 'Fruit',
|
|
|
+ 'Milk': 'Milk and Cream',
|
|
|
+ 'Cream': 'Milk and Cream',
|
|
|
+ 'Coffee Beans': 'Coffee',
|
|
|
+ 'Freerange Eggs': 'Eggs',
|
|
|
+ }
|
|
|
+ GROUPS = [
|
|
|
+ 'Fish, Meat, Eggs',
|
|
|
+ 'Dairy',
|
|
|
+ 'Beverages',
|
|
|
+ 'Treats',
|
|
|
+ 'Produce',
|
|
|
+ ]
|
|
|
+ CATEGORIES = {
|
|
|
+ 'Eggs': 'Fish, Meat, Eggs',
|
|
|
+ 'Milk and Cream': 'Dairy',
|
|
|
+ 'Chocolate': 'Treats',
|
|
|
+ 'Veggies': 'Produce',
|
|
|
+ 'Fruit': 'Produce',
|
|
|
+ 'Coffee': 'Beverages',
|
|
|
+ }
|
|
|
+ STORES = [
|
|
|
+ 'Paknsave',
|
|
|
+ 'Countdown',
|
|
|
+ 'New World',
|
|
|
+ 'Dreamview',
|
|
|
+ 'TOFS',
|
|
|
+ ]
|
|
|
+ CATEGORY_UNITS = {
|
|
|
+ 'Eggs': ('g',),
|
|
|
+ 'Milk and Cream': ('mL', 'L',),
|
|
|
+ 'Chocolate': ('g', 'kg',),
|
|
|
+ 'Veggies': ('g', 'kg', 'Piece',),
|
|
|
+ 'Fruit': ('g', 'kg', 'Piece', 'Bunch',),
|
|
|
+ 'Coffee': ('g', 'kg',),
|
|
|
+ }
|
|
|
+ ORGANIC = [
|
|
|
+ True,
|
|
|
+ False,
|
|
|
+ ]
|
|
|
+ def _dict_choice(self, _dict):
|
|
|
+ key = fake.random.choice([ i for i in _dict ])
|
|
|
+ return key, _dict[key]
|
|
|
+
|
|
|
+ def _product_choice(self):
|
|
|
+ key = None
|
|
|
+ product = []
|
|
|
+ for _dict in (
|
|
|
+ self.PRODUCTS,
|
|
|
+ self.CATEGORIES,
|
|
|
+ dict([
|
|
|
+ (k, None) for k in self.GROUPS
|
|
|
+ ]),
|
|
|
+ ):
|
|
|
+ if key is None:
|
|
|
+ k, key = self._dict_choice(_dict)
|
|
|
+ product.append(k)
|
|
|
+ continue
|
|
|
+ product.append(key)
|
|
|
+ key = _dict[key]
|
|
|
+ return product
|
|
|
+
|
|
|
+ def product(self):
|
|
|
+ return self._product_choice()
|
|
|
+
|
|
|
+ def product_unit(self, product):
|
|
|
+ return fake.random.choice(self.CATEGORY_UNITS[product[1]])
|
|
|
+
|
|
|
+ def description(self, *args, **kwargs):
|
|
|
+ return fake.sentence(*args, **kwargs)
|
|
|
+
|
|
|
+ def store(self):
|
|
|
+ return fake.random.choice(self.STORES)
|
|
|
+
|
|
|
+ def unit(self):
|
|
|
+ return fake.random.choice(self.UNITS)
|
|
|
+
|
|
|
+ def organic(self):
|
|
|
+ return fake.random.choice(self.ORGANIC)
|
|
|
+
|
|
|
+ # then add new provider to faker instance
|
|
|
+ fake.add_provider(Products)
|
|
|
+
|
|
|
import urwid
|
|
|
from urwid import numedit
|
|
|
+from decimal import Decimal
|
|
|
import time
|
|
|
+import datetime
|
|
|
import itertools
|
|
|
from collections import (
|
|
|
OrderedDict,
|
|
@@ -17,8 +119,76 @@ from collections import (
|
|
|
|
|
|
COPYRIGHT = "Copyright (c) Daniel Sheffield 2021"
|
|
|
|
|
|
-conn = psycopg2.connect("dbname=das user=das")
|
|
|
-cur = conn.cursor()
|
|
|
+class mock_conn(object):
|
|
|
+
|
|
|
+ def close(self):
|
|
|
+ pass
|
|
|
+
|
|
|
+class mock_cur(object):
|
|
|
+ def __init__(self, records):
|
|
|
+ self.records = records
|
|
|
+
|
|
|
+ def execute(self, *args, **kwargs):
|
|
|
+ pass
|
|
|
+
|
|
|
+ def fetchall(self):
|
|
|
+ yield from self.records
|
|
|
+
|
|
|
+ def close(self):
|
|
|
+ pass
|
|
|
+
|
|
|
+
|
|
|
+if MOCK:
|
|
|
+ records = [{
|
|
|
+ 'ts': datetime.datetime(2021, 8, 29, 14, 8, 0, 0),
|
|
|
+ 'store': 'Countdown',
|
|
|
+ 'price': 4.30,
|
|
|
+ 'quantity': 250.0,
|
|
|
+ 'unit': 'g',
|
|
|
+ 'organic': False,
|
|
|
+ 'description': 'Whittakers',
|
|
|
+ 'product': 'Dark Chocolate',
|
|
|
+ 'group': 'Treats',
|
|
|
+ 'category': 'Chocolate',
|
|
|
+ }]
|
|
|
+ for i in range(0,100):
|
|
|
+ product = fake.product()
|
|
|
+ quantity = fake.random.random()*500 + 1
|
|
|
+ price = fake.random.random()*10
|
|
|
+ unit = fake.product_unit(product)
|
|
|
+ organic = fake.organic()
|
|
|
+ words = []
|
|
|
+ words.extend(fake.sentence().split())
|
|
|
+ words.append(
|
|
|
+ f'{quantity:.2f} {unit} @ {price:.2f}'
|
|
|
+ )
|
|
|
+ if organic:
|
|
|
+ words.append('organic')
|
|
|
+
|
|
|
+ records.append({
|
|
|
+ 'ts': datetime.datetime(2021, 8, 29, 14, 8, 0, 0),
|
|
|
+ 'store': fake.store(),
|
|
|
+ 'price': price,
|
|
|
+ 'quantity': quantity,
|
|
|
+ 'unit': unit,
|
|
|
+ 'organic': organic,
|
|
|
+ 'description': ' '.join([
|
|
|
+ product[0],
|
|
|
+ *fake.description(ext_word_list=words).split()
|
|
|
+ ]),
|
|
|
+ 'product': product[0],
|
|
|
+ 'group': product[2],
|
|
|
+ 'category': product[1],
|
|
|
+ })
|
|
|
+ col_idx_map = dict([ ( k, idx ) for idx,k in enumerate(sorted(records[0].keys())) ])
|
|
|
+ records = [
|
|
|
+ [ r[k] for k in sorted(r.keys()) ] for r in records
|
|
|
+ ]
|
|
|
+ conn = mock_conn()
|
|
|
+ cur = mock_cur(records)
|
|
|
+else:
|
|
|
+ conn = psycopg2.connect("dbname=das user=das")
|
|
|
+ cur = conn.cursor()
|
|
|
|
|
|
palette = [
|
|
|
('banner', 'light gray', 'dark red'),
|
|
@@ -69,7 +239,9 @@ display_map = {
|
|
|
display = lambda data, name: display_map[name](data) if name in display_map else data
|
|
|
|
|
|
cur.execute("SELECT * FROM transaction_view;")
|
|
|
-col_idx_map = dict([ (d.name, i) for i,d in enumerate(cur.description) if d.name in cols ])
|
|
|
+if not MOCK:
|
|
|
+ col_idx_map = dict([ (d.name, i) for i,d in enumerate(cur.description) if d.name in cols ])
|
|
|
+
|
|
|
def records(cursor, col_idx_map):
|
|
|
for row in cursor.fetchall():
|
|
|
yield dict([
|
|
@@ -93,10 +265,19 @@ def record_matches(record, strict=None, **kwargs):
|
|
|
return True
|
|
|
|
|
|
def unique_suggestions(name, exclude=NON_IDENTIFIER_COLUMNS, **kwargs):
|
|
|
+ exclude = filter(
|
|
|
+ lambda x: x != name or name == 'ts',
|
|
|
+ exclude,
|
|
|
+ )
|
|
|
[ kwargs.pop(k) for k in exclude if k in kwargs]
|
|
|
- return sorted(set(map(lambda x: x[name], suggestions(name, exclude=exclude, **kwargs))))
|
|
|
+ items = suggestions(name, exclude=exclude, **kwargs)
|
|
|
+ return sorted(set(map(lambda x: x[name], items)))
|
|
|
|
|
|
def suggestions(name, exclude=NON_IDENTIFIER_COLUMNS, **kwargs):
|
|
|
+ exclude = filter(
|
|
|
+ lambda x: x != name or name == 'ts',
|
|
|
+ exclude,
|
|
|
+ )
|
|
|
[ kwargs.pop(k) for k in exclude if k in kwargs]
|
|
|
yield from filter(lambda x: record_matches(
|
|
|
x, strict=[ k for k in kwargs if k != name ], **kwargs
|
|
@@ -130,18 +311,25 @@ class AutoCompleteEdit(urwid.Edit):
|
|
|
self.apply = apply_change_func
|
|
|
|
|
|
def keypress(self, size, key):
|
|
|
- if key != 'enter':
|
|
|
- return super(AutoCompleteEdit, self).keypress(size, key)
|
|
|
+ if key == 'enter':
|
|
|
+ self.apply(self.name)
|
|
|
+ return
|
|
|
+ elif key == 'delete':
|
|
|
+ self.set_edit_text('')
|
|
|
+ return
|
|
|
+
|
|
|
+ return super(AutoCompleteEdit, self).keypress(size, key)
|
|
|
+
|
|
|
self.apply(self.name)
|
|
|
|
|
|
class AutoCompleteFloatEdit(numedit.FloatEdit):
|
|
|
def __init__(self, name, *args, apply_change_func=None, **kwargs):
|
|
|
+ self.last_val = None
|
|
|
self.op = '='
|
|
|
self.pallete = None
|
|
|
if isinstance(name, tuple):
|
|
|
- self.pallete, title = name
|
|
|
- self.name = title
|
|
|
- title = title.title()
|
|
|
+ self.pallete, self.name = name
|
|
|
+ title = self.name.title()
|
|
|
passthrough = (self.pallete, f'{self.op} ')
|
|
|
else:
|
|
|
self.name = name
|
|
@@ -157,7 +345,6 @@ class AutoCompleteFloatEdit(numedit.FloatEdit):
|
|
|
else:
|
|
|
self.set_caption(f'{self.op} ')
|
|
|
|
|
|
-
|
|
|
def set_op(self, op):
|
|
|
self.op = op
|
|
|
self.last_val = self.value()
|
|
@@ -166,16 +353,22 @@ class AutoCompleteFloatEdit(numedit.FloatEdit):
|
|
|
|
|
|
def calc(self):
|
|
|
x = self.last_val
|
|
|
- y = self.value()
|
|
|
op = self.op
|
|
|
- if op == '+':
|
|
|
- z = x + y
|
|
|
- elif op == '-':
|
|
|
- z = x - y
|
|
|
- elif op == '*':
|
|
|
- z = x * y
|
|
|
- elif op == '/':
|
|
|
- z = x / y
|
|
|
+ if op in ('+', '-',):
|
|
|
+ y = self.value() or Decimal(0.0)
|
|
|
+ if op == '+':
|
|
|
+ z = x + y
|
|
|
+ else:
|
|
|
+ z = x - y
|
|
|
+ elif op in ('*', '/'):
|
|
|
+ y = self.value() or Decimal(1.0)
|
|
|
+ if op == '*':
|
|
|
+ z = x * y
|
|
|
+ else:
|
|
|
+ z = x / y
|
|
|
+ else:
|
|
|
+ y = self.value() or Decimal(0.0)
|
|
|
+ z = y
|
|
|
|
|
|
self.op = '='
|
|
|
self.update_caption()
|
|
@@ -185,19 +378,26 @@ class AutoCompleteFloatEdit(numedit.FloatEdit):
|
|
|
if isinstance(key, tuple):
|
|
|
return
|
|
|
ops = ('+', '-', '*', '/',)
|
|
|
- if key == 'tab':
|
|
|
- self.apply(self.name)
|
|
|
- return
|
|
|
- elif key in ops:
|
|
|
+
|
|
|
+ if key in ops:
|
|
|
if self.op in ops:
|
|
|
self.calc()
|
|
|
self.set_op(key)
|
|
|
return
|
|
|
- elif key in ('=', 'enter',):
|
|
|
+ elif key == 'enter':
|
|
|
+ if self.get_edit_text() == '' or self.value() == Decimal(0.0):
|
|
|
+ return self.apply(self.name)
|
|
|
+ self.calc()
|
|
|
+ return
|
|
|
+ elif key == '=':
|
|
|
self.calc()
|
|
|
return
|
|
|
- #elif key in ('esc', 'left', 'right', 'up', 'down', 'backspace', 'delete',) or \
|
|
|
- # key in '0123456789.':
|
|
|
+ elif key == 'delete':
|
|
|
+ self.set_edit_text('')
|
|
|
+ self.op = '='
|
|
|
+ self.update_caption()
|
|
|
+ return
|
|
|
+
|
|
|
return super(AutoCompleteFloatEdit, self).keypress(size, key)
|
|
|
|
|
|
class NoTabCheckBox(urwid.CheckBox):
|
|
@@ -231,16 +431,47 @@ class GroceryTransactionEditor(urwid.WidgetPlaceholder):
|
|
|
self.widgets = dict()
|
|
|
self.show('transaction')
|
|
|
|
|
|
- def advance_focus(self):
|
|
|
+ def iter_focus_paths(self):
|
|
|
+ initial = [2,0,0,0]
|
|
|
+ container = self.original_widget.original_widget.original_widget
|
|
|
+ _set_focus_path(container, initial)
|
|
|
+ while True:
|
|
|
+ path = container.get_focus_path()
|
|
|
+ yield path
|
|
|
+ self.advance_focus()
|
|
|
+ path = container.get_focus_path()
|
|
|
+ if path == initial:
|
|
|
+ self.advance_focus()
|
|
|
+ break
|
|
|
+
|
|
|
+ def advance_focus(self, reverse=False):
|
|
|
container = self.original_widget.original_widget.original_widget
|
|
|
path = container.get_focus_path()
|
|
|
- for idx, part in [ i for i in enumerate(path)][::-1]:
|
|
|
+ if reverse:
|
|
|
+ paths = [ i for i in self.iter_focus_paths() ]
|
|
|
+ zipped_paths = zip(paths, [
|
|
|
+ *paths[1:], paths[0]
|
|
|
+ ])
|
|
|
+ prev_path = map(lambda x: x[0], filter(
|
|
|
+ lambda x: x[1] == path,
|
|
|
+ zipped_paths
|
|
|
+ ))
|
|
|
+ _set_focus_path(container, next(prev_path))
|
|
|
+ return
|
|
|
+
|
|
|
+ _iter = [ i for i in enumerate(path) ][::-1]
|
|
|
+
|
|
|
+ for idx, part in _iter:
|
|
|
p = [ i for i in path ]
|
|
|
- p[idx] += 1
|
|
|
+ if reverse:
|
|
|
+ p[idx] -= 1
|
|
|
+ else:
|
|
|
+ p[idx] += 1
|
|
|
+
|
|
|
try:
|
|
|
_set_focus_path(container, p)
|
|
|
if path == [3]:
|
|
|
- self.advance_focus()
|
|
|
+ self.advance_focus(reverse=reverse)
|
|
|
return
|
|
|
except IndexError:
|
|
|
path[idx] = 0
|
|
@@ -254,7 +485,9 @@ class GroceryTransactionEditor(urwid.WidgetPlaceholder):
|
|
|
|
|
|
if key == 'tab':
|
|
|
self.advance_focus()
|
|
|
- else: #if key == 'esc':
|
|
|
+ elif key == 'shift tab':
|
|
|
+ self.advance_focus(reverse=True)
|
|
|
+ else:
|
|
|
return super(GroceryTransactionEditor, self).keypress(size, key)
|
|
|
|
|
|
|
|
@@ -432,6 +665,10 @@ class GroceryTransactionEditor(urwid.WidgetPlaceholder):
|
|
|
widget = urwid.AttrMap(widget, 'bg')
|
|
|
return widget
|
|
|
|
|
|
+#screen = urwid.raw_display.Screen()
|
|
|
+#screen.set_terminal_properties(colors=256, has_underline=True)
|
|
|
+#screen.register_palette(palette)
|
|
|
+
|
|
|
app = GroceryTransactionEditor(cols)
|
|
|
loop = urwid.MainLoop(app, palette, unhandled_input=show_or_exit)
|
|
|
loop.run()
|