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"
Ejemplo n.º 2
0
 def test_cache_head_obeys_explict_utf8_encoding_avoids_chardet_exception(self):
     emoji_header = 'asciiHeader1,🍏Header1,asciiHeader2'.encode('utf-8')
     with mock.patch('builtins.open',
             mock.mock_open(read_data=emoji_header)):
         try:
             function_return = cache._FileMemo('anyFile').head(encoding='utf-8')
         except UnicodeDecodeError:
             self.fail("Failed to decode emoji")
Ejemplo n.º 3
0
    def test_match(self, filename):
        """\
        DATE,TYPE,REF #,DESCRIPTION,FEES,AMOUNT,BALANCE
        2014-04-14,BUY,14167001,BOUGHT +CSKO 50 @98.35,7.95,-4925.45,25674.63
        2014-05-08,BUY,12040838,BOUGHT +HOOL 121 @79.11,7.95,-9580.26,16094.37
        2014-05-11,BUY,41579908,BOUGHT +MSFX 104 @64.39,7.95,-6704.51,9389.86
        2014-05-22,DIV,54857517,ORDINARY DIVIDEND~HOOL,0,28.56,9418.42
        """
        importer = SimpleTestImporter([
            'Filename: .*te?mp.*', 'MimeType: text/', 'Contents:\n.*DATE,TYPE'
        ])
        file = cache._FileMemo(filename)
        self.assertTrue(importer.identify(file))

        importer = SimpleTestImporter(
            ['Filename: .*te?mp.*', 'MimeType: text/xml'])
        file = cache._FileMemo(filename)
        self.assertFalse(importer.identify(file))
Ejemplo n.º 4
0
 def test_importer_methods(self):
     # Kind of a dumb test, but for consistency we just test everything.
     memo = cache._FileMemo('/tmp/test')
     imp = importer.ImporterProtocol()
     self.assertIsInstance(imp.FLAG, str)
     self.assertFalse(imp.identify(memo))
     self.assertFalse(imp.extract(memo))
     self.assertFalse(imp.file_account(memo))
     self.assertFalse(imp.file_date(memo))
     self.assertFalse(imp.file_name(memo))
Ejemplo n.º 5
0
    def test_cache(self):
        with tempfile.NamedTemporaryFile() as tmpfile:
            shutil.copy(__file__, tmpfile.name)
            wrap = cache._FileMemo(tmpfile.name)

            # Check attributes.
            self.assertEqual(tmpfile.name, wrap.name)

            # Check that caching works.
            converter = mock.MagicMock(return_value='abc')
            self.assertEqual('abc', wrap.convert(converter))
            self.assertEqual('abc', wrap.convert(converter))
            self.assertEqual('abc', wrap.convert(converter))
            self.assertEqual(1, converter.call_count)
Ejemplo n.º 6
0
    def test_match(self, filename):
        """\
        DATE,TYPE,REF #,DESCRIPTION,FEES,AMOUNT,BALANCE
        2014-04-14,BUY,14167001,BOUGHT +CSKO 50 @98.35,7.95,-4925.45,25674.63
        2014-05-08,BUY,12040838,BOUGHT +HOOL 121 @79.11,7.95,-9580.26,16094.37
        """
        importer = fileonly.Importer(matchers=[
            ('filename', 'te?mp'), ('mime', 'text/'),
            ('content', 'DATE,TYPE,REF #,DESCRIPTION,FEES,AMOUNT')
        ],
                                     filing='Assets:BofA:Checking',
                                     prefix='bofa')
        file = cache._FileMemo(filename)
        self.assertTrue(importer.identify(file))

        assert importer.file_name(file).startswith('bofa.')
Ejemplo n.º 7
0
    def test_match(self, filename):
        """\
        DATE,TYPE,REF #,DESCRIPTION,FEES,AMOUNT,BALANCE
        2014-04-14,BUY,14167001,BOUGHT +CSKO 50 @98.35,7.95,-4925.45,25674.63
        2014-05-08,BUY,12040838,BOUGHT +HOOL 121 @79.11,7.95,-9580.26,16094.37
        """
        importer = fileonly.Importer([
            'Filename: .*te?mp.*', 'MimeType: text/plain',
            'Contents: .*DATE,TYPE,REF #,DESCRIPTION,FEES,AMOUNT'
        ],
                                     'Assets:BofA:Checking',
                                     basename='bofa')
        file = cache._FileMemo(filename)
        self.assertTrue(importer.identify(file))

        self.assertEqual('bofa', importer.file_name(file))
Ejemplo n.º 8
0
    def test_cache_head_and_contents(self):
        with tempfile.NamedTemporaryFile() as tmpfile:
            shutil.copy(__file__, tmpfile.name)
            wrap = cache._FileMemo(tmpfile.name)

            contents = wrap.convert(cache.contents)
            self.assertIsInstance(contents, str)
            self.assertGreater(len(contents), 128)

            contents2 = wrap.contents()
            self.assertEqual(contents, contents2)

            head = wrap.convert(cache.head(128))
            self.assertIsInstance(head, str)
            self.assertEqual(128, len(head))

            mimetype = wrap.convert(cache.mimetype)
            self.assertRegex(mimetype, r'text/(x-(python|c\+\+)|plain)')
Ejemplo n.º 9
0
def main():
    parser = CsvParser()
    parser.add_argument('-o',
                        '--output',
                        default=None,
                        help="Output file path (For single file input only)")
    parser.add_argument(
        '-d',
        '--directory',
        default=".",
        help="Export to an existing directory. Default in current directory.")
    # parser.add_argument('-f', '--format',
    #                     help="""Default value depends on --type argument.
    #                     HangSeng: 11s58s35s25s24s
    #                     MPower: 11s12s78s46s
    #                     This is the number of bytes expected for each field. For
    #                     example, for HangSeng, we expect Date(11), Title(58),
    #                     Deposit(35), Withdraw(25), and Balance(24).
    #                     Default value is set based on my statements. If it
    #                     doesn't work as expected, use `--verbose` option and
    #                     adjust format string until an aligned table is printed
    #                     out.""")
    parser.add_argument('-t',
                        '--type',
                        help="""Type of PDF.

                        Available types: HangSeng, MPower, DBS.""")
    parser.add_argument('-v',
                        '--verbose',
                        default=False,
                        action="store_true",
                        help="More details.")
    parser.add_argument('file',
                        nargs='+',
                        help='One or more PDF eStatements to process.')

    args = parser.parse_args()
    if args.output and len(args.file) > 1:
        sys.exit(
            "Output option can only be set with single file input. To export multiple files, use -d option."
        )

    for stmt in args.file:
        print("Processing: {}".format(stmt))
        if args.type.lower() == 'hangseng':
            allrecords = HangSengSavingsImporter("Dummy:Account:Name",
                                                 "Dummy",
                                                 debug=args.verbose).extract(
                                                     _FileMemo(stmt))
        elif args.type.lower() == 'mpower':
            allrecords = MPowerMasterImporter("Dummy:Account:Name",
                                              "Dummy",
                                              debug=args.verbose).extract(
                                                  _FileMemo(stmt))
        elif args.type.lower() == 'dbs':
            allrecords = DBSImporter("Dummy:Account:Name",
                                     "Dummy",
                                     debug=args.verbose).extract(
                                         _FileMemo(stmt))
        output_path = args.output if args.output else path.join(
            args.directory,
            path.splitext(path.basename(stmt))[0] + ".csv")
        print("Exporting to {}".format(output_path))

        with open(output_path, mode='w') as csv_file:
            csv_writer = csv.writer(csv_file,
                                    delimiter=',',
                                    quotechar='"',
                                    quoting=csv.QUOTE_MINIMAL)
            if args.type.lower() == 'hangseng':
                csv_writer.writerow(["date", "title", "amount"])
                for txn in allrecords:
                    csv_writer.writerow([
                        txn.date.isoformat(), txn.payee,
                        txn.postings[0].units.number
                    ])
            elif args.type.lower() == 'mpower':
                csv_writer.writerow(
                    ["trans_date", "post_date", "activity", "amount"])
                for txn in allrecords:
                    csv_writer.writerow([
                        txn.meta['txn_date'],
                        txn.date.isoformat(), txn.narration,
                        txn.postings[0].units.number
                    ])
            elif args.type.lower() == 'dbs':
                csv_writer.writerow(
                    ["trans_date", "post_date", "description", "amount"])
                for txn in allrecords:
                    csv_writer.writerow([
                        txn.meta['txn_date'],
                        txn.date.isoformat(), txn.narration,
                        txn.postings[0].units.number
                    ])

    return 0