gcconv.py 7.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  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. # Datum uskutečnění transakce
  64. # Datum zaúčtování transakce
  65. # Popis transakce
  66. # Zpráva pro příjemce
  67. # Plátce/Příjemce
  68. # Číslo účtu plátce/příjemce
  69. # KS
  70. # VS
  71. # SS
  72. # Částka transakce
  73. # Účetní zůstatek po transakci
  74. def __init__(self, line):
  75. fields = [self._cleanup_field(f) for f in line.split(";")]
  76. self.date_r = self._convert_date(fields.pop(0))
  77. self.date_b = self._convert_date(fields.pop(0))
  78. __ = fields.pop()
  79. self.new_balance = self._parse_currency(fields.pop())
  80. amount = self._parse_currency(fields.pop())
  81. if amount >= 0:
  82. self.amountd = amount
  83. self.amountw = 0
  84. else:
  85. self.amountd = 0
  86. self.amountw = -amount
  87. self._scrap = fields
  88. def _cleanup_field(self, field):
  89. x = ' '.join(_unquote(field).strip().split())
  90. return '' if x == '-' else x
  91. def _convert_date(self, text):
  92. day, month, year = text.split('-')
  93. return '-'.join([year, month, day])
  94. def _description(self):
  95. type_, message, party, number, ks, vs, ss = self._scrap
  96. message = message.replace(u' DATUM PROVEDENÍ TRANSAKCE: ', '; DATUM: ')
  97. out = []
  98. if type_:
  99. out.append(type_)
  100. if message:
  101. out.append(message)
  102. if party:
  103. out.append(party)
  104. return " / ".join(out)
  105. def _parse_currency(self, text):
  106. """Read Mbank currency format"""
  107. num = text.replace(' ', '')
  108. num = num.replace(',', '.')
  109. return float(num)
  110. def to_gc(self):
  111. """Convert to GCTransaction"""
  112. return GCTransaction(
  113. date=self.date_r,
  114. deposit=self.amountd,
  115. withdrawal=self.amountw,
  116. description=self._description()
  117. )
  118. class MbankTransactionParser(BaseTransactionParser):
  119. def parse_line(self, line):
  120. return MbankTransaction(line)
  121. def pick_state(self, line, lineno=None):
  122. """Choose parser state according to current line and line no."""
  123. if self.next_state:
  124. self.next_state = False
  125. return True
  126. if self.is_closed() and line.startswith(u'#Datum uskute'):
  127. self.next_state = True
  128. return False
  129. if self.is_open() and not line:
  130. return False
  131. return self.state
  132. class FioTransaction(BaseTransaction):
  133. # ID operace
  134. # Datum
  135. # Objem
  136. # Měna
  137. # Protiúčet
  138. # Název protiúčtu
  139. # Kód banky
  140. # Název banky
  141. # KS
  142. # VS
  143. # SS
  144. # Poznámka
  145. # Zpráva pro příjemce
  146. # Typ
  147. # Provedl
  148. # Upřesnění
  149. # Poznámka
  150. # BIC
  151. # ID pokynu
  152. def __init__(self, line):
  153. fields = [self._cleanup_field(f) for f in line.split(";")]
  154. __ = fields.pop(0)
  155. self.date = self._convert_date(fields.pop(0))
  156. amount = self._parse_currency(fields.pop(0))
  157. if amount >= 0:
  158. self.amountd = amount
  159. self.amountw = 0
  160. else:
  161. self.amountd = 0
  162. self.amountw = -amount
  163. self.currency = fields.pop(0)
  164. assert self.currency == 'CZK'
  165. self._scrap = [self._cleanup_field(f) for f in fields]
  166. def _cleanup_field(self, field):
  167. x = ' '.join(_unquote(field).strip().split())
  168. return '' if x == '-' else x
  169. def _description(self):
  170. (
  171. party_acc,
  172. party_accnname,
  173. party_bankcode,
  174. party_bankname,
  175. ks,
  176. vs,
  177. ss,
  178. note1,
  179. message,
  180. type_,
  181. who,
  182. explanation,
  183. note2,
  184. bic,
  185. comm_id
  186. ) = self._scrap
  187. out = []
  188. if message:
  189. out.append(message)
  190. return " / ".join(out)
  191. def _convert_date(self, text):
  192. day, month, year = text.split('.')
  193. return '-'.join([year, month, day])
  194. def _parse_currency(self, text):
  195. return float(text)
  196. def to_gc(self):
  197. """Convert to GCTransaction"""
  198. return GCTransaction(
  199. date=self.date,
  200. deposit=self.amountd,
  201. withdrawal=self.amountw,
  202. description=self._description()
  203. )
  204. class FioTransactionParser(BaseTransactionParser):
  205. def parse_line(self, line):
  206. return FioTransaction(line)
  207. def pick_state(self, line, lineno=None):
  208. """Choose parser state according to current line and line no."""
  209. if self.next_state:
  210. self.next_state = False
  211. return True
  212. if self.is_closed() and line.startswith(u'"ID operace";"Datum";'):
  213. self.next_state = True
  214. return False
  215. if self.is_open() and line.startswith(u'"bankId";"2010"'):
  216. return False
  217. return self.state
  218. _parsers = {
  219. 'CzMbank': MbankTransactionParser,
  220. 'CzFio': FioTransactionParser,
  221. }