grocery_transactions.py 7.7 KB

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