|
@@ -8,116 +8,13 @@
|
|
|
import pandas as pd
|
|
|
from txn_view import get_statement as get_session_transactions_statement
|
|
|
from dateutil.parser import parse as parse_time
|
|
|
-from datetime import datetime
|
|
|
+from widgets import (
|
|
|
+ NoTabCheckBox,
|
|
|
+ AutoCompleteEdit,
|
|
|
+ AutoCompleteFloatEdit,
|
|
|
+)
|
|
|
import sys
|
|
|
-try:
|
|
|
- import psycopg2
|
|
|
- from psycopg2.sql import SQL
|
|
|
- from db_utils import cursor_as_dict
|
|
|
- MOCK = False
|
|
|
-except:
|
|
|
- from faker import Faker
|
|
|
- def cursor_as_dict(cur):
|
|
|
- yield from cur.fetchall()
|
|
|
-
|
|
|
- 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 itertools
|
|
@@ -125,26 +22,6 @@ from collections import (
|
|
|
OrderedDict,
|
|
|
)
|
|
|
|
|
|
-COPYRIGHT = "Copyright (c) Daniel Sheffield 2021"
|
|
|
-
|
|
|
-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 (i for i in self.records)
|
|
|
-
|
|
|
- def close(self):
|
|
|
- pass
|
|
|
-
|
|
|
try:
|
|
|
from db_credentials import HOST, PASSWORD
|
|
|
host = f'host={HOST}'
|
|
@@ -153,64 +30,18 @@ except:
|
|
|
host = ''
|
|
|
password = ''
|
|
|
|
|
|
-if MOCK:
|
|
|
- records = [{
|
|
|
- 'ts': datetime(2021, 8, 29, 14, 8, 0, 0),
|
|
|
- 'id': 2,
|
|
|
- 'code': fake.store()[:4],
|
|
|
- 'store': 'Countdown',
|
|
|
- 'price': 4.30,
|
|
|
- '$/unit': 4.3/250.0,
|
|
|
- 'quantity': 250.0,
|
|
|
- 'unit': 'g',
|
|
|
- 'organic': False,
|
|
|
- 'total': 0,
|
|
|
- '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(2021, 8, 29, 14, 8, 0, 0),
|
|
|
- 'id': 3,
|
|
|
- 'code': fake.store()[:4],
|
|
|
- 'store': fake.store(),
|
|
|
- 'price': price,
|
|
|
- '$/unit': price/quantity,
|
|
|
- 'quantity': quantity,
|
|
|
- 'unit': unit,
|
|
|
- 'organic': organic,
|
|
|
- 'total': 0,
|
|
|
- 'description': ' '.join([
|
|
|
- product[0],
|
|
|
- *fake.description(ext_word_list=words).split()
|
|
|
- ]),
|
|
|
- 'product': product[0],
|
|
|
- 'group': product[2],
|
|
|
- 'category': product[1],
|
|
|
- })
|
|
|
-
|
|
|
- conn = mock_conn()
|
|
|
- cur = mock_cur(records)
|
|
|
-else:
|
|
|
+try:
|
|
|
+ raise Exception
|
|
|
+ from psycopg2.sql import SQL
|
|
|
+ from db_utils import cursor_as_dict
|
|
|
import os
|
|
|
user = os.getenv('USER')
|
|
|
conn = psycopg2.connect(f"{host} dbname=das user={user} {password}")
|
|
|
cur = conn.cursor()
|
|
|
+except:
|
|
|
+ from mock import *
|
|
|
+
|
|
|
+COPYRIGHT = "Copyright (c) Daniel Sheffield 2021"
|
|
|
|
|
|
palette = [
|
|
|
('banner', 'light gray', 'dark red'),
|
|
@@ -242,8 +73,6 @@ cols = [
|
|
|
)
|
|
|
]
|
|
|
|
|
|
-#cols.remove(None)
|
|
|
-
|
|
|
NON_IDENTIFIER_COLUMNS = [
|
|
|
'ts',
|
|
|
'store',
|
|
@@ -332,130 +161,6 @@ def interleave(_list, div):
|
|
|
yield element
|
|
|
yield div
|
|
|
|
|
|
-class AutoCompleteEdit(urwid.Edit):
|
|
|
- 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' ' if title.lower() == 'unit' else u'')
|
|
|
- else:
|
|
|
- self.name = name
|
|
|
- title = name.title()
|
|
|
- passthrough = u' ' if name.lower() == 'unit' else u''
|
|
|
-
|
|
|
- super(AutoCompleteEdit, self).__init__(passthrough, *args, **kwargs)
|
|
|
- self.apply = apply_change_func
|
|
|
-
|
|
|
- def keypress(self, 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)
|
|
|
-
|
|
|
-
|
|
|
-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, self.name = name
|
|
|
- title = self.name.title()
|
|
|
- passthrough = (self.pallete, f'{self.op} ')
|
|
|
- else:
|
|
|
- self.name = name
|
|
|
- title = name.title()
|
|
|
- passthrough = title
|
|
|
-
|
|
|
- super(AutoCompleteFloatEdit, self).__init__(passthrough, *args, **kwargs)
|
|
|
- self.apply = apply_change_func
|
|
|
-
|
|
|
- def update_caption(self):
|
|
|
- if self.pallete is not None:
|
|
|
- self.set_caption((self.pallete, f'{self.op} '))
|
|
|
- else:
|
|
|
- self.set_caption(f'{self.op} ')
|
|
|
-
|
|
|
- def set_op(self, op):
|
|
|
- self.op = op
|
|
|
- self.last_val = self.value()
|
|
|
- self.set_edit_text('')
|
|
|
- self.update_caption()
|
|
|
-
|
|
|
- def calc(self):
|
|
|
- x = self.last_val
|
|
|
- op = self.op
|
|
|
- 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()
|
|
|
- self.set_edit_text(f'{z:.2f}')
|
|
|
-
|
|
|
- def keypress(self, size, key):
|
|
|
- if isinstance(key, tuple):
|
|
|
- return
|
|
|
- ops = ('+', '-', '*', '/',)
|
|
|
-
|
|
|
- if key in ops:
|
|
|
- if self.op in ops:
|
|
|
- self.calc()
|
|
|
- self.set_op(key)
|
|
|
- return
|
|
|
- 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 == 'delete':
|
|
|
- self.set_edit_text('')
|
|
|
- self.op = '='
|
|
|
- self.update_caption()
|
|
|
- return
|
|
|
-
|
|
|
- return super(AutoCompleteFloatEdit, self).keypress(size, key)
|
|
|
-
|
|
|
-class NoTabCheckBox(urwid.CheckBox):
|
|
|
- def keypress(self, size, key):
|
|
|
- if not isinstance(key, tuple) and key == 'tab':
|
|
|
- return
|
|
|
- else:
|
|
|
- return super(NoTabCheckBox, self).keypress(size, key)
|
|
|
-
|
|
|
-def _set_focus_path(container, path):
|
|
|
- try:
|
|
|
- container.set_focus_path(path)
|
|
|
- return
|
|
|
- except IndexError:
|
|
|
- pass
|
|
|
-
|
|
|
- if path[-1] == 0 and len(path) > 1:
|
|
|
- _set_focus_path(container, path[:-1])
|
|
|
- return
|
|
|
-
|
|
|
- raise IndexError
|
|
|
-
|
|
|
class GroceryTransactionEditor(urwid.WidgetPlaceholder):
|
|
|
def __init__(self, log, fields):
|
|
|
super(GroceryTransactionEditor, self).__init__(urwid.SolidFill(u'/'))
|