reconcile.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. #!/usr/bin/python3
  2. #
  3. # Copyright (c) Daniel Sheffield 2021 - 2024
  4. # Copyright (c) Shannon Sheffield 2021 - 2024
  5. #
  6. # All rights reserved
  7. #
  8. # THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
  9. #
  10. import sys
  11. if sys.argv[1] == '-h':
  12. print("""
  13. usage: ./reconcile.py ~/gnucash/merged.gnucash 2021-11-01 2021-12-10 1 Alcohol Sundries CatFood Cleaning
  14. """)
  15. sys.exit(0)
  16. from datetime import datetime, timedelta
  17. from dateutil.parser import parse as parse_time
  18. import itertools
  19. import gnucash
  20. import os
  21. import psycopg
  22. from app.data.QueryManager import cursor_as_dict
  23. from app.data.TransactionView import get_session_transactions_statement as get_statement
  24. try:
  25. from db_credentials import HOST, PASSWORD
  26. host = f'host={HOST}'
  27. password = f'password={PASSWORD}'
  28. except:
  29. host = ''
  30. password = ''
  31. STORE_CODES = {
  32. 'WOOLWORTHS' : 'CD',
  33. 'countdown': 'CD',
  34. 'pak n save': 'PnS',
  35. 'SEAFOOD BAZAAR': 'SB',
  36. 'GORDONTON FARM SHOP': 'GFS',
  37. 'THE FARM SHOP': 'GFS',
  38. 'THE ORGANIC FOOD SHOP': 'TOFS',
  39. 'ORGANICFOOD': 'TOFS',
  40. 'TOFS' : 'TOFS',
  41. 'WHATAWHATA BERRY FARM': 'Farm',
  42. 'TAUPIRI DAIRY TAUPIRI': 'TD',
  43. 'Dreamview Cre': 'DV',
  44. 'new world': 'NW',
  45. 'BIN INN': 'BI',
  46. 'NEW SAVE': 'NS',
  47. 'ORGANIC NATION' : 'ON',
  48. 'The Warehouse' : 'WH',
  49. 'Goodearth' : 'CO',
  50. 'REDUCED' : 'RTC',
  51. 'Summerglow' : 'SG',
  52. 'IHERB' : 'iHerb',
  53. 'Health Post' : 'HP',
  54. 'Scotsburn' : 'SF',
  55. 'Fruit King' : 'FK',
  56. 'Fresh Choice': 'FC',
  57. 'FC12-3189-0013895-00': 'DV',
  58. }
  59. user = os.getenv('USER')
  60. conn = psycopg.connect(f"{host} dbname=grocery user={user} {password}")
  61. cur = conn.cursor()
  62. output = []
  63. def get_record_from_database(date, store, tags):
  64. cur.execute(get_statement(date, store))
  65. #print(cur.mogrify(get_statement(date, store)))
  66. return sum([
  67. row['price'] for row in cursor_as_dict(cur) if not (
  68. tags & set(row['tags'] or [])
  69. )
  70. ])
  71. def get_store_code(store, value, user_data=dict()):
  72. for k, v in STORE_CODES.items():
  73. if k.lower() in store.lower() or k.lower().replace(' ', '') in store.lower():
  74. return v
  75. if store not in user_data:
  76. user_data[store] = input(f"Enter code for {store} ({int(value)/100:>6.2f}): ")
  77. return user_data[store]
  78. def _unwrap_list(root, blacklist):
  79. for i in map(lambda x: [
  80. (':'.join([x[0], c.name]), True) for c in root.get_children()
  81. ] if x[1] else [ x ], blacklist):
  82. for j in i: yield j
  83. def _accounts(root, path, blacklist, whitelist):
  84. #print('blacklist1: {}'.format(blacklist))
  85. #print(f'whitelist1 ({root.name}: {whitelist}')
  86. if root.name in [ w[0] for w in whitelist ]:
  87. if root.name not in [ b[0] for b in blacklist ]:
  88. yield path, root
  89. blacklist = [b for b in itertools.chain(
  90. _unwrap_list(root, filter(lambda x: (x[0] == root.name), blacklist)),
  91. filter(lambda x: not (x[0] == root.name), blacklist)
  92. )]
  93. whitelist = [b for b in itertools.chain(
  94. _unwrap_list(root, filter(lambda x: (x[0] == root.name), whitelist)),
  95. filter(lambda x: not (x[0] == root.name), whitelist)
  96. )]
  97. path = [*path, root.name]
  98. prefix = '{}:'.format(root.name)
  99. #print('root: {}'.format(root.name))
  100. #print('prefix: {}'.format(prefix))
  101. #print('path: {}'.format(':'.join(path)))
  102. blacklist = [
  103. b for b in map(
  104. lambda x: (x[0][len(prefix):], x[1]), filter(
  105. lambda x: x[0].startswith(prefix), blacklist
  106. )
  107. )
  108. ]
  109. whitelist = [
  110. b for b in map(
  111. lambda x: (x[0][len(prefix):], x[1]), filter(
  112. lambda x: x[0].startswith(prefix), whitelist
  113. )
  114. )
  115. ]
  116. #print('blacklist: {}'.format(blacklist))
  117. for a in root.get_children():
  118. yield from _accounts(a, path, blacklist, whitelist)
  119. def _path(account):
  120. if account.get_parent() is not None: return ':'.join([_path(account.get_parent()), account.name])
  121. else: return account.name
  122. def _iter(root, blacklist, whitelist, _from, _to):
  123. for p, a in _accounts(root, [], blacklist, whitelist):
  124. apath = _path(a)
  125. for t in a.GetSplitList():
  126. tpath = _path(t.account)
  127. ts = t.parent.GetDate()
  128. if datetime.fromtimestamp(_to) > ts >= datetime.fromtimestamp(_from):
  129. #if apath.startswith('Root Account:Trading'):
  130. # if tpath.startswith('Root Account:Trading:CURRENCY'):
  131. # s+=t.GetValue().num()
  132. #elif tpath == apath:
  133. if tpath == apath:
  134. #print(t.parent.GetDescription())
  135. ts = ts.replace(hour=0, minute=0)
  136. yield p, a.name, t.GetValue().num(), t.parent.GetDescription(), ts
  137. if __name__ == '__main__':
  138. args = sys.argv
  139. session = gnucash.Session(args[1])
  140. tags = set()
  141. if len(args) > 5:
  142. for arg in args[5:]:
  143. tags.add(arg)
  144. try:
  145. blacklist = [
  146. # name, also_children
  147. ('Root Account:Assets', True),
  148. ('Root Account:Liabilities', True),
  149. ('Root Account:Equity', True),
  150. ('Root Account:Income', True),
  151. ('Root Account:Expenses:NZD:Food:Groceries:Fee', True),
  152. ('Root Account:Expenses:NZD:Food:Groceries:Rebates', True),
  153. ]
  154. whitelist = [
  155. # name, also_children
  156. ('Root Account:Expenses:NZD:Food:Groceries', True),
  157. ]
  158. _from = int(parse_time(args[2]).timestamp())# 1554030000 # 1 April 2019 as seconds since epoch
  159. _to = int(parse_time(args[3]).timestamp())
  160. _interval = int(args[4])*60*60*24
  161. ranges = dict([
  162. [
  163. el for el in map(lambda x: datetime.fromtimestamp(x), (i, i+_interval))
  164. ] for i in range(_from, _to, _interval)
  165. ])
  166. tot = dict()
  167. for p,n,value,desc,d in sorted(_iter(session.book.get_root_account(), blacklist, whitelist, _from, _to), key=lambda x: x[0]):
  168. account_name = ':'.join([*p, n])
  169. f = next(( f for (f, t) in ranges.items() if f <= d < t ))
  170. tot.update({
  171. f: value if f not in tot else value+tot[f]
  172. })
  173. ts = d + timedelta(days=1)
  174. store = get_store_code(desc, value)
  175. book = None
  176. while d + timedelta(days=2) > ts > d - timedelta(days=3):
  177. book = get_record_from_database(ts, store, tags)
  178. book = int(book*100)
  179. if book == int(value):
  180. break;
  181. ts = ts - timedelta(days=1)
  182. book = get_record_from_database(ts, store, tags)
  183. dbentry = f'{ts} {store:5s}' if book == int(value) else ''
  184. output.append(f'{d} {desc:35s} | {value/100:>6.2f} | {dbentry}')
  185. finally:
  186. session.end()
  187. for line in output:
  188. print(line)