예제 #1
0
 def test_add(self):
     self.assertEqual(Amount(D('117.02'), 'CAD'),
                      amount.add(Amount(D('100'), 'CAD'),
                                 Amount(D('17.02'), 'CAD')))
     with self.assertRaises(ValueError):
         amount.add(Amount(D('100'), 'USD'),
                    Amount(D('17.02'), 'CAD'))
예제 #2
0
def compute_stack(stack):
    for i in range(1, len(stack), 2):
        if stack[i] == '+':
            stack[0] = add(stack[0], stack[i + 1])
        elif stack[i] == '-':
            stack[0] = sub(stack[0], stack[i + 1])
    return stack[0]
예제 #3
0
 def get_entries(self):
     for file, import_map in sorted(self.entry_map.items()):
         if os.path.basename(file) in self.matching_paychecks:
             continue
         entry = None
         for imported_data in import_map:
             if entry is None:
                 entry = Transaction(
                     meta=None,
                     date=imported_data.date,
                     flag=FLAG_OKAY,
                     payee=None,
                     narration=imported_data.narration,
                     tags=EMPTY_SET,
                     links=EMPTY_SET,
                     postings=[],
                 )
             entry.postings.append(imported_data.posting())
         # Verify entry completeness
         entry_sum = Amount(Decimal("0"), "USD")
         self.source.log_status(
             f"Verifying Transaction: {entry.date} {entry.narration}")
         for posting in entry.postings:
             entry_sum = amount.add(entry_sum, posting.units)
             self.source.log_status(pformat(posting, indent=2))
             self.source.log_status(f"entry_sum = {entry_sum}")
         if entry_sum:
             err_msg = ("ERROR: Invalid Transaction posting total: " +
                        f"{entry_sum} from {os.path.basename(file)}")
             self.source.log_status(err_msg)
             print(err_msg)
         else:
             self.results.add_pending_entry(
                 ImportResult(
                     date=imported_data.date,
                     entries=[entry],
                     info=get_info(imported_data),
                 ))
예제 #4
0
    def extract(self, file):
        # Open the CSV file and create directives.
        entries = []
        index = 0
        for index, row in enumerate(csv.DictReader(open(file.name))):
            meta = data.new_metadata(file.name, index)
            date = parse(row['DATE']).date()
            rtype = row['TYPE']
            link = "ut{0[REF #]}".format(row)
            desc = "({0[TYPE]}) {0[DESCRIPTION]}".format(row)
            units = amount.Amount(D(row['AMOUNT']), self.currency)
            fees = amount.Amount(D(row['FEES']), self.currency)
            other = amount.add(units, fees)

            if rtype == 'XFER':
                assert fees.number == ZERO
                txn = data.Transaction(
                    meta, date, self.FLAG, None, desc, data.EMPTY_SET, {link},
                    [
                        data.Posting(self.account_cash, units, None, None,
                                     None, None),
                        data.Posting(self.account_external, -other, None, None,
                                     None, None),
                    ])

            elif rtype == 'DIV':
                assert fees.number == ZERO

                # Extract the instrument name from its description.
                match = re.search(r'~([A-Z]+)$', row['DESCRIPTION'])
                if not match:
                    logging.error("Missing instrument name in '%s'",
                                  row['DESCRIPTION'])
                    continue
                instrument = match.group(1)
                account_dividends = self.account_dividends.format(instrument)

                txn = data.Transaction(
                    meta, date, self.FLAG, None, desc, data.EMPTY_SET, {link},
                    [
                        data.Posting(self.account_cash, units, None, None,
                                     None, None),
                        data.Posting(account_dividends, -other, None, None,
                                     None, None),
                    ])

            elif rtype in ('BUY', 'SELL'):

                # Extract the instrument name, number of units, and price from
                # the description. That's just what we're provided with (this is
                # actually realistic of some data from some institutions, you
                # have to figure out a way in your parser).
                match = re.search(r'\+([A-Z]+)\b +([0-9.]+)\b +@([0-9.]+)',
                                  row['DESCRIPTION'])
                if not match:
                    logging.error("Missing purchase infos in '%s'",
                                  row['DESCRIPTION'])
                    continue
                instrument = match.group(1)
                account_inst = account.join(self.account_root, instrument)
                units_inst = amount.Amount(D(match.group(2)), instrument)
                rate = D(match.group(3))

                if rtype == 'BUY':
                    cost = position.Cost(rate, self.currency, None, None)
                    txn = data.Transaction(
                        meta, date, self.FLAG, None, desc, data.EMPTY_SET,
                        {link}, [
                            data.Posting(self.account_cash, units, None, None,
                                         None, None),
                            data.Posting(self.account_fees, fees, None, None,
                                         None, None),
                            data.Posting(account_inst, units_inst, cost, None,
                                         None, None),
                        ])

                elif rtype == 'SELL':
                    # Extract the lot. In practice this information not be there
                    # and you will have to identify the lots manually by editing
                    # the resulting output. You can leave the cost.number slot
                    # set to None if you like.
                    match = re.search(r'\(LOT ([0-9.]+)\)', row['DESCRIPTION'])
                    if not match:
                        logging.error("Missing cost basis in '%s'",
                                      row['DESCRIPTION'])
                        continue
                    cost_number = D(match.group(1))
                    cost = position.Cost(cost_number, self.currency, None,
                                         None)
                    price = amount.Amount(rate, self.currency)
                    account_gains = self.account_gains.format(instrument)
                    txn = data.Transaction(
                        meta, date, self.FLAG, None, desc, data.EMPTY_SET,
                        {link}, [
                            data.Posting(self.account_cash, units, None, None,
                                         None, None),
                            data.Posting(self.account_fees, fees, None, None,
                                         None, None),
                            data.Posting(account_inst, units_inst, cost, price,
                                         None, None),
                            data.Posting(account_gains, None, None, None, None,
                                         None),
                        ])

            else:
                logging.error("Unknown row type: %s; skipping", rtype)
                continue

            entries.append(txn)

        # Insert a final balance check.
        if index:
            entries.append(
                data.Balance(meta, date + datetime.timedelta(days=1),
                             self.account_cash,
                             amount.Amount(D(row['BALANCE']),
                                           self.currency), None, None))

        return entries