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_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_import_result(parse_result: ultipro_google_statement.ParseResult, accounts: Rules, config: Config, info: dict) -> ImportResult: """Generate journal entries based on a payroll statement. :param all_values: parsed payroll statement. :param errors: errors from parsing payroll statement. :param accounts: maps section names to lists of rules specifying the account corresponding to a line entry in the statement. For the 'Earnings', 'Deductions', 'Taxes', and 'Net Pay Distribution' sections, the rules are specified as (description_regex, account) pairs. The description_regex is matched against the textual description for the line entry (it must match the entire string).. All account names are first transformed by calling format with the year parameter set to the appropriate year. :param config: specifies the configuration. :return: list of beancount entries. """ currency = config.currency all_values = parse_result.all_values general = parse_result.general pay_date = general['Pay Date']['date'] start_date = general['Period Start Date']['date'] end_date = general['Period End Date']['date'] year = pay_date.year txn = Transaction( meta=collections.OrderedDict(), date=pay_date, flag='*', payee=config.company_name, narration='Payroll', tags=EMPTY_SET, links=EMPTY_SET, postings=[], ) for i, error in enumerate(parse_result.errors): txn.meta['ultipro_parse_error%d' % i] = error document_number = general['Document']['number'] txn.meta[config.document_key] = document_number txn.meta[config.pay_date_key] = pay_date txn.meta[config.period_start_date_key] = start_date txn.meta[config.period_end_date_key] = end_date for section in ['Earnings', 'Deductions', 'Taxes', 'Net Pay Distribution']: if section == 'Net Pay Distribution': field_name = 'Amount' else: field_name = 'Current' cur_accounts = accounts[section] for row_name, fields in all_values[section]: value = fields[field_name] if section == 'Earnings': value = -value if value == ZERO: continue account = FIXME_ACCOUNT for row_re, account_pattern in cur_accounts: if re.fullmatch(row_re, row_name) is not None: account = account_pattern.format(year=year) break txn.postings.append( Posting( account=account, units=Amount(currency=currency, number=value), cost=None, meta={config.desc_key: '%s: %s' % (section, row_name)}, price=None, flag=None, )) return ImportResult(date=txn.date, entries=[txn], info=info)
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