grocery_transactions.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. #!/usr/bin/python3
  2. #
  3. # Copyright (c) Daniel Sheffield 2021 - 2022
  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 psycopg
  30. user = os.getenv('USER')
  31. conn: Cursor = psycopg.connect(f"{host} dbname=grocery 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', 'black', 'light red'),
  38. ('popup', 'black', 'dark red'),
  39. ('banner', 'light gray', 'dark red'),
  40. ('streak', 'light red', 'black'),
  41. ('bg', 'light red', 'black'),
  42. ]
  43. light_palette = [
  44. ('popup_focus', 'black', 'light gray'),
  45. ('popup', 'dark gray', 'white'),
  46. ('banner', 'light gray', 'dark red'),
  47. ('streak', 'white', 'dark blue'),
  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 _insert_new_product_callback(activity_manager, query_manager, product, category, group):
  73. activity_manager.app.log.write(
  74. '{};\n'.format(get_insert_product_statement(product, category, group)))
  75. query_manager.insert_new_product(product, category, group)
  76. activity_manager.show(activity_manager.get('transaction').update())
  77. def _new_product_callback(
  78. activity_manager: ActivityManager,
  79. query_manager: QueryManager,
  80. name: str, data: dict
  81. ):
  82. cur = activity_manager.current()
  83. txn : TransactionEditor = activity_manager.get('transaction')
  84. activity_manager.show(
  85. activity_manager.create(NewProduct, 'new_product',
  86. activity_manager, query_manager,
  87. cur, name, txn.data,
  88. lambda w, t, data: _autocomplete_callback(activity_manager, query_manager, w, t, data),
  89. txn.apply_changes,
  90. lambda product, category, group: _insert_new_product_callback(
  91. activity_manager, query_manager, product, category, group),
  92. lambda: activity_manager.show(cur.update()),
  93. )
  94. )
  95. def _autocomplete_callback(
  96. activity_manager: ActivityManager,
  97. query_manager: QueryManager,
  98. widget: Union[AutoCompleteEdit, AutoCompleteFloatEdit],
  99. name: str, data: dict
  100. ):
  101. options = query_manager.unique_suggestions(name, **data)
  102. if len(options) > 0:
  103. widget._emit('open', options)
  104. elif len(options) == 0 and activity_manager.current() is not activity_manager.get('new_product'):
  105. if name in ('product', 'category', 'group'):
  106. _new_product_callback(activity_manager, query_manager, name, data)
  107. def _save_and_clear_callback(activity_manager):
  108. txn = activity_manager.get('transaction')
  109. activity_manager.app.save(txn.data)
  110. txn.clear()
  111. txn.focus_on_product()
  112. activity_manager.show(txn.update())
  113. args = sys.argv
  114. log = args[1]
  115. class GroceryTransactionEditor(urwid.WidgetPlaceholder):
  116. def __init__(self, activity_manager, cur, log):
  117. super().__init__(urwid.SolidFill(u'/'))
  118. self.activity_manager = activity_manager
  119. self.cur = cur
  120. txn: TransactionEditor = self.activity_manager.get('transaction')
  121. with open(log, 'r') as f:
  122. date = None
  123. store = None
  124. for line in f.readlines():
  125. if date is None and store is None:
  126. if '$store$' in line:
  127. date, store, _= line.split('$store$')
  128. date = date.split("'")[1]
  129. else:
  130. assert None not in (date, store,), \
  131. "Both date and store should be set or neither should be set"
  132. if '$store$' in line:
  133. assert date in line and f'$store${store}$store$' in line, \
  134. "Date ({date}) and store ({store}) not found in {line}."\
  135. " Mixing transactions from different dates and stores is not supported"
  136. #print(self.cur.mogrify(line))
  137. #input()
  138. self.cur.execute(line)
  139. if None not in (date, store):
  140. txn.apply_choice('ts', date)
  141. txn.apply_choice('store', store)
  142. self.activity_manager.show(self)
  143. self.activity_manager.show(txn.update())
  144. self.log = self.open(log)
  145. def _open(self, log):
  146. with open(log, 'a') as f:
  147. yield f
  148. def open(self, log):
  149. self._to_close = self._open(log)
  150. return next(self._to_close)
  151. def close(self):
  152. if self._to_close is not None:
  153. self._to_close.close()
  154. self._to_close = None
  155. def save(self, data):
  156. ts = data['ts']
  157. store = data['store']
  158. description = data['description']
  159. quantity = data['quantity']
  160. unit = data['unit']
  161. price = data['price']
  162. product = data['product']
  163. organic = 'true' if data['organic'] else 'false'
  164. statement = \
  165. f"CALL insert_transaction('{ts}', $store${store}$store$, " \
  166. f"$descr${description}$descr$, {quantity}, $unit${unit}$unit$, " \
  167. f"{price}, $produ${product}$produ$, {organic});\n"
  168. self.log.write(statement)
  169. self.cur.execute(statement)
  170. cur.execute("BEGIN")
  171. activity_manager = ActivityManager()
  172. query_manager = QueryManager(cur, display_mapper)
  173. activity_manager.create(TransactionEditor, 'transaction',
  174. activity_manager, query_manager,
  175. cols, grid_layout, side_pane, bottom_pane,
  176. lambda: _save_and_clear_callback(activity_manager),
  177. lambda widget, name, data: _autocomplete_callback(activity_manager, query_manager, widget, name, data),
  178. )
  179. app = None
  180. def iter_palettes():
  181. palettes = [dark_palette, light_palette]
  182. while True:
  183. p = palettes.pop(0)
  184. palettes.append(p)
  185. yield p
  186. palettes = iter_palettes()
  187. palette = next(palettes)
  188. try:
  189. app = GroceryTransactionEditor(activity_manager, cur, log)
  190. screen = raw_display.Screen()
  191. loop = urwid.MainLoop(app, palette, screen=screen,
  192. unhandled_input=lambda k: show_or_exit(k, screen=screen, palettes=palettes),
  193. pop_ups=True
  194. )
  195. loop.run()
  196. finally:
  197. if app is not None:
  198. app.close()
  199. cur.close()
  200. conn.close()