grocery_transactions.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. #!/usr/bin/python3
  2. #
  3. # Copyright (c) Daniel Sheffield 2021
  4. #
  5. # All rights reserved
  6. #
  7. # THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
  8. import itertools
  9. import sys
  10. from sqlite3 import Cursor
  11. from typing import Union
  12. import urwid
  13. from urwid import raw_display
  14. from app.activities import ActivityManager, show_or_exit
  15. from app.activities.NewProduct import NewProduct
  16. from app.activities.TransactionEditor import TransactionEditor
  17. from app.db_utils import (QueryManager, display_mapper,
  18. get_insert_product_statement)
  19. from app.widgets import AutoCompleteEdit, AutoCompleteFloatEdit
  20. try:
  21. from db_credentials import HOST, PASSWORD
  22. host = f'host={HOST}'
  23. password = f'password={PASSWORD}'
  24. except:
  25. host = ''
  26. password = ''
  27. try:
  28. import os
  29. import psycopg2
  30. user = os.getenv('USER')
  31. conn: Cursor = psycopg2.connect(f"{host} dbname=das user={user} {password}")
  32. cur = conn.cursor()
  33. except:
  34. print('Failed to set up db connection. Entering Mock mode')
  35. from mock import *
  36. dark_palette = [
  37. ("popup_focus", "light red", "light gray"),
  38. ('popup', 'light red', 'dark gray'),
  39. ('banner', 'light gray', 'dark red'),
  40. ('streak', 'light red', 'dark gray'),
  41. ('bg', 'light red', 'black'),
  42. ]
  43. light_palette = [
  44. ("popup_focus", "dark blue", "light gray"),
  45. ('popup', 'light blue', 'dark gray'),
  46. ('banner', 'light gray', 'dark red'),
  47. ('streak', 'white', 'dark gray'),
  48. ('bg', 'white', 'dark blue'),
  49. ]
  50. grid_layout = [
  51. [ 'ts', 'store', ],
  52. [ 'organic', 'product', ],
  53. [ 'category', 'group', ],
  54. ]
  55. side_pane = [
  56. 'unit',
  57. 'quantity',
  58. 'price',
  59. ]
  60. bottom_pane = [
  61. 'description',
  62. 'dbview',
  63. ]
  64. cols = [
  65. c for c in filter(
  66. lambda x: x is not None,
  67. itertools.chain(
  68. *grid_layout, side_pane, set(bottom_pane) - set(['dbview'])
  69. )
  70. )
  71. ]
  72. def _apply_choice_callback(
  73. activity_manager: ActivityManager,
  74. base: str, name: str, value: str
  75. ):
  76. base = activity_manager.get(base)
  77. base.apply_choice(name, value)
  78. activity_manager.show(base.update())
  79. def _insert_new_product_callback(activity_manager, query_manager, product, category, group):
  80. activity_manager.app.log.write(
  81. '{};\n'.format(get_insert_product_statement(product, category, group)))
  82. query_manager.insert_new_product(product, category, group)
  83. activity_manager.show(activity_manager.get('transaction').update())
  84. def _new_product_callback(
  85. activity_manager: ActivityManager,
  86. query_manager: QueryManager,
  87. name: str, data: dict
  88. ):
  89. cur = activity_manager.current()
  90. txn : TransactionEditor = activity_manager.get('transaction')
  91. activity_manager.show(
  92. activity_manager.create(NewProduct, 'new_product',
  93. query_manager, cur, name, txn.data,
  94. lambda w, t, data: _autocomplete_callback(activity_manager, query_manager, w, t, data),
  95. txn.apply_changes,
  96. lambda product, category, group: _insert_new_product_callback(
  97. activity_manager, query_manager, product, category, group),
  98. lambda: activity_manager.show(cur.update()),
  99. lambda name, value: _apply_choice_callback(activity_manager, 'new_product', name, value)
  100. )
  101. )
  102. def _autocomplete_callback(
  103. activity_manager: ActivityManager,
  104. query_manager: QueryManager,
  105. widget: Union[AutoCompleteEdit, AutoCompleteFloatEdit],
  106. name: str, data: dict
  107. ):
  108. options = query_manager.unique_suggestions(name, **data)
  109. if len(options) > 0:
  110. widget._emit('open', options)
  111. elif len(options) == 0 and activity_manager.current() is not activity_manager.get('new_product'):
  112. if name in ('product', 'category', 'group'):
  113. _new_product_callback(activity_manager, query_manager, name, data)
  114. def _save_and_clear_callback(activity_manager):
  115. txn = activity_manager.get('transaction')
  116. activity_manager.app.save(txn.data)
  117. txn.clear()
  118. txn.focus_on_product()
  119. activity_manager.show(txn.update())
  120. args = sys.argv
  121. log = args[1]
  122. class GroceryTransactionEditor(urwid.WidgetPlaceholder):
  123. def __init__(self, activity_manager, cur, log):
  124. super().__init__(urwid.SolidFill(u'/'))
  125. self.activity_manager = activity_manager
  126. self.cur = cur
  127. txn: TransactionEditor = self.activity_manager.get('transaction')
  128. with open(log, 'r') as f:
  129. date = None
  130. store = None
  131. for line in f.readlines():
  132. if date is None and store is None:
  133. if '$store$' in line:
  134. date, store, _= line.split('$store$')
  135. date = date.split("'")[1]
  136. else:
  137. assert None not in (date, store,), \
  138. "Both date and store should be set or neither should be set"
  139. if '$store$' in line:
  140. assert date in line and f'$store${store}$store$' in line, \
  141. "Date ({date}) and store ({store}) not found in {line}."\
  142. " Mixing transactions from different dates and stores is not supported"
  143. #print(self.cur.mogrify(line))
  144. #input()
  145. self.cur.execute(line)
  146. if None not in (date, store):
  147. txn.apply_choice('ts', date)
  148. txn.apply_choice('store', store)
  149. self.activity_manager.show(self)
  150. self.activity_manager.show(txn.update())
  151. self.log = self.open(log)
  152. def _open(self, log):
  153. with open(log, 'a') as f:
  154. yield f
  155. def open(self, log):
  156. self._to_close = self._open(log)
  157. return next(self._to_close)
  158. def close(self):
  159. if self._to_close is not None:
  160. self._to_close.close()
  161. self._to_close = None
  162. def save(self, data):
  163. ts = data['ts']
  164. store = data['store']
  165. description = data['description']
  166. quantity = data['quantity']
  167. unit = data['unit']
  168. price = data['price']
  169. product = data['product']
  170. organic = data['organic'] if data['organic'] else 'false'
  171. statement = \
  172. f"CALL insert_transaction('{ts}', $store${store}$store$, " \
  173. f"$descr${description}$descr$, {quantity}, $unit${unit}$unit$, " \
  174. f"{price}, $produ${product}$produ$, {organic});\n"
  175. self.log.write(statement)
  176. self.cur.execute(statement)
  177. cur.execute("BEGIN")
  178. activity_manager = ActivityManager()
  179. query_manager = QueryManager(cur, display_mapper)
  180. activity_manager.create(TransactionEditor, 'transaction',
  181. query_manager, cols, grid_layout, side_pane, bottom_pane,
  182. lambda: _save_and_clear_callback(activity_manager),
  183. lambda widget, name, data: _autocomplete_callback(activity_manager, query_manager, widget, name, data),
  184. lambda name, value: _apply_choice_callback(activity_manager, 'transaction', name, value),
  185. )
  186. app = None
  187. def iter_palettes():
  188. palettes = [dark_palette, light_palette]
  189. while True:
  190. p = palettes.pop(0)
  191. palettes.append(p)
  192. yield p
  193. palettes = iter_palettes()
  194. palette = next(palettes)
  195. try:
  196. app = GroceryTransactionEditor(activity_manager, cur, log)
  197. screen = raw_display.Screen()
  198. loop = urwid.MainLoop(app, palette, screen=screen,
  199. unhandled_input=lambda k: show_or_exit(k, screen=screen, palettes=palettes),
  200. pop_ups=True
  201. )
  202. loop.run()
  203. finally:
  204. if app is not None:
  205. app.close()
  206. cur.close()
  207. conn.close()