|
@@ -8,7 +8,9 @@
|
|
|
|
|
|
import psycopg2
|
|
import psycopg2
|
|
import urwid
|
|
import urwid
|
|
|
|
+from urwid import numedit
|
|
import time
|
|
import time
|
|
|
|
+import itertools
|
|
from collections import (
|
|
from collections import (
|
|
OrderedDict,
|
|
OrderedDict,
|
|
)
|
|
)
|
|
@@ -18,18 +20,37 @@ COPYRIGHT = "Copyright (c) Daniel Sheffield 2021"
|
|
conn = psycopg2.connect("dbname=das user=das")
|
|
conn = psycopg2.connect("dbname=das user=das")
|
|
cur = conn.cursor()
|
|
cur = conn.cursor()
|
|
|
|
|
|
-cols = [
|
|
|
|
- 'ts',
|
|
|
|
- 'store',
|
|
|
|
- 'product',
|
|
|
|
- 'description',
|
|
|
|
|
|
+palette = [
|
|
|
|
+ ('banner', 'light gray', 'dark red'),
|
|
|
|
+ ('streak', 'light red', 'dark gray'),
|
|
|
|
+ ('bg', 'light red', 'black'),
|
|
|
|
+]
|
|
|
|
+
|
|
|
|
+grid_layout = [
|
|
|
|
+ [ 'ts', 'store', ],
|
|
|
|
+ [ 'organic', 'product', ],
|
|
|
|
+ [ 'category', 'group', ],
|
|
|
|
+]
|
|
|
|
+side_pane = [
|
|
'quantity',
|
|
'quantity',
|
|
'unit',
|
|
'unit',
|
|
'price',
|
|
'price',
|
|
- 'organic',
|
|
|
|
- 'category',
|
|
|
|
- 'group',
|
|
|
|
]
|
|
]
|
|
|
|
+bottom_pane = [
|
|
|
|
+ 'description',
|
|
|
|
+]
|
|
|
|
+
|
|
|
|
+cols = [
|
|
|
|
+ c for c in filter(
|
|
|
|
+ lambda x: x is not None,
|
|
|
|
+ itertools.chain(
|
|
|
|
+ *grid_layout, side_pane, bottom_pane
|
|
|
|
+ )
|
|
|
|
+ )
|
|
|
|
+]
|
|
|
|
+
|
|
|
|
+#cols.remove(None)
|
|
|
|
+
|
|
NON_IDENTIFIER_COLUMNS = [
|
|
NON_IDENTIFIER_COLUMNS = [
|
|
'ts',
|
|
'ts',
|
|
'store',
|
|
'store',
|
|
@@ -51,20 +72,34 @@ 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 ])
|
|
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):
|
|
def records(cursor, col_idx_map):
|
|
for row in cursor.fetchall():
|
|
for row in cursor.fetchall():
|
|
- #print(row)
|
|
|
|
- #raise(Exception(repr(row)))
|
|
|
|
yield dict([
|
|
yield dict([
|
|
(name, display(row[i], name)) for name, i in col_idx_map.items()
|
|
(name, display(row[i], name)) for name, i in col_idx_map.items()
|
|
])
|
|
])
|
|
cur.execute("SELECT * FROM transaction_view;")
|
|
cur.execute("SELECT * FROM transaction_view;")
|
|
|
|
|
|
|
|
|
|
-def record_matches(record, **kwargs):
|
|
|
|
|
|
+def record_matches(record, strict=None, **kwargs):
|
|
|
|
+ strict = strict or []
|
|
|
|
+ if kwargs['product']:
|
|
|
|
+ with open('log', 'a') as f:
|
|
|
|
+ f.write(f"Filter {strict}: {kwargs}\n")
|
|
|
|
+
|
|
for k,v in kwargs.items():
|
|
for k,v in kwargs.items():
|
|
|
|
+ with open('log', 'a') as f:
|
|
|
|
+ f.write(f"Record: {record}\n")
|
|
if not v:
|
|
if not v:
|
|
continue
|
|
continue
|
|
|
|
+
|
|
|
|
+ if k in strict and v.lower() != record[k].lower():
|
|
|
|
+ return False
|
|
|
|
+
|
|
if v.lower() not in record[k].lower():
|
|
if v.lower() not in record[k].lower():
|
|
|
|
+ with open('log', 'a') as f:
|
|
|
|
+ f.write(f"No match\n")
|
|
return False
|
|
return False
|
|
|
|
+
|
|
|
|
+ with open('log', 'a') as f:
|
|
|
|
+ f.write(f"Match\n")
|
|
return True
|
|
return True
|
|
|
|
|
|
def unique_suggestions(name, exclude=NON_IDENTIFIER_COLUMNS, **kwargs):
|
|
def unique_suggestions(name, exclude=NON_IDENTIFIER_COLUMNS, **kwargs):
|
|
@@ -73,7 +108,9 @@ def unique_suggestions(name, exclude=NON_IDENTIFIER_COLUMNS, **kwargs):
|
|
|
|
|
|
def suggestions(name, exclude=NON_IDENTIFIER_COLUMNS, **kwargs):
|
|
def suggestions(name, exclude=NON_IDENTIFIER_COLUMNS, **kwargs):
|
|
[ kwargs.pop(k) for k in exclude if k in kwargs]
|
|
[ kwargs.pop(k) for k in exclude if k in kwargs]
|
|
- yield from filter(lambda x: record_matches(x, **kwargs), records(cur, col_idx_map))
|
|
|
|
|
|
+ yield from filter(lambda x: record_matches(
|
|
|
|
+ x, strict=[ k for k in kwargs if k != name ], **kwargs
|
|
|
|
+ ), records(cur, col_idx_map))
|
|
|
|
|
|
def show_or_exit(key):
|
|
def show_or_exit(key):
|
|
if isinstance(key, tuple):
|
|
if isinstance(key, tuple):
|
|
@@ -81,11 +118,24 @@ def show_or_exit(key):
|
|
if key in ('esc'):
|
|
if key in ('esc'):
|
|
raise urwid.ExitMainLoop()
|
|
raise urwid.ExitMainLoop()
|
|
|
|
|
|
|
|
+def interleave(_list, div):
|
|
|
|
+ for element in _list:
|
|
|
|
+ yield element
|
|
|
|
+ yield div
|
|
|
|
+
|
|
class AutoCompleteEdit(urwid.Edit):
|
|
class AutoCompleteEdit(urwid.Edit):
|
|
def __init__(self, name, *args, apply_change_func=None, **kwargs):
|
|
def __init__(self, name, *args, apply_change_func=None, **kwargs):
|
|
- title = name.title()
|
|
|
|
- super(AutoCompleteEdit, self).__init__(f"{title:12s}: ", *args, **kwargs)
|
|
|
|
- self.name = name
|
|
|
|
|
|
+ if isinstance(name, tuple):
|
|
|
|
+ pallete, title = name
|
|
|
|
+ self.name = title
|
|
|
|
+ title = title.title()
|
|
|
|
+ passthrough = (pallete, u'')
|
|
|
|
+ else:
|
|
|
|
+ self.name = name
|
|
|
|
+ title = name.title()
|
|
|
|
+ passthrough = title
|
|
|
|
+
|
|
|
|
+ super(AutoCompleteEdit, self).__init__(passthrough, *args, **kwargs)
|
|
self.apply = apply_change_func
|
|
self.apply = apply_change_func
|
|
|
|
|
|
def keypress(self, size, key):
|
|
def keypress(self, size, key):
|
|
@@ -93,9 +143,40 @@ class AutoCompleteEdit(urwid.Edit):
|
|
return super(AutoCompleteEdit, self).keypress(size, key)
|
|
return super(AutoCompleteEdit, self).keypress(size, key)
|
|
self.apply(self.name)
|
|
self.apply(self.name)
|
|
|
|
|
|
|
|
+class AutoCompleteFloatEdit(numedit.FloatEdit):
|
|
|
|
+ def __init__(self, name, *args, apply_change_func=None, **kwargs):
|
|
|
|
+ if isinstance(name, tuple):
|
|
|
|
+ pallete, title = name
|
|
|
|
+ self.name = title
|
|
|
|
+ title = title.title()
|
|
|
|
+ passthrough = (pallete, u'')
|
|
|
|
+ else:
|
|
|
|
+ self.name = name
|
|
|
|
+ title = name.title()
|
|
|
|
+ passthrough = title
|
|
|
|
+
|
|
|
|
+ super(AutoCompleteFloatEdit, self).__init__(passthrough, *args, **kwargs)
|
|
|
|
+ self.apply = apply_change_func
|
|
|
|
+
|
|
|
|
+ def keypress(self, size, key):
|
|
|
|
+ if isinstance(key, tuple):
|
|
|
|
+ return
|
|
|
|
+
|
|
|
|
+ if key == 'tab':
|
|
|
|
+ self.apply(self.name)
|
|
|
|
+ return
|
|
|
|
+ elif key in ('esc', 'up', 'down', 'left', 'right',):
|
|
|
|
+ return super(AutoCompleteFloatEdit, self).keypress(size, key)
|
|
|
|
+ else:
|
|
|
|
+ return
|
|
|
|
+
|
|
class GroceryTransactionEditor(urwid.WidgetPlaceholder):
|
|
class GroceryTransactionEditor(urwid.WidgetPlaceholder):
|
|
def __init__(self, fields):
|
|
def __init__(self, fields):
|
|
super(GroceryTransactionEditor, self).__init__(urwid.SolidFill(u'/'))
|
|
super(GroceryTransactionEditor, self).__init__(urwid.SolidFill(u'/'))
|
|
|
|
+ self.organic_checkbox = urwid.CheckBox(
|
|
|
|
+ u"Organic",
|
|
|
|
+ on_state_change=self.apply_organic_state
|
|
|
|
+ )
|
|
self._init_data(fields)
|
|
self._init_data(fields)
|
|
self.widgets = dict()
|
|
self.widgets = dict()
|
|
self.show('transaction')
|
|
self.show('transaction')
|
|
@@ -130,7 +211,7 @@ class GroceryTransactionEditor(urwid.WidgetPlaceholder):
|
|
if k == name or v:
|
|
if k == name or v:
|
|
continue
|
|
continue
|
|
options = unique_suggestions(k, **self.data)
|
|
options = unique_suggestions(k, **self.data)
|
|
- if len(options) == 1:
|
|
|
|
|
|
+ if len(options) == 1 and k != 'ts':
|
|
self.data.update({
|
|
self.data.update({
|
|
k: list(options)[0],
|
|
k: list(options)[0],
|
|
})
|
|
})
|
|
@@ -149,6 +230,9 @@ class GroceryTransactionEditor(urwid.WidgetPlaceholder):
|
|
options = unique_suggestions(name, **self.data)
|
|
options = unique_suggestions(name, **self.data)
|
|
if 0 < len(options):
|
|
if 0 < len(options):
|
|
self.show('suggestions', name, options=options)
|
|
self.show('suggestions', name, options=options)
|
|
|
|
+
|
|
|
|
+ def apply_organic_state(self, w, state):
|
|
|
|
+ self.data['organic'] = repr(state).lower()
|
|
|
|
|
|
def apply_changes(self, name):
|
|
def apply_changes(self, name):
|
|
apply = lambda w,x: self._apply_changes(name, x)
|
|
apply = lambda w,x: self._apply_changes(name, x)
|
|
@@ -172,7 +256,7 @@ class GroceryTransactionEditor(urwid.WidgetPlaceholder):
|
|
return original(size, key)
|
|
return original(size, key)
|
|
self.show('transaction')
|
|
self.show('transaction')
|
|
top.keypress = keypress
|
|
top.keypress = keypress
|
|
- return top
|
|
|
|
|
|
+ return urwid.AttrMap(top, 'banner')
|
|
|
|
|
|
def save(self):
|
|
def save(self):
|
|
fmt = '%Y-%m-%dT%H%M'
|
|
fmt = '%Y-%m-%dT%H%M'
|
|
@@ -198,7 +282,7 @@ class GroceryTransactionEditor(urwid.WidgetPlaceholder):
|
|
if k in ('ts', 'store',):
|
|
if k in ('ts', 'store',):
|
|
continue
|
|
continue
|
|
self.data[k] = ''
|
|
self.data[k] = ''
|
|
- #self.data['organic'] = 'false'
|
|
|
|
|
|
+ self.organic_checkbox.set_state(False)
|
|
|
|
|
|
def save_and_clear(self):
|
|
def save_and_clear(self):
|
|
self.save()
|
|
self.save()
|
|
@@ -209,32 +293,69 @@ class GroceryTransactionEditor(urwid.WidgetPlaceholder):
|
|
getattr(self, f"update_{name}")()
|
|
getattr(self, f"update_{name}")()
|
|
|
|
|
|
def update_transaction(self):
|
|
def update_transaction(self):
|
|
- for k in self.data:
|
|
|
|
|
|
+ for k in self.edit_fields:
|
|
self.edit_fields[k].set_edit_text(self.data[k])
|
|
self.edit_fields[k].set_edit_text(self.data[k])
|
|
|
|
+ self.organic_checkbox.set_state(True if self.data['organic'] == 'true' else False)
|
|
|
|
|
|
def transaction(self):
|
|
def transaction(self):
|
|
self.edit_fields = OrderedDict()
|
|
self.edit_fields = OrderedDict()
|
|
for k in self.data:
|
|
for k in self.data:
|
|
- self.edit_fields[k] = AutoCompleteEdit(k, apply_change_func=self._autocomplete)
|
|
|
|
- self.edit_fields[k].set_edit_text(self.data[k])
|
|
|
|
- urwid.connect_signal(self.edit_fields[k], 'change', self.apply_changes(k))
|
|
|
|
|
|
+ if k in side_pane and k != 'unit':
|
|
|
|
+ ef = AutoCompleteFloatEdit(('bg', k), apply_change_func=self._autocomplete)
|
|
|
|
+ elif k != 'organic':
|
|
|
|
+ ef = AutoCompleteEdit(('bg', k), apply_change_func=self._autocomplete)
|
|
|
|
+ else:
|
|
|
|
+ continue
|
|
|
|
+ ef.set_edit_text(self.data[k])
|
|
|
|
+ urwid.connect_signal(ef, 'change', self.apply_changes(k))
|
|
|
|
+ self.edit_fields[k] = ef
|
|
|
|
|
|
- header = urwid.Text('Fill transaction', 'center')
|
|
|
|
|
|
+ header = urwid.Text(u'Fill Transaction', 'center')
|
|
_copyright = urwid.Text(COPYRIGHT, 'center')
|
|
_copyright = urwid.Text(COPYRIGHT, 'center')
|
|
- button = urwid.Button(u'Done')
|
|
|
|
|
|
+ button = urwid.Button(('streak', u'Done'))
|
|
urwid.connect_signal(button, 'click', lambda w: self.save_and_clear())
|
|
urwid.connect_signal(button, 'click', lambda w: self.save_and_clear())
|
|
|
|
+ banner = urwid.Pile([
|
|
|
|
+ urwid.Padding(header, 'center', width=('relative', 100)),
|
|
|
|
+ urwid.Padding(_copyright, 'center', width=('relative', 100)),
|
|
|
|
+ ])
|
|
|
|
+ banner = urwid.AttrMap(banner, 'banner')
|
|
|
|
+ fields = dict([
|
|
|
|
+ (k, urwid.LineBox(urwid.AttrMap(self.edit_fields[k], 'streak'), title=k.title(), title_align='left')) for k in self.edit_fields
|
|
|
|
+ ])
|
|
|
|
+ side_pane_widget = (15, urwid.Pile([
|
|
|
|
+ fields[r] if r is not None else urwid.Divider() for r in side_pane
|
|
|
|
+ ]))
|
|
|
|
+ main_pane_widgets = []
|
|
|
|
+ for r in grid_layout:
|
|
|
|
+ widgets = []
|
|
|
|
+ for c in r:
|
|
|
|
+ if c is not None:
|
|
|
|
+ if c != 'organic':
|
|
|
|
+ widgets.append(fields[c])
|
|
|
|
+ else:
|
|
|
|
+ widgets.append(urwid.LineBox(urwid.AttrMap(self.organic_checkbox, 'bg')))
|
|
|
|
+ else:
|
|
|
|
+ widgets.append(urwid.Divider())
|
|
|
|
+ main_pane_widgets.append(urwid.Columns(widgets))
|
|
|
|
+
|
|
|
|
+ main_pane_widget = (62, urwid.Pile(main_pane_widgets))
|
|
|
|
+
|
|
widget = urwid.Pile([
|
|
widget = urwid.Pile([
|
|
- urwid.Padding(header, 'center', width=('relative', 30)),
|
|
|
|
- urwid.Padding(_copyright, 'center', width=('relative', 30)),
|
|
|
|
- *[ self.edit_fields[k] for k in self.data ],
|
|
|
|
|
|
+ banner,
|
|
|
|
+ urwid.Divider(),
|
|
|
|
+ urwid.Columns((main_pane_widget, side_pane_widget),
|
|
|
|
+ dividechars=2,
|
|
|
|
+ ),
|
|
|
|
+ *[ fields[c] if c is not None else urwid.Divider() for c in bottom_pane ],
|
|
urwid.Divider(),
|
|
urwid.Divider(),
|
|
button,
|
|
button,
|
|
])
|
|
])
|
|
widget = urwid.Filler(widget, 'top')
|
|
widget = urwid.Filler(widget, 'top')
|
|
|
|
+ widget = urwid.AttrMap(widget, 'bg')
|
|
return widget
|
|
return widget
|
|
|
|
|
|
app = GroceryTransactionEditor(cols)
|
|
app = GroceryTransactionEditor(cols)
|
|
-loop = urwid.MainLoop(app, unhandled_input=show_or_exit)
|
|
|
|
|
|
+loop = urwid.MainLoop(app, palette, unhandled_input=show_or_exit)
|
|
loop.run()
|
|
loop.run()
|
|
|
|
|
|
cur.close()
|
|
cur.close()
|