gcconv.py 7.2KB

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