Explorar o código

Add panel showing corresponding DB transactions

Daniel Sheffield %!s(int64=3) %!d(string=hai) anos
pai
achega
b275e0690c
Modificáronse 4 ficheiros con 117 adicións e 71 borrados
  1. 8 0
      db_utils.py
  2. 47 16
      grocery_transactions.py
  3. 12 55
      reconcile.py
  4. 50 0
      txn_view.py

+ 8 - 0
db_utils.py

@@ -0,0 +1,8 @@
+
+def cursor_as_dict(cur):
+    _col_idx_map=dict(map(lambda col: (col[1].name, col[0]), enumerate(cur.description)))
+    for row in map(lambda row, _map=_col_idx_map: dict([
+        (name, row[i]) for name, i in _map.items()
+    ]), cur.fetchall()):
+        #print(row)
+        yield row

+ 47 - 16
grocery_transactions.py

@@ -5,9 +5,11 @@
 # All rights reserved
 #
 # THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
-
+from txn_view import get_statement as get_session_transactions_statement
 try:
     import psycopg2
+    from psycopg2.sql import SQL
+    from db_utils import cursor_as_dict
     MOCK = False
 except:
     from faker import Faker
@@ -137,6 +139,13 @@ class mock_cur(object):
     def close(self):
         pass
 
+try:
+    from db_credentials import HOST, PASSWORD
+    host = f'host={HOST}'
+    password = f'password={PASSWORD}'
+except:
+    host = ''
+    password = ''
 
 if MOCK:
     records = [{
@@ -189,7 +198,7 @@ if MOCK:
 else:
     import os
     user = os.getenv('USER')
-    conn = psycopg2.connect(f"dbname=das user={user}")
+    conn = psycopg2.connect(f"{host} dbname=das user={user} {password}")
     cur = conn.cursor()
 
 palette = [
@@ -210,13 +219,14 @@ side_pane = [
 ]
 bottom_pane = [
     'description',
+    'dbview',
 ]
 
 cols = [
     c for c in filter(
         lambda x: x is not None,
         itertools.chain(
-            *grid_layout, side_pane, bottom_pane
+            *grid_layout, side_pane, set(bottom_pane) - set(['dbview'])
         )
     )
 ]
@@ -240,17 +250,21 @@ display_map = {
 }
 display = lambda data, name: display_map[name](data) if name in display_map else data
 
-cur.execute("SELECT * FROM transaction_view;")
-if not MOCK:
-    col_idx_map = dict([ (d.name, i) for i,d in enumerate(cur.description) if d.name in cols ])
+cur.execute("BEGIN")
 
-def records(cursor, col_idx_map):
-    for row in cursor.fetchall():
-        yield dict([
-            (name, display(row[i], name)) for name, i in col_idx_map.items()
-        ])
-    cur.execute("SELECT * FROM transaction_view;")
+def get_session_transactions(date, store):
+    #print(cur.mogrify(get_session_transactions_statement(date,store,full_name=True)).decode("utf-8"))
+    cur.execute(get_session_transactions_statement(date,store,full_name=True))
+    return '\n'.join(map(lambda x: '|'.join(map(str, x)), cur.fetchall()))
+
+def get_transactions_statement():
+    return SQL("SELECT * FROM transaction_view")
 
+def get_transactions(cursor):
+    cur.execute(get_transactions_statement())
+    yield from  map(lambda x: dict([
+        (k, display(v, k)) for k,v in x.items()
+    ]), cursor_as_dict(cur))
 
 def record_matches(record, strict=None, **kwargs):
     strict = strict or []
@@ -283,7 +297,7 @@ def suggestions(name, exclude=NON_IDENTIFIER_COLUMNS, **kwargs):
     [ 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
-    ), records(cur, col_idx_map))
+    ), get_transactions(cur))
 
 def show_or_exit(key):
     if isinstance(key, tuple):
@@ -616,10 +630,17 @@ class GroceryTransactionEditor(urwid.WidgetPlaceholder):
     def update_transaction(self):
         for k in self.edit_fields:
             self.edit_fields[k].set_edit_text(self.data[k])
+        date, store = self.data['ts'], self.data['store']
+        self.text_fields['dbview'].set_text(
+            get_session_transactions(date, store) if None not in (
+                date or None, store or None
+            ) else ''
+        )
         self.organic_checkbox.set_state(True if self.data['organic'] == 'true' else False)
 
     def transaction(self):
         self.edit_fields = OrderedDict()
+        self.text_fields = OrderedDict()
         for k in self.data:
             if k in side_pane and k != 'unit':
                 ef = AutoCompleteFloatEdit(('bg', k), apply_change_func=self._autocomplete)
@@ -633,8 +654,8 @@ class GroceryTransactionEditor(urwid.WidgetPlaceholder):
     
         header = urwid.Text(u'Fill Transaction', 'center')
         _copyright = urwid.Text(COPYRIGHT, 'center')
-        button = urwid.Button(('streak', u'Done'))
-        urwid.connect_signal(button, 'click', lambda w: self.save_and_clear())
+        done_button = urwid.Button(('streak', u'Done'))
+        urwid.connect_signal(done_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)),
@@ -643,6 +664,16 @@ class GroceryTransactionEditor(urwid.WidgetPlaceholder):
         fields = dict([
             (k, urwid.LineBox(urwid.AttrMap(self.edit_fields[k], 'streak'), title=k.title(), title_align='left')) for k in self.edit_fields
         ])
+        txn_view = urwid.Text('')
+        self.text_fields.update({'dbview': txn_view})
+        fields.update({
+            'dbview': urwid.LineBox(
+                urwid.AttrMap(txn_view, 'streak'),
+                title="Session Data",
+                title_align='left',
+            )
+        })
+        
         side_pane_widget = (12, urwid.Pile([
             fields[r] if r is not None else urwid.Divider() for r in side_pane
         ]))
@@ -670,7 +701,7 @@ class GroceryTransactionEditor(urwid.WidgetPlaceholder):
             ),
             *[ fields[c] if c is not None else urwid.Divider() for c in bottom_pane ],
             urwid.Divider(),
-            button,
+            done_button,
         ])
         widget = urwid.Filler(widget, 'top') 
         widget = urwid.AttrMap(widget, 'bg')

+ 12 - 55
reconcile.py

@@ -9,13 +9,16 @@ import gnucash
 import sys
 import os
 import psycopg2
-from psycopg2.sql import (
-    Identifier,
-    SQL,
-    Literal,
-    Placeholder,
-    Composed,
-)
+from db_utils import cursor_as_dict
+from txn_view import get_statement
+
+try:
+    from db_credentials import HOST, PASSWORD
+    host = f'host={HOST}'
+    password = f'password={PASSWORD}'
+except:
+    host = ''
+    password = ''
 
 STORE_CODES = {
     'countdown': 'CD',
@@ -30,7 +33,7 @@ STORE_CODES = {
 }
 
 user = os.getenv('USER')
-conn = psycopg2.connect(f"host=***REMOVED*** dbname=das user={user} password='***REMOVED***'")
+conn = psycopg2.connect(f"{host} dbname=das user={user} {password}")
 cur = conn.cursor()
 
 def get_record_from_database(date, store):
@@ -38,13 +41,6 @@ def get_record_from_database(date, store):
     #print(cur.mogrify(get_statement(date, store)))
     return sum([row['price'] for row in cursor_as_dict(cur)])
 
-def cursor_as_dict(cur):
-    _col_idx_map=dict(map(lambda col: (col[1].name, col[0]), enumerate(cur.description)))
-    for row in map(lambda row, _map=_col_idx_map: dict([
-        (name, row[i]) for name, i in _map.items()
-    ]), cur.fetchall()):
-        #print(row)
-        yield row
 
 def get_store_code(store, value, user_data=dict()):
     for k, v in STORE_CODES.items():
@@ -55,45 +51,6 @@ def get_store_code(store, value, user_data=dict()):
         user_data[store] = input(f"Enter code for {store} ({int(value)/100:>6.2f}): ")
     return user_data[store]
 
-select = {
-    'ts': SQL("""date_part('day',ts)||'/'||date_part('month',ts)||' '||date_part('hour',ts)::int%12"""),
-    'shop': Identifier('code'),
-    'description': SQL("""substr(description,1,32)"""),
-    'volume': Identifier('quantity'),
-    'unit': SQL("""substr(unit,1,4)"""),
-    'price': Identifier('price'),
-    '$/unit': SQL("""TRUNC(price/quantity,4)"""),
-    'total': SQL("""sum(transaction_view.price) OVER (PARTITION BY transaction_view.ts::date ORDER BY transaction_view.ts, transaction_view.description ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)"""),
-    'group': SQL("""substr(product,1,10) AS product, substr(category,1,8) AS category, substr("group",1,9)"""),
-    'og': Identifier('organic'),
-}
-
-def get_where(date, store):
-    where = [ ]
-    if store is not None:
-        where.append(SQL(' ').join([
-            Identifier('code'), SQL('='), Literal(store)
-        ]))
-    where.append(
-        SQL("{ts} BETWEEN {date}::date AND {date}::date + {interval}::interval").format(
-            ts=Identifier('ts'),
-            date=Literal(str(date)),
-            interval=Literal('23 hours 59 minutes 59 seconds'),
-        )
-    )
-    return SQL(" AND ").join(where)
-
-def get_statement(date, store):
-    statement = SQL("\n").join([
-        SQL("SELECT"),
-        SQL(',\n  ').join([
-            SQL(' ').join([v, SQL('AS'), Identifier(f'{k}')]) for k, v in select.items()
-        ]),
-        SQL("FROM transaction_view"),
-        SQL("WHERE"), get_where(date,store)
-    ])
-    return statement
-
 def _unwrap_list(root, blacklist):
   for i in map(lambda x: [
       (':'.join([x[0], c.name]), True) for c in root.get_children() 
@@ -201,7 +158,7 @@ if __name__ == '__main__':
             ts = ts - timedelta(days=1)
             book = get_record_from_database(ts, store)
         dbentry = f'{ts} {store:5s}' if book == int(value) else ''
-        print(f'{d} {desc:35s} : {value/100:>6.2f} | {dbentry}')
+        print(f'{d} {desc:35s} | {value/100:>6.2f} | {dbentry}')
         
   finally:
     session.end()

+ 50 - 0
txn_view.py

@@ -0,0 +1,50 @@
+from psycopg2.sql import (
+    Identifier,
+    SQL,
+    Literal,
+    Placeholder,
+    Composed,
+)
+
+select = {
+    'ts': SQL("""date_part('day',ts)||'/'||date_part('month',ts)||' '||date_part('hour',ts)::int%12"""),
+    'shop': Identifier('code'),
+    'description': SQL("""substr(description,1,32)"""),
+    'volume': Identifier('quantity'),
+    'unit': SQL("""substr(unit,1,4)"""),
+    'price': Identifier('price'),
+    '$/unit': SQL("""TRUNC(price/quantity,4)"""),
+    'total': SQL("""sum(transaction_view.price) OVER (PARTITION BY transaction_view.ts::date ORDER BY transaction_view.ts, transaction_view.description ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)"""),
+    'group': SQL("""substr(product,1,10) AS product, substr(category,1,8) AS category, substr("group",1,9)"""),
+    'og': Identifier('organic'),
+}
+
+def get_where(date, store, full_name=False):
+    where = [ ]
+    if store is not None:
+        where.append(SQL(' ').join([
+            Identifier('store' if full_name else 'code'), SQL('='), Literal(store)
+        ]))
+    where.append(
+        SQL("{ts} BETWEEN {date}::date AND {date}::date + {interval}::interval").format(
+            ts=Identifier('ts'),
+            date=Literal(str(date)),
+            interval=Literal('23 hours 59 minutes 59 seconds'),
+        )
+    )
+    return SQL("\n      ").join([
+        SQL("WHERE"),
+        SQL("\n  AND ").join(where),
+    ])
+
+def get_statement(date, store, full_name=False):
+    statement = SQL("\n").join([
+        SQL("\n  ").join([
+            SQL("SELECT"),
+            SQL(',\n  ').join([
+                SQL(' ').join([v, SQL('AS'), Identifier(f'{k}')]) for k, v in select.items()
+            ])]),
+        SQL("FROM transaction_view"),
+        get_where(date,store, full_name=full_name)
+    ])
+    return statement