grocery_transactions.py 7.0 KB

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