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