def test_extract_(tmpdir): csv_path = path.join(tmpdir, "微信支付账单(20200830-20200906).csv") with open(csv_path, "w", encoding="utf-8", newline="") as f: f.write("\n" * 16) writer = csv.writer(f) writer.writerow( "交易时间,交易类型,交易对方,商品,收/支,金额(元),支付方式,当前状态,交易单号,商户单号,备注".split(",")) writer.writerow([ "2020-09-06 23:19:24", "零钱充值", "招商银行(1111)", "/", "/", "¥1.00", "招商银行(1111)", "充值完成", 233, "/", "/", ]) file = cache._FileMemo(csv_path) importer: WechatImporter = get_importer("examples/wechat.import") entries = importer.extract(file) assert len(entries) == 1 txn = entries[0] assert [x.account == importer.account for x in txn.postings] == [True, False] assert [x.units for x in txn.postings] == [ Amount(Decimal(1), "CNY"), Amount(Decimal(-1), "CNY"), ] assert txn.postings[1].account == "Assets:Bank:CMB:C1111"
def test_process_match(self): self.skipTest("wip") narration = "Amazon.com*MA4TS16T0" tx = Transaction(narration=narration, date=None, flag=None, payee=None, tags={}, links={}, postings=[ Posting(account="Liablities:Card", units=Amount(D(100), "USD"), cost=None, price=None, flag="*", meta={}), Posting(account="Expenses:FIXME", units=Amount(D(-100), "USD"), cost=None, price=None, flag="!", meta={}) ], meta={ 'file_name': '', 'lineno': 0 }) m = Matcher([AMAZON_RULE]) results = m.process([tx]) self.assertEqual(len(results), 1) result = results[0] print(yaml.dump(AMAZON_RULE))
def make_import_result(receipt: Any, receipt_directory: str, link_prefix: str) -> ImportResult: receipt_id = str(receipt['id']) if receipt['date']: date = datetime.datetime.strptime(receipt['date'], date_format).date() else: date = dateutil.parser.parse(receipt['submitted_at']).date() merchant = receipt['merchant'] note = receipt['note'] if note: payee = merchant narration = note else: payee = None narration = merchant if receipt['total']: amount = Amount( number=D(receipt['total']), currency=receipt['currency_code']) else: amount = Amount( number=ZERO, currency=receipt['currency_code']) postings = [ Posting( account=FIXME_ACCOUNT, units=amount, cost=None, meta=None, price=None, flag=None, ), Posting( account=FIXME_ACCOUNT, units=-amount, cost=None, meta=None, price=None, flag=None, ) ] return ImportResult( date=date, entries=[ Transaction( meta=collections.OrderedDict(), date=date, flag='*', payee=payee, narration=narration, links=frozenset([link_prefix + receipt_id]), tags=frozenset(), postings=postings, ), ], info=dict( type='image/jpeg', filename=os.path.realpath( os.path.join(receipt_directory, receipt_id + '.jpg')), ), )
def make_import_result(purchase: Any, link_prefix: str, tz_info: Optional[datetime.tzinfo], html_path: str) -> ImportResult: purchase_id = str(purchase['id']) date = datetime.datetime.fromtimestamp(purchase['timestamp'] / 1000, tz_info).date() payment_processor = purchase['payment_processor'] merchant = purchase['merchant'] items = purchase['items'] payee = ' - '.join(x for x in [payment_processor, merchant] if x is not None) # type: Optional[str] narration = '; '.join(x for x in items) # type: Optional[str] if not narration: narration = payee payee = None if purchase['currency'] is None or purchase['units'] is None: pos_amount = neg_amount = Amount(D('0.00'), 'USD') else: pos_amount = Amount( round(D(purchase['units']), 2), purchase['currency']) neg_amount = -pos_amount postings = [ Posting( account=FIXME_ACCOUNT, units=pos_amount, cost=None, meta=None, price=None, flag=None, ), Posting( account=FIXME_ACCOUNT, units=neg_amount, cost=None, meta=None, price=None, flag=None, ) ] return ImportResult( date=date, entries=[ Transaction( meta=collections.OrderedDict(), date=date, flag='*', payee=payee, narration=narration, links=frozenset([link_prefix + purchase_id]), tags=frozenset(), postings=postings, ), ], info=dict( type='text/html', filename=os.path.realpath(html_path), ), )
def test_emits_closing_balance_directive(tmp_file): tmp_file.write( _format( ''' "Kreditkarte:";"{card_number} Kreditkarte"; "Von:";"01.01.2018"; "Bis:";"31.01.2018"; "Saldo:";"5000.01 EUR"; "Datum:";"30.01.2018"; {header}; "Ja";"15.01.2018";"15.01.2018";"REWE Filiale Muenchen";"-10,80";""; ''', # NOQA dict( card_number=Constants.card_number.value, header=Constants.header.value, ), ) ) importer = CreditImporter( Constants.card_number.value, 'Assets:DKB:Credit', file_encoding='utf-8' ) with open(str(tmp_file.realpath())) as fd: transactions = importer.extract(fd) assert len(transactions) == 2 assert isinstance(transactions[1], Balance) assert transactions[1].date == datetime.date(2018, 1, 31) assert transactions[1].amount == Amount(Decimal('5000.01'), currency='EUR')
def test_extract_no_transactions(tmp_file): importer = CreditImporter(Constants.card_number.value, 'Assets:DKB:Credit') tmp_file.write( _format( ''' "Kreditkarte:";"{card_number} Kreditkarte"; "Von:";"01.01.2018"; "Bis:";"31.01.2018"; "Saldo:";"5000.01 EUR"; "Datum:";"30.01.2018"; {header}; ''', dict( card_number=Constants.card_number.value, header=Constants.header.value, ), ) ) with open(str(tmp_file.realpath())) as fd: transactions = importer.extract(fd) assert len(transactions) == 1 assert isinstance(transactions[0], Balance) assert transactions[0].date == datetime.date(2018, 1, 31) assert transactions[0].amount == Amount(Decimal('5000.01'), currency='EUR')
def test_mismatching_dates_in_meta(tmp_file): tmp_file.write_text( _format( ''' "Kontonummer:";"{iban} / Girokonto"; "Von:";"01.01.2018"; "Bis:";"31.01.2018"; "Kontostand vom 31.01.2019:";"5.000,01 EUR"; {header}; "16.01.2018";"16.01.2018";"Lastschrift";"REWE Filialen Voll";"REWE SAGT DANKE.";"DE00000000000000000000";"AAAAAAAA";"-15,37";"000000000000000000 ";"0000000000000000000000";""; ''', # NOQA dict(iban=IBAN, header=HEADER), ) ) importer = ECImporter(IBAN, 'Assets:DKB:EC', file_encoding='utf-8') with tmp_file.open() as fd: directives = importer.extract(fd) assert len(directives) == 2 assert isinstance(directives[1], Balance) assert directives[1].date == datetime.date(2019, 2, 1) assert directives[1].amount == Amount(Decimal('5000.01'), currency='EUR')
def test_tagessaldo_with_empty_balance_does_not_crash(tmp_file): tmp_file.write_text( _format( ''' "Kontonummer:";"{iban} / Girokonto"; "Von:";"01.01.2018"; "Bis:";"31.01.2018"; "Kontostand vom 31.01.2018:";"5.000,01 EUR"; {header}; "20.01.2018";"";"";"";"Tagessaldo";"";"";""; ''', dict(iban=IBAN, header=HEADER), ) ) importer = ECImporter(IBAN, 'Assets:DKB:EC', file_encoding='utf-8') with tmp_file.open() as fd: directives = importer.extract(fd) assert len(directives) == 1 assert isinstance(directives[0], Balance) assert directives[0].date == datetime.date(2018, 2, 1) assert directives[0].amount == Amount(Decimal('5000.01'), currency='EUR')
def test_extract_no_transactions(tmp_file): importer = ECImporter(IBAN, 'Assets:DKB:EC') tmp_file.write_text( _format( ''' "Kontonummer:";"{iban} / Girokonto"; "Von:";"01.01.2018"; "Bis:";"31.01.2018"; "Kontostand vom 31.01.2018:";"5.000,01 EUR"; {header}; ''', dict(iban=IBAN, header=HEADER), ) ) with tmp_file.open() as fd: directives = importer.extract(fd) assert len(directives) == 1 assert isinstance(directives[0], Balance) assert directives[0].date == datetime.date(2018, 2, 1) assert directives[0].amount == Amount(Decimal('5000.01'), currency='EUR')
def test_tagessaldo_emits_balance_directive(self): with open(self.filename, 'wb') as fd: fd.write( _format( ''' "Kontonummer:";"{iban} / Girokonto"; "Von:";"01.01.2018"; "Bis:";"31.01.2018"; "Kontostand vom 31.01.2017:";"5.000,01 EUR"; {header}; "20.01.2018";"";"";"";"Tagessaldo";"";"";"2.500,01"; ''', dict(iban=self.iban, header=HEADER))) # NOQA importer = ECImporter(self.iban, 'Assets:DKB:EC', file_encoding='utf-8') with open(self.filename) as fd: transactions = importer.extract(fd) self.assertEqual(len(transactions), 1) self.assertTrue(isinstance(transactions[0], Balance)) self.assertEqual(transactions[0].date, datetime.date(2018, 1, 20)) self.assertEqual(transactions[0].amount, Amount(Decimal('2500.01'), currency='EUR'))
def test_extract_sets_timestamps(self): with open(self.filename, 'wb') as fd: fd.write(_format(''' "Kontonummer:";"{iban} / Girokonto"; "Von:";"01.01.2018"; "Bis:";"31.01.2018"; "Kontostand vom 31.01.2018:";"5.000,01 EUR"; {header}; "16.01.2018";"16.01.2018";"Lastschrift";"REWE Filialen Voll";"REWE SAGT DANKE.";"DE00000000000000000000";"AAAAAAAA";"-15,37";"000000000000000000 ";"0000000000000000000000";""; ''', dict(iban=self.iban, header=HEADER))) # NOQA importer = ECImporter(self.iban, 'Assets:DKB:EC', file_encoding='utf-8') self.assertFalse(importer._date_from) self.assertFalse(importer._date_to) self.assertFalse(importer._balance) with open(self.filename) as fd: transactions = importer.extract(fd) self.assertTrue(transactions) self.assertEqual(importer._date_from, datetime.date(2018, 1, 1)) self.assertEqual(importer._date_to, datetime.date(2018, 1, 31)) self.assertEqual(importer._balance, Amount(Decimal('5000.01'), currency='EUR'))
def test_tagessaldo_with_empty_balance_does_not_crash(tmp_file): tmp_file.write( _format( ''' "Kontonummer:";"{iban} / Girokonto"; "Von:";"01.01.2018"; "Bis:";"31.01.2018"; "Kontostand vom 31.01.2018:";"5.000,01 EUR"; {header}; "20.01.2018";"";"";"";"Tagessaldo";"";"";""; ''', dict(iban=Constants.iban.value, header=Constants.header.value), )) importer = ECImporter(Constants.iban.value, 'Assets:DKB:EC', file_encoding='utf-8') with open(str(tmp_file.realpath())) as fd: transactions = importer.extract(fd) assert len(transactions) == 1 assert isinstance(transactions[0], Balance) assert transactions[0].date == datetime.date(2018, 2, 1) assert transactions[0].amount == Amount(Decimal('5000.01'), currency='EUR')
def test_extract_no_transactions(self): importer = CreditImporter(self.card_number, 'Assets:DKB:Credit') with open(self.filename, 'wb') as fd: fd.write( _format( ''' "Kreditkarte:";"{card_number} Kreditkarte"; "Von:";"01.01.2018"; "Bis:";"31.01.2018"; "Saldo:";"5000.01 EUR"; "Datum:";"30.01.2018"; {header}; ''', dict(card_number=self.card_number, header=HEADER), )) with open(self.filename) as fd: transactions = importer.extract(fd) self.assertEqual(len(transactions), 1) self.assertTrue(isinstance(transactions[0], Balance)) self.assertEqual(transactions[0].date, datetime.date(2018, 1, 31)) self.assertEqual(transactions[0].amount, Amount(Decimal('5000.01'), currency='EUR'))
def test_emits_closing_balance_directive(tmp_file): tmp_file.write_text( _format( ''' "Kreditkarte:";"{card_number} Kreditkarte"; "Von:";"01.01.2018"; "Bis:";"31.01.2018"; "Saldo:";"5000.01 EUR"; "Datum:";"30.01.2018"; {header}; "Ja";"15.01.2018";"15.01.2018";"REWE Filiale Muenchen";"-10,80";""; ''', # NOQA dict(card_number=CARD_NUMBER, header=HEADER), )) importer = CreditImporter(CARD_NUMBER, 'Assets:DKB:Credit', file_encoding='utf-8') with tmp_file.open() as fd: directives = importer.extract(fd) assert len(directives) == 2 assert isinstance(directives[1], Balance) assert directives[1].date == datetime.date(2018, 1, 31) assert directives[1].amount == Amount(Decimal('5000.01'), currency='EUR')
def test_extract_no_transactions(tmp_file): importer = CreditImporter(CARD_NUMBER, 'Assets:DKB:Credit') tmp_file.write_text( _format( ''' "Kreditkarte:";"{card_number} Kreditkarte"; "Von:";"01.01.2018"; "Bis:";"31.01.2018"; "Saldo:";"5000.01 EUR"; "Datum:";"30.01.2018"; {header}; ''', dict(card_number=CARD_NUMBER, header=HEADER), )) with tmp_file.open() as fd: directives = importer.extract(fd) assert len(directives) == 1 assert isinstance(directives[0], Balance) assert directives[0].date == datetime.date(2018, 1, 31) assert directives[0].amount == Amount(Decimal('5000.01'), currency='EUR')
def test_extract_sets_timestamps(tmp_file): tmp_file.write( _format( ''' "Kontonummer:";"{iban} / Girokonto"; "Von:";"01.01.2018"; "Bis:";"31.01.2018"; "Kontostand vom 31.01.2018:";"5.000,01 EUR"; {header}; "16.01.2018";"16.01.2018";"Lastschrift";"REWE Filialen Voll";"REWE SAGT DANKE.";"DE00000000000000000000";"AAAAAAAA";"-15,37";"000000000000000000 ";"0000000000000000000000";""; ''', # NOQA dict(iban=Constants.iban.value, header=Constants.header.value), )) importer = ECImporter(Constants.iban.value, 'Assets:DKB:EC', file_encoding='utf-8') assert not importer._date_from assert not importer._date_to assert not importer._balance_amount assert not importer._balance_date with open(str(tmp_file.realpath())) as fd: transactions = importer.extract(fd) assert transactions assert importer._date_from == datetime.date(2018, 1, 1) assert importer._date_to == datetime.date(2018, 1, 31) assert importer._balance_amount == Amount(Decimal('5000.01'), currency='EUR') assert importer._balance_date == datetime.date(2018, 2, 1)
def test_emits_closing_balance_directive(self): with open(self.filename, 'wb') as fd: fd.write( _format( ''' "Kreditkarte:";"{card_number} Kreditkarte"; "Von:";"01.01.2018"; "Bis:";"31.01.2018"; "Saldo:";"5000.01 EUR"; "Datum:";"30.01.2018"; {header}; "Ja";"15.01.2018";"15.01.2018";"REWE Filiale Muenchen";"-10,80";""; ''', # NOQA dict(card_number=self.card_number, header=HEADER), )) importer = CreditImporter(self.card_number, 'Assets:DKB:Credit', file_encoding='utf-8') with open(self.filename) as fd: transactions = importer.extract(fd) self.assertEqual(len(transactions), 2) self.assertTrue(isinstance(transactions[1], Balance)) self.assertEqual(transactions[1].date, datetime.date(2018, 1, 31)) self.assertEqual(transactions[1].amount, Amount(Decimal('5000.01'), currency='EUR'))
def test_mismatching_dates_in_meta(tmp_file): tmp_file.write( _format( ''' "Kontonummer:";"{iban} / Girokonto"; "Von:";"01.01.2018"; "Bis:";"31.01.2018"; "Kontostand vom 31.01.2019:";"5.000,01 EUR"; {header}; "16.01.2018";"16.01.2018";"Lastschrift";"REWE Filialen Voll";"REWE SAGT DANKE.";"DE00000000000000000000";"AAAAAAAA";"-15,37";"000000000000000000 ";"0000000000000000000000";""; ''', # NOQA dict(iban=Constants.iban.value, header=Constants.header.value), )) importer = ECImporter(Constants.iban.value, 'Assets:DKB:EC', file_encoding='utf-8') with open(str(tmp_file.realpath())) as fd: transactions = importer.extract(fd) assert len(transactions) == 2 assert isinstance(transactions[1], Balance) assert transactions[1].date == datetime.date(2019, 2, 1) assert transactions[1].amount == Amount(Decimal('5000.01'), currency='EUR')
def add_posting(section, row_name, value): account_pattern = account_pattern_for_row_name(row_name, section) txn.postings.append( Posting( account=account_pattern.format(year=year), units=Amount(currency=currency, number=value), cost=None, meta={config.desc_key: '%s: %s' % (section, row_name)}, price=None, flag=None, ))
def unbook_postings(postings: List[Posting]) -> Posting: """Unbooks a list of postings back into a single posting. The combined units are computed, the cost and price are left unspecified. """ if len(postings) == 1: return postings[0] number = sum((posting.units.number for posting in postings), ZERO) return postings[0]._replace(units=Amount( number=number, currency=postings[0].units.currency), cost=CostSpec(number_per=None, number_total=None, currency=None, date=None, label=None, merge=None))
def test_get_features(): date = datetime.date.min amount = Amount.from_string('3 USD') assert training.get_features( training.PredictionInput(date=date, amount=amount, source_account='Assets:Checking', key_value_pairs={ 'a': 'hello', 'b': 'foo bar' })) == { 'account:Assets:Checking': True, 'a:hello': True, 'b:foo': True, 'b:bar': True, 'b:foo bar': True }
def test_extract_no_transactions(self): importer = ECImporter(self.iban, 'Assets:DKB:EC') with open(self.filename, 'wb') as fd: fd.write(_format(''' "Kontonummer:";"{iban} / Girokonto"; "Von:";"01.01.2018"; "Bis:";"31.01.2018"; "Kontostand vom 31.01.2018:";"5.000,01 EUR"; {header}; ''', dict(iban=self.iban, header=HEADER))) with open(self.filename) as fd: transactions = importer.extract(fd) self.assertEqual(len(transactions), 1) self.assertTrue(isinstance(transactions[0], Balance)) self.assertEqual(transactions[0].date, datetime.date(2018, 1, 31)) self.assertEqual(transactions[0].amount, Amount(Decimal('5000.01'), currency='EUR'))
def test_emits_closing_balance_directive(self): with open(self.filename, 'wb') as fd: fd.write(_format(''' "Kontonummer:";"{iban} / Girokonto"; "Von:";"01.01.2018"; "Bis:";"31.01.2018"; "Kontostand vom 31.01.2017:";"5.000,01 EUR"; {header}; "16.01.2018";"16.01.2018";"Lastschrift";"REWE Filialen Voll";"REWE SAGT DANKE.";"DE00000000000000000000";"AAAAAAAA";"-15,37";"000000000000000000 ";"0000000000000000000000";""; ''', dict(iban=self.iban, header=HEADER))) # NOQA importer = ECImporter(self.iban, 'Assets:DKB:EC', file_encoding='utf-8') with open(self.filename) as fd: transactions = importer.extract(fd) self.assertEqual(len(transactions), 2) self.assertTrue(isinstance(transactions[1], Balance)) self.assertEqual(transactions[1].date, datetime.date(2018, 1, 31)) self.assertEqual(transactions[1].amount, Amount(Decimal('5000.01'), currency='EUR'))
def test_extract_no_transactions(tmp_file): importer = ECImporter(Constants.iban.value, 'Assets:DKB:EC') tmp_file.write( _format( ''' "Kontonummer:";"{iban} / Girokonto"; "Von:";"01.01.2018"; "Bis:";"31.01.2018"; "Kontostand vom 31.01.2018:";"5.000,01 EUR"; {header}; ''', dict(iban=Constants.iban.value, header=Constants.header.value), )) with open(str(tmp_file.realpath())) as fd: transactions = importer.extract(fd) assert len(transactions) == 1 assert isinstance(transactions[0], Balance) assert transactions[0].date == datetime.date(2018, 2, 1) assert transactions[0].amount == Amount(Decimal('5000.01'), currency='EUR')
def parse(self): d = self.soup transactions = [] last_account = '' date_string = d.text.split('出单日:')[1].split('日期范围')[0].strip() balance_date = date(int(date_string[0:4]), int( date_string[5:7]), int(date_string[8:10])) balances = d.select('[style="busi-cunkuan1.tab3.display"] .table1 tr') for balance in balances: tds = balance.select('td.dspts') if len(tds) == 0 or len(tds) < 3: continue account = tds[0].text.strip() account = last_account if account == '' else account last_account = account balance_account = get_account_by_name('ICBC_' + account) currency = self.change_currency(tds[3].text.strip()) price = str(tds[5].text.strip().replace(',', '')) entry = Balance( account=balance_account, amount=Amount(Decimal(price), currency), meta={}, tolerance='', diff_amount=Amount(Decimal('0'), currency), date=balance_date ) transactions.append(entry) bands = d.select('[style="busi-other_detail.tab3.display"] .table1 tr') for band in bands: tds = band.select('td.dspts') if len(tds) == 0: continue trade_date = tds[10].text.strip() if trade_date == '': continue time = date(int(trade_date[0:4]), int( trade_date[4:6]), int(trade_date[6:8])) description = tds[6].text.strip() trade_currency = self.change_currency(tds[3].text.strip()) trade_price = tds[7].text.strip() account = tds[0].text.strip() account = last_account if account == '' else account last_account = account print("Importing {} at {}".format(description, time)) trade_account = get_account_by_name('ICBC_' + account) flag = "*" amount = float(trade_price.replace(',', '')) if account == "Unknown": flag = "!" meta = {} meta = data.new_metadata( 'beancount/core/testing.beancount', 12345, meta ) entry = Transaction( meta, time, flag, description, None, data.EMPTY_SET, data.EMPTY_SET, [] ) data.create_simple_posting( entry, trade_account, trade_price, trade_currency) data.create_simple_posting(entry, AccountUnknown, None, None) if not self.deduplicate.find_duplicate(entry, -amount, None, account): transactions.append(entry) self.deduplicate.apply_beans() return transactions
def make_takeout_import_result(purchase: Any, purchase_id: str, link_prefix: str, ignored_transaction_merchants_pattern: str, tz_info: Optional[datetime.tzinfo], html_path: str) -> Optional[ImportResult]: if ('creationTime' not in purchase or 'transactionMerchant' not in purchase or 'name' not in purchase['transactionMerchant'] or 'usecSinceEpochUtc' not in purchase['creationTime']): # May be a reservation rather than a purchase return None date = datetime.datetime.fromtimestamp( int(purchase['creationTime']['usecSinceEpochUtc']) / 1000000, tz_info).date() payment_processor = purchase['transactionMerchant']['name'] if (payment_processor is not None and re.fullmatch( ignored_transaction_merchants_pattern, payment_processor)): return None unique_merchants = set() merchant = None # type: Optional[str] item_names = [] for line_item in purchase['lineItem']: if 'provider' in line_item: merchant = line_item['provider']['name'] unique_merchants.add(merchant) if 'purchase' not in line_item: continue line_item_purchase = line_item['purchase'] if 'productInfo' in line_item_purchase: product_info = line_item_purchase['productInfo'] text = product_info['name'] if 'description' in line_item: text += '; ' text += product_info['description'] item_names.append(text) if len(unique_merchants) != 1: merchant = None inventory = SimpleInventory() for priceline in purchase.get('priceline', []): inventory += parse_amount_from_priceline(priceline['amount']) payee = ' - '.join(x for x in [payment_processor, merchant] if x is not None) # type: Optional[str] narration = '; '.join(x for x in item_names) # type: Optional[str] if not narration: narration = payee payee = None postings = [] if len(inventory) == 0: inventory['USD'] = ZERO for currency, units in inventory.items(): pos_amount = Amount(round(units, 2), currency) neg_amount = -pos_amount postings.append( Posting( account=FIXME_ACCOUNT, units=pos_amount, cost=None, meta=None, price=None, flag=None, )) postings.append( Posting( account=FIXME_ACCOUNT, units=neg_amount, cost=None, meta=None, price=None, flag=None, )) return ImportResult( date=date, entries=[ Transaction( meta=collections.OrderedDict(), date=date, flag='*', payee=payee, narration=narration, links=frozenset([link_prefix + purchase_id]), tags=frozenset(), postings=postings, ), ], info=dict( type='text/html', filename=os.path.realpath(html_path), ), )
def parse_amount_from_priceline(x: Any): return Amount(D(x['amountMicros']) / 1000000, x['currencyCode']['code'])
def parse(self): d = self.soup transactions = [] # balance = d.select('#fixBand16')[0].text.replace('RMB', '').strip() date_range = d.select('#fixBand6 div font')[0].text.strip() transaction_date = dateparser.parse( date_range.split('-')[1].split('(')[0]) transaction_date = date(transaction_date.year, transaction_date.month, transaction_date.day) self.date = transaction_date balance = '-' + \ d.select('#fixBand7 div font')[0].text.replace( '¥', '').replace(',', '').strip() entry = Balance(account=Account招商, amount=Amount(Decimal(balance), 'CNY'), meta={}, tolerance='', diff_amount=Amount(Decimal('0'), 'CNY'), date=self.date) transactions.append(entry) # bands = d.select('#fixBand29 #loopBand2>table>tbody>tr') bands = d.select("#fixBand29 #loopBand2 > table >tr") for band in bands: tds = band.select('td #fixBand15 table table td') if len(tds) == 0: continue # trade_date = tds[1].text.strip() # if trade_date == '': trade_date = tds[2].text.strip() time = self.get_date(trade_date) if '支付宝' in tds[3].text.strip(): full_descriptions = tds[3].text.strip().split('-') payee = full_descriptions[0] description = '-'.join(full_descriptions[1:]) elif '还款' in tds[3].text.strip(): payee = "还款" description = tds[3].text.strip() else: full_descriptions = tds[3].text.strip().split() payee = full_descriptions[0] description = '-'.join(full_descriptions[1:]) trade_currency = self.change_currency(tds[6].text.strip()) trade_price = tds[7].text.replace('\xa0', '').strip() real_currency = 'CNY' real_price = tds[4].text.replace('¥', '').replace('\xa0', '').strip() print("Importing {} at {}".format(description, time)) account = get_account_by_guess(payee, description, time) flag = "*" amount = float(real_price.replace(',', '')) if account == "Unknown": flag = "!" meta = {} meta = data.new_metadata('beancount/core/testing.beancount', 12345, meta) entry = Transaction(meta, time, flag, payee, description, data.EMPTY_SET, data.EMPTY_SET, []) if real_currency == trade_currency: data.create_simple_posting(entry, account, trade_price, trade_currency) else: trade_amount = Amount(Decimal(trade_price), trade_currency) real_amount = Amount( Decimal(abs(round(float(real_price), 2))) / Decimal(abs(round(float(trade_price), 2))), real_currency) posting = Posting(account, trade_amount, None, real_amount, None, None) entry.postings.append(posting) data.create_simple_posting(entry, Account招商, None, None) if not self.deduplicate.find_duplicate(entry, -amount, None, Account招商): transactions.append(entry) self.deduplicate.apply_beans() return transactions
def evensplit(entries, options_map): errors = [] for entry in filter_txns(entries): new_postings_map = ddict(lambda: (D(), {})) special_postings = [] split_others = set( posting.meta.get('split_others') for posting in entry.postings if posting.meta) split_others.discard(None) if split_others: split_others = list(split_others) try: split_others, = split_others except ValueError: pass for i, posting in enumerate(entry.postings): if posting.meta is None: posting = posting._replace(meta={}) if 'split_others' not in posting.meta and 'split' not in posting.meta: posting.meta['split'] = copy(split_others) entry.postings[i] = posting for i, posting in enumerate(entry.postings): if posting.meta and 'split' in posting.meta: assert posting.cost is None save_meta = deepcopy(posting.meta) targets = save_meta.pop('split') if is_account(targets): # special case for single split to account (because halfcents) assert posting.account != "Assets:Receivables" amount = adiv(posting.units, D(2)) posting = posting._replace(units=amount) newacct = targets add_posting(new_postings_map, newacct, amount, save_meta) else: if not isinstance(targets, list): targets = [targets] ntargets = len(targets) if posting.account != "Assets:Receivables": ntargets += 1 split_amount = posting.units.number / ntargets split_amount = round(split_amount, 2) amount = posting.units._replace(number=split_amount) if posting.account != "Assets:Receivables": remainder = posting.units.number - split_amount * len( targets) remainder = posting.units._replace(number=remainder) posting = posting._replace(units=remainder) else: posting = None for target in targets: if is_account(target): newacct = target newamount = amount newcost = None add_posting(new_postings_map, newacct, newamount, save_meta) else: newacct = "Assets:Receivables" newamount = Amount(D(1), "REIMB") newcost = Cost(amount.number, amount.currency, entry.date, target) special_postings.append( Posting(newacct, newamount, newcost, None, None, save_meta)) entry.postings[i] = posting entry.postings[:] = list( filter(lambda x: x is not None, entry.postings)) for (acct, currency), (amt, meta) in new_postings_map.items(): entry.postings.append( Posting(acct, Amount(amt, currency), None, None, None, meta)) entry.postings.extend(special_postings) return entries, errors
def parse_cmb(filename): account = "Liabilities:CreditCard:CMB" transactions = [] with open(filename, "rb") as f: file_bytes = f.read() parsed_eml = eml_parser.eml_parser.decode_email_b(file_bytes, include_raw_body=True) # print(parsed_eml) content = parsed_eml["body"][0]["content"] soup = BeautifulSoup(content, "html.parser") print(soup) # balance according to bill amount date_range = soup.select("#fixBand38 div font")[0].text.strip() transaction_date = dateparser.parse( date_range.split('-')[1].split('(')[0]) transaction_date = date(transaction_date.year, transaction_date.month, transaction_date.day) start_date = dateparser.parse(date_range.split('-')[0]) start_date = date(start_date.year, start_date.month, start_date.day) balance = '-' + \ soup.select('#fixBand40 div font')[0].text.replace( '¥', '').replace(',', '').strip() entry = Balance( account=account, amount=Amount(Decimal(balance), 'CNY'), meta={}, tolerance='', diff_amount=Amount(Decimal('0'), 'CNY'), date=transaction_date ) transactions.append(entry) # bands = soup.select('#fixBand29 #loopBand2>table>tbody>tr') bands = soup.select("#fixBand29 #loopBand2>table>tr") for band in bands: tds = band.select('td #fixBand15 table table td') if len(tds) == 0: continue trade_date = tds[1].text.strip() if trade_date == '': trade_date = tds[2].text.strip() time = get_date(start_date, trade_date) full_descriptions = tds[3].text.strip().split('-') payee = full_descriptions[0] description = '-'.join(full_descriptions[1:]) trade_currency = get_currency(tds[6].text.strip()) trade_price = tds[7].text.replace('\xa0', '').strip() real_currency = 'CNY' real_price = tds[4].text.replace( '¥', '').replace('\xa0', '').strip() print("Importing {} - {} at {}".format(payee, description, time)) category = get_category(description, payee) if (payee == "自动还款" or payee == "掌上生活还款"): description = payee category = "Assets:DepositCard:CMB9843" flag = "*" amount = float(real_price.replace(',', '')) meta = {} entry = Transaction(meta, time, flag, payee, description, data.EMPTY_SET, data.EMPTY_SET, []) if real_currency == trade_currency: data.create_simple_posting( entry, category, trade_price, trade_currency) else: trade_amount = Amount(Decimal(trade_price), trade_currency) real_amount = Amount(Decimal(abs(round(float( real_price), 2))) / Decimal(abs(round(float(trade_price), 2))), real_currency) posting = Posting(category, trade_amount, None, real_amount, None, None) entry.postings.append(posting) data.create_simple_posting(entry, account, None, None) transactions.append(entry) return transactions