reconcile.py 5.7 KB

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