reconcile.py 5.6 KB

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