gcconv.py 7.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. # coding=utf-8
  2. """Convert Bank statement CSV to Gnucash-acceptable format"""
  3. def valid_formats():
  4. return _parsers.keys()
  5. def convert_file(fpath, fmt, encoding='utf-8'):
  6. try:
  7. parser_class = _parsers[fmt]
  8. except KeyError:
  9. raise ValueError('unsupported format: %s' % fmt)
  10. else:
  11. parser = parser_class()
  12. parser.parse_file(fpath, encoding)
  13. return parser.format_csv()
  14. def _unquote(value):
  15. if len(value) < 2:
  16. return value
  17. if value[0] == value[-1] == "'":
  18. return value[1:-1]
  19. if value[0] == value[-1] == '"':
  20. return value[1:-1]
  21. return value
  22. class BaseTransaction(object):
  23. """A CSV-imported transaction"""
  24. pass
  25. class GCTransaction(object):
  26. """A Gnucash-acceptable transaction"""
  27. def __init__(self, date, deposit, withdrawal, description):
  28. self.date = date
  29. self.deposit = deposit
  30. self.withdrawal = withdrawal
  31. self.description = description
  32. def __str__(self):
  33. """Naive CSV line printer"""
  34. values = [self.date, self.deposit, self.withdrawal, self.description]
  35. return ";".join(['"%s"' % v for v in values])
  36. class BaseTransactionParser(object):
  37. """A statement parser"""
  38. name = "__NONE__"
  39. def __init__(self):
  40. self.transactions = []
  41. self.state = False
  42. self.next_state = False
  43. def is_open(self):
  44. return self.state
  45. def is_closed(self):
  46. return not self.state
  47. def parse_file(self, fpath, encoding='utf-8'):
  48. lines_read = 0
  49. with open(fpath) as f:
  50. for line in f.readlines():
  51. line = line.decode(encoding).strip()
  52. lines_read += 1
  53. self.state = self.pick_state(line, lineno=lines_read)
  54. if self.is_open():
  55. self.transactions.append(self.parse_line(line).to_gc())
  56. def format_csv(self):
  57. return "\n".join([
  58. unicode(t).encode('utf-8')
  59. for t in self.transactions
  60. ])
  61. class MbankTransaction(BaseTransaction):
  62. """Item in an Mbank CSV"""
  63. # (a) Datum uskutečnění transakce
  64. # (b) Datum zaúčtování transakce
  65. # (c) Popis transakce
  66. # (d) Zpráva pro příjemce
  67. # (e) Plátce/Příjemce
  68. # (f) Číslo účtu plátce/příjemce
  69. # (g) KS
  70. # (h) VS
  71. # (i) SS
  72. # (j) Částka transakce
  73. # (k) Účetní zůstatek po transakci
  74. # (l) (empty column at the end)
  75. def __init__(self, line):
  76. fields = [self._cleanup_field(f) for f in line.split(";")]
  77. self.date_r = self._convert_date(fields.pop(0)) # (a)
  78. self.date_b = self._convert_date(fields.pop(0)) # (b)
  79. __ = fields.pop() # (l)
  80. self.new_balance = self._parse_currency(fields.pop()) # (k)
  81. amount = self._parse_currency(fields.pop()) # (j)
  82. if amount >= 0:
  83. self.amountd = amount
  84. self.amountw = 0
  85. else:
  86. self.amountd = 0
  87. self.amountw = -amount
  88. self._scrap = fields # (c-i)
  89. def _cleanup_field(self, field):
  90. x = ' '.join(_unquote(field).strip().split())
  91. return '' if x == '-' else x
  92. def _convert_date(self, text):
  93. day, month, year = text.split('-')
  94. return '-'.join([year, month, day])
  95. def _description(self):
  96. type_, message, party, number, ks, vs, ss = self._scrap
  97. dpt = u' DATUM PROVEDENÍ TRANSAKCE: '
  98. if dpt in message:
  99. message, __ = message.split(dpt)
  100. out = []
  101. if type_:
  102. out.append(type_)
  103. if message:
  104. out.append(message)
  105. if party:
  106. out.append(party)
  107. return " / ".join(out)
  108. def _parse_currency(self, text):
  109. """Read Mbank currency format"""
  110. num = text.replace(' ', '')
  111. num = num.replace(',', '.')
  112. return float(num)
  113. def to_gc(self):
  114. """Convert to GCTransaction"""
  115. return GCTransaction(
  116. date=self.date_r,
  117. deposit=self.amountd,
  118. withdrawal=self.amountw,
  119. description=self._description()
  120. )
  121. class MbankTransactionParser(BaseTransactionParser):
  122. def parse_line(self, line):
  123. return MbankTransaction(line)
  124. def pick_state(self, line, lineno=None):
  125. """Choose parser state according to current line and line no."""
  126. if self.next_state:
  127. self.next_state = False
  128. return True
  129. if self.is_closed() and line.startswith(u'#Datum uskute'):
  130. self.next_state = True
  131. return False
  132. if self.is_open() and not line:
  133. return False
  134. return self.state
  135. class FioTransaction(BaseTransaction):
  136. # ID operace
  137. # Datum
  138. # Objem
  139. # Měna
  140. # Protiúčet
  141. # Název protiúčtu
  142. # Kód banky
  143. # Název banky
  144. # KS
  145. # VS
  146. # SS
  147. # Poznámka
  148. # Zpráva pro příjemce
  149. # Typ
  150. # Provedl
  151. # Upřesnění
  152. # Poznámka
  153. # BIC
  154. # ID pokynu
  155. def __init__(self, line):
  156. fields = [self._cleanup_field(f) for f in line.split(";")]
  157. __ = fields.pop(0)
  158. self.date = self._convert_date(fields.pop(0))
  159. amount = self._parse_currency(fields.pop(0))
  160. if amount >= 0:
  161. self.amountd = amount
  162. self.amountw = 0
  163. else:
  164. self.amountd = 0
  165. self.amountw = -amount
  166. self.currency = fields.pop(0)
  167. assert self.currency == 'CZK'
  168. self._scrap = [self._cleanup_field(f) for f in fields]
  169. def _cleanup_field(self, field):
  170. x = ' '.join(_unquote(field).strip().split())
  171. return '' if x == '-' else x
  172. def _description(self):
  173. (
  174. party_acc,
  175. party_accnname,
  176. party_bankcode,
  177. party_bankname,
  178. ks,
  179. vs,
  180. ss,
  181. note1,
  182. message,
  183. type_,
  184. who,
  185. explanation,
  186. note2,
  187. bic,
  188. comm_id
  189. ) = self._scrap
  190. out = []
  191. if message:
  192. out.append(message)
  193. return " / ".join(out)
  194. def _convert_date(self, text):
  195. day, month, year = text.split('.')
  196. return '-'.join([year, month, day])
  197. def _parse_currency(self, text):
  198. return float(text)
  199. def to_gc(self):
  200. """Convert to GCTransaction"""
  201. return GCTransaction(
  202. date=self.date,
  203. deposit=self.amountd,
  204. withdrawal=self.amountw,
  205. description=self._description()
  206. )
  207. class FioTransactionParser(BaseTransactionParser):
  208. def parse_line(self, line):
  209. return FioTransaction(line)
  210. def pick_state(self, line, lineno=None):
  211. """Choose parser state according to current line and line no."""
  212. if self.next_state:
  213. self.next_state = False
  214. return True
  215. if self.is_closed() and line.startswith(u'"ID operace";"Datum";'):
  216. self.next_state = True
  217. return False
  218. return self.state
  219. _parsers = {
  220. 'CzMbank': MbankTransactionParser,
  221. 'CzFio': FioTransactionParser,
  222. }