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"
示例#2
0
    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))
示例#3
0
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')),
        ),
    )
示例#4
0
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),
        ),
    )
示例#5
0
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')
示例#6
0
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')
示例#7
0
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')
示例#8
0
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')
示例#9
0
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')
示例#10
0
    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'))
示例#11
0
    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'))
示例#12
0
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')
示例#13
0
    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'))
示例#14
0
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')
示例#15
0
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')
示例#16
0
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)
示例#17
0
    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'))
示例#18
0
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')
示例#19
0
 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,
         ))
示例#20
0
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))
示例#21
0
    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'))
示例#22
0
    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'))
示例#23
0
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
示例#25
0
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)
示例#26
0
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),
        ),
    )
示例#27
0
def parse_amount_from_priceline(x: Any):
    return Amount(D(x['amountMicros']) / 1000000, x['currencyCode']['code'])
示例#28
0
    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
示例#29
0
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