Browse Source

Generate query for historic prices

Daniel Sheffield 3 years ago
parent
commit
b56668b808
4 changed files with 221 additions and 16 deletions
  1. 31 2
      db_utils.py
  2. 19 14
      price_check.py
  3. 158 0
      price_view.py
  4. 13 0
      txn_view.py

+ 31 - 2
db_utils.py

@@ -5,9 +5,13 @@
 #
 # THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
 from txn_view import (
+    get_table_statement,
     get_transactions_statement,
     get_session_transactions_statement,
 )
+from price_view import(
+    get_historic_prices_statement,
+)
 from dateutil.parser import parse as parse_time
 import pandas as pd
 NON_IDENTIFIER_COLUMNS = [
@@ -27,7 +31,7 @@ def cursor_as_dict(cur):
         #print(row)
         yield row
 
-def get_transactions(cursor, statement, display):
+def get_data(cursor, statement, display):
     cursor.execute(statement)
     yield from  map(lambda x: dict([
         (k, display(v, k)) for k,v in x.items()
@@ -69,6 +73,17 @@ def unique_suggestions(cur, statement, name, display, exclude=NON_IDENTIFIER_COL
     )
     [ kwargs.pop(k) for k in exclude if k in kwargs]
     items = suggestions(cur, statement, name, display, exclude=exclude, **kwargs)
+    ret = sorted(set(map(lambda x: x[name], items)))
+    tables = {
+        'product': 'products',
+        'category': 'categories',
+        'group': 'groups',
+        'unit': 'store',
+    }
+    if len(ret) > 0 or name not in tables:
+        return ret
+        
+    items = get_data(cur, get_table_statement(tables[name]), display)
     return sorted(set(map(lambda x: x[name], items)))
 
 def suggestions(cur, statement, name, display, exclude=NON_IDENTIFIER_COLUMNS, **kwargs):
@@ -79,7 +94,7 @@ def suggestions(cur, statement, name, display, exclude=NON_IDENTIFIER_COLUMNS, *
     [ 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
-    ), get_transactions(cur, statement, display))
+    ), get_data(cur, statement, display))
 
 class QueryManager(object):
     
@@ -88,6 +103,20 @@ class QueryManager(object):
         self.cursor = cursor
         self.activity_manager = activity_manager
     
+    def get_historic_prices(self, sort, product, unit, organic=None, limit='90 days'):
+        statement = get_historic_prices_statement(sort, product, unit, organic=organic, limit=limit)
+        #print(self.cursor.mogrify(statement).decode('utf-8'))
+        #input()
+        df = pd.DataFrame(get_data(self.cursor, statement, self.display))
+        if df.empty:
+            return ''
+        return df.drop(labels=[
+            'id',
+        ], axis=1).to_string(header=[
+            'Date', 'Store', '$/unit', 'Avg.', 'Min', 'Max',
+            'Group', 'Category', 'Product', 'Organic',
+        ], justify='justify-all', max_colwidth=60, index=False)
+    
     def get_session_transactions(self, date, store):
         return get_session_transactions(
             self.cursor, get_session_transactions_statement(

+ 19 - 14
price_check.py

@@ -100,9 +100,9 @@ def show_or_exit(key):
         raise urwid.ExitMainLoop()
 
 def _apply_choice_callback(activity_manager, name, widget, value):
-    txn = activity_manager.get('price_check')
-    txn.apply_choice(name)(widget, value)
-    activity_manager.show(txn.update())
+    activity = activity_manager.get('price_check')
+    activity.apply_choice(name, widget, value)
+    activity_manager.show(activity.update())
 
 def _show_suggestions_callback(activity_manager, name, options):
     txn = activity_manager.get('price_check')
@@ -197,17 +197,14 @@ class PriceCheck(urwid.WidgetPlaceholder):
         ])
         self.clear()
 
-    def _apply_choice(self, name, value):
+    def apply_choice(self, name, widget, value):
         self._apply_changes(name, value)
         for k,v in self.data.items():
             if k == name or v:
                 continue
             options = self.query_manager.unique_suggestions(k, **self.data)
-            if len(options) == 1 and k != 'ts':
+            if len(options) == 1:
                 self._apply_changes(k, list(options)[0])
-    
-    def apply_choice(self, name):
-        return  lambda w,x: self._apply_choice(name, x)
 
     def _apply_changes(self, name, value):
         self.data.update({
@@ -225,18 +222,26 @@ class PriceCheck(urwid.WidgetPlaceholder):
             if k in ('ts', 'store',):
                 continue
             self.data[k] = ''
-        self.organic_checkbox.set_state(False)
+        self.organic_checkbox.set_state('mixed')
     
     def update(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']
+        
+        sort = '$/unit' if self.buttons['sort_price'].state else 'ts'
+        #print(self.organic_checkbox.state)
+        #input()
+        organic = None if self.organic_checkbox.state == "mixed" else self.organic_checkbox.state
+        #print(organic)
+        #input()
         self.text_fields['dbview'].set_text(
-            self.query_manager.get_session_transactions(date, store) if None not in (
-                date or None, store or None
-            ) else ''
+            self.query_manager.get_historic_prices(sort, self.data['product'], self.data['unit'], organic=organic)
         )
-        self.organic_checkbox.set_state(True if self.data['organic'] == 'true' else False)
+        self.organic_checkbox.set_state({
+            "'mixed'": 'mixed',
+            'true': True,
+            'false': False,
+        }[self.data['organic']])
         return self
 
     def __init__(self, query_manager, fields,

+ 158 - 0
price_view.py

@@ -0,0 +1,158 @@
+#
+# Copyright (c) Daniel Sheffield 2021
+#
+# All rights reserved
+#
+# THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
+from psycopg2.sql import (
+    Identifier,
+    SQL,
+    Literal,
+    Placeholder,
+    Composed,
+)
+from collections import (
+    OrderedDict,
+)
+
+def get_where(product, unit, organic=None, limit='90 days'):
+    where = [ ]
+    where.append(SQL(' ').join([
+        Identifier('products', 'name'),
+        SQL('='),
+        Literal(product)
+    ]))
+    where.append(SQL(' ').join([
+        Identifier('units', 'name'),
+        SQL('='),
+        Literal(unit),
+    ]))
+    if organic is not None:
+        where.append(SQL(' ').join([
+            Identifier('organic'),
+            SQL('='),
+            Literal(organic),
+        ]))
+    where.append(
+        SQL("{ts} at time zone 'utc' BETWEEN now()::date - {interval} AND now()::date").format(
+            ts=Identifier('ts'),
+            interval=SQL("{literal}::interval").format(literal=Literal(limit))
+        )
+    )
+    return SQL('').join([
+        SQL("WHERE"
+            "\n      "),
+        SQL("\n  AND ").join(where),
+    ])
+
+def get_historic_prices_statement(sort, product, unit, organic=None, limit='90 days'):
+    partition = f"(PARTITION BY {'organic,' if organic is not None else ''} product_id, unit_id)"
+    organic_sort = f"{'organic,' if organic is not None else ''}"
+    select = OrderedDict([
+        ('id', Identifier('transactions','id')),
+        #('ts', SQL('{identifier}::date').format(
+        #    identifier=Identifier('ts'),
+        #)),
+        ('date', SQL("""date_part('day',ts)||'/'||date_part('month',ts)||'/'||date_part('year',ts)""")),
+        #('store', Identifier('stores', 'name')),
+        ('code', Identifier('stores', 'code')),
+        #('description', Identifier('transactions','description')),
+        #('volume', Identifier('quantity')),
+        #('unit', Identifier('units', 'name')),
+        #('price', Identifier('price')),
+        ('$/unit', SQL("""TRUNC(price/quantity,4)""")),
+        ('avg', SQL(f"""TRUNC(sum(price) OVER {partition} / sum(quantity) OVER {partition}, 4)""")),
+        ('min', SQL(f"""TRUNC(min(price/quantity) OVER {partition}, 4)""")),
+        ('max', SQL(f"""TRUNC(max(price/quantity) OVER {partition}, 4)""")),
+        #('total', SQL("""sum(transactions.price) OVER (PARTITION BY transactions.ts::date ORDER BY transactions.id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)""")),
+        ('group', Identifier('groups','name')),
+        ('category', Identifier('categories','name')),
+        ('product', Identifier('products','name')),
+        ('organic', Identifier('organic')),
+        
+        #('price_sum', SQL(f"""sum(price) OVER {partition}""")),
+        #('quantity_sum', SQL(f"""sum(quantity) OVER {partition}""")),
+        #('price_min', SQL(f"""min(price/quantity) OVER {partition}""")),
+        #('price_max', SQL(f"""max(price/quantity) OVER {partition}""")),
+    ])
+    statement = SQL('\n').join([
+        SQL('').join([
+            SQL("SELECT"
+                "\n  "),
+            SQL(','
+                "\n  ").join([
+                SQL(' ').join([v, SQL('AS'), Identifier(f'{k}')]) for k, v in select.items()
+            ])
+        ]),
+        SQL('').join([
+            SQL("FROM"
+                "\n       "),
+            SQL("\n  JOIN ").join([
+                SQL("transactions"),
+                SQL("{table} ON {table}.{key} = {index}").format(
+                    table=Identifier('units'),
+                    key=Identifier('id'),
+                    index=Identifier('unit_id'),
+                ),
+                SQL("{table} ON {table}.{key} = {index}").format(
+                    table=Identifier('stores'),
+                    key=Identifier('id'),
+                    index=Identifier('store_id')
+                ),
+                SQL("{table} ON {table}.{key} = {index}").format(
+                    table=Identifier('products'),
+                    key=Identifier('id'),
+                    index=Identifier('product_id')
+                ),
+                SQL("{table} ON {table}.{key} = {index}").format(
+                    table=Identifier('categories'),
+                    key=Identifier('id'),
+                    index=Identifier('category_id')
+                ),
+                SQL("{table} ON {table}.{key} = {index}").format(
+                    table=Identifier('groups'),
+                    key=Identifier('id'),
+                    index=Identifier('group_id')
+                ),
+            ]),
+        ]),
+        get_where(product, unit, organic=organic, limit=limit),
+        SQL('ORDER BY {organic_sort} {sort} {direction}, code, "$/unit" ASC, ts DESC').format(
+            sort=Identifier(sort),
+            direction=SQL('DESC' if sort == 'ts' else 'ASC'),
+            organic_sort=SQL(organic_sort),
+        ),
+    ])
+    return statement
+    # cols = ('product', 'unit', 'organic', 'store', '$/unit',)
+    # aggs = {
+        # 'avg': SQL("""TRUNC(price_sum/quantity_sum, 4)"""),
+        # 'min': SQL("""TRUNC(price_min, 4)"""),
+        # 'max': SQL("""TRUNC(price_max, 4)"""),
+        # #'$/unit': SQL("""sum({price})/sum({quantity})""").format(
+        # #    price=Identifier("$/unit"),
+        # #    quantity=Identifier("quantity"),
+        # #)
+    # }
+    # groups = ( 'price_sum', 'quantity_sum', 'price_min', 'price_max')
+    # ret = SQL('\n').join([
+        # SQL('').join([
+            # SQL("SELECT"
+                # "\n  "),
+            # SQL(','
+                # "\n  ").join([Identifier(f'{k}') for k in groups ])
+            # ])
+        # SQL('').join([
+            # SQL("SELECT"
+                # "\n  "),
+            # SQL(','
+                # "\n  ").join([Identifier(f'{k}') for k in select ])
+            # ])
+        # SQL('ORDER BY {_id} DESC').format(_id=Identifier('transactions','id')),
+        # ])
+    #SELECT product, organic, store, unit, TRUNC("$/unit",2) AS "$/unit", TRUNC(price_sum/quantity_sum, 2) AS "avg", TRUNC(price_min,2) AS "min", TRUNC(price_max,2) AS "max", date 
+#FROM  (
+#sub
+#) AS subq
+#WHERE product = 'Apples'
+#GROUP BY organic, product, unit, "$/unit", price_sum, quantity_sum, price_min, price_max, date, store ;

+ 13 - 0
txn_view.py

@@ -15,6 +15,19 @@ from collections import (
     OrderedDict,
 )
 
+def get_table_statement(alias):
+    tables = {
+        'product': 'products',
+        'category': 'categories',
+        'group': 'groups',
+        'unit': 'store',
+    }
+    return SQL("SELECT {column} AS {alias} FROM {table}").format(
+        column=Identifier(tables[alias], 'name'),
+        table=Identifier(tables[alias]),
+        alias=Identifier(alias),
+    )
+
 def get_transactions_statement():
     return SQL("SELECT * FROM transaction_view")